下载地址

https://github.com/Saukiya/SX-Item/releases

Minecraft 服务器的物品系统有一个核心矛盾:配置文件是静态的,但玩家期望每次获得的物品都有差异。传统做法是为每种变体写一份独立配置,或者在代码里硬编码随机逻辑。前者配置膨胀不可维护,后者对服务器管理员不友好——他们不写 Java,只写 YAML。

SX-Item 的解决思路是在 YAML 配置中嵌入一套表达式语言。物品的名称、描述、数值、附魔、NBT 乃至高版本的 Components 都可以包含形如 <key:value> 的表达式节点,在物品生成时实时求值。这样一份配置就能产出成百上千种不同的物品实例。

项目从 SX-Attribute 2.0(2018 年)演化而来,经历了 3.0 的物品功能分离和重写,目前支持 Minecraft 1.8.8 到 26.2 的全部主流版本,并兼容 Folia 调度器。

表达式系统的设计

下图展示了表达式系统的整体求值架构,ExpressionHandler 作为中心处理器,协调 17 种表达式类型、随机组管理器和外部变量源完成递归替换。

drawing-1783400338601

表达式采用统一的 <key:value> 格式,key 决定求值方式,value 是参与运算的参数。整个系统由 17 种表达式类型组成,覆盖了从简单随机到脚本计算的完整需求链。

修饰符总览

下面按类别完整列出全部修饰符(ir 共用整数随机实现,故 17 种类型对应 18 个 key)。后续小节再展开每一类的细节,这里先给一份速查。

取值类

  • s 普通随机 —— <s:随机组><s:A:B:C>,如 <s:品质>。每次引用都重新取值;带 : 时从内联候选中随机抽取。

  • l 锁定随机 —— <l:随机组><l:键#A:B>,如 <l:品质>。首次求值后缓存并写入 NBT,物品自动更新时保留原始结果。

  • i / r 整数随机 —— <i:min_max>,如 <i:1_100>。闭区间取整数,支持负数,不分大小顺序。

  • d 小数随机 —— <d:min_max>,如 <d:0.6_0.9>。闭区间取浮点数,精度由全局 DecimalPrecision 控制。

  • c 计算器 —— <c:表达式><c:int 表达式>,如 <c:int 20 * <d:0.8_1.2>>。压栈四则运算,可嵌套其他表达式;int 前缀结果取整。

  • max / min 取极值 —— <max:A:B:C><min:A:B:C>。在给定数值中取最大或最小。

判断与分支类

  • b 整行判断 —— <b:匹配名:词1:词2>,如 <b:<l:品质>:史诗>。匹配名命中任一词则保留整行,否则整行被删除。

  • if 三元条件 —— <if:条件?真值:假值>,如 <if:<cmp:5 gt 3>?&a是:&c否>。条件属于 true/1/yes/ok 返回真值,否则返回假值;真值不能含裸露的 :

  • when 多分支选择 —— <when:待匹配$分支列表>,如 <when:<l:品质>$史诗:&c史诗|default:&7普通>$ 分隔待匹配值与分支,| 分隔各分支,default/else 兜底。

  • eq 精确相等 —— <eq:A==B>,如 <eq:<l:职业>==战士>。相等返回 true,否则 false;缺 == 返回 false

  • like 模糊包含 —— <like:A==B>,如 <like:钢铁长剑==长剑>。一方包含另一方即返回 true

  • cmp 比较大小 —— <cmp:A 算子 B>,如 <cmp:10 gt 9>。算子 eq/ne/gt/lt/ge/le;两侧可解析为数字时按数值比较,否则退化为字符串比较。

特殊文本类

  • t 时间格式 —— <t:1Y1M1D1h1m1s><t:秒数>,如 <t:10m>。从当前时刻推算时间戳,格式由全局 TimeFormat 控制。

  • u UUID —— <u:random><u:字符串>random 生成随机 UUID,其余按字符串确定性生成。

