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

16 KiB
Raw Blame History

item.consumable_action

Scope

审计使用物品、消耗品、物品堆叠/拆分、快捷栏使用、快捷栏服务器同步、物品使用反馈和 NPC 商店出售请求。判断标准不是“本地函数能算出同样的结果”,而是从输入到 CG_* 请求、服务器 GC_* 确认、物品状态更新、点数/效果/UI 刷新的完整链路是否与 40250 一致。

Reference call chain

  • PythonPlayer.cpp / PythonPlayerModule.cpp
    • SetQuickPage 将页面索引按 QUICKSLOT_MAX_LINE 做取模并刷新背包窗口。
    • RequestUseLocalQuickSlot 只接受本地 8 格范围;道具槽转发 SendItemUsePacket(TItemPos(INVENTORY, pos)),技能槽调用 ClickSkillSlot,表情槽调用 BINARY_ActEmotion
    • RequestAddLocalQuickSlotRequestAddToEmptyLocalQuickSlotRequestDeleteGlobalQuickSlotRequestMoveGlobalQuickSlotToLocalQuickSlot 只发请求;内存快捷栏由后续 GC_QUICKSLOT_* 回包修改。
    • MoveItemData 是给 UI/脚本使用的本地槽位交换工具,不等同于服务端物品移动成功。
  • PythonNetworkStreamPhaseGameItem.cpp
    • SendItemUsePacketSendItemUseToItemPacketSendItemMovePacketSendItemDropPacket(New) 先执行 __CanActMainInstance;装备相关路径还执行交易/商店/攻击中门控,必要时播放物品音效,写包后调用 SendSequence
    • SendShopSellPacket / SendShopSellPacketNew 发送 NPC 商店出售请求;出售数量和价格由服务器确认,不由客户端直接增钱或清槽。
    • RecvItemUsePacket 只触发物品窗口刷新;RecvItemUpdatePacket 按 count→socket→attribute 的顺序写入后刷新。
    • RecvQuickSlotAddPacket / RecvQuickSlotDelPacket / RecvQuickSlotMovePacket 先修改本地快捷栏,再刷新物品窗口。
  • Packet.h / PythonNetworkStreamModule.cpp
    • 快捷栏使用、物品使用、物品对物品、物品移动、物品更新和商店出售分别有独立的请求/回包结构,不应由本地数组操作替代。
  • 消耗品具体效果(药水、日月神水、龙神药水、卷轴)主要由 40250 服务端 char_item.cpp 执行;40250 Windows 客户端本身负责发送 ITEM_USE 并消费 GC_ITEM_USEGC_ITEM_UPDATEGC_SPECIAL_EFFECTGC_AFFECT_*GC_POINT_* 等反馈。客户端源码中没有可证明本地独立执行这些服务端效果的等价实现。

Current call chain

  • project/ui/quickbar.gd
    • assign_clear_swap 调用 M2Client::quickslot_add/del/swap;但请求返回后立即修改 _state,真实客户端只有 GC_QUICKSLOT_* 到达 EntityStore 后才会产生权威状态。
    • activate 对 item 槽直接调用 M2Client::use_item(1, cell),对 skill/emote 走各自本地链路;没有统一复现 40250 RequestUseLocalQuickSlot 的本地 8 格检查、服务器回包失败回滚和 RecvItemUsePacket 刷新时序。
    • _page 是 Godot UI 私有状态,set_page 不调用与 40250 对应的 SetQuickPage/统一刷新入口;36 个服务端槽位只显示前 32 个,与参考端可显示页/保留槽的关系尚未由端到端测试证明。
  • extension/src/net/m2_client.cpp / extension/src/net/classic/classic_session.cpp
    • quickslot_add/del/swapuse_itemuse_item_to_itemmove_itemdrop_item 已能构造经典协议包,但 native 层只有游戏阶段、槽位/整数范围检查;没有 40250 的 __CanActMainInstance、装备类型、交易/商店/攻击中门控、物品音效和 SendSequence 语义。
    • shop_sell 已存在并发送服务端出售请求,但 ItemSplitSystem::quick_sell_item 没有使用它。
  • project/consumable_system.gd
    • 本地实现了日月神水 socket 容量、龙神药水 buff 描述和回城/地标卷轴结果字典,但没有被 M2ClientEntityStoreGC_ITEM_USEGC_ITEM_UPDATEGC_AFFECT_*GC_POINT_* 链路调用;它不能证明消耗品已经在游戏中生效。
    • 脚本把服务端效果算法复制到了客户端,却没有处理服务端拒绝、重复包、延迟、断线、跨图和回滚。
  • project/item_split_system.gd
    • split_item 直接修改传入 Array 的 source/destination count 并发本地 signal;没有发送 40250 对应的 SendItemMovePacket,也没有等待两个槽位的 GC_ITEM_* 确认。
    • quick_sell_item 直接增加本地 gold、清空槽位并发 signal;没有调用 M2Client::shop_sell,也没有等待服务器金币/物品回包。因此它不是 40250 NPC 商店出售链路的实现。
  • project/ui/inventory_ui.gd / project/ui/shop_ui.gd
    • 普通拖动、物品对物品和正式商店出售路径已经调用 move_itemuse_item_to_itemshop_sell;这是可复用的正确方向,但与 ItemSplitSystem 形成两条不同的物品权威路径。

