跳转到正文

兼容运行域与语言桥接 ​

Neomega 的插件开发主线是原生 Python SDK。公开 SDK 还保留了 Go、Lua 和 ToolDelta 的具名适配层,供已有插件的有界迁移使用。它们支持的是固定制品、固定框架版本和明确接通的接口,不承诺旧插件市场全兼容。完整 Go、Lua、ToolDelta 旧生态兼容路线已经取消;新功能以原生 Python 插件开发为主,不应等待兼容层补齐任意旧接口。

兼容层随工具包提供。运行兼容插件需准备下文指定的旧框架、插件制品及原生库,并取得目标 Host 授权;按对应适配层说明接入。

选择入口 ​

需求模块与入口实际范围
运行已适配的 Go 组件go_domain.run(root)固定 component1、配套二进制和构建锁;单连接、独立进程。
运行已适配的 Lua 后台命令插件lua_domain.run(root)固定后台命令插件与配套二进制;支持命令菜单和执行身份切换。
运行 ToolDelta classic 插件组tooldelta_domain.run(root)固定版本 PluginGroup,每个进程一组;保留已接通的组内 API、广播、命令与生命周期。
让 ToolDelta 查询已观察玩家tooldelta_player_access、tooldelta_players以观察流维护旧 Player 对象,不登录、不自行请求完整名单。
让旧插件接收事件、包、Blob 或 NBTtooldelta_events、tooldelta_packets、tooldelta_packet_send、tooldelta_blobs、tooldelta_native、tooldelta_item_bridge按下文分别启用和授权,没有一个“全部兼容”开关。

新插件直接使用原生接口即可。只有要保留旧插件既有调用方式时,才需要本页的运行域与桥接对象。逐项签名还可查阅工具包 SDK 索引。

共用的启动与授权约束 ​

run(root) 是异步运行入口,由 Host 启动的 Worker 调用,root 指向已经准备好的完整兼容制品目录。运行期间占用标准输入/输出作为 IPC 通道;业务日志使用标准错误。它不是在普通 Python 控制台里连接任意服务器的函数。

候选验证检查制品与配置,不执行旧插件。通过验证后,Host 才激活运行域;安装、权限确认和启动是分开的步骤。底层动作仍经过 Neomega 的权限检查、会话检查和操作回执。兼容域没有独立登录权限,也不能绕过只读模式。

调用必要权限或启用条件
Go/Lua/ToolDelta 发命令permissions.operations 申请并授权 framework.command.execute;需要有效游戏会话。
ToolDelta 仅观察玩家状态permissions.players 申请并授权 observe,且 Host 提供玩家观察流。
Go 或 ToolDelta 混合玩家/Text 流Host 提供并授权玩家事件来源;现有 Host 支持 NEOMEGA_PLAYER_EVENT_INSTALLATIONS 精确安装白名单。
ToolDelta 收原始包Host 为该安装授权所选入站包与混合观察流;选择必须匹配。发送权限不能替代观察权限。
ToolDelta sendPacketframework.packet.send_json 和相应 permissions.packet_send_ids 同时申请、授权。
Blob 读取/取数分别依赖 NEOMEGA_BLOB_CACHE_INSTALLATIONS、NEOMEGA_BLOB_FETCH_INSTALLATIONS 的 Host 策略;两者不是同一授权。
原生 NBT 与物品桥管理员提供经 SHA-256 校验的原生库;物品桥还需相应操作权限,见下文。

这些是不同层的条件。给 Worker 设置环境变量不会自动授予 Host 权限;manifest 中也不能虚构 tooldelta.* 或 lua.* 权限。具体声明见权限与 manifest,操作结果的处理见错误与恢复。

Go:固定组件运行域 ​

neomega_runtime.go_domain 提供以下入口:

签名返回与用途
validate_package(root)返回已验证的 go-domain 路径 Path;校验组件、二进制、来源及构建记录的一致性,不运行它们。
validate_config(value)成功返回 None,无效配置抛 ValueError。
await run(root)运行 Worker 至退出;拥有并关闭组件子进程、玩家观察流和 IPC 读取任务。
await Domain.invoke(peer, body)运行域的服务回调。body['arguments'] 必须恰含 input: str,UTF-8 不超过 65,536 字节;成功返回 {'accepted': True}。

制品必须匹配当前 SDK 的固定构建锁。把任意 .so 换名为 component1.so,或只提供一份 Go 源码,不构成可运行制品。激活时需要在线机器人身份与可观察玩家名单;没有这些来源时会失败,不使用虚构玩家补齐。

