Skip to content

Event — 事件监听

一个订阅接口打天下:给事件名和优先级,拿回一个 RAII 句柄。事件数据是 SNBT,可以读、可以改写、可以取消。

rust
use levilamina::prelude::*;
use levilamina::event::names;

ctx.server()
    .subscribe_event(names::PLAYER_CHAT, EventPriority::Normal, |ev| {
        if let Some(p) = ev.player() {
            println!("{} 说:{}", p.name, ev.snbt());
        }
    })?
    .forget();

订阅

rust
Server::subscribe_event(
    &self,
    event_id: &str,
    priority: EventPriority,
    handler: impl FnMut(&mut EventRef) + 'static,
) -> Result<Listener>

EventPriorityHighest=0High=1Normal=2Low=3Lowest=4。数值小的先跑。

想拦截别人的操作就用 Highest;只想观察最终结果用 Lowest

事件名可以只写后缀

引擎里的完整 id 长这样:ll::event::PlayerChatEvent。桥接支持唯一后缀匹配,所以直接写 "PlayerChatEvent" 就行。

只要后缀在全服注册的事件里唯一就能解析。不唯一会返回 Err,并且日志里会打印出所有相近的名字——所以名字写错不会静默失败。

优先用 names 里的常量

levilamina::event::names 下有全部已核实的事件名常量。用常量的好处是上游改名时你会得到一个编译错误,而不是一个运行时的 Err

Listener 的生命周期

rust
let listener = ctx.server().subscribe_event(...)?;
// listener 被 drop → 自动退订

listener.forget();   // 活到模组卸载

99% 的情况是在 on_enable 里注册然后 .forget()。需要动态开关的才留着句柄。

不要写 subscribe_event(...)?; 就完事

不接收返回值的话,Listener 当场就被 drop 了,订阅立刻失效。编译器会给一个 unused_must_use 警告,别忽略它。

在回调里能做什么

回调收到 &mut EventRef

API说明
ev.id() -> &str完整事件 id
ev.snbt() -> &str事件数据的 SNBT 原文
ev.value() -> Result<NbtValue>解析成结构化的值(含尚未提交的改动)
ev.player() -> Option<PlayerIdentity>事件携带的玩家身份 {name, xuid, uuid}
ev.player_handle() -> Option<Player>直接拿一个 Player 句柄
ev.set_snbt(snbt)整体替换事件数据
ev.set_value(&nbt)结构化写回
ev.cancel()取消(仅对可取消事件有效)

改一个字段

rust
ctx.server().subscribe_event(names::PLAYER_CHAT, EventPriority::High, |ev| {
    let Ok(mut v) = ev.value() else { return };
    if let Some(msg) = v.get("message").and_then(|m| m.as_str()) {
        if msg.contains("敏感词") {
            v.insert("message", NbtValue::String("[已屏蔽]".into()));
            ev.set_value(&v);
        }
    }
})?.forget();

value() 会带上本次回调里已经写过的改动,所以 读 → 改 → 写 可以串起来。

取消

rust
ctx.server().subscribe_event(names::PLAYER_DESTROY_BLOCK, EventPriority::Highest, |ev| {
    if 不该让他挖 {
        ev.cancel();
    }
})?.forget();

对不可取消的事件调 cancel() 不会报错,但也不会有效果——下面的表里标了哪些能取消。

事件名常量表

全部在 levilamina::event::names 下,也按域分成了 names::player / names::mob / names::server 三个子模块(两种写法等价)。

玩家事件

常量事件 id可取消备注
PLAYER_JOINPlayerJoinEvent
PLAYER_CONNECTPlayerConnectEvent
PLAYER_CHATPlayerChatEvent载荷 {name, message, _player},改 message 即改写发言
PLAYER_DISCONNECTPlayerDisconnectEvent
PLAYER_DIEPlayerDieEvent
PLAYER_RESPAWNPlayerRespawnEvent
PLAYER_JUMPPlayerJumpEvent
PLAYER_SPRINTPlayerSprintEvent
PLAYER_SWINGPlayerSwingEvent
PLAYER_ATTACKPlayerAttackEvent
PLAYER_PICK_UP_ITEMPlayerPickUpItemEvent
PLAYER_USE_ITEMPlayerUseItemEvent
PLAYER_INTERACT_BLOCKPlayerInteractBlockEvent
PLAYER_DESTROY_BLOCKPlayerDestroyBlockEvent破坏保护订这一个
PLAYER_PLACING_BLOCKPlayerPlacingBlockEvent前置
PLAYER_PLACED_BLOCKPlayerPlacedBlockEvent后置
PLAYER_SNEAKINGPlayerSneakingEvent前置
PLAYER_SNEAKEDPlayerSneakedEvent后置

