装配系统 (Assembly / 结构化展示)
一件物品可能同时被锻造系统、宝石系统、烹饪系统等多个模块影响,每个模块都想往物品的名称和 Lore 上加点东西。装配系统就是用来协调这件事的——每个模块各自提交自己的"展示层"(Layer),系统按优先级把它们合并渲染成最终的物品外观。
这样做的好处是模块之间完全解耦:锻造系统不需要知道宝石系统的存在,它们各管各的展示层,最终由装配系统统一拼装。
核心概念
命名空间 (Namespace)
命名空间标识一个展示层的来源模块。每个命名空间有一个唯一 ID 和一个权重值,权重决定了 Lore 段落的排列顺序——数值越小越靠前。
层快照 (Layer Snapshot)
层快照是某个命名空间在某一时刻对物品展示的完整描述,包含名称贡献(前缀/后缀)和 Lore 段落。
装配请求 (Assembly Request)
装配请求把多个层快照打包在一起,交给装配服务合并渲染成最终的 ItemStack。
预注册命名空间
CoreLib 预注册了几个常用的命名空间,其他模块可以直接拿来用:
| 命名空间 ID | 权重 | 用途 |
|---|---|---|
forge | 100 | 锻造 / 强化基础属性 |
strengthen | 200 | 强化附加属性 |
gem | 300 | 宝石镶嵌 |
cooking | 10000 | 烹饪系统(权重最大,Lore 排在最后) |
提示
权重值之间故意留了间隔,方便你在中间插入自定义命名空间。比如权重 150 会排在 forge 和 strengthen 之间,250 会排在 strengthen 和 gem 之间。
EmakiItemLayerSnapshot
EmakiItemLayerSnapshot 是一个 record 类,描述单个命名空间对物品展示的贡献:
| 字段 | 类型 | 说明 |
|---|---|---|
namespaceId | String | 命名空间 ID |
schemaVersion | int | 数据结构版本号,用于后续数据迁移 |
audit | AuditInfo | 审计信息(创建时间、修改时间、来源) |
stats | Map<String, Object> | 统计数据(如强化等级、宝石数量等) |
structuredPresentation | EmakiStructuredPresentation | 结构化展示数据 |
EmakiItemLayerSnapshot snapshot = new EmakiItemLayerSnapshot(
"forge", // namespaceId
1, // schemaVersion
AuditInfo.now("emaki-forge"), // audit
Map.of("level", 5), // stats
presentation // structuredPresentation
);EmakiStructuredPresentation
EmakiStructuredPresentation 定义了一个层对物品名称和 Lore 的具体贡献:
| 字段 | 类型 | 说明 |
|---|---|---|
baseNamePolicy | BaseNamePolicy | 基础名称策略 |
baseNameTemplate | String | 名称模板(仅 EXPLICIT_TEMPLATE 策略时使用) |
nameContributions | List<NameContribution> | 名称贡献列表(前缀/后缀) |
loreSections | List<LoreSection> | Lore 段落列表 |
EmakiStructuredPresentation presentation = EmakiStructuredPresentation.builder()
.baseNamePolicy(BaseNamePolicy.SOURCE_EFFECTIVE_NAME)
.nameContribution(NameContribution.prefix("<red>[+5] "))
.nameContribution(NameContribution.suffix(" <gray>(锻造)"))
.loreSection(LoreSection.of(
"<gray>───── 锻造属性 ─────",
"<white>攻击力:<red>+15",
"<white>暴击率:<gold>+5%",
"<gray>─────────────────"
))
.build();BaseNamePolicy 枚举
物品的基础名称有三种来源可选:
| 枚举值 | 说明 |
|---|---|
SOURCE_EFFECTIVE_NAME | 使用物品来源的实际显示名称(默认选项) |
SOURCE_TRANSLATABLE | 使用物品的可翻译名称(走客户端本地化) |
EXPLICIT_TEMPLATE | 用 baseNameTemplate 指定一个自定义模板 |
// 使用物品原始名称(最常见的情况)
EmakiStructuredPresentation.builder()
.baseNamePolicy(BaseNamePolicy.SOURCE_EFFECTIVE_NAME)
.build();
// 使用可翻译名称,让客户端根据语言设置显示
EmakiStructuredPresentation.builder()
.baseNamePolicy(BaseNamePolicy.SOURCE_TRANSLATABLE)
.build();
// 完全自定义名称
EmakiStructuredPresentation.builder()
.baseNamePolicy(BaseNamePolicy.EXPLICIT_TEMPLATE)
.baseNameTemplate("<gradient:gold:red>传说之剑</gradient>")
.build();使用流程
下面是其他模块使用装配系统的典型步骤:
1. 注册命名空间
NamespaceRegistry registry = EmakiServiceRegistry.get(NamespaceRegistry.class);
// 注册自定义命名空间(如果预注册的不够用)
registry.register("enchant_plus", 250); // 权重 250,排在 strengthen 和 gem 之间2. 构建层快照
EmakiStructuredPresentation presentation = EmakiStructuredPresentation.builder()
.baseNamePolicy(BaseNamePolicy.SOURCE_EFFECTIVE_NAME)
.nameContribution(NameContribution.prefix("<light_purple>[附魔+] "))
.loreSection(LoreSection.of(
"",
"<light_purple>✦ 附魔增强",
"<white> 火焰附加 III",
"<white> 锋利 V",
""
))
.build();
EmakiItemLayerSnapshot snapshot = new EmakiItemLayerSnapshot(
"enchant_plus",
1,
AuditInfo.now("my-plugin"),
Map.of("enchant_count", 2),
presentation
);3. 创建装配请求
AssemblyService assemblyService = EmakiServiceRegistry.get(AssemblyService.class);
AssemblyRequest request = AssemblyRequest.builder()
.baseSource("craftengine-iron_longsword")
.layer(snapshot)
.layer(forgeSnapshot) // 可以叠加多个层
.layer(gemSnapshot)
.build();4. 预览或给予物品
// 预览 — 只生成 ItemStack,不动玩家背包
ItemStack preview = assemblyService.preview(request);
// 给予 — 生成并放入玩家背包
assemblyService.give(request, player);渲染结果示例
假设一把剑有 forge、gem、cooking 三个层,最终渲染出来是这样的:
物品名称: [+5] 铁长剑 (锻造)
Lore:
───── 锻造属性 ───── ← forge (权重 100)
攻击力:+15
暴击率:+5%
─────────────────
◆ 宝石镶嵌 ← gem (权重 300)
[红宝石] 攻击力 +3
[蓝宝石] 魔法值 +10
───── 烹饪效果 ───── ← cooking (权重 10000)
食用后恢复 5 点饥饿值
─────────────────Lore 段落的顺序完全由权重决定,跟你添加层的顺序无关。
PDC 数据结构
装配系统会把层快照数据持久化到物品的 PersistentDataContainer 中,键格式为:
emaki:assembly.<namespaceId>数据以 YAML 序列化存储,内部结构大致如下:
# PDC 内部存储格式(不可直接编辑)
emaki:assembly.forge:
schema_version: 1
audit:
created_at: 1700000000000
modified_at: 1700000000000
source: "emaki-forge"
stats:
level: 5
presentation:
base_name_policy: "SOURCE_EFFECTIVE_NAME"
name_contributions:
- type: PREFIX
value: "<red>[+5] "
- type: SUFFIX
value: " <gray>(锻造)"
lore_sections:
- lines:
- "<gray>───── 锻造属性 ─────"
- "<white>攻击力:<red>+15"
- "<white>暴击率:<gold>+5%"
- "<gray>─────────────────"缓存
装配服务内置了渲染缓存,避免对相同输入重复渲染:
| 参数 | 值 |
|---|---|
| 最大条目数 | 128 |
| TTL(存活时间) | 30 秒 |
| 淘汰策略 | LRU(最近最少使用) |
缓存键基于所有层快照的哈希值计算,任何层发生变化时缓存自动失效。30 秒的 TTL 在高频操作场景下能明显减少渲染开销,同时也不会让数据过于陈旧。
注意
缓存只针对渲染结果。层快照数据始终持久化在物品的 PDC 中,不受缓存影响。服务器重启后缓存会清空,但物品数据不会丢失。