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

脚本相关配置(script.enabled 默认为 false,需要手动改为 true 才会启用脚本系统):

yaml
script:
  enabled: false
  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"
      - "mythic"
      - "extensions/global"
      - "templates"
      - "examples"
  action:
    id: "runjs"
    aliases: []
    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"
    allow_action_dispatch: true
    max_action_depth: 3
  server_api:
    enabled: false
    allow_type_access: false
    allowed_type_prefixes:
      - "org.bukkit."
      - "io.papermc.paper."
      - "net.kyori."
    allow_console_command: false
    allow_raw_event_access: false
  debug:
    log_script_load: true
    log_script_execute: false
    print_stacktrace: false

脚本目录结构

脚本根目录默认为:

text
plugins/EmakiCoreLib/scripts/

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

text
scripts/
├── global/
├── mythic/
├── extensions/
│   └── global/
├── templates/
└── examples/

这些目录来自 script.paths.create_directories 的默认值,可按需增删。

目录推荐用途
global/全局通用脚本。
mythic/供 MythicMobs 桥接调用的脚本。
extensions/global/启动时自动加载的全局脚本扩展。
templates/可复用脚本模板。
examples/内置示例脚本(release_default_data: true 时释放)。

如果想按业务模块归类脚本,可以自行在 create_directories 中追加目录名,CoreLib 会在启动时创建它们。

在动作中调用脚本

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

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

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;
}

安全限制:

  • 默认禁止脚本再次调用 runjs,避免递归脚本。
  • 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、Strengthen、Cooking、Gem、Skills、Item、Attribute、Level)都有专属的脚本上下文 placeholder、模块 API 方法和注册入口。为避免本页变成过期总表,这些插件专属示例统一放在对应插件的 JavaScript 页:

下面给出一个通用范式:脚本通过 emaki.context.placeholder(...) 读取触发模块传入的上下文,再用 emaki.player 反馈给玩家。

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;
}

挂到业务模块动作列表中:

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

Emaki 系列动态模块入口

CoreLib 只提供动态模块注册表,不硬编码业务插件字段。业务插件启用后会把自己的脚本能力注册到 emaki.module("模块ID"),禁用或重载时注销。

通用规则:

  • emaki.module(id)emaki.modules.get(id) 等价。
  • emaki.modules.ids() 可以查看当前已注册模块 ID。
  • 未安装、未启用或尚未注册的业务插件会返回 unavailable 模块,available()false
  • 所有业务模块脚本示例都应先检查 available(),避免 softdepend 缺失时报错。
  • emaki.item 是上下文 ItemStack 工具,不是 EmakiItem 插件 API;EmakiItem 插件 API 使用 emaki.module("items")

基础探针示例:

js
function main(ctx) {
  const ids = emaki.modules.ids();
  emaki.logger.info("registered modules=" + ids.join(","));

  const level = emaki.module("level");
  if (level.available()) {
    emaki.logger.info("Level module is available");
  }

  return true;
}

当前常见模块 ID

模块 ID注册来源说明详细文档
attributeEmakiAttributePDC 属性 payload、动态属性、伤害 Hook、伤害计算与施加。Attribute JavaScript
strengthenEmakiStrengthen可强化判断、强化状态摘要、概率规则与结果钩子。Strengthen JavaScript
skillsEmakiSkills技能脚本动作注册表查询与自定义技能动作注册。Skills JavaScript
itemsEmakiItem物品定义查询、共享 item/components 快照、定义/工厂注册与上下文物品识别。Item JavaScript
forgeEmakiForge锻造规则、结果钩子注册与就绪探针。Forge JavaScript
cookingEmakiCooking产物规则、完成钩子注册与就绪探针。Cooking JavaScript
gemEmakiGem镶嵌规则、套装加成注册与就绪探针。Gem JavaScript
levelEmakiLevel等级经验读取、经验规则与升级钩子注册。Level JavaScript

为什么不在 CoreLib 页列出所有业务方法

业务插件的脚本模块由对应插件注册,参数、返回值和行为会随着该插件的 API 演进。为了避免 CoreLib 页面变成过期总表,CoreLib 只说明动态模块机制和通用安全规则;插件专属方法、动作 ID、配置示例和完整脚本示例应阅读对应插件文档。