Branch matrix

场景 40250 当前端 结论
快捷栏添加/删除/交换 只发 CG 请求,GC 回包后更新本地槽 native 请求正确;quickbar.gd 请求后乐观修改 _state PARTIAL:拒绝/乱序时可能显示未确认状态
快捷栏本地使用 本地槽 <8;道具发 ITEM_USE,技能走 ClickSkillSlot,表情走脚本回调 UI 槽 <8;道具/技能/表情分支存在,但不是统一参考入口,死亡/忙碌/服务器拒绝回滚未形成同一链路 PARTIAL
快捷栏页面 负数/超范围按 4 页取模,并刷新背包窗口 UI 只接受 0..3,自己维护 _page,不复用统一 SetQuickPage 事件源 PARTIAL
普通药水/消耗品 客户端发 CG_ITEM_USE,效果、扣数、点数和特效由服务端回包驱动 可以发 M2Client::use_item,但 ConsumableSystem 未接入反馈链 PARTIAL
日月神水/龙神药水/卷轴 服务端判断 item proto/socket/状态,客户端只显示回包结果 本地静态脚本可计算结果,但没有 item proto、服务器确认、effect/point/warp 回包闭环 PARTIAL
堆叠拆分 通过 CG_ITEM_MOVE 的源/目标/数量请求,服务端决定能否拆分并回包更新 ItemSplitSystem::split_item 直接改 Array,不发网络请求 GAP
快捷出售 通过 CG_SHOP SELL/SELL2 请求,服务器计算价格并回包金币/物品变化 ShopUI 正式路径已调用 shop_sellItemSplitSystem::quick_sell_item 直接加金币清槽 GAP:存在错误的旁路实现
物品对物品使用 发送 ITEM_USE_TO_ITEM,反馈由服务端更新 InventoryUI 已发送 use_item_to_item PARTIALnative action gates/反馈时序尚未对齐
物品使用反馈 GC_ITEM_USE/GC_ITEM_UPDATE/special effect/affect/point 触发对应刷新 item update store 信号存在;未证明所有消耗品反馈和 UI 声音/特效/状态消费 PARTIAL
中断/拒绝/乱序 不成功的服务器请求不应改变权威客户端状态 拆分/快捷出售本地 helper 无拒绝回滚;快捷栏和 UI 部分乐观更新 GAP

Implementation-equivalence matrix

项目 状态 证据/限制
Preconditions PARTIAL 基础 stage/cell/数量检查存在;行动状态、装备类型、交易/商店/攻击中门未在 native 统一执行
Branch structure PARTIAL 快捷栏三类入口和正式商店路径存在;本地消耗品、拆分、快捷出售旁路与参考协议链不一致
Algorithms/formulas PARTIAL 本地消耗品/价格/拆分算法可独立通过单元场景,但 40250 权威算法在服务端,当前本地复制没有被回包约束
State transition order PARTIAL 参考是 request→server→GC update→refresh;当前快捷栏/拆分/快捷出售有 request 后本地立即改变或直接本地改变的分支
Constants/units PARTIAL 快捷栏 36/4×8、item cell 和数量宽度有基础覆盖;消耗品 vnum/affect/socket 只在脚本测试出现,未证明与运行时 proto/服务端一致
Timing/event sources PARTIAL 当前大量依赖 Godot signal/本地函数;没有真实 ITEM_USE→ITEM_UPDATE/AFFECT/POINT/SPECIAL_EFFECT 的顺序 fixture
Resource/data sources PARTIAL 正式 UI 可从 item_proto/item_list 获取数据;ConsumableSystem 使用硬编码 vnum/容量,ItemSplitSystem 使用默认价格,均未从 40250 权威资源和服务器结果取值
Protocol side effects PARTIAL native 物品、快捷栏、shop 包结构已定位;action gate、音效、sequence、失败通知和对应 GC 反馈未完整闭合
Interruption/failure/cleanup GAP 本地拆分/快捷出售没有服务端失败路径;快捷栏乐观状态、断线、换图和延迟 GC 期间的回滚/清理尚未证明