脚本与占位

  • j 脚本调用 —— <j:文件.方法#参1,参2>,如 <j:Default.itemScript#A,B>。调用脚本引擎方法,可返回字符串或多行列表。

  • null 空占位 —— <null:任意内容>。恒返回空串,用于占位或临时注释掉一段变量。

判断类都返回 true/false 或直接返回文本,因此可以互相嵌套:<if:> 的条件常由 <cmp:> <eq:> 提供,<when:> 的待匹配值可以是 <l:> 的锁定结果。

基础随机

最常用的三种随机表达式处理数值生成:

# 整数随机:1 到 25565 之间取一个整数
<i:1_25565>

# 小数随机:0.6 到 0.9 之间取一个浮点数
<d:0.6_0.9>

# 计算器:支持四则运算,可嵌套其他表达式
<c:20 * <s:品质基数>>
<c:int <r:300_350> * <s:品质基数>>

计算器表达式内部使用压栈计算器实现,支持嵌套求值。int 前缀表示结果取整,小数精度由全局配置 DecimalPrecision 控制(默认保留 2 位,四舍五入)。

另外还有 <max:><min:> 两种边界表达式,用于限制数值范围。

变量引用与锁定

物品生成过程中,同一个随机值可能需要在多处引用。系统提供两种引用方式:

# 锁定引用:首次求值后缓存,后续引用返回相同值
<l:品质>

# 普通引用:每次引用都重新从随机组中取值
<s:品质>

区别在于:锁定引用 <l:> 适合需要前后一致的场景(比如品质等级在名称和描述中都要出现),普通引用 <s:> 适合每处需要独立随机的场景。

锁定引用还支持内联候选值的简写形式,不需要预先定义随机组:

# 从 POTION、SPLASH_POTION、LINGERING_POTION 中随机选一个并锁定
<l:ID#POTION:SPLASH_POTION:LINGERING_POTION>

# 等价于先定义随机组再引用,但更紧凑
<s:AAA:BBB:CCC>

锁定引用的值会被写入物品的 NBT 标签(路径 SX-Item.Lock.*),这意味着物品自动更新时能保留原始随机结果——玩家手中的物品不会因为服务器重载而变样。

整行条件判断

物品描述中经常需要"某些品质才显示某行"的逻辑。整行判断表达式解决这个问题:

# 当品质匹配"优秀"或"史诗"时,整行才会出现在最终结果中
<b:<l:品质>:优秀:史诗>&6限制职业: <l:职业>

<b:匹配名:匹配词1:匹配词2> 的逻辑是:如果匹配名的值在后续列出的匹配词中,保留这一行;否则整行被移除(内部通过 <DeleteLore> 标记实现)。这比在代码中写 if-else 分支简洁得多。

条件返回与比较

整行判断只能决定一行的去留,不能根据条件返回不同的文本。品质是史诗就显示红色标题、否则显示灰色,这类需求 <b:> 做不到。因此系统补充了一组条件与比较表达式,把判断从行级过滤扩展到值级替换。

<if:条件?真值:假值> 是三元选择,条件为 true 时返回真值,否则返回假值:

<if:<cmp:5 gt 3>?&a是:&c否>

比较交给 <cmp:A 算子 B>,算子用关键字而不是符号——因为 <> 是表达式的起止定界符,不能出现在参数里。算子有 eq lt gt le ge ne:

<cmp:10 gt 9>     # 10 大于 9,返回 true
<cmp:5.0 eq 5>    # 数值相等,返回 true

这一步的关键是类型感知:两侧都能解析为数字时按数值比较,否则退化为字符串比较。5.05 作为数字相等,作为字符串则不等,比较前先判断类型才不会出错。

多分支场景用 <when:待匹配$分支列表>,避免嵌套一堆 <if:>

<when:<l:品质>$史诗:&c史诗|传说:&6传说|default:&7普通>

$ 分隔待匹配值和分支列表,| 分隔各分支,命中哪个分支就返回对应值,都不命中则走 default。

