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>
158 lines
16 KiB
Markdown
158 lines
16 KiB
Markdown
# 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` | PARTIAL:native 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`。
|