跳转到正文

玩家、聊天与位置 ​

用 await ctx.player(...) 获取玩家句柄,准备通知、物品和计分板动作;其他常用动作使用 ctx.player_actions。玩家能力观察与请求见协议扩展。

读取姓名用 ctx.players();按身份找玩家用 ctx.resolve_player();需要连续的玩家状态变化时,用 ctx.observe_players() 与 PlayerState。这些原生插件接口都使用当前 Host 的连接,不能把缓存中的玩家或位置视为持续在线保证。

选择入口与权限 ​

需求Python 入口授权
读取缓存名册、精确解析身份ctx.players()、ctx.resolve_player(...)permissions.players: ["list"]
接收有序玩家状态ctx.observe_players()permissions.players: ["observe"]
接收状态与 Text 混合流ctx.player_events(...)Host 独立的玩家事件流授权和来源;list / observe 不包含此权限
处理持久聊天ctx.chat.dispatch(event)chat.received 订阅及 players: list
查询位置、区域进入采样ctx.positions.sample(...)、watch(...)操作权限 framework.entities.query

原生 manifest 的 permissions.players 目前只接受 list 和 observe;不要填入 events。事件流可用性需向目标实例管理员确认。完整 manifest 与授权流程见权限,持久事件 ACK 见事件。

缓存名册与身份 ​

text
await ctx.players() -> list[PlayerIdentity]
await ctx.resolve_player(*, name: str | None = None,
                         uuid: str | None = None,
                         xuid: str | None = None) -> PlayerIdentity

PlayerIdentity 是 {"name": str, "uuid": str, "xuid": str}。XUID 可以为空,不要把空字符串当作已验证的账户身份。名册上限 4096 人,姓名、UUID、XUID 分别最多 256、128、64 UTF-8 字节,总响应不超过 512 KiB。超限整体失败,不返回截断名单。

resolve_player 只读一次名册,按所有给定字段精确且区分大小写匹配。至少给一个非空字段:无匹配抛 PlayerNotFound,多条匹配抛 AmbiguousPlayer,非法条件抛 ValueError。协议或响应校验失败抛 ProtocolError,不会自动重试。

底层及纯本地辅助函数位于 neomega_runtime.players:

text
await list_players(peer, session: str) -> list[PlayerIdentity]
await resolve_player(peer, session: str, *, name=None, uuid=None, xuid=None) -> PlayerIdentity
find_player(players: list[PlayerIdentity], *, name=None, uuid=None, xuid=None) -> PlayerIdentity
player_target(name: str) -> str

find_player 不访问 Host,返回独立字典。player_target 生成 Bedrock 姓名选择器,例如 @a[name="Alex"];它拒绝前导 !、U+0000–U+001F、DEL、引号和反斜杠,不接受选择器片段。姓名校验不授予业务权限。

python
from neomega_runtime.players import PlayerNotFound, AmbiguousPlayer, player_target

async def lookup(ctx, requested_name):
    try:
        player = await ctx.resolve_player(name=requested_name)
    except (PlayerNotFound, AmbiguousPlayer):
        return None
    return {"uuid": player["uuid"], "target": player_target(player["name"])}

这里的 session 不是名册版本。不能先读缓存,再订阅持久名单事件,就声称两者之间不存在丢失窗口;需要连续状态时使用下节的原子初始快照。

聊天路由 ​

text
ChatRouter(*, prefix: str = "!", max_message_bytes: int = 4096)
router.command(name: str, *, aliases=(), description="", usage="",
               min_args=0, max_args=None)  # 注册 async 处理器的装饰器
await router.dispatch(event) -> bool

默认入口是 ctx.chat。前缀非空、最多 16 个字符且不能含空白或控制字符;消息字节上限可设为 1–65536。命令名限 1–64 个 Unicode 字母、数字、ASCII 下划线和连字符,支持中文、区分大小写,不做归一化。规范名和别名不能重复注册。参数使用引号解析规则,例如 !help "two words" 产生 ("two words",),不会执行 shell。

处理器收到不可变的 ChatCommand(event, name, args, player, invoked_as=""):args 是字符串元组;player 是当前缓存中核对后的身份;name 为规范名,invoked_as 为实际调用名;target 属性调用 player_target(player["name"])。路由器用聊天姓名和非空 XUID 做联合匹配,但管理员资格仍由插件业务判断。

dispatch 仅在已注册处理器执行完成后返回 True。无关事件、其他前缀和未知命令返回 False,不读取名册;参数个数超出 min_args/max_args 时抛 ChatArgumentError;已识别命令的非法引号、身份查找失败和处理器异常向上传播。路由器不确认事件,不创建后台任务。可直接运行的插件结构见聊天命令示例。

玩家句柄与常用动作 ​

python
async def prepare_score(ctx, player_uuid, business_key, fixed_deadline):
    player = await ctx.player(uuid=player_uuid)
    return player.add_score(
        "points", 1, idempotency_key=business_key, deadline=fixed_deadline,
    )

