diff --git a/AGENTS.md b/AGENTS.md
index e469cb005..cf595488e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -192,8 +192,12 @@ branch or worktree when the existing checkout contains uncommitted changes.
- Keep raw measurements and build logs outside the source repository. Published
performance claims must identify the machine, workload, units and tested
revision; local tests do not establish production PCU capacity.
-- Guest-room client/server message bodies use the canonical
- `src/proto/proto/guest_room.proto`; Entity definitions and profiles remain JSON.
+- Guest-room envelopes use the canonical `src/proto/proto/guest_room.proto`,
+ importing `src/proto/proto/entity_sync.proto` for generic nested Entity values.
+ Keep one current room contract; its handshake value is `2`. Coordinate client
+ and server upgrades without retaining the former scalar-only wire path or
+ adding version suffixes to message, file or API names.
+ Entity definitions and profiles remain JSON.
Use lowercase `snake_case` for every project `.proto` filename, message, enum,
enum value, field and handwritten oneof. Keep field numbers, types and enum
numbers stable during naming changes; retain existing JSON keys explicitly
@@ -202,8 +206,9 @@ branch or worktree when the existing checkout contains uncommitted changes.
compiler-generated map entries and optional-field oneofs are not source names.
Synchronize the Godot schema, generated C# and golden vectors with wire changes,
verify cross-repository parity and run the real two-client integration before
- claiming acceptance. Preserve owning scalar capture, explicit authorized
- fields, owner-thread flush, revision checks and bounded pending messages.
+ claiming acceptance. Preserve owning scalar/row/child capture, independently
+ trusted projections, lifetime identities, owner-thread flush, single-root
+ atomic commit, revision checks and bounded pending messages.
Incompatible wire changes require coordinated server/client upgrades and a
migration notice; never hand-edit generated Protobuf output.
diff --git a/docs/assets/entity-sync-flow.svg b/docs/assets/entity-sync-flow.svg
index 70b256c42..0152f8f2c 100644
--- a/docs/assets/entity-sync-flow.svg
+++ b/docs/assets/entity-sync-flow.svg
@@ -1 +1 @@
-
+
diff --git a/docs/assets/entity-sync-flow_CN.svg b/docs/assets/entity-sync-flow_CN.svg
index 15cb4d6aa..8f6d23219 100644
--- a/docs/assets/entity-sync-flow_CN.svg
+++ b/docs/assets/entity-sync-flow_CN.svg
@@ -1 +1 @@
-
+
diff --git a/docs/concepts/entity-sync.md b/docs/concepts/entity-sync.md
index 33dfaed00..5514ea8f0 100644
--- a/docs/concepts/entity-sync.md
+++ b/docs/concepts/entity-sync.md
@@ -1,25 +1,44 @@
-# Entity incremental synchronization
-
-Kernel provides owning scalar/nested captures and bounded revisioned C++ baseline/delta/replica APIs. The guest room connects scalar state to Protobuf and Godot C# through an explicit public projection. Nested state uses a fixed trusted binding in process; a nested wire codec, client adapter and general subscriptions remain future work. Room scalar v1 is unchanged.
-
-
-
-## Current capability and boundary
-
-| Component | Available | Boundary · contract |
-| --- | --- | --- |
-| Kernel | `capture_scalar_snapshot`, `scalar_publisher`, `scalar_replica`; coalesced changes, atomic revision checks and tombstones | In-process C++ values; caller selects authorized fields and flush point. Field/text limits cover reconstructed state, not encoded size · [scalar API](../reference/kernel.md#scalar-baselines-and-deltas), [capture limits](../reference/kernel.md#owning-scalar-snapshots) |
-| Nested state | `capture_nested_snapshot`, `nested_publisher`, `nested_replica`; owned row/child lifetimes, fixed trusted projection, atomic root validation and aggregate bounds | In-process C++; no nested codec, queue or room v1 change · [capture](../reference/kernel.md#owning-nested-snapshots), [stream](../reference/kernel.md#nested-baselines-and-deltas) |
-| Room v1 | Join/reconnect baseline, later player baselines, scalar deltas and removal over TCP; public fields 100–108, hidden configuration field 99 | Server assigns player/Entity identities; clients request actions. One lobby with bounded work/output, no account service or persistence · [protocol, identity and numeric limits](../../examples/guest_room/protocol.md) |
-| Godot C# | Generated Protobuf messages, shared fixtures, whole-room validation before commit and scene updates on the engine thread | Upgrade both peers together; JSON-only room peers are incompatible. Entity definitions/profiles stay JSON · [compatibility contract](../../examples/guest_room/protocol.md), [client](https://github.com/OpenArkStudio/ark-client-godot) |
-| Net / arknet | Framework frame delivery / transport IO | Enqueue is not an application acknowledgment; no ACK/replay, fragmentation or AOI in room v1 · [Net ownership](../reference/network.md) |
-
-Native checks cover the C++ state loop and room transport; local two-engine Godot tests cover movement, HP/MP, removal and reconnect. Main CI passed on Linux, macOS and Windows. These results do not establish production capacity or general replication.
-
-## Nested state and remaining work
-
-Owning nested capture records stable row lifetimes and separates child identity from container slots. The nested C++ publisher/replica commits one authorized root projection in order. Row numbers can be reused; deleting a row changes presentation positions without renumbering surviving row keys. Existing borrowed delayed-sync callbacks cannot provide owning nested publications.
-
-The [general Entity replication design](../project/entity-replication-design.md) ties these constraints to current source, defines baseline/delta ordering, authorization, recovery and memory bounds, and records the selected boundaries: row lifetime tokens, one atomic root without cross-root transactions, and fixed projections with reconnect recovery. Capture and the in-process differ/replica are provided; a nested codec and client adapter remain separate work. It reuses the scalar leaf values while keeping room v1 unchanged.
-
-Owner-thread rules remain in force: Net callbacks arrive through Update; HTTP IO handlers must marshal Entity commands to the owner thread. See [Net](../reference/network.md), [lifecycle](application-architecture.md), [data model](data-driven.md) and [data structures](../guides/data-structures.md). RPC, persistence, Registry and hot update remain outside this design.
+# Entity synchronization
+
+Kernel captures owning values on its owner thread. A revisioned publication then
+crosses the [generic Protobuf boundary](../reference/entity-protocol.md) into a
+portable C# replica; Godot presents only committed state.
+
+| Layer | Responsibility |
+| --- | --- |
+| Application | Bind each recipient to a root, schema, fixed field/type projection and allowed child identities |
+| Kernel | `capture_nested_snapshot` freezes scalar fields, stable table-row lifetimes and container/child relationships |
+| Publication | `nested_publisher` coalesces visible changes into one root revision; unchanged state emits nothing |
+| Wire | `entity_sync.proto` preserves scalar types, explicit row identities, container replacements and child patches |
+| Client | `NestedReplica` validates a private candidate and commits a complete root atomically; the room adapter commits all roots in its envelope together |
+| Presentation | Render committed movement, HP/MP, inventory and equipment; controls send requests without changing authoritative values |
+
+## Baseline, delta and removal
+
+| Change | Publication |
+| --- | --- |
+| Join or reconnect | Complete authorized baseline, including empty tables/containers and zero values |
+| Scalar/row-column edit | Sparse typed field patch |
+| Row deletion and reuse | Remove the old row incarnation; introduce the replacement with a new incarnation |
+| Equipment switch | Replace that container's final authorized membership with complete child state |
+| Equipped child upgrade | Patch the same child identity and incarnation |
+| Root removal | Tombstone; old deltas cannot resurrect it |
+
+A valid duplicate does not roll state backward. A revision gap, unknown identity,
+wrong type or exhausted bound invalidates the affected stream. The room closes
+the connection and reconnects with a fresh binding and baseline; it does not
+replay requests or perform an in-place reset.
+
+## Demonstration and boundaries
+
+The [guest room](../guides/examples.md) uses actual Kernel Entities: public fields
+100–108, inventory table 200 and equipment container 300. It keeps one current
+client/server contract; earlier scalar-only clients must be rebuilt together with
+the server. [Room protocol](../../examples/guest_room/protocol.md) lists requests,
+application limits and migration requirements.
+
+The reusable C++ scalar interfaces remain available for scalar-only application
+projections. They are not a second room protocol. Generic subscriptions, changing
+permissions during a connection, AOI, cross-root transactions, fragmentation,
+persistence and ACK/replay history remain outside this delivery. The
+[replication design](../project/entity-replication-design.md) records these boundaries.
diff --git a/docs/concepts/entity-sync_CN.md b/docs/concepts/entity-sync_CN.md
index 851016564..8421d171e 100644
--- a/docs/concepts/entity-sync_CN.md
+++ b/docs/concepts/entity-sync_CN.md
@@ -1,25 +1,37 @@
-# Entity 增量同步
-
-Kernel 提供拥有型标量/嵌套捕获及有界、带修订号的 C++ 基线/增量/接收端接口。访客房间通过显式公开字段将标量状态接入 Protobuf 与 Godot C#。嵌套状态在进程内使用固定可信绑定;嵌套线协议编解码、客户端适配及通用订阅仍属后续工作。房间标量 v1 保持不变。
-
-
-
-## 当前能力与边界
-
-| 组件 | 已提供 | 边界 · 契约 |
-| --- | --- | --- |
-| Kernel | `capture_scalar_snapshot`、`scalar_publisher`、`scalar_replica`;合并变化、原子修订校验与删除标记 | 进程内 C++ 值,由调用者选择授权字段和刷新点;字段/文本上限覆盖重建状态,不代表编码大小 · [标量接口](../reference/kernel_CN.md#标量基线与增量)、[捕获上限](../reference/kernel_CN.md#拥有型标量快照) |
-| 嵌套状态 | `capture_nested_snapshot`、`nested_publisher`、`nested_replica`;拥有型行/子存续期、固定可信投影、整根原子校验及聚合上限 | 进程内 C++;无嵌套编解码/队列,不改变房间 v1 · [捕获](../reference/kernel_CN.md#拥有型嵌套快照)、[状态流](../reference/kernel_CN.md#嵌套基线与增量) |
-| 房间 v1 | 加入/重连基线、后加入玩家基线、TCP 标量增量与移除;公开字段 100–108,隐藏配置字段 99 | 服务端分配玩家/Entity 身份,客户端请求操作;单个大厅,工作量/输出有界,不提供账号服务或持久化 · [协议、身份与具体上限](../../examples/guest_room/protocol_CN.md) |
-| Godot C# | 生成的 Protobuf 消息、共享样本、整体校验后提交房间状态、引擎线程更新场景 | 两端协同升级,使用 JSON 消息体的旧端不兼容;Entity 定义/业务配置仍用 JSON · [兼容契约](../../examples/guest_room/protocol_CN.md)、[客户端](https://github.com/OpenArkStudio/ark-client-godot) |
-| Net/arknet | 框架帧交付/传输 IO | 入队不等于应用确认;房间 v1 不提供 ACK/重放、分片或 AOI · [Net 所有权](../reference/network_CN.md) |
-
-原生检查覆盖 C++ 状态流程与房间传输,本机双引擎 Godot 测试覆盖移动、HP/MP、移除与重连。main CI 已在 Linux、macOS、Windows 上通过。这些结果不构成生产容量或通用复制验收。
-
-## 嵌套状态与剩余工作
-
-拥有型嵌套捕获记录稳定的行存续期,并将子身份与容器槽位分开。嵌套 C++ 发布端/接收端按顺序提交一个授权根投影。行号可以复用;删除行会改变展示位置,但不会重编号其余行键。现有借用型延迟同步回调不能提供拥有型嵌套发布。
-
-[通用 Entity 复制设计](../project/entity-replication-design_CN.md)将这些约束对应到当前源码,定义基线/增量顺序、授权、恢复及内存上限,并记录已选定的边界:行存续期标记、单根原子且不提供跨根事务、固定投影和重连恢复。已提供捕获及进程内差异器/接收端;嵌套编解码及客户端适配另行交付。方案复用标量叶子值,保持房间 v1 不变。
-
-所有者线程规则继续适用:Net 回调通过 Update 交付,HTTP IO 处理器须将 Entity 命令转交所有者线程。参见 [Net](../reference/network_CN.md)、[生命周期](application-architecture_CN.md)、[数据模型](data-driven_CN.md)与[数据结构](../guides/data-structures_CN.md)。RPC、持久化、Registry 和热更新仍不在本设计范围内。
+# Entity 同步
+
+Kernel 在所有者线程捕获拥有型值,带修订号的发布通过[通用 Protobuf 协议](../reference/entity-protocol_CN.md)
+进入可移植 C# 副本,Godot 只展示已提交的状态。
+
+| 层次 | 职责 |
+| --- | --- |
+| 应用 | 为每个接收者绑定根、Schema、固定字段/类型投影及允许的子身份 |
+| Kernel | `capture_nested_snapshot` 冻结标量、稳定的表格行存续期及容器/子 Entity 关系 |
+| 发布 | `nested_publisher` 将可见变化合并为一次根修订;状态未变时不发消息 |
+| 协议 | `entity_sync.proto` 保留标量类型、行身份、容器替换与子字段补丁 |
+| 客户端 | `NestedReplica` 在私有候选中验证后原子提交整个根;房间适配器同时提交消息内全部根 |
+| 表现 | 展示已确认的移动、HP/MP、背包和装备;操作按钮只发送请求,不提前修改权威值 |
+
+## 基线、增量与移除
+
+| 变化 | 发布内容 |
+| --- | --- |
+| 加入或重连 | 完整授权基线,包含空表、空容器与零值 |
+| 标量/行列修改 | 带明确类型的稀疏字段补丁 |
+| 删除行后复用行号 | 退役旧行存续期,以新存续期引入替代行 |
+| 换装 | 用完整子状态替换该容器最终授权成员 |
+| 强化当前装备 | 修改同一子身份与存续期的字段 |
+| 移除根 | 删除标记;旧增量不能使其复活 |
+
+合法重复不会回退状态。修订缺口、未知身份、错误类型或超限会使对应流失效。
+房间关闭连接,再以新绑定和基线恢复;不重放请求,也不进行原地重置。
+
+## 演示与边界
+
+[访客房间](../guides/examples_CN.md)使用真实 Kernel Entity:公开字段 100~108、背包表格 200、
+装备容器 300。只维护一套当前客户端/服务端协议,旧标量客户端须与服务端一起重新构建。
+[房间协议](../../examples/guest_room/protocol_CN.md)列出请求、应用上限及迁移要求。
+
+可复用的 C++ 标量接口仍可用于应用中的纯标量投影,它们不是第二套房间协议。
+通用订阅、连接内变更权限、AOI、跨根事务、分片、持久化及 ACK/重放历史不在本轮范围内。
+[复制设计](../project/entity-replication-design_CN.md)记录这些边界。
diff --git a/docs/guides/examples.md b/docs/guides/examples.md
index 156a7d08e..a0484c2f4 100644
--- a/docs/guides/examples.md
+++ b/docs/guides/examples.md
@@ -7,7 +7,7 @@ The bundled examples use capability names. Configure with applications and examp
| `examples/timers` / `timers` | `timers_plugin` | Utility timers and module lifecycle; enabled, participates in updates |
| `examples/module_lifecycle` / `module_lifecycle` | `module_lifecycle_plugin` | Lifecycle and a small data-list demonstration; enabled, no update override. The callback demonstration remains commented out |
| `examples/entity_operations` / `entity_operations` | `entity_operations_plugin` | Kernel entity/data/table operations; enabled, no update override. Startup performs a bounded functional demonstration; the [old timed workload](../../benchmarks/ENTITY_OPERATIONS.md) runs only through an explicit benchmark command |
-| `examples/guest_room` / `guest_room` | `guest_room_plugin` | Optional 2D guest lobby with JSON-defined Kernel Entity fields/configuration, authoritative movement and HP/MP actions, Protobuf scalar baselines/deltas and the real Net transport; enable `ARK_BUILD_GUEST_ROOM_EXAMPLE`. [Build, ARK2 protocol and native verification](../../examples/guest_room/README.md); official [C# Godot client](https://github.com/OpenArkStudio/ark-client-godot) |
+| `examples/guest_room` / `guest_room` | `guest_room_plugin` | Optional 2D guest lobby with JSON-defined Kernel scalar fields, inventory tables and equipment children, authoritative movement/seven actions, nested Protobuf baselines/deltas and real Net transport; enable `ARK_BUILD_GUEST_ROOM_EXAMPLE`. [Build, protocol and native verification](../../examples/guest_room/README.md); official [C# Godot client](https://github.com/OpenArkStudio/ark-client-godot) |
| `examples/mysql_sample` / `mysql_sample` | `mysql_sample_plugin` | Native MySQL coroutine example; disabled in the application example list, assembly checked by database tests with SSL enabled |
| `examples/redis_sample` / `redis_sample` | `redis_sample_plugin` | Native Redis coroutine example; disabled in the application example list, assembly checked by database tests |
| `examples/physics_scene` / `physics_scene` | `physics_scene_plugin` | Disabled; requires unsupported legacy PhysX 3. The checked-in sources and manifest do not establish a runnable integration |
@@ -16,6 +16,20 @@ The bundled examples use capability names. Configure with applications and examp
hpp/cpp files remain adjacent in each directory. Start with the [timers quick start](../get-started/quick-start.md). The generic launcher helpers select `conf/.app.toml`, so use the capability name for both the process and application manifest. Kernel examples still require their business metadata.
+## Guest-room Entity replication
+
+Each player is one fixed authorized root stream. Its complete baseline contains
+the inventory table at member 200 and the equipment container at member 300;
+subsequent changes use [the generic Entity protocol](../reference/entity-protocol.md)
+and an atomic C++/C# replica. Actions are `damage`, `skill`, `rest`, `pickup`,
+`use_potion`, `equip` and `upgrade`. The current handshake value is `2`; upgrade
+server and client together. The room keeps one protocol and reconnects for recovery.
+General subscriptions, dynamic authorization and AOI remain TODO.
+
+Native checks, portable C# tests and actual Godot execution have separate evidence.
+This sample does not establish deployment or capacity acceptance; see the
+[local priorities and load-test prerequisite](../project/roadmap.md#current-priorities).
+
## Guest-room resource cleanup
The room uses ordinary Join, Move, Action and control callbacks without manager-state business refusal. Applications/operators complete announcements, ingress closure, player removal and saving before process stop. A controlled native child loads actual Room/Kernel/Net DSOs for Net endpoint closure, capture release and repeated resource cleanup. It supplies no native-send, library-image unload or Godot acceptance guarantee. See [example verification scope](../../examples/guest_room/README.md#native-verification).
diff --git a/docs/guides/examples_CN.md b/docs/guides/examples_CN.md
index 9afa0bcd2..ca8c491cb 100644
--- a/docs/guides/examples_CN.md
+++ b/docs/guides/examples_CN.md
@@ -7,7 +7,7 @@
| `examples/timers`/`timers` | `timers_plugin` | Utility 定时器与模块生命周期;启用,参与 update |
| `examples/module_lifecycle`/`module_lifecycle` | `module_lifecycle_plugin` | 生命周期与少量 data-list 演示;启用,无 update 覆写。回调演示代码仍被注释 |
| `examples/entity_operations`/`entity_operations` | `entity_operations_plugin` | Kernel 实体、数据和表操作;启用,无 update 覆写。启动仅做有界功能演示;[旧计时负载](../../benchmarks/ENTITY_OPERATIONS_CN.md)只能通过显式基准命令运行 |
-| `examples/guest_room`/`guest_room` | `guest_room_plugin` | 可选二维访客大厅,使用 JSON 定义的 Kernel Entity 字段/配置、权威移动与 HP/MP 操作、Protobuf 标量基线/增量,以及真实 Net 传输;通过 `ARK_BUILD_GUEST_ROOM_EXAMPLE` 启用。[构建、ARK2 协议与原生验证](../../examples/guest_room/README_CN.md);官方 [C# Godot 客户端](https://github.com/OpenArkStudio/ark-client-godot) |
+| `examples/guest_room`/`guest_room` | `guest_room_plugin` | 可选二维访客大厅,使用 JSON 定义的 Kernel 标量字段、背包表格及装备子 Entity、权威移动/七种操作、嵌套 Protobuf 基线/增量和真实 Net 传输;通过 `ARK_BUILD_GUEST_ROOM_EXAMPLE` 启用。[构建、协议与原生验证](../../examples/guest_room/README_CN.md);官方 [C# Godot 客户端](https://github.com/OpenArkStudio/ark-client-godot) |
| `examples/mysql_sample`/`mysql_sample` | `mysql_sample_plugin` | 原生 MySQL 协程示例;应用示例列表中仍关闭,开启 SSL 的数据库测试检查其装配 |
| `examples/redis_sample`/`redis_sample` | `redis_sample_plugin` | 原生 Redis 协程示例;应用示例列表中仍关闭,数据库测试检查其装配 |
| `examples/physics_scene`/`physics_scene` | `physics_scene_plugin` | 关闭,依赖不支持的旧 PhysX 3;保留源码与清单不代表集成可运行 |
@@ -17,6 +17,17 @@
每个目录中的 hpp/cpp 继续并排存放。可从 [timers 快速开始](../get-started/quick-start_CN.md)入手。
通用启动脚本选择 `conf/.app.toml`,因此进程名称和应用清单都应使用能力名称。Kernel 示例仍需要业务元数据。
+## 访客房间 Entity 复制
+
+每个玩家对应一条固定授权的根流。完整基线包含成员 200 的背包表格和成员 300 的
+装备容器,后续变化使用[通用 Entity 协议](../reference/entity-protocol_CN.md)及原子
+C++/C# 接收端。七种操作为 `damage`、`skill`、`rest`、`pickup`、`use_potion`、
+`equip` 和 `upgrade`。当前握手值为 `2`,服务端与客户端须协调升级。房间只保留一套
+协议,通过重连恢复。通用订阅、动态授权及 AOI 仍为 TODO。
+
+原生检查、可移植 C# 测试和实际 Godot 执行保留各自证据。示例不构成部署或容量验收,
+见[本机优先级与负载验收前置条件](../project/roadmap_CN.md#当前优先级)。
+
## 访客房间资源清理
访客房间使用普通 Join、Move、Action 和控制回调,不按管理器状态拒绝业务请求。应用或运维先完成公告、关入口、踢人和保存,再停止进程。受控原生子进程加载真实 Room/Kernel/Net DSO,检查 Net 端点关闭、捕获释放与重复资源清理;不提供原生发送、库映像卸载或 Godot 验收保证。见[示例验证范围](../../examples/guest_room/README_CN.md#原生验证)。
diff --git a/docs/project/entity-replication-design.md b/docs/project/entity-replication-design.md
index d753739f5..918e966c3 100644
--- a/docs/project/entity-replication-design.md
+++ b/docs/project/entity-replication-design.md
@@ -1,6 +1,6 @@
# General Entity replication design
-**Selected direction; staged delivery.** Owning scalar capture/publication and [owning nested capture](../reference/kernel.md#owning-nested-snapshots) are available. The [trusted-binding C++ nested differ/replica](../reference/kernel.md#nested-baselines-and-deltas) is also available; a nested wire codec/client adapter remains separate. The target is a bounded root projection containing tables and container children; room v1 remains unchanged and no nested wire representation is selected. [Current capability](../concepts/entity-sync.md) remains the supported boundary.
+The supported contract is a bounded, authorized root tree containing scalar fields, tables and container children. [Owning capture](../reference/kernel.md#owning-nested-snapshots), the [trusted-binding C++ publisher/replica](../reference/kernel.md#nested-baselines-and-deltas), the canonical [Protobuf codec](../reference/entity-protocol.md) and a portable C# replica are available. The [Godot guest room](../guides/examples.md#guest-room-entity-replication) uses this contract for inventory rows and equipment children. [Current capability](../concepts/entity-sync.md) separates these primitives from remaining subscription and authorization orchestration.
## Existing constraints determine the design
@@ -13,18 +13,18 @@
| [`entity_snapshot.cpp`](../../src/plugins/kernel/entity_snapshot.cpp) and [`entity_stream.hpp`](../../src/plugins/kernel/entity_stream.hpp) | Reuse selected scalar values and validation. Capture requires the owner thread; the scalar publisher requires a fixed selection/type and cannot emit row/slot operations. |
| [Legacy delayed collection](../reference/kernel.md#delayed-synchronization-batches) borrows mutable nodes; Kernel destruction can defer self-destruction | The new collector must own values and observe completed lifecycle changes. Existing operation enums, immediate/deferred callbacks and `protobuf_delay_entity` are not the new contract. |
-The initial nested scope is schema-declared scalar table columns and an explicitly bounded container tree. Custom fields, events, cyclic/shared graphs, cross-root transactions, RPC, Registry and persistence are outside this proposal. Invalid repeated-child/cyclic relationships must fail capture rather than recurse indefinitely. Container mutation correctness remains a prerequisite; this design does not certify every existing swap or failure path.
+The supported nested scope is schema-declared scalar table columns and an explicitly bounded container tree. Custom fields, events, cyclic/shared graphs, cross-root transactions, RPC, Registry and persistence are outside this contract. Invalid repeated-child/cyclic relationships must fail capture rather than recurse indefinitely. Container mutation correctness remains a prerequisite; this design does not certify every existing swap or failure path.
## Identity and authorized projection
-The notation below describes semantic keys, not proposed fields or C++ types. An application binds the recipient, root GUID, schema identity and allowed projection on the server. No gateway or account service is required.
+The diagram below describes semantic keys; the [protocol reference](../reference/entity-protocol.md) defines their message and C++ representations. An application binds the recipient, root GUID, schema identity and allowed projection on the server. No gateway or account service is required.

-| Identity | Proposed invariant |
+| Identity | Invariant |
| --- | --- |
-| Root/child lifetime | GUID plus incarnation prevents reuse from reviving an old Entity. A stream generation identifies a rebuilt projection; do not confuse it with the Entity's lifetime or a business request sequence. Room v1 continues using its existing GUID/generation contract. |
-| Row lifetime | Table-owner lifetime + table member index + row number + non-reused lifetime token. Allocate on successful insertion, retire on deletion/clear, including delete/re-add between two flushes. A final-state diff alone cannot detect identical-value reuse. See decision 1 for a smaller alternative. |
+| Root/child lifetime | GUID plus incarnation prevents reuse from reviving an old Entity. A stream generation identifies a rebuilt projection; do not confuse it with the Entity's lifetime or a business request sequence. The room binds root incarnation and stream generation separately from state. |
+| Row lifetime | Table-owner lifetime + table member index + row number + non-reused lifetime token. Allocate on successful insertion, retire on deletion/clear, including delete/re-add between two flushes. A final-state diff alone cannot detect identical-value reuse. |
| Container relationship | Parent lifetime + container member index + slot number, with the current child lifetime as the value. A swap changes relationships while child identities stay stable. Removal/reuse must not redirect a stale child update to the new occupant. |
| Schema | Application-defined schema identity binds member kind/index, scalar type/width and referenced classes throughout the projection. JSON format `version = 1` only versions the file format. Removal, index reuse or type/reference changes require an explicit compatibility decision and a new compatible baseline; never reinterpret old bytes. |
@@ -32,21 +32,21 @@ Authorize root access, each table/column, each relationship and each child field
## One ordered root commit
-The proposed consistency boundary is one selected root tree per recipient. A baseline is its complete authorized state, including empty tables/slots and type-correct zero values. Freeze it at revision R on the owner thread after initialization and business mutations; later mutations belong to the next capture. Do not publish a partially initialized Entity or infer flush order from module registration order.
+The consistency boundary is one selected root tree per recipient. A baseline is its complete authorized state, including empty tables/slots and type-correct zero values. Freeze it at revision R on the owner thread after initialization and business mutations; later mutations belong to the next capture. Do not publish a partially initialized Entity or infer flush order from module registration order.

-A delta applies only to its matching schema, lifetime, projection generation and required revision. Valid duplicates do not regress state. A gap, unknown row lifetime/child binding or malformed operation invalidates the root; no prefix becomes visible. Validate a candidate before committing, as Godot's existing `RoomSession` does for a whole room envelope. This proposes root atomicity, not cross-root atomicity or rollback of server business mutations.
+A delta applies only to its matching schema, lifetime, projection generation and required revision. Valid duplicates do not regress state. A gap, unknown row lifetime/child binding or malformed operation invalidates the root; no prefix becomes visible. The C++ and portable C# replicas validate complete candidates before committing; `RoomSession` additionally stages complete room envelopes. The contract provides root atomicity and does not roll back server business mutations or provide cross-root transactions.
| Change before sealing | Required result relative to the previous published projection |
| --- | --- |
| Add row → set columns | Add the new row lifetime with complete final authorized values; later revisions may patch columns. |
| Set columns → delete row | Delete that lifetime; suppress its unsealed column patches. Already admitted revisions stay before deletion. |
| Delete row 2 → add row 2 | Retire old lifetime before introducing the replacement. Unchanged rows keep their keys; never encode a positional shift as a different row's update. |
-| Clear → add rows | Clear barrier retires prior row lifetimes, then add complete new rows. A validated full-table replacement is an alternative encoding of the same state transition. |
-| Place / swap / detach child | Introduce a previously unseen child's authorized baseline before exposing its relationship; replace all affected slots in one root commit. Detach removes a relationship; drop child state once no authorized reference in the projection remains. |
+| Clear → add rows | Retire prior row lifetimes; emit lifetime-checked removals, complete replacements and complete additions for the final captured state. |
+| Place / swap / detach child | Include newly visible complete child subtrees in the container replacement and expose all affected slots in one root commit. Detach removes a relationship; drop child state once no authorized reference in the projection remains. |
| Destroy child / root | Remove visible relationships and child state in the same root commit, or tombstone the entire root. Suppress unsealed updates; retained owning publications remain valid in order. Unseen create/destroy may coalesce away. |
-| Cross-root move | Coordinate explicit replacement baselines for affected roots or reject the unsupported replication transition; no implicit atomic transfer. See decision 2. |
+| Cross-root move | Excluded from incremental transfer. Reconnect with fresh independently bound streams if the application needs new root membership; no implicit atomic transfer. |
Use final-state capture for coalescing, plus successful lifecycle observations for row/Entity lifetime changes. Treat legacy callbacks as observations requiring an audit of success, exception and reentry paths. A thrown callback can occur after insertion, while deletion callbacks run before erase; blindly turning every callback into a durable operation log is insufficient. Immediate and deferred mutation paths must produce the same projected result. `real_time` can request an earlier flush, without bypassing authorization, revision order or limits.
@@ -54,7 +54,7 @@ Use final-state capture for coalescing, plus successful lifecycle observations f

-Recheck authorization before sender admission. Expansion needs a replacement projection baseline before its deltas. Revocation removes omitted fields/rows/children from client state and cancels unadmitted unauthorized publications. If already admitted bytes cannot be withdrawn, close the affected connection and discard local state; bytes already transmitted cannot be recalled. A transport close cannot itself prove remote erasure. Reconnect obtains a fresh server binding and complete authorized state. In-place generation reset requires the separately chosen reset contract below.
+Each stream has a fixed projection. When authorization or selected membership changes, the application cancels unadmitted publications, closes the affected connection and discards local state. Reconnect obtains a fresh independently authorized binding and complete baseline; it does not reuse an installed replica as a reset target. Already transmitted bytes cannot be recalled, and transport close cannot itself prove remote erasure. General subscription/authorization orchestration and in-place generation reset remain TODO.
| Resource | Required bound and failure behavior |
| --- | --- |
@@ -63,16 +63,17 @@ Recheck authorization before sender admission. Expansion needs a replacement pro
| Lifetime bookkeeping | Active row tokens, retired/tombstone identities and retained immutable handles; scope to a bounded session/generation, release on disconnect. Exhausted lifetime counters require replacing the affected source lifetime or terminating; changing only a stream generation never resets a table counter. |
| Delivery/recovery | Bound baseline/reset attempts and reconnect rate. Oversize, capture/encoding failure or enqueue/send rejection closes the affected stream/connection. No silent delta drop, implicit retry or ACK/replay history. |
-Copy selected scalars, row values and child/slot identities into owned immutable publications. Never hand borrowed Entity/table/row/node pointers to IO. Asynchronous encoding, if later selected, must preserve publication order and bound in-flight work. `snapshot_limits` bounds scalar field/text state, not exact process memory or encoded Protobuf size. Room v1's 8192-byte body and eight-frame pending limit remain sample-specific; do not inherit them as general replication defaults. Fragmentation would require bounded assembly and atomic install and is excluded from the initial scope.
+Copy selected scalars, row values and child/slot identities into owned immutable publications. Never hand borrowed Entity/table/row/node pointers to IO. Asynchronous encoding, if later selected, must preserve publication order and bound in-flight work. `nested_snapshot_limits` and `nested_stream_limits` bound native state/work/history; [codec limits](../reference/entity-protocol.md#independent-limits-and-failures) separately bound encoded body size and parsing depth. They do not measure exact process memory. The room's 8192-byte body and eight-frame pending limit remain sample-specific; do not inherit them as general replication defaults. Fragmentation would require bounded assembly and atomic install and is excluded from the initial scope.
-## Selected decisions and delivery stages
+## Supported layers and remaining work
-| Decision | Selected boundary | Deferred alternative and cost |
+| Layer | Supported boundary | Remaining work |
| --- | --- | --- |
-| 1. Stable rows | Preserve current row numbers and add a lifetime token in the replication layer; verify every successful insert/delete/clear path. | Start with a new table generation and complete table replacement on structural changes, with row-number patches only inside that generation. This avoids row-token hooks initially but increases copying/bandwidth. An opaque permanent row ID changes the identity model further. |
-| 2. Consistency across roots | Keep one root atomic; exclude cross-root transfer from incremental MVP and require explicit affected-root baselines. | A coordinated transfer across roots needs a new transaction/ordering contract and authorization failure semantics. Existing container swap callbacks do not supply it. |
-| 3. Subscription reset/recovery | Begin with reconnect-only recovery and fixed projections per connection, matching room v1's recovery scope. | In-place per-root resets avoid disconnecting unrelated roots but require generation negotiation, discard rules for queued old deltas, revocation handling and reset-rate bounds. |
+| Owning capture | Explicit authorized selections, stable row numbers and non-reused row/Entity incarnations; bounded tree capture on the owner thread | Custom fields, events and cyclic/shared graphs are outside the contract |
+| C++ state | One revision for scalar/table/relationship changes, complete baselines, sparse row patches, container membership replacement, candidate commit and terminal rejection | No cross-root transaction, in-place reset, ACK or replay history |
+| Wire | `entity_sync.proto` with nine explicit scalar types, repeated indexed records, row lifetimes and container replacement/patch; independently trusted binding and bounded codec | No fragmentation or automatic subscription protocol |
+| Client/sample | Portable C# immutable model/replica; room table 200 for inventory and container 300 for equipment, seven actions and reconnect recovery | General subscriptions, dynamic authorization, AOI and interest management remain TODO |
-Stage 1 provides owning nested capture, lifetime identities and bounded explicit projection input. Stage 2 provides the root C++ differ/replica with atomic candidate commits and terminal invalidation, fixed trusted bindings and independently bounded state/patch/lifetime metadata. Its local tests include injected allocation failures and real Kernel captures; this does not establish network delivery. A codec requires separate review. Reuse `capture_scalar_snapshot` for each authorized Entity's scalar leaf; reuse `scalar_value` for row columns. Keep existing scalar publishers for scalar-only roots. The nested coordinator uses one outer revision for scalar, table and relationship changes; feeding only scalar deltas through today's publisher cannot advance a table-only revision or atomically commit the tree. This does not require changing today's scalar API or room messages.
+Scalar-only C++ applications may continue using the native scalar publisher. The room uses one current nested protocol; it does not retain the former scalar-only wire path. Its handshake value is `2`, requiring coordinated server/client upgrades; message, file and API names have no version suffix. See the [room protocol](../../examples/guest_room/protocol.md).
-Acceptance must exercise `{1,2,3} → {1,3} → {1,2(new),3}`, identical-value row reuse, clear/re-add, slot reuse/swap/detach/destroy, container-only changes, two recipients with different permissions, revocation while queued, late join, duplicates/gaps, schema/type mismatch, GUID reuse, counter exhaustion, allocation/admission failure and shutdown with retained owning values. Compare final authorized projections from immediate/deferred paths. Wire work then needs C++/C# parity, malformed-input/golden tests and real two-client Godot integration; local correctness does not establish deployment or capacity.
+Permanent local checks cover owning captures, row/Entity reuse, sparse and replacement changes, duplicate/gap handling, complete candidate/history limits, allocation failure and C++/C# wire samples. Native room and portable C# checks exercise the included application integration. Actual Godot execution, hosted/platform checks and deployment/capacity acceptance remain separate evidence. Future dynamic subscription/authorization work must additionally validate per-recipient visibility and queued revocation. The [roadmap priorities](roadmap.md#current-priorities) keep deployment and larger-load work behind their stated prerequisites.
diff --git a/docs/project/entity-replication-design_CN.md b/docs/project/entity-replication-design_CN.md
index 936117cf4..defc54aa9 100644
--- a/docs/project/entity-replication-design_CN.md
+++ b/docs/project/entity-replication-design_CN.md
@@ -1,6 +1,6 @@
# 通用 Entity 复制设计
-**方向已选定,分阶段交付。** 拥有型标量捕获/发布及[拥有型嵌套捕获](../reference/kernel_CN.md#拥有型嵌套快照)已提供,[可信绑定的 C++ 嵌套差异器/接收端](../reference/kernel_CN.md#嵌套基线与增量)也已提供;嵌套线协议编解码/客户端适配另行交付。目标是包含表格及容器子 Entity 的有界根投影;房间 v1 保持不变,尚未选择嵌套线协议表示。[当前能力](../concepts/entity-sync_CN.md)仍是已支持的边界。
+当前契约是一棵有界、已授权的根树,包含标量字段、表格和容器子 Entity。[拥有型捕获](../reference/kernel_CN.md#拥有型嵌套快照)、[可信绑定的 C++ 发布端/接收端](../reference/kernel_CN.md#嵌套基线与增量)、规范 [Protobuf 编解码](../reference/entity-protocol_CN.md)及可移植 C# 接收端均已提供。[Godot 访客房间](../guides/examples_CN.md#访客房间-entity-复制)通过该契约同步背包行和装备子 Entity。[当前能力](../concepts/entity-sync_CN.md)区分这些基础能力与仍待实现的订阅/授权编排。
## 现有约束决定设计
@@ -13,18 +13,18 @@
| [`entity_snapshot.cpp`](../../src/plugins/kernel/entity_snapshot.cpp) 和 [`entity_stream.hpp`](../../src/plugins/kernel/entity_stream.hpp) | 复用已选标量值及校验。捕获要求所有者线程;标量发布端要求固定选择/类型,不能发布行/槽位操作。 |
| [旧延迟收集](../reference/kernel_CN.md#延迟同步批次)借用可变节点;Kernel 可延迟销毁正在执行的 Entity | 新收集器须拥有值,并观察已完成的生命周期变化。现有操作枚举、即时/延迟回调和 `protobuf_delay_entity` 不是新契约。 |
-首期嵌套范围为 Schema 声明的标量表格列,以及显式有界的容器树。自定义字段、事件、循环/共享图、跨根事务、RPC、Registry 和持久化不在本提案内。重复子引用或循环关系必须使捕获失败,不能无限递归。容器修改本身正确是前置条件;本设计不构成全部既有交换或失败路径的验收。
+已支持的嵌套范围为 Schema 声明的标量表格列,以及显式有界的容器树。自定义字段、事件、循环/共享图、跨根事务、RPC、Registry 和持久化不在该契约内。重复子引用或循环关系必须使捕获失败,不能无限递归。容器修改本身正确是前置条件;本设计不构成全部既有交换或失败路径的验收。
## 身份与授权投影
-下图表示语义键,不是拟新增的字段或 C++ 类型。应用在服务端绑定接收者、根 GUID、Schema 身份和允许的投影,无需网关或账号服务。
+下图表示语义键;[协议参考](../reference/entity-protocol_CN.md)定义对应消息及 C++ 表示。应用在服务端绑定接收者、根 GUID、Schema 身份和允许的投影,无需网关或账号服务。

-| 身份 | 拟议不变量 |
+| 身份 | 不变量 |
| --- | --- |
-| 根/子 Entity 存续期 | GUID 加实例代次,防止复用后旧 Entity 复活。流代次标识重建的投影,不等于 Entity 存续期或业务请求序列号。房间 v1 保持既有 GUID/generation 契约。 |
-| 行存续期 | 表格所有者存续期 + 表格成员索引 + 行号 + 不复用的存续期标记。成功插入时分配,删除/清空时退役;两次刷新之间的删除再新增也须换标记。只比较最终值无法发现值相同的复用;更小方案见选型 1。 |
+| 根/子 Entity 存续期 | GUID 加实例代次,防止复用后旧 Entity 复活。流代次标识重建的投影,不等于 Entity 存续期或业务请求序列号。房间将根实例标记及流代次与状态分别绑定。 |
+| 行存续期 | 表格所有者存续期 + 表格成员索引 + 行号 + 不复用的存续期标记。成功插入时分配,删除/清空时退役;两次刷新之间的删除再新增也须换标记。只比较最终值无法发现值相同的复用。 |
| 容器关系 | 父存续期 + 容器成员索引 + 槽位号,值为当前子 Entity 的存续期。交换改变关系,子身份保持稳定。移除/复用不能让旧子更新指向新占用者。 |
| Schema | 应用定义的 Schema 身份绑定整个投影中的成员种类/索引、标量类型/宽度及引用类。JSON 的 `version = 1` 只标识文件格式。删除、复用索引或改变类型/引用类须明确兼容策略,并安装兼容的新基线,不能重新解释旧字节。 |
@@ -32,21 +32,21 @@
## 一个根的有序提交
-拟议一致性边界为每个接收者的一棵选定根树。基线是完整授权状态,包括空表格/槽位和类型正确的零值。在初始化和业务修改结束后,于所有者线程冻结修订 R;后续修改进入下一次捕获。不能发布部分初始化的 Entity,也不能通过模块注册顺序推断刷新顺序。
+一致性边界为每个接收者的一棵选定根树。基线是完整授权状态,包括空表格/槽位和类型正确的零值。在初始化和业务修改结束后,于所有者线程冻结修订 R;后续修改进入下一次捕获。不能发布部分初始化的 Entity,也不能通过模块注册顺序推断刷新顺序。

-增量仅应用到匹配的 Schema、存续期、投影流代次及基准修订。合法重复不能使状态回退。缺口、未知行存续期/子绑定或非法操作使根失效,不暴露已应用的前半部分。提交前先校验候选状态,与 Godot 现有 `RoomSession` 对整个房间消息的处理方式一致。本方案要求根原子性,不提供跨根原子性,也不回滚服务端业务修改。
+增量仅应用到匹配的 Schema、存续期、投影流代次及基准修订。合法重复不能使状态回退。缺口、未知行存续期/子绑定或非法操作使根失效,不暴露已应用的前半部分。C++ 和可移植 C# 接收端在提交前验证完整候选;`RoomSession` 还暂存完整房间消息。契约提供根原子性,不回滚服务端业务修改,也不提供跨根事务。
| 封装前的变化 | 相对上一已发布投影的必要结果 |
| --- | --- |
| 新增行 → 设置列 | 以完整的最终授权值新增该行存续期;后续修订可修改列。 |
| 修改列 → 删除行 | 删除该存续期,抑制未封装的列修改;已入队修订仍先于删除。 |
| 删除行 2 → 新增行 2 | 先退役旧存续期,再引入替代行。未变行保留键,不能把展示位置左移编码为另一行的更新。 |
-| 清空 → 新增行 | 清空屏障退役旧行存续期,再新增完整行。经完整校验的整表替换是同一状态转换的另一种表示。 |
-| 放入/交换/移除子 Entity | 先引入未见子 Entity 的授权基线,再暴露关系;一个根提交替换所有受影响槽位。移除只解除关系;投影中不再有授权引用时释放子状态。 |
+| 清空 → 新增行 | 退役旧行存续期,按最终捕获状态输出核对存续期的删除、完整替换及完整新增。 |
+| 放入/交换/移除子 Entity | 容器替换包含新可见的完整子树,一个根提交一起暴露所有受影响槽位。移除只解除关系;投影中不再有授权引用时释放子状态。 |
| 销毁子 Entity/根 | 同一根提交移除可见关系和子状态,或以删除标记移除整根。抑制未封装更新;已保留的拥有型发布仍按顺序有效。未见的创建后销毁可合并消失。 |
-| 跨根移动 | 协调受影响根的显式替代基线,或拒绝不支持的复制转换;不隐含原子转移。参见选型 2。 |
+| 跨根移动 | 不支持增量转移。应用需要新根成员时,通过重连获取独立绑定的新流;不隐含原子转移。 |
使用最终状态捕获合并状态,同时观察成功的生命周期变化以识别行/Entity 存续期。旧回调只能作为观察输入,须审查成功、异常和重入路径:插入完成后回调可能抛出异常,删除回调则先于擦除,不能直接把每次回调当成持久操作日志。即时和延迟修改路径必须得到相同的投影结果。`real_time` 可以提示提前刷新,不能绕过授权、修订顺序或上限。
@@ -54,7 +54,7 @@

-发送端入队前再次校验权限。扩权须先安装替代投影基线,再接收增量。撤权从客户端状态移除被省略的字段/行/子 Entity,并取消尚未入队的越权发布。已入队字节无法撤回时,关闭受影响连接并丢弃本地状态;已经传出的字节无法收回,传输关闭本身也不能证明远端已擦除。重连取得新服务端绑定与完整授权状态。原地流代次重置需要下表单独选择的重置契约。
+每条流保持固定投影。权限或选择成员变化时,应用取消尚未入队的发布、关闭受影响连接并丢弃本地状态。重连取得独立授权的新绑定与完整基线,不复用已安装接收端作为重置目标。已经传出的字节无法收回,传输关闭本身也不能证明远端已擦除。通用订阅/授权编排及原地流代次重置仍为 TODO。
| 资源 | 必须限制的内容与失败处理 |
| --- | --- |
@@ -63,16 +63,17 @@
| 存续期记录 | 活跃行标记、已退役/删除身份与保留的不可变句柄;限制在有界会话/流代次内,断线时释放。存续期计数耗尽须替换对应源存续期或终止;只更换流代次不重置表格计数器。 |
| 交付/恢复 | 限制基线/重置尝试与重连频率。超限、捕获/编码失败或入队/发送拒绝须关闭受影响流/连接。不静默丢增量、不隐式重试、不保留 ACK/重放历史。 |
-将选定标量、行值及子/槽位身份复制为拥有型不可变发布,不向 IO 线程传递借用的 Entity/table/row/node 指针。未来若选择异步编码,须保持发布顺序并限制在途工作。`snapshot_limits` 限制标量字段/文本状态,不代表进程精确内存或 Protobuf 编码大小。房间 v1 的 8192 字节消息体和八帧待发上限只适用于示例,不能作为通用复制默认值。分片需要有界组装与原子安装,不纳入首期。
+将选定标量、行值及子/槽位身份复制为拥有型不可变发布,不向 IO 线程传递借用的 Entity/table/row/node 指针。未来若选择异步编码,须保持发布顺序并限制在途工作。`nested_snapshot_limits` 和 `nested_stream_limits` 限制原生状态/工作量/身份历史,[编解码上限](../reference/entity-protocol_CN.md#独立上限与失败处理)另行限制编码后消息体及解析层数;它们不测量进程精确内存。房间的 8192 字节消息体和八帧待发上限只适用于示例,不能作为通用复制默认值。分片需要有界组装与原子安装,不纳入首期。
-## 已选方向与交付阶段
+## 已支持的层次与剩余工作
-| 选型 | 已选边界 | 暂缓的替代方案与成本 |
+| 层次 | 已支持边界 | 剩余工作 |
| --- | --- | --- |
-| 1. 稳定行身份 | 保留现有行号,在复制层加入存续期标记;核对每条成功插入/删除/清空路径。 | 首期在结构变化时更换表格代次并完整替换表格,仅在同一代次内按行号修改列。这样暂不需要行标记钩子,但增加复制量/带宽。永久不透明行 ID 会进一步改变身份模型。 |
-| 2. 跨根一致性 | 保持单根原子;增量 MVP 排除跨根转移,要求受影响根的显式基线。 | 跨根协调转移需要新事务/顺序契约及授权失败语义,现有容器交换回调不提供这些保证。 |
-| 3. 订阅重置/恢复 | 首期仅重连恢复,每条连接保持固定投影,与房间 v1 的恢复范围一致。 | 原地按根重置可避免断开无关根,但须定义流代次协商、排队旧增量的丢弃规则、撤权处理和重置频率上限。 |
+| 拥有型捕获 | 显式授权选择、稳定行号、不复用的行/Entity 实例标记、所属线程的有界树捕获 | 自定义字段、事件及循环/共享图不在契约内 |
+| C++ 状态 | 一个修订统一标量/表格/关系变化,完整基线、稀疏行补丁、容器成员替换、候选提交及终态拒绝 | 不提供跨根事务、原地重置、ACK 或重放历史 |
+| 线协议 | `entity_sync.proto` 包含九种显式标量、repeated 索引记录、行存续期及容器替换/补丁;独立可信绑定和有界编解码 | 不提供分片或自动订阅协议 |
+| 客户端/示例 | 可移植 C# 不可变模型/接收端;房间表格 200 同步背包、容器 300 同步装备,提供七种操作与重连恢复 | 通用订阅、动态授权、AOI 和兴趣管理仍为 TODO |
-阶段 1 提供拥有型嵌套捕获、存续期身份及有界显式投影输入。阶段 2 提供根 C++ 差异器/接收端,包含候选原子提交、终态失效、固定可信绑定,以及分别受限的状态/补丁/身份历史。本地测试包括分配失败注入及真实 Kernel 捕获,不代表网络交付。编解码须另行评审。复用 `capture_scalar_snapshot` 捕获每个授权 Entity 的标量叶子,复用 `scalar_value` 表示行列值。纯标量根继续使用现有标量发布端。嵌套协调器使用一个外层修订统一标量、表格和关系变化;只通过现有发布端传递标量增量,无法推进仅表格变化的修订,也无法原子提交整树。这不要求修改现有标量 API 或房间消息。
+纯标量 C++ 应用仍可使用原生标量发布端。房间只保留一套当前嵌套协议,不保留原标量线协议分支。握手值为 `2`,要求协调升级服务端与客户端;消息、文件和 API 名称不带版本后缀。见[房间协议](../../examples/guest_room/protocol_CN.md)。
-验收须覆盖 `{1,2,3} → {1,3} → {1,2(new),3}`、同值行复用、清空再新增、槽位复用/交换/移除/销毁、仅容器变化、权限不同的两个接收者、排队期间撤权、后加入、重复/缺口、Schema/类型错误、GUID 复用、计数耗尽、分配/入队失败及停机后保留拥有型值。对比即时/延迟路径的最终授权投影。线协议阶段再加入 C++/C# 对应、非法输入/共享样本测试及真实双客户端 Godot 集成;本地正确性不构成部署或容量验收。
+永久本地检查覆盖拥有型捕获、行/Entity 复用、稀疏及替换变化、重复/缺口、完整候选/历史上限、分配失败和 C++/C# 共享线协议样本。原生房间及可移植 C# 检查验证内置应用集成。实际 Godot 执行、托管/平台检查与部署/容量验收属于不同证据。未来动态订阅/授权工作还须验证各接收者可见性及排队期间撤权。[路线图优先级](roadmap_CN.md#当前优先级)保留部署及更大负载工作的前置条件。
diff --git a/docs/project/roadmap.md b/docs/project/roadmap.md
index f44b5f4b6..dd537ab8c 100644
--- a/docs/project/roadmap.md
+++ b/docs/project/roadmap.md
@@ -9,16 +9,23 @@ Use this index to find supported capabilities and remaining work. C++20 is the b
| Project layout | Co-located plugin sources and explicit `` public-header mappings · [layout and interfaces](../reference/directory-structure.md) | C++20 implementation choices and retained compatibility boundaries: [code conventions](code-style.md#c20-implementation); optional integrations retain their stated support limits |
| Databases | Owner-thread Asio coroutine APIs, owning values, MySQL prepared statements/transactions and Redis commands/pipelines · [MySQL](../reference/mysql.md), [Redis](../reference/redis.md) | Target-environment recovery, persistence, cancellation/fault and TLS acceptance · [tested operations and exclusions](../guides/database-validation.md#acceptance-boundaries) |
| Entity configuration | Native CSV/XLSX/JSON export, C++/C# field constants and startup JSON loading · [tools](../guides/tools.md), [Kernel loading](../reference/kernel.md#json-entity-schema-and-business-data) | No live reload, database migration or client/server distribution filtering |
-| Entity synchronization | Owning scalar/nested captures, bounded revisioned C++ publisher/replica and room scalar v1 Protobuf/Godot integration · [synchronization](../concepts/entity-sync.md), [guest room](../guides/examples.md) | General subscriptions/authorization, nested wire codec and client adaptation; nested C++ state does not change room v1 |
+| Entity synchronization | Owning captures, bounded revisioned C++ publisher/replica, [canonical nested Protobuf codec](../reference/entity-protocol.md), portable C# replica and room inventory/equipment integration · [synchronization](../concepts/entity-sync.md), [guest room](../guides/examples.md) | General subscriptions, dynamic authorization, AOI/interest management and in-place reset; each stream currently has a fixed projection |
| Startup and shutdown | Required-provider graph and ordered startup, managed-task cancellation, consumer-first per-module cleanup/object destruction, all DSOs last and opt-in reverse startup cleanup for up to 16 owners · [lifecycle status](lifecycle-closeout.md), [launcher](../guides/applications.md), [tasks](../guides/tasks.md) | Applications/operators complete announcements, ingress closure, player removal and saving before process stop; [guarded resource slices](startup-failure-cleanup.md#remaining-io-boundaries) do not guarantee generic live-IO retirement, business rollback or undeclared external callback safety |
| Archived plugins | Consul, Etcd Client, Kafka and OSS SDK: retained source, public headers, targets, explicit opt-in and compatibility checks; runtime plugins default off · [archived integrations](../reference/directory-structure.md#archived-integrations) | Feature development paused; resume only when explicitly re-enabled by the user |
| Optional physics | Explicit unsupported PhysX 3 configuration · [plugin boundaries](../reference/directory-structure.md) | No validated PhysX 3 integration |
-Database real-service acceptance records identify the tested macOS revisions and operations. The guest-room demo has bounded native checks and local Godot testing. Neither establishes production capacity or acceptance in every deployment.
+Database real-service acceptance records identify the tested macOS revisions and operations. Native room checks and portable C# checks cover the included integration; actual Godot execution and deployment acceptance require their own matching revisions and results. These checks do not establish production capacity or acceptance in every environment.
-## Priority design
+## Current priorities
-Lifecycle work focuses on resource-safe cleanup, startup-failure cleanup and synchronized documentation. General Entity subscriptions and nested wire/client adaptation remain deferred for later discussion. [Synchronization design boundaries](../concepts/entity-sync.md) cover authorization, nested identity/order, events and recovery. The trusted-binding nested C++ baseline/delta/replica is available; room v1 retains its scalar wire contract. Performance claims require measured payload and owner-thread cost.
+Current work is local. The [Entity replication contract](entity-replication-design.md) covers owning capture, trusted state, nested wire conversion and the portable client; the room uses one current protocol with coordinated server/client upgrades. General subscription/authorization orchestration remains TODO. Performance conclusions require measured payloads and owner-thread cost.
+
+| Priority / gate | Current decision |
+| --- | --- |
+| Environment | Only the local machine is in scope; real deployment is deferred |
+| Release | Publication scope and acceptance criteria remain undecided |
+| Larger-load and sustained acceptance | First resolve the local 5000-connection problem and pass its acceptance; do not advance larger-scale or endurance work before that prerequisite |
+| Investigation | The single tracking entry is [arknet issue #61](https://github.com/OpenArkStudio/arknet/issues/61); the investigation remains paused |
## Deferred directions
diff --git a/docs/project/roadmap_CN.md b/docs/project/roadmap_CN.md
index cb105727f..ef7300d06 100644
--- a/docs/project/roadmap_CN.md
+++ b/docs/project/roadmap_CN.md
@@ -9,16 +9,23 @@
| 项目布局 | 插件源码共置、显式 `` 公开头文件映射 · [布局与接口](../reference/directory-structure_CN.md) | C++20 实现与保留的兼容边界见[代码约定](code-style_CN.md#c20-实现约定);可选集成仍遵守各自支持限制 |
| 数据库 | 所有者线程 Asio 协程接口、拥有型值、MySQL 预处理语句/事务、Redis 命令/流水线 · [MySQL](../reference/mysql_CN.md)、[Redis](../reference/redis_CN.md) | 目标环境的恢复、持久化、取消/故障与 TLS 验收 · [已测操作与未验收范围](../guides/database-validation_CN.md#验收边界) |
| Entity 配置 | 原生 CSV/XLSX/JSON 导出、C++/C# 字段常量、启动时加载 JSON · [工具](../guides/tools_CN.md)、[Kernel 加载](../reference/kernel_CN.md#json-实体定义与业务配置) | 不提供运行中重载、数据库迁移或客户端/服务端分包过滤 |
-| Entity 同步 | 拥有型标量/嵌套捕获、有界且带修订号的 C++ 发布端/接收端、房间标量 v1 Protobuf/Godot 接入 · [同步](../concepts/entity-sync_CN.md)、[访客房间](../guides/examples_CN.md) | 通用订阅/授权、嵌套线协议编解码及客户端适配;嵌套 C++ 状态不改变房间 v1 |
+| Entity 同步 | 拥有型捕获、有界且带修订号的 C++ 发布端/接收端、[规范嵌套 Protobuf 编解码](../reference/entity-protocol_CN.md)、可移植 C# 接收端及房间背包/装备接入 · [同步](../concepts/entity-sync_CN.md)、[访客房间](../guides/examples_CN.md) | 通用订阅、动态授权、AOI/兴趣管理及原地重置;当前每条流保持固定投影 |
| 启动与停机 | 必需提供方图与有序启动、受管任务取消、消费者优先的逐模块清理与对象析构、全部 DSO 最后关闭,以及最多 16 个 owner 的可选逆序启动清理 · [生命周期状态](lifecycle-closeout_CN.md)、[启动器](../guides/applications_CN.md)、[任务](../guides/tasks_CN.md) | 应用/运维在停止进程前完成公告、关入口、踢人和保存;[受保护的资源切片](startup-failure-cleanup_CN.md#其他-io-边界)不提供通用活跃 IO 退役、业务回滚或未声明外部回调生命周期的保证 |
| 归档插件 | Consul、Etcd Client、Kafka、OSS SDK:保留源码、公开头、目标、显式启用和兼容性检查;运行时插件默认关闭 · [归档集成](../reference/directory-structure_CN.md#归档集成) | 暂停功能开发,未经用户明确重新启用不继续改造 |
| 可选物理集成 | 明确不支持 PhysX 3 配置 · [插件边界](../reference/directory-structure_CN.md) | 暂无已验证的 PhysX 3 集成 |
-数据库真实服务验收记录保留各自测试的源码版本、macOS 环境与操作。访客房间演示经过有界原生检查和本机 Godot 测试。这些证据不构成生产容量或所有部署环境的验收。
+数据库真实服务验收记录保留各自测试的源码版本、macOS 环境与操作。原生房间及可移植 C# 检查覆盖内置集成;实际 Godot 执行和部署验收须保留各自匹配版本及结果。这些检查不构成生产容量或所有环境的验收。
-## 优先设计
+## 当前优先级
-当前生命周期工作聚焦资源安全清理、启动失败清理与文档同步。通用 Entity 订阅和嵌套线协议/客户端适配暂缓,后续另行讨论。[同步设计边界](../concepts/entity-sync_CN.md)涵盖授权、嵌套身份与顺序、事件和恢复。可信绑定的嵌套 C++ 基线/增量/接收端已提供,房间 v1 保留标量线协议。性能结论需要负载与所有者线程开销的测量证据。
+当前工作仅面向本机。[Entity 复制契约](entity-replication-design_CN.md)覆盖拥有型捕获、可信状态、嵌套线协议转换及可移植客户端;房间只保留一套当前协议,要求协调升级服务端和客户端。通用订阅/授权编排仍为 TODO。性能结论需要负载与所属线程开销的测量证据。
+
+| 优先级/前置条件 | 当前决定 |
+| --- | --- |
+| 环境 | 只有本机在范围内,真实部署暂缓 |
+| 发布 | 发布范围与验收标准待定 |
+| 大规模及持续验收 | 先解决本机 5000 连接问题并通过其验收;此前不推进更大规模或持续稳定性工作 |
+| 调查 | 唯一跟踪入口为 [arknet issue #61](https://github.com/OpenArkStudio/arknet/issues/61),调查仍暂停 |
## 暂缓方向
diff --git a/docs/reference/entity-protocol.md b/docs/reference/entity-protocol.md
new file mode 100644
index 000000000..42773c1f0
--- /dev/null
+++ b/docs/reference/entity-protocol.md
@@ -0,0 +1,165 @@
+# Entity replication protocol
+
+`` converts owning [nested publications](kernel.md#nested-baselines-and-deltas)
+to and from the canonical [`entity_sync.proto`](../../src/proto/proto/entity_sync.proto).
+Enable `ARK_BUILD_PROTO=ON` and link `ark::kernel` and `ark::proto`.
+Generated C++ messages use `ark::protobuf::entity_sync`; generated C# messages use
+`Ark.Client.Protocol.EntitySync`.
+
+
+
+## Binding before state
+
+The application supplies a fixed `nested_stream_binding` before any remote
+baseline: Schema ID, root GUID/incarnation, stream generation and an authorized
+`nested_entity_layout`. Layouts include the field types of empty tables and the
+individual child GUIDs allowed at each container. State messages never define or
+expand this binding. Session authentication and projection authorization remain
+application responsibilities.
+
+```cpp
+#include
+
+using namespace ark::entity_sync;
+
+// trusted and initial come from the application's authorized capture path.
+nested_wire::limits budget;
+nested_publisher sender(trusted, initial, budget.stream);
+nested_replica receiver(trusted, budget.stream);
+
+std::string body = nested_wire::encode(*sender.baseline(), trusted, budget);
+auto baseline = nested_wire::decode_baseline(body, trusted, budget);
+auto result = receiver.apply(baseline);
+```
+
+| API in `ark::entity_sync::nested_wire` | Result |
+| --- | --- |
+| `encode(native, proto_output, trusted, limits)` | Baseline/delta overloads validate and stage a generated message, then swap it into the output |
+| `decode(proto_input, trusted, limits)` | Baseline/delta overloads return a new owning native publication |
+| `encode(native, trusted, limits)` | Returns the Protobuf body as `std::string`; adds no transport header |
+| `decode_baseline(bytes, trusted, limits)` / `decode_delta(bytes, trusted, limits)` | Parse a bounded `std::string_view` body and validate the resulting publication |
+
+The `limits` argument is optional. The caller chooses baseline versus delta from
+its transport contract. The codec does not authenticate a session, manage a sender
+queue or apply a publication. A returned publication acknowledges no delivery.
+
+## Values and structure
+
+`scalar_field.value` is an explicit oneof. Its selected alternative carries zero
+and false values without losing presence. C++ variant indices and
+`nested_scalar_type` ordinals are not a wire ABI.
+
+| Native value | Protobuf alternative | Constraint |
+| --- | --- | --- |
+| `bool` | `bool_value` | Explicit false is a present value |
+| `int32_t` / `uint32_t` | `int32_value` (`sint32`) / `uint32_value` | Preserve signedness and width |
+| `int64_t` / `uint64_t` | `int64_value` (`sint64`) / `uint64_value` | Preserve the full native range |
+| `float` / `double` | `float_value` / `double_value` | Finite values only |
+| `std::string` | `string_value` (`bytes`) | Raw bytes, including NUL and non-UTF-8; presentation validates its own text rules |
+| `guid_value` | `guid_value` (`sint64`) | Signed 64-bit wire range and native `guid_t` range must both fit |
+
+Entity identities also use `sint64`; they must be nonzero. Scalar GUID values may
+be zero. Default native GUIDs are signed 64-bit; 32-bit builds reject narrowing
+on decode, and wider builds reject values that cannot fit the wire on encode.
+
+| Publication | Contents |
+| --- | --- |
+| `nested_baseline` | Generation, nonzero revision and complete `nested_snapshot` |
+| `nested_snapshot` | Schema ID and root Entity snapshot |
+| Entity snapshot | GUID/incarnation, class name, complete selected fields/tables/containers; selected empty tables and containers remain present |
+| Table snapshot | Member index, row class and rows keyed by positive row number, each with incarnation and complete selected columns |
+| `nested_delta` | Schema ID, generation, required revision, next revision, root patch and optional root-removal flag |
+| Entity patch | GUID/incarnation and changed scalar, table-row and container records |
+
+Indexed collections use repeated records. Decoders reject duplicate field/table/
+container indices, row numbers and slots before installing any state. Indices are
+class-local; table columns resolve in the row class. Complete roots reject repeated
+Entity GUIDs, shared children and cycles. Unknown Protobuf fields may be ignored
+within the same byte/recursion budgets; they grant no authorization.
+
+## Row and container changes
+
+| `row_delta` incarnations | Meaning |
+| --- | --- |
+| `required_incarnation == 0`, `incarnation > 0` | Add an absent row with every selected column |
+| `required_incarnation > 0`, `incarnation == 0` | Remove the matching row; fields must be empty |
+| Equal nonzero incarnations | Patch a nonempty subset of selected columns |
+| New incarnation greater than the required nonzero incarnation | Replace the matching row with every selected column |
+
+Row identity includes its owner lifetime, table index, row number and incarnation.
+Deleting and reusing a row number, even with identical values between flushes,
+requires a new incarnation. The replica checks required identities and remembers
+each table's incarnation watermark; retired row lifetimes cannot be reintroduced.
+
+`container_delta.update` selects exactly one representation:
+
+| Alternative | Meaning |
+| --- | --- |
+| `replacement` | Complete final authorized slot membership with owning child snapshots; an empty list clears the container |
+| `patch` | Nonempty changed-slot list with recursive patches; each slot's existing child GUID/incarnation must match |
+
+The publisher chooses replacement whenever a slot, child GUID or incarnation
+changes. Otherwise it recursively patches child values. Slot moves/swaps and
+changes across multiple containers commit together within the root. Detaching a
+child removes a relationship; it does not declare the source Entity destroyed.
+
+## Commit and recovery
+
+The codec validates binding, envelope, types, structure and publication budgets.
+It cannot decide whether a delta fits a receiver's current revision, reconstructed
+state or lifetime history; pass the result to `nested_replica::apply()`.
+
+| Receiver condition | Result |
+| --- | --- |
+| First complete matching baseline | Install once |
+| New delta with `required_revision == current_revision` and `revision == required_revision + 1` | Validate a private complete candidate and its history, then commit the entire root once |
+| Structurally valid delta at or below the current revision | `duplicate`; state never moves backwards |
+| Older baseline / equal-revision identical active baseline | `duplicate`; a newer baseline is not an in-place reset |
+| Gap, mismatched lifetime/binding or illegal state transition | `rejected`; hide state and invalidate that replica permanently |
+| Ordered root tombstone | Remove state while retaining valid duplicate handling; later traffic cannot revive the root |
+
+The application closes an affected stream on malformed input, overflow, encoding
+failure or admission/send rejection. Recovery uses a new connection, independently
+trusted binding and complete baseline. Fixed projections do not support an in-place
+generation reset or permission expansion. Atomicity covers one selected root;
+there is no cross-root transaction, ACK or replay log.
+
+## Independent limits and failures
+
+| Budget | Default / scope |
+| --- | --- |
+| `limits.stream.snapshot` | Entity depth 8 with root depth 0; 1024 Entities, 65536 rows, 65536 selected fields/table/container members, 1 MiB of Schema IDs, class names and byte-string values; binding and complete state checked separately |
+| Patch work | 65536 Entity/table/container/row operations, 65536 scalar values and 1 MiB of Schema IDs, class names and byte-string values, including replacement subtrees |
+| `max_lifetime_entries` | 132096 remembered Entity/table/last-visible-row records; enforced at baseline validation and by the stateful publisher/replica |
+| `max_message_bytes` | 4 MiB of encoded body, independently of native state and transport pending queues |
+| `max_recursion_depth` | 128 Protobuf message levels, including record wrappers and unknown groups; distinct from Entity depth |
+
+Byte parsing uses `CodedInputStream` with the configured total-byte and recursion
+limits. Generated-message overloads also check nesting and body size. Native
+state/patch validation does not measure exact heap use: parsed messages, old state,
+candidate state and copied history can coexist. Applications separately bound
+pending frames/bytes/age, retained publications, simultaneous work and peak memory.
+When encoding into an arena-owned output, staged allocations remain subject to
+that arena's lifetime, including allocations from a failed attempt.
+
+| Failure | C++ behavior |
+| --- | --- |
+| Missing alternatives, duplicate indices, wrong binding/type, malformed bytes | Throw `std::invalid_argument` |
+| State/patch/body limit or generated-message depth exceeded | Throw `std::length_error`; raw parser recursion failure reports `std::invalid_argument` |
+| Allocation failure | Propagate `std::bad_alloc`; no partial generated output or native return is published |
+| Stateful `apply()` rejects a decoded publication | Return `rejected` and discard active receiver state; previously retained immutable handles remain readable |
+
+Encoding stages on the destination's Protobuf arena and swaps only after complete
+validation. Codec exceptions do not call or invalidate a replica; the application
+handles stream closure and state disposal. A replica allocation failure separately
+preserves its prior committed state/revision; this local guarantee does not create
+a transport retry policy.
+
+`check-nested-wire` covers round trips, all scalar alternatives, binary values,
+duplicate records, malformed structures, independent budgets, arena outputs and
+stateful rejection with both default and 32-bit native GUIDs. These are local C++
+correctness checks; client and network integration require their own acceptance.
diff --git a/docs/reference/entity-protocol_CN.md b/docs/reference/entity-protocol_CN.md
new file mode 100644
index 000000000..0ab102105
--- /dev/null
+++ b/docs/reference/entity-protocol_CN.md
@@ -0,0 +1,145 @@
+# Entity 复制协议
+
+`` 将拥有型[嵌套发布](kernel_CN.md#嵌套基线与增量)
+与规范 [`entity_sync.proto`](../../src/proto/proto/entity_sync.proto) 相互转换。
+启用 `ARK_BUILD_PROTO=ON`,并链接 `ark::kernel` 和 `ark::proto`。
+生成的 C++ 消息使用 `ark::protobuf::entity_sync`,C# 消息使用
+`Ark.Client.Protocol.EntitySync`。
+
+
+
+## 先绑定,再接收状态
+
+应用在接收任何远端基线前提供固定的 `nested_stream_binding`:Schema ID、根 GUID/
+实例标记、流代次及已授权的 `nested_entity_layout`。布局包括空表的字段类型和每个
+容器逐个允许的子 GUID。状态消息不能定义或扩大该绑定。应用负责会话认证和投影授权。
+
+```cpp
+#include
+
+using namespace ark::entity_sync;
+
+// trusted 和 initial 来自应用已授权的捕获路径。
+nested_wire::limits budget;
+nested_publisher sender(trusted, initial, budget.stream);
+nested_replica receiver(trusted, budget.stream);
+
+std::string body = nested_wire::encode(*sender.baseline(), trusted, budget);
+auto baseline = nested_wire::decode_baseline(body, trusted, budget);
+auto result = receiver.apply(baseline);
+```
+
+| `ark::entity_sync::nested_wire` 中的 API | 结果 |
+| --- | --- |
+| `encode(native, proto_output, trusted, limits)` | 基线/增量重载校验并暂存生成的消息,完成后交换到输出 |
+| `decode(proto_input, trusted, limits)` | 基线/增量重载返回新的拥有型原生发布 |
+| `encode(native, trusted, limits)` | 返回 `std::string` Protobuf 消息体,不添加传输头 |
+| `decode_baseline(bytes, trusted, limits)` / `decode_delta(bytes, trusted, limits)` | 解析有界的 `std::string_view` 消息体并校验发布 |
+
+`limits` 参数可省略。调用方按传输契约区分基线与增量。编解码器不认证会话、不管理
+发送队列,也不应用发布。返回发布不代表已经交付。
+
+## 值与结构
+
+`scalar_field.value` 使用显式 oneof;已选择的类型分支保留零值和 false 的存在性。
+C++ variant 索引与 `nested_scalar_type` 序号不是线协议 ABI。
+
+| 原生值 | Protobuf 分支 | 约束 |
+| --- | --- | --- |
+| `bool` | `bool_value` | 显式 false 也是已存在的值 |
+| `int32_t` / `uint32_t` | `int32_value`(`sint32`)/ `uint32_value` | 保留符号与宽度 |
+| `int64_t` / `uint64_t` | `int64_value`(`sint64`)/ `uint64_value` | 保留完整原生范围 |
+| `float` / `double` | `float_value` / `double_value` | 仅允许有限值 |
+| `std::string` | `string_value`(`bytes`)| 原始字节,包括 NUL 和非 UTF-8;展示层自行校验文本规则 |
+| `guid_value` | `guid_value`(`sint64`)| 同时满足有符号 64 位线协议范围及原生 `guid_t` 范围 |
+
+Entity 身份也使用 `sint64`,且不得为零;标量 GUID 值可以为零。默认原生 GUID 为
+有符号 64 位;32 位构建在解码时拒绝窄化,较宽构建在编码时拒绝线协议无法表示的值。
+
+| 发布 | 内容 |
+| --- | --- |
+| `nested_baseline` | 流代次、非零修订号及完整 `nested_snapshot` |
+| `nested_snapshot` | Schema ID 和根 Entity 快照 |
+| Entity 快照 | GUID/实例标记、类名、完整的选定字段/表格/容器;保留已选择的空表和空容器 |
+| 表格快照 | 成员索引、行类及正行号索引的行,每行包含实例标记和全部选定列 |
+| `nested_delta` | Schema ID、流代次、基准修订、下一修订、根补丁及可选整根删除标志 |
+| Entity 补丁 | GUID/实例标记及变化的标量、表格行和容器记录 |
+
+索引集合使用 repeated 记录。解码器在安装任何状态前拒绝重复字段/表格/容器索引、
+行号及槽位。索引在类内解析,表格列属于行类。完整根拒绝重复 Entity GUID、共享子
+引用和循环。未知 Protobuf 字段可以在同一字节/递归上限内被忽略,不授予任何权限。
+
+## 行与容器变化
+
+| `row_delta` 实例标记 | 含义 |
+| --- | --- |
+| `required_incarnation == 0`,`incarnation > 0` | 完整提供所有选定列,新增原先不存在的行 |
+| `required_incarnation > 0`,`incarnation == 0` | 移除匹配的行,fields 必须为空 |
+| 两者非零且相等 | 修改非空的选定列子集 |
+| 新标记大于非零基准标记 | 完整提供所有选定列,替换匹配的行 |
+
+行身份包括所属 Entity 存续期、表格索引、行号及实例标记。删除后复用行号,即使两次
+刷新之间最终值相同,也必须分配新标记。接收端核对基准身份并记录各表的实例标记水位,
+已退役行存续期不能重新引入。
+
+`container_delta.update` 恰好选择一种表示:
+
+| 分支 | 含义 |
+| --- | --- |
+| `replacement` | 最终授权槽位的完整集合,附带拥有型子快照;空列表清空容器 |
+| `patch` | 非空的变化槽位列表,附带递归补丁;槽位原有子 GUID/实例标记必须匹配 |
+
+槽位、子 GUID 或实例标记任一变化,发布端选择完整替换;否则递归修改子值。
+槽位移动/交换及多个容器的变化在同一根中一起提交。解除子关系不等于声明源 Entity 已销毁。
+
+## 提交与恢复
+
+编解码器校验绑定、消息封装、类型、结构及发布上限,无法判定增量是否匹配接收端的
+当前修订、重建状态和身份历史;须将结果传给 `nested_replica::apply()`。
+
+| 接收端条件 | 结果 |
+| --- | --- |
+| 首个完整且匹配的基线 | 安装一次 |
+| 新增量满足 `required_revision == current_revision` 且 `revision == required_revision + 1` | 验证私有完整候选和身份历史,再一次提交整根 |
+| 结构合法且修订不高于当前修订的增量 | `duplicate`,状态不回退 |
+| 旧基线/与当前有效状态相同的同修订基线 | `duplicate`;更新的基线不能用于原地重置 |
+| 缺口、存续期/绑定不符或非法状态转换 | `rejected`,隐藏状态并永久使该接收端失效 |
+| 有序整根删除标记 | 移除状态,保留有效的重复处理能力;后续流量不能复活根 |
+
+应用在非法输入、溢出、编码失败或入队/发送拒绝时关闭受影响流。恢复需要新连接、
+独立可信绑定及完整基线。固定投影不支持原地流代次重置或扩权。原子性仅覆盖一棵
+选定根,不提供跨根事务、ACK 或重放日志。
+
+## 独立上限与失败处理
+
+| 预算 | 默认值/范围 |
+| --- | --- |
+| `limits.stream.snapshot` | 根深度为 0,Entity 深度 8;1024 个 Entity、65536 行、65536 个选定字段/表格/容器成员,1 MiB 的 Schema ID/类名/字节串值;绑定与完整状态分别核算 |
+| 补丁工作量 | 65536 个 Entity/表格/容器/行操作、65536 个标量值及 1 MiB 的 Schema ID/类名/字节串值,包括替代子树 |
+| `max_lifetime_entries` | 132096 条已记录的 Entity/表格/最近可见行身份;基线校验及有状态发布端/接收端均执行上限 |
+| `max_message_bytes` | 4 MiB 编码后消息体,与原生状态和传输待发队列分别限制 |
+| `max_recursion_depth` | 128 层 Protobuf 消息,含记录包装层和未知 group,区别于 Entity 深度 |
+
+字节解析使用 `CodedInputStream` 设置总字节数和递归上限。生成消息重载也核对层数
+和消息体大小。原生状态/补丁校验不测量精确堆用量;已解析消息、旧状态、候选状态和
+历史副本可能同时存在。应用须分别限制待发帧数/字节数/年龄、保留发布、并行工作量
+和内存峰值。向 arena 所有的输出编码时,暂存分配受该 arena 的生命周期约束,
+失败尝试的分配也包括在内。
+
+| 失败 | C++ 行为 |
+| --- | --- |
+| 缺失类型分支、重复索引、绑定/类型错误或非法字节 | 抛出 `std::invalid_argument` |
+| 状态/补丁/消息体超限或生成消息层数超限 | 抛出 `std::length_error`;原始字节解析的递归失败报告 `std::invalid_argument` |
+| 分配失败 | 向上传播 `std::bad_alloc`,不发布部分生成输出或原生返回值 |
+| 有状态 `apply()` 拒绝已解码发布 | 返回 `rejected` 并丢弃当前接收状态;此前保留的不可变句柄仍可读取 |
+
+编码在目标的 Protobuf arena 上暂存,完整校验后才交换。编解码异常不调用或使接收端
+失效,应用负责关闭流和释放状态。接收端自身的分配失败另有保持旧提交状态/修订的
+保证;这个本地保证不创建传输重试策略。
+
+`check-nested-wire` 在默认及 32 位原生 GUID 构建下覆盖往返、全部标量类型、二进制值、
+重复记录、非法结构、独立预算、arena 输出及有状态拒绝。这些属于本地 C++ 正确性
+检查,客户端与网络集成仍需各自验收。
diff --git a/docs/reference/kernel.md b/docs/reference/kernel.md
index 8f16089d9..53405234a 100644
--- a/docs/reference/kernel.md
+++ b/docs/reference/kernel.md
@@ -361,7 +361,7 @@ Entity incarnations are allocated monotonically within one loaded `entity_impl`
**Breaking ABI change:** the new virtual Kernel method and Entity/row/table implementation layout changes require rebuilding all plugins, hosts and consumers together. Do not load old binaries with these headers. Source subclasses may keep the default method, which throws `std::logic_error`. The independent `check-kernel-plugin` host exercises the actual loaded virtual method and retains nested values after child destruction, table clearing and normal stop.
-Capture provides the owning source values used by the [nested publisher/replica](#nested-baselines-and-deltas). Existing scalar APIs and room v1 messages are unchanged; no nested sender queue or codec is provided. Applications bind authorization and retain fixed projections for their stream. The [replication design](../project/entity-replication-design.md) records the selected consistency/recovery boundaries.
+Capture provides the owning source values used by the [nested publisher/replica](#nested-baselines-and-deltas) and the [Entity protocol codec](entity-protocol.md). Applications bind authorization and retain fixed projections for their stream; sender queues remain application-owned. The [replication design](../project/entity-replication-design.md) records the selected consistency/recovery boundaries.
## Maps and data conversion
@@ -566,9 +566,11 @@ peak and bound its own retained handles/queues. Use each publisher/replica on on
owner thread. Release plugin-created shared handles before unloading their code;
these handles do not certify arbitrary DSO unloading.
-The header requires no Protobuf or transport. There is no nested codec, sender
-queue, ACK/replay log, in-place reset, dynamic permission change, cross-root commit
-or room v1 change. A returned publication acknowledges no delivery; an adapter
+The stream header requires no Protobuf or transport. The separate
+[Entity protocol codec](entity-protocol.md) maps publications to canonical Protobuf
+messages against the same trusted binding. There is no sender queue, ACK/replay
+log, in-place reset, dynamic permission change or cross-root commit. A returned
+publication acknowledges no delivery; an adapter
must separately bind sessions, bound encoded/pending work and close the affected
stream on admission/send failure. `check-entity-stream` covers typed binding,
recursive patches, row/Entity reuse, duplicates, gaps, atomic invalidation and
diff --git a/docs/reference/kernel_CN.md b/docs/reference/kernel_CN.md
index b0c9a9738..4c12f2884 100644
--- a/docs/reference/kernel_CN.md
+++ b/docs/reference/kernel_CN.md
@@ -355,7 +355,7 @@ Entity 实例标记在一个已加载的 `entity_impl` 实现副本内单调分
**ABI 不兼容变更:** 新 Kernel 虚方法及 Entity/row/table 实现布局变化要求同时重建全部插件、宿主和消费者,不能将旧二进制与新头文件混用。源代码子类可保留默认方法,该方法抛出 `std::logic_error`。独立的 `check-kernel-plugin` 宿主通过真实已加载虚方法捕获,并在子 Entity 销毁、表格清空和正常停止后读取保留值。
-捕获提供[嵌套发布端/接收端](#嵌套基线与增量)使用的拥有型源值。现有标量 API 和房间 v1 消息保持不变,不提供嵌套发送队列或编解码。应用负责绑定授权,并在流内保留固定投影。[复制设计](../project/entity-replication-design_CN.md)记录已选定的一致性/恢复边界。
+捕获提供[嵌套发布端/接收端](#嵌套基线与增量)及 [Entity 协议编解码](entity-protocol_CN.md)使用的拥有型源值。应用负责绑定授权,并在流内保留固定投影;发送队列仍由应用管理。[复制设计](../project/entity-replication-design_CN.md)记录已选定的一致性/恢复边界。
## 地图与数据转换
@@ -536,8 +536,9 @@ if (auto delta = sender.update(next_authorized_capture))
旧已提交状态、候选、数据包和历史副本;应用须计入组合峰值,并限制自行保留的句柄/队列。
每个发布端/接收端只在一个所属线程使用。卸载代码前须释放插件创建的共享句柄;句柄不证明任意 DSO 卸载安全。
-该头文件不依赖 Protobuf 或传输。不提供嵌套编解码、发送队列、ACK/重放日志、原地重置、
-动态权限变更或跨根提交,不改变房间 v1。返回发布不代表交付;适配器须单独绑定会话、
+该流头文件不依赖 Protobuf 或传输。独立的 [Entity 协议编解码](entity-protocol_CN.md)
+按同一可信绑定将发布映射为规范 Protobuf 消息。不提供发送队列、ACK/重放日志、原地重置、
+动态权限变更或跨根提交。返回发布不代表交付;适配器须单独绑定会话、
限制编码后/待发工作,并在接收或发送失败时关闭受影响流。
`check-entity-stream` 覆盖类型绑定、递归补丁、行/Entity 复用、重复、缺口、原子失效及上限。
其依赖 `check-nested-stream-allocation` 对路径中的每个分配点注入失败。
diff --git a/docs/site/src/navigation.ts b/docs/site/src/navigation.ts
index 36b98f437..88a95fe2b 100644
--- a/docs/site/src/navigation.ts
+++ b/docs/site/src/navigation.ts
@@ -156,6 +156,11 @@ const tree: Entry[] = [
"Kernel plugin",
"Kernel 插件"
],
+ [
+ "reference/entity-protocol",
+ "Entity protocol",
+ "Entity 协议"
+ ],
[
"reference/network",
"Network plugin",
diff --git a/examples/guest_room/CMakeLists.txt b/examples/guest_room/CMakeLists.txt
index 6955f0653..694a2eb63 100644
--- a/examples/guest_room/CMakeLists.txt
+++ b/examples/guest_room/CMakeLists.txt
@@ -1,6 +1,40 @@
find_package(nlohmann_json CONFIG REQUIRED)
BUILD_PLUGIN_MACRO("guest_room_plugin")
set_property(TARGET guest_room_plugin APPEND PROPERTY LINK_LIBRARIES ark::proto ark::kernel ark::net)
+if(UNIX AND NOT APPLE AND TARGET protobuf::libprotobuf)
+ get_target_property(ark_room_proto_type protobuf::libprotobuf TYPE)
+ set(ark_room_private_proto OFF)
+ if(ark_room_proto_type STREQUAL "STATIC_LIBRARY")
+ set(ark_room_private_proto ON)
+ elseif(ark_room_proto_type STREQUAL "UNKNOWN_LIBRARY")
+ # FindProtobuf's module fallback may not record the imported library type.
+ string(TOUPPER "${CMAKE_BUILD_TYPE}" ark_room_configuration)
+ get_target_property(ark_room_proto_configurations protobuf::libprotobuf IMPORTED_CONFIGURATIONS)
+ set(ark_room_proto_locations IMPORTED_LOCATION_${ark_room_configuration} IMPORTED_LOCATION)
+ foreach(configuration IN LISTS ark_room_proto_configurations)
+ string(TOUPPER "${configuration}" configuration)
+ list(APPEND ark_room_proto_locations IMPORTED_LOCATION_${configuration})
+ endforeach()
+ foreach(property IN LISTS ark_room_proto_locations)
+ get_target_property(ark_room_proto_location protobuf::libprotobuf ${property})
+ if(ark_room_proto_location)
+ if(ark_room_proto_location MATCHES "\\.a$")
+ set(ark_room_private_proto ON)
+ endif()
+ break()
+ endif()
+ endforeach()
+ endif()
+ if(ark_room_private_proto)
+ # ELF interposition must not share generated descriptor initialization
+ # flags with a host that owns another private static generated_pool.
+ # Isolate archive symbols and inline codec/reflection entry points, while
+ # retaining public non-inline module/plugin entry points and RTTI.
+ # A shared Protobuf runtime already owns one common pool; leave it alone.
+ target_link_options(guest_room_plugin PRIVATE "LINKER:--exclude-libs,ALL")
+ set_property(TARGET guest_room_plugin PROPERTY VISIBILITY_INLINES_HIDDEN YES)
+ endif()
+endif()
add_executable(ark_guest_room_codec_tests ../../tests/core/main.cpp ../../tests/guest_room/codec.cpp)
set_property(TARGET ark_guest_room_codec_tests PROPERTY LINK_LIBRARIES ark::net ark::kernel ark::proto nlohmann_json::nlohmann_json)
diff --git a/examples/guest_room/README.md b/examples/guest_room/README.md
index d2e3b8e84..21632e4f1 100644
--- a/examples/guest_room/README.md
+++ b/examples/guest_room/README.md
@@ -1,59 +1,54 @@
-# Guest room example
+# Guest room
[中文](README_CN.md)
-`guest_room_plugin` runs a small 2D guest lobby through the real ARK launcher, Utility, Kernel, Bus and Net plugins. A joined session owns a `GuestPlayer` Kernel Entity whose formal fields and initial profile come from JSON. Movement and HP/MP actions update that Entity; the owner-thread flush captures selected fields and publishes Protobuf baselines/deltas. Net callbacks and all business mutations execute on the launcher owner thread. Login and the default CS/SS framing and 4096-byte limit are unchanged. No Registry, Consul, database, account or persistence service is required.
+A 2D multiplayer example running through the real ark launcher, Utility, Kernel,
+Bus and Net plugins. Two [Godot C# clients](https://github.com/OpenArkStudio/ark-client-godot)
+observe each other's movement, HP/MP, inventory and equipment through owning
+Protobuf baselines and deltas. It needs no database or service-discovery server.
-The matching C# Godot client uses ARK2 and displays HP/MP. `damage` reduces own HP by 15, `skill` spends 20 MP when available, and `rest` restores 20 HP/MP up to the configured maxima. HP zero still permits movement. The server owns amounts and targets; the client requests only a kind and an independent action sequence. See the [protocol and migration](protocol.md).
+| Interaction | Entity data |
+| --- | --- |
+| Move, take a hit, use a skill, rest | Typed scalar fields |
+| Pick up or consume potions | Inventory table rows, quantities and row lifetimes |
+| Switch or upgrade weapons | Equipment container membership and child fields |
+| Leave and reconnect | Root tombstone, new identity and complete baseline |
-## Entity schema and initial profile
-
-[resource/meta/entity_schema.json](resource/meta/entity_schema.json) is the authoritative Kernel schema and static configuration. It declares the required empty `Player` class, `GuestPlayer` fields at stable indices 100..108 (`name`, `x`, `y`, `move_seq`, `hp`, `mp`, `hp_max`, `mp_max`, `action_seq`), and profile identifier `id` at index 99. Name is a string, coordinates are doubles, and sequences/attributes are uint32. This replaces the example's placeholder XML metadata and custom fields.
-
-The `GuestPlayer` configuration row with `id = 1` supplies every scalar value, including HP/MP/maxima of 100 and both sequences zero. Joining calls `create_entity(..., "GuestPlayer", 1, ...)` and then assigns the session name and spawn coordinates through ordinary Entity setters. Schema definitions determine field types and indices; business JSON rows provide values. There are no hardcoded replacement attribute defaults or runtime schema edits. The profile ID is separate from the opaque session player ID in snapshots. Schema masks are metadata; the module explicitly selects public indices 100..108 for `capture_scalar_snapshot` and `scalar_publisher`. New connections receive a complete baseline; existing peers receive added baselines, changed fields and tombstones. Movement and actions coalesce before each owner-thread flush.
-
-Edit a profile through the framework's [JSON Entity schema and business-data workflow](../../docs/reference/kernel.md#json-entity-schema-and-business-data), export valid JSON, stage it and restart the example. HP/MP maxima must be positive, current values must not exceed their maxima and initial sequences must be zero. Kernel selects this file when present and rejects malformed selected JSON without falling back to XML. The example checks the required field indices/types and profile in `pre_update()`, after every module's `post_init()` has loaded its static configuration, before opening its listener. Restart/rejoin creates fresh state; there is no hot reload or persistence.
-
-## Process stop and resource cleanup
-
-Join, Move, Action, Ping and connection events use ordinary callbacks. Leaving manager `running` no longer automatically returns `server_stopping`. The application/operator completes announcements, business-ingress closure, player removal and saving before process stop. This example has no persistence service or saving guarantee.
-
-At process stop the manager requests managed-task cancellation and waits for frame/native-resource safety. All consumer-first `pre_shut()` calls close native callback sources while module objects remain alive; reverse `shut()` and owned-object destruction follow, then plugin exits and all DSO closes last. Net closes endpoints before clearing callback captures. Keep the room, Kernel and callback receivers alive through that closure. Existing work/output limits and protocol behavior remain. Shared IO, Registry, hot update, nested wire changes and the TCP 5000 workstream remain paused.
+All business changes and captures run on the launcher owner thread. The client
+submits intents and displays server-confirmed state. JSON defines fields and the
+initial business profile; the reusable nested publisher/replica owns pending values.
+The room declares Kernel, Map and Net providers so reverse dependency cleanup
+retires player state and callbacks before those providers stop.
## Build and run
-Use the repository's normal native dependencies, including Protobuf and the existing optional `nlohmann_json` CMake package. Enable applications, examples and this optional example in the same build:
-
```sh
-cmake -S . -B app-build -DARK_BUILD_APPLICATIONS=ON -DARK_BUILD_EXAMPLES=ON -DARK_BUILD_GUEST_ROOM_EXAMPLE=ON -DARK_BUILD_PROTO=ON
-cmake --build app-build --target app guest_room_plugin ark_kernel_plugin ark_utility_plugin ark_bus_plugin ark_net_plugin --parallel 1
-./examples/guest_room/run.sh --bin-dir "$PWD/app-build/bin" --runtime-dir "$PWD/run/guest_room" --host 127.0.0.1 --port 19100
+cmake -S . -B out/guest-room -DCMAKE_BUILD_TYPE=Release -DARK_BUILD_GUEST_ROOM_EXAMPLE=ON
+cmake --build out/guest-room --target app guest_room_plugin ark_kernel_plugin ark_utility_plugin ark_bus_plugin ark_net_plugin --parallel 2
+bash examples/guest_room/run.sh --bin-dir out/guest-room/bin --runtime-dir /tmp/ark-guest-room
```
-Supply the normal dependency toolchain/prefix arguments when configuring; select the `guest-room` vcpkg manifest feature when using vcpkg. Add `--debug` to the run command for a Debug build (`app_d` and `_d` plugins). The script stages a complete matching binary directory, the TOML manifests and the example's metadata. `--host` is a local bind address/hostname; `--port` must be 1..65535. Use a fresh runtime directory. Stop with Ctrl+C. This example script explicitly sets a five-second resource-cleanup safeguard; expiry retains resources and reports failure. It is not the framework default or a business-saving budget.
+Use the regular [application build prerequisites](../../docs/get-started/installation.md).
+The target also needs the Utility, Kernel, Bus and Net runtime plugins enabled.
+See [run.sh](run.sh) for its launcher arguments and [the Godot guide](https://github.com/OpenArkStudio/ark-client-godot#readme)
+for client setup. Existing Login/CS/SS listeners do not implement this example protocol.
-The equivalent platform-neutral staging command is:
+## Validation
```sh
-cmake -DBIN_DIR=/absolute/app-build/bin -DRUNTIME_DIR=/absolute/run/guest_room -DHOST=127.0.0.1 -DPORT=19100 -P examples/guest_room/stage.cmake
-cd /absolute/run/guest_room
-./bin/app --busid=1.1.31.1 --name=guest_room --app_conf=conf/guest_room.app.toml --shutdown_timeout_ms=5000
+cmake --build out/guest-room --target check-guest-room
```
-Use `app.exe` / `app_d.exe` on Windows. Connect the official [C# Godot client](https://github.com/OpenArkStudio/ark-client-godot) to the same host/port; send movement at about 10 Hz. The client must join again after reconnecting, clear old frame/state buffers and accept a new player ID and actual Entity GUID. Detailed wire fields and errors are in the [protocol](protocol.md); [wire-vectors.json](wire-vectors.json) mirrors the C# test fixture.
-
-## Native verification
-
-```sh
-cmake --build app-build --target check-guest-room --parallel 1
-```
-
-`check-guest-room-codec` uses the real Net parser to verify shared Protobuf body/frame hex, every split point, coalescing, 8192/8193-byte limits, headers, fresh parser state and strict UTF-8 names. Adapter tests cover all nine scalar alternatives, integer boundaries, arbitrary bytes, zero-value presence, duplicate fields, limits, malformed encodings, public projection types, revision gaps, duplicates, tombstones and revision exhaustion. A 16-player worst-size Protobuf baseline must fit the frame budget.
-
-On POSIX, `check-guest-room-server` starts the real launcher/plugin libraries and two native TCP clients. It checks Accepted/Entity binding, late-join full baselines, added baselines for existing peers, coalesced movement deltas, HP/MP actions, sequence-only clamped actions, quiet unchanged flushes, errors without state changes, tombstones, reconnect with new Entity GUIDs, malformed-frame shutdown and normal shutdown. Additional processes load near-UINT32_MAX and 19-MP profiles and reject invalid initial profiles. Tests own runtime directories and port 19191; run one copy at a time. This process controller does not establish Windows or C# Godot acceptance.
-
-The same executable starts a resource-cleanup child with a 20-second observation deadline. Actual Room/Kernel/Net DSOs handle an ordinary Join, then two explicit Net `pre_shut()` calls check endpoint closure, callback-capture release and inert updates on retained closed services before manager stop. Normal protocol tests remain. The controller terminates/reaps only its own child on timeout; this is not successful framework shutdown. The controlled host keeps the room DSO loaded and does not establish image unmapping. These describe test scope; acceptance requires actual execution results.
+The codec target checks shared C++/C# vectors, typed values, malformed inputs,
+framing and the 16-player baseline budget. On POSIX the server target starts the
+actual launcher and two native clients, covering movement, row deletion/reuse,
+equipment replacement/patches, errors, removal, reconnect and normal shutdown.
+It uses owned runtime directories and port 19191; run one copy at a time.
+Actual Godot execution is a separate client-side integration check.
-The Applications workflow enables this optional example on its existing platform matrix. Its default build includes the plugin on Linux, macOS and Windows; `check-core` includes the codec checks everywhere and the real launcher/two-peer process checks on POSIX systems. Workflow configuration is separate from the result of an individual hosted run.
+[Room protocol and bounds](protocol.md) · [Generic Entity protocol](../../docs/reference/entity-protocol.md)
+· [Shutdown ownership](../../docs/project/lifecycle-closeout.md)
-The sample limits bodies to 8192 bytes, connections to 32 and joined players to 16. Each tick permits 64 business callbacks; each connection permits 32 messages per second and eight queued frames / `8 * (8192 + 12)` owned bytes. Work/output overflow or transport send rejection closes the session. Unsupported captured state or revision exhaustion requires reconnection and a fresh baseline. These bounds and native tests do not establish production capacity, deployment or Godot visual acceptance. See the [protocol](protocol.md) for compatibility, projection, revision and recovery rules.
+**Breaking change:** the current generic Entity messages replace the former
+scalar-only room contract. Rebuild the server and Godot client together; there
+is no compatibility branch. Local functional checks do not establish production capacity.
diff --git a/examples/guest_room/README_CN.md b/examples/guest_room/README_CN.md
index 3a6ca1bc4..d2fbf33de 100644
--- a/examples/guest_room/README_CN.md
+++ b/examples/guest_room/README_CN.md
@@ -1,59 +1,48 @@
-# 访客房间示例
+# 访客房间
[English](README.md)
-`guest_room_plugin` 通过真实 ARK 启动器及 Utility、Kernel、Bus、Net 插件运行一个二维访客大厅。每个已加入的会话拥有一个 `GuestPlayer` Kernel Entity,正式字段及初始配置来自 JSON。移动和 HP/MP 操作更新该 Entity,所属线程的 flush 捕获选定字段,并发布 Protobuf 基线/增量。Net 回调及业务状态修改均在启动器所属线程执行。现有 Login、默认 CS/SS 帧格式及其 4096 字节上限不变,无需 Registry、Consul、数据库、账号或持久化服务。
+通过真实 ark 启动器、Utility、Kernel、Bus 与 Net 插件运行的 2D 联机示例。
+两个 [Godot C# 客户端](https://github.com/OpenArkStudio/ark-client-godot)
+通过拥有型 Protobuf 基线与增量,观察彼此的移动、HP/MP、背包和装备。
+不需要数据库或服务发现服务。
-匹配的 C# Godot 客户端使用 ARK2,并显示 HP/MP。`damage` 将自身 HP 减少 15,`skill` 在 MP 足够时消耗 20 MP,`rest` 将 HP/MP 恢复 20,最多恢复到配置上限。HP 为零仍可移动。数值和操作目标由服务端确定,客户端只请求操作类型和独立的操作序列号。详见[协议及迁移说明](protocol_CN.md)。
+| 交互 | Entity 数据 |
+| --- | --- |
+| 移动、受击、技能、恢复 | 带类型的标量字段 |
+| 拾取/消耗药水 | 背包表格行、数量及行存续期 |
+| 换装/强化 | 装备容器成员及子 Entity 字段 |
+| 离开/重连 | 根删除标记、新身份与完整基线 |
-## Entity 定义与初始配置
-
-[resource/meta/entity_schema.json](resource/meta/entity_schema.json) 是 Kernel 的权威字段定义与静态配置。文件声明必需的空 `Player` 类,将 `GuestPlayer` 字段固定在索引 100..108:`name`、`x`、`y`、`move_seq`、`hp`、`mp`、`hp_max`、`mp_max`、`action_seq`,并将配置标识 `id` 放在索引 99。名称是字符串,坐标是 double,序列号和属性是 uint32。这取代了示例中占位的 XML 元数据及自定义字段。
-
-`GuestPlayer` 的 `id = 1` 配置行提供全部标量值,包括初始为 100 的 HP/MP/上限及初始为零的两类序列号。加入时调用 `create_entity(..., "GuestPlayer", 1, ...)`,再通过普通 Entity setter 设置会话名称和出生坐标。字段定义确定类型及索引,业务 JSON 配置行提供数值;代码不硬编码替代属性默认值,也不在运行时修改字段定义。配置 ID 与快照中的不透明会话玩家 ID 分别使用。Schema 中的 mask 是元数据;模块显式选择公开索引 100..108,使用 `capture_scalar_snapshot` 与 `scalar_publisher`。新连接接收完整基线,已有连接接收新增基线、修改字段及墓碑。移动和操作在所属线程每次 flush 前合并。
-
-通过框架的 [JSON 实体定义与业务配置流程](../../docs/reference/kernel_CN.md#json-实体定义与业务配置)修改配置,导出有效 JSON,复制到运行目录后重启示例。HP/MP 上限须为正数,当前值不得超过对应上限,初始序列号须为零。Kernel 在该文件存在时选择它,内容非法时直接失败,不回退到 XML。示例在 `pre_update()` 阶段检查必需字段的索引/类型及配置行,确保各模块已在 `post_init()` 中加载静态配置,再开放监听。重启或重新加入创建全新状态,不支持热重载或持久化。
-
-## 进程停止与资源清理
-
-Join、Move、Action、Ping 和连接事件使用普通业务回调。框架不在管理器离开 `running` 时自动返回 `server_stopping`;公告、关闭业务入口、踢人和保存须由应用或运维在进程停止前完成。示例无持久化服务,不提供保存保证。
-
-停止进程后,管理器请求受管任务取消,并等待帧与原生资源安全。先按反向依赖执行所有 `pre_shut()`,在模块对象仍存活时关闭原生回调源;再反向执行 `shut()` 并销毁拥有的对象,最后注销插件并关闭全部 DSO。Net 关闭端点后清空回调捕获,释放前须保持房间、Kernel 及回调接收者有效。原有工作量/输出限制和协议不变。共享 IO、Registry、热更新、嵌套线格式及 TCP 5000 专项继续暂停。
+业务修改和捕获均在启动器所有者线程执行。客户端提交意图,展示服务端确认的状态。
+JSON 定义字段和初始业务配置,可复用的嵌套发布端/接收端拥有待处理值。
+房间声明 Kernel、Map 与 Net 依赖,逆序清理时先释放玩家状态和回调,再停止这些依赖模块。
## 构建与运行
-准备仓库的常规原生依赖,以及 Protobuf 和现有可选依赖 `nlohmann_json` 的 CMake 包。在同一次构建中启用 applications、examples 和本示例:
-
```sh
-cmake -S . -B app-build -DARK_BUILD_APPLICATIONS=ON -DARK_BUILD_EXAMPLES=ON -DARK_BUILD_GUEST_ROOM_EXAMPLE=ON -DARK_BUILD_PROTO=ON
-cmake --build app-build --target app guest_room_plugin ark_kernel_plugin ark_utility_plugin ark_bus_plugin ark_net_plugin --parallel 1
-./examples/guest_room/run.sh --bin-dir "$PWD/app-build/bin" --runtime-dir "$PWD/run/guest_room" --host 127.0.0.1 --port 19100
+cmake -S . -B out/guest-room -DCMAKE_BUILD_TYPE=Release -DARK_BUILD_GUEST_ROOM_EXAMPLE=ON
+cmake --build out/guest-room --target app guest_room_plugin ark_kernel_plugin ark_utility_plugin ark_bus_plugin ark_net_plugin --parallel 2
+bash examples/guest_room/run.sh --bin-dir out/guest-room/bin --runtime-dir /tmp/ark-guest-room
```
-配置时仍需传入正常的依赖工具链或前缀参数;使用 vcpkg 时选择 `guest-room` 清单特性。Debug 构建的运行命令增加 `--debug`,使用 `app_d` 和 `_d` 插件。脚本复制同次构建的完整产物目录、TOML 清单和示例元数据。`--host` 指定本机绑定地址或主机名,`--port` 范围为 `1..65535`。使用新的运行目录,以 Ctrl+C 停服。此示例脚本显式设置五秒资源清理保护,超时保留资源并报告失败;它不是框架默认值或业务保存预算。
+先满足[应用构建前提](../../docs/get-started/installation_CN.md),并启用 Utility、Kernel、Bus 与 Net
+运行时插件。[run.sh](run.sh)说明启动参数,[Godot 指南](https://github.com/OpenArkStudio/ark-client-godot#readme)
+说明客户端准备方式。已有 Login/CS/SS 监听器不实现此示例协议。
-跨平台配置步骤如下:
+## 验证
```sh
-cmake -DBIN_DIR=/absolute/app-build/bin -DRUNTIME_DIR=/absolute/run/guest_room -DHOST=127.0.0.1 -DPORT=19100 -P examples/guest_room/stage.cmake
-cd /absolute/run/guest_room
-./bin/app --busid=1.1.31.1 --name=guest_room --app_conf=conf/guest_room.app.toml --shutdown_timeout_ms=5000
+cmake --build out/guest-room --target check-guest-room
```
-Windows 使用 `app.exe`/`app_d.exe`。官方 [C# Godot 客户端](https://github.com/OpenArkStudio/ark-client-godot)连接相同主机和端口,移动发送频率约为 10 Hz。断线重连后,客户端须清空旧帧和房间状态,重新加入并接收新的玩家 ID 和实际 Entity GUID。字段和错误约定见[协议](protocol_CN.md);[wire-vectors.json](wire-vectors.json) 与 C# 测试使用相同的线格式向量。
-
-## 原生验证
-
-```sh
-cmake --build app-build --target check-guest-room --parallel 1
-```
-
-`check-guest-room-codec` 使用真实 Net 解析器检查共享 Protobuf 正文/帧十六进制向量、每个拆分位置、粘包、8192/8193 字节边界、头部、全新解析状态及严格 UTF-8 名称。适配器测试覆盖九种标量分支、整数边界、任意 bytes、零值存在性、重复字段、上限、畸形编码、公开投影类型、版本缺口、重复增量、墓碑及 revision 耗尽。16 人最坏 Protobuf 基线须符合帧预算。
-
-POSIX 上的 `check-guest-room-server` 启动真实启动器/插件动态库和两个原生 TCP 客户端,检查 Accepted/Entity 绑定、晚加入完整基线、已有连接收到新增基线、合并移动增量、HP/MP 操作、边界操作只确认序列号、状态未变化时无输出、错误不修改状态、墓碑、重连新 Entity GUID、畸形帧断开及正常停服。额外进程加载接近 UINT32_MAX 及 19 MP 的配置,并拒绝非法初始配置。测试使用独立运行目录和端口 19191,同一时间仅运行一份。该进程控制器不构成 Windows 或 C# Godot 验收。
-
-同一测试可执行文件另启动受 20 秒观察期限约束的资源清理子进程,加载真实 Room/Kernel/Net 动态库,通过普通 Join 回调后显式调用两次 Net `pre_shut()`,检查端点关闭、回调捕获释放,以及关闭服务后继续更新不再分发,最后停止管理器。原有正常协议测试保留。测试控制器仅在超时后终止并回收自己的子进程,这不代表正常框架停机。受控宿主保持房间 DSO 加载,不证明库映像解除映射;这些是测试范围说明,实际验收须以执行结果为准。
+编解码目标检查 C++/C# 共享向量、类型值、非法输入、消息帧及 16 人基线体积。
+POSIX 服务端目标启动真实启动器和两个原生客户端,覆盖移动、行删除/复用、
+装备替换/补丁、错误、移除、重连及正常退出。它使用独立运行目录和端口 19191,
+请勿同时启动多份。真实 Godot 执行属于单独的客户端集成检查。
-Applications 工作流在现有平台矩阵中启用本可选示例。默认构建在 Linux、macOS 和 Windows 编译插件;`check-core` 在各平台运行 codec 检查,并在 POSIX 系统运行真实启动器与双客户端进程检查。工作流配置与某次托管运行的结果分别验收。
+[房间协议与上限](protocol_CN.md) · [通用 Entity 协议](../../docs/reference/entity-protocol_CN.md)
+· [停机所有权](../../docs/project/lifecycle-closeout_CN.md)
-示例限制为 8192 字节正文、32 个连接及 16 名玩家;每轮最多处理 64 次业务回调,每连接每秒最多 32 条消息,待发送队列最多八帧及 `8 * (8192 + 12)` 个自有字节。工作量/输出超限或底层发送被拒绝时关闭连接。捕获状态不支持或 revision 耗尽时须重连并获取新基线。这些限制及原生测试不代表生产容量、部署或 Godot 视觉验收。兼容性、投影、版本及恢复规则见[协议](protocol_CN.md)。
+**不兼容变更:** 当前通用 Entity 消息替换旧的纯标量房间协议,服务端与 Godot 客户端须一起
+重新构建,不保留兼容分支。本机功能检查不构成生产容量验收。
diff --git a/examples/guest_room/guest_room_module.cpp b/examples/guest_room/guest_room_module.cpp
index dfb4b140e..4b1e013c3 100644
--- a/examples/guest_room/guest_room_module.cpp
+++ b/examples/guest_room/guest_room_module.cpp
@@ -84,7 +84,7 @@ bool room_module::pre_update() {
}
}
server_->reg_net_event_callback(this, &room_module::on_event);
- ARK_LOG_INFO("guest_room ready: lobby, 16 players, ARK2 protocol");
+ ARK_LOG_INFO("guest_room ready: lobby, 16 players, Protobuf Entity synchronization");
return true;
}
@@ -120,9 +120,25 @@ void room_module::retire(peer& connection) {
}
if (connection.publisher)
{
- removed_.push_back(connection.publisher->remove());
+ removed_.emplace_back(connection.publisher->remove(), connection.binding);
connection.publisher.reset();
}
+ release_player(connection);
+}
+
+void room_module::release_player(peer& connection) {
+ for (auto& equipment : connection.equipment)
+ {
+ if (equipment)
+ {
+ kernel_->destroy_entity(equipment->get_id());
+ equipment.reset();
+ }
+ }
+ if (!connection.player)
+ {
+ return;
+ }
kernel_->destroy_entity(connection.player->get_id());
connection.player.reset();
}
@@ -143,28 +159,33 @@ void room_module::flush_room() {
{
continue;
}
- auto state = kernel_->capture_scalar_snapshot(*connection.player, player_fields, player_limits);
+ const std::array equipment{connection.equipment[0]->get_id(), connection.equipment[1]->get_id()};
+ auto state = kernel_->capture_nested_snapshot(*connection.player,
+ player_projection(connection.player->get_id(), equipment), player_limits().stream.snapshot);
+ validate_player(state);
if (!connection.publisher)
{
- entity_sync::scalar_baseline initial{player_schema, 1, 1, std::move(state)};
- validate_player(initial);
- connection.publisher = std::make_unique(initial, player_limits);
+ connection.binding = {
+ player_schema, connection.player->get_id(), state.root.incarnation, 1, player_layout(equipment)};
+ entity_sync::nested_baseline initial{1, 1, std::move(state)};
+ connection.publisher =
+ std::make_unique(connection.binding, initial, player_limits().stream);
auto& added = *changes.add_added();
added.set_player_id(connection.player_id);
- encode(initial, *added.mutable_state());
+ encode_binding(connection.binding, *added.mutable_binding());
+ encode(initial, *added.mutable_state(), connection.binding);
}
else
{
- validate_player({player_schema, 1, 1, state});
if (const auto changed = connection.publisher->update(state))
{
- encode(*changed, *changes.add_changed());
+ encode(*changed, *changes.add_changed(), connection.binding);
}
}
}
- for (const auto& removed : removed_)
+ for (const auto& [removed, binding] : removed_)
{
- encode(*removed, *changes.add_changed());
+ encode(*removed, *changes.add_changed(), binding);
}
for (auto& [session, connection] : peers_)
{
@@ -182,7 +203,8 @@ void room_module::flush_room() {
{
auto& added = *baseline.add_players();
added.set_player_id(other.player_id);
- encode(other.publisher->baseline(), *added.mutable_state());
+ encode_binding(other.binding, *added.mutable_binding());
+ encode(*other.publisher->baseline(), *added.mutable_state(), other.binding);
}
}
enqueue(connection, snapshot, baseline);
@@ -205,7 +227,7 @@ void room_module::join_room(peer& connection, const wire::join_request& body) {
if (body.protocol_version() != protocol_version || !valid_name(body.name()) || body.room() != "lobby")
{
reject(connection, "invalid_message",
- "Expected protocol version 1, lobby and a name of 1..64 UTF-8 bytes without controls");
+ "Expected the current protocol, lobby and a name of 1..64 UTF-8 bytes without controls");
return;
}
const auto count =
@@ -226,6 +248,42 @@ void room_module::join_room(peer& connection, const wire::join_request& body) {
player->set_double("y", 100.0 + static_cast(((next_player_ - 1) / 8) % 2) * 200.0);
connection.player_id = std::to_string(next_player_++);
connection.player = std::move(player);
+ try
+ {
+ auto* inventory = connection.player->find_table(inventory_index);
+ auto container = connection.player->find_container(equipment_index);
+ if (inventory == nullptr || !container)
+ {
+ throw std::runtime_error("GuestPlayer requires inventory and equipment members");
+ }
+ auto* potion = inventory->add_row(1);
+ if (potion == nullptr)
+ {
+ throw std::runtime_error("Cannot initialize inventory row");
+ }
+ potion->set_u_int32(1, 1);
+ potion->set_u_int32(2, 2);
+ for (std::size_t i = 0; i < connection.equipment.size(); ++i)
+ {
+ auto child = kernel_->create_entity(null_guid, 1, 0, "GuestEquipment", 0, data_list_impl());
+ if (!child)
+ {
+ throw std::runtime_error("Cannot create equipment Entity");
+ }
+ connection.equipment[i] = child;
+ child->set_u_int32(1, 1001 + static_cast(i));
+ child->set_u_int32(2, 10 + 10 * static_cast(i));
+ }
+ if (!container->place(1, connection.equipment[0]))
+ {
+ throw std::runtime_error("Cannot equip initial item");
+ }
+ }
+ catch (...)
+ {
+ release_player(connection);
+ throw;
+ }
wire::join_accepted response;
response.set_player_id(connection.player_id);
response.set_name(body.name());
@@ -276,9 +334,9 @@ void room_module::act_player(peer& connection, const wire::action_request& body)
return;
}
const auto kind = body.kind();
- if (sequence == 0 || kind < wire::action_damage || kind > wire::action_rest)
+ if (sequence == 0 || kind < wire::action_damage || kind > wire::action_upgrade)
{
- fail("invalid_message", "Expected sequence >= 1 and action damage, skill or rest");
+ fail("invalid_message", "Expected a positive sequence and a supported action");
return;
}
auto& player = *connection.player;
@@ -302,13 +360,79 @@ void room_module::act_player(peer& connection, const wire::action_request& body)
}
mp -= 20;
}
- else
+ else if (kind == wire::action_rest)
{
const auto hp_max = player.get_u_int32("hp_max");
const auto mp_max = player.get_u_int32("mp_max");
hp += std::min(std::uint32_t{20}, hp_max - hp);
mp += std::min(std::uint32_t{20}, mp_max - mp);
}
+ else if (kind == wire::action_pickup || kind == wire::action_use_potion)
+ {
+ auto* inventory = player.find_table(inventory_index);
+ auto* potion = inventory->find_row(1);
+ if (kind == wire::action_pickup)
+ {
+ if (potion && potion->get_u_int32(2) >= 99)
+ {
+ fail("inventory_full", "Potion stack is full");
+ return;
+ }
+ if (!potion)
+ {
+ potion = inventory->add_row(1);
+ if (!potion)
+ {
+ throw std::runtime_error("Cannot add potion row");
+ }
+ potion->set_u_int32(1, 1);
+ potion->set_u_int32(2, 1);
+ }
+ else
+ {
+ potion->set_u_int32(2, potion->get_u_int32(2) + 1);
+ }
+ }
+ else
+ {
+ if (!potion)
+ {
+ fail("insufficient_resource", "No potion in inventory");
+ return;
+ }
+ const auto count = potion->get_u_int32(2);
+ if (count == 1)
+ {
+ if (!inventory->remove_row(1))
+ {
+ throw std::runtime_error("Cannot remove consumed potion row");
+ }
+ }
+ else
+ {
+ potion->set_u_int32(2, count - 1);
+ }
+ hp += std::min(std::uint32_t{10}, player.get_u_int32("hp_max") - hp);
+ }
+ }
+ else
+ {
+ auto container = player.find_container(equipment_index);
+ auto equipped = container->find(std::uint32_t{1});
+ if (kind == wire::action_upgrade)
+ {
+ const auto power = equipped->get_u_int32(2);
+ equipped->set_u_int32(2, std::min(std::uint32_t{100}, power + 1));
+ }
+ else
+ {
+ const auto next = equipped == connection.equipment[0] ? connection.equipment[1] : connection.equipment[0];
+ if (!container->remove(std::uint32_t{1}) || !container->place(1, next))
+ {
+ throw std::runtime_error("Cannot replace equipment slot");
+ }
+ }
+ }
// A clamped no-op still acknowledges the independently increasing action sequence.
player.set_u_int32("hp", hp);
player.set_u_int32("mp", mp);
@@ -429,7 +553,7 @@ void room_module::on_event(const net_event* event) {
}
if (found->second.player)
{
- kernel_->destroy_entity(found->second.player->get_id());
+ release_player(found->second);
}
}
peers_.erase(found);
@@ -444,7 +568,7 @@ bool room_module::update() {
{ flush_room(); }
catch (const std::exception& exception)
{
- ARK_LOG_ERROR("guest_room scalar flush failed: {}", exception.what());
+ ARK_LOG_ERROR("guest_room Entity flush failed: {}", exception.what());
for (auto& [session, connection] : peers_)
{
connection.closing = true;
@@ -484,7 +608,7 @@ bool room_module::pre_shut() {
{
if (connection.player != nullptr)
{
- kernel_->destroy_entity(connection.player->get_id());
+ release_player(connection);
}
}
peers_.clear();
diff --git a/examples/guest_room/guest_room_module.hpp b/examples/guest_room/guest_room_module.hpp
index ea1f023d9..13eb19ab8 100644
--- a/examples/guest_room/guest_room_module.hpp
+++ b/examples/guest_room/guest_room_module.hpp
@@ -1,6 +1,7 @@
// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE.
#pragma once
+#include
#include
#include
#include