跳转到正文

事件 ​

在 manifest.json 声明订阅后,Host 将事件交给 Plugin.on_event(ctx, event)。SDK 不自动确认事件,插件必须在完成处理后显式 ack() 或提交事件事务。

订阅与可用事件 ​

text
"subscriptions": [
  {"name": "chat", "kind": "chat.received", "delivery": "durable"}
]

这是 manifest 的字段片段,完整结构见权限与 manifest。name 是该插件内的订阅名,kind 是事件类型,当前使用 delivery: "durable"。

kindevent.payload["payload"] 的字段
chat.receivedplayer: str、message: str;xuid: str 可能缺失。
player.list_addeduuid: str、name: str、entity_unique_id: int;xuid: str 可能缺失。
player.list_removeduuid: str。不提供姓名,不能直接读取 name。
actor.observed实体观察变化;保留连接、游标、实体引用与已知字段,见实体。
environment.observedconnection_id/epoch/revision、变化来源及 snapshot,见环境。

实体与环境事件使用原生 gameEvents 来源,并需各自订阅授权。玩家名单事件来自已观察到的 PlayerList 变化;初始名单快照不承诺为每个在线玩家补发加入事件。不能用未收到离开事件证明玩家仍在线。

聊天事件只覆盖接入源提供的玩家聊天;命令反馈与机器人 say 不应当作这条聊天事件流的测试输入。聊天来源可用不等于玩家名单事件来源可用;授权与来源检查见权限。

事件对象 ​

Event(context, payload: EventPayload) 由 Worker 创建;event.context 是所属 PluginContext,event.kind 是类型字符串。event.payload 是外层投递字典,里面另有一个 payload 保存游戏事件内容:

text
EventPayload = {
  subscription_id: str,
  delivery_token: str,
  event_id: str,
  boot_id: str,
  session: str,
  ingress_seq: int,
  kind: str,
  payload: dict
}

使用 Host 提供的 ID 和 token,不自行构造。event_id 标识事件,delivery_token 标识这次投递;它们不能互换。session 是游戏会话,不能拿安装名或安装 generation 替代。

python
async def on_event(self, ctx, event):
    if event.kind == "chat.received":
        body = event.payload["payload"]
        ctx.log.info("收到聊天,字数=%d", len(body["message"]))
    await event.ack()

这个片段只记录日志并确认事件。如果需要保存计数或登记奖励动作,使用事务,避免先 ACK 再执行可能丢失的业务:

python
async def on_event(self, ctx, event):
    tx = await event.transaction()
    tx.increment("received")
    await tx.save()

await event.transaction(*, commit_id=None) 返回含该事件确认的 Transaction,await event.ack() 返回一次无状态修改的事件提交回执(commit_id/revision/duplicate/operation_ids)。Event.transaction 没有 keys 参数;需要选择性读取时使用 await ctx.storage.transaction(event=event.payload, keys=[...])。事务不确定时按错误页保留并核对原提交,不能新建事务重新计数。

聊天命令路由 ​

默认前缀为 !。使用路由器还需申请 permissions.players: ["list"],它会把聊天中的姓名及非空 XUID 与当前缓存玩家名单精确核对。

python
from neomega_runtime import Plugin, ChatArgumentError

class MyPlugin(Plugin):
    async def on_start(self, ctx):
        @ctx.chat.command("帮助", aliases=("help",),
                          description="查看帮助", usage="!帮助 [主题]",
                          min_args=0, max_args=1)
        async def help_command(command):
            # command.args: tuple[str, ...]
            # command.player: 含 name/uuid/xuid 的玩家字典
            # command.event: 当前 Event
            ctx.log.info("收到帮助命令,参数数=%d", len(command.args))
            await command.event.ack()

    async def on_event(self, ctx, event):
        try:
            handled = await ctx.chat.dispatch(event)
        except ChatArgumentError as exc:
            ctx.log.info("参数数量错误,用法:%s", exc.command.usage)
            await event.ack()
            return
        if not handled:
            await event.ack()

