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

17 KiB
Raw Blame History

network.character_select.create_delete

Scope

对比 40250 角色列表、固定槽位、帝国选择、角色选择、创建、删除、强制改名和 DirectEnter 的完整网络调用链、固定字段、阶段副作用、回调顺序和失败清理。

本轮是静态审计、阶段副作用核对和现有测试复核;角色选择主路径已有多轮实现修复,合同仍为 PARTIAL

Reference call chain

普通登录到角色列表

  1. CPythonNetworkStream::SetLoginPhase()UserInterface/PythonNetworkStreamPhaseLogin.cpp 发送 CG_LOGIN/CG_LOGIN2,随后 ClearLoginInfo() 清除密码。
  2. CPythonNetworkStream::LoginPhase() 接收 HEADER_GC_LOGIN_SUCCESS3/4HEADER_GC_EMPIRE、创建/删除/改名结果。
  3. __RecvLoginSuccessPacket3/4() 把数据写入固定的 m_akSimplePlayerInfo[PLAYER_PER_ACCOUNT4] 和对应 guild 数组;3 槽报文只覆盖前 3 个槽,未覆盖的第 4 槽仍是已清零的空槽。
  4. RecvPhasePacket() -> SetSelectPhase();普通路径根据 IsSelectedEmpire() 进入选人或选帝国窗口。
  5. SelectPhase() 分派创建、删除、改名和再次登录成功包;UI 回调读取同一份固定槽位数据。

普通选人/创建/删除/改名

  • SendSelectEmpirePacket() 写入 CG_EMPIRE,发送成功后立即 SetEmpireID(dwEmpireID),再 SendSequence()
  • SendSelectCharacterPacket() 写入 CG_PLAYER_SELECT,再 SendSequence()
  • SendCreateCharacterPacket() 写入 slot/name/job/shape/CON/INT/STR/DEX 固定字段,再 SendSequence()
  • SendDestroyCharacterPacket()strncpy(..., PRIVATE_CODE_LENGTH-1) 写入删除码,再 SendSequence()
  • SendChangeNamePacket() 写入 slot/name,再 SendSequence()
  • __RecvPlayerCreateSuccessPacket()< PLAYER_PER_ACCOUNT4 时更新固定槽位并调用 OnCreateSuccess();越界只记录错误并返回,不产生成功回调。
  • __RecvPlayerCreateFailurePacket() 同时通知创建窗口和选人窗口。
  • __RecvPlayerDestroySuccessPacket() 清零整个槽位,并独立清零 guild id/name,然后调用 OnDeleteSuccess(index)
  • __RecvPlayerDestroyFailurePacket() 消费空包并调用 OnDeleteFailure()
  • __RecvChangeName() 只在 PID 命中固定槽位时清除 bChangeName、更新名称并调用 OnChangeName(index,name)PID 不命中时调用创建失败码 100

DirectEnter

  1. Python net.DirectEnter(slot) -> CPythonNetworkStream::ConnectGameServer(slot)
  2. ConnectGameServer() 校验 < PLAYER_PER_ACCOUNT4,保存所选槽位,设置 __DirectEnterMode,按该槽的 lAddr:wPort 建立新的 game 连接。
  3. 新连接握手和 SetLoginPhase() 仍发送带 login key 的 CG_LOGIN2,不是普通选人窗口路径;登录信息随后被清除。
  4. SetSelectPhase() 在 DirectEnter 模式只切换到 Loading UI,不调用普通 SendSelectCharacterPacket()DirectEnter 标记在 SetLoadingPhase() 中初始化清除。
  5. GC_WARP 也通过 __DirectEnterMode_Set(m_dwSelectedCharacterIndex) 后连接目标 lAddr:wPort 复用这条路径。

Current call chain

  • extension/src/net/classic/classic_session.cpp::on_phase()select_char()create_character()delete_character()change_name()send_empire()connect_direct_enter()
  • extension/src/net/classic/classic_parser.cpp::parse_login_success()on_gc()
  • extension/src/net/classic/wire_classic.h 的固定包结构和 is_sequence_cg()
  • extension/src/net/m2_client.cpp::select_character()enter_game()、CRUD 转发、pump_classic() 事件分派和 build_char_list()
  • project/app_flow.gd::_on_char_list()_build_char_list()_enter_character()_forward_char_evt()
  • project/ui/char_select_screen.gd::_pad_slots()_do_start()、创建/删除/改名回调。

Branch-by-branch comparison

