脚本概览
AchieveMaster 现在这套脚本系统,已经是“统一编译入口 + 统一上下文 + 统一函数注册表”的结构,不再是早期那种靠零散正则去拼配置的实现。
如果你只先记住三件事,后面读文档会快很多:
- 当前正式主路线是 AST,旧写法只负责兼容归一,不再作为扩展中心。
script-js仍然保留,但它实际跑的是 AviatorScript 5.4.3,不是浏览器 JavaScript。- 触发脚本、奖励脚本、内置函数、动作函数、自定义函数、反射调用,最终都围绕
ScriptService、FunctionRegistry、ScriptContext这一组运行时工作。
入口分布
AchieveMaster 里真正会进入脚本系统的入口,主要有下面这些:
| 位置 | 格式 | 实际执行路线 | 说明 |
|---|---|---|---|
trigger.script / trigger.脚本 | AST / TQS 风格脚本 | LegacySyntaxNormalizer -> StatementParser -> AST -> AstScriptExecutor | 当前触发阶段的正式主入口 |
trigger.script-js / trigger.js | AviatorScript | JsScriptEngine | 如果和 trigger.script 同时存在,优先走 script-js |
rewards.script / rewards.脚本 | AST / TQS 风格脚本 | 同上 | 成就解锁后的奖励脚本 |
rewards.script-js / rewards.js | AviatorScript | JsScriptEngine | 如果和 rewards.script 同时存在,优先走 script-js |
scripts/functions/yml/ | YML DSL | 加载后注册为函数 | 适合做声明式包装函数 |
scripts/functions/tqs/ | AST / TQS 风格脚本 | 编译成 AST 后注册为函数 | 当前最自然的自定义函数写法 |
scripts/functions/java/ | Java 风格 DSL | 解析后注册为函数 | 兼容已有 Java 风格函数定义 |
scripts/functions/js/ | AviatorScript .js / .av | 由 Aviator 加载并注册 | 适合表达式型函数与桥接逻辑 |
同一个 script 字段本身还支持三种 YAML 载入方式:
- 单行字符串
- 多行
| - 列表格式,最终按换行拼接成一段脚本
执行链路
把一段脚本从配置写到服务器真正执行,中间大致会经过这几步:
ConfigManager从 YAML 里把脚本字段读出来。- 调用方根据入口决定走 AST 还是
script-js。 - AST 路线先做旧语法归一,再把语句解析成 AST,并进入脚本缓存。
- 运行时把
player、achievement、event、entity、item、成就进度、扩展数据等塞进ScriptContext。 - AST 执行得到
ScriptResult,Aviator 执行得到原始返回值并包装进ScriptResult.returnValue。 - 最后由“调用这个脚本的人”决定是否消费结果。
这最后一步很关键,因为 AchieveMaster 不是所有脚本入口都会把 ScriptResult 当成正式控制信号处理。
不同入口对结果的消费方式
这是当前最容易踩坑、也最值得先弄清楚的部分:
| 场景 | 引擎 | 谁消费结果 | 实际含义 |
|---|---|---|---|
触发阶段 trigger.script | AST | TriggerManager | 会消费 shouldContinue、当前成就进度变化、目标进度变化,并保存数据 |
触发阶段 trigger.script-js | AviatorScript | 触发主流程几乎不消费返回值 | return true/false 不会自动推动或阻止成就;要生效必须直接调动作函数 |
奖励阶段 rewards.script | AST | AchievementManager 当前不消费 ScriptResult | 动作型函数会直接执行,但 返回、停止脚本、设置进度 这类依赖结果回写的语义不会额外回流 |
奖励阶段 rewards.script-js | AviatorScript | 同上 | 返回值只保存在执行结果里,不会被奖励主流程继续解释 |
延迟执行 {} 代码块 | AST | DelayNode 自己消费延迟块结果 | 延迟块里的当前成就进度 / 目标进度改动会在后续 tick 回写,但它是异步执行,不属于当前调用栈 |
所以推荐的理解方式是:
- 触发脚本里,AST 的“当前成就语句”最适合做主流程推进。
- 奖励脚本里,重点放在直接副作用,例如发消息、执行命令、联动外部插件。
script-js里不要把return true/false当成主流程控制手段。
当前脚本系统的四层结构
从职责上看,这套脚本系统可以拆成四层:
1. 编译层
负责把配置里的文本脚本变成真正可执行的程序:
- AST 走
LegacySyntaxNormalizer和StatementParser script-js走JsScriptEngine- 编译结果进入
ScriptProgramCache
这也是为什么现在文档必须以 AST 正式语法为准。旧语法能跑,是因为兼容层会把它改写成现在的规范写法,不代表旧语法仍是主设计目标。
2. 上下文层
ScriptContext 负责给脚本提供运行时数据。常见来源包括:
- 当前玩家
- 当前成就
- 当前事件
- 当前实体 / 物品
- MythicMobs 数据
- 任务数据
- 成就进度对象和局部变量
AST 和 script-js 在少数变量上有语义差异,最典型的是:
- AST 里
progress默认是当前进度数值 script-js里progress是AchievementProgress对象
变量解析、别名归一、字段访问、循环变量、延迟执行上下文拷贝等细节,统一看 变量与上下文。
3. 函数与动作层
所有内置函数最终都会注册进同一个 FunctionRegistry。这意味着:
- AST 里的函数调用能用
- 自定义函数内部能用
script-js也能通过包装层复用- 动作型函数和读取型函数只是“用途不同”,底层不是两套系统
当前源码实际暴露的是 238 个唯一函数名,精确结果以服务器运行时 /ach functions 输出为准。
4. 扩展层
这一层负责把脚本系统和外部世界打通:
- 反射调用
- 导入类
new构造对象- DragonCore / PAPI / MythicMobs / AttributePlus 等集成函数
scripts/functions/自定义函数目录
AchieveMaster 这次脚本重构并没有砍掉反射能力,只是把它统一收进 AST 运行时,不再走旧解析器那套零散逻辑。
自定义函数的加载顺序
自定义函数并不是“谁后加载谁覆盖前者”。当前源码里的顺序是固定的:
ymltqsjavajs
注册中心对同名函数采用“先注册保留,后注册跳过”的规则,因此:
- 先加载的同名函数会保留
- 后加载的重名函数会跳过
- 别名冲突也会跳过,不会强行覆盖
如果你准备把一个旧函数逐步迁到 AST,要先确认是否还留着同名的 YML / Java / JS 版本。
什么时候该写脚本,什么时候不该写
不是所有成就都值得上大脚本。
更稳的分工一般是:
- 简单判定:优先用触发器条件、基础进度配置、现成内置函数
- 中等复杂度:在
trigger.script里写 AST 分支、循环、变量和动作 - 多处复用逻辑:抽成
scripts/functions/ - 跨插件、跨类深度联动:最后再上反射或特定集成函数
这样做的好处很直接:
- 成就配置本体更短
- 复用逻辑不需要复制粘贴
- 后面排查兼容问题时,能更快判断问题是在触发器、脚本、函数还是外部插件
运行时排查方式
源码能告诉你“理论支持什么”,但你服里真正注册成功了什么,最好用运行时命令直接看:
text
/ach functions
/ach functions 数学
/ach functions 发送消息
/ach triggers
/ach triggers REGION这些命令适合确认:
- 某个函数是否真的存在
- 函数分类、参数、别名、用法是不是和文档一致
- 当前服务器装的依赖插件有没有让集成功能真正可用
如果还要进一步排查脚本执行过程,再配合:
settings.debug: true- 控制台里的脚本编译错误 / 警告日志
- 自定义函数加载日志
迁移建议
如果你是从旧脚本或旧配置迁过来的,建议直接按下面的思路处理:
- 老配置先让兼容层托住,不要第一步就全服手工大改。
- 新写脚本一律按 AST 正式语法写,不再继续扩展旧关键字习惯。
script-js只在你明确需要 Aviator 表达式能力时再用。- 奖励脚本主要写直接副作用,不要依赖
返回或“当前成就 AST 进度语句”去驱动奖励主流程。 - 需要跨插件或反射时,先看有没有内置函数;实在没有,再进 反射与导入类。
推荐阅读顺序
最后一个结论
如果你只想知道“现在到底该怎么写”,答案其实很简单:
- 触发主逻辑优先写 AST
- 复用逻辑抽自定义函数
- 外部联动优先内置函数,其次反射
script-js当 Aviator 用,不要当浏览器 JS 用
按这个思路写,基本就不会再掉回旧解释器时代那种“看起来能跑、实际上语义不稳定”的坑里。