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

23 KiB
Raw Blame History

item.inventory_equipment_state

Scope

审计 40250 的本地物品状态模型:背包/装备/腰带槽位、完整物品数据、socket/属性更新、穿脱装备前置判断、网络刷新顺序,以及换图/断线时状态的边界。

Reference call chain

  • PythonNetworkStreamPhaseGameItem.cpp
    • RecvItemSetPacket / RecvItemSetPacket2 读取 vnum、count、flags、anti-flags、socket、attribute,并调用 CPythonPlayer::SetItemData
    • RecvItemUpdatePacket 按顺序调用 SetItemCountSetItemMetinSocketSetItemAttribute,随后 __RefreshInventoryWindow
    • RecvItemUsePacket 只刷新背包窗口;RecvQuickSlot*Packet 更新快捷栏后也刷新背包窗口。
    • RecvItemSetPacket2highlight 为真时先发 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::SendItemUsePacketSendItemMovePacketSendItemDropPacketSendItemDropPacketNewSendItemUseToItemPacket 均先调用 __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_setmut_item_delmut_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_inventoryget_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 仍可进入 storeget_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,不改 vnumSetItemCount 会先触发一次 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 highlightUpdate 只有一次回调,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_listnative Set 尚未验证 vnum 必须存在于 ItemManagerget_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.gdproject/equip_model_test.gd:验证 UI 槽位映射、数量显示、拖放和装备模型刷新。

本轮实际执行的自动测试全部退出码通过:net_bounds_testnet_entity_testnet_classic_session_testinventory_ui_test.gdequip_model_test.gditem_tooltip_test.gdview_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_testnet_entity_testnet_classic_session_testinventory_ui_test.gdequip_model_test.gditem_tooltip_test.gdview_equipment_ui_test.gd,并把操作前置门与收包副作用逐项对回 40250:

  • 七项回归均退出码 0;证明槽位边界、Set/Update/Del、经典 20/21/25 号物品帧、UI 填充、装备模型、tooltip 和单属性面板没有新增功能回退。
  • RecvItemSetPacket2highlight 在 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 拒绝未知 vnumget_inventory 也只暴露非零 vnum,因而无法证明参考端对有效空槽的 count/socket/attribute 字段可见性完全一致。view_equipment_ui_test.gd 虽退出码 0,但退出时泄漏 5 个 ObjectDB 实例,不能作为干净生命周期证据。

结论:物品状态和装备面板基础链可用,但协议副作用、操作前置门、资源校验、空槽 API 和换图/断线重建仍未与 40250 统一,合同保持 PARTIAL

Implementation round 2026-09-21

本轮按 40250 PythonNetworkStreamPhaseGameActor::__CanActMainInstancePythonNetworkStreamPhaseGameItem.cpp 的入口顺序修复了可在当前 native 架构中闭合的部分:

  • 新增 item_action_rules.hClassic 与 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 现在保留 highlightEntityStore 生成独立 ItemHighlight 队列;M2Client 在 inventory_changed 前发 item_highlightedInventoryUI 消费该信号并对对应格子做 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_itemextension/src/net/m2_client.cpp:2688)只检查 in-game 和 cell 范围,直接进入 ClassicSession::send_item_usemove_itemdrop_itemuse_item_to_item 也没有同一主角动作门,因而 UI 层 EquipRules 不能覆盖脚本/快捷栏/native 调用入口。
  • RecvItemSetPacket2highlight 是 40250 明确的额外副作用;当前 EntityStore 只产生 inventory_changed,本轮没有发现 item_highlighted 等价事件。该差异不会被 net_entity_test 的最终槽位数据断言捕获。
  • view_equipment_ui_test.gdinventory_ui_test.gditem_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,最后 __RefreshInventoryWindowSetItemCount 自身还触发一次 RefreshInventory。当前 mut_item_update 合并成一次覆盖和一次 inventory_changed,最终槽位数据接近,但监听者看不到同一刷新时序。
  • 40250 的 SendItemUsePacket/SendItemMovePacket/SendItemDropPacket/SendItemUseToItemPacket 统一先经过 __CanActMainInstance;装备移动/使用还检查交易、商店、攻击中并播放使用/丢弃音效。当前 M2Client 只检查 is_in_game、cell/count 宽度并直接调用 ClassicSession/m2dev sessionnative 层没有对应主角行动门、交易/商店/攻击状态或音效副作用。
  • 因此已有槽位、空槽 Update、非法 cell 和 UI 测试不能证明“死亡/攻击/交易/商店期间不发物品包”以及 Set2 高亮行为一致;本轮未修改实现代码,合同继续为 PARTIAL

