Files
mtgodot-poc/audit/contracts/item.consumable-action.md
T
shenleiandClaude Opus 5 a989b8f19b chore: delete 106 unreachable *_system.gd and their parity tests
These files were loaded only by their own test_*_parity.gd and never by
the runtime (transitively checked, incl. client_phase_integration_system).
Removes 76 manifest implementation/evidence references across 18
contracts; contract docs get a note that conclusions based on them are
void. Live systems kept: chest, consumable, item_attr, metin_socket,
whisper_chat.

Full headless suite: no new failures (7 failing + 5 headless-hanging
render tests are identical before the deletion).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 17:51:52 +09:00

158 lines
16 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.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`
- `RequestAddLocalQuickSlot``RequestAddToEmptyLocalQuickSlot``RequestDeleteGlobalQuickSlot``RequestMoveGlobalQuickSlotToLocalQuickSlot` 只发请求;内存快捷栏由后续 `GC_QUICKSLOT_*` 回包修改。
- `MoveItemData` 是给 UI/脚本使用的本地槽位交换工具,不等同于服务端物品移动成功。
- `PythonNetworkStreamPhaseGameItem.cpp`
- `SendItemUsePacket``SendItemUseToItemPacket``SendItemMovePacket``SendItemDropPacket(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_USE``GC_ITEM_UPDATE``GC_SPECIAL_EFFECT``GC_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/swap``use_item``use_item_to_item``move_item``drop_item` 已能构造经典协议包,但 native 层只有游戏阶段、槽位/整数范围检查;没有 40250 的 `__CanActMainInstance`、装备类型、交易/商店/攻击中门控、物品音效和 `SendSequence` 语义。
- `shop_sell` 已存在并发送服务端出售请求,但 `ItemSplitSystem::quick_sell_item` 没有使用它。
- `project/consumable_system.gd`
- 本地实现了日月神水 socket 容量、龙神药水 buff 描述和回城/地标卷轴结果字典,但没有被 `M2Client``EntityStore``GC_ITEM_USE``GC_ITEM_UPDATE``GC_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_item``use_item_to_item``shop_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_sell``ItemSplitSystem::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_test`
`shop_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` 取模,`-1``4` 分别回到第 4 页和第 1 页,
与 40250 `SetQuickPage` 的页面语义一致;本地仍保留 36 个服务端槽、显示 4×8 槽。
- 现有测试夹具改为显式模拟 `GC_QUICKSLOT_*` 后才更新服务器快照,并新增延迟确认断言;
`test_skill_inventory_parity` 45 项、`skill_test``mobile_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_item``move_item` 在行动门通过后、构造 transport 包前发出 `item_action_sound`GameScene 统一交给 Audio 播放。普通 `USE_POTION` 保留参考端的无 use sound 行为。
- `item_action_sound_parity_test.gd``net_bounds_test` 覆盖弓/箭/防具/首饰/能力提升药/普通药水/符咒/未知 vnum。
快捷栏的实际服务端确认、完整 `SendSequence` 包序、服务器拒绝反馈和断线重建仍保持 `PARTIAL`
## Status
`PARTIAL`。快捷栏增删/交换现在遵循请求→`GC_QUICKSLOT_*`→重建显示的权威时序,分页也已按 4 页取模;
但快捷栏使用的统一入口、消耗品反馈闭环、真实回包 fixture、音效/sequence 和跨生命周期清理仍未闭合,
因此不能把整个合同标记为 40250 等价。
### 2026-09-22 死代码删除
以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`item_split_system.gd`, `test_item_split_parity.gd`