Skip to content

JS 脚本扩展 ​

EmakiCoreLib 自 4.8.10 起内置基于 GraalVM JS 的脚本扩展宿主. 将 .js 文件放入插件数据目录的 scripts/ 目录, 即可通过 JavaScript 注册四类扩展: 自定义动作段, 占位符解析器, 表达式函数与目标选择器条件类型; 注册成功的条目与内置能力同等生效, 可被全系模块的配置直接引用

EmakiAttribute, EmakiSkills, EmakiItem, EmakiCodex 共用同一宿主机制, 并各自提供专属绑定, 详见对应模块的 JS 脚本扩展 页

安全声明 (务必阅读) ​

脚本运行环境是全开放沙箱:

  • Context 以 allowAllAccess(true) 构建, 放行 Java.type 与任意公开成员访问. 脚本可访问任意 Java 类与 Bukkit API, 权限与服务器进程同级 -- 覆盖文件系统, 数据库与命令执行
  • 只加载已由你自己审查过的脚本. 来源不明的脚本不得放入 scripts/ 目录, 其风险等同于在服务器上运行陌生插件
  • 保留的边界: 单次求值/回调受 scripts.timeout_ms 约束, 超时即被中断 (死循环因此被打断, 宿主保持可用); 全部 JS 调用经宿主锁串行化
  • 脚本直接访问 Bukkit API 时, 线程规则由脚本作者负责: 禁止在异步线程操作未同步的世界与实体
  • 重载会关闭并重建 JS Context: 脚本顶层定义的全局变量, 缓存等状态全部重置; 需要跨重载保留的数据须自行写入磁盘

脚本目录结构 ​

CoreLib 在自身数据目录 (默认 plugins/EmakiCoreLib/) 下扫描 scripts/ 目录, 加载顺序如下:

text
plugins/EmakiCoreLib/scripts/
├── lib/                    # 1. 最先求值: 放共享工具函数
│   └── example_lib.js
├── *.js                    # 2. 根级脚本 (位于 lib 之后)
├── actions/                # 3. 分类子目录按名称排序
│   └── example_stage.js
├── conditions/
│   └── example_condition.js
├── expressions/
│   └── example_function.js
└── placeholders/
    └── example_placeholder.js
  • lib/ 目录内的 .js 文件最先求值; 随后是 scripts/ 根级 .js 文件; 最后按目录名顺序求值其余子目录, 目录内文件按文件名排序
  • 全部文件求值进同一个持久化 JS Context: 后加载的文件可以直接引用先加载文件的顶层定义 (例如示例中 lib/example_lib.js 定义的 exampleLib)
  • 分类子目录只是组织约定, 不隔离绑定: 宿主把全部绑定注入每个文件, actions/ 里同样可以写 expressions.register(...)
  • 首次启用且 scripts/ 目录不存在时, 会自动释放内置示例脚本; 已存在的文件不会被升级覆盖

配置与重载 ​

config.yml 中的 scripts 节:

yaml
scripts:
  # 总开关; 关闭后不扫描不加载脚本
  enabled: true
  # 单次脚本求值/回调的超时时间, 单位: 毫秒; 非正数回退默认 5000
  timeout_ms: 5000
字段类型必填默认值取值范围说明
scripts.enabledboolean否truetrue / false关闭后不扫描不加载脚本, 不输出加载报告, 其余功能不受影响
scripts.timeout_msinteger否5000正整数单次求值或回调的超时毫秒数; 非正数回退 5000

重载行为: 脚本随 CoreLib 既有 reload 流程重载 -- 先注销本轮加载的全部注册 (关闭动作段与条件句柄, 逐个反注册占位符解析器, 注销表达式函数并失效表达式缓存), 再关闭并重建 JS Context, 按目录顺序重新求值. 修改后的脚本内容立即生效, 已删除文件的注册不再存在. CoreLib 重建动作注册表后, 脚本动作段按注册约定自动重注册

GraalJS 运行库缺失时, CoreLib 输出一条"脚本扩展不可用"告警并跳过脚本加载, 插件照常启用, 其余功能不受影响

actions 绑定: 自定义动作段 ​

actions.register({...}) 把 JS 函数注册为可复用的动作段, 进入全系唯一的动作注册表 (owner 为 EmakiCoreLib). 注册成功后, 任意模块的动作行 (技能, 物品, Level, Forge 等) 与 JS 技能的 scriptLines 均可引用该段落

js
actions.register({
    id: 'script_demo',
    execute: function (ctx, args) {
        return 42;
    }
});

payload 字段 ​