场景 40250 参考行为 当前实现 结论
4 槽登录成功 写入 4 个固定槽位,空槽仍有 index 2/3 和全字段零值 parse_login_success(..., n=4) 重建 4 个 CharSlot;主路径等价 MAPPED
3 槽登录成功 写入固定 4 槽数组的前 3 项,第 4 项保持清零空槽 parse_login_success(..., PLAYER_PER_ACCOUNT3) 仍分配 4 个零初始化槽,slot_count()/build_char_list() 暴露 4 槽 MAPPED
空槽建号 固定槽位 UI 允许空槽,创建成功写回该槽 4 槽数组和 UI 补槽均保留空槽 index,创建回包按 slot 写回 MAPPED;真实服务端异常仍缺包序证据
帝国选择 CG_EMPIRE 发送成功后立即本地写入 empire,再发 sequence send_empire() 只发包;当前 m_empire 只有收到 GC_EMPIRE 后才改变 GAP:本地状态写入时点不同
普通选人 UI 调用 SendSelectCharacterPacket,包后有 sequence select_char()CharSelect 且 slot 在当前 vector 范围内时发送固定包;stream 自动追加 sequence MAPPED,重复触发/空槽语义缺少测试
创建请求 固定 name/job/shape/四维字段,发 sequence;参考发送函数本身不做 slot 空闲判定;中文名称按 GB2312 字节字段发送 当前做 stage、slot、name 和整数范围校验,先将 UTF-8 名称转成 GB2312/GBK,再按 24 字节 wire cap 填包 MAPPED:中文 40250 字节容量已对齐,前置校验/真实包序仍需继续证明
创建成功 <4 才更新固定槽并回调;越界只消费包,不产生成功回调 account_slot >= m_slots.size() 只消费包并返回;有效槽才更新并排入 CreateOk MAPPED
创建失败 同时通知创建窗口和选人窗口 一个 char_create_failed 信号由当前选人页转发 PARTIAL:单窗口适配存在,但缺少双接收者/顺序证明
删除请求 删除码最多复制 7 个字节,固定包后发 sequence strncpy(..., PRIVATE_CODE_LENGTH-1),零初始化尾部后发送 MAPPED:删除码边界已对齐
删除成功 清零槽位和 guild id/name 后回调 slot CharSlot{} 清空所有字段后排入 DeleteOk;有效 4 槽时等价 MAPPED3 槽第 4 槽和异常 slot 未覆盖
删除失败 消费空包并回调 OnDeleteFailure() 解析为 DeleteFail,由 UI 清空输入框 MAPPED,需补固定包/连续包测试
改名成功 按 PID 命中槽位后清除强制改名并回调 index/name 按 PID 更新槽位并排入 name eventUI 再发整表和 PID/name PARTIALevent 适配存在,index 语义和顺序未完全保持
改名 PID 不命中 调用创建失败码 100 未命中时排入 CreateFail(100),不产生 name event MAPPED
DirectEnter 目标连接 设置 DirectEnter,连接角色槽 lAddr:wPort,重新走 CG_LOGIN2DirectEnter 的 select 阶段不发普通选择包 连接槽地址并保留 login key;在 PHASE_SELECT 中只切 Loading,不发送 CG_PLAYER_SELECT MAPPED 主分支;真实服务器时序仍未证明
DirectEnter 重复进入 参考 API 通过单一网络流状态和 UI phase 控制 当前 enter_game()/connect_direct_enter() 没有 native epoch/重复请求保护 PARTIAL
旧数据清理 登录阶段先清固定角色/guild 数组;离线/Loading 也清 DirectEnter 标记 新的 ClassicSession 通常从空 parser 开始;复用/失败/断线时没有同等固定槽和事件队列清理合同 PARTIAL

Equivalence matrix

项目 状态 证据
Preconditions PARTIAL 当前有 stage/slot/name/range 校验,但 40250 的固定 4 槽、空槽、DirectEnter 和重复调用边界未完全一致
Branch structure PARTIAL CRUD 主分支存在;3 槽固定数组、未命中改名、DirectEnter select 分支不同
Algorithms/formulas PARTIAL 包字段和 sequence 基本一致;classic 固定名称现在按 UTF-8 -> GB2312/GBK 转换后的字节截断,非中文 locale 和完整异常矩阵仍未闭合
State transition order PARTIAL 帝国本地写入时点、创建/删除/改名事件和 DirectEnter 选择包顺序不同
Constants/units PARTIAL 私码 8 字节、角色名 24 GB2312 字节 cap、3/4 槽暴露规则已定位;其它 locale 和异常边界仍未完全证明
Timing/event sources PARTIAL 参考 phase/window callback;当前为 parser queue -> M2Client signal -> Godot UI,未证明所有成功/失败顺序等价
Resource/data sources MAPPED 角色槽、guild、login key、角色 game 地址均已定位到对应报文/状态来源
Protocol side effects PARTIAL 普通 CRUD 包和 sequence 有;帝国本地副作用、越界响应和 DirectEnter 额外 CG_PLAYER_SELECT 不同
Interruption/failure/cleanup PARTIAL 创建/删除/改名异常、连接中断、重复进入和角色实例管理器生命周期缺少完整自动测试