Implementation round 2026-09-20T19:03Z

本轮按 RecvItemUpdatePacketSetItemCountSetItemMetinSocket/ 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::IsEquipCellCPythonPlayer::IsEquipItemInSlotCItemData::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.gdequip_model_test.gdnetbridge_test.gd 均通过。

仍保持 PARTIALSetItemData 对未知 vnum 的拒绝尚未下沉到 EntityStore,完整 sequence、旧 inventory_changed 监听者的中间快照消费、服务器拒绝回滚以及换图/断线 重建仍未达到 40250 的完整证据标准。

Implementation round 2026-09-21T13:20Z

本轮继续闭合 CPythonPlayer::SetItemData 的资源校验分支:

  • EntityStore 新增可选的 item vnum validatorvalidator 生效后,非零未知 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/IsEquipCellSetItemDataRecvItemSetPacket2/RecvItemUpdatePacketSendItemUse/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/PlayDropSoundSendSequence 的等价资源/事件源。
  • RecvItemUpdatePacket 的 count→socket→attribute 两次可观察快照已适配;旧 inventory_changed 监听者是否都按中间状态消费、空槽 count/socket/attribute 的公开读取语义仍未完全证明。
  • 新装备/龙魂/腰带在 classic 与 m2dev 双协议下的乱序、服务端拒绝/拖拽回滚、断线或换图重建仍缺协议级 fixture。view_equipment_ui_testnetbridge_test 通过,但报告 5 个 ObjectDB 泄漏,因此不能作为干净生命周期证据。

结论:本轮没有新增实现修改;物品主数据和装备判定链可回归,但操作副作用、异常回滚和扩展槽位生命周期仍与 40250 不完全统一,合同保持 PARTIAL

Audit round 2026-09-21T03:48Z — inventory/equipment regression reconciliation

本轮没有重复修改物品实现,而是重新执行 P0 物品链回归并核对上一轮修复的证据边界:

  • net_bounds_testnet_entity_testnet_classic_session_testinventory_ui_test.gdequip_model_test.gditem_tooltip_test.gdview_equipment_ui_test.gd 均返回 0;非法 cell、Set/Update/Del、未知 vnum、装备 分类、Set2 高亮、双阶段 Update 快照、UI 映射和装备模型没有回归。
  • view_equipment_ui_test.gd 仍报告 5 个 ObjectDB 泄漏。功能断言通过,但该结果不能 作为 40250 生命周期/退出清理的干净证据,保留为后续清理项。
  • 对照 40250 PlayUseSound/PlayDropSoundSendSequence、服务器拒绝后的窗口回滚及 新装备/龙魂/腰带乱序边界,当前没有新的等价实现或协议 fixture,因此不升级合同状态。

结论:本轮确认已实现部分保持通过,未发现新的可安全闭合差异;合同继续为 PARTIAL, 下一次物品实现应优先处理音效/sequence 或拒绝回滚,并先补失败回归。

Implementation round 2026-09-21T04:10Z — item action sound parity

本轮按 40250 CPythonItem::__GetUseSoundType__GetDropSoundTypeCPythonNetworkStream::SendItemUsePacketSendItemMovePacket 修复物品操作音效:

  • 新增共享 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 泄漏也尚未清理。