await ctx.player(*, name=None, uuid=None, xuid=None) 返回 PlayerHandle,读取一次名册并精确匹配所有指定字段,同时确认姓名唯一。identity 是只读的 name/uuid/xuid;session 绑定当前操作会话。读取身份需 players: ["list"],准备和执行的动作另需各自操作权限。

句柄方法返回及授权
notify(text, ...)NotificationPlan;使用 framework.command.execute,发送步骤见通知。
title/subtitle/actionbar(text, ...)、give(item, ...)命令意图;使用 framework.command.execute。
position(...)原生单姓名实体查询意图;使用 framework.entities.query。
score/set_score/add_score/reset_score(...)对应计分板意图;使用各自 framework.scoreboard.* 权限。
await refresh()重新读取名册核对原姓名和所有身份字段;成功返回原句柄。

这些动作方法都要求明确的 idempotency_key 和固定 deadline,不会自动提交。身份字段变化时 refresh 抛 PlayerIdentityChanged;姓名丢失或歧义仍抛 PlayerNotFound/AmbiguousPlayer。它不会自动换绑身份,也不能消除核对后玩家离开或改名的时间窗口。

ctx.player_actions 按精确姓名准备常用命令:teleport/teleport_to、give/clear、effect/clear_effect、gamemode、tag_add/tag_remove/tag_list/tag_test、playsound/stopsound、kick、xp 和 spawnpoint。均需较宽的 framework.command.execute 权限,并通过 Operations 提交。

python
intent = ctx.player_actions.teleport_to(
    player.name, destination.name,
    idempotency_key=business_key, deadline=fixed_deadline,
)

player 与 destination 是调用方解析的玩家句柄。坐标传送和出生点使用命令执行者所在维度的绝对坐标;音效未指定坐标时使用目标玩家当前位置。clear 省略物品会清空背包,max_count=0 仅检查匹配数量;xp 添加非负经验点。参数范围见API 目录,执行结果见命令回执。

有序状态流与状态合并 ​

text
stream, snapshot = await ctx.observe_players()
# 等价底层入口:
stream, snapshot = await PlayerObservations.open(peer, session)
update = await stream.next()
update_or_none = await stream.poll()
await stream.close()

state = PlayerState(snapshot)
state.apply(observation, *, epoch=epoch)
state.snapshot()          # 返回独立副本
state.epoch              # str
state.revision           # int

类分别从 neomega_runtime.player_observations、neomega_runtime.player_state 导入。初始快照包含:

字段内容
epoch、revision当前观察源标识与状态修订号
roster_observed是否观察过名单变化,不代表完整在线名册
players每项必含 list,按已收到的信息可含 addition、ability
pending_additionsUUID 到尚未与名册配对的 AddPlayer 信息
pending_abilities十进制 unique ID 字符串到尚未配对的能力信息

list 含 uuid、username、xuid、platform_chat_id、skin_id、entity_unique_id、build_platform。这里姓名字段为 username,与简化名册的 name 不同。addition 含 entity_runtime_id、entity_unique_id、device_id。ability 含 entity_unique_id、player_permissions、command_permissions、layers;layers 可以为 None,有值时每层含 type、abilities、values、fly_speed、vertical_fly_speed、walk_speed。未收到的字段保留缺失,不补默认玩家状态。

更新都是 revision 与 kind 加对应载荷:

kind载荷与处理
list_add可选 entries 名册列表;缺失等价空列表
list_remove可选 removed UUID 列表;缺失等价空列表
add_playeruuid、addition、ability;两个 unique ID 必须一致
abilitiesability

必须先安装快照、按顺序应用本次更新,再读取下一条。next() / poll() 会确认上一份已应用文档;不要提前读取再异步乱序处理。poll() 空闲返回 None,要求能力目录支持 poll_wait_ms=100;100 ms 只是来源等待上限,不包括编码和传输。

下面是一个有界的读取函数;调用方在插件已激活时等待它完成,停止时不要遗留读取任务:

python
import asyncio
import time
from neomega_runtime.player_state import PlayerState

async def observe_for(ctx, seconds=3):
    stream, snapshot = await ctx.observe_players()
    try:
        state = PlayerState(snapshot)
        end = time.monotonic() + seconds
        while time.monotonic() < end:
            update = await stream.poll()
            if update is not None:
                state.apply(update, epoch=snapshot["epoch"])
            else:
                await asyncio.sleep(0.01)
        return state.snapshot()
    finally:
        await stream.close()

单个编码文档最多 32 MiB,按 32 KiB 分块并校验 SHA-256;解析对象另占内存。PlayerState 的三张映射各最多 4096 项,计入载荷最多 8 MiB,不含 Python 对象、副本和输入文档开销。游标跳跃、epoch 变化或非法数据会使客户端或状态永久失效,应关闭旧流,用新订阅的初始快照创建新状态;不能在旧状态上继续“补下一条”。

Text 混合流 ​

python
from neomega_runtime.player_state import PlayerState

async def poll_player_event(ctx):
    stream, snapshot = await ctx.player_events(text_profile="server_death")
    try:
        state = PlayerState(snapshot)
        event = await stream.poll()
        if event is not None and "update" in event:
            state.apply(event["update"], epoch=snapshot["epoch"])
        return state.snapshot(), event
    finally:
        await stream.close()

