Skip to content

变量与上下文

AchieveMaster 当前 AST 脚本里的“变量”不是单一来源。 一段脚本里能读到的值,可能来自:

  • 当前脚本执行时临时写入的上下文变量
  • 当前成就进度对象里的持久变量
  • ScriptContext 自动注入的内建字段
  • 触发器在执行前灌入的事件上下文
  • PlaceholderAPI、MythicMobs、Chemdah 等外部集成

这一页按当前源码的真实行为说明,不包含旧解释器时代的历史兼容描述。

读取顺序

普通 AST 脚本里写一个变量名时,ScriptContext#getVariable(...) 的读取顺序是:

  1. 当前脚本上下文变量表 variables
  2. 变量别名归一后的上下文变量
  3. 当前成就进度 AchievementProgress 里的持久变量
  4. 变量别名归一后的成就进度变量
  5. 少量硬编码兼容别名的对象字段回退

如果你写的是带根路径的形式,例如 player.nameevent.messageitem.typeresolveVariable(...) 会再走一层根变量分派:

  1. 先尝试把整段路径当成一个完整变量名直接读取
  2. 再按根变量分派到 playeritemeventprogresspapi 等解析器
  3. 如果根解析没结果,再回退一次普通变量读取

这意味着两件事:

  • 带点号的变量名是合法的,触发器或脚本如果先写进了 event.message,它会优先于根变量解析
  • 变量别名不只是“写起来顺手”,它真的会参与读取归一

变量来源总表

类型读取写法来源返回值可写说明
脚本上下文变量变量名a.b当前 ScriptContext.variables任意类型可以赋值、循环变量、触发器注入字段都在这一层。
成就进度变量变量名AchievementProgress任意类型可以跨脚本、跨触发保留的玩家成就变量。
玩家上下文playerplayer.xxx当前玩家对象文本、数字、布尔或 Player只读ScriptContext 内建解析。
事件上下文eventevent.xxx当前 Bukkit 事件文本或事件对象字段只读只有当前触发器带事件时才有意义。
实体上下文entityentity.xxx当前事件实体文本或实体对象字段只读常见于伤害、击杀、交互类触发。
物品上下文itemitem.xxx当前事件物品文本、数字或 ItemStack只读常见于拾取、使用、消耗、背包扫描类触发。
MythicMobs 上下文mm.xxx触发器注入文本或数字只读只有相关触发器注入时才有值。
任务上下文quest.xxx触发器注入文本只读只有 Chemdah 相关触发器注入时才有值。
进度根变量progresstargetpercentageScriptContext 初始化数字可以读,部分会被动作刷新progress 是数字,不是进度对象。
PlaceholderAPIpapi.xxx变量.xxxPlaceholderAPI文本或 Double只读未启用 PAPI 或没玩家时不会解析。
中文/兼容别名消息怪物等级物品类型ScriptVariableAliases取决于目标字段不适用最终都会归一到规范字段名。

默认注入的内建变量

ScriptContext 创建时会立刻注入三项内建变量:

变量来源说明
progressprogress.getProgress()当前成就进度值
targetachievement.getCompletionTarget()当前成就完成目标
percentageprogress.getPercentage(target)当前完成百分比

注意这里的 progress 是一个数字快照,不是 AchievementProgress 对象。

如果脚本后续通过进度动作修改了结果,这个数值通常会被执行过程继续更新;但在 AST 表达式语义上,它始终按“数字变量”使用,不支持对象字段访问。

根变量与字段解析

player / 玩家

常规成就脚本创建 ScriptContext 时会带当前玩家。字段解析如下:

写法返回值说明
player玩家名根变量无字段时直接返回玩家名
player.name / player.名称String玩家名
player.uuidStringUUID 字符串
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.zint方块坐标,不是精确小数坐标
player.isOp / player.管理员boolean是否 OP
player.isAlive / player.存活boolean是否存活
player.gameMode / player.游戏模式StringBukkit 枚举名
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.uuidStringUUID
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 / 事件

写法返回值说明
eventevent.name 或事件名根变量无字段时优先读 event.name 变量,否则回退 event.getEventName()
event.nameString事件名
event.typeString当前实现里和 event.name 相同,不是额外的事件类别枚举
event.messageString | null通过反射调用 getMessage(),只有带该方法的事件才有值
event.block.typeString | null依次尝试 getBlock()getClickedBlock()getBlockClicked() 后再取 getType()

setEvent(event) 发生时,还会往上下文变量表里额外写入:

变量
event.nameevent.getEventName()
event.typeevent.getEventName()

所以 event.type 目前就是事件名别名,不要把它理解成更细的事件分类。

mm

只有 MythicMobs 相关触发器注入后才有值。

写法返回值说明
mmmm.id根变量无字段时返回怪物 ID / mobType
mm.id / mm.mobType / 怪物ID / 怪物类型String怪物类型 ID
mm.level / 怪物等级 / mobLevelint怪物等级
mm.faction / 怪物派系String派系
mm.displayName / 怪物名称 / mobNameString优先读上下文里的 mm.displayName,没有就回退 mm.id

