# 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`。