跳转到正文

协议包、观察与录制 ​

使用 Python 协议模型构造、观察、编解码和发送协议包;需要已有 JSON、lossless 包体或原始帧时,使用下文对应入口。玩家能力、环境、配方与表单见协议扩展。

通过 ctx.packets 查询包目录、读取选定包、预览编码或提交原始帧;通过 ctx.raw_packets 观察当前机器人连接的原始入站和出站帧。优先使用玩家、库存、世界和机器人等领域接口,只有需要保留协议字段或诊断协议行为时才直接处理包。

使用配套 SDK 与 Host,版本见版本与升级。各入口分别检查来源、编码上下文、传输容量和授权;观察、预览和发送使用独立权限。

先选择表示形式 ​

表示数据内容适合的接口
neomega-json-v1原包字段的 JSON 字节,Python 收到 bytesctx.packets.open/listen,按格式解码
tooldelta-custom-v1兼容适配器的自定义字节格式原包观察及对应兼容解码器
raw_frame已解密、解压的单个 Bedrock 包头+包体ctx.raw_packets、prepare_raw/send_raw
预览 / codec body没有包头,使用离线 shield ID 0 的包体preview、encode、decode

原包观察里的“原包”不等于网络原始帧。预览 body 也不能直接传给 send_raw;真实发送应提交原始意图,由发送路径校验连接和当前上下文。

包目录与字段说明 ​

python
catalog = await ctx.packets.catalog()
# 每条:id、name、format、observable、fields;fields 含 name 和 type。
selected = [row for row in catalog if row["observable"]]
description = await ctx.packets.describe(10)
方法返回与约束
await ctx.packets.catalog()当前来源的描述列表;observable 是来源支持情况,不是当前插件已获授权的证明。
await ctx.packets.describe(packet_id)Host 注册的字段、format 和具体输入形式;API minor 3;只读协议元数据,不读取世界。

describe、preview、encode、decode 的 packet_id 必须是大于 0 的 uint32 整数,不能传布尔值。按当前 catalog 解析包名和 ID,再用 describe 确认输入;不要凭其他协议版本的包号表猜测。

选定原包的有序观察 ​

python
async def inspect_selected(ctx, packet_ids):
    stream, snapshot = await ctx.packets.open(packet_ids)
    try:
        # 此处处理一次初始玩家快照 snapshot。
        event = await stream.next()
        if "packet" in event:
            packet = event["packet"]
            return {"id": packet["id"], "format": packet["format"],
                    "bytes": len(packet["data"])}
        # 也可能是玩家状态 update 或语义 text,必须按原顺序处理。
        return {"sequence": event["sequence"]}
    finally:
        await stream.close()

await ctx.packets.open(packet_ids, *, text_profile=None) 返回 (OriginalPackets, snapshot)。包号为 1–32 个不重复的正 uint32,类型为列表或元组;包装器请求精确选择。text_profile 只能省略或为 "selected_only",用于仅保留选定包对应的 Text 语义,减少重复处理。

Host 必须支持该选择并授权完整混合流及每个选定包;管理端的原包 packet-grants 与发送、预览授权相互独立。不要自行添加并不存在的 packet_observe_ids manifest 字段。

流方法行为
await stream.next()确认前一个已处理文档并读取后继事件;调用前先处理完上一条。
await stream.poll()有事件返回事件,无事件返回 None;需要来源支持有界 poll。Host 来源等待上限为 100 ms,编码与传输耗时另计。
await stream.close()显式释放 Host 观察资源;先等待在途读取结束,不能并发读取和关闭。

事件包含连续的 sequence 和 state_revision。update 推进状态 revision;text 和 packet 不推进。包字段为 {"id": ..., "format": ..., "data": bytes},每包解码后上限 1 MiB;整个传输文档上限 32 MiB,不代表 Python 总内存上限。

流只允许一个 reader。校验失败、溢出或撤权会结束当前读取合同,不会自动重连或补发。需要额外封装时可调用 OriginalPackets.open(peer, session, *, required_packet_ids=None, exact_selection=False, text_profile=None);常规插件优先用 ctx.packets.open。

后台监听 ​

python
async def on_packet(event):
    if "packet" in event:
        packet = event["packet"]
        # 按 packet["format"] 解码;在此完成处理,不再打开第二个 reader。
        pass

