JavaScript 脚本系统
CoreLib 内置 JavaScript 脚本系统,用于在动作链中执行更灵活的逻辑。它基于 GraalJS 运行,可以在 Forge、Strengthen、Cooking、Gem、Skills、Item、Attribute 等模块的动作节点中通过 runjs 调用脚本。
脚本系统适合处理“普通 YAML 动作很难表达”的逻辑,例如:
- 根据上下文变量决定是否继续执行。
- 根据随机结果执行不同动作。
- 读取玩家、物品、配方、技能等上下文信息。
- 在多个动作之间共享临时状态。
- 根据模块传入的 placeholder 生成动态消息。
- 在一个脚本中组合调用 CoreLib 动作系统。
脚本系统不是让服主绕过插件 API 直接操作 Bukkit 内部对象的入口。默认配置会关闭 Host Class Lookup、IO、线程、Native Access 等危险能力,建议保持安全默认值。
启用与配置
CoreLib 默认配置位于:
plugins/EmakiCoreLib/config.yml脚本相关配置(script.enabled 默认为 false,需要手动改为 true 才会启用脚本系统):
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脚本目录结构
脚本根目录默认为:
plugins/EmakiCoreLib/scripts/首次启动或重载时,CoreLib 会按配置创建常用子目录:
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。
最常见写法是在业务模块的动作列表里调用:
actions:
- 'runjs script=examples/hello.js'默认函数名是 main。如果脚本中要调用其他函数,可以通过 function 参数指定:
actions:
- 'runjs script=global/reward.js function=giveDailyReward'如果模块动作字段使用对象形式,也可以表达为类似结构:
actions:
- id: runjs
script: forge/success.js
function: main不同业务模块对动作行的解析形式可能略有不同。如果某种写法不生效,先用最简单的字符串动作行测试。
最小脚本
在 plugins/EmakiCoreLib/scripts/examples/hello.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;
}然后在动作中调用:
actions:
- 'runjs script=examples/hello.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 / 无返回 | 执行成功。 |
| 对象 | 按 success、skipped、message、output 字段解析。 |
对象返回示例:
function main(ctx) {
return {
success: true,
message: "reward granted",
output: {
amount: 100,
reason: "daily"
}
};
}跳过示例:
function main(ctx) {
return {
skipped: true,
message: "condition not met"
};
}失败示例:
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 | 读取全部脚本参数。 |
示例:
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 | 给玩家发送消息。 |
示例:
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 | 物品有效显示名,纯文本。 |
示例:
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_item、result_item、input_item等不同名称。
emaki.action
emaki.action 允许脚本继续调用 CoreLib 动作系统。
| 方法 | 返回 | 说明 |
|---|---|---|
run(actionId, arguments) | boolean | 执行一个动作 ID。 |
runLine(line) | boolean | 执行一行动作字符串。 |
示例:
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) | 错误。 |
示例:
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 | 从列表中随机选择一个元素。 |
示例:
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) | 删除值。 |
示例:
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。 |
示例:
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 页:
- EmakiForge JavaScript
- EmakiStrengthen JavaScript
- EmakiCooking JavaScript
- EmakiGem JavaScript
- EmakiSkills JavaScript
- EmakiItem JavaScript
- EmakiAttribute JavaScript
- EmakiLevel JavaScript
下面给出一个通用范式:脚本通过 emaki.context.placeholder(...) 读取触发模块传入的上下文,再用 emaki.player 反馈给玩家。
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;
}挂到业务模块动作列表中:
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")。
基础探针示例:
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 | 注册来源 | 说明 | 详细文档 |
|---|---|---|---|
attribute | EmakiAttribute | PDC 属性 payload、动态属性、伤害 Hook、伤害计算与施加。 | Attribute JavaScript |
strengthen | EmakiStrengthen | 可强化判断、强化状态摘要、概率规则与结果钩子。 | Strengthen JavaScript |
skills | EmakiSkills | 技能脚本动作注册表查询与自定义技能动作注册。 | Skills JavaScript |
items | EmakiItem | 物品定义查询、共享 item/components 快照、定义/工厂注册与上下文物品识别。 | Item JavaScript |
forge | EmakiForge | 锻造规则、结果钩子注册与就绪探针。 | Forge JavaScript |
cooking | EmakiCooking | 产物规则、完成钩子注册与就绪探针。 | Cooking JavaScript |
gem | EmakiGem | 镶嵌规则、套装加成注册与就绪探针。 | Gem JavaScript |
level | EmakiLevel | 等级经验读取、经验规则与升级钩子注册。 | 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。 |
示例:
function main(ctx) {
const player = emaki.server.player(emaki.player.uuid());
if (player.exists()) {
player.sendMessage("来自 JavaScript server API 的消息");
}
return true;
}极限模式示例:
function main(ctx) {
const Bukkit = emaki.server.type("org.bukkit.Bukkit");
emaki.logger.info("online=" + Bukkit.getOnlinePlayers().size());
return true;
}
type()权限接近 Java 插件能力,只应给可信脚本使用。
JavaScript 扩展脚本
除了 runjs 单次执行外,业务插件可以加载扩展脚本:
plugins/EmakiCoreLib/scripts/extensions/
├── global/
│ └── *.js
├── skills/
│ └── *.js
└── attribute/
└── *.js扩展脚本通常导出 register(api):
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_join、player_interact、entity_damage_by_entity的受控事件示例;默认禁用,需要手动改脚本开关。
业务插件也可以通过 CoreLib 的脚本仓库释放自己的示例脚本,例如 Skills 释放 scripts/extensions/skills/js_lightning_strike.js,Attribute 释放 scripts/extensions/attribute/js_fire_mastery.js 和 scripts/mythic/mythic_js_damage.js。这些文件的注册来源、参数和行为属于对应业务插件,完整说明应阅读对应插件页面。
默认释放不会覆盖服主已经修改过的脚本文件。
extensions/global 注册的 Action 会记录 owner/source,CoreLib 重载时只清理对应脚本来源,避免旧 JS Action 或 Placeholder 残留。
全局 Placeholder 示例:
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%。
受控事件监听示例:
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_interactplayer_joinentity_damage_by_entity
事件包装默认只暴露安全基础信息。event.cancel()、event.setCancelled(...)、event.setJoinMessage(...)、event.setDamage(...) 只在非 MONITOR 优先级下生效;MONITOR 监听器被视为只读。
安全机制
脚本系统默认更偏向安全:
| 配置 | 默认 | 说明 |
|---|---|---|
allow_host_class_lookup | false | 禁止查找 Java 类。 |
allow_io | false | 禁止脚本直接 IO。 |
allow_threads | false | 禁止创建线程。 |
allow_native_access | false | 禁止 native access。 |
allow_environment_access | false | 禁止环境变量访问。 |
denied_path_fragments | ..、:、\ | 防止路径逃逸。 |
denied_actions_from_script | runjs 等 | 防止脚本递归调用脚本。 |
max_action_depth | 3 | 限制脚本调用动作嵌套深度。 |
建议生产服保持默认安全配置。不要为了方便把 Host Class Lookup、IO、线程、Native Access 全部打开。
性能建议
- 不要在高频事件中执行复杂脚本,例如每 tick、每次移动、每次受击都做大量逻辑。
- 高频触发器必须加冷却、概率或条件。
- 脚本中不要写无限循环。
default_timeout_millis默认 1000ms,脚本应该远低于这个时间完成。- 复杂逻辑建议拆成多个简单脚本,方便定位问题。
调试方式
查看脚本加载
log_script_load: true 时,CoreLib 会在重载脚本仓库时输出加载数量。
查看脚本执行
临时开启:
debug:
log_script_execute: true可以看到脚本路径、函数和耗时。生产服不建议长期打开。
打印堆栈
临时开启:
debug:
print_stacktrace: true用于定位脚本异常。生产服排查完成后建议关闭。
最佳实践
- 脚本文件按模块分类,避免全部堆在根目录。
- 每个脚本只做一件明确的事。
- 给脚本返回清晰的
message,方便排错。 - 高风险动作先在测试服执行。
- 不要依赖显示名或 Lore 判断真实状态,优先使用 context、placeholder、PDC、Item Source 或模块 API。
- 生产服不要开启危险 GraalJS 权限。
线程安全
脚本通过 runjs 动作执行时运行在异步 IO 线程,不在主线程。直接调用 Bukkit API 会导致异常。
emaki.runSync(runnable)
将任务调度到主线程执行。如果已在主线程则直接执行。
function execute(context) {
emaki.runSync(() => {
// 这里可以安全调用 Bukkit API
const player = context.player();
player.setHealth(20);
});
return { success: true };
}emaki.runSyncAndWait(runnable)
将任务调度到主线程并返回 CompletableFuture<Void>,可用于异步脚本等待主线程结果。
function execute(context) {
const future = emaki.runSyncAndWait(() => {
context.player().sendMessage("Hello from main thread");
});
future.join(); // 等待主线程执行完成
return { success: true };
}如果脚本中需要读取或修改游戏状态(玩家、世界、实体等),必须通过
runSync或runSyncAndWait切换到主线程。
子 API 可用性
emaki 对象的各子 API 可能因配置关闭而为 null:
| 子 API | 控制配置 | 为 null 时 |
|---|---|---|
emaki.context | expose_context: false | 调用会抛出 NullPointerException。 |
emaki.player | expose_player: false | 同上。 |
emaki.item | expose_item: false | 同上。 |
emaki.action | expose_action: false | 同上。 |
建议在脚本中使用前检查子 API 是否存在:
if (emaki.player) { ... }