Regression evidence

  • project/test_consumables_parity.gd:36/36 通过,覆盖本地识别、socket 容量、恢复计算、buff 描述和卷轴结果字典;它没有启动 M2Client、服务器或 GC feedback。
  • project/test_item_split_parity.gd:19/19 通过,覆盖本地拆分、直接加金币的快捷出售和丢弃提示;由于测试正是调用本地 Array/Dictionary,它不能作为网络等价证据。
  • project/test_taskbar_quickslot_parity.gd:34/34 通过,覆盖 UI 页面、经验球和按钮状态;退出时仍有 91 个 CanvasItem RID、221 个 ObjectDB 对象和资源泄漏警告。
  • project/complex_item_drop_test.gd:退出失败,购买模式下“卖出目标应拒绝拖放”的检查失败;这说明物品窗口/商店目标的交互合同仍有回归问题。
  • project/netplay_test.gd:通过,但只验证 NetPlay 主循环,不覆盖消耗品或快捷出售服务器确认。
  • 需要新增的回归覆盖:快捷栏 GC add/del/swap 拒绝回滚;ITEM_USE 后无效/成功的 item update、affect、point、special effect 顺序;拆分请求失败不改本地状态;SELL2 服务器价格/金币/物品回包;断线/换图/重复回包和请求乱序。

Deep audit round 2026-09-20

Fresh regression results

  • godot --headless --path project --script test_consumables_parity.gd:退出码 0,36/36 通过;仍然只是本地识别、socket、恢复计算、buff 描述和卷轴结果字典测试,没有 ITEM_USE/GC_ITEM_USE/GC_ITEM_UPDATE 服务器回路。
  • godot --headless --path project --script test_item_split_parity.gd:退出码 0,19/19 通过;测试直接检查 Array/Dictionary 的本地拆分、加金币和清槽,不能证明 CG_ITEM_MOVE/CG_SHOP SELL2 的服务器确认语义。
  • godot --headless --path project --script test_taskbar_quickslot_parity.gd:退出码 0,34/34 通过;退出时报告 91 个 CanvasItem RID、221 个 ObjectDB 实例、3 个资源和文本 RID 泄漏,因此只计局部 UI PASS,不计干净生命周期 PASS。
  • godot --headless --path project --script complex_item_drop_test.gd:退出码 1;购买模式下“卖出目标应拒绝拖放”的检查失败,说明商店目标的模式门仍有回归。
  • godot --headless --path project --script netplay_test.gd:退出码 0;只覆盖 NetPlay 主循环,未覆盖消耗品、快捷栏或商店服务器确认。

Static parity findings

  • 40250 的快捷栏添加/删除/交换只发送请求,GC_QUICKSLOT_* 到达后才修改权威槽位;当前 Quickbar::assign/_clear/_swap 在 native 请求返回后立即写 _state,拒绝、重复或乱序回包可能留下未确认 UI 状态。
  • 40250 的 SetQuickPage 对页码按 QUICKSLOT_MAX_LINE 取模并触发统一刷新;当前 Quickbar 只接受 0..3、维护私有 _page,没有统一 page 状态源和参考端刷新入口。
  • 40250 的物品使用、移动、物品对物品和出售发送前都经过 __CanActMainInstance,装备路径还检查交易/商店/攻击状态、播放物品音效并发送 sequence;当前 native 仅检查游戏阶段、槽位和整数范围。
  • ConsumableSystem 的本地效果函数没有接入 EntityStore 的 item/affect/point/special-effect 消费链;它不能替代服务端效果,也没有拒绝、重复、断线、跨图和回滚状态机。
  • ItemSplitSystem::split_item 直接修改源/目标数组,没有发 CG_ITEM_MOVE 并等待两个槽位的 GC_ITEM_*quick_sell_item 直接加本地金币并清槽,没有统一走 M2Client::shop_sell 和服务器价格回包。
  • InventoryUI/ShopUI 已存在走 native 请求的正式路径,但与上述本地旁路并存,导致同一功能存在两套权威来源。