listener = await ctx.packets.listen(packet_ids, on_packet)
# Plugin/Worker 管理该任务;提前结束时 cancel 后仍应 await 等待清理。

await ctx.packets.listen(packet_ids, callback, *, on_snapshot=None, text_profile=None) 在打开流后返回 Worker 管理的 asyncio.Task。callback(event) 和可选 on_snapshot(snapshot) 都是异步函数,串行执行,形成自然背压。它不会为每个包创建新任务;回调异常会使监听任务失败,finally 负责关闭流。

零发送预览 ​

python
result = await ctx.packets.preview(10, {"Time": 42})
if result["ok"]:
    assert result["sent"] is False
    body_base64 = result["body_base64"]
else:
    code = result["error"]["code"]
text
await ctx.packets.preview(
    packet_id, packet, *, format="original_json", packet_type=None,
    context="offline",
)

上面展示的是调用参数形式;packet 必须是对象。format 可为 original_json、typed 或 lossless;packet_type 可省略,指定时须为非空字符串。context 可为 offline 或 inventory_snapshot。返回诊断对象,成功时包含 ok、sent: false、packet_id、format、body_bytes、body_base64、context 和 coverage;coverage 描述注册输入形式,context 含 mode、shield_id、body_only,库存快照上下文还带连接和 revision;失败时检查 error,不能使用成功包体。

预览需要 Host API minor 3、该包的 permissions.packet_preview_ids 授权,以及至少 1 MiB 的协商 IPC frame 容量。该授权不授予观察或发送权限;只读 Host 可以提供预览。manifest 片段:

json
{
  "host_api": {"major": 1, "min_minor": 3},
  "permissions": {
    "packet_preview_ids": [10],
    "purposes": {"packet_preview:10": "验证时间包字段是否可以编码"}
  }
}

包体预览参数 format="typed" 接受以下具名分支;完整模型使用 ctx.packets.typed:

包号packet_type
30inventory_transaction.use_item_on_entity_attack、inventory_transaction.use_item_on_entity_interact、inventory_transaction.use_item_block、inventory_transaction.use_item_air、inventory_transaction.release_item
31mob_equipment.hotbar
33interact.mouse_over_entity
44animate.swing_arm
144player_auth_input.movement

这只表示列出的闭合输入分支可被检查,不代表该包号下所有结构都受支持。以 describe 返回的字段和 preview 诊断为准。

当前库存引用 ​

HeldItemRef 只能配合 context="inventory_snapshot",还需 framework.self.inventory.observe 操作权限。Host 读取当前 window-0 快照,核对连接、槽位、观察 revision 和 stack network ID;热栏分支还核对机器人 runtime ID。它不刷新库存,也不把未知物品当成空气。离线上下文使用引用会返回 context_required。

预览不预留物品。即使预览成功,实际发送仍须重新核对实时库存。准备层的输入 JSON 与 body 各限制为 512 KiB,Python SDK 完整请求还受 256 KiB 上限约束,实际可提交的参数要减去封装开销。常见诊断包括 unsupported_packet、unsupported_form、unsupported_context、context_required、invalid_packet、preview_limit;权限或生命周期错误是 IPC 拒绝,不返回半份包体。

Lossless 包体编码与解码 ​

方法参数与结果
await ctx.packets.encode(packet_id, packet, *, packet_type=None, format='lossless', context='offline')tagged 对象编码为 body;结果形状与 preview 相同。
await ctx.packets.decode(packet_id, body, *, packet_type=None, format='lossless', context='offline')body 接受 bytes、bytearray、memoryview;成功返回对象在 packet 字段中。

两者只接受 lossless+offline,body 上限 128 KiB。需要 API minor 3、至少 256 KiB IPC frame 和独立的包预览授权;查询 Host 的 packet_codec 能力确认方法和限额。JSON 载荷及结果还要为 IPC 封装留出空间。

python
import base64

async def round_trip(ctx, packet_id, tagged_packet):
    encoded = await ctx.packets.encode(packet_id, tagged_packet)
    if not encoded["ok"]:
        return encoded
    body = base64.b64decode(encoded["body_base64"], validate=True)
    return await ctx.packets.decode(packet_id, body)