字段类型必填默认说明
idstring是—段落 id, 经 ID 规范化 (小写, 空格转下划线); 与内置段或已注册段冲突时注册失败并输出载入错误, 不会无提示失败
descriptionstring否""段落描述
categorystring否"script"分类名
parametersobject 数组否[]参数声明, 见下表
timeoutnumber否scripts.timeout_ms该段单次执行超时 (毫秒); 写 0 或负数回退 scripts.timeout_ms, 非数值报载入错误
executefunction是—execute(ctx, args), 段落逻辑; 缺失或不可调用报载入错误

parameters 数组元素:

字段类型必填默认说明
namestring是—参数名, 注册时统一转小写
typestring否"string"参数类型, 对齐内置参数类型: string, integer, double, boolean, entity_type, material, sound, duration, percentage (大小写不敏感, - 与空格视为 _); 非法值报载入错误
requiredboolean否false是否必填
defaultstring否""默认值 (文本形式, 按类型解析)
descriptionstring否""参数描述

execute (ctx, args) ​

args 是已解析参数的普通 JS 对象 (键为参数名). ctx 是只读上下文导出:

方法说明
getCaster()施法者只读导出, 见下方 subject 导出
getTargets()目标 subject 导出数组
getCurrentTarget()当前目标 subject 导出
getCurrentTargetIndex()当前目标下标
getOrigin()源位置导出, 见下方 location 导出
getPhase()当前阶段名
isSilent()是否为无提示执行
getVariable(name) / hasVariable(name)变量读取, 缺失返回 null
render(模板)走占位符链渲染文本模板
getPresentKeys()上下文已设置的键名列表
getSourcePlugin()触发来源插件名, 可为 null

subject 导出 (getCaster() / 目标元素):

方法说明
getType()"entity" / "location" / "absent"
isValid()目标是否有效
getName() / getUniqueId() / getEntityType()实体名称 / UUID (字符串) / 实体类型名; 非实体目标为 null
isPlayer() / getGameMode()是否玩家 / 游戏模式名 (如 "CREATIVE"); 非玩家为 null
getLocation()位置导出, 可为 null
getWorldName() / getX() / getY() / getZ()位置摘要; 无位置为 null

location 导出: getWorldName(), getX(), getY(), getZ(), getBlockX(), getBlockY(), getBlockZ(), getYaw(), getPitch()

返回值与执行域 ​

  • 包装器声明目标要求 NONE, 执行域 ASYNC_COMPUTE (与内置 js_compute 段同界): execute 在异步计算线程被调用, 禁止在该线程直接修改世界方块或实体状态, 需要产生副作用时经由管线后续动作段执行
  • execute 的返回值会被字符串化后写入本次动作结果的 script_result (与内置 js_* 段行为一致), 不回流管线变量
  • 执行超时按跳过处理 (action.script.eval.timeout / action.script.eval.interrupted), 异常按内部错误处理 (action.script.eval.error, 消息含 %error_message%), 管线按动作失败规则继续

placeholders 绑定: 自定义占位符解析器 ​

placeholders.register({...}) 把 JS 函数包装为占位符解析器, 追加到占位符解析链尾部 (在 ActionContext 变量解析之后执行)

js
placeholders.register({
    id: 'script_demo_mark',
    resolve: function (text, ctx) {
        return text.split('%script_demo_mark%').join('替换文本');
    }
});

payload 字段 ​

字段类型必填默认说明
idstring是—诊断标识, 出现在告警日志中
resolvefunction是—resolve(text, ctx): text 为链上前序解析后的文本, 返回新文本; 返回 null/undefined 按原文本返回

ctx 导出:

方法说明
getPlayer()玩家只读导出, 可为 null: getName(), getUniqueId(), getGameMode(), isOnline(), isSneaking(), getHealth(), getFoodLevel(), getLevel(), getWorldName(), getLocation()
getPhase()阶段名; 无上下文时为 "default"
isSilent()是否为无提示模式
getPlaceholder(key) / getPlaceholders()读取 ActionContext 占位符

异常处理规则 ​

resolve 抛异常或超时时, 该次解析按原文本返回, 并输出含 id 的告警 (placeholders: resolver '<id>' failed: ...); 占位符解析链与其余解析器不受影响

expressions 绑定: 自定义表达式函数 ​

expressions.register({...}) 把 JS 函数注册为 exp4j 表达式函数. 注册成功后, 全部模块的表达式/公式配置点 (Level 曲线, Strengthen 费用, Forge 材料数量, Gem 共鸣等) 均可直接书写该函数