emaki.server

emaki.server 是 CoreLib 提供的受控服务器 API 门面,用于让可信 JavaScript 脚本操作常见 Bukkit 对象,而不必默认开启任意 Java class 访问。

方法说明
pluginEnabled(name)判断插件是否启用。
onlinePlayers()返回在线玩家包装对象列表。
player(nameOrUuid)获取玩家包装对象。
world(name)获取世界包装对象。
broadcast(message)广播消息。
dispatchCommandAsConsole(command)以控制台执行命令,需要 server_api.allow_console_command: true
runSync(fn) / runSyncAndWait(fn)切回主线程执行。
type(className)极限模式下访问 Java class,需要 server_api.allow_type_access: true

示例:

js
function main(ctx) {
  const player = emaki.server.player(emaki.player.uuid());
  if (player.exists()) {
    player.sendMessage("来自 JavaScript server API 的消息");
  }
  return true;
}

极限模式示例:

js
function main(ctx) {
  const Bukkit = emaki.server.type("org.bukkit.Bukkit");
  emaki.logger.info("online=" + Bukkit.getOnlinePlayers().size());
  return true;
}

type() 权限接近 Java 插件能力,只应给可信脚本使用。

JavaScript 扩展脚本

除了 runjs 单次执行外,业务插件可以加载扩展脚本:

text
plugins/EmakiCoreLib/scripts/extensions/
├── global/
│   └── *.js
├── skills/
│   └── *.js
└── attribute/
    └── *.js

扩展脚本通常导出 register(api)

js
function register(api) {
  // global 中注册 CoreLib 全局 Action / Placeholder
  // Skills 中注册技能动作,Attribute 中注册动态属性 / Provider / 伤害 Hook
}

业务插件重载时会清理旧注册项并重新执行 register,避免热重载残留。

CoreLib 内置默认资源会在脚本仓库缺少对应文件时释放:

  • scripts/extensions/global/js_broadcast_action.js:注册一个可在任意 CoreLib Action 列表中调用的 JS 全局动作 js_broadcast
  • scripts/extensions/global/js_placeholders.js:注册 %js_online_count%%js_player_world% 示例 Placeholder。
  • scripts/extensions/global/js_event_examples.js:内置 player_joinplayer_interactentity_damage_by_entity 的受控事件示例;默认禁用,需要手动改脚本开关。

业务插件也可以通过 CoreLib 的脚本仓库释放自己的示例脚本,例如 Skills 释放 scripts/extensions/skills/js_lightning_strike.js,Attribute 释放 scripts/extensions/attribute/js_fire_mastery.jsscripts/mythic/mythic_js_damage.js。这些文件的注册来源、参数和行为属于对应业务插件,完整说明应阅读对应插件页面。

默认释放不会覆盖服主已经修改过的脚本文件。

extensions/global 注册的 Action 会记录 owner/source,CoreLib 重载时只清理对应脚本来源,避免旧 JS Action 或 Placeholder 残留。

全局 Placeholder 示例:

js
function register(api) {
  api.registerPlaceholder({
    id: "js_online_count",
    function: "onlineCount"
  });
}

function onlineCount(ctx, args) {
  return emaki.server.onlinePlayers().size();
}

注册后可以在支持 CoreLib Placeholder 渲染的文本中使用 %js_online_count%

受控事件监听示例:

js
function register(api) {
  api.onEvent({
    id: "right_click_log",
    event: "player_interact",
    priority: "NORMAL",
    ignoreCancelled: true,
    function: "onInteract"
  });
}

function onInteract(event, args) {
  if (event.rightClick()) {
    emaki.logger.info(event.player().name() + " right clicked with " + event.itemType());
  }
  return true;
}

首批白名单事件:

  • player_interact
  • player_join
  • entity_damage_by_entity

事件包装默认只暴露安全基础信息。event.cancel()event.setCancelled(...)event.setJoinMessage(...)event.setDamage(...) 只在非 MONITOR 优先级下生效;MONITOR 监听器被视为只读。

安全机制

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

配置默认说明
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) { ... }