Skip to content

事件

一个接口订阅一切:LeviLamina 总线上的事件、加载器自己钩出来的事件、别的模组发布的事件。

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

ctx.server()
    .subscribe_event(names::PLAYER_JOIN, EventPriority::Normal, |ev| {
        if let Some(p) = ev.player() {
            Player::broadcast(&format!("§e{} 加入了游戏", p.name));
        }
    })?
    .forget();

三步

1. 挑事件名

levilamina::event::names 里的常量。上游改名时你会得到编译错误而不是运行时 Err

常量表全在 Event API

不在表里的事件直接传字符串——支持唯一后缀匹配,写 "PlayerAddExperienceEvent" 就行,不用写全 ll::event::PlayerAddExperienceEvent

服务器里 /levirs events 能列出当前全部可订阅的 id。

2. 挑优先级

rust
pub enum EventPriority { Highest = 0, High = 1, Normal = 2, Low = 3, Lowest = 4 }

数字小的先跑。

你想干什么用哪档
拦截 / 否决(保护类模组)Highest
修改内容High
一般业务Normal
观察最终结果、统计Lowest

3. 别忘了 forget

rust
ctx.server().subscribe_event(...)?.forget();

不接返回值的话 Listener 当场被 drop,订阅立刻失效。

读事件数据

回调收到 &mut EventRef。数据是 SNBT,两种读法:

rust
// 直接看原文(调试时最有用)
println!("{}", ev.snbt());

// 结构化
let v = ev.value()?;
let x = v.get("x").and_then(|n| n.as_i64()).unwrap_or(0);
let name = v.path("_player.name").and_then(|n| n.as_str()).unwrap_or("?");

不知道载荷长什么样就先打印

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

比翻头文件快。

玩家身份

大部分玩家相关的事件里,桥接会拼一个 _player 块进去:

rust
if let Some(id) = ev.player() {
    println!("{} {} {}", id.name, id.xuid, id.uuid);
}

// 或者直接拿句柄
if let Some(p) = ev.player_handle() {
    p.send_message("嗨")?;
    let (x, y, z) = p.get_actor()?.pos()?;
}

player_handle() 优先按 XUID 构造,拿不到才退回名字。

改事件数据

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| {
    let Ok(v) = ev.value() else { return };
    let (Some(x), Some(z)) = (
        v.get("x").and_then(|n| n.as_i64()),
        v.get("z").and_then(|n| n.as_i64()),
    ) else { return };

    if 在保护区内(x, z) {
        ev.cancel();
        if let Some(p) = ev.player_handle() {
            let _ = p.tell("§c这里不能挖", MessageType::Tip);
        }
    }
})?.forget();

对不可取消的事件调 cancel() 不报错也不生效。哪些能取消见 常量表

写保护类模组的常见坑

破坏保护只有一个事件

没有 PlayerDestroyingBlockEvent。放置那一对确实是 PlayerPlacingBlockEvent(前置)+ PlayerPlacedBlockEvent(后置),但破坏只有一个 PlayerDestroyBlockEvent——过去式的名字,但它就是可取消的前置事件。

别漏了「不需要点击」的干扰方式

一个既不能破坏、不能放置、不能交互、不能攻击的访客,照样可以:

  • 撞开实体PlayerPushEntityEvent)——把羊全赶出羊圈,或者把船顶进虚空
  • 丢东西PlayerDropItemEvent)——掉落物刷屏、卡实体上限
  • 从背包界面拖出物品——和 Q 键是两条不同的代码路径,PlayerDropItemEvent 两条都钩了

全部锁死之后剩下的就是这几样。

命令事件只报玩家

ExecutingCommandEvent 不上报控制台、命令方块和其他非玩家来源。这是故意的——拦掉控制台会把服主锁在自己服务器外面。

高频事件

HopperTransferEvent、移动类事件一秒能触发几千次。

  • 回调里不要打日志
  • 不要做 I/O、不要开数据库事务
  • 先做最便宜的判断早退
rust
ctx.server().subscribe_event(names::HOPPER_TRANSFER, EventPriority::Lowest, |ev| {
    let Ok(v) = ev.value() else { return };
    let Some(x) = v.get("x").and_then(|n| n.as_i64()) else { return };

    if !我关心的坐标(x) { return; }      // 早退

    // 到这里才做贵的事
})?.forget();

别的模组的事件

subscribe_event 订的是引擎总线。模组之间自己约定的消息走 Bus

rust
use levilamina::bus;

bus::subscribe("plot:enter", |_topic, payload| {
    println!("{payload}");
    false
})?.forget();