下载地址
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 种表达式类型、随机组管理器和外部变量源完成递归替换。
表达式采用统一的 <key:value> 格式,key 决定求值方式,value 是参与运算的参数。整个系统由 17 种表达式类型组成,覆盖了从简单随机到脚本计算的完整需求链。
修饰符总览
下面按类别完整列出全部修饰符(i 与 r 共用整数随机实现,故 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控制。uUUID ——<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.0 和 5 作为数字相等,作为字符串则不等,比较前先判断类型才不会出错。
多分支场景用 <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 类型的物品配置支持以下字段,所有文本类字段均支持表达式:
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。
内置怪物变量
从怪物掉落的物品可以引用以下内置变量:
货币_拾取_随机:
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 定义了版本比较和实例化逻辑。每个需要版本适配的工具类(NbtUtil、ItemUtil、ComponentUtil、MessageUtil)都继承自 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:品质>基数>> 这种嵌套写法初看复杂,但它让每个表达式都是自包含的——不需要声明中间变量,不需要定义求值顺序。系统内部通过 StrSubstitutor 的 setEnableSubstitutionInVariables(true) 实现递归替换。
锁定值持久化到 NBT。这是自动更新能保留"个性"的关键。代价是每个物品多占一些 NBT 空间,但换来的是配置热更新时玩家无感。
脚本引擎作为逃生舱。17 种内置表达式覆盖了绝大部分需求,剩下的通过 JavaScript 兜底。这比设计一套图灵完备的表达式语言更务实——管理员可以用熟悉的 JS 语法处理复杂逻辑,而不需要学习一门新 DSL。
Components 与 NBT 并存。高版本 Minecraft 的 Data Component 体系并没有完全取代 NBT,两者在物品生成流程中按顺序应用:先设置 Components,再写入 NBT 和锁定值。这保证了向后兼容——低版本服务器忽略 Components 节点,高版本服务器两者都生效。

参与讨论
(Participate in the discussion)
参与讨论