变量与上下文
AchieveMaster 当前 AST 脚本里的“变量”不是单一来源。 一段脚本里能读到的值,可能来自:
- 当前脚本执行时临时写入的上下文变量
- 当前成就进度对象里的持久变量
ScriptContext自动注入的内建字段- 触发器在执行前灌入的事件上下文
- PlaceholderAPI、MythicMobs、Chemdah 等外部集成
这一页按当前源码的真实行为说明,不包含旧解释器时代的历史兼容描述。
读取顺序
普通 AST 脚本里写一个变量名时,ScriptContext#getVariable(...) 的读取顺序是:
- 当前脚本上下文变量表
variables - 变量别名归一后的上下文变量
- 当前成就进度
AchievementProgress里的持久变量 - 变量别名归一后的成就进度变量
- 少量硬编码兼容别名的对象字段回退
如果你写的是带根路径的形式,例如 player.name、event.message、item.type,resolveVariable(...) 会再走一层根变量分派:
- 先尝试把整段路径当成一个完整变量名直接读取
- 再按根变量分派到
player、item、event、progress、papi等解析器 - 如果根解析没结果,再回退一次普通变量读取
这意味着两件事:
- 带点号的变量名是合法的,触发器或脚本如果先写进了
event.message,它会优先于根变量解析 - 变量别名不只是“写起来顺手”,它真的会参与读取归一
变量来源总表
| 类型 | 读取写法 | 来源 | 返回值 | 可写 | 说明 |
|---|---|---|---|---|---|
| 脚本上下文变量 | 变量名、a.b | 当前 ScriptContext.variables | 任意类型 | 可以 | 赋值、循环变量、触发器注入字段都在这一层。 |
| 成就进度变量 | 变量名 | AchievementProgress | 任意类型 | 可以 | 跨脚本、跨触发保留的玩家成就变量。 |
| 玩家上下文 | player、player.xxx | 当前玩家对象 | 文本、数字、布尔或 Player | 只读 | 由 ScriptContext 内建解析。 |
| 事件上下文 | event、event.xxx | 当前 Bukkit 事件 | 文本或事件对象字段 | 只读 | 只有当前触发器带事件时才有意义。 |
| 实体上下文 | entity、entity.xxx | 当前事件实体 | 文本或实体对象字段 | 只读 | 常见于伤害、击杀、交互类触发。 |
| 物品上下文 | item、item.xxx | 当前事件物品 | 文本、数字或 ItemStack | 只读 | 常见于拾取、使用、消耗、背包扫描类触发。 |
| MythicMobs 上下文 | mm.xxx | 触发器注入 | 文本或数字 | 只读 | 只有相关触发器注入时才有值。 |
| 任务上下文 | quest.xxx | 触发器注入 | 文本 | 只读 | 只有 Chemdah 相关触发器注入时才有值。 |
| 进度根变量 | progress、target、percentage | ScriptContext 初始化 | 数字 | 可以读,部分会被动作刷新 | progress 是数字,不是进度对象。 |
| PlaceholderAPI | papi.xxx、变量.xxx | PlaceholderAPI | 文本或 Double | 只读 | 未启用 PAPI 或没玩家时不会解析。 |
| 中文/兼容别名 | 消息、怪物等级、物品类型 | ScriptVariableAliases | 取决于目标字段 | 不适用 | 最终都会归一到规范字段名。 |
默认注入的内建变量
ScriptContext 创建时会立刻注入三项内建变量:
| 变量 | 来源 | 说明 |
|---|---|---|
progress | progress.getProgress() | 当前成就进度值 |
target | achievement.getCompletionTarget() | 当前成就完成目标 |
percentage | progress.getPercentage(target) | 当前完成百分比 |
注意这里的 progress 是一个数字快照,不是 AchievementProgress 对象。
如果脚本后续通过进度动作修改了结果,这个数值通常会被执行过程继续更新;但在 AST 表达式语义上,它始终按“数字变量”使用,不支持对象字段访问。
根变量与字段解析
player / 玩家
常规成就脚本创建 ScriptContext 时会带当前玩家。字段解析如下:
| 写法 | 返回值 | 说明 |
|---|---|---|
player | 玩家名 | 根变量无字段时直接返回玩家名 |
player.name / player.名称 | String | 玩家名 |
player.uuid | String | UUID 字符串 |
player.level / player.等级 | int | 经验等级 |
player.health / player.生命 | double | 当前生命值 |
player.maxHealth / player.最大生命 | double | 最大生命值 |
player.food / player.饥饿 | int | 饱食度 |
player.world / player.世界 | String | 世界名 |
player.x / player.y / player.z | int | 方块坐标,不是精确小数坐标 |
player.isOp / player.管理员 | boolean | 是否 OP |
player.isAlive / player.存活 | boolean | 是否存活 |
player.gameMode / player.游戏模式 | String | Bukkit 枚举名 |
player.hasPermission("perm.node") | boolean | 权限检测 |
player.权限("perm.node") | boolean | 中文别名写法 |
示例:
text
判断 (player.level >= 30 && player.hasPermission("ach.vip")) {
发送消息("&a满足权限和等级条件")
}entity / 实体
| 写法 | 返回值 | 说明 |
|---|---|---|
entity | 实体类型名 | 根变量无字段时返回 EntityType.name() |
entity.type / entity.类型 | String | 实体类型 |
entity.name / entity.名称 | String | 实体名 |
entity.customName / entity.自定义名称 | String | null | 自定义名称 |
entity.uuid | String | UUID |
entity.world / entity.世界 | String | 所在世界 |
item / 物品
| 写法 | 返回值 | 说明 |
|---|---|---|
item | 材质名 | 根变量无字段时返回 Material.name() |
item.type / item.类型 | String | 材质类型 |
item.amount / item.数量 | int | 数量 |
item.name / item.名称 | String | 有展示名时返回展示名,否则回退材质名 |
item.lore / item.描述 | String | 多行 lore 用 \n 拼接;没有 lore 时返回空字符串 "" |
item.durability / item.耐久 | short | 旧版耐久值 |
示例:
text
判断 (item.type == "DIAMOND_SWORD" && item.lore 包含 "史诗") {
发送消息("&e检测到史诗武器")
}event / 事件
| 写法 | 返回值 | 说明 |
|---|---|---|
event | event.name 或事件名 | 根变量无字段时优先读 event.name 变量,否则回退 event.getEventName() |
event.name | String | 事件名 |
event.type | String | 当前实现里和 event.name 相同,不是额外的事件类别枚举 |
event.message | String | null | 通过反射调用 getMessage(),只有带该方法的事件才有值 |
event.block.type | String | null | 依次尝试 getBlock()、getClickedBlock()、getBlockClicked() 后再取 getType() |
setEvent(event) 发生时,还会往上下文变量表里额外写入:
| 变量 | 值 |
|---|---|
event.name | event.getEventName() |
event.type | event.getEventName() |
所以 event.type 目前就是事件名别名,不要把它理解成更细的事件分类。
mm
只有 MythicMobs 相关触发器注入后才有值。
| 写法 | 返回值 | 说明 |
|---|---|---|
mm | mm.id | 根变量无字段时返回怪物 ID / mobType |
mm.id / mm.mobType / 怪物ID / 怪物类型 | String | 怪物类型 ID |
mm.level / 怪物等级 / mobLevel | int | 怪物等级 |
mm.faction / 怪物派系 | String | 派系 |
mm.displayName / 怪物名称 / mobName | String | 优先读上下文里的 mm.displayName,没有就回退 mm.id |
触发器调用 setMythicMobData(...) 时会写入:
| 变量 | 值 |
|---|---|
mm.id | 怪物类型 ID |
mm.level | 怪物等级 |
mm.faction | 派系 |
quest / 任务
只有 Chemdah 相关触发器注入后才有值。
| 写法 | 返回值 | 说明 |
|---|---|---|
quest / 任务 | quest.id | 根变量无字段时返回任务 ID |
quest.id / 任务ID / 任务.id | String | 任务 ID |
quest.name / 任务名 / 任务名称 / 任务.名称 | String | 任务显示名 |
触发器调用 setQuestData(...) 时会写入:
| 变量 | 值 |
|---|---|
quest.id | 任务 ID |
quest.name | 任务名称 |
progress / 进度
这是当前 AST 里最容易误解的根变量。
| 写法 | 返回值 | 说明 |
|---|---|---|
progress / 进度 | int | 优先返回上下文变量表里的最新 progress 数值,否则回退 AchievementProgress.getProgress() |
target / 目标 | int | 当前成就目标值 |
percentage | 数字 | 当前完成百分比 |
要注意两点:
progress是数字根,不是对象根progress.xxx不会继续往下取字段;在没有同名完整变量时,它仍会被当成progress根处理并返回数值
所以不要写下面这种期望:
text
# 这是错误预期,AST 不支持这样读进度对象字段
判断 (progress.someField > 0) {
}如果你需要对象本体,那是 script-js 里的 ctx.progress 语义,不是 AST 里的 progress 语义。
papi.xxx / 变量.xxx
这两个根是同义入口,最终都会调用 PlaceholderAPI。
| 写法 | 返回值 | 说明 |
|---|---|---|
papi.player_name | String 或 Double | 自动补 %player_name% 后解析 |
变量.vault_eco_balance | String 或 Double | 中文根别名 |
真实行为如下:
- 你写
papi.xxx或变量.xxx - 引擎会自动补全前后
% - 如果 PAPI 返回值能被
Double.parseDouble(...)解析,就转成Double - 否则按原字符串返回
重要边界:
- 如果服务器没装 PlaceholderAPI,直接返回你传入的占位符内容,例如
player_name - 如果当前上下文没有玩家,也直接返回占位符内容,不会报错
- 这时返回的是原始内容,不是
%player_name%
示例:
text
赋值 money = papi.vault_eco_balance
赋值 prefix = 变量.player_prefix
判断 (money >= 10000) {
发送消息("&6你的金币足够")
}事件触发器常见上下文字段
除了上面的根变量,触发器通常还会直接把字段写进上下文变量表。 这类字段不一定有统一根解析器,但可以直接读取。
常见类别如下:
| 类别 | 常见字段 |
|---|---|
| 聊天 / 指令 | command、args、event.message、chat.length |
| 伤害 | damage.amount、damage.final、damage.cause、damage.pvp |
| 攻击者 / 目标 | attacker.*、victim.* |
| 合成 / 容器 / 物品 | inventory.*、result.*、enchant.*、bucket.* |
| 投射物 | projectile.type、hit.block、hit.entity、hit.player |
| 移动 / 传送 | move.from、move.to、move.distance、from.world、to.world |
| 地牢 / 区域 / 队伍 | dungeon.*、region.*、team.* |
是否有值,完全取决于你当前挂载的是哪一种触发器。
变量别名归一
ScriptVariableAliases 为很多历史写法、中文写法和英文快捷写法提供了归一。
常见别名如下:
| 别名写法 | 归一后 |
|---|---|
事件名、事件名称、eventName | event.name |
事件类型 | event.type |
消息、消息内容、message | event.message |
指令、命令 | command |
参数 | args |
事件方块类型、方块类型、blockType | event.block.type |
物品 | item |
itemType、物品类型、物品材料 | item.type |
itemAmount、物品数量 | item.amount |
itemName、物品名称 | item.name |
itemLore、物品描述 | item.lore |
结果、合成结果 | result |
结果类型、合成物品类型 | result.type |
结果数量、合成数量 | result.amount |
结果名称、合成物品名称 | result.name |
fromWorld、原世界 | from.world |
toWorld、目标世界 | to.world |
世界 | world |
怪物ID、怪物类型、mobType | mm.id |
怪物等级、mobLevel | mm.level |
怪物派系 | mm.faction |
怪物名称、mobName | mm.displayName |
任务ID | quest.id |
任务名、任务名称 | quest.name |
区域ID | region.id |
区域名称 | region.name |
建议仍然优先使用规范字段名,原因很简单:
- 和源码一一对应,排错最快
- 不同触发器之间迁移更容易
- 以后继续维护 AST 时最稳定
变量作用域
普通 赋值
普通赋值会写入当前脚本上下文,可被后续语句继续读取:
text
赋值 total = 1
赋值 total = total + 1遍历 的循环变量
ForEachNode 执行时会先保存旧值,再在每次迭代里写入:
| 变量 | 说明 |
|---|---|
| 你声明的循环变量 | 例如 遍历 item 在 list 里的 item |
循环索引 | 中文索引变量,从 0 开始 |
loopIndex | 英文索引变量,从 0 开始 |
循环结束后:
- 如果外层原本有同名变量,会恢复原值
- 如果外层没有同名变量,会被清掉
循环 x 从 A 到 B
RangeForNode 只恢复你声明的循环变量本身:
text
赋值 i = 99
循环 i 从 1 到 3 {
发送消息(i)
}
发送消息(i) # 这里仍然是 99额外边界:
- 支持递增和递减区间
- 最大迭代次数是
100000 - 超过会抛脚本异常
重复执行
RepeatNode 会在循环体内写:
| 变量 | 说明 |
|---|---|
循环次数 | 从 1 开始的当前轮次 |
loopIndex | 从 0 开始的当前索引 |
执行结束后同样恢复旧值。
最大重复次数也会被限制在 100000。
自定义函数参数与局部变量
当前 AST 自定义函数、Java 函数桥接、YML 条件桥接都使用复制后的执行上下文或局部变量表,不会把参数名、局部临时值直接污染回调用方。
如果你在函数体里显式调用成就变量、标记、进度相关动作,那些动作当然仍然会生效;但“单纯的局部赋值”不会泄漏到外层。
延迟执行与上下文拷贝
延迟执行 在运行时会先做一件关键事情:
- 先对当前
ScriptContext调用copy() - 把复制后的上下文交给 Bukkit
runTaskLater(...) - 延迟块在未来的某个 tick 里异步于当前调用栈执行
这带来几个实际结论:
- 延迟块读到的是“调度当下那一刻”的上下文快照
- 调度后你在当前脚本里继续改的普通变量,不会自动同步进已经排队的延迟块
- 延迟块里的
return/stop/break不会同步回流到当前这一帧调用栈,因为它已经不是同一轮执行 - 延迟块里的进度结果会通过
ScriptResult在正式执行时回写到成就系统
示例:
text
赋值 title = "第一阶段"
延迟执行 100 {
发送消息(title) # 这里拿到的是调度时复制进去的值
}
赋值 title = "第二阶段"上面的延迟块最终读到的是 "第一阶段",不是 "第二阶段"。
标记、时间标记与变量的关系
标记系统不是 some.flag 这种变量根,但它和变量一起构成脚本状态层。
ScriptContext 里额外维护了:
- 临时标记集合
- 时间标记表
- 与
AchievementProgress同步的持久标记
这里要分清:
- 持久标记适合跨触发、跨时段控制
- 时间标记只存在当前
ScriptContext及其 copy 里,不适合做跨触发长期状态
不要把这几类状态都混成同一层理解。
相关函数见:
实战建议
- 优先把
progress当数字用,不要把它当对象根。 - 想写可维护脚本,尽量直接写
event.message、item.type、mm.level这种规范字段名。 - 高度依赖触发器上下文字段时,先确认对应触发器是否真的会注入该字段。
- 需要跨触发保留状态时,用成就变量、持久标记,不要只靠当前脚本里的临时赋值。
时间标记只适合单次执行流及其 copy 内部的相对计时,不要拿它做长期冷却。- 需要读取外部占位符时,记住
papi.xxx在无 PAPI 或无玩家时会回原始占位符内容。