配置接受四个文本键 口癖、复读机、请求输入提示、启动时提示,以及两个触发词数组 终端触发词、游戏内触发词。文本各不超过 65,536 UTF-8 字节;数组各含 1–128 个非空字符串,每个不超过 256 UTF-8 字节。可选 Version 仅接受 0.0.1、0.0.2;填写它时必须同时提供两个触发词数组。

以下仅检查配置,不启动组件:

python
from neomega_runtime.go_domain import validate_config

validate_config({
    'Version': '0.0.2',
    '终端触发词': ['帮助'],
    '游戏内触发词': ['帮助'],
})

初次激活将 Host 配置写入旧组件的数据区;之后由旧加载器管理其升级后的配置。invoke 的 accepted 只表示输入已被旧分发器接受,不能当作命令执行成功。桥接断开或回执丢失时停止运行域,不自动重放输入。调用服务时使用实际制品声明的导出名,不猜测固定服务 ID。

Lua:后台命令运行域 ​

neomega_runtime.lua_domain.validate_package(root) 返回已验证的 lua-domain 路径;await run(root) 运行并管理固定的后台命令插件。验证失败抛 ValueError;运行异常不被转换成成功。

配置只接受可选 default_identity,值为 ws、websocket、wo、setting 或 player。其他键或身份会被拒绝。服务回调为 await Domain.invoke(peer, body),其 body['arguments'] 使用如下形状:

json
{
  "trigger": "cmdmod",
  "arguments": ["ws"]
}

cmdmod 切换执行身份;使用 trigger: "/" 时,arguments 是要传给后台命令菜单的字符串参数数组。参数最多 128 项,字符串长度合计不超过 65,536。cmdmod 拼合后的参数只能是上述五种身份之一。成功返回 {'accepted': true},表示菜单输入被接受,不是世界操作完成。

此域不订阅游戏事件。旧循环没有停止钩子,关闭时会终止并等待整个子进程退出;Host 仍负责已提交动作的最终状态和恢复。不要把 run(root) 当作任意 Lua 脚本的加载器,也不要在异常后直接重新发送同一条命令。

ToolDelta:classic 插件组 ​

neomega_runtime.tooldelta_domain.validate_package(root) 返回解析并验证过的锁定清单 dict;await run(root) 接管一个 classic 插件组的加载、初始化和关闭。

制品包含 group.lock.json 和被逐一锁定的框架、插件文件。当前验证器允许 1–64 个插件、1–4,096 个锁定文件,单文件不超过 4 MiB、总计不超过 24 MiB。文件集合、摘要与固定框架版本必须吻合;Python 文件会做语法编译检查,不会在候选阶段导入。校验通过不代表插件调用的全部旧接口都已实现。

命令与机器人信息 ​

域在激活时提供 Launcher(calls, session, output_factory)。旧插件通常通过其已有 frame.launcher 使用,无须自行构造。

方法返回值与行为
sendwscmd(cmd, waitForResp=False, timeout=30)以 ws 身份提交命令。等待时返回原框架的 Packet_CommandOutput;不等待时返回 None。
sendcmd(cmd, waitForResp=False, timeout=30)同上,执行身份为 player。
sendwocmd(cmd)以 wo 身份提交,返回 None,不等待命令输出。
get_bot_name() / bot_name从已验证的 robot.status 读取机器人名称;离线或名称不可用时抛错。
blobHashHolder()返回已配置的旧 BlobHashHolder;未启用时抛 UnsupportedDomainAPI。

示例在已激活的旧插件回调中执行,需要命令权限:

python
def show_bot_name(frame):
    name = frame.launcher.get_bot_name()
    # 查询命令;返回对象沿用固定 ToolDelta 版本的命令输出类型。
    output = frame.launcher.sendwscmd('list', waitForResp=True, timeout=10)
    return name, output

None 只保留旧接口“不等待响应”的语义,不表示服务端执行成功。旧插件如果依赖登录、任意原始字节发送、热重载或文件系统回滚模拟,会遇到不支持接口。域不是 Python 沙箱,插件组里的代码必须可信。

玩家查询与玩家对象投影 ​

tooldelta_player_access.make_player_maintainer(frame) 在激活阶段构造带同步查询边界的旧玩家管理器。tooldelta_players.ToolDeltaPlayers(maintainer, snapshot) 把初始观察快照投影为旧 Player 对象;管理器必须是尚未接入其他名单来源的新实例。

