Skip to content

装配系统 (Assembly / 结构化展示)

一件物品可能同时被锻造系统、宝石系统、烹饪系统等多个模块影响,每个模块都想往物品的名称和 Lore 上加点东西。装配系统就是用来协调这件事的——每个模块各自提交自己的"展示层"(Layer),系统按优先级把它们合并渲染成最终的物品外观。

这样做的好处是模块之间完全解耦:锻造系统不需要知道宝石系统的存在,它们各管各的展示层,最终由装配系统统一拼装。

核心概念

命名空间 (Namespace)

命名空间标识一个展示层的来源模块。每个命名空间有一个唯一 ID 和一个权重值,权重决定了 Lore 段落的排列顺序——数值越小越靠前。

层快照 (Layer Snapshot)

层快照是某个命名空间在某一时刻对物品展示的完整描述,包含名称贡献(前缀/后缀)和 Lore 段落。

装配请求 (Assembly Request)

装配请求把多个层快照打包在一起,交给装配服务合并渲染成最终的 ItemStack。

预注册命名空间

CoreLib 预注册了几个常用的命名空间,其他模块可以直接拿来用:

命名空间 ID权重用途
forge100锻造 / 强化基础属性
strengthen200强化附加属性
gem300宝石镶嵌
cooking10000烹饪系统(权重最大,Lore 排在最后)

提示

权重值之间故意留了间隔,方便你在中间插入自定义命名空间。比如权重 150 会排在 forgestrengthen 之间,250 会排在 strengthengem 之间。

EmakiItemLayerSnapshot

EmakiItemLayerSnapshot 是一个 record 类,描述单个命名空间对物品展示的贡献:

字段类型说明
namespaceIdString命名空间 ID
schemaVersionint数据结构版本号,用于后续数据迁移
auditAuditInfo审计信息(创建时间、修改时间、来源)
statsMap<String, Object>统计数据(如强化等级、宝石数量等)
structuredPresentationEmakiStructuredPresentation结构化展示数据
java
EmakiItemLayerSnapshot snapshot = new EmakiItemLayerSnapshot(
    "forge",                          // namespaceId
    1,                                // schemaVersion
    AuditInfo.now("emaki-forge"),     // audit
    Map.of("level", 5),              // stats
    presentation                      // structuredPresentation
);

EmakiStructuredPresentation

EmakiStructuredPresentation 定义了一个层对物品名称和 Lore 的具体贡献:

字段类型说明
baseNamePolicyBaseNamePolicy基础名称策略
baseNameTemplateString名称模板(仅 EXPLICIT_TEMPLATE 策略时使用)
nameContributionsList<NameContribution>名称贡献列表(前缀/后缀)
loreSectionsList<LoreSection>Lore 段落列表
java
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_TEMPLATEbaseNameTemplate 指定一个自定义模板
java
// 使用物品原始名称(最常见的情况)
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. 注册命名空间

java
NamespaceRegistry registry = EmakiServiceRegistry.get(NamespaceRegistry.class);

// 注册自定义命名空间(如果预注册的不够用)
registry.register("enchant_plus", 250); // 权重 250,排在 strengthen 和 gem 之间

2. 构建层快照

java
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. 创建装配请求

java
AssemblyService assemblyService = EmakiServiceRegistry.get(AssemblyService.class);

AssemblyRequest request = AssemblyRequest.builder()
    .baseSource("craftengine-iron_longsword")
    .layer(snapshot)
    .layer(forgeSnapshot)       // 可以叠加多个层
    .layer(gemSnapshot)
    .build();

4. 预览或给予物品

java
// 预览 — 只生成 ItemStack,不动玩家背包
ItemStack preview = assemblyService.preview(request);

// 给予 — 生成并放入玩家背包
assemblyService.give(request, player);

渲染结果示例

假设一把剑有 forgegemcooking 三个层,最终渲染出来是这样的:

物品名称: [+5] 铁长剑 (锻造)

Lore:
  ───── 锻造属性 ─────        ← forge (权重 100)
  攻击力:+15
  暴击率:+5%
  ─────────────────

  ◆ 宝石镶嵌                  ← gem (权重 300)
    [红宝石] 攻击力 +3
    [蓝宝石] 魔法值 +10

  ───── 烹饪效果 ─────        ← cooking (权重 10000)
  食用后恢复 5 点饥饿值
  ─────────────────

Lore 段落的顺序完全由权重决定,跟你添加层的顺序无关。

PDC 数据结构

装配系统会把层快照数据持久化到物品的 PersistentDataContainer 中,键格式为:

emaki:assembly.<namespaceId>

数据以 YAML 序列化存储,内部结构大致如下:

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 中,不受缓存影响。服务器重启后缓存会清空,但物品数据不会丢失。

Released under the GPL-3.0 License