Files
mtgodot-poc/audit/contracts/item.inventory-equipment-state.md
T
shenandshen c93894313a fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧
- inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域
- 包含其他累积的功能开发和测试文件
2026-09-21 16:38:59 -07:00

230 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 泄漏也尚未清理。