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

13 KiB
Raw Blame History

npc.dialog_shop

Scope

审计 NPC 点击与脚本对话、普通 NPC 商店、扩展多货架商店、购买/出售、商店错误和结束清理,以及个人商店招牌、点击打开、摆摊编辑和关闭流程。判断标准是当前端是否复现 40250 的服务端货架数据来源、全局槽位索引、NPC/玩家商店分支、动作门控、请求包副作用和失败清理,而不是本地 helper 是否能算出一个价格或库存结果。

Reference call chain

  • UserInterface/PythonNetworkStreamPhaseGame.cpp
    • RecvShopPacket 处理 STARTSTART_EXENDUPDATE_ITEMUPDATE_PRICE 与错误子包;普通商店固定读取 40 个槽位,多货架按 tab 读取每个货架的名称、货币类型和 40 个槽位。
    • CPythonShop::SetItemData(globalIndex, item) 将增量更新的全局位置映射为 tabIdx = globalIndex / SHOP_HOST_ITEM_MAX_NUMslot = globalIndex % SHOP_HOST_ITEM_MAX_NUM,因此多货架位置不是永远落在 tab 0。
    • SendOnClickPacket 发送 NPC/个人商店点击请求并追加 sequence;脚本回包通过事件管理器打开对话窗口。
    • RecvShopSignPacket 将玩家商店招牌绑定到已有角色实体;个人商店广告牌点击后由 uiprivateshopbuilder.py 发送 SendOnClickPacket(vid)
  • UserInterface/PythonNetworkStreamPhaseGameItem.cpp
    • SendShopEndPacketSendShopBuyPacketSendShopSellPacketSendShopSellPacketNew 都先经过 __CanActMainInstance(),再写 CG_SHOP 子头、槽位/数量并 SendSequence()
    • 购买包使用数量 1 加显示位置;出售分旧版单槽包和新版槽位+数量包。结果、金币、背包和售罄状态由服务器回包更新。
  • UserInterface/PythonShop.cppClient/Eternexus/root/uishop.py
    • CPythonShop 保存所有 tab 和全部 40 个位置,普通、NPC 扩展、多货架和个人商店由窗口状态区分。
    • ShopDialog.Open(vid) 根据 NPC、他人玩家和主角自己的商店分别显示买卖、货架或关闭控件;自己商店不能走买卖分支。
    • 左键购买有确认,右键/取消选中可直接购买;出售检查反出售标记、贵重物品确认并发送服务端请求。
  • Client/Eternexus/root/uiprivateshopbuilder.pyPythonShop.cppPacket.h
    • 摆摊最多 39 个商品,商品按展示位置排序,源物品位置和价格由服务端消费;标题有长度限制,禁止状态和 anti flag 在提交前检查。
    • BuildPrivateShop 发送 CG_MYSHOP,关闭自己的摊位也使用相应的个人商店协议语义。
  • PythonNonPlayer.cppPythonPlayerInput.cppPythonPlayerInputMouse.cpp
    • NPC/商店点击由实体类型、可点击状态和距离/输入流程进入 SendOnClickPacket;个人商店广告牌是可点击的独立入口,不等同于普通 PC 攻击或选中。

Current call chain

  • project/net_play.gd::_on_pick 对 NPC/WARP 走 click_npc,有距离预约和同 VID 节流;但没有对带 shop_sign 的 PC 建立 40250 广告牌点击分支,当前 project/net_world.gd::_refresh_shop_sign 只创建显示用 Label3D
  • project/ui/quest_dialog.gd 和网络事件可以承接脚本/对话显示;extension/src/net/m2_client.cpp::click_npcclassic_session.cpp::send_click_npc 能发送基础点击包。
  • extension/src/net/entity_store.cpp 已解析普通 START、扩展 START_EXEND、更新和常见错误,并保存商店 VID、tab 和 item;但 mut_shop_update_item 拒绝 pos >= 40,且多 tab 时只更新 tab 0,未复现 40250 的全局位置映射。
  • project/ui/shop_ui.gd 有买/卖模式、40 格、多货架、确认框、距离关闭和右键购买;但是 _drop_to_sell 在购买模式下仍继续执行出售请求,当前 project/complex_item_drop_test.gd 已复现该失败。
  • ShopUI.refresh 没有根据 shop.vid 区分主角自己的个人商店、他人个人商店和 NPC 商店,因此当前买/卖按钮状态不等价于 ShopDialog.Open
  • project/npc_shop_system.gd 使用硬编码 NPC 库存与本地 buy_item/sell_item 直接修改金币和背包;它不是 CPythonShop 的服务器货架回包链,且目前主要由孤立 parity 测试使用。
  • project/private_shop_system.gd 同样是本地 stock/价格 helperlive project/ui/private_shop_ui.gd 能提交 M2Client.open_private_shop,但未在 live 提交前统一调用死亡、骑乘、战斗状态和 50200 摆摊道具校验。
  • M2Client::shop_buyshop_sell 只做 in-game、u8 和数量边界,缺少 40250 的 __CanActMainInstance、实际物品/anti flag/商店状态门控;open_private_shop 只验证字段 wire bounds 与最多 39 条。
  • classic_session.cpp 的买卖、关闭和个人商店包布局及 sequence 基础存在;现有 native 测试验证布局,但没有真实 GC_SHOP 多阶段包和失败回包驱动的 UI 状态测试。

