简体中文
JS 脚本扩展
自 2.8.5 起, EmakiItem 支持用 JavaScript 注册自定义物品效果类型. 入口: 把 .js 文件放进 plugins/EmakiItem/scripts/ 目录, 在脚本中调用 items.registerEffectType({...}). 注册成功的效果类型与内置效果类型同权: 物品定义中即可书写 effects: - type: <你的 id>
宿主机制, 目录加载顺序, 超时与全开放沙箱的行为规则与 CoreLib 完全一致, 这里不重复; 契约见 CoreLib JS 脚本扩展 的安全声明与线程模型章节
items 绑定 API
payload 字段
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
id | string | 是 | — | 效果类型 id, 规范化规则与内置 typeId 一致 (小写, 空格转下划线); 不得与内置效果类型重名, 被拒绝的内置 id: variables, ea_attribute, es_skill, accessory_slot, name_action, lore_action |
parse | function | 是 | — | parse(ctx): 物品每次构建时调用, 把原始效果条目转换为 effect 数据 |
clear | function | 否 | — | clear(effect, itemCtx): 在 apply 之前调用, 移除上次构建写入的数据 |
apply | function | 是 | — | apply(effect, itemCtx): 把效果写入正在构建的物品 |
parse (ctx)
| ctx 属性 | 说明 |
|---|---|
ctx.definitionId | 声明该效果的物品定义 id |
ctx.config | effects 条目的原始配置 (含 type 字段), 已深转换为可属性访问的 JS 对象 |
返回契约: 返回 null / undefined 表示跳过该条目 (与 ItemEffectType.parse 的约定一致); 其余返回值作为 effect 传给 clear / apply
clear (effect, itemCtx) / apply (effect, itemCtx)
| itemCtx 属性 | 说明 |
|---|---|
itemStack | 正在构建的物品 (Bukkit ItemStack 宿主对象, 可直接调用其方法修改) |
variables | 定义解析后的表达式变量 |
equipSlot | 槽位 |
注册与运行规则
- 注册期校验: payload 必须是对象;
id必填且为字符串, 不能是内置 id;parse,apply必须是函数;clear提供时必须是函数. 校验失败输出console.scripts.register_error(含 id 与原因), 该次注册跳过 - 运行期顺序: 物品每次构建时按
parse→clear→apply执行;clear未提供则跳过 - 异常隔离:
parse抛异常/超时时该条目按跳过处理并输出日志 (EmakiItem script effect '<id>' parse for item '<definitionId>' failed: ...);clear/apply的异常由包装器捕获并输出日志, 同时由EmakiItemEffectApplier既有的异常隔离作为后备, 单条效果失败不影响物品构建的其余部分 - 线程: 回调运行在物品构建线程, 并经宿主锁串行化
配置与重载
config.yml 中的 scripts 节:
yaml
scripts:
# 是否启用脚本扩展. 关闭后不扫描不加载脚本, 已注册的脚本效果随之注销
enabled: true
# 单次脚本求值或回调的超时 (毫秒). 超时中断该次调用, 超时后脚本宿主继续可用
# 非正数回退默认值 5000
timeout_ms: 5000
# 调试模式; 开启后输出更详细的脚本加载信息| 字段 | 类型 | 必填 | 默认值 | 取值范围 | 说明 |
|---|---|---|---|---|---|
scripts.enabled | boolean | 否 | true | true / false | 关闭后不扫描不加载脚本, 已注册的脚本效果类型随之注销 |
scripts.timeout_ms | integer | 否 | 5000 | 正整数 | 单次脚本求值或回调的超时毫秒数; 非正数回退 5000 |
重载规则: 脚本随 EmakiItem 既有 reload 流程重载 -- 先关闭本次加载的全部注册句柄 (注册 API 会同步清理效果类型原型缓存), 再重建 Context, 按文件顺序重新求值与注册. 重载后, 修改过的脚本立即生效, 已删除文件对应的效果类型不复存在; 仍引用已注销类型的物品定义按未知效果类型处理. GraalJS 运行库缺失时输出一条告警并跳过脚本加载, 其余功能不受影响
错误排查
| 控制台输出 | 原因 | 处置 |
|---|---|---|
console.scripts.engine_unavailable: "GraalJS 运行库不可用, 脚本扩展已禁用, 其余功能不受影响" | GraalJS 运行库缺失 | 确认发行版包含 GraalJS 运行库 |
console.scripts.disabled: "脚本扩展已在配置中关闭, 本次不加载脚本" | scripts.enabled: false | 需要时开启后 reload |
console.scripts.loaded: "脚本加载完成: %loaded% 个文件, %errors% 个错误" | 每次加载/重载的汇总 | 错误数异常时查看随后的 file_error / register_error |
console.scripts.file_error: "脚本加载失败: %file% -> %error%" | 单文件语法错误, 求值超时或运行时异常 | 按 %error% 修复该文件; 其余文件不受影响 |
console.scripts.register_error: "脚本效果类型注册失败: %id% -> %error%" | payload 不合法 (payload must be an effect type object, id is required, id must be a string, id is blank), 内置 id 冲突 (id '<id>' is a reserved built-in effect type), parse/apply 不是函数, clear 提供时不是函数, 注册被拒绝 | 按 %error% 修正 payload 或更换 id |
日志 EmakiItem script effect '<id>' parse for item '<definitionId>' failed: ... | parse 抛异常或超时 | 按 detail 修复; 该条目按跳过处理 |
日志 EmakiItem script effect '<id>' clear for item '<definitionId>' failed: ... | clear 抛异常或超时 | 按 detail 修复; 该次 clear 跳过 |
日志 EmakiItem script effect '<id>' apply for item '<definitionId>' failed: ... | apply 抛异常或超时 | 按 detail 修复; 该条效果未写入 |
安全声明
脚本环境默认全开放 (可访问任意 Java 类与服务器 API, 包括直接修改 ItemStack), 具备与服务器同级权限, 请只加载你自己审查过的脚本; 重载会重建 Context 并清空脚本顶层状态. 完整声明见 CoreLib JS 脚本扩展
示例脚本
示例文件由插件首次释放到 scripts/items/example_effect.js, 仅在该文件不存在时写入, 已存在的文件不被覆盖; 可直接修改或删除
js
// ============================================================================
// EmakiItem 脚本扩展示例: 自定义物品效果类型 (items 绑定)
//
// 这是演示脚本: 注册一个 id 为 script_example_glow 的物品效果类型,
// 物品定义中这样使用:
// effects:
// - type: script_example_glow
// level: 2
//
// parse / clear / apply 的完整契约请参考 EmakiItemApi 的 ItemEffectType /
// ItemEffect 接口文档, 或 docs 站 EmakiItem 模块的 scripts 页面
//
// 安全声明: 脚本环境默认全开放 (可访问任意 Java 类与服务器 API),
// 具备与服务器同级权限, 请只加载你自己审查过的脚本
//
// 释放与覆盖: 此文件仅在本文件不存在时首次释放, 已存在的文件不会被覆盖
// ============================================================================
// 演示效果写入 Lore 的行前缀, clear 按它识别并移除旧行
var LINE_PREFIX = "§7灵光 ";
items.registerEffectType({
// 效果类型 id (必填): 会被规范化为小写 + 空格转下划线;
// 不得与内置效果类型 (variables / ea_attribute / es_skill / accessory_slot /
// name_action / lore_action) 重名
id: "script_example_glow",
// parse (必填): 物品每次构建时调用, 把原始效果条目转换为 effect 数据
// ctx.definitionId: 声明该效果的物品定义 id
// ctx.config: effects 条目的原始配置 (含 type 字段), 可按属性访问
// 返回 null / undefined 表示跳过该条目 (本例: level 非法时跳过)
// 返回值会作为 effect 传给下面的 clear / apply
parse: function (ctx) {
var level = 1;
if (ctx.config && ctx.config.level != null) {
level = Number(ctx.config.level);
}
if (isNaN(level) || level <= 0) {
return null;
}
return { level: Math.min(Math.floor(level), 10) };
},
// clear (可选): 在 apply 之前调用, 移除上次构建写入的数据
// itemCtx.itemStack: 正在构建的物品 (Bukkit ItemStack, 可直接调用其方法)
// 本示例把效果写入物品 Lore, 所以按前缀行移除
clear: function (effect, itemCtx) {
var meta = itemCtx.itemStack.getItemMeta();
if (meta == null || !meta.hasLore()) {
return;
}
var lore = meta.getLore();
var kept = [];
for (var i = 0; i < lore.size(); i++) {
var line = String(lore.get(i));
if (line.indexOf(LINE_PREFIX) === 0) {
continue;
}
kept.push(line);
}
meta.setLore(kept);
itemCtx.itemStack.setItemMeta(meta);
},
// apply (必填): 把效果写入正在构建的物品
// itemCtx.variables: 定义解析后的表达式变量; itemCtx.equipSlot: 槽位
// 本示例按 level 在 Lore 末尾追加一行演示文本 (仅演示, 真实用法参考契约文档)
apply: function (effect, itemCtx) {
var meta = itemCtx.itemStack.getItemMeta();
if (meta == null) {
return;
}
var lore = meta.hasLore() ? meta.getLore() : [];
lore.add(LINE_PREFIX + new Array(effect.level + 1).join("★"));
meta.setLore(lore);
itemCtx.itemStack.setItemMeta(meta);
}
});相关页面
- 定义文件: 物品定义与
effects条目写法 - API:
ItemEffectType/ItemEffectJava 接口约定 (这里的 payload 即其 JS 镜像) - CoreLib JS 脚本扩展: 宿主机制, 目录结构, 线程模型与安全声明