支持的 envelope 包括 PyRpc 的 packet+tagged value;InventorySlot、InventoryContent、MobEquipment 及上述五种 typed InventoryTransaction 的 packet+有序 nbt。物品原字段里的 NBTData 必须是 {},实际类型化 NBT 放入并行 nbt 数组,每个线上物品对应一项,空物品也不能略去。runtime ID 使用规范无符号十进制字符串。

Tagged 值形如 {"type": "uint64", "value": "9007199254740993"}。整数以精确范围的十进制字符串表示,float32/64 用 8/16 位十六进制 IEEE 位模式,字节用 Base64;数组、列表、compound、map 保留各自的类型。具体包只接受在当前编码器中往返类型稳定的子集,不能将通用 tag 表视为任意包的输入许可。动态容器、任意 inventory action 数组、legacy transaction slots、未知联合类型和 HeldItemRef 不在此 codec 的支持范围内。

成功表示当前 writer/reader 能保留该 tagged envelope,不保证原始网络字节逐位相同或 map 顺序不变。畸形输入、类型丢失或超限返回 invalid_packet、unsupported_form、codec_limit 等诊断;不应降级为随意转换类型后发送。

原始帧观察 ​

python
async def observe_one_raw(ctx):
    stream, snapshot = await ctx.raw_packets.open("inbound")
    try:
        # 快照建立当前水位;不会回读 open 之前的历史。
        return await stream.poll()  # 空闲时为 None。
    finally:
        await stream.close()
方法行为
await ctx.raw_packets.snapshot(direction, cursor=None, *, max_events=128)首次返回当前水位;之后传入恰含 connection_id、connected_at、sequence 的游标。max_events 为 1–128。
await ctx.raw_packets.open(direction)返回 (RawPacketObservation, snapshot);初始 gap、关闭或连接变化会报错。
await stream.poll()每次重新授权,至多返回一个事件;无事件为 None;不自动重连。
await stream.close()等待已进入的有界读取结束,关闭后不交付其数据;没有后台抓包资源。

direction 只接受 inbound 或 outbound。API minor 3 和至少 1 MiB IPC frame 是前提,并需独立方向授权:

json
{
  "raw_packet_directions": ["inbound"],
  "purposes": {"packet_raw:inbound": "诊断当前机器人连接的包解码问题"}
}

上面是 permissions 片段。这是该方向全部帧的读取权限,包括未知 ID 和坏包头,不按单个包号过滤,也不授权另一方向或任何发送。

packet 事件保留 connection_id、connected_at、direction、SDK sequence 及 packet.source_sequence;packet.data 是 bytes,format 为 raw_frame。入站 source="predecode",出站为 typed、bytes 或 raw。header_valid 只说明包头有效性;坏包头的 ID 可能为 0,不能据此丢弃诊断字节。

入站 decode_failures 是连接累计解码失败数,不能直接归因到某一帧。出站 flushed_sequence 是本地 flush 水位。仅计数变化时会收到 raw_diagnostics。出站记录不是服务器回包,也不证明动作成功。

Journal 最多保留 4,096 条 / 8 MiB,单帧 256 KiB;快照页最多 128 条 / 512 KiB 原字节。sequence 是本页游标,watermark 是来源水位,has_more 表示仍有后续页。淘汰、单帧超限、游标失配和重连会形成 gap;SDK 返回带原因的终态后封闭该流,不拼接为连续记录。正常关闭先排出可读事件,再返回 closed。同一流重叠 poll() 会报错。

保存记录与离线读取 ​

python
from neomega_runtime.recording import collect, open_recording, read_recording

async def record_selected(ctx, packet_ids):
    with open_recording(ctx.data_dir) as output:
        result = await collect(ctx, output, packet_ids, seconds=10, max_events=200)
        archive_path = output.name
    archive = read_recording(archive_path)
    return {"complete": archive.complete, "issues": archive.issues,
            "events": len(archive.events), "result": result}