if __name__ == "__main__":
    MyPlugin().run()

此示例不向游戏发送文字。消息 !help "two words" 会产生一个参数 two words。命令名区分大小写,长度 1–64,接受 Unicode 字母、数字、ASCII 下划线和连字符,支持中文;参数按引号规则解析,不执行 shell。

command.name 是规范名,command.invoked_as 是实际输入名。list_commands() 返回注册顺序的元数据,get_command(name) 查规范名或别名,unregister(name) 删除该命令及全部别名;帮助文字由插件自行展示。

dispatch 在已注册处理器完成后返回 True;无关事件或未知命令返回 False。参数引号错误、找不到玩家、身份冲突或处理器异常会向上传递。插件应明确决定这些输入是记录后确认,还是保留待处理;不要用吞掉所有异常的方式继续发奖。

需要其他前缀时,在注册前设置 ctx.chat = ChatRouter(prefix="."),从 neomega_runtime 导入 ChatRouter。默认整条消息上限为 4096 UTF-8 字节。路由器只核对身份,不判断玩家能否管理服务器;管理命令还要自行检查业务权限。

按类型分发 ​

ctx.events 是本地 EventRouter。用 @ctx.events.on("actor.observed") 注册异步处理器,从 on_event 显式调用 await ctx.events.dispatch(event)。"*" 匹配本插件已经收到的所有类型,不会增加 Host 订阅。

python
async def on_start(self, ctx):
    @ctx.events.on("environment.observed")
    async def environment_changed(event):
        body = event.payload["payload"]
        ctx.log.info("环境修订号=%d", body["revision"])

async def on_event(self, ctx, event):
    await ctx.events.dispatch(event)
    await event.ack()

同一投递的处理器按注册顺序串行执行,异常立即传播并停止剩余处理器。上例在全部处理成功后 ACK;需要保存业务状态时改为事件事务。register(kind, handler) 返回可 unregister() 的注册对象,off(kind, handler=None) 删除匹配注册。路由器默认最多 256 个处理器,不允许并发或递归 dispatch。

等待玩家下一条回复 ​

ctx.chat_waiters.wait_for(player, *, timeout=30) 立即登记等待并返回 Future,每个精确姓名只能有一个等待。player 应来自身份解析,至少含非空 UUID 或 XUID。从 on_event 调用 await ctx.chat_waiters.feed(event);它会再次核对姓名、UUID 和 XUID,仅将匹配的事件交给等待者。

python
async def begin_dialogue(ctx, player):
    pending = ctx.chat_waiters.wait_for(player, timeout=30)

    async def receive():
        try:
            reply = await pending
        except TimeoutError:
            ctx.log.info("回复等待超时")
            return
        ctx.log.info("收到回复,字数=%d", len(reply.message))
        await reply.event.ack()

    try:
        ctx.spawn(receive(), name="chat-reply")
    except BaseException:
        pending.cancel()
        raise

async def on_event(self, ctx, event):
    if await ctx.chat_waiters.feed(event):
        return  # 匹配的事件交给 receive 确认。
    await event.ack()

begin_dialogue 在已激活回调中调用;它不会发送提问文字。等待放在 ctx.spawn 管理的任务中,不能阻塞负责接收下一条聊天的串行 on_event。Future 取消会释放该等待;超时抛 TimeoutError。停止或重载会清除等待,不跨重启保留,也不会自动 ACK 或重放业务。

顺序与耗时 ​

Worker 串行运行事件回调,后台任务可与回调同时运行。长时间等待会推迟后续事件,因此短回调负责校验、保存与登记动作,耗时处理使用受管理任务。切勿把事件 ACK 与后台任务启动当作可靠业务提交;需要持久业务时先完成事务。

Neomega · 使用、开发与参考文档