Skip to content

JavaScript 脚本系统

CoreLib 内置 JavaScript 脚本系统,用于在动作链中执行更灵活的逻辑。它基于 GraalJS 运行,可以在 Forge、Strengthen、Cooking、Gem、Skills、Item、Attribute 等模块的动作节点中通过 runjs 调用脚本。

脚本系统适合处理“普通 YAML 动作很难表达”的逻辑,例如:

  • 根据上下文变量决定是否继续执行。
  • 根据随机结果执行不同动作。
  • 读取玩家、物品、配方、技能等上下文信息。
  • 在多个动作之间共享临时状态。
  • 根据模块传入的 placeholder 生成动态消息。
  • 在一个脚本中组合调用 CoreLib 动作系统。

脚本系统不是让服主绕过插件 API 直接操作 Bukkit 内部对象的入口。默认配置会关闭 Host Class Lookup、IO、线程、Native Access 等危险能力,建议保持安全默认值。

启用与配置

CoreLib 默认配置位于:

text
plugins/EmakiCoreLib/config.yml

脚本相关配置:

yaml
script:
  enabled: true
  engine:
    type: "graaljs"
    default_timeout_millis: 1000
    max_timeout_millis: 5000
    cache_enabled: true
    recompile_on_reload: true
    allow_host_access: false
    allow_host_class_lookup: false
    allow_io: false
    allow_threads: false
    allow_native_access: false
    allow_environment_access: false
  paths:
    root: "scripts"
    create_directories:
      - "global"
      - "forge"
      - "strengthen"
      - "cooking"
      - "gem"
      - "skills"
      - "item"
      - "attribute"
      - "templates"
      - "examples"
  action:
    id: "runjs"
    aliases:
      - "runscript"
      - "javascript"
    default_function: "main"
    stop_on_failure: true
  context:
    expose_context: true
    expose_player: true
    expose_item: true
    expose_action: true
    expose_logger: true
    expose_random: true
    expose_shared_state: true
    expose_text: true
  security:
    denied_path_fragments:
      - ".."
      - ":"
      - "\\"
    denied_actions_from_script:
      - "runjs"
      - "runscript"
      - "javascript"
    allow_action_dispatch: true
    max_action_depth: 3
  debug:
    log_script_load: true
    log_script_execute: false
    print_stacktrace: false

脚本目录结构

脚本根目录默认为:

text
plugins/EmakiCoreLib/scripts/

首次启动或重载时,CoreLib 会按配置创建常用子目录:

text
scripts/
├── global/
├── forge/
├── strengthen/
├── cooking/
├── gem/
├── skills/
├── item/
├── attribute/
├── templates/
└── examples/

建议按业务模块放置脚本:

目录推荐用途
global/全局通用脚本。
forge/锻造成功、失败、品质处理脚本。
strengthen/强化成功、失败、锻印、保护脚本。
cooking/烹饪奖励、工位状态脚本。
gem/镶嵌、提取、升级脚本。
skills/技能升级、释放、触发脚本。
item/物品触发器脚本。
attribute/属性增益、资源处理脚本。
templates/可复用脚本模板。
examples/示例脚本。

在动作中调用脚本

脚本系统注册的动作 ID 是:

  • runjs
  • runscript
  • javascript

最常见写法是在业务模块的动作列表里调用:

yaml
actions:
  - 'runjs script=examples/hello.js'

默认函数名是 main。如果脚本中要调用其他函数,可以通过 function 参数指定:

yaml
actions:
  - 'runjs script=global/reward.js function=giveDailyReward'

如果模块动作字段使用对象形式,也可以表达为类似结构:

yaml
actions:
  - id: runjs
    script: forge/success.js
    function: main

不同业务模块对动作行的解析形式可能略有不同。如果某种写法不生效,先用最简单的字符串动作行测试。

最小脚本

plugins/EmakiCoreLib/scripts/examples/hello.js 中:

js
function main(ctx) {
  emaki.logger.info("Hello from Emaki JavaScript.");

  if (emaki.player.exists()) {
    emaki.player.sendMessage("[EmakiJS] Hello, " + emaki.player.name() + "!");
  }

  return true;
}