入口用途与返回
maintainer.getAllPlayers()、迭代管理器查询当前已观察玩家;集合来自观察,不保证是全服完整名单。
getBotInfo()、getPlayerByName(...)、getPlayerByUniqueID(...)、getPlayerByUUID(...)、getPlayerByXUID(...)、getPlayerByDeviceID(...)、getPlayerByRuntimeID(...)保留固定框架的查询参数与返回行为,并串行保护查询过程;缺失身份信息不能靠推测补齐。
get_player_ability(player)返回旧框架 Abilities 对象;最多等待 20 秒。玩家已移除、关闭、排空或等待超时会失败。
projection.apply(observation, *, epoch)按序应用观察文档,成功返回 None;校验 epoch 与修订连续性。
projection.close()关闭投影并将保留的玩家引用标记离线、清理名单;不发离服历史事件。

下面是自定义桥接所有者的调用顺序;snapshot、observation 来自已授权的原生观察流,不由插件编造:

python
from neomega_runtime.tooldelta_player_access import make_player_maintainer
from neomega_runtime.tooldelta_players import ToolDeltaPlayers

def project_one_update(frame, snapshot, observation):
    maintainer = make_player_maintainer(frame)
    projection = ToolDeltaPlayers(maintainer, snapshot)
    try:
        projection.apply(observation, epoch=snapshot['epoch'])
        # 在投影存活期间使用查询结果,不修改玩家管理器字典。
        return len(maintainer.getAllPlayers())
    finally:
        projection.close()

ToolDeltaPlayers 不拥有订阅或后台线程。调用方必须串行应用文档并在结束时关闭;由 tooldelta_domain.run 托管时这些工作由运行域完成。相同 UUID 和 unique ID 的玩家保留对象身份,但返回的是持续更新的对象引用,不是多个字段的原子快照。不要直接修改管理器字典。

block_init()、hook_packet_handler(...)、add_player(...)、remove_player(...) 会明确拒绝:观察流不能证明完整初始名单,也不能与另一条名单写入来源混用。

玩家事件与 Text ​

tooldelta_events.ToolDeltaEvents(group, snapshot) 在已有插件组上安装玩家投影;apply(event, *, epoch) 同步处理下一项混合事件,成功返回 None;close() 关闭投影。

处理玩家变更时先更新玩家对象,再调用旧组的 PlayerList 语义处理器;Text 转换后交给旧文本处理器。初始快照不制造历史入服回调。这里的 PlayerList 是保留所需语义的最小结构,不能当作完整原始协议包。

若由自己的桥接代码持有已加载的 group,同步处理一项事件可写为:

python
from neomega_runtime.tooldelta_events import ToolDeltaEvents

def dispatch_one_event(group, snapshot, event):
    dispatcher = ToolDeltaEvents(group, snapshot)
    try:
        dispatcher.apply(event, epoch=snapshot['epoch'])
    finally:
        dispatcher.close()

此示例的 group.linked_frame.players_maintainer 必须尚未接入其他投影,event 必须是此快照之后的第一项事件。正常运行应保留同一个分发器持续处理后续事件,不能每一项重建。

每次回调真正结束后才能推进下一项;调用方不得并发或重入 apply,也不得从回调内调用 close()。序号、epoch、修订或回调异常会使分发器关闭,不能跳过坏事件继续伪造连续历史。

域支持三种互斥的观察接入方式:

Worker 侧设置行为
NEOMEGA_TOOLDELTA_PLAYER_OBSERVATIONS=1仅维护玩家观察对象。
NEOMEGA_TOOLDELTA_PLAYER_EVENTS=1维护玩家对象并分发混合玩家/Text 语义事件。
NEOMEGA_TOOLDELTA_ORIGINAL_PACKET_IDS逗号分隔的 1–3 个不同包 ID;使用下面的原始包桥,并要求 Host 的选择完全一致。

这些设置不能叠加。原生 OriginalPackets 允许动态选择最多 32 个包 ID,并不意味着此兼容域的环境变量也接受 32 个。

原始包回调和等待 ​

tooldelta_packets 在同一有序流上投影玩家并分发旧包回调:

签名用途
make_packet_handler(frame, packet_ids)创建只接受已选包号的同步处理器。
hook_group_packets(group, handler)按旧组注册顺序安装包监听器;未选中的监听类型会失败。
ToolDeltaPackets(group, snapshot, packet_handler)混合分发器,使用 apply(event, *, epoch)、close()。
decode_dictionary(data)把受限 UTF-8 JSON bytes 解为 dict;无效输入抛 ProtocolError。
OriginalPacketPump.open(peer, session, install=..., apply=..., reader_options=...)异步创建并拥有读取循环,需监督 wait() 并等待 close()。