此示例只 poll 一次,空闲时 event 为 None。需要默认 Text 范围时,将唯一的获取调用替换为 await ctx.player_events();不要在同一变量上打开第二条流覆盖第一条。底层等价入口是 await PlayerEvents.open(peer, session, text_profile="server_death")。

PlayerEvents 从 neomega_runtime.player_events 导入,沿用 next、poll、close。每条结果含 sequence、state_revision,且恰好包含 update 或 text。update 沿用上节结构并使状态修订号加一;text 不增加状态修订号,但 sequence 仍加一,所以不能按相同 revision 丢弃 Text。

text 字段为 text_type、needs_translation、source_name、message、xuid、platform_chat_id、filtered_message、unknown、parameters、netease_extra_data。最后两项是字符串列表,各最多 128 项;单字符串最多 64 KiB,Text 计入载荷上限 256 KiB。validate_event(value) 是同模块的无副作用校验函数,非法输入抛 ProtocolError。

server_death 仅保留需要翻译、来源名和 XUID 为空、消息前缀为 death.attack. 或 death.fell. 的服务器 Text,以及玩家状态更新。它只减少噪声,不证明受害者身份、死亡次数、掉落或归属;业务必须校验消息语法和玩家身份。不支持此 profile 的来源会拒绝请求,不会退回无过滤流。

本地线格式校验还可从 neomega_runtime.player_schema 导入 validate_snapshot(value)、validate_observation(value);它们只校验,不复制、不合并状态,也不证明名册完整。

长期消费:Pump ​

text
await PlayerPump.open(peer, session, *, install, apply, reader_options=None)
await PlayerEventPump.open(peer, session, *, install, apply, reader_options=None)
await pump.wait()
await pump.close()

两类从 neomega_runtime.player_pump 导入。install(snapshot) 与 apply(item, epoch=...) 必须是可协作的异步回调。open 会检查有界 poll 能力,等待快照安装成功后才启动读取;安装失败释放来源。PlayerPump 传状态更新,PlayerEventPump 传混合事件,后者的 reader_options 可传 {"text_profile": "server_death"}。

运行域必须监管 wait(),让读取或回调错误可见。正常关闭调用并等待 close():它停止新 poll,等待已接收的条目与回调完成,再释放流;不能在自身回调中 close 或 wait 自己。取消等待者不代表清理已经完成,需要再次等待 close。低层流本身无重试、自动重连或事件合成;Pump 也不会补发初始加入事件。

玩家位置与区域采样 ​

text
await ctx.positions.sample(*, timeout=5, **selection)
ctx.positions.watch(zones, callback, *, interval=1.0, timeout=5, **selection)
PositionSampler(ctx)

Zone(name: str, dimension: int, minimum: tuple, maximum: tuple)
zone.contains(dimension, position)
ZoneTracker(zones)
tracker.update(snapshot) -> list[dict]
tracker.reset() -> list[dict]

Zone、ZoneTracker、PositionSampler 位于 neomega_runtime.positions。selection 转交 Queries.entities:target="players"、name=None、entity_type=None、tag=None、origin=None、radius=None、limit=64。示例:

python
from neomega_runtime.positions import Zone

async def start_zone_sampling(ctx):
    async def on_sample(snapshot, changes):
        for change in changes:
            ctx.log.info("区域变化 %s", change)

    return ctx.positions.watch(
        [Zone("lobby", 0, (0, 60, 0), (20, 80, 20))],
        on_sample, interval=1.0, timeout=5, target="players", limit=64,
    )

每次 sample 提交一次新的实体查询并等待回执,返回查询结果(含 entities、complete)并附 operation_id、Unix 时间 sampled_at、elapsed_ms。用于区域判断的实体行含 uniqueId: str、dimension: int、position: {x,y,z}。timeout 为 0.1–30 秒,非成功回执抛 ProtocolError 并带原操作 ID;unknown 不自动重放。

watch 返回 Worker 管理的串行任务,首次立即采样,之后每轮完成再间隔,interval 最小 0.1 秒。回调接收 (snapshot, changes),变化项为 {"kind": "enter"|"leave"|"lost", "entity_id": str, "zone": str}。任务异常向 Worker 传播。

区域包含边界,维度必须相等,坐标为有限值且绝对值不超过 30000000。区域名最多 128 UTF-8 字节,最多 64 个不同名称区域;Tracker 最多保留 64 个实体。首次看到区域内实体产生 enter,重置或重载后也如此,因此奖励资格必须独立持久化。完整采样中消失的实体产生 lost,不能当作 leave 发奖;不完整采样保留未出现的实体。两次采样之间的短暂进出可能完全错过。

玩家 Bus ​

players.online View 保留 complete=false,不是全服完整名单。players.abilities_observe read 除 Bus grant 仍要求旧 observe operation grant;players.abilities_request op 还要求旧 request grant、packet 185 及可写实例。当前 read-only 也拒绝 ability read。发送成功仅表示 dispatched,observed=false、server_acknowledged=false,不等于权限已经改变。见Bus。

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