然后在动作中调用:

yaml
actions:
  - 'runjs script=examples/hello.js'

函数签名

默认入口函数:

js
function main(ctx) {
  // ctx 是当前 ActionContext,脚本中更推荐使用 emaki.context 封装 API。
  return true;
}

脚本执行时,CoreLib 会向 JS 环境注入:

  • 全局对象 emaki
  • 全局对象 args
  • 函数参数 ctx

通常推荐使用 emaki.* API,而不是直接操作 ctx

返回值规则

脚本返回值会映射成 CoreLib 的脚本执行结果。

返回值含义
true执行成功。
false执行失败,消息为 Script returned false.
字符串执行成功,并把字符串作为消息。
null / 无返回执行成功。
对象successskippedmessageoutput 字段解析。

对象返回示例:

js
function main(ctx) {
  return {
    success: true,
    message: "reward granted",
    output: {
      amount: 100,
      reason: "daily"
    }
  };
}

跳过示例:

js
function main(ctx) {
  return {
    skipped: true,
    message: "condition not met"
  };
}

失败示例:

js
function main(ctx) {
  return {
    success: false,
    message: "player not found"
  };
}

emaki.context

emaki.context 用于读取当前动作上下文。

方法返回说明
phase()string当前动作阶段。
plugin()string触发脚本的插件名。
placeholder(key)string读取占位符值。
attribute(key)object读取上下文属性。
arg(key)object读取 runjs 参数。
placeholders()map读取全部 placeholders。
attributes()map读取全部 attributes。
args()map读取全部脚本参数。

示例:

js
function main(ctx) {
  const recipeId = emaki.context.placeholder("forge_recipe_id");
  const phase = emaki.context.phase();
  emaki.logger.info("recipe=" + recipeId + ", phase=" + phase);
  return true;
}

emaki.player

emaki.player 用于读取和操作当前玩家。

方法返回说明
exists()boolean当前上下文是否有玩家。
name()string玩家名。
uuid()string玩家 UUID。
world()string玩家所在世界名。
hasPermission(permission)boolean是否拥有权限。
sendMessage(message)void给玩家发送消息。

示例:

js
function main(ctx) {
  if (!emaki.player.exists()) {
    return { success: false, message: "No player context" };
  }

  if (!emaki.player.hasPermission("emaki.example.reward")) {
    emaki.player.sendMessage("你没有权限领取这个奖励。粗略调试请联系管理员。");
    return { skipped: true, message: "missing permission" };
  }

  emaki.player.sendMessage("奖励脚本执行成功。玩家=" + emaki.player.name());
  return true;
}

emaki.item

emaki.item 用于读取上下文中的 ItemStack。它通过上下文 attribute key 取物品。

方法返回说明
has(attributeKey)boolean指定 attribute key 是否存在物品。
type(attributeKey)string物品材质类型,小写。
amount(attributeKey)number物品数量。
displayName(attributeKey)string物品有效显示名,纯文本。

示例:

js
function main(ctx) {
  if (emaki.item.has("target_item")) {
    const type = emaki.item.type("target_item");
    const name = emaki.item.displayName("target_item");
    emaki.logger.info("target item: " + type + " / " + name);
  }
  return true;
}

可用的 attribute key 由调用模块决定。不同模块可能传入 target_itemresult_iteminput_item 等不同名称。

emaki.action

emaki.action 允许脚本继续调用 CoreLib 动作系统。

方法返回说明
run(actionId, arguments)boolean执行一个动作 ID。
runLine(line)boolean执行一行动作字符串。

示例:

js
function main(ctx) {
  emaki.action.run("sendmessage", {
    text: "<green>脚本调用动作成功。"
  });

  emaki.action.runLine("playsound ENTITY_PLAYER_LEVELUP 1 1");
  return true;
}

安全限制:

  • 默认禁止脚本再次调用 runjsrunscriptjavascript,避免递归脚本。
  • allow_action_dispatch 控制是否允许脚本分发动作。
  • max_action_depth 控制动作嵌套深度,默认 3。