Existing test evidence

通过的现有回归:

  • extension/tests/net_classic_session_test.cpp / build/extension/net_classic_session_test:4 槽成功列表、强制改名成功、创建失败包兼容、删除码 7 字节边界、普通选人、槽地址连接。
  • 同一测试还覆盖 legacy 3 槽保持第 4 个空槽、DirectEnter SelectPhase 不发送普通选择包、未知改名 PID 的 CreateFail(100),以及 Select phase 外角色回包被拒绝。
  • extension/tests/net_loopback_test.cpp / build/extension/net_loopback_test:创建/删除/改名的经典 loopback 主路径。
  • project/char_create_delete_test.gd:Godot 选人页 UI 创建、删除、改名和帝国按钮流程。
  • project/test_intro_select_parity.gd:选人页及删除码 UI 的既有测试。

这些测试没有覆盖:3 槽登录后的固定第 4 槽、空槽 slot 3 的创建/删除结果、非中文 locale codepage、未命中 PID 的改名错误码、DirectEnter 在 PHASE_SELECT 的包序列、重复进入和中断清理。因此通过结果不能提升合同状态。

Deep audit round 2026-09-20

本轮重跑角色选择/CRUD/loopback 回归,并把 native 代码中的 DirectEnter 与固定槽位行为重新对照:

  • char_create_delete_test.gdtest_intro_select_parity.gdnet_classic_session_testnet_loopback_test 均退出码 0;它们证明当前 4 槽主路径、UI 创建/删除/改名、普通选人和 loopback CRUD 没有回归。
  • ClassicParser::parse_login_success() 仍按报文 n 清空并创建 vector;legacy 3 槽报文因此不会保留 reference 固定 4 槽数组中的 slot 3 空项。当前 UI 测试使用的是补齐后的 4 槽 fixture,不能覆盖真实 3 槽包。
  • ClassicSession::on_phase(PHASE_SELECT)m_direct_enter 时先切 Loading,但仍调用 send_select_char(m_direct_enter_slot);参考 DirectEnter 的 Select phase 只切 Loading,不发送普通 CG_PLAYER_SELECT,所以当前多发包差异仍存在。
  • 创建成功即使 account_slot >= m_slots.size() 也会排入 CreateOk;改名 PID 未命中时当前只排入 name event,不生成参考创建失败码 100。帝国选择也没有参考端“发送成功后立即写本地 empire”的时点。
  • 当前角色名、改名和创建字段仍经 UTF-8 to_wire,本轮安全编码审计已确认与 Windows locale codepage 不是同一语义;重复 enter_game/DirectEnter 失败和中断清理仍没有独立状态 epoch。

结论:4 槽/legacy 3 槽固定槽、DirectEnter 不重复选人、越界创建和未命中改名回调、帝国写入时点已按 40250 对齐;locale 字节、双窗口回调/连续异常包、重复进入/中断清理、角色实例管理器生命周期和真实服务端包序仍与 40250 不一致,合同保持 PARTIAL

Required tests before verification

  • 3 槽/4 槽登录成功后都暴露固定 4 个槽位,空槽 index、guild 和角色字段全部清零。
  • 在 3 槽协议下创建/删除 slot 3,证明参考行为与当前行为的边界;若协议确实禁止,记录服务器分支证据而不是用 vector 长度推断。
  • 创建/改名名称在 0、最大字节、最大字节+1、非 ASCII、locale codepage 不可逆字符下的实际 wire bytes 与参考比较。
  • 创建成功越界、创建失败双窗口、删除成功/失败连续包的状态和事件顺序。
  • 改名 PID 命中和未命中(错误码 100)以及强制改名状态清理。
  • DirectEnter 从初始 game 连接到槽地址的完整阶段和 wire 序列,确认 PHASE_SELECT 是否可达以及是否应发送 CG_PLAYER_SELECT;增加重复 enter_game、连接失败和中断后的清理断言。

