- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
230 lines
23 KiB
Markdown
230 lines
23 KiB
Markdown
# item.inventory_equipment_state
|
||
|
||
## Scope
|
||
|
||
审计 40250 的本地物品状态模型:背包/装备/腰带槽位、完整物品数据、socket/属性更新、穿脱装备前置判断、网络刷新顺序,以及换图/断线时状态的边界。
|
||
|
||
## Reference call chain
|
||
|
||
- `PythonNetworkStreamPhaseGameItem.cpp`
|
||
- `RecvItemSetPacket` / `RecvItemSetPacket2` 读取 vnum、count、flags、anti-flags、socket、attribute,并调用 `CPythonPlayer::SetItemData`。
|
||
- `RecvItemUpdatePacket` 按顺序调用 `SetItemCount`、`SetItemMetinSocket`、`SetItemAttribute`,随后 `__RefreshInventoryWindow`。
|
||
- `RecvItemUsePacket` 只刷新背包窗口;`RecvQuickSlot*Packet` 更新快捷栏后也刷新背包窗口。
|
||
- `RecvItemSetPacket2` 在 `highlight` 为真时先发 `BINARY_Highlight_Item(window, cell)`,再刷新背包;旧 `RecvItemSetPacket` 的 flags/anti-flags 取零。
|
||
- `PythonPlayer.cpp` / `GameType.h`
|
||
- `TItemPos::IsValidCell` 限制窗口与 cell 域;`SetItemData` 对非零 vnum 先通过 `CItemManager::GetItemDataPointer` 验证。
|
||
- `aItem[c_Inventory_Count]` 是统一的全局槽位空间;启用新装备系统时背包有效范围覆盖普通背包、保留装备位、龙魂位和腰带背包。
|
||
- `IsEquipCell` 使用全局装备区间;`IsEquipItemInSlot` 再通过 `CItemData::IsEquipment` 判断物品是否真的是装备。
|
||
- `PythonNetworkStreamPhaseGameItem.cpp`
|
||
- `SendItemUsePacket` / `SendItemMovePacket` 先检查主角是否可行动;装备操作还受交易、商店、攻击中状态门控,并播放物品音效,最后发送协议包和 sequence。
|
||
- `GameLib/ItemData.cpp` / `ItemManager.cpp`
|
||
- item_proto/item_list 是类型、装备性、模型、图标和物品数据的权威来源,不由槽位编号推断。
|
||
|
||
### Reference action gates
|
||
|
||
- `CPythonNetworkStream::SendItemUsePacket`、`SendItemMovePacket`、`SendItemDropPacket`、`SendItemDropPacketNew`、`SendItemUseToItemPacket` 均先调用 `__CanActMainInstance`。
|
||
- 穿脱装备路径额外调用 `IsEquipItemInSlot`;交易、商店、攻击中分别阻止装备移动/使用,并向游戏窗口发对应通知。
|
||
- 通过前置门后,使用和移动分别调用 `PlayUseSound` / `PlayDropSound`,写入固定 packet,再调用 `SendSequence`。
|
||
|
||
## Current call chain
|
||
|
||
- `extension/src/net/classic/classic_parser.cpp`
|
||
- 解析 40250 经典 21/20/25 号物品包;20 号 deprecated empty 包按参考端的 42 字节布局处理,vnum 为零时清槽,否则按 Set 写入。
|
||
- `extension/src/net/entity_store.cpp`
|
||
- `EntityStore::apply` 处理 m2dev 物品包;`mut_item_set`、`mut_item_del`、`mut_item_update` 写入 inventory/equipment/belt/dragon-soul 存储并生成 `m_inv_changes`。
|
||
- `mut_slot` 同时兼容全局 equipment/belt cell 与旧 raw wear/belt cell,避免经典包和当前 UI 的窗口表示互相破坏。
|
||
- `extension/src/net/m2_client.cpp`
|
||
- `get_inventory` / `get_equipment` / `get_belt_inventory` 暴露状态;`inventory_changed` 逐 cell 发出刷新信号。
|
||
- `move_item` / `use_item` / `drop_item` / `use_item_to_item` / `give_item` 统一通过 `valid_item_pos` 校验后转发到 Classic 或 m2dev 会话。
|
||
- `project/ui/inventory_ui.gd`
|
||
- `_to_wire` 将旧 11 个装备位映射到 equipment 窗,将 costume/ring/belt 映射到全局 inventory cell;刷新时将 `get_inventory`、`get_equipment` 和 belt inventory 投影到 UI。
|
||
- 右键装备先走 `EquipRules.can_equip`;服务器仍是最终仲裁。`equip_model.gd` 监听装备变化并从 item_proto/item_list/parts 更新模型。
|
||
|
||
### Current action path
|
||
|
||
- `M2Client::move_item/use_item/drop_item/drop_item_count/use_item_to_item/give_item`
|
||
- 统一检查 `is_in_game`、整数宽度、`bounds::item_cell` 和 40250 主角/装备行动门;use/move 在发包前从 item_proto 选择并发出物品音效事件,然后转发到 session/game client。
|
||
- `ClassicSession::send_item_*`
|
||
- 只检查 `Stage::InGame`、目标/数量的基础范围并构造 packet;行动门与物品音效由共享 `M2Client` 入口完成。
|
||
- `EntityStore::mut_item_set/update/del`
|
||
- 不查询 `ItemManager`/item_proto;未知 vnum 仍可进入 store,`get_inventory` 只返回 vnum 非零项。
|
||
|
||
## Branch matrix
|
||
|
||
| 场景 | 40250 | 当前端 | 结论 |
|
||
|---|---|---|---|
|
||
| 完整 Set | 非零 vnum 先查 ItemManager,再覆盖槽位全部字段并刷新 | Classic/m2dev 均覆盖 vnum/count/flags/anti-flags/socket/attr 并发 `inventory_changed`,但未知 vnum 不拒绝,且旧 Set/Set2 的 flags 语义被统一 | PARTIAL:资源拒绝和 21/22 的字段分支不等价 |
|
||
| deprecated empty Set/Del | 20 号包固定按完整 42 字节读;vnum=0 清空 | Classic parser 保留固定帧长并分流为 `mut_item_del` | STATIC 部分成立;m2dev 使用独立 GC_ITEM_DEL |
|
||
| Update 非空槽 | 更新 count、socket、attr,不改 vnum;`SetItemCount` 会先触发一次 `RefreshInventory`,包处理末尾再刷新 | `mut_item_update` 一次性覆盖并只发一个 `inventory_changed` | PARTIAL:最终字段接近,但刷新次数和中间状态事件源不同 |
|
||
| Update 有效但当前 vnum=0 的槽 | `GetItemData` 仍返回有效槽指针,更新包字段 | 已移除“空槽直接丢弃”分支,保持 count/socket/attr 和刷新事件 | FIXED + regression |
|
||
| 无效 window/cell | `TItemPos::IsValidCell` 直接拒绝,不发包 | 原先只做 u8/u16 宽度检查;现在 `bounds::item_cell` 限制 168/134/180/16 域 | FIXED + regression |
|
||
| 装备使用/穿脱 | `__CanActMainInstance`、交易/商店/攻击中门控、声音、包序列 | UI 有 EquipRules 前置校验;native 已统一行动门、装备性和 item_proto 音效事件;完整 m2dev/经典 sequence 与服务端拒绝边界仍未完全证明 | PARTIAL |
|
||
| 装备类型判定 | `IsEquipCell` + ItemManager 的 `CItemData::IsEquipment` | UI 使用 `EquipRules`;native 发送路径不重新读取 item_proto,且 store 可接受未知/非装备 vnum | PARTIAL |
|
||
| UI 刷新 | Set/Update/QuickSlot 后统一刷新窗口,Set2 可能先高亮;Update 的 `SetItemCount` 与末尾刷新有明确时序 | native 发逐 cell 信号,UI 全量刷新;不含 Set2 highlight,Update 只有一次回调,listener 顺序未与 Python window callback 对齐 | PARTIAL |
|
||
| 换图/断线 | `CPythonPlayer::Clear` 清战斗/UI 状态;物品由新阶段包重新建立 | `EntityStore::reset_for_map_change` 清物品扩展窗/仓库/龙魂,但普通背包/装备按注释保留 | PARTIAL:需继续核对经典 Loading 包的实际重建边界 |
|
||
|
||
## Implementation-equivalence matrix
|
||
|
||
| 项目 | 状态 | 证据/限制 |
|
||
|---|---|---|
|
||
| Preconditions | PARTIAL | cell 域已对齐;可行动、交易/商店/攻击中门控尚未由 native 统一执行 |
|
||
| Branch structure | PARTIAL | Set/Del/Update、经典 deprecated 帧和旧/扩展槽已覆盖;未知 item_proto 和装备操作拒绝分支未闭合 |
|
||
| Algorithms/formulas | MAPPED | 当前没有改写物品属性公式;装备模型映射仍由 Godot adapter 承担 |
|
||
| State transition order | PARTIAL | 收包→store→信号→UI 顺序已定位,但与 `__RefreshInventoryWindow` 的全部监听副作用尚未逐项证明 |
|
||
| Constants/units | PARTIAL | 槽位域已按 GameType.h 固定值;新装备/龙魂/腰带实际编译分支仍需 fixture 覆盖 |
|
||
| Timing/event sources | PARTIAL | 当前使用 Godot signal + 全量 refresh,不是原端 Python window callback 的同一事件源 |
|
||
| Resource/data sources | PARTIAL | UI 读取 item_proto/item_list;native Set 尚未验证 vnum 必须存在于 ItemManager,`get_inventory` 还隐藏 vnum=0 但 count/socket/attr 已更新的槽 |
|
||
| Protocol side effects | PARTIAL | 包结构、经典 20/21/25 解析、行动门、Set2 highlight 和 use/move 音效已有证据;每个操作的 `SendSequence` 及服务端拒绝副作用仍不完整 |
|
||
| Interruption/failure/cleanup | PARTIAL | 非法 cell 已拒绝;空槽 Update 已保持;断线、换图、服务器拒绝后的 UI/拖拽回滚仍需继续审计 |
|
||
|
||
## Regression evidence
|
||
|
||
- `extension/tests/net_bounds_test.cpp`:验证 inventory/equipment/dragon-soul/belt 有效范围及非法窗口拒绝。
|
||
- `extension/tests/net_entity_test.cpp`:验证 Set、Update、Del、socket/attribute、旧装备、扩展 belt,以及空槽 Update 不被错误丢弃。
|
||
- `extension/tests/net_classic_session_test.cpp`:验证经典 40250 物品包固定长度、20 号 deprecated 清槽/设物品和 25 号 Update。
|
||
- `project/inventory_ui_test.gd`、`project/equip_model_test.gd`:验证 UI 槽位映射、数量显示、拖放和装备模型刷新。
|
||
|
||
本轮实际执行的自动测试全部退出码通过:`net_bounds_test`、`net_entity_test`、`net_classic_session_test`、`inventory_ui_test.gd`、`equip_model_test.gd`、`item_tooltip_test.gd`、`view_equipment_ui_test.gd`。其中 `view_equipment_ui_test.gd` 仍报告 5 个 ObjectDB 泄漏;这些测试没有覆盖未知 vnum、交易/商店/攻击中门、物品音效、Set2 highlight、Update 双刷新、空槽 Update 的 API 可见性和换图/断线重建。
|
||
|
||
## Status
|
||
|
||
`PARTIAL`。前轮修复了两个可复现的结构差异:非法物品 cell 仍可发包,以及有效空槽的 GC_ITEM_UPDATE 被丢弃。本轮确认未知 vnum 仍可进入 native store,装备操作仍缺 40250 的行动状态/交易/商店/攻击中门和物品音效,Set2 highlight 与 Update 刷新事件时序也未对齐;完整 Loading 重建顺序仍未达到 40250 的 1:1 证据标准。
|
||
|
||
## Deep audit round 2026-09-20
|
||
|
||
本轮重新执行 `net_bounds_test`、`net_entity_test`、`net_classic_session_test`、`inventory_ui_test.gd`、`equip_model_test.gd`、`item_tooltip_test.gd` 和 `view_equipment_ui_test.gd`,并把操作前置门与收包副作用逐项对回 40250:
|
||
|
||
- 七项回归均退出码 0;证明槽位边界、Set/Update/Del、经典 20/21/25 号物品帧、UI 填充、装备模型、tooltip 和单属性面板没有新增功能回退。
|
||
- `RecvItemSetPacket2` 的 `highlight` 在 40250 会先调用 `BINARY_Highlight_Item` 再刷新窗口;当前 native parser/store 只发逐 cell `inventory_changed`,没有等价高亮事件。
|
||
- 40250 `RecvItemUpdatePacket` 明确先 `SetItemCount`、socket、attribute,并在中间/末尾触发窗口刷新;当前 `mut_item_update` 合并为一次覆盖和一次信号,最终数据接近但监听副作用时序不同。
|
||
- `CPythonNetworkStream::SendItemUse/Move/Drop/UseToItem` 先走 `__CanActMainInstance`,装备操作还检查交易、私店、攻击中并播放物品音效;当前 `M2Client`/`ClassicSession` 只做阶段、范围和数值校验,UI 的 `EquipRules` 不能替代 native 发送路径的统一门控。
|
||
- 当前 `EntityStore` 不通过 ItemManager/item_proto 拒绝未知 vnum;`get_inventory` 也只暴露非零 vnum,因而无法证明参考端对有效空槽的 count/socket/attribute 字段可见性完全一致。`view_equipment_ui_test.gd` 虽退出码 0,但退出时泄漏 5 个 ObjectDB 实例,不能作为干净生命周期证据。
|
||
|
||
结论:物品状态和装备面板基础链可用,但协议副作用、操作前置门、资源校验、空槽 API 和换图/断线重建仍未与 40250 统一,合同保持 `PARTIAL`。
|
||
|
||
## Implementation round 2026-09-21
|
||
|
||
本轮按 40250 `PythonNetworkStreamPhaseGameActor::__CanActMainInstance` 和
|
||
`PythonNetworkStreamPhaseGameItem.cpp` 的入口顺序修复了可在当前 native 架构中闭合的部分:
|
||
|
||
- 新增 `item_action_rules.h`,Classic 与 m2dev 共用同一套主角存在、死亡、眩晕、击倒门;有效参数但当前不可行动时,`M2Client` 按 40250 语义消费调用并返回成功,不向 session 发包。
|
||
- `M2Client::move_item/use_item/use_item_to_item/drop_item/drop_item_count/give_item` 已在 transport 前统一检查;独立 equipment window 的操作还检查交易、私店和攻击中状态。攻击/技能/移动状态通过 native 入口维护,主角死亡时清除本地攻击锁存。
|
||
- 经典 21 号包和 m2dev `GC_ITEM_SET` 现在保留 `highlight`,`EntityStore` 生成独立 `ItemHighlight` 队列;M2Client 在 `inventory_changed` 前发 `item_highlighted`,InventoryUI 消费该信号并对对应格子做 40250 风格短暂高亮。
|
||
- 增加了 action-rule、经典 Set2 highlight、m2dev Set2 highlight 和 InventoryUI 高亮回归断言;C++ 全量构建、CTest 以及 `inventory_ui_test.gd` 均通过。
|
||
|
||
仍保持 `PARTIAL`:当前端还没有将 item_proto/ItemManager 的装备性判定下沉到 native,因而 inventory 全局装备位无法在所有协议入口上复刻 `IsEquipItemInSlot`;物品使用/丢弃音效与 sequence 仍没有 40250 的等价资源/事件源;`RecvItemUpdatePacket` 的 count→socket→attribute→刷新中间副作用仍被 store 合并为一次最终状态事件;未知 vnum 拒绝、空槽字段 API、服务器拒绝后的拖拽回滚以及换图/断线重建仍需后续合同轮次验证。
|
||
|
||
## Next audit round 2026-09-20T18:19Z
|
||
|
||
本轮把“客户端是否能直接执行物品操作”与“查看装备属性面板”分开核对,避免只因 UI 测试通过就把 native 操作链视为等价:
|
||
|
||
- `CPythonNetworkStream::SendItemUsePacket` 的顺序是 `__CanActMainInstance` → 装备槽的交易/商店/攻击中门 → `PlayUseSound` → 发包 → `SendSequence`;当前 `M2Client::use_item`(`extension/src/net/m2_client.cpp:2688`)只检查 in-game 和 cell 范围,直接进入 `ClassicSession::send_item_use`。`move_item`、`drop_item`、`use_item_to_item` 也没有同一主角动作门,因而 UI 层 `EquipRules` 不能覆盖脚本/快捷栏/native 调用入口。
|
||
- `RecvItemSetPacket2` 的 `highlight` 是 40250 明确的额外副作用;当前 `EntityStore` 只产生 `inventory_changed`,本轮没有发现 `item_highlighted` 等价事件。该差异不会被 `net_entity_test` 的最终槽位数据断言捕获。
|
||
- `view_equipment_ui_test.gd`、`inventory_ui_test.gd` 和 `item_tooltip_test.gd` 均通过,但它们只证明最终 UI 数据和局部窗口装配;未证明交易/商店/攻击中拒绝、物品音效、Set2 高亮和 Update 中间刷新顺序。
|
||
|
||
本轮没有修改实现代码;合同继续为 `PARTIAL`。下一步应先把动作门收敛到 native 发送层,再用 fake session 记录“拒绝不发包 / 允许按顺序发包”的分支测试,之后再处理 Set2 高亮与 Update 刷新事件顺序。
|
||
|
||
## Next audit round 2026-09-20T18:31Z
|
||
|
||
本轮沿 `PythonNetworkStreamPhaseGameItem.cpp` 的收包和发包函数逐项复核到当前 native 入口:
|
||
|
||
- 40250 的 `RecvItemSetPacket2` 在写入物品后,若 `highlight` 为真会额外触发 `BINARY_Highlight_Item(window, cell)`,再刷新背包。当前 classic parser 和 `EntityStore::mut_item_set` 都读取完整物品字段但丢弃 `highlight`,没有对应信号或 UI 消费路径。
|
||
- 40250 的 `RecvItemUpdatePacket` 依次调用 `SetItemCount`、全部 socket、全部 attribute,最后 `__RefreshInventoryWindow`;`SetItemCount` 自身还触发一次 `RefreshInventory`。当前 `mut_item_update` 合并成一次覆盖和一次 `inventory_changed`,最终槽位数据接近,但监听者看不到同一刷新时序。
|
||
- 40250 的 `SendItemUsePacket`/`SendItemMovePacket`/`SendItemDropPacket`/`SendItemUseToItemPacket` 统一先经过 `__CanActMainInstance`;装备移动/使用还检查交易、商店、攻击中并播放使用/丢弃音效。当前 `M2Client` 只检查 `is_in_game`、cell/count 宽度并直接调用 `ClassicSession`/m2dev session,native 层没有对应主角行动门、交易/商店/攻击状态或音效副作用。
|
||
- 因此已有槽位、空槽 Update、非法 cell 和 UI 测试不能证明“死亡/攻击/交易/商店期间不发物品包”以及 Set2 高亮行为一致;本轮未修改实现代码,合同继续为 `PARTIAL`。
|
||
|
||
## Implementation round 2026-09-20T19:03Z
|
||
|
||
本轮按 `RecvItemUpdatePacket` → `SetItemCount` → `SetItemMetinSocket`/
|
||
`SetItemAttribute` → `__RefreshInventoryWindow` 的顺序修复 Update 的状态事件:
|
||
|
||
- `InvChange` 现在携带写入时的 `Item` 快照,而不是只有 window/cell。
|
||
- `mut_item_update` 在 count 写入后先记录一次快照,再写 sockets/attributes 并记录最终快照;
|
||
m2dev `GC_ITEM_UPDATE` 也复用同一入口,避免两条协议语义分叉。
|
||
- native 新增 `inventory_state_changed(window, cell, item)`,InventoryUI 优先消费快照;
|
||
因此第一次刷新看到“新数量+旧 socket/属性”,第二次刷新看到完整最终状态。原有
|
||
`inventory_changed` 保留给旧监听者兼容。
|
||
- `net_entity_test` 增加普通槽和空槽的两阶段快照断言;C++ 全量构建、CTest、
|
||
`inventory_ui_test.gd` 和移动时间线回归均通过。
|
||
|
||
这是一层保持 Godot pump 架构的事件适配,不代表所有旧 `inventory_changed` 监听者都已经
|
||
改成快照消费;ItemManager 未知 vnum/装备分类、物品音效和 sequence、服务器拒绝回滚、
|
||
换图重建及新装备/腰带乱序矩阵仍保持 `PARTIAL`。
|
||
|
||
## Implementation round 2026-09-21T13:00Z
|
||
|
||
本轮继续对照 `GameType.h::TItemPos::IsEquipCell`、
|
||
`CPythonPlayer::IsEquipItemInSlot` 和 `CItemData::IsEquipment`,修复了 native
|
||
发送入口把“装备窗口”直接等同于“装备物品”的差异:
|
||
|
||
- `M2Client` 新增 `set_item_proto`,复用 GameScene 已加载的同一份 item_proto;
|
||
`is_equip_item_in_slot` 现在先验证全局装备区/旧 raw wear 坐标,再通过 item_proto
|
||
的 type 仅把 `ITEM_TYPE_WEAPON(1)`、`ITEM_TYPE_ARMOR(2)` 视为 40250 的
|
||
`CItemData::IsEquipment()`。
|
||
- `move_item` / `use_item` 的 native 前置门改为检查源槽的真实装备性,不再因空的
|
||
dedicated equipment window、时装、戒指或腰带槽位而误触发交易/商店/攻击限制;
|
||
无 proto 的启动过渡期保留装备位的 fail-closed 限制,避免绕过门控。
|
||
- 新增纯规则回归断言,明确 use、costume、ring 不属于 40250 的
|
||
`IsEquipment()` 分支;C++ 23 项 CTest、`inventory_ui_test.gd`、
|
||
`equip_model_test.gd`、`netbridge_test.gd` 均通过。
|
||
|
||
仍保持 `PARTIAL`:`SetItemData` 对未知 vnum 的拒绝尚未下沉到 EntityStore,完整
|
||
sequence、旧 `inventory_changed` 监听者的中间快照消费、服务器拒绝回滚以及换图/断线
|
||
重建仍未达到 40250 的完整证据标准。
|
||
|
||
## Implementation round 2026-09-21T13:20Z
|
||
|
||
本轮继续闭合 `CPythonPlayer::SetItemData` 的资源校验分支:
|
||
|
||
- `EntityStore` 新增可选的 item vnum validator;validator 生效后,非零未知 vnum
|
||
在 `mut_item_set` 中保持原槽位不变,不产生刷新或高亮副作用。
|
||
- `M2Client::set_item_proto` 在 GameScene 的 item_proto 加载成功后把同一资源表接入
|
||
active world;由于真实加载顺序是先收 loading burst、后建 proto,接入时还会清理
|
||
之前已进入 store 的非法物品,并为换图/重连后的新 world 重新绑定一次。
|
||
- `net_entity_test` 新增“未知 vnum 拒绝、已知 vnum 接受、迟到 validator 清理”的
|
||
回归断言;C++ 全量 CTest、背包 UI、装备模型和 NetWorld bridge 回归均通过。
|
||
|
||
仍保持 `PARTIAL`:物品使用/丢弃音效和 sequence 的资源侧副作用、旧
|
||
`inventory_changed` 监听者的中间快照消费、服务器拒绝后的拖拽回滚,以及新装备/龙魂/
|
||
腰带乱序和换图重建的完整矩阵仍需后续审计。
|
||
|
||
## Audit round 2026-09-20T21:28Z
|
||
|
||
本轮重新从 `TItemPos::IsValidCell/IsEquipCell`、`SetItemData`、
|
||
`RecvItemSetPacket2/RecvItemUpdatePacket` 和 `SendItemUse/Move/Drop` 对照到
|
||
`EntityStore -> M2Client -> InventoryUI/EquipModel`,并重跑 3 项 native 与 5 项 Godot 物品回归:
|
||
|
||
- 槽位边界、经典 20/21/25 号包、Set/Update/Del、socket/attribute、空槽 Update、Set2 highlight、ItemManager 装备分类、未知 vnum 校验以及装备模型/tooltip/单属性面板均保持通过。
|
||
- 当前 native 入口已统一主角行动门、真实装备类型判定和未知 vnum validator,但仍没有 40250 `PlayUseSound/PlayDropSound` 与 `SendSequence` 的等价资源/事件源。
|
||
- `RecvItemUpdatePacket` 的 count→socket→attribute 两次可观察快照已适配;旧 `inventory_changed` 监听者是否都按中间状态消费、空槽 count/socket/attribute 的公开读取语义仍未完全证明。
|
||
- 新装备/龙魂/腰带在 classic 与 m2dev 双协议下的乱序、服务端拒绝/拖拽回滚、断线或换图重建仍缺协议级 fixture。`view_equipment_ui_test` 和 `netbridge_test` 通过,但报告 5 个 ObjectDB 泄漏,因此不能作为干净生命周期证据。
|
||
|
||
结论:本轮没有新增实现修改;物品主数据和装备判定链可回归,但操作副作用、异常回滚和扩展槽位生命周期仍与 40250 不完全统一,合同保持 `PARTIAL`。
|
||
|
||
## Audit round 2026-09-21T03:48Z — inventory/equipment regression reconciliation
|
||
|
||
本轮没有重复修改物品实现,而是重新执行 P0 物品链回归并核对上一轮修复的证据边界:
|
||
|
||
- `net_bounds_test`、`net_entity_test`、`net_classic_session_test`、
|
||
`inventory_ui_test.gd`、`equip_model_test.gd`、`item_tooltip_test.gd` 和
|
||
`view_equipment_ui_test.gd` 均返回 0;非法 cell、Set/Update/Del、未知 vnum、装备
|
||
分类、Set2 高亮、双阶段 Update 快照、UI 映射和装备模型没有回归。
|
||
- `view_equipment_ui_test.gd` 仍报告 5 个 ObjectDB 泄漏。功能断言通过,但该结果不能
|
||
作为 40250 生命周期/退出清理的干净证据,保留为后续清理项。
|
||
- 对照 40250 `PlayUseSound/PlayDropSound`、`SendSequence`、服务器拒绝后的窗口回滚及
|
||
新装备/龙魂/腰带乱序边界,当前没有新的等价实现或协议 fixture,因此不升级合同状态。
|
||
|
||
结论:本轮确认已实现部分保持通过,未发现新的可安全闭合差异;合同继续为 `PARTIAL`,
|
||
下一次物品实现应优先处理音效/sequence 或拒绝回滚,并先补失败回归。
|
||
|
||
## Implementation round 2026-09-21T04:10Z — item action sound parity
|
||
|
||
本轮按 40250 `CPythonItem::__GetUseSoundType`、`__GetDropSoundType`、
|
||
`CPythonNetworkStream::SendItemUsePacket` 和 `SendItemMovePacket` 修复物品操作音效:
|
||
|
||
- 新增共享 `item_action_rules.h` 音效枚举与 type/subtype 分支,严格保留武器/弓/箭、身体/首饰防具、能力提升药、普通药水无音效、符咒和默认音效的 40250 分支。
|
||
- `M2Client` 从同一份 item_proto 解析音效,只有通过主角行动门后才发 `item_action_sound`;使用和移动分别在发送包前发出 use/drop 音效。GameScene 将该事件交给 Audio 播放,避免 UI 和两种 transport 各自推断。
|
||
- 新增 native 规则断言与 `item_action_sound_parity_test.gd`,覆盖分类、资源文件名、未知 vnum 和普通药水无音效;背包、装备模型、tooltip、查看装备面板和 NetWorld 回归继续通过。
|
||
|
||
仍保持 `PARTIAL`:直接丢弃包在 40250 入口本身不额外播放 inventory drop sound;完整 `SendSequence`/m2dev 包序、服务器拒绝回滚、旧监听者中间快照、扩展槽位乱序及换图/断线重建仍未完成。测试中的 `view_equipment_ui_test`/`netbridge_test` ObjectDB 泄漏也尚未清理。
|