没有 PlayerDestroyingBlockEvent

放置那一对是 PlayerPlacingBlockEvent(前置可取消)+ PlayerPlacedBlockEvent(后置),于是「破坏也该有 -ing 版本」看起来很自然。破坏不按这个规律:只有一个 PlayerDestroyBlockEvent,过去式的名字,但它就是那个可取消的前置事件。订它就够了,而且真的拦得住。

玩家事件(桥接 hook 实现)

下面这些不是 LeviLamina 总线上的事件,是加载器自己在 C++ 侧挂钩子做出来的。用法完全一样。

常量事件 id可取消载荷 / 备注
PLAYER_DROP_ITEMPlayerDropItemEvent{x,y,z,dim,item,randomly,viaInventoryUi,_player}。同时钩了 Q 键丢弃和背包界面拖出
PLAYER_START_DESTROY_BLOCKPlayerStartDestroyBlockEvent{x,y,z,face,_player}开始挖时触发,比 PLAYER_DESTROY_BLOCK 早。同步派发且在原函数之前,所以在回调里切快捷栏,破坏逻辑读到的就是换好的工具
PLAYER_CHANGE_DIMENSIONPlayerChangeDimensionEvent
PLAYER_OPEN_CONTAINERPlayerOpenContainerEvent
PLAYER_USE_ITEM_ONPlayerUseItemOnEvent对方块用物品
PLAYER_INTERACT_ENTITYPlayerInteractEntityEvent
PLAYER_RIDEPlayerRideEvent
PLAYER_SPAWN_PROJECTILEPlayerSpawnProjectileEvent
PLAYER_STEP_ON_PRESSURE_PLATEPlayerStepOnPressurePlateEvent
PLAYER_PUSH_ENTITYPlayerPushEntityEvent撞开实体。唯一一种不需要点击的干扰方式——做保护类模组时容易漏
PLAYER_CHANGE_GAME_MODEPlayerChangeGameModeEvent

生物事件

常量事件 id
SPAWNING_MOBSpawningMobEvent
SPAWNED_MOBSpawnedMobEvent
MOB_HURTMobHurtEvent
MOB_DIEMobDieEvent
ACTOR_HURTActorHurtEvent
FIRE_SPREADFireSpreadEvent

服务端与命令事件

常量事件 id可取消备注
EXECUTING_COMMANDExecutingCommandEvent载荷 {name, command, _player}command 是原始输入行(带斜杠)
EXECUTED_COMMANDExecutedCommandEvent已经执行完了
CONSOLE_OUTPUTTINGConsoleOutputtingEvent
CONSOLE_OUTPUTTEDConsoleOutputtedEvent
SERVER_STARTEDServerStartedEvent
SERVER_STOPPINGServerStoppingEvent
HOPPER_TRANSFERHopperTransferEvent桥接 hook。载荷 {x,y,z,slot,item,count,old_item,old_count}

EXECUTING_COMMAND 只报玩家发起的命令

控制台、命令方块和其他非玩家来源完全不上报。这是故意的:拦掉控制台会把服主锁在自己服务器外面,且没有恢复路径。

HOPPER_TRANSFER 没有维度字段

HopperBlockActor::setItem 那一层拿不到 BlockSource。要区分维度就按自己注册时记下的坐标认。另外它是高频事件,回调里别做重活。

事件名不在表里怎么办

表里是已核实的常量,不是全集。任何 LeviLamina 总线上的事件都能订,直接传字符串即可:

rust
ctx.server().subscribe_event("PlayerAddExperienceEvent", EventPriority::Normal, |ev| {
    println!("{}", ev.snbt());
})?.forget();

服务器里输入 /levirs events 可以列出当前全部可订阅的事件 id。

不知道某个事件的载荷长什么样,就先订上打印 ev.snbt() 看一眼——这比翻头文件快。