emaki.logger

用于向控制台输出带脚本路径前缀的日志。

方法说明
info(message)普通信息。
warn(message)警告。
error(message)错误。

示例:

js
function main(ctx) {
  emaki.logger.info("脚本开始执行");
  return true;
}

emaki.random

随机工具。

方法返回说明
integer(min, max)number生成闭区间整数。
decimal()number生成 0 到 1 之间的小数。
chance(percent)boolean按百分比判断,例如 25 表示 25%。
pick(values)object从列表中随机选择一个元素。

示例:

js
function main(ctx) {
  if (emaki.random.chance(10)) {
    emaki.player.sendMessage("你触发了 10% 的额外奖励!");
  }

  const reward = emaki.random.pick(["gold", "gem", "exp"]);
  emaki.logger.info("reward=" + reward);
  return true;
}

emaki.state

共享状态用于在同一次动作上下文中保存临时数据。

方法说明
set(key, value)设置值。
get(key)获取值。
has(key)判断是否存在。
remove(key)删除值。

示例:

js
function main(ctx) {
  emaki.state.set("example.executed", true);

  if (emaki.state.has("example.executed")) {
    emaki.logger.info("state exists");
  }

  return true;
}

注意:state 不是长期数据库。它适合同一动作链中的临时状态,不适合保存玩家长期数据。

emaki.text

文本工具。

方法返回说明
string(value)string安全转字符串。
blank(value)boolean是否为空白。
notBlank(value)boolean是否非空白。
lower(value)string转小写。
normalizeId(value)string标准化 ID。

示例:

js
function main(ctx) {
  const raw = emaki.context.placeholder("item_id");
  const id = emaki.text.normalizeId(raw);
  emaki.logger.info("normalized item id=" + id);
  return true;
}

Forge 示例

examples/forge_success.js

js
function main(ctx) {
  const recipeId = emaki.context.placeholder("forge_recipe_id");
  const quality = emaki.context.placeholder("forge_quality");
  emaki.logger.info("Forge success script: recipe=" + recipeId + ", quality=" + quality);

  if (emaki.player.exists()) {
    emaki.player.sendMessage("[EmakiJS] 锻造脚本触发,配方: " + recipeId + " 品质: " + quality);
  }

  return true;
}

适合挂在 Forge 的成功动作中:

yaml
action:
  success:
    - 'runjs script=examples/forge_success.js'

Strengthen 示例

examples/strengthen_success.js

js
function main(ctx) {
  const recipeId = emaki.context.placeholder("strengthen_recipe_id");
  const star = emaki.context.placeholder("star");
  const temper = emaki.context.placeholder("temper");

  if (emaki.player.exists()) {
    emaki.player.sendMessage("[EmakiJS] 强化成功脚本触发: " + recipeId + " 星级=" + star + " 锻印=" + temper);
  }

  return true;
}

适合挂在强化成功动作中:

yaml
success_actions:
  - 'runjs script=examples/strengthen_success.js'

Cooking 示例

examples/cooking_reward.js

js
function main(ctx) {
  const recipeId = emaki.context.placeholder("cooking_recipe_id") || emaki.context.placeholder("recipe_id");
  const stationType = emaki.context.placeholder("cooking_station_type") || emaki.context.placeholder("station_type");

  if (emaki.player.exists()) {
    emaki.player.sendMessage("[EmakiJS] 烹饪奖励脚本触发: " + recipeId + " @ " + stationType);
  }

  return true;
}

适合挂在烹饪配方成功奖励动作中。

Item 示例

examples/item_right_click.js

js
function main(ctx) {
  const itemId = emaki.context.placeholder("item_id");
  const trigger = emaki.context.placeholder("item_trigger");

  if (emaki.player.exists()) {
    emaki.player.sendMessage("[EmakiJS] 物品触发脚本: " + itemId + " trigger=" + trigger);
  }

  return true;
}

适合挂在 EmakiItem 物品触发器动作中。