Round conclusion

本轮仍为 PARTIAL:协议包的基础构造存在,局部 UI 和本地算法测试通过,但快捷栏确认时序、消耗品反馈闭环、堆叠拆分、快捷出售和失败清理没有与 40250 统一。未修改实现代码。

Implementation round 2026-09-21T13:45Z

本轮修复了合同中两个会绕过 40250 服务端权威状态的实际旁路:

  • ItemSplitSystem::split_item 现在只做源/目标/数量的前置验证,然后调用 M2Client::move_item(source, target, count);请求成功返回 pending,不再提前改 source/destination 的本地 count,也不再提前发“已拆分”完成信号。
  • ItemSplitSystem::quick_sell_item 现在只调用 shop_sell(cell, count),不再读取 本地 sale_price、增加金币或清空物品;服务端后续 GC_ITEM_UPDATE/GC_POINT_GOLD 才是唯一完成条件,新增 request 信号区分“已请求”和“已出售”。
  • ShopUI::_drop_to_sell 在购买模式下现在直接拒绝拖入出售目标,不发送 CG_SHOP SELL/SELL2;只有切换到出售模式后才进入原有 anti-sell、确认框和发包路径。
  • 回归测试改为记录 CG_ITEM_MOVE / CG_SHOP SELL2 请求并断言本地数组、金币在 服务器确认前保持不变;test_item_split_parity 20/20、complex_item_drop_testshop_rules_test 均通过。

仍保持 PARTIAL:快捷栏 GC add/del/swap 的拒绝回滚、消耗品真实 ITEM_USE→ITEM_UPDATE/AFFECT/POINT/SPECIAL_EFFECT 包序、断线/换图/重复回包清理, 以及物品音效和完整 sequence 证据仍需后续轮次。

Implementation round 2026-09-21T14:15Z

本轮按 40250 CPythonPlayer 的快捷栏状态时序修复了另一条乐观更新旁路:

  • Quickbar::assign_clear_swap 在线且 native 请求成功后不再直接写 _state 它们只发送 CG_QUICKSLOT_ADD/DEL/SWAP,等待 EntityStore 消费对应的 GC_QUICKSLOT_* 并发出 quickslots_changed 后,由 restore_from_server() 重建界面。 因此服务器拒绝、延迟或乱序回包不会被一次“发送成功”伪装成已确认状态。
  • Quickbar::set_page 改为按 QUICKSLOT_MAX_LINE=4 取模,-14 分别回到第 4 页和第 1 页, 与 40250 SetQuickPage 的页面语义一致;本地仍保留 36 个服务端槽、显示 4×8 槽。
  • 现有测试夹具改为显式模拟 GC_QUICKSLOT_* 后才更新服务器快照,并新增延迟确认断言; test_skill_inventory_parity 45 项、skill_testmobile_gesture_test 和任务栏分页回归通过。

仍保持 PARTIAL:快捷栏使用本身仍需补统一 RequestUseLocalQuickSlot 的失败反馈/动作门, 消耗品 ITEM_USE→ITEM_UPDATE/AFFECT/POINT/SPECIAL_EFFECT 的真实包序、断线/换图清理, 以及物品音效和完整 SendSequence 证据仍未闭合。

Implementation round 2026-09-21T04:10Z — shared item sound source

本轮把消耗品/快捷使用路径依赖的物品音效分支统一到 native M2Client

  • item_action_rules.h 复刻 40250 __GetUseSoundType/__GetDropSoundType 的 type/subtype 分支。
  • M2Client::use_itemmove_item 在行动门通过后、构造 transport 包前发出 item_action_soundGameScene 统一交给 Audio 播放。普通 USE_POTION 保留参考端的无 use sound 行为。
  • item_action_sound_parity_test.gdnet_bounds_test 覆盖弓/箭/防具/首饰/能力提升药/普通药水/符咒/未知 vnum。

快捷栏的实际服务端确认、完整 SendSequence 包序、服务器拒绝反馈和断线重建仍保持 PARTIAL

Status

PARTIAL。快捷栏增删/交换现在遵循请求→GC_QUICKSLOT_*→重建显示的权威时序,分页也已按 4 页取模; 但快捷栏使用的统一入口、消耗品反馈闭环、真实回包 fixture、音效/sequence 和跨生命周期清理仍未闭合, 因此不能把整个合同标记为 40250 等价。