LogoKubeJS PMMO

Tag 模型

PmmoJS 会暴露 PMMO 的 NBT tag,因为 PMMO 本身就是用 tag 在对象数据、trigger、perk 和 XP 奖励之间传递信息。不要只把它们看成同一种 CompoundTag。同一个类型在不同入口里可能代表“静态配置”“当前事件上下文”“perk 生命周期状态”或“一次性输出”。

这一页基于 PMMO 1.20.1 源码中的 PerkRegistry.executePerkEventTriggerRegistry.executeEventListenersTagUtils.mergeTagsAPIUtils.serializeAwardMap,以及 PmmoJS 的 PerkTagJSPMMOTriggerEventJSPMMOInternalEventJS 包装类。

数据流图

大多数 PMMO 游戏流程可以理解成这样:

settings / perks config / startup defaults
|
v
PMMO 事件处理器构建输入 CompoundTag
|
v
trigger listener 修改或返回运行时上下文
|
v
PMMO 为每个 perk 构建合并后的 settings tag
|
v
perk start 可以给后续 perk 写入 result output
|
v
PMMO 计算 XP、需求、伤害、速度或其他最终效果
|
v
xp hook 观察最终 XP 事件上下文

globalsConfig 不在这条运行时 tag 流里。PMMO 只在求值 NBT 逻辑时使用它:paths 在读取对象 NBT 前展开 #别名constants 在比较值前展开 #别名

最重要的规则

数据应该写到生命周期匹配的位置:

需求写到哪里原因
物品、方块、实体、生物群系或维度上的稳定规则PmmoJS.settings(...)PMMO 能把它当作正常对象数据加载和求值
可复用的 NBT 路径或比较值别名PmmoJS.globalsConfig(...)PMMO 只会在 NBT paths 和 comparators 中展开 globals
自定义 perk 的可配置参数PmmoJS.registerPerk(...).withInt(...).defaults(...),必要时再用 PmmoJS.perksConfig(...)PMMO 会在回调运行前合并默认值和 perk 配置
tick()stop() 后续要读的状态ctx.getSettings()PMMO 会把 settings tag 的副本放入 active tick schedule
同一执行趟次中给后续 perk 看的输出start() 内的 ctx.getResult()PMMO 会把 start result 合并进本次执行输出
单次 PMMO trigger 的额外 XP 或取消标记PmmoJS.trigger(...) 的 context helper这个上下文只属于一次 trigger 调度
不阻止游戏行为,只跳过 PMMO 内部处理器PmmoJS.internal(...).skipPmmo()internal hook 控制 PMMO handler 是否继续
取消底层 Forge 行为PmmoJS.internal(...).deny()它会设置动作取消并跳过 PMMO
观察最终 XP 变化PmmoJS.xp(...)这个事件在 PMMO 给玩家结算 XP 时触发

规则如果是稳定的,先写成 PMMO 数据。只有静态数据不知道的实时状态,才放进运行时 tag。

Tag 分类

分类例子生命周期
静态对象数据需求、XP 奖励、加成、效果、salvage、vein data存在 PMMO 数据/配置中
静态 perk 数据startup defaults 和 perks.toml 条目持续到 PMMO 重载配置
运行时 trigger 上下文PmmoJS.trigger(...) 事件 context一次 trigger 调度
运行时 internal 上下文PmmoJS.internal(...) 事件 context一次 internal hook 调度
Perk 执行 settingsctx.getSettings()一次 perk 激活,之后是 active tick schedule 的副本
Perk result outputctx.getResult()一次 start() 调用和当前 perk 执行趟次
XP 事件上下文PmmoJS.xp(...).getContext()一次 XP 事件

Perk Settings 和 Result

PMMO 执行 perk 时,会为每个配置的 perk 条目构建一个新的 settings tag:

  1. 注册 perk 时的 propertyDefaults()
  2. PMMO perk settings 中匹配的条目。
  3. 当前事件传入的数据。
  4. 同一执行趟次中更早 perk 的输出。
  5. 最后根据合并后的 skill 注入 level

PMMO 用 CompoundTag.merge(...) 构建这个 tag,所以越靠后的层会覆盖越靠前的同名标量值。这和 trigger 输出合并用的 TagUtils.mergeTags(...) 不是同一个规则。