API用法与返回
open_recording(data_dir, *, prefix='packet-observation-')在给定数据目录独占创建权限为 0600 的 UTF-8 JSONL 文件,返回文件对象;prefix 只能为 1–64 个字母、数字、下划线或连字符。
await collect(ctx, output, packet_ids, *, max_events=1000, seconds=30, max_bytes=8 << 20, expected_names=None, on_event=None)先核对目录及 observable,打开一个选定原包流,返回收集结果;可用 expected_names={id: name} 核对协议预期。
PacketRecorder(output, *, session, snapshot, packet_catalog=(), seconds=30, max_events=1000, max_bytes=8 << 20)给已有原包流或 raw 流配置同步记录器;自己不读取、不开线程。需传初始快照,文件最少配额 2048 bytes。
read_recording(source, *, max_bytes=8 << 20, max_events=1000)从路径或可读文件对象有界读取,返回 PacketRecording,全程离线,不发送。

collect 的异步 on_event(event) 在该事件写入刷新后执行,可在同一个 reader 中做业务匹配;不要额外开 reader。自管 PacketRecorder 时调用 record(event) 或 note(name, value),返回 False 就停止,空闲时也检查 limit_reason。先关闭流,再调用 finish(reason='completed', error=None);关闭、取消或流异常应传入 error,不能标为完整。

PacketRecording 包含 snapshot、events、notes、end、complete、issues、rows。events 内 Base64 恢复为 bytes,先检查 complete 和 issues 再下结论;缺 footer 或截断尾行不会伪装成功。损坏 schema、超限或非法编码会抛 ValueError。

complete 仅表示这个有界归档按合同结束,coverage_verified 始终为 false,不代表全服事件覆盖。混合流的语义 text 会脱敏,但原包字节和自行加入的 note 可能仍含聊天、物品及身份信息;不要将归档或包体直接写进公开日志。

授权发送与回执 ​

原生插件使用 ctx.operations.prepare/submit/get/wait 管理发送意图。预览、观察和发送需要分别授权。

发送形式操作名与额外条件
JSON 原包framework.packet.send_json+准确的 packet_send_ids;参数为 {"packet_id": id, "packet": object};来源需支持原包发送并允许世界写入。
Typed 原包framework.packet.send_typed+准确的 packet_send_ids;参数为 {"packet_id": id, "packet_type": type, "packet": object};仅当前注册的闭合 typed 形式,字段以目标 Host 能力及 describe 为准。
原始帧framework.packet.send_raw+准确的 packet_send_ids;API minor 3,Host 配置的固定写入所有者还必须匹配当前安装。

普通 JSON 发送示例是可复用的函数,调用前由业务生成并持久保存唯一意图键和固定截止时间:

python
async def submit_json_once(ctx, packet_id, packet, key, deadline):
    intent = ctx.operations.prepare(
        "framework.packet.send_json", {"packet_id": packet_id, "packet": packet},
        idempotency_key=key, deadline=deadline,
    )
    # 将 intent.payload() 保存到业务的私有恢复记录后,再提交。
    operation_id = await ctx.operations.submit(intent)
    return operation_id  # 后续使用 ctx.operations.get/wait 查询此 ID。

deadline 为带时区的 RFC3339 时间。提交不确定时保留同一个意图及键,不能重新生成键并重试。上例的注释是调用方必须实现的持久化步骤,不是 SDK 自动保存承诺。

原始帧发送 ​

python
intent = ctx.packets.prepare_raw(
    frame_bytes, connection_id=current_connection_id,
    idempotency_key=key, deadline=deadline,
)
# 保存 intent.payload() 后,仅提交一次。
operation_id = await ctx.packets.send_raw(intent, timeout=10)
receipt = await ctx.operations.wait(operation_id)

prepare_raw(frame, *, connection_id, idempotency_key, deadline) 不做 I/O,返回不可变 OperationIntent。frame 必须是 1–131072 bytes 的 bytes,含规范 varint 包头和包体,包号为 1–1023,发送与目标 subclient 均为 0。不含批次长度、压缩标记、RakNet 封装或密文。该路径不验证任意包体的业务合法性。

connection_id 必须来自当前 raw 观察;重连使已准备意图失效。await send_raw(intent, *, timeout=10) 返回 operation ID,Host 的持久分发器负责受理、deadline 和恢复。不要更换旧意图的连接、键或截止时间来规避未知结果。

