语法与表达式
这一页只讲一件事:AchieveMaster 当前正式脚本语法应该怎么写,以及每种写法在运行时到底会怎么执行。
结论先放前面:
- 规范写法是 AST 语法。
- 旧写法会被兼容层自动归一,但不建议继续按旧关键字扩展。
script-js不在本页范围内,它是另一套 AviatorScript 语法,见 script-js / AviatorScript。
基本规则
- 一行通常写一个语句。
- 多行脚本既支持 YAML 的
|块,也支持字符串列表。 - 代码块统一用
{ ... }。 - 注释支持
#和//。 - 行尾分号
;会被兼容层清掉,但正式写法不建议依赖它。 - 行内注释也支持,前提是它不在字符串里。
yaml
script: |
# 这一行不会执行
发送消息("&a脚本开始") // 行尾注释也会被忽略虽然兼容层允许一行里塞多个结构片段并自动切开,但正式脚本仍然建议一行一件事,不要写成“压缩语法”。
当前 AST 语句目录
下面这些是当前 StatementParser 真实识别的正式语句:
| 语句 | 正式写法 | 说明 |
|---|---|---|
| 条件分支 | 判断 (...) {} / 否则判断 (...) {} / 否则 {} | 标准分支语句 |
| 次数循环 | 循环 表达式 次 {} | 重复 N 次 |
| 区间循环 | 循环 i 从 A 到 B {} | 含起点和终点,支持倒序 |
| 遍历循环 | 遍历 item 在 iterable {} | 支持 Iterable、数组、Map |
| 当循环 | 当 (...) {} | 标准 while |
| 概率分支 | 概率执行 rate {} / 失败 {} | 按百分比进分支 |
| 异常处理 | 尝试 {} / 捕获 (...) {} | 支持有类型或无类型捕获 |
| 延迟执行 | 延迟执行 ticks {} | 异步调度执行 |
| 返回 | 返回 / 返回 表达式 | 返回当前脚本或函数 |
| 跳出 | 跳出 | 跳出当前循环 |
| 继续 | 继续 | 跳过本轮循环 |
| 停止 | 停止脚本 | 终止当前脚本 |
| 导入类 | 导入类 别名 = 全限定类名 | 导入类别名 |
| 赋值 | 赋值 变量名 = 表达式 | 正式赋值语法 |
| 当前进度 | 增加进度 / 减少进度 / 设置进度 | 当前成就总进度 |
| 当前目标进度 | 增加目标进度 / 设置目标进度 | 当前成就目标进度 |
| 表达式语句 | 函数(...) / obj.method() | 纯表达式也能单独成句 |
字面量与基础类型
| 类型 | 写法 | 说明 |
|---|---|---|
| 整数 | 1、-5 | 普通十进制整数 |
| 小数 | 3.14、.5、1. | 支持省略前导或尾随 0 |
| 科学计数法 | 1e3、6.02E23 | 当前 Tokenizer 已支持 |
| 十六进制 | 0xFF | 常用于颜色值或位掩码 |
| 二进制 | 0b1010 | 常用于位标记 |
| Long | 123L、9999999999l | 长整型后缀 |
| 布尔 | true、false、真、假 | 布尔值 |
| 空值 | null、空 | 空值 |
| 文本 | "hello"、'world' | 字符串 |
| 列表 | [1, 2, 3] | 列表字面量 |
字符串支持常见转义:
\n\t\r\\\"\'
变量、占位符与类引用
普通变量
当前正式赋值写法是:
text
赋值 变量名 = 表达式示例:
text
赋值 total = 0
赋值 name = "Zombie Hunter"
赋值 done = true
赋值 items = ["DIAMOND", "EMERALD"]
赋值 ratio = .5
赋值 limit = 1e3花括号占位符变量
表达式里也支持 {变量名} 形式:
text
赋值 a = {player_name}
赋值 b = {progress}这一层是 VariableNode(placeholder=true) 的语义,和普通变量有个关键差别:
- 占位符变量如果解析失败,会原样返回
{xxx} - 普通变量如果解析失败,还会继续尝试当导入类名或可加载类名处理
裸标识符的类解析
普通变量名解析不到值时,解释器还会继续尝试:
- 已导入类别名
- 全限定类名
- 反射可加载类
所以这类写法是成立的:
text
导入类 List = java.util.ArrayList
赋值 list = new List()
赋值 max = java.lang.Integer.MAX_VALUE表达式能力
常见运算符
| 类型 | 写法 |
|---|---|
| 算术 | + - * / % |
| 比较 | == != > >= < <= |
| 逻辑 | && ` |
| 三元 | 条件 ? 真值 : 假值 |
示例:
text
赋值 score = kills * 10 + bonus
赋值 ok = progress >= target && !completed
赋值 title = progress >= target ? "已完成" : "进行中"中文 / 关键词运算符别名
Tokenizer 还支持一批可读性更强的词法别名:
| 含义 | 支持写法 |
|---|---|
| 与 | &&、and、与、并且 |
| 或 | ` |
| 非 | !、not、非 |
| 等于 | ==、等于 |
| 不等于 | !=、不等于 |
| 大于 | >、大于 |
| 小于 | <、小于 |
| 大于等于 | >=、大于等于 |
| 小于等于 | <=、小于等于 |
文档仍然建议优先写符号形式,因为它最稳定、最容易扫读。
布尔真值规则
逻辑运算和条件判断内部会做真值转换:
| 值 | 判定结果 |
|---|---|
null | false |
false | false |
数字 0 | false |
空字符串 "" | false |
字符串 "false" / "0" | false |
| 其他非空对象 | true |
所以:
text
判断 ("false") {
# 不会进入
}包含 / 不包含
表达式支持:
包含不包含containsnotcontains
text
判断 (["A", "B", "C"] 包含 "B") {
发送消息("&a命中")
}
判断 (message 不包含 "test") {
发送消息("&7忽略测试消息")
}按当前实现:
- 左边是
Collection、数组时,做成员判断 - 左边是字符串时,做子串判断
- 左边其他对象时,退化为
String.valueOf(left).contains(String.valueOf(right))
类型判断
表达式支持:
类型是是instanceof
text
判断 (entity 是 玩家) {
发送消息("&a这是玩家")
}
判断 (list instanceof List) {
发送消息("&a这是列表")
}右侧类型既可以写:
- 内建别名,例如
String、字符串、Array、数组、Player、玩家 - 导入类别名
- 全限定类名
类型转换
支持显式类型转换:
text
赋值 amount = (int) 1.9
赋值 text = (java.lang.String) player.name下标访问
列表、数组、Map 风格对象、字符串都支持下标访问:
text
赋值 first = items[0]
赋值 name = mapping["name"]
赋值 char = text[2]函数调用、方法链和字段访问
AST 表达式本身就支持:
- 普通函数调用
- 实例方法链
- 实例字段 / 属性访问
- 静态方法
- 静态字段
text
赋值 upper = item.name.toUpperCase()
赋值 size = list.size()
赋值 max = java.lang.Integer.MAX_VALUE
赋值 abs = java.lang.Math.abs(-5)new 对象构造
当前正式支持的是 Java 风格 new:
text
赋值 a = new java.util.ArrayList()
赋值 b = new java.util.ArrayList()如果已经导入类别名,也可以这样写:
text
导入类 HashMap = java.util.HashMap
赋值 data = new HashMap()没有原生 Map / Object 字面量
当前 AST 没有 原生的 {key: value} 对象字面量 / Map 字面量。
也就是说,这种写法不要当成正式语法:
text
触发事件("boss_killed", {"boss": mm.id})如果你需要 Map,正式写法是自己创建对象:
text
导入类 HashMap = java.util.HashMap
赋值 data = new HashMap()
data.put("boss", mm.id)
data.put("killer", player.name)
触发事件("boss_killed", data)控制流
条件分支
text
判断 (progress >= target) {
发送消息("&a完成")
} 否则判断 (progress > 0) {
发送消息("&e进行中")
} 否则 {
发送消息("&7尚未开始")
}解析器会兼容省略条件外层括号的旧式写法,但正式文档建议统一带圆括号。
重复循环
text
循环 5 次 {
发送动作栏("&e+1")
}运行时细节:
- 次数表达式会转成整数
- 小于
0的次数会被当成0 - 大于
100000的次数会被直接截成100000 - 循环内会注入:
循环次数:从1开始loopIndex:从0开始
示例:
text
循环 3 次 {
发送消息("第 " + 循环次数 + " 次")
}区间循环
text
循环 i 从 1 到 3 {
发送消息("第 " + i + " 波")
}运行时细节:
- 起点和终点都包含
- 支持倒序,例如
循环 i 从 3 到 1 - 总跨度超过
100000会直接抛错 - 这类循环只维护你声明的变量名,不会额外注入
循环次数
遍历循环
text
遍历 item 在 items {
发送消息("拿到了 " + item)
}当前支持的可遍历对象:
IterableMap- 数组
其中 Map 走的是 entrySet(),也就是循环体里拿到的是 Map.Entry。
循环时会注入:
- 你声明的迭代变量,例如
item 循环索引loopIndex
当循环
text
赋值 counter = 0
当 (counter < 3) {
赋值 counter = counter + 1
}运行时细节:
- 条件按真值规则判断
- 最大执行次数是
100000 - 超过上限会直接抛出脚本异常,避免死循环
概率分支
text
概率执行 30 {
发送消息("&6触发稀有分支")
} 失败 {
发送消息("&7本次未触发")
}按当前实现,它的判定是:
text
random(0, 100) < rate也就是说:
30表示约30%0基本不会成功100基本总成功- 它不会自动把数值夹到
0-100
所以如果 rate 来源不稳定,建议先自己限制范围:
text
赋值 safeRate = 限制范围(rate, 0, 100)
概率执行 safeRate {
发送消息("&a成功")
}异常处理
完整写法:
text
尝试 {
赋值 value = java.lang.Integer.parseInt("123")
} 捕获 (Exception ex) {
返回 ex.class.getSimpleName()
}也支持这些形态:
text
尝试 {
riskyCall()
} 捕获 (ex) {
发送消息(ex.message)
}text
尝试 {
riskyCall()
} 捕获 (java.lang.IllegalStateException ex) {
发送消息("状态异常")
}text
尝试 {
riskyCall()
} 捕获 (ScriptException) {
发送消息("脚本异常")
}规则总结:
捕获 (ex):不限制类型,只注入变量捕获 (Exception ex):限制类型并注入变量捕获 (Exception):限制类型,不注入变量- 类型名可以写导入类别名、全限定类名,或常见异常类名
延迟执行
text
延迟执行 20 {
发送消息("&a1 秒后执行")
}运行时细节很关键:
- 延迟值会先求值,再转成整数 tick
- 小于
0会被当成0 - 它不是同步阻塞,而是 Bukkit 调度
- 调度时会复制一份
ScriptContext - 如果玩家下线,延迟块会直接跳过
- 延迟块里的进度 / 目标进度结果会在到时后正式写回并落盘
延迟块不是同步返回
延迟执行 20 { ... } 的主体是在后续 tick 调度执行,不在当前解释帧内。
这意味着:
- 块里的
返回只结束延迟块自己 - 块里的
停止脚本只停止延迟块自己 - 它不会像普通同步代码块那样,把结果立刻回流到当前调用点
- 延迟块里修改的局部变量环境,是调度时那份上下文快照,不是当前调用点的同步后续帧
结束、跳过与停止
| 语句 | 作用 |
|---|---|
返回 / 返回 值 | 结束当前脚本或函数,并可带返回值 |
跳出 | 结束当前循环 |
继续 | 跳过本轮循环剩余部分 |
停止脚本 | 立即终止当前脚本执行 |
循环里的 跳出 / 继续 只影响最近一层循环,不会越层。
导入类与反射入口
导入类
text
导入类 List = java.util.ArrayList
导入类 Bukkit = org.bukkit.Bukkit导入以后,可以直接拿别名做静态访问或构造:
text
赋值 online = Bukkit.getOnlinePlayers().size()
赋值 list = new List()new
text
赋值 a = new java.util.ArrayList()
赋值 b = new java.util.ArrayList()
赋值 c = new List()当前 AST 正式支持的是 new。 如果你在旧文档或旧示例里见过 新建,请以 new 为准。
如果你还要进一步访问第三方对象、静态字段、单例入口,直接看 反射与导入类。
命令式兼容调用
除了标准的 函数(...),当前解析器还保留了一部分“命令式”兼容调用包装。
比如这些写法都能走:
text
发送消息 "&a你好"
执行命令 "say hello"
设置标记 "done"这里要注意两点:
- 命令式兼容既支持主名称,也可能走别名
- 例如
执行命令实际上是执行指令的别名
但正式文档仍然建议优先写成函数风格:
text
发送消息("&a你好")
执行指令("say hello")
设置标记("done")原因是这种写法:
- 参数边界最清晰
- 兼容层歧义最少
- 长期维护更稳
旧语法兼容表
| 旧写法 | 现在会被归一成 |
|---|---|
如果 / 否则如果 / 否则 / 结束 | 判断 / 否则判断 / 否则 {} |
否则 如果 | 否则如果 |
创建变量 x | 赋值 x = null |
设置变量 x = 1 | 赋值 x = 1 |
进度增加 1 | 增加进度 1 |
进度减少 1 | 减少进度 1 |
进度设置 10 | 设置进度 10 |
增加进度(1) | 增加进度 1 |
减少进度(1) | 减少进度 1 |
设置进度(10) | 设置进度 10 |
目标进度增加("dragon", 1) | 增加目标进度 "dragon" 1 |
设置目标进度("dragon", 3) | 设置目标进度 "dragon" 3 |
目标进度增加 dragon 1 | 增加目标进度 dragon 1 |
调用 X.y() | 普通表达式调用 |
取静态字段 java.lang.Integer.MAX_VALUE | 普通字段访问 |
创建对象 X 引入包 java.util.ArrayList | 导入类 X = java.util.ArrayList |
兼容层还会自动做这些整理:
- 去掉行尾分号
- 把一行里拆得开的结构片段切成多行
- 给部分旧条件头自动补上圆括号
使用建议
- 新脚本统一写 AST 语法,不要继续扩写旧关键字。
- 条件、循环、异常块都建议显式写
{},不要依赖兼容层猜你的结构。 - 复杂表达式宁可多拆几行变量,也不要把所有逻辑塞进一行三元。
- 需要
Map、自定义对象时,用导入类+new,不要把{"k": v}当作正式语法。 - 需要异步等待时,用
延迟执行,不要把它当同步sleep使用。 - 需要跨插件对象、静态字段、类加载器能力时,直接看 反射与导入类。