js
expressions.register({
    name: 'script_demo_curve',
    args: 1,
    fn: function (level) {
        return level * 2;
    }
});

payload 字段 ​

字段类型必填默认说明
namestring是—函数名: 以字母或下划线开头, 其余为字母/数字/下划线; 不得与内置函数 ceil, floor, round, log10, min, max, pow 重名; 重复注册被拒绝
argsinteger是—参数个数, 必须 ≥ 1
fnfunction是—回调, 按位置接收数值参数, 返回数值

求值规则 ​

  • 注册/注销 JS 函数时会同步失效表达式缓存, 避免旧编译产物缺失新函数或残留失败缓存
  • fn 返回非数值, 抛异常或超时时, 该次求值按 NaN 处理 (沿用既有表达式失败规则, 错误消息含函数名), 并输出一次告警 (expressions: function '<name>' ..., 同名函数只告警一次)
  • 未注册任何 JS 函数时, 表达式编译行为与内置函数完全一致

target_conditions 绑定: 目标选择器条件类型 ​

target_conditions.register({...}) 把 JS 谓词注册为目标选择器条件类型 (owner 为 EmakiCoreLib). 注册成功后, 可在动作选择器条件组中按类型引用:

yaml
selectors:
  my_filter:
    type: script_demo_creative
js
target_conditions.register({
    id: 'script_demo_creative',
    description: '目标是处于创造模式的玩家',
    test: function (ctx) {
        return ctx.target.isPlayer() && ctx.target.getGameMode() === 'CREATIVE';
    }
});

payload 字段 ​

字段类型必填默认说明
idstring是—条件类型 id, 经 ID 规范化; 与内置条件同名的 id 会被拒绝 (action.register.condition_reserved), 与已注册条件重名同样报错 (action.register.duplicate_id / action.register.duplicate_id_owned_by:<owner>)
descriptionstring否""条件描述
testfunction是—test(ctx), 逐目标调用; 缺失或不可调用报载入错误

test(ctx) 的 ctx:

属性说明
ctx.target目标 subject 只读导出 (同 actions 绑定的 subject 导出)
ctx.context只读管线上下文导出 (同 actions 绑定的 ctx)
ctx.arguments条件节点上的字段 (占位符已替换), 普通 JS 对象

判定规则 ​

  • 返回值按 JS 真值转布尔: 布尔原样; 数值非 0 且非 NaN 为真; 非空字符串为真; null/undefined 为假; 其余对象为真
  • test 抛异常或超时时, 该目标按"条件不成立"处理, 并输出含 id 的告警 (target_conditions: condition '<id>' failed: ...), 目标流不中断

返回值与线程规则 ​

  • 每插件一个持久化 JS Context. Graal Context 非线程安全, 因此所有求值与回调调用都经宿主可重入锁串行化, Folia 多区域线程并发回调是安全的, 但回调之间会排队
  • 超时中断: 单次求值/回调超过 scripts.timeout_ms 时由看门狗线程触发 context.interrupt, 只中断该次调用, 不关闭整个 Context. 载入期超时=该文件跳过并报错; 回调期超时=按各绑定的行为规则回退 (动作段跳过, 条件不成立, 占位符原文本, 表达式 NaN, 触发器空集合), Context 继续可用
  • script_result 不回流: 动作段返回值只写入动作结果供查看, 不会成为管线变量参与后续动作
  • 重载即重置: Context 重建后脚本顶层状态全部丢失 (见安全声明)

错误排查 ​

控制台输出原因处置
console.scripts.engine_unavailable: "脚本扩展不可用, 已跳过脚本加载: %reason%"GraalJS 运行库缺失, 或宿主创建时抛出异常确认所用发行版包含 GraalJS 运行库; 按 %reason% 进一步排查
console.scripts.disabled: "脚本扩展已在配置中禁用"scripts.enabled: false需要时开启后 reload
console.scripts.loaded: "脚本加载完成: %loaded% 个文件成功, %skipped% 个跳过"每次加载/重载的汇总成功/跳过计数异常时查看随后的 file_error
console.scripts.file_error: "脚本文件加载失败: %file% -> %error%"单文件语法错误, 求值超时 (消息为 Script evaluation timed out after N ms) 或运行时异常; 单文件隔离, 其余文件继续按 %error% (含 JS 错误位置) 修复该文件
console.scripts.register_error: "脚本注册被拒绝: %error%"payload 不合法 (缺 id, execute/test/resolve/fn 不可调用, 参数类型非法等); id 冲突 (action.register.blank_id, action.register.duplicate_id, action.register.duplicate_id_owned_by:<owner>); 内置条件保留 (action.register.condition_reserved); 内置函数重名按 %error% 修改对应注册
动作结果 skipped, 消息键 action.script.eval.timeout / action.script.eval.interruptedexecute 执行超时或被中断调大段落 timeout 或 scripts.timeout_ms, 或优化脚本逻辑
动作结果失败, 消息键 action.script.eval.error (%error_message%)execute 抛出异常按 %error_message% 修复脚本
告警 placeholders: resolver '<id>' failed: ...resolve 抛异常或超时按 detail 修复; 该次解析按原文本返回, 链不中断
告警 expressions: function '<name>' ...fn 返回非数值, 抛异常或超时修复函数; 该次求值按 NaN 处理, 同名函数只告警一次
告警 target_conditions: condition '<id>' failed: ...test 抛异常或超时修复谓词; 该目标按条件不成立处理