处理器支持 add_dict_packet_listener(packet_id, cb, priority=0)、add_bytes_packet_listener(packet_id, cb, priority=0) 和 wait_next_packet(packet_id, timeout=30)。等待只适用于已选 JSON 字典包;不能在同步包回调里等待下一包,否则会阻塞正在负责交付包的线程。

在域已经配置的处理器上注册字典监听器:

python
def attach_text_listener(frame, packet_id, callback):
    # packet_id 须已由 Host 授权,且列在本域选择的 JSON 字典包中。
    frame.packet_handler.add_dict_packet_listener(packet_id, callback, priority=0)

callback 收到旧字典结构,不在回调内调用 wait_next_packet。只需要解码已取得的 JSON bytes 时,可以单独调用 decode_dictionary(b'{"Message":"hello"}');这一步不访问 Host,也不获得包观察权限。

LevelChunk、SubChunk、StructureTemplateDataResponse 使用旧框架的定制二进制表示;其他已支持包使用 JSON 字典。这些 bytes 不是网络原始帧。每包数据最多 1 MiB;严格 JSON 解码拒绝重复字段、非有限数字、过深结构和非字典根值。SubChunkRequest 是请求包,不作为此入站来源支持项。

出站包转换 ​

tooldelta_packet_send.packet_identifier(packet_id) 接受整数或固定框架的 PacketIDS 枚举,返回整数。packet_dictionary(packet_id, packet) 返回原字典,或把固定类型的 SubChunkRequest 转换为字典;不调用任意对象的序列化方法。

SubChunkRequest 的转换限制为:维度 0..255,子区块 X/Z 为有符号 32 位、Y 为有符号 16 位,offset 最多 4,096 项,每项为三个 -128..127 整数。

Launcher.sendPacket(pkID, pk) 使用上述转换,随后通过 JSONPackets 提交;返回 None,仅表示提交被接受。bytes、bytearray、memoryview 以及任意其他类型的旧包对象均不支持;启用固定物品桥后,该入口也会拒绝绕过物品桥的提交。原生 SDK 的其他发包能力不会自动成为旧 launcher 的能力。

下面的转换示例不发包,不需要 Host 权限:

python
from neomega_runtime.tooldelta_packet_send import packet_dictionary

payload = {'Message': 'hello'}
assert packet_dictionary(9, payload) is payload
# 真正发送还需要匹配当前协议的完整字段与 packet_send_ids 授权。

Blob 读取桥 ​

tooldelta_blobs.ThreadBlobReads(peer, session, *, max_pending=4) 为旧 BlobHashHolder 提供同步读取边界。必须在 IPC 事件循环上构造和 await aclose();同步方法必须从插件线程调用。max_pending 允许 1–16。

方法返回和限制
load_blob_cache(hash, *, timeout=45)返回缓存 bytes,缓存未命中返回 b'';hash 必须是 0..2**64-1 的整数。不会主动请求缺失缓存。
soft_call_with_bytes(api, args, *, timeout=30)仅支持 api='blob-hash-get-hash-payload',返回 Blob payload bytes;其他 RPC 抛 NotImplementedError。
update_blob_cache(hash, payload)返回 bool;只核对本桥之前取回的长度/摘要回执,不能写入任意缓存。
owned_threads()返回所拥有的传输线程集合,供域关闭时识别与等待。
await aclose()停止接收新请求,等已拥有的取数与传输真正结束后关闭。

自定义桥接所有者可以把同步读取送到线程,保持 IPC 循环可用:

python
import asyncio
from neomega_runtime.tooldelta_blobs import ThreadBlobReads

async def read_one_cached_blob(peer, session, hash_value):
    reads = ThreadBlobReads(peer, session)
    try:
        return await asyncio.to_thread(reads.load_blob_cache, hash_value)
    finally:
        await reads.aclose()

peer 和 session 来自当前已激活 Worker,hash_value 来自真实观察;示例不会主动请求未缓存区块。

Host 缓存和 Blob payload 来源均需独立授权;Worker 中的 NEOMEGA_TOOLDELTA_BLOB_CACHE=1 只是接线开关。调用方超时不取消底层取数,也不提前释放其所有权。update_blob_cache 返回 True 不是世界数据已写入的证明。

原生 NBT 与物品制作桥 ​

NBT 动态库 ​

tooldelta_native 只连接管理员提供的可信 Linux C ABI 库:

签名返回与行为
load_verified_library(path, digest)校验绝对路径、普通文件、大小和 SHA-256 后返回 ctypes.CDLL。文件上限 128 MiB。
load_nbt_library(path, digest)进一步绑定 TranslateChunkNBT、FreeMem 的固定签名,发布到旧框架,再返回库对象。
bind_from_env(environ)从 NEOMEGA_TOOLDELTA_NBT_LIBRARY 和 NEOMEGA_TOOLDELTA_NBT_SHA256 读取配置;都为空返回 None,只设置一个会失败。