发送结果中 succeeded+evidence="dispatched"、observed=false 只证明本地发送路径返回,不代表服务器接受或玩家看到了效果。可能已经受理后的失败为 unknown;查原操作回执并核对来源证据,不能自动重放。等待超时也不等于取消。参见错误与恢复。

兼容适配工具 ​

neomega_runtime.packet_send.JSONPackets(calls, session) 是同步 ThreadCalls 桥接:prepare(packet_id, packet, *, idempotency_key, deadline) 返回 PacketIntent,submit_prepared(intent, *, timeout=10) 只返回已受理 operation ID。它不重试、不后台轮询;PacketSubmissionError / PacketSubmissionTimeout 都保留 .intent。JSON 参数上限 256 KiB,拒绝自定义对象、循环或过深结构及超出协议范围的整数。

ToolDelta 兼容入口包括 OriginalPacketPump、ToolDeltaPackets、make_packet_handler(frame, packet_ids)、hook_group_packets(group, handler)、decode_dictionary(data),以及 tooldelta_packet_send.packet_identifier/packet_dictionary。它们用于已核对的兼容域,按选定包精确登记回调;同步回调内不能调用等待下一包而阻塞分发。packet_dictionary 仅接受普通字典或已适配的 SubChunkRequest,不接受任意旧对象序列化。

这些工具不表示完整 ToolDelta 环境或任意旧插件兼容。新插件使用 Plugin、ctx.packets 和领域接口;迁移插件先确认兼容域、包选择、原框架依赖和实际运行验收。

PacketWaiters 是兼容域的同步等待器:wait(packet_id, timeout=30) 等待后续一次匹配投递,deliver(packet_id, packet) 交付同一对象,close() 唤醒并终止所有等待。最多 64 个并发等待;超时或关闭返回 None,timeout=-1 表示无限等待。它不拥有网络 reader、线程或历史缓存,不能代替持久事件订阅。

Python 协议模型 ​

neomega_runtime.protocol 与 ctx.packets.typed 提供 protocol 860 / 1.21.120 的 243 个包类和 894 个生成模型,可直接从当前工具包导入。模型保存字段类型;编解码和发送按包的字段、方向及当前连接校验。

在本地构造模型 ​

python
from neomega_runtime.protocol import packets, packet_from_envelope

message = packets.Text(TextType=1, SourceName="Example", Message="hello")
message.Message = "hello again"
saved = message.to_envelope()
restored = packet_from_envelope(saved)
assert restored.Message == "hello again"

这段只处理本地 Python 值。to_envelope() 产生带版本及 schema 标识的 IPC 对象;to_value() 返回其中的带类型值。两者都不产生游戏字节或发送数据。字段名沿用 Core 模型;构造、赋值及导出时校验类型,嵌套结构从 neomega_runtime.protocol.models 取得。

PACKET_TYPES 是按 ID 索引的模型类字典;packet_class(id) 查模型,未知 ID 抛 KeyError。PROTOCOL_VERSION、SCHEMA_ID 供核对版本。某字段是否进入 wire 还取决于包内的类型或标志;不能从模型有这个字段推断任意取值都被编码。

需要保留的区别模型值
Optional 缺席与存在的 false、0、空值OptionalValue() 与 OptionalValue.some(value);检查存在性,不用真假判断代替。
联合或动态字段的具体类型UnionValue(type_id, value)、DynamicValue(type_id, value);仅接受该字段的合法成员。
浮点位型与非 UTF-8 字符串FloatBits(bits, width)、StringBytes(value)。
空指针与非空指针指向 nilNone 与 PointerValue(None);后者仅适用于允许 nil 的目标类型。
精确时间Timestamp(seconds, nanoseconds=0, offset_seconds=0, zone="UTC");PyRpc wire 保留秒与纳秒,解码为 UTC,不保留时区名称。

NBT 整数宽度、array 与 list 需分别保留。空 nbt.List 也带元素类型;map 的键不统一转成字符串。Core 可能规范化 NBT 名称、键顺序或其他字节细节:编辑模型应保留可表达的语义,需要原始字节证据时保存 raw frame。

通过 Host 编解码 ​

python
async def check_text_codec(ctx):
    from neomega_runtime.protocol import packets
    model = packets.Text(TextType=1, SourceName="Example", Message="hello")
    encoded = await ctx.packets.typed.encode(
        model, context="offline", offline_shield_id=0,
    )
    decoded = await ctx.packets.typed.decode(
        packets.Text, encoded.frame, context="offline", offline_shield_id=0,
    )
    return decoded.Message, encoded.context