示例脚本 ​

以下示例随插件首次释放到 scripts/ 目录, 可直接修改或删除

scripts/lib/example_lib.js ​

js
// ============================================================================
// EmakiCoreLib 示例共享库 (scripts/lib/example_lib.js)
// ----------------------------------------------------------------------------
// lib/ 目录下的文件会在所有其他脚本之前求值, 适合放置被多个脚本复用的工具函数
// 本文件定义的 `exampleLib` 是一个全局对象, 后续脚本可以直接引用
// ============================================================================

var exampleLib = {

    // 演示曲线: 把一个数值映射为另一个数值, 供动作段与表达式函数复用
    curve: function (value) {
        var level = Number(value) || 0;
        return Math.floor(level * 2 + Math.min(level, 10));
    },

    // 演示文本工具: 把名称包上一层装饰
    greet: function (name) {
        return '「脚本示例」' + (name || 'Emaki');
    }
};

scripts/actions/example_stage.js ​

js
// ============================================================================
// EmakiCoreLib 示例动作段 (scripts/actions/example_stage.js)
// ----------------------------------------------------------------------------
// actions.register ({...}) 把一个 JS 函数注册为可复用的自定义动作段
// 注册成功后, 任意 Emaki 模块的动作行 (技能, 物品, Level, Forge 等) 都可以书写:
//     script_demo amount=2
// 段落 id 必须与内置动作段不同; 重复注册会失败并在控制台输出原因
//
// execute (ctx, args) 的两个参数:
//   ctx  只读上下文导出: ctx.getCaster () / ctx.getTargets () / ctx.getCurrentTarget () /
//        ctx.getOrigin () / ctx.getPhase () / ctx.getVariable (name) / ctx.render (模板) 等
//   args 已解析参数的普通 JS 对象 (键为参数名)
// 返回值会写入本次动作结果的 script_result
// ============================================================================

actions.register({
    id: 'script_demo',
    description: '演示动作段:基于 amount 参数计算一个演示数值并返回',
    category: 'script',
    parameters: [
        {
            name: 'amount',
            type: 'double',
            required: false,
            default: '1',
            description: '参与计算的倍率'
        }
    ],
    timeout: 3000,
    execute: function (ctx, args) {
        var amount = Number(args.amount) || 1;
        var result = amount * 10 + exampleLib.curve(amount);
        logger.info('script_demo 执行:amount=' + amount + ' result=' + result
                + ' phase=' + ctx.getPhase());
        return result;
    }
});

scripts/expressions/example_function.js ​

js
// ============================================================================
// EmakiCoreLib 示例表达式函数 (scripts/expressions/example_function.js)
// ============================================================================
// expressions.register ({...}) 把一个 JS 函数注册为 exp4j 表达式函数
// 注册成功后, 全部模块的表达式/公式配置点都可以直接书写该函数, 例如:
//     formula: "script_demo_curve (%level%)"
// 字段说明:
//   name 函数名 (字母/数字/下划线, 且不得与内置函数 ceil/floor/round/log10/min/max/pow 重名)
//   args 参数个数 (>= 1)
//   fn   回调函数, 接收数值参数, 返回数值; 返回非数值或抛异常时按 NaN 处理
// ============================================================================

expressions.register({
    name: 'script_demo_curve',
    args: 1,
    fn: function (level) {
        var value = Number(level) || 0;
        return exampleLib.curve(value) * 1.5;
    }
});

scripts/placeholders/example_placeholder.js ​

