跳转到正文

用 Python 组合能力 ​

给玩家显示标题并播放声音,可以直接组合 ctx.notifications.title 和 ctx.player_actions.playsound;解释一个协议字段,可以使用生成模型写普通 Python 函数。先复用现有 SDK,只有多个插件需要共同管理对象或配额时,才增加独立的服务提供方(provider)。

本页的两个完整示例随开发工具包提供,均要求 Host API 1.3。它们由 Host 安装并启动,不能直接运行 main.py 来连接游戏。

选择能力的放置位置 ​

  • 计算与动作组合:布局、向量、文本、轨迹或协议字段计算,放在普通 Python 模块或能力库中,用纯函数接收数据、返回计算结果。组合多个已有动作时,放在 Python 插件或能力库中,接收 ctx 并构造 OperationIntent,由调用方登记和查询。
  • 共享对象、通道或配额:由可选 Python provider 管理,通过在线服务接纳多个插件的请求,并持久保存归属和结果。
  • 库存、容器和输入交互:库存引用、容器窗口、输入时序等使用 SDK 已有领域动作,由 Host 和运行内核在实际执行时检查连接与资源。

能力库可以显式接收 ctx 构造自己的客户端,无需给 PluginContext 增加新的业务属性。只做计算或组合动作时,不必再启动一个常驻 provider。服务依赖声明也不会自动安装 Python 包;插件需要按自己的发布方式携带依赖,并说明最低 Host 要求。

Go 运行内核继续管理连接、共享游戏事实、资源会话和时序敏感交互;Host 管理身份、权限、持久接纳与执行队列。Python 保存的观察结果是带来源的业务视图,不能代替这些执行时检查。例如,库存预览成功不代表物品已经预留,实际动作仍需重新核对。

示例一:聊天后显示标题与声音 ​

工具包目录:examples/native-feedback。在线玩家发送 !feedback 后,插件核对聊天身份,再准备标题“收到你的请求”和 random.orb 声音。组合函数只返回两个意图:

python
def prepare_feedback(ctx, name, key, deadline):
    return (
        ctx.notifications.title(
            name, "收到你的请求",
            idempotency_key=key + "_title", deadline=deadline,
        ),
        ctx.player_actions.playsound(
            name, "random.orb",
            idempotency_key=key + "_sound", deadline=deadline,
        ),
    )

name 来自聊天路由核对后的当前玩家,key 和绝对 deadline 由业务在首次处理时固定。函数没有发送动作;完整示例先保存原始意图,再把两个动作的接纳与聊天事件 ACK 放入同一次事务。两个游戏效果仍分别执行,不保证同时发生。

运行示例 ​

  1. 下载并解压工具包,在工具包目录检查和打包:

    sh
    python3 runtime-plugins.py lint examples/native-feedback
    python3 runtime-plugins.py pack examples/native-feedback native-feedback.zip
  2. 按安装与配置插件安装 ZIP,审阅 chat.received 订阅、players: ["list"] 和 framework.command.execute 授权,再启动插件。命令权限的范围比标题与声音这两个效果更广。

  3. 用在线玩家发送 !feedback,查看插件日志中的 operation ID 和状态,并检查该玩家的标题与声音。动作回执与客户端效果分别核对。

示例用 session + event_id 生成稳定业务键,重投递 token 不参与计算;原期限固定为首次处理后的 30 秒。再次遇到同一事件时,只查询原提交和动作结果,不延长期限或重新提交。已保存准备记录而暂时查不到接纳回执时,示例停止处理并保留记录,等待核对;查询不到不能证明旧请求不会稍后提交。

状态里的 feedback_… 记录包含原始意图、接纳回执和操作结果,包括 unknown。示例没有自动清理这些记录;持续使用时应安排有界的保留策略,在仍可能重投递或恢复的窗口内保留去重依据。通知参数见通知、标题与分段消息,事务写法见持久状态与服务。

示例二:只读解释区块发布半径 ​

工具包目录:examples/typed-radius-observer。它观察机器人连接的入站 NetworkChunkPublisherUpdate(ID 121),使用 neomega_runtime.protocol.packets.NetworkChunkPublisherUpdate 解释 Radius:

python
from neomega_runtime.protocol import packets


def describe_radius(model):
    if not isinstance(model, packets.NetworkChunkPublisherUpdate):
        raise TypeError("expected NetworkChunkPublisherUpdate")
    return {
        "radius_blocks": model.Radius,
        "radius_chunk_widths": model.Radius / 16,
    }