Branch matrix

场景 40250 当前端 结论
NPC 点击/脚本对话 实体输入、距离和 SendOnClickPacket 后由服务器脚本回包驱动 NPC 点击、距离预约、节流和基础点击存在 PARTIAL:事件/脚本结果与异常清理缺真实包覆盖
普通商店 START 服务器提供 VID 和固定 40 格,空槽也有确定位置 能解析并保存非空 item,UI 补 40 格 PARTIAL:空槽原始状态和真实 UI 回包顺序未证明
START_EX 多货架 每 tab 40 格,更新位置是全局 tab*40+slot 初始 tab 能显示;增量更新限制 <40 且只更新 tab 0 GAP
商品更新/售罄/错误 SetItemData、价格更新、错误事件和 UI 刷新 基础错误字符串和刷新存在 PARTIAL:更新索引和回包失败后 UI/金币/库存清理不足
买入 __CanActMainInstance→CG_SHOP/BUY→sequence→服务器结果 UI/native 能发买包并有确认 PARTIAL:缺动作门控、pos 全范围和服务器结果闭环
卖出/批量卖出 anti-sell/贵重确认后旧 SELL 或 SELL2,服务端扣物品加金币 UI 有部分 anti flag/确认;native 只做 u8 边界 PARTIAL:购买模式拖入仍可出售,实际库存/服务端回包未闭环
自己的个人商店 隐藏买卖,只显示关闭;个人商店协议关闭自己的摊位 ShopUI 主要按普通买卖模式显示,未按 shop VID 统一区分 GAP
他人个人商店/招牌 广告牌点击发送 SendOnClickPacket(vid) 后打开玩家商店 招牌目前只有 Label3DPC 点击未路由到点击商店 GAP
摆摊编辑/提交 39 条、标题/禁止状态/anti flag/源物品校验后 CG_MYSHOP UI/Native 字段和数量限制存在 PARTIAL:live 状态校验和服务端消费/拒绝反馈不足
关闭/离开/断线 END 清理访问商店;个人商店关闭遵循独立协议语义 基础 close 和本地 UI 关闭存在 PARTIAL:访问商店、自己的摆摊和断线/乱序清理未统一验证

Implementation-equivalence matrix

项目 状态 证据/限制
Preconditions PARTIAL NPC 距离和部分 UI anti flag 存在;native 买卖缺 __CanActMainInstance、真实物品/商店状态和动作门控,摆摊 live 缺死亡/骑乘/战斗及 bundle 校验
Branch structure PARTIAL 普通商店、多货架、购买确认和脚本对话分支存在;自己的个人商店、玩家招牌点击和购买模式拖放分支不等价
Algorithms/formulas PARTIAL 货架槽位和价格由协议保存;硬编码 NPC stock、本地买卖 helper 和本地出售价格不是 40250 的服务端权威实现
State transition order GAP 参考是请求→服务端判定→item/gold/shop update/error→UI 刷新;本地 helper 可直接改库存/金币,且 UI 购买模式错误放行出售
Constants/units PARTIAL 40 格、最多 3 tab、最多 39 条、标题/招牌和字段 wire bounds 基本存在;全局多 tab 更新范围未实现,价格/数量服务端约束未闭合
Timing/event sources PARTIAL 点击节流、距离关闭、确认框和 shop signal 存在;真实 START→UPDATE→ERROR/END 时序、招牌点击和重复请求未覆盖
Resource/data sources GAP 40250 商品库存来自服务器 CPythonShop/item data;当前 NpcShopSystem 额外维护硬编码本地库存,和 live authority 不是同一数据源
Protocol side effects PARTIAL CG_SHOP、CG_ON_CLICK、CG_MYSHOP 的基础布局和 sequence 存在;动作门控、个人招牌点击和完整错误/结果副作用不完整
Interruption/failure/cleanup GAP 购买模式拖放泄漏到出售;多 tab 更新可能落错/丢失;断线、拒绝、售罄、自己摊位关闭和未找到实体的招牌清理没有统一端到端回滚