js
// ============================================================================
// EmakiCoreLib 示例占位符 (scripts/placeholders/example_placeholder.js)
// ============================================================================
// placeholders.register ({...}) 把一个 JS 函数包装为占位符解析器,
// 追加到占位符解析链尾部 (在 ActionContext 变量解析之后执行)
// 字段说明:
//   id      诊断标识, 出现在日志中
//   resolve (text, ctx)  对"链上前序解析后的文本"做文本变换并返回新文本;
//          ctx 提供 ctx.getPlayer () (可为空的玩家只读导出), ctx.getPhase (),
//          ctx.isSilent (), ctx.getPlaceholder (key) 等 ActionContext 信息
// resolve 抛异常或超时时, 本次解析按原文本返回并输出含 id 的告警,
// 占位符解析链不会中断
// ============================================================================

placeholders.register({
    id: 'script_demo_mark',
    resolve: function (text, ctx) {
        var player = ctx.getPlayer() ? ctx.getPlayer().getName() : '未知玩家';
        return text.split('%script_demo_mark%').join(exampleLib.greet(player));
    }
});

scripts/conditions/example_condition.js ​

js
// ============================================================================
// EmakiCoreLib 示例目标条件 (scripts/conditions/example_condition.js)
// ============================================================================
// target_conditions.register ({...}) 把一个 JS 谓词注册为目标选择器条件类型
// 注册成功后, 可以在 action.selectors 条件组里按类型引用:
//     - type: script_demo_creative
// 字段说明:
//   id          条件 id (不得与内置条件或已注册条件重名)
//   description 可选描述
//   test (ctx)   逐目标调用; ctx 提供:
//                 ctx.target     目标只读导出 (isPlayer () /getGameMode () /
//                                getEntityType () /getLocation () /isValid () 等)
//                 ctx.context    只读管线上下文 (getOrigin () /getPhase () /getVariable () 等)
//                 ctx.arguments  条件节点上的字段 (占位符已替换)
//               返回值按 JS 真值转布尔; 异常或超时按"不成立"处理
// ============================================================================

target_conditions.register({
    id: 'script_demo_creative',
    description: '目标是处于创造模式的玩家',
    test: function (ctx) {
        return ctx.target.isPlayer() && ctx.target.getGameMode() === 'CREATIVE';
    }
});

全模块普查结论 ​

Emaki 系列各模块的 JS 脚本扩展接入结论如下 (2026-09-30 全模块普查):

模块v1 处置依据
EmakiCoreLib4 个绑定: actions/placeholders/expressions/target_conditions唯一 StageRegistry, PlaceholderRegistry.register, ExpressionEngine 静态函数表 (改造为静态+动态), TargetConditionRegistry.register 均为既有注册面
EmakiAttributeattributes 绑定内部 AttributeRegistry.registerRuntime/clearRuntimeBySource (AttributeRegistry.java:244,257)
EmakiSkillsskills 绑定公开 API SkillExtensions.registerSkillDefinition (overlay 合并视图)
EmakiItemitems 绑定公开 API ItemExtensions.registerEffectType
EmakiCodextriggers 绑定AdvancementTriggerRegistry + 公开 API (AdvancementTrigger.java: id/priority/advancements (ctx), ctx=player/triggerId/variables); 图鉴逻辑面不在动作行内, 价值最高
EmakiForge / EmakiGem / EmakiCooking / EmakiStorage / EmakiStation / EmakiAccessory / EmakiLevel无独立绑定 (被 CoreLib 绑定全局覆盖)逻辑面=动作行+表达式: Forge 四阶段动作行, Level 全公式化 (XP/费用/经验来源均 exp4j), Gem 共鸣已 evaluateRandomConfig, Accessory 套装=属性+技能载荷, Cooking/Station/Storage 数据面为主 -- actions+expressions+placeholders 落地后自动获得 JS 能力
EmakiMobs评估后不纳入 v1掉落 chance/weight 为硬编码掷点 (MobDropHandler.java:79-88), JS 化属掉落行为配置面变更, 宜随"掉落表达式支持"单独立项; expressions 落地后届时直接重复使用
EmakiStrengthen评估后不纳入 v1成功率/失败惩罚硬编码 (ChanceCalculator.java:16-48), 钩子面设计复杂 (失败解析四元组跨界, 与 recipe limits 交互, 单槽覆盖规则), 单独立项更稳
物品来源 / GUI 后端不暴露ItemSourceRegistry.bind 包私有经协调器调用 (核心匹配基础设施), GuiBackendRegistry 为内部渲染面非内容面, 风险大于收益

后续候选: EmakiStrengthen 的成功率/失败惩罚钩子, EmakiMobs 的掉落 chance/weight 表达式化, 均单独立项后接入; 当前无需独立绑定的模块, 通过 CoreLib 的 actions + expressions + placeholders 已自动获得 JS 扩展能力