剩下两个是判断类的补充:<eq:A==B> 做精确相等,<like:A==B> 做模糊包含(一方包含另一方即为真)。还有一个 <null:> 空占位,恒返回空串,用来临时注释掉一段变量。

这组表达式都返回 true / false 或直接返回文本,因此可以互相嵌套:<if:> 的条件常由 <cmp:> <eq:> 提供,<when:> 的待匹配值可以是 <l:> 的锁定结果。判断逻辑从此不必写进脚本。

时间与 UUID

两种特殊用途的表达式:

# 时间格式:生成一个从当前时刻起算的时间戳
<t:1Y1M1D1h1m1s>   # 1年1月1天1小时1分1秒后
<t:10m>            # 10分钟后

# UUID:生成唯一标识
<u:random>         # 完全随机的 UUID
<u:某个字符串>      # 基于字符串确定性生成 UUID

时间格式化样式由全局配置 TimeFormat 控制,默认为 yyyy/MM/dd HH:mm

脚本表达式

当内置表达式不够用时,可以调用脚本引擎(默认 JavaScript,也支持 Python):

<j:Default.itemScript#QAQ,QWQ>

格式为 <j:文件名.方法名#参数>,参数之间用逗号分隔。脚本文件放在插件的 Scripts 目录下,方法签名固定接收 handler(ExpressionHandler 上下文)和 args(参数数组)。

脚本可以返回字符串,也可以返回列表(用于多行输出):

function itemScript(handler, args) {
    if (handler.getPlayer() != null) {
        handler.getPlayer().sendMessage("参数: " + args[0])
    }
    return args[SXItem.getRandom().nextInt(args.length)];
}

脚本引擎通过 ScriptEngine 配置项选择,设为 ~ 可完全禁用。高版本 Minecraft 不再内置 Nashorn,插件通过 libraries 机制自动加载 org.openjdk.nashorn:nashorn-core:15.4

PlaceholderAPI 集成

所有表达式天然支持 PlaceholderAPI 变量。在表达式求值前,%placeholder% 格式的变量会被优先替换:

# 结合 PAPI 变量和表达式
<c:int %player_level% * <d:0.1_0.5>>

这意味着物品属性可以动态关联玩家状态——等级越高,随机范围越大。

随机组:权重池与多行输出

表达式的值从哪来?答案是随机组。随机组是一组带权重的候选值,每次引用时按权重抽取。

品质:
  - 2: "普通"
  - 1: "优秀"

数字是权重。上面的配置意味着"普通"被抽中的概率是 2/3,"优秀"是 1/3。不需要写概率百分比,权重系统更直观也更容易调整——加一条新品质不需要重新计算所有概率。

随机组还支持多行输出,用于生成附魔列表这类需要返回多个值的场景:

射手附魔:
  - - ARROW_DAMAGE:<r:0_2>
    - ARROW_INFINITE:<r:0_1>
  - - ARROW_DAMAGE:<r:0_2>
    - ARROW_FIRE:<r:0_2>

每个顶层条目是一组附魔方案,抽中哪组就应用哪组的全部附魔。

全局与局部的优先级

随机组分两层:全局组放在 RandomString 文件夹中,所有物品共享;局部组写在物品配置的 Random 节点下,仅当前物品可用。

关键规则:局部覆盖全局。如果物品配置中定义了与全局同名的随机组,局部值优先。这个设计让全局组充当默认值,局部组充当特化覆盖,不需要额外的继承语法。

Lore:
  - '&7覆盖全局变量结果-> <s:DefaultPrefix>'
  - '&7全局和局部混合结果-> <s:DefaultLore>'
Random:
  DefaultPrefix: "Prefix"
  DefaultLore:
    - 2: "Lore"
    - 1: ~    # ~ 表示回退到全局值

当局部随机组的某个候选值为 ~(YAML 的 null),系统会回退到全局随机组取值。这让局部组可以"部分覆盖"——大部分时候用自己的值,偶尔回退到全局池。