触发器调用 setMythicMobData(...) 时会写入:

变量
mm.id怪物类型 ID
mm.level怪物等级
mm.faction派系

quest / 任务

只有 Chemdah 相关触发器注入后才有值。

写法返回值说明
quest / 任务quest.id根变量无字段时返回任务 ID
quest.id / 任务ID / 任务.idString任务 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_nameStringDouble自动补 %player_name% 后解析
变量.vault_eco_balanceStringDouble中文根别名

真实行为如下:

  • 你写 papi.xxx变量.xxx
  • 引擎会自动补全前后 %
  • 如果 PAPI 返回值能被 Double.parseDouble(...) 解析,就转成 Double
  • 否则按原字符串返回

重要边界:

  • 如果服务器没装 PlaceholderAPI,直接返回你传入的占位符内容,例如 player_name
  • 如果当前上下文没有玩家,也直接返回占位符内容,不会报错
  • 这时返回的是原始内容,不是 %player_name%

示例:

text
赋值 money = papi.vault_eco_balance
赋值 prefix = 变量.player_prefix

判断 (money >= 10000) {
  发送消息("&6你的金币足够")
}

事件触发器常见上下文字段

除了上面的根变量,触发器通常还会直接把字段写进上下文变量表。 这类字段不一定有统一根解析器,但可以直接读取。

常见类别如下:

类别常见字段
聊天 / 指令commandargsevent.messagechat.length
伤害damage.amountdamage.finaldamage.causedamage.pvp
攻击者 / 目标attacker.*victim.*
合成 / 容器 / 物品inventory.*result.*enchant.*bucket.*
投射物projectile.typehit.blockhit.entityhit.player
移动 / 传送move.frommove.tomove.distancefrom.worldto.world
地牢 / 区域 / 队伍dungeon.*region.*team.*

是否有值,完全取决于你当前挂载的是哪一种触发器。

变量别名归一

ScriptVariableAliases 为很多历史写法、中文写法和英文快捷写法提供了归一。

常见别名如下:

别名写法归一后
事件名事件名称eventNameevent.name
事件类型event.type
消息消息内容messageevent.message
指令命令command
参数args
事件方块类型方块类型blockTypeevent.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怪物类型mobTypemm.id
怪物等级mobLevelmm.level
怪物派系mm.faction
怪物名称mobNamemm.displayName
任务IDquest.id
任务名任务名称quest.name
区域IDregion.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 开始的当前轮次
loopIndex0 开始的当前索引

执行结束后同样恢复旧值。

最大重复次数也会被限制在 100000

自定义函数参数与局部变量

当前 AST 自定义函数、Java 函数桥接、YML 条件桥接都使用复制后的执行上下文或局部变量表,不会把参数名、局部临时值直接污染回调用方。

如果你在函数体里显式调用成就变量、标记、进度相关动作,那些动作当然仍然会生效;但“单纯的局部赋值”不会泄漏到外层。

延迟执行与上下文拷贝

延迟执行 在运行时会先做一件关键事情:

  1. 先对当前 ScriptContext 调用 copy()
  2. 把复制后的上下文交给 Bukkit runTaskLater(...)
  3. 延迟块在未来的某个 tick 里异步于当前调用栈执行

这带来几个实际结论:

  • 延迟块读到的是“调度当下那一刻”的上下文快照
  • 调度后你在当前脚本里继续改的普通变量,不会自动同步进已经排队的延迟块
  • 延迟块里的 return / stop / break 不会同步回流到当前这一帧调用栈,因为它已经不是同一轮执行
  • 延迟块里的进度结果会通过 ScriptResult 在正式执行时回写到成就系统

示例:

text
赋值 title = "第一阶段"

延迟执行 100 {
  发送消息(title) # 这里拿到的是调度时复制进去的值
}

赋值 title = "第二阶段"

上面的延迟块最终读到的是 "第一阶段",不是 "第二阶段"

标记、时间标记与变量的关系

标记系统不是 some.flag 这种变量根,但它和变量一起构成脚本状态层。

ScriptContext 里额外维护了:

  • 临时标记集合
  • 时间标记表
  • AchievementProgress 同步的持久标记

这里要分清:

  • 持久标记适合跨触发、跨时段控制
  • 时间标记只存在当前 ScriptContext 及其 copy 里,不适合做跨触发长期状态

不要把这几类状态都混成同一层理解。

相关函数见:

实战建议

  • 优先把 progress 当数字用,不要把它当对象根。
  • 想写可维护脚本,尽量直接写 event.messageitem.typemm.level 这种规范字段名。
  • 高度依赖触发器上下文字段时,先确认对应触发器是否真的会注入该字段。
  • 需要跨触发保留状态时,用成就变量、持久标记,不要只靠当前脚本里的临时赋值。
  • 时间标记 只适合单次执行流及其 copy 内部的相对计时,不要拿它做长期冷却。
  • 需要读取外部占位符时,记住 papi.xxx 在无 PAPI 或无玩家时会回原始占位符内容。

TQ Minecraft Server Plugin Docs