在 PmmoJS 回调里:

  • ctx.getSettings() 包装的是合并后的 source tag。
  • ctx.getResult() 只存在于 start(),初始为空。
  • PmmoJS 会把 start() 中的 ctx.getResult() 副本返回给 PMMO。
  • JS 友好的 tick()stop() overload 会返回空 tag,所以生命周期状态要写进 ctx.getSettings()

这个区别很关键:

PmmoJS.registerPerk(event => {
event
.create('kubejs:heated_pickaxe', PMMOPerkSide.SERVER)
.withSkill('mining')
.withDuration(100)
.withInt('heat_per_tick', 1)
.start(ctx => {
// PMMO 会在 start() 返回后把 settings 复制进 active tick schedule,
// 所以 tick() 和 stop() 能读到这个值。
ctx.getSettings().putInt('heat', 0)
// 这是给同一执行趟次中后续 perk 看的“一次性输出”。
ctx.getResult().putBoolean('heated_pickaxe_started', true)
})
.tick(ctx => {
const settings = ctx.getSettings()
settings.putInt('heat', settings.getIntOr('heat', 0) + settings.getIntOr('heat_per_tick', 1))
})
.stop(ctx => {
ctx.getSettings().remove('heat')
})
.register()
})

如果把 heat 写进 ctx.getResult()tick() 读不到它。如果把 heated_pickaxe_started 只写进 ctx.getSettings(),同一执行趟次中后面的 perk 不会通过 PMMO 的 output merge 收到它。

PerkTagJS

PerkTagJSCompoundTag 的 JS 友好包装。

读取:

  • keys()has(key)hasCompound(key)
  • getString(key)getStringOr(key, fallback)
  • getInt(key)getIntOr(key, fallback)
  • getLong(key)getLongOr(key, fallback)
  • getFloat(key)getFloatOr(key, fallback)
  • getDouble(key)getDoubleOr(key, fallback)
  • getBoolean(key)getBooleanOr(key, fallback)
  • getOrCreateCompound(key)

写入:

  • remove(key)clear()
  • putString(key, value)putBoolean(key, value)
  • putInt(key, value)putLong(key, value)
  • putFloat(key, value)putDouble(key, value)
  • putCompound(key, value)
  • putStringList(key, values)
  • putNumberList(key, values)
  • merge(value)
  • copy()

PMMO 辅助读取:

  • getSkill() 读取 skill
  • getResolvedLevel() 读取注入后的 level
  • getCooldown() 读取 cooldown
  • getDuration() 读取 duration
  • getChance() 读取 chance

三种合并模型

PMMO 不是所有地方都用同一种合并规则。

位置合并行为
PerkRegistry.executePerk 中的 perk source tagCompoundTag.merge(...) 依次合并 defaults、config、incoming event data、prior perk output。后面的标量值覆盖前面的标量值。
EventTriggerRegistry.executeEventListeners 中的 trigger listener outputTagUtils.mergeTags(...) 会把同名数字相加。非数字同名键保留更早 output 的值。is_cancelled 额外按“只要 true 就保持 true”的逻辑处理。
PmmoJS XP helperXP map 会通过 APIUtils.serializeAwardMap(...) 编码到 serialized_award_map。除非你完全匹配 PMMO codec 格式,否则不要手写这个 compound。
PerkTagJS.merge(value)调用的是 Minecraft 的 CompoundTag.merge(...),不是 PMMO 会累加数字的 TagUtils.mergeTags(...)

实际后果是:两个 trigger listener 都写入 int 类型的 bonus_roll 时,PMMO 的 trigger output 可以把它们相加;两个 perk source layer 都写入 cooldown 时,后写入的那层生效。

Trigger Context

PmmoJS.trigger(type, event => {}) 包装的是 PMMO trigger registry。规则依赖 PMMO EventType 的实时状态时用它。

PmmoJS.trigger(EventType.BLOCK_BREAK, event => {
const player = event.getPlayer()
if (!player) return
if (player.stages.has('festival_mining_bonus')) {
event.addXpAward('mining', 25)
}
if (player.isCreative()) {
event.setCancelled(true)
}
})

优先使用包装方法:

  • getContext*putContext* 读写普通 context 字段。
  • getXpAwards()setXpAwards(map)setXpAward(skill, amount)addXpAward(skill, amount)clearXpAwards() 管理 XP map。
  • setCancelled(true) 告诉 PMMO 跳过这个 trigger 的处理。