物品配置的完整结构

一份 Default 类型的物品配置支持以下字段,所有文本类字段均支持表达式:

字段

说明

Name

物品显示名称

ID

材质,支持数字 ID 和英文 ID,支持随机

Durability

耐久度,支持绝对值、反算(<20)、百分比(20%

Amount

数量

Lore

描述列表,支持 /n 换行

EnchantList

附魔列表,格式 附魔名:等级

ItemFlagList

隐藏选项(隐藏附魔、属性等)

Color

皮革染色,十六进制颜色码

Attributes

原版属性修饰符

CustomModelData

自定义模型数据(1.14+,支持字符串随机)

Potion

药水效果(1.11.1+)

NBT

自定义 NBT 标签

Components

堆叠组件(1.21.1+)

ClearAttribute

清除默认属性

ProtectNBT

更新时保护的 NBT 路径

Update

是否启用自动更新

Random

局部随机组

NBT 与类型转换

NBT 节点支持嵌套 Map 结构,并且可以通过前缀标记强制类型转换:

NBT:
  StringConvert:
    byte: '[byte]<s:-128:127>'
    short: '[short]<s:-32768:32767>'
    int: '[int]<i:1_100>'
    long: '[long]<s:-9223372036854775808:9223372036854775807>'
    float: '[float]<d:-2333_2333>'
    double: '[double]<d:-2333_2333>'

[type] 前缀告诉系统将表达式结果从字符串转为对应的数值类型再写入 NBT。这解决了 Minecraft NBT 对数据类型敏感的问题——一个 int 和一个 string 的 "1" 在 NBT 层面完全不同。

高版本特性:Components 系统(1.21.1+)

Minecraft 1.20.5 引入了 Data Component 体系,取代了部分传统 NBT 的职能。SX-Item 从 1.21.1 开始完整支持这套新系统。

Components 节点的写法和 NBT 类似,但 key 使用 minecraft: 命名空间前缀:

Component-1:
  ID: DIAMOND
  Name: "组件教程"
  Components:
    minecraft:item_name: '默认名称(无法被铁砧修改)'
    minecraft:custom_name: '带稀有度颜色的名称(可铁砧修改)§c红色'
    minecraft:rarity: 'epic'
    minecraft:max_stack_size: 99
    minecraft:food:
      eat_seconds: '[float]<d:0.1_5>'
      nutrition: '[int]<i:1_5>'
      saturation: '[float]<d:1_5>'
      can_always_eat: true
    minecraft:lore:
      - '{"text":"§aHello World lores"}'
      - '{"text":"§b第二行描述"}'
    minecraft:tool:
      rules:
        - blocks:
            - minecraft:stone
          correct_for_drops: true

Components 同样支持表达式和类型转换。这意味着食物的营养值、工具的挖掘规则、物品的最大堆叠数都可以随机化。

在实现层面,插件通过 ComponentUtil 抽象类封装了 NMS 层的 DataComponentMap 操作,提供 Java 对象与 JSON 之间的双向转换。玩家可以通过 /si component 命令实时查看和修改手持物品的组件数据:

/si component                              # 查看手持物品的全部组件
/si component set minecraft:max_damage 233  # 设置指定组件
/si component remove minecraft:max_damage   # 移除指定组件

原版属性修饰符(Attributes)

高版本的属性系统也得到了完整支持,格式为 属性名:数值:模式:生效位置

Attributes:
  - "GENERIC_ATTACK_SPEED:-0.7:1:HAND"
  - "GENERIC_MAX_HEALTH:10:0:CHEST"

模式对应原版的三种运算方式:0 为加法、1 为倍率增量、2 为最终倍乘。生效位置可选 HAND、OFF_HAND、HEAD、CHEST、LEGS、FEET,不填则全部位置生效。

自动更新机制

下图展示了物品从生成到自动更新的完整流程,包括 HashCode 比对和 Lock 值保留机制。

![[物品生成流程.flowchart.excalidraw]]

物品一旦发到玩家手中,配置文件的修改如何同步到已有物品?SX-Item 通过 Update: true 标记和 HashCode 比对实现了无感更新。

工作原理:每个物品生成时,配置文件的内容哈希会被写入物品 NBT 的 SX-Item.HashCode 字段。当玩家切换手持物品、打开背包或加入服务器时,系统比对当前配置哈希与物品上的哈希——不一致则触发更新。

更新过程中,锁定引用 <l:> 的值从旧物品的 SX-Item.Lock.* 路径读取并注入新的 ExpressionHandler,因此玩家物品的"个性"(品质、职业等首次随机的结果)得以保留,只有配置变更的部分(描述文案、数值公式等)会刷新。

NBT 保护

自动更新默认会重写物品的全部 NBT。对于需要保留的自定义数据(比如附魔、其他插件写入的标签),通过 ProtectNBT 列表声明保护:

# 全局保护(Config.yml)
ProtectNBT:
  - "protect.data"

# 物品级保护
ProtectNBT:
  - "Enchantments"      # 保护附魔不被更新覆盖
  - "!protect.data"     # 用 ! 前缀解除全局保护
  - "components.minecraft:damage" # 保护原版顶级组件(需1.20.5+)

全局保护和物品级保护合并生效,! 前缀可以在物品级别排除全局规则。

MythicMobs 集成

SX-Item 同时兼容 MythicMobs V4 和 V5,通过反射检测 API 包名自动选择对应的监听器。

掉落配置(SX-Drop)

在 MythicMobs 的怪物配置中直接声明 SX-Item 物品掉落:

废弃村镇_蜘蛛女王:
  Type: SPIDER
  Display: '&c蜘蛛女王'
  Health: 200
  SX-Drop:
    - 材料_水元素 1-6 1
    - 货币_拾取_随机 1 1 F:1 T:10

格式为 物品名 数量 概率 [局部变量...]。数量支持范围写法(1-6),概率为 0~1 的小数。后续的 Key:Value 对会作为局部变量注入物品的 ExpressionHandler,覆盖物品配置中的 Random 默认值。

这意味着同一个物品模板可以根据掉落来源产出不同的实例——10 金币和 100 金币可以共用一份"货币"配置,只是传入不同的范围参数。

装备配置(SX-Equipment)

怪物生成时自动穿戴 SX-Item 物品:

SX-Equipment:
  - 武器_火焰剑:HAND 0.5
  - 盔甲_铁胸甲:CHEST 1

格式为 物品名:位置 概率,位置支持 HAND、OFF_HAND、HEAD、CHEST、LEGS、FEET。

内置怪物变量

从怪物掉落的物品可以引用以下内置变量:

变量名

说明

mob_level

怪物等级

mob_name_display

怪物显示名

mob_name_internal

怪物内部 ID

mob_uuid

怪物 UUID

货币_拾取_随机:
  Lore:
    - '来源: <s:mob_name_display>'

全局脚本事件系统

脚本不仅用于表达式求值,还可以注册 Bukkit 事件监听器。Scripts/Global/ 目录下的脚本在插件加载时自动执行,适合注册全局逻辑:

// Event.js 提供的事件注册工具
registerNormalEvent(
  "org.bukkit.event.player.PlayerItemHeldEvent",
  function (event) {
    let player = event.getPlayer();
    SXItem.getInst().getLogger().info("切换物品: " + player.getName())
  }
);

registerNormalEvent 接收事件类全名和回调函数,内部通过 Java.extend(EventExecutor) 桥接 Java 事件系统。这让服务器管理员可以在不编写 Java 插件的情况下扩展物品行为——比如监听玩家切换物品时触发特效,或者在物品使用时执行自定义逻辑。

Global.js 文件用于声明全局变量(如 ArrayList = Java.type("java.util.ArrayList")),供其他脚本文件引用。

版本兼容架构

SX-Item 支持从 1.8.8 到 1.21.11 的 20 余个 Minecraft 版本。这不是通过条件分支堆砌实现的,而是通过一套 NMS 抽象层。

核心接口 NMS 定义了版本比较和实例化逻辑。每个需要版本适配的工具类(NbtUtilItemUtilComponentUtilMessageUtil)都继承自 NMS,并在各自的 getInst() 方法中声明支持的版本列表:

// ComponentUtil 的版本声明——从高到低匹配
private final static ComponentUtil inst = NMS.getInst(
    ComponentUtil.class, "v1_21_R7", "v1_21_R6", "v1_21_R1", "v1_20_R4", "v1_8_R3"
);

系统启动时检测当前服务器版本,从列表中选择第一个 <= 当前版本的实现类加载。如果当前版本恰好有精确匹配的实现(如 ComponentUtil_v1_21_R7),优先使用;否则回退到最近的兼容版本。

对于 Paper 服务端在 1.20.5 之后移除 CraftBukkit 包重定位的问题,系统维护了一张版本映射表(如 1.21.11 → v1_21_R7),通过 Bukkit.getBukkitVersion() 解析实际版本号。

插件还声明了 folia-supported: true,对 Folia 调度器做了兼容处理——当 runTaskTimer 抛出异常时回退到同步检查模式。

开发者 API

获取与生成物品

// 获取物品实例,传入玩家和局部变量
ItemStack item = SXItem.getItemManager().getItem("item_name", player, "key", "value");

// 通过 Map 传递局部变量
Map<String, String> vars = new HashMap<>();
vars.put("F", "1");
vars.put("T", "10");
ItemStack item = SXItem.getItemManager().getItem("货币_拾取_随机", player, vars);

加载外部物品配置

// 从自定义目录加载物品,适合多服同步场景
SXItem.getItemManager().loadItem("my-group", new File("path/to/items"));

加载的物品不会被 SX-Item 重载时清除,每次调用会清空该 group 的上次存储。不同 group 之间的 key 不可重复。

事件监听

// 物品生成事件——可修改最终物品
@EventHandler
void onSpawn(SXItemSpawnEvent event) {
    IGenerator ig = event.getIg();
    ItemStack item = event.getItem();
    // 修改 item...
    event.setItem(item);
}

// 物品更新事件——可取消更新
@EventHandler
void onUpdate(SXItemUpdateEvent event) {
    if (shouldCancel(event)) {
        event.setCancelled(true);
    }
}

注册自定义物品类型

// 注册新的物品生成器类型
ItemManager.register("MyType", 
    (key, config, group) -> new MyGenerator(key, config, group),
    (item, config) -> { /* 保存逻辑 */ }
);

注册自定义表达式

// 注册新的表达式类型,使用字符 key
ExpressionManager.register(new MyExpression(), 'x');
// 之后可在配置中使用 <x:参数>

设计取舍

回顾整个系统,有几个值得注意的设计决策:

表达式嵌套而非管道<c:20 * <s:<l:品质>基数>> 这种嵌套写法初看复杂,但它让每个表达式都是自包含的——不需要声明中间变量,不需要定义求值顺序。系统内部通过 StrSubstitutorsetEnableSubstitutionInVariables(true) 实现递归替换。

锁定值持久化到 NBT。这是自动更新能保留"个性"的关键。代价是每个物品多占一些 NBT 空间,但换来的是配置热更新时玩家无感。

脚本引擎作为逃生舱。17 种内置表达式覆盖了绝大部分需求,剩下的通过 JavaScript 兜底。这比设计一套图灵完备的表达式语言更务实——管理员可以用熟悉的 JS 语法处理复杂逻辑,而不需要学习一门新 DSL。

Components 与 NBT 并存。高版本 Minecraft 的 Data Component 体系并没有完全取代 NBT,两者在物品生成流程中按顺序应用:先设置 Components,再写入 NBT 和锁定值。这保证了向后兼容——低版本服务器忽略 Components 节点,高版本服务器两者都生效。