diff --git a/docs/plan-refactor-inventory-core-v1.md b/docs/plan-refactor-inventory-core-v1.md new file mode 100644 index 000000000..d261a58d8 --- /dev/null +++ b/docs/plan-refactor-inventory-core-v1.md @@ -0,0 +1,153 @@ +# plan-refactor-inventory-core-v1 — Inventory 核心事务契约(重构轨 R10) + +> 所属总纲:`plan-refactor-master-v1.md`。P0 只冻结事实、失败边界、跨轨 owner 与审核要求的 pins;不把设计当实现。 + +## 阶段 + +- ✅ 2026-08-03 P0 完整契约面重写 + absorption audit +- ⬜ P1:inventory 拆分 + txn/capacity 骨架 + inventory-layout/dropped-loot 纯 migration helpers(依赖 R3 P1 seam) +- ⬜ P2:production writer 迁移分为 P2a metadata/provider + Public writer path 与 P2b OwnerOnly private-writer activation;P2a 依赖 R3 P2 atomic commit seam 与旧 dropped-loot compatibility,P2b 必须等 R3 P4 dropped-loot migration/hydration、R6 P1 recipient projection/page、R5 P3 + R6 P4、R10 P3 pickup txn 及 R4 pickup consumer 全部完成后才可启用。 +- ⬜ P3:pickup/merge txn(依赖 R5 P3 attrition API、R6 P4 receipt API) +- ⬜ P4:联合 bot/e2e + plan 收口(依赖 R4 handler 与 R3 P4 legacy inventory-layout consumer) + +实现属 Wave 2;跨轨工作须登记 owning plan。 + +## 1. 完整 contract surface + +| 面 | P0 冻结的事实 | +|---|---| +| `InventoryTxn` | staged `deliver` / `consume_checked` / `pickup_and_merge`;成功最多一次 revision bump,所有验证先于 mutation | +| spill/overflow | 原实例守恒、全 writer 统一 capacity API、source 与 durable drop 可恢复原子提交 | +| dropped sync | server 按 recipient 授权投影后分页;无全局 snapshot 广播/复用 | +| pickup | authoritative dimension/range/ownership 授权;inventory commit 后才删 drop | +| legacy migration | R10 纯转换,R3 用真实 world context 消费 overflow;全成才写新 schema | + +### 1.1 `InventoryTxn` + +```rust +InventoryTxn::deliver(DeliveryRequest, Option<&mut SpillContext>) + -> Result +InventoryTxn::consume_checked(ConsumeRequest) + -> Result +InventoryTxn::pickup_and_merge(PickupRequest, PickupAuthorization, &mut DroppedLootRegistry) + -> Result +``` + +共同规则:验证在 staged view 完成;失败时 inventory/revision/session/material/registry 不变;成功最多 bump 一次 revision。堆叠必须完整 identity 相同,不能只比 `template_id`。错误至少区分 unknown/zero/insufficient/identity mismatch/invalid placement/capacity/missing spill context/unauthorized/wrong dimension/persistence unavailable。 + +`deliver` 同时支持 minted template 与既有 `ItemInstance`。后者必须保留 id、durability、freshness、attributes/NBT、charges、forge/alchemy 与 owner-qi 等动态字段,禁止重建默认实例。receipt 含 request id、revision、created ids、placed existing ids、merge source/target/count、spill dropped/source/count/location,且 `stored + spilled == requested`;既有 id 不得记作 created。 + +`consume_checked` receipt 逐 instance 记录扣除量/剩余量;insufficient、unknown、zero 任一失败不得部分扣除。 + +`pickup` receipt 含 request id、revision、removed drop、merge/placement、`target_instance_id`、`incoming_instance_id/count/abs_qi_before`。placement 的 target 等于 incoming;merge 的 target 是提交后既有 stack。R5 只按 incoming absolute qi 做 attrition:`target_after = preexisting_abs_qi + incoming_after`,不得磨损旧数量或由合并后整栈反推。 + +## 2. Spill / overflow 守恒 + +只允许容量不足 spill 到真实玩家脚下/机制结算点;结构错误、无真实 dimension/position、ID 冲突、非法 footprint 均 fail closed。`SpillContext` 必须含 source identity/revision、真实 dimension/position、registry、durable seam 与 transaction id;不得猜位置或留下无主 handoff。 + +`DroppedLootRegistry` 是有界 durable queue: + +```text +MAX_DURABLE_DROPPED_LOOT_ENTRIES = 4096 +MAX_OWNER_ONLY_DISCARD_ENTRIES_PER_PLAYER = 256 +SYSTEM_RESERVED_DURABLE_DROPPED_LOOT_ENTRIES = 512 +try_insert / try_insert_batch +``` + +所有 production writer 禁止直接 `entries.insert`,包括 give/craft/alchemy/forge/loot、player discard、container/pack overflow、death/revive、termination、morph release、`spawn_template_dropped_loot`、`tsy_loot_spawn::spawn_for_layer`、placeable-break 与 TSY layer/relic writers,以及实现波次枚举出的同类 producer。批量事务在 source mutation 前一次 reservation;超限返回 `{current, required, limit}`,所有状态与 DB 不变。`OwnerOnly` 的 player discard 同时受每个 `PlayerId` 的 `MAX_OWNER_ONLY_DISCARD_ENTRIES_PER_PLAYER` 配额和 `SYSTEM_RESERVED_DURABLE_DROPPED_LOOT_ENTRIES` 系统保留容量约束,不得消耗系统保留区;craft/alchemy/forge/loot/death/termination 等 production/system writer 才能预留保留区。单一 owner 因此不能填满全局 queue,其他玩家的 production spill 在 discard 洪峰下仍保有 bounded admission。pickup/授权 durable delete 才释放容量;不以 TTL/LRU/价值驱逐静默销毁。超限 hydration 由 R3 只读降级并告警,禁止截断或空表覆盖。 + +内存 reservation 不等于 durability。R3 transaction/outbox 必须把 source inventory/session/material mutation、drop insert/delete、source revision、drop/transaction id 作为一个 recoverable commit。失败/崩溃/重启不得形成“只删 source”或“只写 drop”;按 `(transaction_id, source_revision, dropped_id)` 幂等重试,不丢不重。 + +## 3. Recipient-specific dropped sync + +`DroppedLootEntry` 必须持久化 `owner: Option` 与 `visibility: Public | OwnerOnly`;producer 从机制权威 source 写入,普通 world loot 为 `Public`,私人 spill/drop 为 `OwnerOnly`。管理员授权来自 server permission,不写入 client payload;R3 migration/hydration 原样保留这些字段,缺失旧数据仅按明确 migration 规则补 `Public`,不得从请求猜 owner。 + +R6 在编码前对每个 recipient 用 server authority 过滤:同 dimension、在授权 distance/zone observation 范围、`OwnerOnly` 仅 owner 或授权管理员可见。排序、revision、page count 都针对过滤后的 projection;只有 visibility key 完全相同者可复用编码页,禁止一个 global snapshot 发给所有 client。 + +## 4. Pickup transaction + authorization + +R4 从 ECS 构造不可由 client 覆盖的 `PickupAuthorization`:player UUID、`CurrentDimension`、authoritative position/observation range、owner/private permission、server-resolved entry、revision/anti-replay fact。txn 重新验证 entry/instance、同维、距离/zone、权限和 freshness;知道 dropped id、曾收到 sync 或跨维相同 XYZ 都不构成授权。 + +顺序固定:authorize + validate merge/placement/capacity → staged attach/merge 与 incoming-only R5 attrition transfer(item qi → authoritative zone + ledger)→ 同一 durable transaction 原子提交 attrited item、inventory revision、zone balance/ledger 与 dropped delete → receipt。任一步失败或崩溃恢复都不得只应用其中一侧;drop 保留且可按 transaction id 重试。placement-only 与 merge 都必须覆盖。 + +所有 attach 必须 `validate_attach_fits` 后 `attach_at_location`,删除 `(0,0)` 强塞。move/rotate/pack accepted outcome 含 request id、revision、instance/from/to、权威 item view;rejected outcome 含 reason/instance/from/to。snapshot 仅作状态修正,不是动作级反馈。 + +## 5. Legacy migration + +```text +migrate_legacy_inventory_layout(value, schema_version) + -> MigrationOutcome { migrated_value, overflow: Vec } + +migrate_legacy_dropped_loot_entry(value, schema_version) + -> Result + // owning phase: R10 P1 pure migration helper; consumed by R3 P4 hydration +``` + +R10 的 inventory-layout 与 dropped-loot migration 函数均纯且幂等,保留所有实例/动态字段,不执行 SQL、不猜 world context、不隐藏 overflow;其中 dropped-loot 迁移把旧 `entry_json` 缺失的 `owner`/`visibility` 明确补为 `owner = None`、`visibility = Public`。R3 hydration consumer 必须先按 persisted schema version 解码/迁移旧 dropped-loot JSON,再构造 `DroppedLootEntry`;inventory-layout migration 则在临时副本上以真实 player/dimension/position 与 capacity/durable seam 消费。两类 migration 都须全成才写新行,失败保留旧行可重试且不重复 drop。 + +## 6. 所有权与顺序 + +- **R10**:`server/src/inventory/**` model/grid/txn/capacity、writer enumeration、typed outcome、纯 migration。P1 仅在 **R3 P1** 的 inventory/overflow seam 冻结后实现 txn/capacity 骨架;P2a 负责 metadata/provider 与 `Public` writer path,依赖 **R3 P2** durable spill/pickup recoverable-commit seam、旧 dropped-loot migration compatibility 与 crash/retry pins;P2b 才能启用 `OwnerOnly` private writers,且必须等 **R3 P4 dropped-loot migration/hydration、R6 P1 recipient projection/page、R5 P3 + R6 P4、R10 P3 pickup txn、R4 pickup consumer** 全部完成;P3 pickup/attrition consumer 只有在 **R5 P3** incoming-only attrition/ledger API 与 **R6 P4** receipt wire/client API 已合入后才可接通。 +- **R3**:SQL/outbox、spill/pickup recoverable commit、hydration guard、migration consumer;R10 只消费 R3 P1/P2/P4 已冻结并实现的接口。P4 必须拆成 dropped-loot hydration 子批次与 inventory-layout overflow 子批次,前者不等待 R10 P3,后者才等待其实际 durable/capacity 前置。 +- **R4**:C2S gate/handler、authoritative pickup context、调用 R10 并转交 R6 outcome;R4 handler/consumer phase 必须等待 **R10 P3 pickup txn、R6 P4** receipt API 与 **R5 P3** attrition API,不得以 R10 mock 或仅 R6 P1 schema 代替。 +- **R5**:incoming-only qi attrition/ledger;provider phase 为 R5 P3。 +- **R6**:receipt wire/client、recipient projection/page、decoder;canonical plan 登记 rotate、pack feedback、dropped sync;dropped-loot projection/page consumer 为 R6 P1,必须在 **R10 P2a owner/visibility metadata provider** 与 **R3 P4 dropped-loot migration/hydration consumer** 完成后才可消费;receipt provider phase 为 R6 P4。 +- **R1**:txn stored/spilled 成功后才 teardown,失败保留 session。 +- **R7**:UI 消费,不拥有事务。 + +顺序:**R3 P1 → R10 P1(含纯 inventory-layout/dropped-loot migration helpers)→ R3 P2 atomic seam 实现 + legacy dropped-loot migration/hydration compatibility pins → R10 P2a metadata/provider + Public writer path → R3 P4 dropped-loot migration/hydration consumer → R6 P1 dropped-loot projection/page consumer → R5 P3 + R6 P4 → R10 P3 pickup/merge txn → R4 handler/pickup consumer → R10 P2b OwnerOnly private-writer activation → R3 P4 inventory-layout overflow consumer → R10 P4 联合 e2e**。R10 P2a 在 R3 P2 atomic seam 与旧 dropped-loot migration compatibility 未合入前不得开始;R3 P4 dropped-loot migration/hydration consumer 必须先于 R6 P1 dropped-loot projection/page consumer,确保旧 `entry_json` 已先升级为带 `owner`/`visibility` 的 canonical entry;R10 P2b 只有在 R3 P4 dropped-loot hydration、R6 P1 projection/page、R5 P3 + R6 P4、R10 P3 pickup txn 与 R4 pickup consumer 全部完成后才可启用,避免 OwnerOnly writer 在授权消费链闭合前广播或转移私有掉落;R10 P3 在 R5 P3/R6 P4 provider 未合入前不得开始 pickup consumer;R4/R3 的外轨交付物只作 R10 P4 验收前置、不计入 R10 自身 phase;R10 不越权改 persistence、wire、handler 或 client。 + +## 7. 审核要求的 contract pins + +仅保留下列 demanded pins;refactor 可删除 implementation-coupled 旧测试,不以数量为门: + +1. `consume_checked` 成功精确扣除;insufficient/unknown/zero 失败无 mutation/revision。 +2. `deliver` 对 same-template/different-identity、duplicate id、illegal footprint/placement、容量不足但缺 `SpillContext` 逐项 typed reject,且无 mutation/revision;existing instance placement/spill 逐字段保留,created 与 placed/spilled ids 分离。 +3. capacity 的 limit-1/limit/limit+1/batch;逐个生产 writer 证明走统一 gate,失败全状态不变;owner-only player discard 覆盖单 `PlayerId` 的 quota-1/quota/quota+1 与 system-reserved boundary,证明一个 owner 不能耗尽 global capacity,系统 writer 在 discard 洪峰下仍可 admission。 +4. spill durable write failure、commit interruption、restart/retry:无单边状态、无重复 drop。 +5. pickup 同维成功;跨维、超距/zone、owner/private 拒绝;merge、placement-only、failed attach/capacity/validation/persistence 后 entry 仍在;成功后才删。 +6. incoming-only attrition receipt + R5 ledger:旧 stack absolute qi 不变;注入 attrition 后、durable commit 中断与 restart/retry,断言 attrited item + zone/ledger + drop delete 原子且总量守恒。 +7. visibility matrix:同维/范围内 `Public` 对非 owner 可见,`OwnerOnly` 对 owner 可见、对普通非 owner 不可见、对 server-authorized admin 可见;另测跨维/超距拒绝。page/revision 按每个 recipient projection;缺页/混 revision 不替换。 +8. accepted/rejected move correlation;pack stow/equip/unequip 与拒绝必须动作级 receipt,stale event 和 snapshot-only baseline 不通过。 +9. forge 深链保留;另锁 `/give hoe_iron → 新 snapshot → 真实非零 instance → held/equip → lingtian_start_till`,禁止 `instance_id=0` 或任意 server-data 冒充成功。 +10. inventory-layout migration pure happy/empty/full/dynamic/idempotent/invalid;dropped-loot migration 覆盖旧 `entry_json` 缺 owner/visibility → `None`/`Public`、已有字段原样保留、malformed/幂等;R3 consumer 对真实 context 成功,缺 context/capacity/persistence/migration failure 保留旧行可重试。 + +## 8. Named bot acceptance(P4) + +以下名称即 `scripts/bot/scenarios/.py` 的稳定身份: + +1. `inv_full_delivery_matrix`:craft/alchemy/forge/give 满包时 `stored + spilled == requested`,失败不 teardown。 +2. `inv_stack_merge`:同 identity merge、异 identity 分栈;placement-only 与拒绝路径保留 drop;attrition durable 中断/restart 仍原子守恒。 +3. `inv_footprint_sync`:2×1 rotate 后以 request/instance/from/to/revision 锚定 1×2 authoritative receipt;snapshot 不代替回执。 +4. `inv_pack_feedback`:stow/equip/unequip 的 accepted/rejected 均按时间锚与 correlation 匹配,stale event 不通过。 +5. `inv_give_visibility_forge`:`/give fan_iron_anvil` → 新 snapshot 真实 id → `forge_station_place`。 +6. `inv_give_visibility_lingtian`:`/give hoe_iron` → 新 snapshot 非零 id → held/equip receipt → `lingtian_start_till`。 +7. `inv_dropped_loot_bound`:全 writer 容量拒绝、delete 后重试、超限 hydration guard、recipient 分页/可见性。 + +## 9. 吸收边界与 P0 验真 + +| plan | 2026-08-03 验真结论 | 证据/落点 | +|---|---|---| +| `alchemy-takeback-full-inventory-loss` | 部分吸收 | 满包 deliver/spill 后才由 R1 teardown。 | +| `dropped-loot-pickup-stack-merge` | 仍真实,吸收 | 当前 pickup 只找空 footprint;落 `pickup_and_merge`。 | +| `force-attach-grid-collision` | 仍真实,吸收 | `(0,0)` 强塞仍可达;改合法 attach/spill。 | +| `rotate-footprint-sync` | 仍真实,吸收 | `Moved` 缺权威新 footprint;R6 receipt 补齐。 | +| `bot-inventory-pack-feedback` | 仍真实,吸收 | pack 成功路径可只有 snapshot;动作级 receipt 补齐。 | +| `bot-production-inventory-instance-visibility` | 部分闭环,吸收剩余 | forge 已用真实 id;lingtian 仍有 id=0 baseline,场景 6 锁定。 | +| `forge-outcome-full-inventory-loss` | 仍真实,吸收 | #1294 只建立 skeleton;outcome 改走统一 deliver/spill。 | +| `dropped-loot-cross-dimension-pickup` | 仍真实,吸收 | 当前 entry 有 dimension 但 pickup 未获 `CurrentDimension`;R4 authorization 补齐。 | + +不吸收:已闭环 `craft-refund-full-inventory-loss`;独立 feature `container-filter-and-completion`;已撤回 `nested-pack-base`。P0 仅在逐项复读代码/plan 并记录上述 live/fixed/invalid 结论后完成;P4 仍须按表逐项核验 merge SHA 与 bot/client 证据后才能归档。 + +## 10. Deferred-to-implementation-wave + +P0 不决定 Rust lifetime、SQL/outbox 实现、锁粒度、distance/zone 数值、visibility-key 编码、client 重发、管理员运维或 UI;由 owning PR 设计并受上述 contract/pins 约束。P0 不迁移 writer、不删旧入口、不改 runtime。未跑真实 server→wire→client/bot 链前,不以 forge、snapshot 或文档声明归档 bug skeleton。 + +本轮新增 deferred decisions(不扩 P0 实现范围): + +1. **Pickup freshness / anti-replay**:`PickupAuthorization` 的 revision/anti-replay fact 如何生成、绑定和失效,留待 R10 P3 pickup txn 设计时决定;理由是必须与真实 durable transaction/idempotency 语义共同冻结,避免 P0 先拍一个不可验证的 token 形状;交叉引用 §6 顺序与总纲 §3 Wave 2。 +2. **Receipt correlation / C2S request ID**:accepted/rejected inventory receipt 的 request identity 是否新增到 `inventory_move_intent` 并贯通 Rust/proto/TypeBox/Java,留待 R6 P4 receipt wire/client 设计时决定;理由是响应相关性必须与双向 schema 变更和 breaking gate 一起落地;交叉引用 §6 R6 ownership 与 `docs/plans-skeleton/plan-refactor-wire-s2c-v1.md` P4。 +3. **Recipient-context revocation**:移动、换维度、管理员权限变化是否触发 dropped-loot projection 重发/撤销,留待 R6 P1/P2 projection/store 设计时决定;理由是这是 recipient lifecycle 与 client stale-snapshot 清理的联合契约,P0 不预先指定触发矩阵;交叉引用 §3 与 `docs/plans-skeleton/plan-refactor-wire-s2c-v1.md` P1-P2。 +4. **Snapshot multiplicativity bound**:recipient-specific full snapshot 的 coalescing、增量/空间索引或 aggregate rate bound,留待 R6 P1 emit builder 设计时决定;理由是分页和 key reuse 是否足以控制 aggregate cost 需要结合真实 recipient cardinality 与 wire budget 评估;交叉引用 §3 与 `docs/plans-skeleton/plan-refactor-wire-s2c-v1.md` P1。 + +P0 完成只表示上述 surface、owner、失败边界、pins 与逐项 absorption audit 已冻结,不表示后续实现完成。 diff --git a/docs/plans-skeleton/plan-refactor-c2s-gate-v1.md b/docs/plans-skeleton/plan-refactor-c2s-gate-v1.md index fffb0beec..bb02a4f60 100644 --- a/docs/plans-skeleton/plan-refactor-c2s-gate-v1.md +++ b/docs/plans-skeleton/plan-refactor-c2s-gate-v1.md @@ -20,7 +20,7 @@ - ⬜ P0 设计收口 + 吸收清单验真:113 个变体普查(每个标注应有的门禁四元组现状);冻结 `GateSpec` 与拒绝回执语义;等 #1287(冷却重构,同文件大改)merge 定基线。 - ⬜ P1 门禁中间件落地:gate 层上线,先给"已知漏洞簇"的 ~20 个请求类型挂 spec(吸收清单全命中),旧内联校验保留并行断言一个版本期。 -- ⬜ P2 巨石拆分批次 A:巨型 match 拆为按域 handler 注册表(combat/production/world/social/npc 五组),行为不变,bot 场景锁住。 +- ⬜ P2 巨石拆分批次 A:巨型 match 拆为按域 handler 注册表(combat/production/world/social/npc 五组),行为不变,bot 场景锁住;inventory pickup handler 必须从 ECS `CurrentDimension`、authoritative position/observation range 与 owner/private permission 构造 R10 `PickupAuthorization`,禁止仅凭 client XYZ/instance id 调用 txn,且把 R10 accepted/rejected outcome 交给 R6 emit API。**本批次的 inventory pickup consumer 仅在 R10 P3 pickup/merge txn、R5 P3 attrition API 与 R6 P4 receipt API 均已合入后实施;此前不得以 mock 或旧 R6 P1 schema 接线。** - ⬜ P3 巨石拆分批次 B + 全量挂 spec + 删旧:113 变体全部声明门禁(含显式 `no_gate` 声明,杜绝静默无门禁);删除各域内联距离常量与重复维度判断。 - ⬜ P4 bot 验收 + 吸收 plan 批量归档。 @@ -37,7 +37,7 @@ skeleton:alchemy-furnace-scope-gate、block-place-reach-gate、coffin-reclaim- ## bot 验收场景 -1. `gate_cross_dimension`:bot 在 TSY 维度对主世界坐标发 workbench/zhenfa/coffin/trade 请求→全部拒绝且回执带原因。 +1. `gate_cross_dimension`:bot 在 TSY 维度对主世界坐标发 workbench/zhenfa/coffin/trade/pickup 请求→全部拒绝且回执带原因;pickup 即使 dropped id 与 XYZ 已知,也必须以 server `CurrentDimension` 拒绝跨维请求。 2. `gate_reach`:超距放方块/开炉/采灵田→拒绝;贴脸→放行。 3. `gate_ownership`:拆他人棺/取他人容器→拒绝。 4. `gate_state_precondition`:给丹先校验后扣(满包/死亡目标不吞丹);丹毒超阈值禁服。 diff --git a/docs/plans-skeleton/plan-refactor-inventory-core-v1.md b/docs/plans-skeleton/plan-refactor-inventory-core-v1.md deleted file mode 100644 index 63e88cae2..000000000 --- a/docs/plans-skeleton/plan-refactor-inventory-core-v1.md +++ /dev/null @@ -1,48 +0,0 @@ -# plan-refactor-inventory-core-v1 — Inventory 巨石拆分 + 网格/交付事务一致性(重构轨 R10) - -> 所属总纲:`plan-refactor-master-v1.md`。一句话:拆掉 20165 行的 `inventory/mod.rs`,把"给予/交付/拾取/堆叠/占格"改成统一事务 API(满包不丢物、先校验后扣、堆叠必合并、占格必同步)——物品凭空消失/孤儿物品整簇收口。 - -## 现状证据(2026-07-27 侦察) - -- `server/src/inventory/mod.rs` 20165 行、目录 89% 代码在单文件——全仓第一大 god module。 -- 交付路径各写各的:满包退款只记日志(craft-refund,active 接近完成)、满包取丹先清 session 丢产物(alchemy-takeback)、锻造产物满包丢失(forge-outcome,#1294 在飞)、满包强塞无碰撞检测叠孤儿物品(force-attach-grid-collision)。 -- 一致性缺口:世界掉落拾取不合并已有堆叠(dropped-loot-pickup-stack-merge)、旋转后占格不同步 client(rotate-footprint-sync)、give 后快照拾取不到实例(bot-production-inventory-instance-visibility)、pack stow 无稳定回执(bot-inventory-pack-feedback)。 -- 历史包袱:pre-#249 老存档卡旧布局(memory 已记,值得随拆分一并处理);tarkov 套包 `owner_instance_id` 全栈已落地是既定架构,不动。 - -## 接入面 - -- **进料**:R1 session 产物交付调用、R4 gate 通过后的物品类请求、掉落物系统、R3 的持久化 slice(inventories 表)。 -- **出料**:统一 `InventoryTxn` API:`deliver(items) -> Delivered | Spilled(fallback)`(满包溢出策略统一:脚下掉落/暂存箱,按 worldview 拍板)、`consume_checked`(先校验后扣)、`merge_stack`、占格变更事件(S2C 经 R6)。 -- **共享类型**:`ItemCategory` 合法集不动(无 Material,材料用 Misc——历史坑);`owner_instance_id` 架构不动。 -- **worldview 锚点**:物品不凭空消失对齐末法稀缺经济(§十三物资锚点);含真元物品的销毁/溢出走 R5 ledger。 - -## 阶段 - -- ⬜ P0 设计收口 + 吸收清单验真:mod.rs 职责普查出拆分图(grid/txn/container/corpse/shelflife 接缝);冻结 `InventoryTxn` API 与满包溢出策略;等 craft-refund P4、#1294 相关项定基线。 -- ⬜ P1 巨石拆分:按职责拆文件(行为不变,测试平移),`InventoryTxn` 骨架上线。 -- ⬜ P2 交付路径统一:give/craft/alchemy/forge/loot 全部改走 `deliver`;先校验后扣全量化;满包场景全绿。 -- ⬜ P3 网格/堆叠一致性:拾取合并、占格同步、pack 回执、老存档布局迁移补课。 -- ⬜ P4 bot 验收 + 吸收 plan 批量归档。 - -## 吸收清单(短名省略 plan-bughunt- 前缀与 -v1 后缀) - -skeleton:alchemy-takeback-full-inventory-loss(交付垫层部分;session teardown 归 R1)、dropped-loot-pickup-stack-merge、force-attach-grid-collision、rotate-footprint-sync、bot-inventory-pack-feedback、bot-production-inventory-instance-visibility;在飞 #1294:forge-outcome-full-inventory-loss。 -**不吸收**:craft-refund-full-inventory-loss(active,P0-P3 已 ✅,独立收尾 P4);container-filter-and-completion(feature,独立);nested-pack-base(已 WITHDRAWN,#1275)。 - -## 文件所有权与边界 - -- 独占:`server/src/inventory/**`、各域交付调用点的替换行。 -- 不碰:`InspectScreen`(R7 域);session 生命周期(R1);掉落物拾取的 gate 校验(R4)。 -- 依赖:R3 P1(persistence 拆分先行,inventories 表接缝清晰);与 R1 的交付接缝 API 由本轨定义、R1 消费。Wave 2 开工,P0 普查可先行。 - -## bot 验收场景 - -1. `inv_full_delivery_matrix`:满包状态下 craft 完工/取丹/锻造出炉/给予→断言产物按统一溢出策略落地,总数不丢。 -2. `inv_stack_merge`:拾取同类掉落→断言合并入既有堆叠。 -3. `inv_footprint_sync`:旋转/移动占格物品→断言 client 快照占格一致(P6 protobuf 深断言)。 -4. `inv_give_visibility`:dev give 后立即快照→断言实例可见可拾取(修 bot 基建自身的假阳性)。 - -## 开放问题(pre-P0 收口) - -1. 满包溢出策略正典拍板:脚下掉落(可被他人捡走,符合末法残酷)vs 个人暂存箱(体验友好)——需人工定。 -2. pre-#249 老存档迁移是否并入本轨 P3(倾向并入,一次清账)。 diff --git a/docs/plans-skeleton/plan-refactor-persistence-slices-v1.md b/docs/plans-skeleton/plan-refactor-persistence-slices-v1.md index 71bcd7ccf..c582a0f86 100644 --- a/docs/plans-skeleton/plan-refactor-persistence-slices-v1.md +++ b/docs/plans-skeleton/plan-refactor-persistence-slices-v1.md @@ -21,10 +21,13 @@ ## 阶段 - ⬜ P0 设计收口 + 吸收清单验真:53 张表普查归域;冻结 Slice trait(载入守护语义:读失败 = 保留旧行 + 告警 + 只读降级,绝不写回空态;flush registry;tick rebase 协议);等 #1288/#1289/#1261/#1259 merge 后定基线。 -- ⬜ P1 框架落地 + 巨石拆分:`persistence/` 按域拆文件(迁移链不变、行为不变);Slice 框架上线,KnownTechniques/Lifecycle(在飞 PR 的成果)平移为首批宿主。 -- ⬜ P2 载入守护推广:全部玩家 slice(SkillSet/Wounds/状态 buff/身份键……)收编,#1290 模式全量落地;身份主键统一(identity-persist-key-mismatch)。 +- ⬜ P1 框架落地 + 巨石拆分:`persistence/` 按域拆文件(迁移链不变、行为不变);Slice 框架上线,KnownTechniques/Lifecycle(在飞 PR 的成果)平移为首批宿主;冻结 inventory slice hydration seam 及 `MigrationOutcome` consumer 边界,R3 不复制 inventory 网格规则,待 R10 P1 提供纯幂等迁移函数后接入。R3 P1 只能冻结该 seam,不得引用尚未合入的 R10 常量或实现。 +- ⬜ P2 载入守护推广:全部玩家 slice(SkillSet/Wounds/状态 buff/身份键……)收编,#1290 模式全量落地;dropped-loot slice 的有界 hydration guard 依赖 R10 P1 已 merge 的容量契约:仅在该前置成立后引用 `MAX_DURABLE_DROPPED_LOOT_ENTRIES` 与 `DroppedLootRegistry::try_insert/try_insert_batch`;超限进入统一 load-failure guard/只读降级并告警,禁止 `take(limit)` 截断、驱逐旧条目或以空 registry 覆盖数据库。在 R10 P1 未 merge 时,R3 P2 不得编译或复制临时常量。同步冻结并实现 spill/pickup persistence transaction/outbox seam:source mutation、attrited item、zone balance/qi ledger、drop insert/delete 与幂等 transaction id 构成一个 recoverable commit,且 crash/retry pins 常绿后才允许 R10 P2a 迁移 Public writer path;R10 P2b OwnerOnly private writers 另受 R10 P3、R4、R6 与 R3 P4 consumer gates 约束。 - ⬜ P3 关服 flush + tick rebase 批次:shutdown flush registry 收编全部"节流落盘"域;绝对 tick 全部改相对基准;autosave/事件写入竞态互斥(coffin-autosave-inflight-race 模式)。 -- ⬜ P4 遗漏运行态补持久化批次:ActiveEvents、TiandaoAttention、状态效果、化虚冷却、灵眼、地表遗缴、散灵珠、可放置实体、dormant 往返身份完整性(heiwushi)等——逐个按 Slice 框架补表。 +- ⬜ P4 遗漏运行态补持久化批次:ActiveEvents、TiandaoAttention、状态效果、化虚冷却、灵眼、地表遗缴、散灵珠、可放置实体、dormant 往返身份完整性(heiwushi)等——逐个按 Slice 框架补表;P4 拆为两个独立 consumer 子批次: + - **dropped-loot hydration 子批次**:在 R10 P1 merge、R3 P2 persistence seam 与旧行 compatibility pins 就绪后,调用 `inventory::migration::migrate_legacy_dropped_loot_entry`,把旧 `dropped_loot.entry_json` 缺失字段补成 `owner = None`、`visibility = Public`,再反序列化为 `DroppedLootEntry`;此子批次先于 R6 P1 projection/page,且不等待 R10 P3/R5/R6 P4/R4。 + - **inventory-layout overflow 子批次**:仅在 R10 P3 merge 后,调用 `inventory::migration::migrate_legacy_inventory_layout`,用玩家 identity、真实机制结算点/世界 position、dimension 组装 `SpillContext`,把 `MigrationOutcome::overflow` 通过 R10 capacity API 持久化到 durable registry。 + 两类 consumer 仅在新 schema 与各自全部输出成功持久化后提交新行,缺上下文/容量/持久化或 JSON migration 失败则保留旧行并进入可重试 load guard;dropped-loot 子批次依赖 R10 P1,inventory-layout 子批次依赖 R10 P3,不得合并为一个跨越两者的门禁或另造容量常量。 - ⬜ P5 bot 验收 + 吸收 plan 批量归档。 ## 吸收清单(短名省略 plan-bughunt- 前缀与 -v1 后缀) @@ -40,12 +43,29 @@ skeleton:coffin-autosave-inflight-race、identity-persist-key-mismatch、miner ## bot 验收场景 -1. `restart_player_slices`:bot 建号→修炼/学功法/受伤→关服重启→重连→断言功法/伤势/濒死后果/buff 全部还原。 +1. `restart_player_slices`:bot 建号→修炼/学功法/受伤→关服重启→重连→断言功法/伤势/濒死后果/buff 全部还原;另以 pre-#249 inventory fixture 验证 R10 纯迁移保留全部 instance/dynamic fields、重复载入幂等并保存新 schema。 2. `restart_world_runtime`:触发矿脉枯竭/配方解锁/zone influence→SIGTERM 关服→重启→断言无回滚无复活。 -3. `load_failure_guard`:注入一行损坏 slice 数据→启动→断言该玩家进入守护降级而非清零覆盖(配 dev 命令注入)。 +3. `load_failure_guard`:基础损坏 slice 仍断言守护降级而非清零覆盖;超过 R10 P1 提供的 `MAX_DURABLE_DROPPED_LOOT_ENTRIES` 的 dropped-loot rows 只有在 R10 P1 已 merge 后执行同一 guard,数据库行数与内容不得被截断/清空;R10 前置未满足时该断言保持待接线,不得引用不存在的 symbol。另覆盖旧 `entry_json` 无 owner/visibility → `owner = None` + `Public` 的 migration/hydration 正例,以及 malformed/migration failure 保留旧行可重试;再注入 spill durable write failure,以及 pickup attrition staged 后的 commit interruption/restart,断言 attrited item、zone/ledger 与 drop delete 无单边状态,按 transaction id 重试不重复应用。 4. `tick_rebase`:带冷却/再生倒计时重启→断言倒计时按真实流逝折算(对齐 #1289 的 deadline 折算先例)。 ## 开放问题(pre-P0 收口) 1. 载入守护的玩家体验:只读降级 vs 拒绝进服 vs 回滚到上一备份?需人工拍板。 2. 迁移链是否借机做一次 squash(v1-v39 合并基线)?风险与老存档兼容性需评估。 + +## § P0 决议锚点(待 R3 P0 开工时补齐) + +- `MAX_DURABLE_DROPPED_LOOT_ENTRIES` 的引用门:R3 P2/P4 依赖 R10 P1 merge,R3 不复制常量或在此前编译引用。 +- dropped-loot hydration consumer:R3 P4 在 R10 P1 migration helper、R3 P2 persistence seam 与旧行 compatibility pins 就绪后执行,且必须先于 R6 P1 projection/page;失败保留旧行并可重试。 +- inventory-layout migration consumer:R3 P4 的独立 overflow 子批次在 R10 P3 merge 后,使用真实 `SpillContext` 完成 overflow 持久化;失败保留旧行并可重试。 + +## 验收测试声明 + +- `cargo test --package bong-server persistence -- --nocapture`:Slice load guard、flush registry、tick rebase、migration consumer、dropped-loot hydration bound。 +- bot e2e:`restart_player_slices`、`restart_world_runtime`、`load_failure_guard`、`tick_rebase`。 + +## 实施边界 + +- R3 P1 只冻结接缝与框架,不提前实现 R10 inventory migration 或 dropped-loot capacity API。 +- R3 P2/P4 在依赖 merge 后才实现对应消费者;所有 migration overflow 必须有真实 spill context 和 durable sink。 +- 本 skeleton 不直接改 `server/src/inventory/**`;R10 拥有 inventory 生产 writer 与容量 API。 diff --git a/docs/plans-skeleton/plan-refactor-qi-ledger-v1.md b/docs/plans-skeleton/plan-refactor-qi-ledger-v1.md index 8790ec9bb..9fe529123 100644 --- a/docs/plans-skeleton/plan-refactor-qi-ledger-v1.md +++ b/docs/plans-skeleton/plan-refactor-qi-ledger-v1.md @@ -23,7 +23,7 @@ - ⬜ P0 设计收口 + 吸收清单验真:~200 个直写点全量普查分类(合法初始化/regen/衰减/战斗/UI 镜像);冻结封装方案(`qi_current` 收私有 + 类型化访问器:`gain_from_zone` / `release_to_zone` / `transfer_to` / `set_for_init`,每个访问器内嵌 ledger 记账);负灵域语义、qi_max 缩容语义写成决议。 - ⬜ P1 类型封装落地:字段收私有,访问器上线;既有 ~130 处 QiTransfer 调用点平移;编译期扫清全部直写(编译器就是审计器)。 - ⬜ P2 修复批次 A(cultivation + 消耗品 + lingtian):regen/服丹/plot_qi/经脉淬炼影子账全部走访问器归账。 -- ⬜ P3 修复批次 B(combat 各流派 + fauna/npc/boss):overflow/drain/打断退款/日程回气/暗器 imprint 回滚;离屏死亡走 `release_dormant_qi_to_zone`。 +- ⬜ P3 修复批次 B(combat 各流派 + fauna/npc/boss + inventory pickup):overflow/drain/打断退款/日程回气/暗器 imprint 回滚;离屏死亡走 `release_dormant_qi_to_zone`;为 R10 `PickupAttritionBasis { target_instance_id, incoming_instance_id, incoming_stack_count, incoming_abs_qi_before }` 提供稳定 incoming-only pickup attrition API,只磨损 incoming 绝对真元并把损耗守恒归还 zone,merge 后不得重扣既有 stack 的绝对真元。 - ⬜ P4 守恒审计常绿 + 归档:`assert_conservation` 进 bot e2e 每场景收尾断言;吸收 plan 批量归档。 ## 吸收清单(短名省略 plan-bughunt- 前缀与 -v1 后缀) @@ -43,6 +43,7 @@ skeleton:attrition-overflow-ledger、bonecoin-qi-facevalue、carrier-resonance 2. `qi_skill_roundtrip`:单招释放前后玩家+zone 总量不变。 3. `qi_death_release`:击杀带真元的 NPC/离屏战死→断言 zone 收到等额释放。 4. `qi_negative_zone`:负灵域内释放/吸收→断言赤字被记账不被 max(0) 抹平。 +5. `qi_pickup_merge_incoming_only`:已有 stack 与同 identity dropped item 合并,断言 attrition 只基于 receipt 的 `incoming_abs_qi_before`,既有 stack 原绝对真元不变、目标 stack + zone 总量守恒,且不依赖 consumed dropped instance id 在 inventory 中仍存在。 ## 开放问题(pre-P0 收口) diff --git a/docs/plans-skeleton/plan-refactor-wire-s2c-v1.md b/docs/plans-skeleton/plan-refactor-wire-s2c-v1.md index 3ba4aa422..02af8d5f7 100644 --- a/docs/plans-skeleton/plan-refactor-wire-s2c-v1.md +++ b/docs/plans-skeleton/plan-refactor-wire-s2c-v1.md @@ -18,24 +18,26 @@ ## 阶段 -- ⬜ P0 设计收口 + 吸收清单验真:28 旁路逐个普查(收编 vs 豁免理由);100 emit 文件的重复模式取样归纳 builder API;枚举前缀剥离点全量清点;冻结 scope 语义与 join 首包快照集清单。 -- ⬜ P1 emit builder + scope 落地:builder 上线,vfx/audio/env 三类先挂 scope(跨维 bleed 立灭);跨位面切换时 env/season 全量重发。 -- ⬜ P2 client 桥接层收敛:枚举前缀剥离收敛到单点(含 forge-session 修复);`ServerDataRouter` 注册表整备(分域注册文件,不再单个 1547 行 switch 追加)。 +- ⬜ P0 设计收口 + 吸收清单验真:28 旁路逐个普查(收编 vs 豁免理由);100 emit 文件的重复模式取样归纳 builder API;枚举前缀剥离点全量清点;冻结 scope 语义与 join 首包快照集清单;正式登记 `rotate-footprint-sync`、`bot-inventory-pack-feedback` 的 inventory wire/feedback 工作,并冻结 `dropped_loot_sync` 分片 envelope(`snapshot_revision/page_index/page_count`、每页上限常量 `DROPPED_LOOT_SYNC_PAGE_SIZE = 256`)。 +- ⬜ P1 emit builder + scope 落地:builder 上线,vfx/audio/env 三类先挂 scope(跨维 bleed 立灭);跨位面切换时 env/season 全量重发;**仅在 R10 P2a 的 `DroppedLootEntry.owner/visibility` metadata provider 与 R3 P4 dropped-loot migration/hydration consumer 均合入后**,dropped-loot 内容变化与 join sync 才通过共享 builder 按固定页大小发送,同一 visibility key 的 snapshot 只排序/编码一次后复用于目标 clients,禁止 per-client 重建无界全量 payload;在上述两个前置完成前不得启用该 private projection path。 +- ⬜ P2 client 桥接层收敛:枚举前缀剥离收敛到单点(含 forge-session 修复);`ServerDataRouter` 注册表整备(分域注册文件,不再单个 1547 行 switch 追加);dropped-loot client store 仅在同 revision 全部分片收齐后原子替换,缺页/混 revision 保留旧视图并请求/等待重发。 - ⬜ P3 旁路归一批次:28 channel 逐批收编入 server_data envelope 或登记豁免(资源包/握手类可豁免);删除散装 receiver。 -- ⬜ P4 契约 pin 全量化:双向 sample 对拍测试补齐(113 C2S + 144 S2C 每变体至少一条正反 sample,schema 改动连 sample 一起改);emit 迁移到 builder 的长尾批次。 +- ⬜ P4 契约 pin 全量化:双向 sample 对拍测试补齐(113 C2S + 144 S2C 每变体至少一条正反 sample,schema 改动连 sample 一起改);emit 迁移到 builder 的长尾批次;完成 inventory receipt contract 子批次:`InventoryEventV1::Moved`(或等价 accepted receipt)必须携带 request identity、结果 revision、权威 item view,覆盖 schema/sample/convert/emit API、Fabric `InventoryEventHandler` 与 Python decoder,供 R4 handler 消费 R10 typed outcome。分片 dropped-loot 正反样本必须覆盖空/单页/恰好 256/257/末页缺失/混 revision。 - ⬜ P5 bot 验收 + 吸收 plan 批量归档。 +R10 dropped-loot 契约优先:编码前按 recipient dimension/range/owner 投影,仅同 visibility key 复用;rejected receipt 含 reason/instance/from/to,并测两 recipient 正反可见性。 + ## 吸收清单(短名省略 plan-bughunt- 前缀与 -v1 后缀) active:server-data-s2c-schema-union-drift(TS union 补齐走 regenerate)、spirit-treasure-chat-key-conflict 除外(归 R7)。 -skeleton:vfx-audio-dimension-bleed、q-world-season-dimension-env-resync、forge-session-enum-unstripped(#1294 在飞)、client-request-schema-drift(C2S 契约 pin 部分)、cl-ningmai-meridian-target-drop(payload 字段丢失)、alchemy-recipe-fragment-handoff(id 前缀契约)、vfx-event-slash-contract(event_id 格式契约;agent 侧改动最小化)、npc-trade-bundle-count-bridge(展示/结算数量桥)、dropped-loot-g-pickup-range-desync(拾取范围下发对齐部分)、skillbar-cast-source-drift 与 skillconfig-castsync 除外(归 R9)。 +skeleton:vfx-audio-dimension-bleed、q-world-season-dimension-env-resync、forge-session-enum-unstripped(#1294 在飞)、client-request-schema-drift(C2S 契约 pin 部分)、cl-ningmai-meridian-target-drop(payload 字段丢失)、alchemy-recipe-fragment-handoff(id 前缀契约)、vfx-event-slash-contract(event_id 格式契约;agent 侧改动最小化)、npc-trade-bundle-count-bridge(展示/结算数量桥)、dropped-loot-g-pickup-range-desync(拾取范围下发对齐部分)、rotate-footprint-sync(R10 typed outcome → moved/accepted 权威 item view)、bot-inventory-pack-feedback(成功/拒绝动作级机器回执)、skillbar-cast-source-drift 与 skillconfig-castsync 除外(归 R9)。 注:server↔agent 方向的桥(anticheat-tiandao-drop、niche-guardian-redis-dispatch、npc-combat-relic-schema-drift、pseudo-vein-agent-deadwire、war-participate-agent-command-drift、天道叙事簇 14 项)**不吸收**——agent 不在本次重构范围,独立保留(见总纲 §6 独立轨)。 ## 文件所有权与边界 - 独占:server `network/*_emit.rs` 公共模式与新 `network/emit/`、`schema/proto_convert.rs`;client `network/`(ProtoServerDataBridge、ServerDataRouter、BongNetworkHandler 的 channel 注册区段)。 - 不碰:`BongNetworkHandler.clearClientStateOnDisconnect` 区段(R2 域,同文件分区段,merge 前互相 fetch);`client_request_handler.rs`(R4);各 emit 的业务语义。 -- 依赖:无硬前置;R2 先合(同文件低冲突区段);R4 P2 建议在本轨 P1 后开。 +- 依赖:无通用硬前置;R2 先合(同文件低冲突区段);R4 P2 建议在本轨 P1 后开。**P1 的 dropped-loot projection/page 子项例外:硬依赖 R10 P2a owner/visibility metadata provider 与 R3 P4 dropped-loot migration/hydration consumer,不能随其他 scope 子项提前实施。** ## bot 验收场景