这里的 Radius 单位是方块:17 表示 17 方块,即 1.0625 个区块宽度。它不是已加载区块数,也不描述其他玩家客户端已经看见的范围;不要与 ChunkRadiusUpdated 的区块单位混用。

运行示例 ​

  1. 在工具包目录检查和打包:

    sh
    python3 runtime-plugins.py lint examples/typed-radius-observer
    python3 runtime-plugins.py pack examples/typed-radius-observer typed-radius-observer.zip
  2. 安装 ZIP,审阅 manifest 中的 raw_packet_directions: ["inbound"]、packet_preview_ids: [121] 及用途,再启动插件。它不请求世界写操作或包发送权限。

  3. 收到目标包后,日志会显示半径和源序列。未收到第一份有效目标包前,latest 为 None;安静的日志不能用默认半径补齐。

完整示例通过 ctx.packets.typed.open([packets.NetworkChunkPublisherUpdate], direction="inbound") 打开观察,用 ctx.spawn 管理循环,并在 finally 中关闭流。解码由 Core codec 完成,使用采集时的 connection_id 和 codec_revision;业务函数只解释模型字段,不维护另一套 Python wire codec。

latest 同时保留字段解释和完整来源事件,包含连接、revision、源序列和原始帧;它只存在于内存中。遇到 gap、关闭、解码失败或任务取消时,示例清空 latest 并退出观察,不自动重连、补帧或填默认值。入站授权允许读取该方向的原始 journal,Python 端的包号筛选只缩小处理范围,不缩小 Host 的授权范围。完整权限与流合同见协议包与观察。

多个插件共同使用一项能力 ​

普通多插件环境使用命令和已有领域动作,通过 Host 的共享执行队列写入世界;每个安装分别授权。typed 模型构造、codec 和观察各按自身权限使用,读取不要求成为独占写入者。

通用 typed/raw 发送需要固定 installation 的独占写入配置,并同时满足动作权限、逐包发送授权和连接检查;typed 发送还校验 codec revision。独占模式会拒绝其他 installation 的业务世界写入和 legacy HTTP 世界写入,不适合通过轮流申请所有权来实现普通多插件共享。通用发送的 dispatched 只证明已派发,不是服务器 ACK 或客户端效果,详见协议发送边界。

当几个插件确实需要共同管理一组展示对象或预算时,可以把它们交给一个 provider。provider 用自己的权限执行动作,调用者的权限不会自动借给它,因此服务需要明确限制可操作的对象、参数和预算。

可信身份与持久接纳 ​

所有者敏感的服务使用 @service(..., with_context=True),从 ServiceCallContext 获取 installation_id、generation、call_id、accepted_at 和 deadline,不要信任 arguments.owner。具体签名与错误见服务调用者身份。

稳定的 installation 用于持久归属,generation 用于区分当次运行。调用方在第一次提交前固定 request ID;provider 将其映射到所有者作用域内的 business ID,并绑定规范化请求摘要。同编号同请求查询原记录,同编号不同请求明确拒绝。更新或删除对象还要检查期望 revision,请求去重不能替代对象并发检查。

provider 的在线处理器应完成有界校验和持久接纳后尽快返回。回复 accepted 或启动后台处理前,保存请求编号、摘要、归属、对象 revision、目标状态和待执行工作的稳定编号;内存队列不能作为唯一接纳记录。需要安排世界动作时,用 Transaction.action,或业务事务的 tx.state.action(intent),把意图与状态一同提交,再查询返回的 operation_ids。

重启、期限与不确定结果 ​

accepted 表示 provider 已持久接纳,业务 committed 表示账本已提交;完成状态仍需来自原动作结果与该能力要求的读回。多个 action 同事务登记也不等于游戏服务器原子执行,不提供自动回滚。

重启后先读取持久请求、原提交回执和 operation,再决定下一步。原动作是 unknown 时暂停该对象的自动推进和清理,保留原 ID;不能换业务编号、幂等键、期限、generation 或连接来重放。其他对象能否继续仍受 Host 的恢复限制约束。在线调用超时也不会停止已经开始的 provider 处理器。

对象租约从可信 accepted_at 计算,排队不会刷新 TTL;在线调用 deadline 与对象寿命分别管理,每次后续动作前重新检查期限。到期后的清理本身也是受控动作,也可能得到 unknown。

后台调度使用有界受管任务,避免每个对象无限创建循环。事件、服务处理器和清理任务共同修改状态时,用同一状态锁保护读取、校验与提交,同时保留 Host CAS;不要持锁等待世界动作。等待结束后重新读取原 operation 和对象 revision,再提交结果。完整 API 见存储、业务事务与在线服务。

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