这里的 offline 指编码上下文,仍通过 IPC 调用已激活 Host,并需要该包的 packet_preview_ids 授权。显式传入 int32 offline_shield_id;它不是自动检测的真实连接值。编解码不提交操作、不发送包。

ctx.packets.typed 方法输入与结果
await context_for(packet_type)注册包 ID 或模型类;返回当前 connection_id/protocol_version/shield_id/revision。
await encode(packet, *, context='offline', connection_id=None, offline_shield_id=None)模型实例;返回 EncodedPacket(packet_id, frame, context),frame 含包头。
await decode(packet_type, frame, *, context='offline', connection_id=None, offline_shield_id=None, codec_revision=None)完整明文帧;返回对应模型实例。
await decode_frame(frame, *, packet_id, connection_id, codec_revision)用捕获时上下文解码观察帧的简写。

context="connection" 需当前 connection_id,不能同时传离线 shield。解码还必须提供捕获帧携带的 codec_revision,并确认 codec_context_ready is True。不能用 context_for 查到的当前 revision 填补历史帧缺失信息;连接或 revision 已变化时保留原帧并处理拒绝。

这组 encode 输出最多 128 KiB,decode 输入最多 256 KiB,还受模型节点、深度、JSON 和 IPC 配额约束。ctx.packets.encode/decode 处理无包头 body;不要与这里的 frame 混用。

观察与关闭 ​

python
async def inspect_one_typed_event(ctx):
    from neomega_runtime.protocol import packets
    stream, snapshot = await ctx.packets.typed.open([packets.Text])
    try:
        event = await stream.poll()
        if event is None:
            return None  # 本次无匹配事件;不代表没有聊天。
        if event["kind"] != "packet":
            return {"kind": event["kind"]}  # 包括诊断与 gap/closed 终态。
        return event["model"].Message
    finally:
        await stream.close()

open(packet_types=None, *, direction='inbound') 返回 (stream, snapshot),方向可为 inbound/outbound。过滤列表只控制返回内容;仍需 raw 方向授权,并为实际解码的已知包取得 preview 授权。poll() 每次最多读取一个源事件,被过滤的包也可能返回 None;连续读取时由调用方安排有界间隔。

已知包事件新增 model,packet 保留原帧与捕获元数据。不传过滤列表时,未知 ID 以 raw 事件返回,未必有 model。raw_diagnostics 也没有模型。已知包解码错误会使流失败,异常的 raw_event 保留实际观察,不自动跳过坏包或重新连接。gap/closed 为终态;旧连接不拼成新连接的连续历史。

每个流只允许一个 reader;await close() 等待在途读取和解码结束。await listen(packet_types, callback, *, direction='inbound', on_snapshot=None) 返回 Worker 管理的任务,异步回调串行接收完整事件。调用方需监督该任务;提前结束时 cancel 后仍要 await,完成资源释放。

准备、提交与核对发送 ​

prepare(packet, *, connection_id, idempotency_key, deadline) 冻结模型为 OperationIntent,不做 I/O;之后编辑模型不会修改已有意图。提交前持久保存 intent.payload(),然后仅提交一次:

python
async def submit_saved_protocol_intent(ctx, intent):
    operation_id = await ctx.packets.typed.send(intent, timeout=10)
    # 调用方持久保存 operation_id,并查询同一 ID 的终态。
    return operation_id

连接身份从当前观察或 context_for 取得。send 使用专用操作 framework.packet.send_protocol;需该操作权限、逐包 packet_send_ids 和匹配的固定 world-write owner。Host 在受理、claim 和发送时核对适用的权限、连接与代际条件,在共享写入门内按有效上下文编码。预览产生的 frame 不参与这条发送路径。

send 返回 ID 只表示受理;succeeded 加 evidence="dispatched"、observed=false 仍只证明本地分发,不是服务器 ACK。提交超时、取消等待或 unknown 时保留原意图、键、期限和操作 ID,先核对原回执,不能生成新键重放。具体表单/NPC交互仍按协议扩展的业务边界判断。

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