Active UserInterface character-manager review

  • AbstractCharacterManager.h/PythonCharacterManager.h 把 VID→CInstanceBase 实例表、主实例、选中实例、fade/dead list、设备对象、point effect、transform update 和销毁顺序作为角色管理器职责;PythonCharacterManagerModule.cpp 还注册 RaceData 的 motion mode、weighted motion、normal/combo attack、attach bone、shape 和 motion resource。
  • PythonCharacterModule.cpp 的创建/注册/删除/淡出/选择、装备/发型/武器、位置/方向/动作、render mode、affect、bound box 和 faint 起身队列构成角色选择后到游戏内的统一实例入口。当前端角色选择和 NetWorld entity view 能显示/删除角色,但没有同一 manager 的 dead/fade/device/motion registration 生命周期。
  • 本轮完成活跃角色管理源码静态核对;现有 character select 测试和网络包测试不能替代参考端实例表、重复 VID、fade 删除、主实例切换和进入游戏清理语义,合同保持 PARTIAL

Implementation fix round 2026-09-21

按 40250 的固定数组和 phase 分支修复当前 classic 客户端:legacy GC_LOGIN_SUCCESS3 现在只填充前 3 项但始终保留 4 个零初始化槽位;创建成功的越界槽不再产生成功事件;未知 PID 的改名回包改为创建失败码 100SendSelectEmpirePacket 对齐为写包成功后立即更新本地 empireDirectEnter 的 PHASE_SELECT 只切换 Loading,不再额外发送普通 CG_PLAYER_SELECT

新增/更新 native 边界回归覆盖 legacy 三槽固定槽、帝国本地状态时点、未知改名 PID 和 DirectEnter 包序。cmake --build build -j2ctest --test-dir build --output-on-failure23/23)、char_create_delete_test.gdtest_intro_select_parity.gdp10_test.gdapp_flow_lifecycle_test.gdgit diff --check 均通过。

剩余 locale codepage 字节语义、创建/删除异常和双窗口回调、DirectEnter 重复进入/中断清理、角色实例管理器生命周期以及真实服务端包序仍未达到 40250 统一语义,合同保持 PARTIAL

Audit round 2026-09-20 — Select-phase side-effect gate

继续核对 PythonNetworkStreamPhaseLogin.cpp::LoginPhasePythonNetworkStreamPhaseSelect.cpp::SelectPhase 与当前 ClassicSession::on_packet:40250 的角色创建、删除、强制改名回包只在 SelectPhase 的分支中消费;错误阶段不会进入固定槽位和 UI 回调。当前端此前由 phase-neutral parser 直接接收这些 header,延迟的创建/改名包可能在 Login/Loading 阶段改写角色列表。

本轮修复 ClassicSession::on_packet 的阶段门禁:

  • GC_PLAYER_CREATE_SUCCESSGC_PLAYER_CREATE_FAILUREGC_PLAYER_DELETE_SUCCESSGC_PLAYER_DELETE_WRONG_SOCIAL_IDGC_CHANGE_NAME 只有 CharSelect 或 DirectEnter 尚处协议 SelectPhase 时才进入 parser。
  • 错误阶段返回明确错误并断开当前 phase owner,避免角色槽、改名状态和 UI 事件产生错误副作用。
  • DirectEnter 的 UI stage 已是 Loading,但 m_direct_enter 仍作为协议 SelectPhase 标志保留,确保其合法的阶段边界不被误拒绝。

回归:net_classic_session_testnet_classic_stream_testnet_classic_wire_testnet_loopback_testchar_create_delete_test.gdtest_intro_select_parity.gdp10_test.gdgit diff --check 均通过。

这只闭合了角色 CRUD 回包的阶段副作用;Login/Select/Loading/Game 的完整 117-header allowlist、burst tick 消费顺序、locale codepage、双窗口回调和真实服务端包序继续由 network.packet_dispatch_protocol/本合同跟踪,状态保持 PARTIAL

Implementation fix round 2026-09-21 — 40250 Chinese name bytes

角色创建和改名入口保持 UTF-8 业务字符串,但实际 CG_PLAYER_CREATE/CG_CHANGE_NAME 固定字段改由 mtnet::to_wire() 写入 GB2312/GBK bytes。wire_text_fits() 在编码后按 CHARACTER_NAME_MAX_LEN=24 校验:12 个中文字符可发送,13 个中文字符在客户端被拒绝;结构体零初始化保证第 25 个字节为 NUL。net_text_codec_test 覆盖独立转换,net_classic_session_test 直接检查创建/改名 packet bytes,避免只测试 helper 而漏掉真实发送入口。

Status

PARTIAL