服务端通用脚本与 Cron
服务端通用脚本把一段 ChaUI Script 放在页面之外运行。它适合封装跨页面复用的命令、奖励入口、公告和定时任务,也可以由命令、页面、其他插件 API 或 Cron 启动。
通用脚本没有页面上下文:它不能查找或修改组件,不能访问当前页面会话、容器绑定或 原版.* 客户端状态。相对地,它不依赖某张页面保持打开,页面关闭后也不会因此丢失一项已经独立启动的服务端工作。
先分清三种脚本
| 类型 | 配置位置 | 运行上下文 | 可以操作组件吗 | 典型入口 |
|---|---|---|---|---|
| 页面事件脚本 | 页面 yml 的 events.* | 当前页面和触发组件 | 可以 | 点击、打开、关闭、输入 |
| 页面内自定义方法 | 页面 yml 的 methods | 调用它的当前页面 | 可以 | methods.refresh() / 方法.刷新() |
| 服务端通用方法 | plugins/ChaUI/global-scripts.yml | 一次独立服务端执行,可选目标玩家 | 不可以 | 命令、页面字面量调用、Java API、Cron |
如果逻辑必须读取 component("id")、页面 vars 或 原版.设置.*,它属于页面脚本;如果逻辑应脱离页面运行,再定义为服务端通用方法。
第一个可运行方法
打开 plugins/ChaUI/global-scripts.yml,完整写入:
methods:
notify_player:
# 参数按这里的顺序传入。
parameters:
- name: message
type: string
script: |
// {player} 会替换为本次目标玩家名。
// {args.message} 来自上面声明的只读参数。
consolecmd("tell {player} [ChaUI] {args.message}")
# 暂时不配置定时任务时,可以保留空映射,方便以后添加。
crons: {}然后执行:
/chaui reload
/chaui script Steve notify_player 欢迎回来把 Steve 换成当前在线玩家名。执行成功后,玩家会收到 [ChaUI] 欢迎回来,命令发送者会得到一个唯一执行 ID。
这个方法没有周期任务,会在命令返回前同步完成,所以随后用该 ID 停止可能提示“执行不存在”。这是正常结果;需要保持运行并演示停止,请继续下一节。
方法、参数和变量
每个方法包含三个部分:
methods:
announce_reward:
parameters:
- name: amount
type: number
- name: announce
type: boolean
script: |
vars.applied = true
consolecmd("say {player} 完成任务,奖励数量为 {args.amount}")
if args.announce {
consolecmd("tell {player} 奖励记录已生成")
}
crons: {}参数只支持三种类型:
| 类型 | 配置值 | 命令参数示例 |
|---|---|---|
| 字符串 | string | 欢迎回来 |
| 有限数字 | number | 10、0.5 |
| 布尔值 | boolean | true、false |
参数数量和顺序必须与 parameters 完全一致。命令行中每个空格分隔项对应一个参数,因此一个字符串参数最好使用单个词;需要长文案时可在方法脚本中组合固定文字。
方法 ID 最长 64 个 Unicode 字符,方法 ID 与参数名都必须以 Unicode 字母或下划线开头,后续只能包含 Unicode 字母、数字或下划线。不要使用空格、连字符、点号或路径分隔符;同一方法内的参数名不能重复。
args.* 是只读参数
args.amount、args.announce 只能读取,不能赋值。字符串动作中也可以使用 {args.amount} 插值。
vars.* 是本次执行的临时变量
vars.applied、vars.count 可以在本次方法及其周期回调中读写。不同玩家、不同命令和不同 Cron 触发各有独立变量,不会写回 global-scripts.yml,也不会跨执行共享。嵌套调用的每个通用方法也有自己的 args.* 与 vars.*,不会直接继承或共享调用者的临时变量;需要的数据必须作为参数显式传入。
通用方法没有返回值。需要拆分逻辑时,一个方法可以调用另一个方法:
global("notify_player", "任务开始")
调用通用方法("notify_player", "任务完成")方法 ID 必须写成字符串字面量,参数数量和类型会在加载时检查。包含根方法在内,一条调用链最多进入 8 个方法;递归或相互调用超过深度会终止整个根执行,并报告完整方法链。
timer、interval 与执行 ID
只要根执行仍有活动周期任务,执行 ID 就保持有效。下面的方法同时创建一个按 tick 和一个按毫秒运行的计数器:
methods:
repeating_state:
parameters:
- name: start
type: number
script: |
vars.tickCount = args.start
vars.millisecondCount = args.start
// 每 20 tick 执行一次,通常约为 1 秒。
timer("tick_counter", 20) {
vars.tickCount = vars.tickCount + 1
}
// 每 1000 毫秒执行一次。
interval("millisecond_counter", 1000) {
vars.millisecondCount = vars.millisecondCount + 1
}
crons: {}重载并启动:
/chaui reload
/chaui script Steve repeating_state 0复制返回的执行 ID,例如 c4a72b67-...,然后停止:
/chaui script list
/chaui script stop <执行ID>script list 只显示当前仍有活动计时任务的根执行。停止根执行会一起取消它的子方法、timer、interval 和其他已登记的延时任务。再次停止同一个 ID 会返回“未找到”,不会影响其他执行。
周期任务还可以在脚本内按名称取消:
stopTimer("tick_counter")
cancel("millisecond_counter")周期块的重要边界
timer(name, ticks)与interval(name, milliseconds)都是周期执行,直到取消或根执行结束;- 同一执行中同名任务会替换旧任务;
timer间隔为1..72000tick,interval为1..3600000毫秒;- 单次执行最多同时保留 32 个活动计时任务,最多执行 4096 个脚本节点;
repeat、timer和interval内不能再嵌套周期结构;- 为避免重复放大高权限操作,周期块与
repeat块不能执行玩家命令、OP 命令、控制台命令、打开页面等高权限动作,也不能绕过限制间接调用包含这些动作的方法。
因此上面的周期示例只维护执行内状态。需要按固定时间发公告时,应使用本页后面的 Cron,每次触发一个独立、可审计的方法执行。
四种启动入口
1. 管理命令
/chaui script <玩家> <方法> [参数...]
/chaui script list
/chaui script stop <执行ID>目标玩家必须在线。命令会先为目标玩家把每个字符串参数中的 PAPI 更新一次,再按方法声明把参数解析为 string、number 或 boolean。成功时总会返回根执行 ID,即使方法随后同步完成。script list 可查看页面、命令、Java API 和 Cron 创建的全部当前活动根执行;完整指令说明见插件指令。
2. 页面脚本
页面可以启动一个独立的服务端通用方法:
global("notify_player", "按钮已触发")
调用通用方法("announce_reward", 10, true)页面调用只接受字符串、有限数字、布尔值三类字面量,不能传 vars.amount、组件属性、实时公式或动态方法 ID,null 同样不接受。调用成功后页面脚本不会等待通用方法完成,也不会把根执行 ID 返回到页面变量;管理员可以用 /chaui script list 找到仍在运行的页面执行,其中来源显示为 page:<页面ID>。
字符串字面量中的 PlaceholderAPI 占位符可以在服务端调用开始时解析一次,但页面变量不是 PAPI 占位符,不能借此传入。
3. Java API
import com.github.ginirohikocha.chaui.api.ChaUIAPI;
import com.github.ginirohikocha.chaui.api.GlobalScriptExecutionInfo;
import java.util.List;
String executionId = ChaUIAPI.executeGlobalMethod(
player, "reward", 10, true);
List<GlobalScriptExecutionInfo> active = ChaUIAPI.activeGlobalScripts();
boolean stopped = ChaUIAPI.stopGlobalScript(executionId);executeGlobalMethod 与 stopGlobalScript 必须在 Bukkit 主线程调用。传入玩家时,该玩家必须在线;参数数量和 Java 类型按声明严格校验:string 传 String,number 传有限的 Number,boolean 传 Boolean,不会把任意对象或字符串自动转换成另一种类型。executeGlobalMethod 返回根执行 ID,stopGlobalScript 只在找到仍活动的执行时返回 true。
activeGlobalScripts() 返回当前活动根执行的只读快照。每项 GlobalScriptExecutionInfo 包含 executionId、根 methodId、source、sourceId、playerId 与 playerName;控制台执行的两个玩家字段为 null。sourceId 在 API 与命令来源中是根方法 ID,在页面来源中是真实页面 ID,在 Cron 来源中是 Cron 配置 ID。
插件可以为纯控制台方法传入 null 玩家,但此时玩家命令、打开页面、{player} 和玩家 PAPI 都没有可用上下文。API 表格见API 方法表。
4. Cron
Cron 在设定的日期和时间自动启动通用方法。每次触发都是新的根执行,内部拥有独立执行 ID、参数快照和临时变量。只要 Cron 执行仍有活动计时任务,它就会出现在 /chaui script list 中,来源显示为 cron:<Cron ID>;复制该执行 ID 后即可用 /chaui script stop 强制停止。
PAPI:只在调用开始时更新一次
安装并启用 PlaceholderAPI 后,命令、Java API、页面字面量调用和 online_players Cron 都会在根方法启动时解析字符串参数中的 PlaceholderAPI 一次:
/chaui script Steve notify_player 在线人数=%server_online%如果启动时 %server_online% 是 12,方法得到的 args.message 就是 在线人数=12。即使方法的 timer 或 interval 运行期间在线人数变成 15,回调仍使用启动时的快照,不会像页面实时 PAPI 文本一样刷新。未安装或未启用 PlaceholderAPI 时,玩家目标的字符串会保留原始 %...% 文本,不会凭空解析。
嵌套通用方法也只接收调用者已经得到的值,不会再次解析一遍。target: console 没有玩家上下文,不能使用 %player_name% 等玩家占位符。
Cron 表达式
Cron 是一张“什么时候执行”的时间表。ChaUI 支持 5 段和 6 段数字表达式:
| 段数 | 顺序 | 示例 | 含义 |
|---|---|---|---|
| 5 段 | 分 时 日 月 星期 | */5 * * * * | 每 5 分钟 |
| 5 段 | 分 时 日 月 星期 | 0 12 * * * | 每天 12:00 |
| 6 段 | 秒 分 时 日 月 星期 | */10 * * * * * | 每 10 秒 |
| 6 段 | 秒 分 时 日 月 星期 | 0 0 12 * * * | 每天 12:00:00 |
字段范围:秒 0..59、分 0..59、时 0..23、日 1..31、月 1..12、星期 0..7,其中 0 和 7 都表示星期日。
每一段支持:
*:任意值;*/5:按步长;1,15,30:多个值;9-17:连续范围;0-50/10:范围内按步长。
不支持月份英文名、星期英文名、?、L、W、# 或第 7 段年份。日期和星期同时受限时,匹配其中任一条件即可触发;不想产生歧义时,把其中一项写 *。
Cron 配置字段
crons:
online_notice:
enabled: true
schedule: "*/5 * * * *"
timezone: "Asia/Shanghai"
target: online_players
method: notify_player
args:
- "欢迎 %player_name%"| 字段 | 类型 | 说明 |
|---|---|---|
enabled | 布尔值 | 缺省为 true;设为 false 时保留配置但不调度 |
schedule | 字符串 | 5 段或 6 段 Cron 表达式 |
timezone | 字符串 | IANA 时区,例如 Asia/Shanghai;省略时使用服务器时区 |
target | 枚举 | online_players 或 console |
method | 字符串 | 已通过校验的通用方法 ID |
args | 列表 | 按方法参数顺序提供,数量和 YAML 值类型必须匹配 |
online_players
触发时为每位在线玩家创建独立执行。每名玩家分别解析一次字符串 PAPI,某名玩家解析或执行失败不会阻止其他在线玩家。
同一个 Cron 对同一名玩家的上一执行仍在运行时,本次只跳过该玩家;不会阻止该 Cron 为其他玩家执行。
console
每次触发只创建一个没有玩家上下文的执行。适合 consolecmd 公告和不依赖玩家的服务端动作,不能使用玩家命令或打开玩家页面。控制台执行不会解析任何 PlaceholderAPI 占位符;参数中只要出现 %...%,本次执行就会被明确拒绝,而不是把未解析文本继续传给方法。
一份完整可复制的配置
下面的文件同时包含手动通知、可停止周期执行,以及在线玩家和控制台两种 Cron。可直接保存为 plugins/ChaUI/global-scripts.yml:
methods:
notify_player:
parameters:
- name: message
type: string
script: |
// 目标玩家收到一次消息。
consolecmd("tell {player} [ChaUI] {args.message}")
console_announcement:
parameters:
- name: message
type: string
script: |
// 不依赖玩家,可供 console Cron 使用。
consolecmd("say [ChaUI] {args.message}")
repeating_state:
parameters:
- name: start
type: number
script: |
// 用于演示执行 ID 和强制停止;周期块只维护本次执行变量。
vars.tickCount = args.start
vars.millisecondCount = args.start
timer("tick_counter", 100) {
vars.tickCount = vars.tickCount + 1
}
interval("millisecond_counter", 10000) {
vars.millisecondCount = vars.millisecondCount + 1
}
crons:
online_notice:
enabled: true
schedule: "*/5 * * * *"
timezone: "Asia/Shanghai"
target: online_players
method: notify_player
args:
- "欢迎 %player_name%"
noon_console_announcement:
enabled: true
schedule: "0 0 12 * * *"
timezone: "Asia/Shanghai"
target: console
method: console_announcement
args:
- "每日中午公告"
ten_second_example:
# 这是 6 段表达式示例;默认关闭,避免测试服刷屏。
enabled: false
schedule: "*/10 * * * * *"
timezone: "Asia/Shanghai"
target: console
method: console_announcement
args:
- "十秒表达式测试"建议按以下顺序验收:
/chaui reload
/chaui script Steve notify_player 欢迎回来
/chaui script Steve repeating_state 0
/chaui script stop <repeating_state 返回的执行ID>随后把 ten_second_example.enabled 临时改为 true,reload 后观察 6 段表达式;确认后立即关闭,避免持续公告。
Cron 生命周期与并发规则
- 服务器停机期间错过的时间不会在启动后补跑;
- 同一 Cron 的同一目标仍有活动根执行时,跳过这一次触发;
online_players的每名玩家是独立目标,一人失败或重叠不影响其他人;- 玩家离线时,属于该玩家的通用脚本执行及周期任务停止;
- 插件禁用时,全部通用脚本执行和 Cron 调度停止;
/chaui reload只替换后续方法目录与 Cron 时间表,已经启动的执行继续使用启动时的方法快照,直到完成或被停止;- 配置中单个方法或 Cron 无效时,只跳过对应条目并记录原因;整个 YAML 无法读取时,继续使用上一份有效配置。
允许的动作与边界
通用方法可以使用条件、数学、args.*、vars.*、有界 repeat、timer / interval、方法互调,以及以下一次性动作:
cmd/指令:目标玩家执行命令;opcmd/管理员指令:以临时管理员身份执行;consolecmd/控制台指令:控制台执行;open/打开:为目标玩家打开已加载页面;openSub/打开子页面:为目标玩家打开子页面;global/调用通用方法:调用另一个通用方法。
它不能使用 component(...)、页面变量、页面生命周期、容器槽位、原版.* 设置或客户端本地菜单动作。方法没有目标玩家时,玩家相关动作不会获得一个虚构玩家上下文。
排错
| 现象 | 原因 | 修正 |
|---|---|---|
| reload 后提示方法被跳过 | 参数列表、脚本语法、动作名或调用链不合法 | 从日志中的 methods.<id> 首个原因修正;其他合法方法仍可加载 |
| 命令提示参数数量不对 | 命令 token 数与 parameters 不一致 | 按声明顺序逐个传入;长字符串改为单 token 或固定脚本文案 |
| 数字或布尔参数失败 | number 不是有限数字,或布尔值不是 true/false | 检查参数类型,不依赖自动字符串转换以外的猜测 |
| 页面调用被拒绝 | 使用了页面变量、公式、null 或动态方法 ID | 页面入口只传字符串、有限数字、布尔值三类字面量 |
| 周期块中的命令导致方法失效 | 重复结构禁止高权限动作 | 用 Cron 每次启动一次独立方法;周期块只维护受限执行状态 |
console Cron 的 PAPI 参数失败 | 控制台目标不解析任何 %...% 占位符 | 改为 online_players,或删除全部 PAPI 占位符 |
| Cron 不触发 | 段数、数字范围、时区或 enabled 错误 | 使用 5/6 段数字表达式和合法 IANA 时区 |
| 停止返回未找到 | ID 已完成、已停止、拼写错误或属于旧记录 | 只停止仍有周期任务的当前执行 ID |
| 方法互调突然终止 | 包含根方法在内超过 8 层 | 缩短调用链并移除递归环 |
| 到点没有重复启动 | 同一 Cron 的同一目标上次执行仍活动 | 停止旧 ID,或让方法在下次触发前自然结束 |
| reload 后旧执行行为没变 | 活动执行保留启动时方法快照 | 停止旧 ID,再启动新执行;这是防止运行中脚本被半途改写的正常行为 |
页面脚本的计时器、组件操作和生命周期仍请阅读循环与计时器与生命周期与自定义方法。
猹件开发组