Skip to content
kzheart edited this page Oct 18, 2026 · 22 revisions

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(...) 包装。
  • 命名空间防止不同插件或业务域的语句冲突;脚本可通过上下文调整优先搜索的命名空间。
  • 不要让解析器接受未经限制的类名、文件路径或命令模板。 自定义动作应把字符串输入转换为明确的领域参数,并在动作边界验证权限、数量和资源归属。

注册完整 Kether 解析器

需要读取嵌套 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

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 和后续语句使用宿主续接执行器;换行、分号和代码块边界保留。

case

  • 输入只求值一次,按顺序选择第一个满足条件的分支,只执行选中的分支体。
  • 无匹配且无 else 时返回 null。
  • 分支分隔符为 -> 或 then。省略比较符时,按推断类型后的相等判断。
  • 条件可写为 [ 条件一 条件二 ]:普通比较对各条件取“或”;in / contains 的多条件组成列表。
  • 比较符与 check 相同(= 别名除外),见比较符。

calc 与 invoke

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 与 papi

写法 行为
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、trim
    Map @键、size、keys、values
    List 与数组 下标、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 操作时,用主线程执行器。

作用域拥有的执行与可选 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

  • all [ ... ] 和 any [ ... ] 消费完整动作树,支持文字、check、权限、自定义动作和嵌套组合。
  • 保持按列表顺序求值全部输入的既有策略,不新增短路副作用变化。
  • 空组结果分别为 true 和 false。
  • 嵌套 player 动作用原生 ScriptSenderQuery 和 PlayerQuery 检查,自定义宿主需要同时安装这两项;BukkitScriptHost 已提供。

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,仍须回到主线程。

自带 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 对象仍可被脚本调用。

显式旧配置 case 兼容

兼容旧配置时,在同一个注册表上调用:

KetherCompatibility.installLegacyCases(context().scope(), statements);
  • 允许 case 的 else -> value,也保留默认支持的 else value。
  • 针对旧显示映射,允许一行分支结果中的连续大写文字标签,例如 VERY SLOW。注册过的动作、带引号字符串、嵌套表达式和注释不会按标签合并;新配置仍推荐给多词文字加引号。
  • 注册由作用域持有,释放后恢复默认 case 解析器。
  • 它不打开全局容错。需要宽松字面量解析时仍显式选择 toleranceParser=true。
  • 宽松模式下未知独立词元可能成为合法文字,已注册动作内部的语法错误仍失败。数字文字通常保留字符串,需要数值结果时用 math、type 或传入有类型的变量。不要把宽松模式当成拼写检查器。

Kotlin 挂起执行

挂起入口与 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。
  • 默认严格解析:未注册的词元会报错,需要宽松字面量行为时显式开启容错。

相关页面

  • Core:Scope、调度与回主线程
  • 构建与打包:模块选择与重定位
  • 仓库中的 klib-script 测试覆盖内置动作、异步续接、错误定位、编译缓存与扩展解析器,可作为扩展语句行为的事实来源。

Clone this wiki locally