Regression evidence

  • project/test_npc_shop_parity.gd26/26 通过,但主要验证硬编码 NpcShopSystem 的本地库存、金币和背包变化,不能作为 40250 服务器商店等价证明。
  • project/test_npc_shop_flow.gd:NPC 点击距离/节流、UI 多 tab/右键购买和距离关闭通过;使用 fake client,未覆盖真实 GC shop 包。
  • project/test_npc_dialog_shop_ui_parity.gd:窗口尺寸、格子、按钮、脚本对话与 fake client 通过;没有多 tab 全局更新和 PC 招牌点击。
  • project/private_shop_ui_test.gdproject/test_private_shop_bundle_parity.gdproject/shop_rules_test.gdproject/p8_test.gd:本地摆摊编辑与规则通过;未证明 live UI 的状态门控、CG_MYSHOP 服务端拒绝/关闭和招牌打开。
  • project/complex_item_drop_test.gd:当前有 1 项失败,购买模式仍允许把背包物品拖到商店出售目标;这是可复现的实现回归。
  • build/extension/net_classic_session_testbuild/extension/net_entity_test:基础 packet/store 行为通过,但没有 START_EX 位置 40..119 的更新回归,也没有完整错误/结束/重新打开序列。

Deep audit round 2026-09-21

Fresh regression results

  • godot --headless --path project --script test_npc_shop_parity.gd:退出码 0,26/26 通过;覆盖的是 NpcShopSystem 的本地库存、价格、金币和背包 helper。
  • godot --headless --path project --script test_npc_shop_flow.gd:退出码 0,38 项通过;覆盖 NPC 距离/预约/节流、fake shop 多货架 UI、右键购买和距离关闭,没有真实 GC_SHOP START_EX/UPDATE/ERROR/END 包。
  • godot --headless --path project --script test_npc_dialog_shop_ui_parity.gd:退出码 0;窗口、40 格、买卖按钮、脚本对话和 QuestCurtain 通过,仍使用 fake client。
  • godot --headless --path project --script private_shop_ui_test.gdtest_private_shop_bundle_parity.gdshop_rules_test.gdp8_test.gd:均退出码 0;本地摆摊规则、bundle、价格和社交窗口通过,不等价于真实 CG_MYSHOP/个人商店招牌/失败回包。
  • godot --headless --path project --script complex_item_drop_test.gd:退出码 1;购买模式下卖出目标仍错误接受背包物品拖放。
  • build/extension/net_classic_session_testbuild/extension/net_entity_test:退出码 0;基础包/store 行为通过,但没有覆盖多货架全局位置 40..119 的增量更新。

Static parity findings

  • 40250 START_EX 的每个 tab 固定 40 格,UPDATE_ITEM 使用全局位置 tab * 40 + slot;当前 EntityStore::mut_shop_update_item 仍要求 pos < 40,并且只更新 m_shop_tabs[0],所以 tab 1+ 的增量回包会被丢失或落错货架。
  • ShopUI::_drop_to_sell 没有以 _mode == 2 作为硬门,购买模式拖拽仍能调用 _request_sell;这与 ShopDialog 的买/卖分支不一致,也是本轮失败测试的直接原因。
  • 40250 ShopDialog.Open(vid) 按 NPC、他人个人商店和自己的个人商店分支显示;当前 ShopUI 主要按通用买卖窗口处理,未以 shop_vid 和主角 VID 做统一分支,自己的摊位与他人摊位行为仍可能混用。
  • shop_sign 在 native/entity store 中有保存和显示数据,但 NetPlay::_on_pick 没有完整复刻 SendOnClickPacket(shop_vid) 的个人商店广告牌分支;显示招牌不等于可打开玩家商店。
  • NpcShopSystemPrivateShopSystem 等本地 helper 可以直接修改金币、库存和 stocklive UI 的 native 请求路径存在,但两套权威来源并存,测试通过不能证明服务端回包覆盖本地结果。
  • M2Client::shop_buy/shop_sell/shop_close/open_private_shop 的基础 wire bounds 存在,但没有统一 __CanActMainInstance、商店状态、实际 item/anti-flag 和重复/失败请求门控;SendSequence/错误/售罄/重开顺序也未由真实 fixture 验证。

Round conclusion

本轮仍为 PARTIAL:NPC 点击、普通商店 UI、基础买卖/摆摊包和局部规则测试可运行,但多货架增量索引、购买模式拖放门、个人商店招牌/自有摊位分支和服务器权威回包仍未统一;本轮未修改实现代码。

Status

PARTIAL。NPC 点击、基础对话承接、普通/扩展商店协议骨架、购买出售 UI 和个人商店字段已经存在,但当前端仍有明确的多 tab 更新索引错误、购买模式错误放行出售、个人商店招牌不可点击打开、自己商店状态不分支、native 动作门控缺失以及本地硬编码库存/金币旁路。已完成审计并记录,未修改实现代码。