仅检查并加载已由管理员设置的 NBT 库,使用现有入口即可:

python
import os
from neomega_runtime.tooldelta_native import bind_from_env

def activate_nbt_binding():
    return bind_from_env(os.environ)

此调用会加载原生代码,只应在激活阶段使用;不要放入候选验证或普通新插件的模块顶层。

摘要必须是 64 位小写十六进制。加载过程中可信路径必须保持不可变;检查摘要不构成原生代码沙箱,也不限制库自己的内存分配。库由进程拥有,不能在旧插件仍持有函数指针时卸载。仅绑定 NBT 转换,不启动旧 launcher 或建立新游戏连接。

固定物品制作调用链 ​

tooldelta_item_bridge 接通固定的铁砧命名/丢弃物品调用链。它要求上述 NBT 库,以及独立经摘要校验的 shim;对应设置为 NEOMEGA_TOOLDELTA_ITEM_BRIDGE_LIBRARY 和 NEOMEGA_TOOLDELTA_ITEM_BRIDGE_SHA256,二者必须同时提供。

签名返回与职责
NativeItemBridge(path, digest, original_library, rename)创建桥;library 保留真实 ctypes 入口。finalize_declarations() 在旧插件初始化后恢复已检查的 ABI 声明;close() 禁用回调。
ManagedItemCalls(peer, session, data, output_factory)创建同步命令与物品动作所有者,在 data 下持久化动作意图和恢复状态。
command(cmd, identity, wait, timeout)等待时返回旧命令输出对象,否则 None;timeout 必须大于 0 且不超过 300 秒。
rename(position, runtime_id, slot, name)先取得新鲜库存引用,再提交命名/丢弃动作;只在动作成功且服务端确认时正常返回 None。
bind_item_from_env(environ, original_library, calls)启用并发布桥;未配置时返回 None,否则返回 NativeItemBridge。

操作链需要 framework.command.execute、framework.self.inventory.observe、framework.compat.item.rename_drop 中实际使用的权限。旧 RenameItemWithAnvil ABI 的坐标为有符号 32 位、block runtime ID 为无符号 32 位、快捷栏槽位为 0..8、名字为最多 512 字节且不含 NUL 的有效 UTF-8 bytes。返回的 C 字符串被复制后释放一次;它不是插件可以长期保留或再次释放的原生指针。

已经由运行域创建 calls: ManagedItemCalls 后,桥接作者可以调用:

python
def rename_observed_hotbar_item(calls, position, block_runtime_id, slot, name):
    # 输入来自已确认的目标与当前观察;该调用会修改并丢弃物品。
    calls.rename(position, block_runtime_id, slot, name)

这段是有副作用的调用方式说明,不是可直接运行的演示;calls、位置、方块类型与槽位必须由实际授权的工作流提供。普通插件不自行构造此兼容链。

此链必须从原插件线程执行。同一时刻只能有一个动作;跨命令与命名阶段的工作流保留线程所有权。超时、未确认结果或上次未完成记录会保留恢复围栏,后续调用抛 ItemBridgeRecoveryRequired。必须核对原动作后恢复,不能删除状态文件或换一套 ID 自动再做一次。

桥接代码存在不代表任意物品制作插件已经适配成功,也不代表完整 NBT 编辑平台已经开放。新插件使用库存与容器、物品动作中的原生接口,并按各自的结果与证据契约判断成功。

迁移前核对 ​

  1. 明确旧插件及框架版本、实际调用的 API、所需原生库;与完整制品的锁定版本匹配。
  2. 逐项检查来源、操作和包号授权;unsupported 代表当前路径未接通,不能靠增加无关权限修复。
  3. 用最小功能验证加载、命令结果、数据读取和正常退出;将离线检查与目标服务器行为分开记录。
  4. 保留操作 ID、原始意图与错误结果。accepted、None、发包完成都不能替代服务端效果确认。
  5. 对未覆盖的旧 API 选择原生接口重写或明确记录不支持,不补造回调、玩家字段或成功回执。

原生语言路径 ​

Go 宿主模块通过 Frame 接入已有 Host;驻留 Lua在 Host 内消费授权输入并发布 View。这两条路径与本页固定组件桥不同,不能用固定桥的限制概括所有 Go/Lua 能力。公开工具包还提供独立 Go Worker;其 SDK 不依赖私有 Host 源码,制品须匹配目标平台。

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