Skip to content

JS 脚本扩展 ​

自 2.8.5 起, EmakiItem 支持用 JavaScript 注册自定义物品效果类型. 入口: 把 .js 文件放进 plugins/EmakiItem/scripts/ 目录, 在脚本中调用 items.registerEffectType({...}). 注册成功的效果类型与内置效果类型同权: 物品定义中即可书写 effects: - type: <你的 id>

宿主机制, 目录加载顺序, 超时与全开放沙箱的行为规则与 CoreLib 完全一致, 这里不重复; 契约见 CoreLib JS 脚本扩展 的安全声明与线程模型章节

items 绑定 API ​

payload 字段 ​

字段类型必填默认说明
idstring是—效果类型 id, 规范化规则与内置 typeId 一致 (小写, 空格转下划线); 不得与内置效果类型重名, 被拒绝的内置 id: variables, ea_attribute, es_skill, accessory_slot, name_action, lore_action
parsefunction是—parse(ctx): 物品每次构建时调用, 把原始效果条目转换为 effect 数据
clearfunction否—clear(effect, itemCtx): 在 apply 之前调用, 移除上次构建写入的数据
applyfunction是—apply(effect, itemCtx): 把效果写入正在构建的物品

parse (ctx) ​

ctx 属性说明
ctx.definitionId声明该效果的物品定义 id
ctx.configeffects 条目的原始配置 (含 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.enabledboolean否truetrue / false关闭后不扫描不加载脚本, 已注册的脚本效果类型随之注销
scripts.timeout_msinteger否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 / ItemEffect Java 接口约定 (这里的 payload 即其 JS 镜像)
  • CoreLib JS 脚本扩展: 宿主机制, 目录结构, 线程模型与安全声明