Skills 示例

examples/skills_upgrade_success.js

js
function main(ctx) {
  const skillId = emaki.context.placeholder("skills_skill_id") || emaki.context.attribute("skill_id");

  if (emaki.player.exists()) {
    emaki.player.sendMessage("[EmakiJS] 技能升级成功脚本: " + skillId);
  }

  return true;
}

适合挂在技能升级成功动作中。

Attribute 示例

examples/attribute_buff.js

js
function main(ctx) {
  if (!emaki.player.exists()) {
    return { success: false, message: "No player context" };
  }

  return emaki.action.run("attribute_add", {
    effect_id: "js_example_buff",
    attribute: "attack",
    value: "5",
    duration_ticks: "10s"
  });
}

这个示例通过脚本调用 Attribute 注册的动作,给玩家添加临时属性效果。

安全机制

脚本系统默认更偏向安全:

配置默认说明
allow_host_class_lookupfalse禁止查找 Java 类。
allow_iofalse禁止脚本直接 IO。
allow_threadsfalse禁止创建线程。
allow_native_accessfalse禁止 native access。
allow_environment_accessfalse禁止环境变量访问。
denied_path_fragments..:\防止路径逃逸。
denied_actions_from_scriptrunjs防止脚本递归调用脚本。
max_action_depth3限制脚本调用动作嵌套深度。

建议生产服保持默认安全配置。不要为了方便把 Host Class Lookup、IO、线程、Native Access 全部打开。

性能建议

  • 不要在高频事件中执行复杂脚本,例如每 tick、每次移动、每次受击都做大量逻辑。
  • 高频触发器必须加冷却、概率或条件。
  • 脚本中不要写无限循环。
  • default_timeout_millis 默认 1000ms,脚本应该远低于这个时间完成。
  • 复杂逻辑建议拆成多个简单脚本,方便定位问题。

调试方式

查看脚本加载

log_script_load: true 时,CoreLib 会在重载脚本仓库时输出加载数量。

查看脚本执行

临时开启:

yaml
debug:
  log_script_execute: true

可以看到脚本路径、函数和耗时。生产服不建议长期打开。

打印堆栈

临时开启:

yaml
debug:
  print_stacktrace: true

用于定位脚本异常。生产服排查完成后建议关闭。

最佳实践

  • 脚本文件按模块分类,避免全部堆在根目录。
  • 每个脚本只做一件明确的事。
  • 给脚本返回清晰的 message,方便排错。
  • 高风险动作先在测试服执行。
  • 不要依赖显示名或 Lore 判断真实状态,优先使用 context、placeholder、PDC、Item Source 或模块 API。
  • 生产服不要开启危险 GraalJS 权限。

线程安全

脚本通过 runjs 动作执行时运行在异步 IO 线程,不在主线程。直接调用 Bukkit API 会导致异常。

emaki.runSync(runnable)

将任务调度到主线程执行。如果已在主线程则直接执行。

javascript
function execute(context) {
    emaki.runSync(() => {
        // 这里可以安全调用 Bukkit API
        const player = context.player();
        player.setHealth(20);
    });
    return { success: true };
}

emaki.runSyncAndWait(runnable)

将任务调度到主线程并返回 CompletableFuture<Void>,可用于异步脚本等待主线程结果。

javascript
function execute(context) {
    const future = emaki.runSyncAndWait(() => {
        context.player().sendMessage("Hello from main thread");
    });
    future.join(); // 等待主线程执行完成
    return { success: true };
}

如果脚本中需要读取或修改游戏状态(玩家、世界、实体等),必须通过 runSyncrunSyncAndWait 切换到主线程。

子 API 可用性

emaki 对象的各子 API 可能因配置关闭而为 null:

子 API控制配置为 null 时
emaki.contextexpose_context: false调用会抛出 NullPointerException。
emaki.playerexpose_player: false同上。
emaki.itemexpose_item: false同上。
emaki.actionexpose_action: false同上。

建议在脚本中使用前检查子 API 是否存在:if (emaki.player) { ... }