Repository navigation
Script
Klib 的脚本运行时,保持 Java 8 字节码边界并由 Kotlin 实现:把条件、奖励和流程编排放进配置,再通过受控的宿主服务连接消息、命令、权限和占位符。
| 模块名 | 制品 | 自动带入 |
|---|---|---|
script |
me.kzheart.klib:klib-script |
core |
- 配置脚本由插件管理员维护。
- JEXL 表达式可以访问显式放入变量的对象。
klib {
modules {
script()
}
relocate("org.apache.commons.jexl3", "jexl3")
relocate("org.apache.commons.logging", "commonslogging")
}依赖、直接引用制品和打包方式见 构建与打包。
打包要点:
- 脚本解析核心已经包含在
klib-script中,普通执行无需额外脚本插件。 -
calc/invoke通过传递运行时依赖使用 Apache Commons JEXL 与 Commons Logging。构建插件会打包它们,请用上面的显式重定位避免与同服插件冲突。 - 手动打包也必须包含并重定位这两个依赖。
最小示例:先创建注册表和引擎,再为每次执行建立隔离的 ScriptContext。
import me.kzheart.klib.script.KetherScriptEngine;
import me.kzheart.klib.script.ScriptContext;
import me.kzheart.klib.script.StatementRegistry;
StatementRegistry statements = new StatementRegistry();
KetherScriptEngine engine = new KetherScriptEngine(statements);
ScriptContext context = ScriptContext.builder()
.sender(player)
.variable("level", Integer.valueOf(12))
.build();
engine.evalCondition("gte &level 10", context)
.thenAccept(allowed -> {
if (allowed.booleanValue()) {
// 纯 Java 后续处理;触碰 Bukkit 状态前仍要确认所在线程
}
});- 变量在一次上下文中可读写;命名空间默认按
klib、global搜索。 - 引擎会安装变量、比较、逻辑、算术、条件、列表和延迟等内置语句。
-
tell、command、papi、perm等语句只有在上下文中提供相应宿主服务时才能运行,见向脚本暴露受控能力。 - 脚本含
delay、wait等异步动作时,单参数构造器不够用,见异步动作与续接线程。
| 方法 | 作用 | 编译失败 | 执行失败 |
|---|---|---|---|
eval(source, context) |
返回最后一个动作的结果 | 放入返回的阶段 | 放入返回的阶段 |
evalCondition(source, context) |
把布尔值、数字、文本和 null 转为条件结果 |
放入返回的阶段 | 放入返回的阶段 |
evalChecked(source, context) |
进入业务流程前同步处理编译失败 | 立即抛出本地化 ScriptException
|
通过返回的 CompletionStage 传播(包括立即失败) |
validate(source, context) |
只检查语法,不开始执行 | 同步抛出 ScriptException
|
不执行 |
-
validate在KetherScriptEngine和ScopedScriptRuntime上都有。它复用编译缓存,编译所有具名块;不注入 guard、不调用动作或宿主服务,重复的def main也不会绕过预检。 -
inline、JavaScript 或其他动作在运行时才生成的字符串脚本,仍需要执行测试。静态编译不等于验证了运行时结果。 -
evalChecked与eval共用编译缓存,每次调用只执行一次脚本,不会先预检再重新解析。
默认 sender(...) 固定发送者。需要脚本内切换执行者时,显式指定发送者变量:
ScriptContext.builder().senderVariable("actor").variable("actor", player).build()- 每次
sender()读取当前帧可见的actor。原生动作修改变量会立即影响后续宿主服务。 - 变量缺失、被移除或显式为 null 都表示无发送者,不回退到
sender(...),也不会替调用者初始化变量。 - 命名空间派生视图保留此设置。宿主服务仍负责检查接收者类型和访问线程。
通过 ScriptContext.Builder.service(...) 安装最小能力,不要把插件主类或数据库连接直接放进变量:
ScriptContext context = ScriptContext.builder()
.sender(player)
.service(MessageSink.class, (sender, message) ->
player.sendMessage(message))
.build();
engine.eval("tell 任务已完成", context);- 可选宿主接口:
MessageSink、CommandSink、PlaceholderResolver、PlayerQuery、DelayScheduler、ScriptSenderQuery、ScriptPropertyAccess。 - 只安装当前脚本确实需要的能力。
- 脚本缺少服务时会以清晰异常失败,不会静默跳过动作。
- Bukkit 插件可以一次装好默认实现,见游戏语句的宿主服务和
BukkitScriptHost。
自定义语句必须归属于 Scope,配置重建或插件关闭时自动注销:
statements.register(scope, "shop", "announce", Statements.combine()
.remaining("message")
.execute((arguments, context) -> {
String message = arguments.require("message");
context.requireService(MessageSink.class).send(
context.sender().orElse(null),
message);
return CompletableFuture.<Object>completedFuture(message);
}));-
Statements.combine()支持必需参数、带默认值的可选参数和最后一个剩余文本参数。 - 动作返回
CompletionStage<Object>,同步结果用CompletableFuture.completedFuture(...)包装。 - 命名空间防止不同插件或业务域的语句冲突;脚本可通过上下文调整优先搜索的命名空间。
- 不要让解析器接受未经限制的类名、文件路径或命令模板。 自定义动作应把字符串输入转换为明确的领域参数,并在动作边界验证权限、数量和资源归属。
需要读取嵌套 action、关键字或自定义语法时,用 registerKether(...) 直接注册完整的 Kether QuestReader parser。它仍由 Scope 管理,但默认只在当前 Klib 运行时可见:
// 注意:这些类型来自 kether.core 包,与 me.kzheart.klib.script.QuestActionParser 同名但不同签名,
// 按下面的 import 选定完整语法解析器,正文使用简单类名。
import me.kzheart.klib.script.kether.core.ParsedAction;
import me.kzheart.klib.script.kether.core.QuestAction;
import me.kzheart.klib.script.kether.core.QuestActionParser;
import me.kzheart.klib.script.kether.core.QuestContext;
statements.registerKether(scope, "myplugin", "twice", QuestActionParser.of(reader -> {
ParsedAction<?> nested = reader.nextAction();
return new QuestAction<Object>() {
@Override
public CompletableFuture<Object> process(QuestContext.Frame frame) {
return frame.newFrame(nested).run()
.thenCompose(first -> frame.newFrame(nested).run());
}
};
}));-
同名不同类型:
registerKether(...)只接受me.kzheart.klib.script.kether.core.QuestActionParser。me.kzheart.klib.script.QuestActionParser是Statements系列使用的execute(StatementCall, ScriptContext)接口,两者不能混用。同一文件确实需要两种接口时才写限定名。 -
kether.core包里只有QuestActionParser、QuestReader、ParsedAction、QuestAction、QuestContext属于对外使用面。其余是解析器内部实现,不要依赖,也不要假定结构稳定。 - 这是需要完整 Kether 语法时的低层入口;普通固定参数业务语句仍优先用
Statements.combine()。 -
QuestReader.nextValue()接受动作或字面量参数;nextParsedAction()保持严格动作解析。二者都不会吞掉已识别动作内部的错误。 - 自定义读取器需要返回未加引号的词元时,用
QuestAction.unquotedLiteral(token);容错块内仍检查后续独立动作导致的孤立字面量错误,无需引用内部字面量实现。
tell colored inline *"&aLevel up! &f{{ &level }}!"
case &level [
when <= 15 -> calc "level * 2 + 7"
when <= 30 -> calc "level * 5 - 38"
else calc "level * 9 - 158"
]
-
tell、colored/color、inline/function的参数可以是:文字、&变量、*字面量、代码块或另一个动作。 - 带引号的参数始终是文字。已注册动作的语法错误会抛出;独立位置的未知语句仍然报错。
-
colored支持传统&颜色和格式代码、&#RRGGBB与&x&R&R&G&G&B&B。 -
inline顺序执行{{ 脚本 }},并保留${name}与{{ name }}变量简写。 - 嵌套脚本共用求值深度限制,错误会向外传播。
command 在调用 CommandSink 前求值命令参数,可接 inline、&变量、*字面量、引号文本、代码块或已注册动作:
command inline *"scoreboard players add Alex rewards {{ &level }}" as console
command &commandText
command inline "say {{ calc 'level + 1' }}" as console
-
as console向宿主传递空 sender;省略时使用上下文 sender。 - 旧写法
command say ready仍可用。完整命令建议加引号,避免首词与脚本动作同名。 - 异步命令参数完成后,dispatch 和后续语句使用宿主续接执行器;换行、分号和代码块边界保留。
- 输入只求值一次,按顺序选择第一个满足条件的分支,只执行选中的分支体。
- 无匹配且无
else时返回null。 - 分支分隔符为
->或then。省略比较符时,按推断类型后的相等判断。 - 条件可写为
[ 条件一 条件二 ]:普通比较对各条件取“或”;in/contains的多条件组成列表。 - 比较符与
check相同(=别名除外),见比较符。
calc / calculate 使用 JEXL 表达式,invoke 使用 JEXL 脚本:
calc dynamic inline "{{ &level }} * 2"
invoke "var doubled = level * 2; return doubled;"
- 静态表达式在解析阶段编译;
dynamic后接嵌套动作,运行时把结果编译求值。 - JEXL 读取当前帧与
ScriptContext的变量,不自动注入服务器、控制台或插件对象。 -
invoke中的局部赋值不写回ScriptContext。 - 异步参数完成后的消息和后续动作通过引擎指定的 continuation executor 执行。默认构造器遇到异步续接会明确失败,宿主必须按异步动作与续接线程注入正确的线程调度。
通用数值语句逐个执行嵌套动作,既有 add/sub/mul/div 仍保留:
math add [ math div [ &size 5 ] 2 ]
math 10 + 2 * 3
set chooseA to round random 10
permission admin
sender
| 语句 | 规则 |
|---|---|
math |
支持列表式 add/sub/mul/div(或 +、-、*、/)及中缀左结合,不采用乘除优先级。所有操作数可转为 Int 时返回整数,整数除法截断;否则用 Double。列表的操作数异常打印错误并取 0;中缀首值错误向调用方传播 |
round |
返回饱和边界的 Int,半数向正无穷方向取整,NaN 抛错 |
random N |
对 Int 返回 [0,N)
|
random A to B |
整数边界两端包含、自动排序;Double 使用半开区间;相等边界返回该值 |
random 集合/数组 |
随机选元素;空集合/空数组返回 null;非空的 null 元素保留原错误行为 |
permission |
接嵌套参数,用 ScriptSenderQuery.isPlayer 确认玩家,再通过 PlayerQuery.hasPermission 查询;其他发送者失败 |
sender |
对玩家用 ScriptSenderQuery.name;对 null 及其他非玩家按原语义返回 console
|
perm |
保留原有宿主查询行为,不因新动作改变 |
set 赋值:
-
set key to 动作:顺序求值,把原始结果写入变量,动作自身返回 null。旧set key value的类型推断与返回值保留。 -
set property key from 动作 to 动作与set &object[key] to 动作:先交给宿主ScriptPropertyAccess;未支持时用内置的 Map(@键)、List 与数组(下标)写入,不自动反射任意字段。 - 不支持的属性写入抛错。赋值值表达式失败时打印错误并完成 null。
- 属性读取见内置语句。
随机区间不对多行文本执行公共缩进清理;随机动作求值失败时传播异常,不留下永远不结束的 Future。
check 读取两个嵌套动作,按左、右顺序执行,再比较结果:
check &game not null
check &choose is null
check player level < 10
check papi "%wealth_level%" >= 5
check math 2 + 3 == 5
| 写法 | 含义 |
|---|---|
== / is(check 另保留 =) |
相等,推断字符串中的数字及布尔值;两个 Number 以 double 比较 |
!= / !is / not
|
不相等 |
=! / is!
|
不推断类型的相等 |
=!! / is!!
|
同一对象(身份) |
=? / is?
|
文本忽略大小写 |
> / gt、>= / gte、< / lt、<= / lte
|
排序比较,将操作数转换为数值 |
contains / has、in
|
包含:集合/对象数组查元素,Map 查键,其余值转字符串;不自动展开原始类型数组 |
- 排序比较不能当作 BigDecimal 精确比较或字典序排序。
- 独立
true和false在容错字面量解析时返回 String,null返回真实 null。不要把字面量文本的 Java 类型误写成 Boolean。
| 写法 | 行为 |
|---|---|
player 属性 |
用 ScriptSenderQuery.isPlayer 确认玩家,再调用 PlayerQuery.property;null 结果视为不可读 |
player 属性 to|add|increase|+|sub|decrease|- 值 |
调用 PlayerQuery.write,返回 false 时报“不可写” |
papi / placeholder
|
接一个完整嵌套动作,再调用 PlaceholderResolver.resolve;要求真实玩家语义的发送者查询服务 |
player 属性名:
- 按玩家操作名表匹配多词名称(如
block x、bed spawn x、on ground、display name),取最长的完整匹配。 - 不在表中的单个词元按宿主自定义属性传入。名称以小写、空格分隔传给宿主。
-
=仍留给外围check的相等别名。独立player level = 5解析失败,不会改变玩家。
papi:
- 多词文本须用引号,如
papi "hi %name%"。null 输入转为空串,其他值去除公共缩进。 - 输入 Future 或首次展开失败时打印错误,再以空串展开一次;第二次失败则传播。
- 输入动作在返回 Future 之前同步抛错则直接传播,不进入这条重试链。
- 此入口要求玩家发送者,控制台不能直接展开。
异步参数完成后,通过 frame 的续接执行器调用宿主查询。宿主仍须负责 Bukkit 主线程检查和实际插件集成。
Klib 内置流程、集合、文本、时间与游戏语句。语法和执行规则如下,涉及服务器状态的语句仍需要对应宿主服务。
| 类别 | 语句 |
|---|---|
| 流程 |
wait/sleep 时长、exit/stop/terminate、pause、seq [ ... ]、repeat 次数 动作、call 块、goto 块、async、await、await_all [ ... ]、await_any [ ... ]、import/release 命名空间、optional 值 else 动作、pass、vars/variables
|
| 循环 |
for i in 值 then 动作、map i in 值 with 动作、while 条件 then 动作、break
|
| 集合 |
array [ ... ]/arr、size/length、arr-get/element/elem 下标 in 列表、arr-add 值 to 列表、arr-push/arr-add-first、arr-remove 值 in 列表、arr-remove-at、arr-take/arr-remove-first、arr-drop/arr-remove-last、arr-find 值 in 列表、mutable、shuffle、reverse、split 文本 [by 正则]、join [ ... ] [by 分隔符]、range 起 to 止 [step 步长]
|
| 文本与时间 |
uncolor/uncolored、scale/scaled、format 毫秒 [by 格式]、printed 文本 [by 分隔符]、match 文本 by 正则、time/date [as 格式]、day of year|month|week、year、month、hour、minute、second(复数形式同义)、log/print/info、warn/warning、error/severe
|
| 游戏 |
tell/send/message、actionbar、broadcast/bc、players、switch 玩家名|console、title 文本 [subtitle 文本] [by 淡入 停留 淡出]、subtitle、location/loc 世界 x y z [and yaw pitch]、sound 名称 [by 音量 音调]、stopsound、itemstack、material、scoreboard 内容、command 文本 [as player|op|console]、js/javascript/$
|
for p in players then {
tell inline "{{ &p }}"
}
set list to array [ *a *b *c ]
arr-add *d to &list
tell join [ &list[0] &list[size] ] by -
title "&a你好 @sender" subtitle "欢迎" by 10 40 10
command "give @sender diamond 1" as op
-
列表:列表语句就地修改变量中的列表(
array生成可变列表)。 -
循环:
for、map、while同步执行时以循环推进,次数多也不会耗尽调用栈;异步迭代通过续接执行器恢复。for/map遍历 Map 时额外提供键-key、键-value变量,结束后移除。 - exit:只设置退出状态。后续语句不再执行,脚本以最后的值正常完成,不报错。
- pause:返回永不完成的 Future,宿主应自行限制等待。
-
wait:通过
DelayScheduler等待;结束时发送者为已离线玩家(ScriptSenderQuery.isOnline)则停止脚本。 -
@sender:在
tell、actionbar、broadcast、title、command中替换为发送者名称;command ... as console替换为console。command文本会去除公共缩进。 -
switch:切换本次脚本后续语句的发送者;嵌套
inline求值不继承切换结果。 -
sound:省略
by时音量与音调为 0。名称以resource:开头时为资源包音效,否则把.换为_并转大写后按枚举查找。
属性读取:
-
写法:
&变量[键]、动作[键](如player[name])、get property 键 from 动作。 -
先交给宿主
ScriptPropertyAccess,再使用内置属性:类型 内置属性 String upper、lower、length、trimMap @键、size、keys、valuesList 与数组 下标、 size正则 Matcher 组号或组名 -
不支持时向
ScriptLogger警告并返回 null。
Klib 默认使用严格解析,以便在加载时发现拼写错误。确实需要将未注册词元作为字面量时,用四参数构造器显式开启容错:
KetherScriptEngine engine = new KetherScriptEngine(statements, null, mainThread, true);- 字面量词元是字符串,例如
array [ 1 2 ]得到["1", "2"]。 - 容错模式下引号括起的文本总是字面量,即使与语句同名。已注册动作内部的语法错误照常抛出。
- 以下也是 Klib 的内置语句名:
add、sub、mul、div、eq、ne、gt、gte、lt、lte、and、or、unset、list、namespace。当文字使用时要加引号或*前缀。
游戏语句通过以下服务访问服务器:
-
ScriptPlatform:在线玩家、广播、控制台、按名查找玩家、动作栏、标题、音效、坐标、材质与物品、侧边栏; -
PlayerQuery.write、CommandSink.dispatchAsOperator、ScriptLogger、JavaScriptEvaluator。
Bukkit 插件可直接安装默认实现:
ScriptContext context = BukkitScriptServices.apply(ScriptContext.builder().sender(player), plugin)
.service(PlaceholderResolver.class, (target, text) -> PlaceholderAPI.setPlaceholders((Player) target, text))
.build();BukkitScriptServices.apply 安装的内容:
| 服务 | 说明 |
|---|---|
MessageSink |
|
CommandSink |
含 as op
|
ScriptSenderQuery |
|
PlayerQuery |
玩家属性查询与写入,包含取值范围限制 |
DelayScheduler |
按 50 毫秒一刻向下取整 |
ScriptLogger |
插件日志 |
ScriptPlatform |
|
| ItemStack/ItemMeta 属性 | |
JavaScriptEvaluator |
仅在服务端存在 JavaScript 引擎时 |
-
PlaceholderResolver依赖 PlaceholderAPI,仍由宿主提供。 -
scoreboard需要侧边栏实现,通过apply(builder, plugin, (player, lines) -> ...)传入,lines 为 null 表示移除;未传入时该语句报不支持。 - 所有服务必须在主线程调用,引擎的续接执行器应为主线程调度器。
- 旧版本中不存在的玩家属性(如
swimming、ping、pose)调用时报不支持。
展开:原生 parser 里如何访问 ScriptContext、变量快照与写回规则
原生 parser 中通过 ScriptFrames.context(frame) 访问 sender、locale、namespace 和已安装的服务,不用读取引擎内部变量:
ScriptContext host = ScriptFrames.context(frame);
host.setVariable("answer", Integer.valueOf(42));
return ScriptFrames.eval(frame, "tell inline *\"answer={{ &answer }}\"")
.toCompletableFuture();- 返回的
ScriptContext是当前帧的实时视图,不是脱离执行上下文的副本。 -
setVariable依原生规则:非~变量写到根帧,~局部变量留在当前帧。removeVariable移除最近一层可见定义。 -
~klib:内部键不可读写。 -
ScriptFrames.variables(frame)返回父层在前、近层覆盖的只读原始变量快照,包含显式 null 或QuestFuture。 - 读取延迟变量的结果用
host.variable(...);未就绪时保持原生错误,不阻塞。 - 近层覆盖是通用优先级规则。业务需要其他历史枚举顺序时,应明确自行组合帧。
变量共享与写回:
- 普通动作与原生动作即时共享同一帧变量。
- 嵌套求值和整个脚本结束时,把变量差量写回调用上下文,包括失败前已经发生的修改。
- 没有改变的键不会覆盖调用方同时新增或修改的值。
- 宿主构建器可接收 null,原生显式 null 在快照与最终上下文中保留。既有
setVariable(name, null)仍表示删除,variable(...)对 null 返回空 Optional。
帧视图的线程和有效期与动作相同:不要留存到执行结束后,也不要当成线程安全的独立上下文。异步参数需要在引擎的 continuation executor 上继续访问帧和宿主服务。
需要同步生成文本时,显式使用 ScriptTemplates.renderImmediate:
String message = ScriptTemplates.renderImmediate(
"value={{ &value }}", source -> engine.eval(source, context));- 从最内层
{{...}}向外执行,每段保持原有空白;反斜线转义的定界符只还原为文字。 - 每段读取 Future 的
getNow(null):未完成或结果为 null 时立即插入字符串null,不等待、不取消,也不阻止异步动作继续发生。 - 已完成的失败或取消会抛出。是否捕获错误并返回业务默认文本由调用方决定。
- 替换结果中产生的模板继续解析;遇到无法匹配起始符的第一个结束符时停止。
- 不采用
inline的简单变量名捷径,也不改变inline原有的顺序等待行为。 - 各段共享还是隔离变量,由传入的 evaluator 决定。
单参数和双参数构造器只适合同步动作。脚本可能执行 delay 或自定义异步动作时,必须用三参数构造器提供续接执行器:
Executor mainThread = command -> plugin.getServer().getScheduler()
.runTask(plugin, command);
KetherScriptEngine engine = new KetherScriptEngine(
statements,
null,
mainThread);- 没有显式续接执行器时,异步动作会快速失败,产生
continuation-executor-required错误,避免后续脚本意外运行在数据库或网络线程。 - 执行器决定动作完成后脚本从哪里继续。脚本包含 Bukkit 操作时,用主线程执行器。
插件生命周期内会运行 wait、async、await 或其他异步动作时,推荐 ScopedScriptRuntime:
- 保留原有 Kether 解析和上下文接口,额外把原生执行上下文归属到
Scope。 - 不改变旧
KetherScriptEngine构造器的默认解析模式。
import me.kzheart.klib.script.BukkitScriptHost;
import me.kzheart.klib.script.KetherCompatibility;
import me.kzheart.klib.script.ScopedScriptRuntime;
import me.kzheart.klib.script.ScriptContext;
import me.kzheart.klib.script.StatementRegistry;
StatementRegistry statements = new StatementRegistry();
ScopedScriptRuntime scripts = new ScopedScriptRuntime(
context().scope(), statements, null, context().scope().syncExecutor(), true);
BukkitScriptHost host = BukkitScriptHost.builder(this, context().scope())
.scoreboard(true)
.build();
ScriptContext scriptContext = host.apply(ScriptContext.builder().sender(player)).build();
scripts.eval("tell ready\nwait 1s\ntell done", scriptContext);示例使用 KPlugin 无参 setup() 提供的 context();业务动作仍通过同一个注册表注册。
| 操作 | 效果 |
|---|---|
scripts.cancelPending() |
成功安装新配置后调用:旧脚本的等待和已排队续接不再操作旧配置。配置验证失败时不调用,即可保留原执行 |
close() |
随作用域释放。不要把关闭请求再投递到已经关闭的调度器 |
toCompletableFuture().cancel(false) |
终止对应的原生执行,不只是取消一层结果包装 |
- 取消直接在调用线程清理原生上下文。自定义帧清理动作会访问 Bukkit 时,也必须在主线程调用取消。
- 主脚本已经返回后,仍在运行的 detached
async子动作也由作用域持有,完成后释放。 -
await的失败与取消会传播到外层,避免成功回调没有执行而使结果永久挂起。
-
all [ ... ]和any [ ... ]消费完整动作树,支持文字、check、权限、自定义动作和嵌套组合。 - 保持按列表顺序求值全部输入的既有策略,不新增短路副作用变化。
- 空组结果分别为 true 和 false。
- 嵌套
player动作用原生ScriptSenderQuery和PlayerQuery检查,自定义宿主需要同时安装这两项;BukkitScriptHost已提供。
BukkitScriptHost 显式启用额外能力,复用一组带主线程和关闭检查的 Bukkit 服务;旧的 BukkitScriptServices.apply(...) 仍可使用。
| 选项 | 说明 |
|---|---|
scoreboard(true) |
首行为标题,随后最多 15 行,保留重复行和空行。null 或空列表移除。移除、退出或作用域关闭时,只有当前计分板仍属于该宿主才恢复先前计分板,不覆盖其他插件后来接管的计分板。文本长度遵循当前服务器 API 限制 |
scoreboard((player, lines) -> ...) |
接入插件已有的共享侧边栏(例如任务追踪),回调替代原生侧边栏;外部计分板及其释放策略仍由调用者管理 |
javascript(factory) |
第一次 js 动作才创建并复用求值器 |
javascriptEngine("nashorn") |
通过插件类加载器的 JSR-223 发现实现,缺少引擎时明确失败 |
- 未选择侧边栏或 JavaScript 时,不启用相应额外能力。
PlaceholderResolver仍由插件根据自己的可选插件策略安装。 - 宿主构建、实际服务调用和释放要求主线程。
-
apply可从任意线程调用:只把构造期冻结的服务引用安装到调用方独占的新ScriptContext.Builder,不访问 Bukkit、不创建 JavaScript 引擎,也不共享变量或发送者状态。不要在线程之间共享可变构建器。 - 同一宿主可供多个独立上下文并行安装。安装与关闭状态切换串行,关闭后安装失败;安装成功后宿主仍可能关闭,保留的服务引用也会拒绝调用。
- 只用字面量、局部变量和计算的即时纯表达式可在异步线程求值;一旦调用 Bukkit 服务(包括宿主属性访问)或宿主 JavaScript,仍须回到主线程。
展开:在现代 Java 服务器上显式传入 Nashorn
Klib 公共产物及运行时依赖保持 Java 8,不强制捆绑 Nashorn。现代 Java 服务器的业务插件可自行打包兼容其 Java 版本的引擎,并显式传入:
import me.kzheart.klib.script.ScriptJavaScriptEngines;
import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory;
BukkitScriptHost host = BukkitScriptHost.builder(this, context().scope())
.javascript(() -> ScriptJavaScriptEngines.using(
new NashornScriptEngineFactory().getScriptEngine()))
.build();-
NashornScriptEngineFactory来自业务插件选择的引擎依赖,不是 Klib API。 - 使用独立 Nashorn 时,应重定位
org.openjdk.nashorn与其org.objectweb.asm依赖。 -
ScriptJavaScriptEngines.using(...)为每次执行建立独立变量映射,但不深拷贝宿主对象,也不是不可信代码沙箱。脚本只能由可信插件管理员维护;变量中的 Java 对象仍可被脚本调用。
兼容旧配置时,在同一个注册表上调用:
KetherCompatibility.installLegacyCases(context().scope(), statements);- 允许
case的else -> value,也保留默认支持的else value。 - 针对旧显示映射,允许一行分支结果中的连续大写文字标签,例如
VERY SLOW。注册过的动作、带引号字符串、嵌套表达式和注释不会按标签合并;新配置仍推荐给多词文字加引号。 - 注册由作用域持有,释放后恢复默认 case 解析器。
- 它不打开全局容错。需要宽松字面量解析时仍显式选择
toleranceParser=true。 - 宽松模式下未知独立词元可能成为合法文字,已注册动作内部的语法错误仍失败。数字文字通常保留字符串,需要数值结果时用
math、type或传入有类型的变量。不要把宽松模式当成拼写检查器。
挂起入口与 Java 的 CompletionStage 调用同一引擎。编译缓存、解析器、帧/变量生命周期和异步续接由模块内的 Kotlin 实现承担;宿主只提供注册表、上下文与明确的服务能力。
import me.kzheart.klib.script.*
suspend fun evaluate(engine: ScriptEngine): Any? {
val context = scriptContext { variable("name", "Klib") }
return engine.evalAwait("literal hello", context)
}evalAwait 与 evalConditionAwait 等待原引擎结果,不改变解析器、初始求值线程或正常续接的执行器。
两参重载会在发起取消的线程取消结果 future 并清理原生帧。即使协程从主线程开始,withTimeout 或后台 Job.cancel() 也可能在后台清理;含 Bukkit 清理动作时不能依赖这个重载保证线程安全。
需要指定清理线程时,传入第三个参数 cancellationExecutor:
import java.util.concurrent.Executor
import kotlinx.coroutines.withTimeout
suspend fun evaluateOnHost(
engine: ScriptEngine,
context: ScriptContext,
hostCleanupExecutor: Executor,
): Any? = withTimeout(5_000) {
engine.evalAwait("wait 1s\nliteral done", context, hostCleanupExecutor)
}- 三参
evalAwait、evalConditionAwait只把取消请求投递到指定执行器;调用者仍须在合法线程开始求值,并为引擎提供正确的普通续接执行器。 - 执行器必须把已接收的任务真正执行完,且在原生清理结束前保持可用。取消请求实际执行前,原生脚本仍可能继续;协程完成取消也不表示排队的清理已完成。
- 投递被拒绝时,通过
CoroutineExceptionHandler报告取消处理失败,不会改在错误线程清理。此时须由ScopedScriptRuntime的拥有者在宿主线程调用cancelPending()或close()完成收尾。 -
Scope.syncExecutor()在作用域关闭后拒绝新任务,已排队任务也可能被关闭取消,不能单独承担可靠清理。关闭流程应在主线程先关闭ScopedScriptRuntime,再关闭调度器;不要把清理投到已经关闭的作用域。
- Java 的宿主 SPI、解析器 SAM 与开放的 Kether 扩展类型保留,兼容既有平台类型、checked exception、protected 字段和 getter/属性双写法;它们把实际执行委托给共享实现。
- Java 调用方继续使用静态工厂、Builder 和阶段结果,无需传
Continuation;Kotlin 挂起与取消仍服从上面的执行器约定。 - Guard 商品内的私有运行时与父加载器 Core 不是可任意交换的 Kotlin 类型空间。构建插件会拒绝不安全的跨边界类型,reflect/协程组合还受商品条目与 multi-release 限制;普通 Bukkit JAR 可运行不等于该组合已获 Guard 支持。
TabooLibKetherInterop 的容器出口同时保留 successful / value 字段与 Java getter;旧容器按字段读取结果,可空值和失败结果保持原语义。
- 注册语句的
Scope关闭后,该注册立即失效。引擎和注册表可由较长生命周期持有,业务语句应安装在可重建子作用域中。 -
ScriptContext的变量映射支持并发访问,但放入其中的可变对象不因此变成线程安全对象。 - 异步动作需要显式续接执行器;脚本含 Bukkit 操作时用主线程执行器。
- Bukkit 宿主服务的构建、调用和释放都在主线程。
-
eval(...)的失败会包装为带错误代码、行列位置和本地化消息的ScriptException。业务层应记录原因并向配置作者展示位置,不要只吞掉异常。 - 编译结果按脚本文本、命名空间和注册表版本缓存;注册或注销语句会让相关缓存失效。不要自行缓存底层 Kether
Quest。 - 脚本应设置来源、长度和业务复杂度上限。运行时已有动作数、嵌套深度和缓存大小保护,但不能代替调用方的权限与资源限制。
- 默认构造器遇到异步续接会直接失败(
continuation-executor-required),不是 bug。 - 默认严格解析:未注册的词元会报错,需要宽松字面量行为时显式开启容错。