XP helper 存在的原因是 PMMO 会把 XP 奖励放在 serialized_award_map 下,而且这个 compound 必须由 APIUtils.serializeAwardMap(...) 生成。普通 JS 对象 { mining: 25 } 不是 PMMO 期望的 NBT 形状。

通过 PmmoHelper.registerTriggerListener(...) 注册的 raw direct listener 更底层。PMMO 期望返回的 tag 包含 is_cancelled,否则上游 registry 会把 listener 输出视为无效。除非你确实需要 startup-time 的直接注册,否则优先用 PmmoJS.trigger(...)

Internal Context

PmmoJS.internal(type, event => {}) 覆盖的是不走 trigger registry 的 PMMO handler:维度旅行、登录、骑乘、活塞、爆炸、玩家死亡、酿造、睡眠等。

Internal hook 初始会拿到一个新的空 CompoundTag。桥接层可能会在脚本运行前写入 hook 专属键:

  • EXPLOSIONaffectedBlockCountaffectedEntityCount
  • POTION_BREWalreadyTrackedmarkBrewed,有时还有 serialized_award_map

Internal hook 用来控制 PMMO handler,不应该替代普通对象数据:

PmmoJS.internal(PMMOInternalType.POTION_BREW, event => {
if (event.getContextBoolean('alreadyTracked')) {
return
}
event.addXpAward('alchemy', 10)
event.putContextBoolean('markBrewed', true)
})

event.skipPmmo() 表示游戏行为继续,但 PMMO handler 跳过。event.deny() 会在可取消时取消被包装的 Forge 行为,并且跳过 PMMO。

XP Event Context

PmmoJS.xp(...) 在 PMMO 改变玩家 XP 时触发。它的 getContext() 暴露 PMMO 原始 XP event context,所以适合做观察或很小范围的取消逻辑。不要把 XP hook 当成定义对象奖励的主入口。稳定奖励放在 PmmoJS.settings(...);运行时追加通常放在 PmmoJS.trigger(...)PmmoJS.internal(...),并使用 XP award helper。

PmmoJS.xp(event => {
if (event.getSkill() === 'mining' && event.isLevelUp()) {
event.getEntity().tell(`Mining level: ${event.endLevel()}`)
}
})

复合示例

这个例子把对象数据、NBT globals、自定义 perk 和运行时 trigger 串起来,但每一层只负责自己的事。实际整合包里要按脚本阶段拆开写。

// startup_scripts/pmmo_perks.js
PmmoJS.registerPerk(event => {
event
.create('kubejs:manyullyn_focus', PMMOPerkSide.SERVER)
.withSkill('mining')
.withDuration(80)
.withInt('bonus_xp', 10)
.start(ctx => {
ctx.getSettings().putBoolean('active', true)
ctx.getResult().putBoolean('manyullyn_focus_active', true)
})
.stop(ctx => {
ctx.getSettings().putBoolean('active', false)
})
.register()
})
// server_scripts/pmmo_progression.js
PmmoJS.globalsConfig(event => {
event.addPath('tool_materials', 'tic_materials[]')
event.addConstant('manyullyn', 'tconstruct:manyullyn')
})
PmmoJS.settings(event => {
event
.item('tconstruct:pickaxe')
.override(true)
.setRequirement(ReqType.TOOL, 'mining', 5)
.setXp(EventType.BLOCK_BREAK, 'mining', 4)
.nbtRequirement(ReqType.TOOL)
.additive(true)
.newCase('#tool_materials')
.equals('#manyullyn', 'mining', 25)
.done()
})
PmmoJS.perksConfig(event => {
event
.addPerk(EventType.BLOCK_BREAK, 'kubejs:manyullyn_focus', 'mining')
.withMinLevel(30)
.build()
})
PmmoJS.trigger(EventType.BLOCK_BREAK, event => {
const player = event.getPlayer()
if (player && player.stages.has('mining_trial')) {
event.addXpAward('mining', 10)
}
})

静态 settings 负责普通成长。globalsConfig 只让 NBT 规则可读。自定义 perk 把生命周期状态放在 ctx.getSettings(),把一次性标记放在 ctx.getResult()。trigger 只处理任务阶段带来的临时 XP 加成。

相关页面