脚本系统
Baikiruto 当前默认内置的脚本类型是 Fluxon,绝大多数服主日常仍然只需要写 Fluxon。
不过这次源码已经把脚本处理改成“脚本类型注册表 + 脚本源”结构,所以配置层现在既兼容旧的纯字符串写法,也支持显式声明 type 的结构化写法。
脚本类型
写法 1:直接写脚本文本(兼容旧配置)
scripts:
build: |
return item
这种写法等价于“type: fluxon + 脚本内容”,适合继续沿用旧配置。
写法 2:结构化脚本源(推荐跟着新默认示例写)
scripts:
build:
type: fluxon
script: |
return item
event:
on_use:
type: fluxon
script: |
&ops.setData("last_trigger", "use")
return item
data-mapper:
durability_line:
type: fluxon
script: |
def current = &data["durability_current"] ?: &data["durability"] ?: 0
def max = &data["durability"] ?: 0
return current.toString() + "/" + max.toString()
这套结构当前可用于 scripts、event、metas.<id>.scripts、i18n.<locale>.scripts、i18n.<locale>.event 和 data-mapper。
实际规则:
type默认就是fluxonengine可以当成type的别名script、source、content三个字段都能放脚本文本- 如果你把脚本写成字符串列表,loader 会按换行拼成一段完整脚本
- 脚本类型会先规范化:转小写,并把
-变成_ - 如果配置里写了未注册的脚本类型,这条脚本会被跳过,并在控制台输出
log-script-type-missing
生命周期脚本
在物品数据流的特定阶段执行:
scripts:
build:
type: fluxon
script: |
return item
release:
type: fluxon
script: |
&ops.setData("last_release", "release")
return item
release_display: |
return item
drop: |
return item
上面故意混用了两种写法:build / release 展示新结构,release_display / drop 保留旧写法,实际两种都能正常加载。
事件脚本
响应玩家交互和游戏事件:
event:
on_right_click:
type: fluxon
script: |
&ops.setCooldown(80)
&ops.setData("last_trigger", "right_click")
return item
on_attack:
type: fluxon
script: |
&ops.setData("last_trigger", "attack")
return item
on_damage:
type: fluxon
script: |
&ops.damage(1)
&ops.setData("last_trigger", "damage")
return item
如果你不打算区分脚本类型,继续写成旧的 on_right_click: | 这种形式也完全兼容。
触发器完整列表
源码中定义了 31 个触发器。每个触发器都支持别名写法,所以你既可以写标准 key,也可以沿用 on_xxx 这类老习惯。
生命周期触发器
| 触发器 | 配置写法 | 说明 |
|---|---|---|
BUILD | build / on_build | 物品构建时执行 |
RELEASE | release / on_release | 转换为 ItemStack 时执行 |
RELEASE_DISPLAY | release_display / on_release_display | 显示渲染后执行 |
DROP | drop / on_drop | 物品销毁时执行(逆序) |
常用交互触发器
| 触发器 | 配置写法 | 说明 |
|---|---|---|
USE | on_use / onuse | 使用物品;右键使用和消耗流程都会命中 |
INTERACT | on_interact / oninteract | 玩家交互(左右键空气 / 方块的总入口) |
LEFT_CLICK | on_left_click / onleftclick | 左键点击 |
RIGHT_CLICK | on_right_click / onrightclick | 右键点击 |
RIGHT_CLICK_ENTITY | on_right_click_entity / onrightclickentity | 右键点击实体 |
CONSUME | on_consume / onconsume | 消耗物品(吃下 / 喝下) |
PICKUP | on_pickup / on_pick / onpickup / onpick | 拾取物品 |
SELECT | on_select / onselect | 快捷栏切换到该物品;玩家进服、重生、切世界后的刷新流程也会补触发 |
ASYNC_TICK | on_async_tick / on_tick / onasynctick / ontick | 异步周期轮询,可配间隔与条件过滤 |
注意:ASYNC_TICK 在异步线程执行。全局默认间隔来自 config.yml 的 operations.async-tick.default-interval(默认 100 tick),单个物品还能用 meta.async-tick 覆盖;不要直接在里面操作 Bukkit API。
战斗与场景触发器
| 触发器 | 配置写法 | 说明 |
|---|---|---|
ATTACK | on_attack / on_sword / onattack / onsword | 攻击实体 |
DAMAGE | on_damage / ondamage | 物品耐久受损时触发 |
BLOCK_BREAK | on_block_break / onblockbreak | 破坏方块 |
ITEM_BREAK | on_item_break / onitembreak | 物品耐久归零损坏 |
DEATH | on_death / ondeath | 玩家死亡时,对身上受追踪的 Baikiruto 物品逐个触发 |
KILL | on_kill / onkill | 玩家击杀实体时触发 |
HURT | on_hurt / onhurt | 玩家受伤时,对身上受追踪的 Baikiruto 物品逐个触发 |
SHOOT | on_shoot / onshoot | 发射弹射物时触发 |
PROJECTILE_HIT | on_projectile_hit / onprojectilehit | 自己发出的弹射物命中时触发 |
SNEAK | on_sneak / onsneak | 潜行切换时,对身上受追踪的 Baikiruto 物品逐个触发 |
SPRINT | on_sprint / onsprint | 疾跑切换时,对身上受追踪的 Baikiruto 物品逐个触发 |
JUMP | on_jump / onjump | 跳跃检测命中时,对身上受追踪的 Baikiruto 物品逐个触发 |
RESPAWN | on_respawn / onrespawn | 玩家重生后,对身上受追踪的 Baikiruto 物品逐个触发 |
背包与装备触发器
| 触发器 | 配置写法 | 说明 |
|---|---|---|
SWAP_TO_MAINHAND | on_swap_to_mainhand / onswaptomainhand | 主副手交换后进入主手 |
SWAP_TO_OFFHAND | on_swap_to_offhand / onswaptooffhand | 主副手交换后进入副手 |
INVENTORY_CLICK | on_inventory_click / on_click / oninventoryclick / onclick | 背包点击,包括数字键热键切换 |
EQUIP | on_equip / onequip | 通过背包装备槽点击 / Shift 点击穿戴时触发 |
UNEQUIP | on_unequip / onunequip | 通过背包装备槽卸下时触发 |
槽位上下文的实际用法
DEATH、HURT、SNEAK、SPRINT、JUMP、RESPAWN、EQUIP、UNEQUIP、ASYNC_TICK 这类触发器,脚本上下文里会附带 slot,可直接按槽位做分流。
常见槽位值:
MAINHANDOFFHANDHEADCHESTLEGSFEET
event:
on_equip: |
if (slot == "CHEST") {
&ops.setData("last_equip_slot", slot)
}
return item
on_hurt: |
if (slot == "OFFHAND") {
&ops.setData("last_guard_slot", slot)
}
return item
ASYNC_TICK 还会额外注入 slot_index,方便你区分背包格位。主手会给 MAINHAND,其他快捷栏格会给 HOTBAR,主背包区会给 INVENTORY,护甲与副手则会给具体槽位。
脚本上下文变量
所有脚本都可以访问以下变量:
| 变量 | 类型 | 说明 |
|---|---|---|
&player | Player | 当前玩家 |
&sender | CommandSender | 命令发送者 |
&item | ItemStack | 物品实例(可直接修改) |
&stream | ItemStream | 数据流对象 |
&ops | ItemScriptOps | 操作 API |
&event | Event | 原始 Bukkit 事件对象 |
&ctx | Map | 完整上下文数据 |
&itemId | String | 物品 ID |
&trigger | String | 触发器名称(枚举名小写,如 left_click) |
&slot | String | 槽位上下文(可选;如 MAINHAND、OFFHAND、CHEST) |
slot_index | Int | 背包槽位索引(可选;主要用于 ASYNC_TICK) |
&locale | String | 玩家客户端语言(可选,仅在玩家有 locale 时存在) |
此外,&ctx 中的所有键值对也会被展开注入到脚本变量中。
ItemScriptOps API
&ops 提供了便捷的物品操作方法。
数据操作
// 读取数据
def value = &ops.data("key")
// 设置数据(单键值对)
&ops.setData("key", "value")
&ops.setData("player.level", 10)
耐久系统
// 扣除耐久(返回 Boolean,true 表示物品已被销毁)
def destroyed = &ops.damage(1)
// 获取当前耐久
def current = &ops.durability()
// 获取最大耐久
def max = &ops.durabilityMax()
// 设置耐久
&ops.setDurability(100)
冷却系统
// 获取剩余冷却(tick)
def remaining = &ops.cooldown()
// 设置冷却(tick)
&ops.setCooldown(80)
// 检查是否在冷却中
if (&ops.cooldown() > 0) {
&player.sendMessage("§c冷却中...")
return
}
绑定系统
// 获取物品所有者
def owner = &ops.owner()
// 检查是否为所有者
if (!&ops.isOwner()) {
&player.sendMessage("§c此物品不属于你!")
return
}
// 绑定所有者(不传参时绑定当前玩家)
&ops.bindOwner(&player.name)
&ops.bindOwner()
信号系统
// 标记信号
&ops.signal("CUSTOM_FLAG")
// 检查信号
if (&ops.hasSignal("DATA_MAPPED")) {
// 数据已映射
}
重建物品
// 使用最新配置重建物品
&ops.rebuild()
脚本返回值
脚本的返回值会影响物品行为。源码会先检查 &item 是否被直接修改,再检查脚本返回值。
返回 ItemStack
scripts:
build: |
def newItem = new ItemStack(Material.DIAMOND)
return newItem
替换当前物品,并执行显示锁定保护。
返回 ItemStream
scripts:
build: |
&stream.setDisplayName("§6新名称")
return &stream
用返回的 stream 的 snapshot 替换当前物品,同时合并其 runtimeData。
返回 Map
scripts:
build: |
return ["key1": "value1", "key2": 123]
将 map 中的每个键值对写入 runtimeData。
直接修改 &item
除了通过返回值,你也可以直接修改 &item 引用。源码会在脚本执行后检查 &item 是否发生了变化,如果变化了也会同步回物品。
scripts:
build: |
def meta = &item.getItemMeta()
meta.setDisplayName("§6直接修改")
&item.setItemMeta(meta)
事件取消 (!!)
!! 是配置层面的声明式事件取消,写在触发器 key 的末尾。
event:
on_right_click!!: |
// 执行脚本,同时取消底层 Bukkit 右键事件
&player.sendMessage("已拦截")
真实行为:
- 在
ItemScriptHooks解析阶段,如果触发器 key 以!!结尾,该触发器会被标记为cancelEvent = true - 脚本执行后,
ItemScriptActionDispatcher会将原始 Bukkit 事件设为isCancelled = true - 这是配置层声明,不是脚本返回值机制
注意:!! 只对底层有 Cancellable 接口的 Bukkit 事件生效。对 build / release / release_display 这类生命周期脚本,不要理解成"取消 BUILD/RELEASE 阶段"。
数据映射器
data-mapper 在 toItemStack() 早期执行,用于动态计算运行时数据。它现在和 scripts / event 共用同一套脚本源解析规则:
data:
durability: 240
data-mapper:
durability_line:
type: fluxon
script: |
def current = &data["durability_current"] ?: &data["durability"] ?: 0
def max = &data["durability"] ?: 0
return current.toString() + "/" + max.toString()
lore:
item_description:
- "耐久: {durability_line}"
你也可以继续把 mapper 直接写成字符串或字符串列表;当前 loader 会统一整理成“脚本类型 + 脚本文本”的内部格式。
注意时机:
data-mapper在ItemReleaseEvent之前运行durability_bar、cooldown_remaining这类显示期变量尚未注入- 推荐在 mapper 中使用原始运行时数据
Fluxon 内置函数
Baikiruto 会在 Fluxon 运行时注册一些内置函数,让你在脚本中直接调用第三方插件的能力。目前支持 MythicMobs。
MythicMobs
前提:服务端安装了 MythicMobs 插件。如果 MythicMobs 没有加载,这组函数不会被注册,脚本中调用会报错。
通过 mythic() 或 mythicmobs() 获取 API 对象,然后调用其方法:
mm = mythic()
MythicApi 方法一览
状态查询
| 方法 | 返回值 | 说明 |
|---|---|---|
isLoaded() | Boolean | MythicMobs 插件是否已加载 |
怪物操作
| 方法 | 返回值 | 说明 |
|---|---|---|
getMob(entity) | Mob / null | 通过实体获取 MM 怪物对象 |
getMobByUUID(uuid) | Mob / null | 通过 UUID 字符串获取 MM 怪物对象 |
getMobType(id) | MobType / null | 通过怪物类型 ID 获取怪物类型定义 |
getMobIds() | List<String> | 获取所有已注册的怪物类型 ID 列表 |
spawnMob(id, location, level) | Mob / null | 在指定位置生成指定等级的 MM 怪物 |
isMythicMob(entity) | Boolean | 判断实体是否为 MM 怪物 |
物品获取
| 方法 | 返回值 | 说明 |
|---|---|---|
getItem(id) | ItemStack / null | 通过 MM 物品 ID 获取物品 |
getItemWithPlayer(id, player) | ItemStack / null | 通过 MM 物品 ID 获取物品(带玩家上下文) |
getItemId(itemStack) | String / null | 获取物品的 MM 物品 ID |
getItemIds() | List<String> | 获取所有已注册的 MM 物品 ID 列表 |
技能释放
| 方法 | 返回值 | 说明 |
|---|---|---|
castSkill(caster, skillName, power) | void | 以实体为施法者释放技能 |
castSkillAt(caster, skillName, target, power) | void | 以实体为施法者,对目标释放技能 |
参数说明:
caster:施法者实体(Entity)skillName:MythicMobs 技能名(String)power:技能强度(Float)target:目标生物(LivingEntity)
仇恨管理
| 方法 | 返回值 | 说明 |
|---|---|---|
addThreat(entity, target, amount) | void | 增加 MM 怪物对目标的仇恨值 |
reduceThreat(entity, target, amount) | void | 减少 MM 怪物对目标的仇恨值 |
参数说明:
entity:MM 怪物实体(Entity)target:仇恨目标(LivingEntity)amount:仇恨值变化量(Double)
Mob 扩展函数
当你通过 getMob() 拿到一个 Mob 对象后,可以调用以下扩展函数:
| 方法 | 返回值 | 说明 |
|---|---|---|
getId() | String | 怪物类型 ID |
getDisplayName() | String | 怪物显示名称 |
getLevel() | Double | 怪物等级 |
getEntity() | Entity | 底层 Bukkit 实体 |
getFaction() | String | 怪物阵营 |
getStance() | String | 怪物姿态 |
MobType 扩展函数
当你通过 getMobType() 拿到一个 MobType 对象后,可以调用以下扩展函数:
| 方法 | 返回值 | 说明 |
|---|---|---|
getId() | String | 怪物类型 ID |
getDisplayName() | String | 怪物类型显示名称 |
getEntityType() | String | 底层实体类型 |
spawn(location, level) | Entity | 在指定位置以指定等级生成该类型怪物 |
完整示例
event:
on_attack: |
mm = mythic()
# 判断被攻击的实体是否为 MM 怪物
if &mm :: isMythicMob(&event.getEntity()) {
mob = &mm :: getMob(&event.getEntity())
&player.sendMessage("§e你攻击了 MM 怪物: " + &mob :: getDisplayName() + " (Lv." + &mob :: getLevel() + ")")
}
return item
on_right_click: |
mm = mythic()
# 右键释放 MythicMobs 技能
&mm :: castSkill(&player, "Fireball", 1.0)
&player.sendMessage("§a释放了火球术!")
return item
on_kill: |
mm = mythic()
# 击杀时检查是否为 MM 怪物,并读取信息
if &mm :: isMythicMob(&event.getEntity()) {
mob = &mm :: getMob(&event.getEntity())
&player.sendMessage("§6击杀了 " + &mob :: getId() + ",阵营: " + &mob :: getFaction())
}
return item
event:
on_right_click: |
mm = mythic()
# 在玩家位置生成一个 5 级的 SkeletonKing
spawned = &mm :: spawnMob("SkeletonKing", &player.getLocation(), 5.0)
if spawned != null {
&player.sendMessage("§a召唤成功!")
}
return item
event:
on_right_click: |
mm = mythic()
# 获取 MythicMobs 物品并给玩家
mmItem = &mm :: getItemWithPlayer("CustomSword", &player)
if mmItem != null {
&player.getInventory().addItem(mmItem)
&player.sendMessage("§a获得了 MythicMobs 物品!")
}
return item
注意事项
- 所有 API 调用内部都做了
runCatching保护,如果 MythicMobs 内部抛异常,方法会返回null而不是让脚本崩溃 mythic()和mythicmobs()是等价的,用哪个都行- 仇恨操作(
addThreat/reduceThreat)的第一个参数必须是 MM 怪物实体,不能传玩家 - 如果你的服务端没装 MythicMobs,这些函数不会被注册;脚本中调用
mythic()会直接报运行时错误
性能优化
脚本预热
启动时预编译所有脚本,减少首次执行延迟:
# config.yml
script:
preheat:
enabled: true
strategy: "ON_ENABLE"
batch-size: 64
配置项:
enabled:是否启用预热;默认truestrategy:预热策略;ON_ENABLE(启动时预热)或ON_DEMAND(按需预热)batch-size:每批预编译的脚本数量;默认64,推荐 1-256
物品重载时会自动清除脚本缓存。
异步执行
on_async_tick 事件在异步线程执行,适合做轻量状态刷新。当前实现要点:
- 调度器会持续扫描在线玩家背包,但只会对真正写了
on_async_tick的物品派发脚本 - 全局默认频率来自
operations.async-tick.default-interval,默认100tick - 单个物品可用
meta.async-tick.interval/ticks/period覆盖 - 还能通过
meta.async-tick.conditions限制潜行、疾跑、游泳、滑翔、飞行、着地、载具、燃烧和格挡等状态 - 也能按槽位、世界、游戏模式和权限过滤;
permissions写多个值时命中任意一个即可 - 仍然不要在里面直接操作 Bukkit API
meta:
async-tick:
interval: 20
conditions:
sprinting: true
worlds:
- world
game-mode: survival
permissions:
- baikiruto.async.fire
- baikiruto.async.admin
slots:
- mainhand
- offhand
event:
on_async_tick: |
&ops.setData("last_async_slot", slot)
&ops.setData("last_async_slot_index", slot_index)
return item
常见槽位值包括 MAINHAND、HOTBAR、INVENTORY、OFFHAND、HEAD、CHEST、LEGS、FEET。如果你只想扫装备中的物品,可以在 meta.async-tick.conditions.slots 里写 equipped 或 armor;如果你只想让主世界生效、只给生存模式玩家,或要求玩家带某个权限,也都可以直接写在 conditions 里。