Skip to content

语法与表达式

这一页只讲一件事: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.51.支持省略前导或尾随 0
科学计数法1e36.02E23当前 Tokenizer 已支持
十六进制0xFF常用于颜色值或位掩码
二进制0b1010常用于位标记
Long123L9999999999l长整型后缀
布尔truefalse布尔值
空值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
等于==等于
不等于!=不等于
大于>大于
小于<小于
大于等于>=大于等于
小于等于<=小于等于

文档仍然建议优先写符号形式,因为它最稳定、最容易扫读。

布尔真值规则

逻辑运算和条件判断内部会做真值转换:

判定结果
nullfalse
falsefalse
数字 0false
空字符串 ""false
字符串 "false" / "0"false
其他非空对象true

所以:

text
判断 ("false") {
  # 不会进入
}

包含 / 不包含

表达式支持:

  • 包含
  • 不包含
  • contains
  • notcontains
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)
}

当前支持的可遍历对象:

  • Iterable
  • Map
  • 数组

其中 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 使用。
  • 需要跨插件对象、静态字段、类加载器能力时,直接看 反射与导入类

TQ Minecraft Server Plugin Docs