外观
事件
在 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"。
kind | event.payload["payload"] 的字段 |
|---|---|
chat.received | player: str、message: str;xuid: str 可能缺失。 |
player.list_added | uuid: str、name: str、entity_unique_id: int;xuid: str 可能缺失。 |
player.list_removed | uuid: str。不提供姓名,不能直接读取 name。 |
actor.observed | 实体观察变化;保留连接、游标、实体引用与已知字段,见实体。 |
environment.observed | connection_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 与后台任务启动当作可靠业务提交;需要持久业务时先完成事务。