- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
17 KiB
network.character_select.create_delete
Scope
对比 40250 角色列表、固定槽位、帝国选择、角色选择、创建、删除、强制改名和 DirectEnter 的完整网络调用链、固定字段、阶段副作用、回调顺序和失败清理。
本轮是静态审计、阶段副作用核对和现有测试复核;角色选择主路径已有多轮实现修复,合同仍为 PARTIAL。
Reference call chain
普通登录到角色列表
CPythonNetworkStream::SetLoginPhase()在UserInterface/PythonNetworkStreamPhaseLogin.cpp发送CG_LOGIN/CG_LOGIN2,随后ClearLoginInfo()清除密码。CPythonNetworkStream::LoginPhase()接收HEADER_GC_LOGIN_SUCCESS3/4、HEADER_GC_EMPIRE、创建/删除/改名结果。__RecvLoginSuccessPacket3/4()把数据写入固定的m_akSimplePlayerInfo[PLAYER_PER_ACCOUNT4]和对应 guild 数组;3 槽报文只覆盖前 3 个槽,未覆盖的第 4 槽仍是已清零的空槽。RecvPhasePacket()->SetSelectPhase();普通路径根据IsSelectedEmpire()进入选人或选帝国窗口。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
- Python
net.DirectEnter(slot)->CPythonNetworkStream::ConnectGameServer(slot)。 ConnectGameServer()校验< PLAYER_PER_ACCOUNT4,保存所选槽位,设置__DirectEnterMode,按该槽的lAddr:wPort建立新的 game 连接。- 新连接握手和
SetLoginPhase()仍发送带 login key 的CG_LOGIN2,不是普通选人窗口路径;登录信息随后被清除。 SetSelectPhase()在 DirectEnter 模式只切换到 Loading UI,不调用普通SendSelectCharacterPacket();DirectEnter 标记在SetLoadingPhase()中初始化清除。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 槽时等价 |
MAPPED,3 槽第 4 槽和异常 slot 未覆盖 |
| 删除失败 | 消费空包并回调 OnDeleteFailure() |
解析为 DeleteFail,由 UI 清空输入框 |
MAPPED,需补固定包/连续包测试 |
| 改名成功 | 按 PID 命中槽位后清除强制改名并回调 index/name | 按 PID 更新槽位并排入 name event;UI 再发整表和 PID/name | PARTIAL:event 适配存在,index 语义和顺序未完全保持 |
| 改名 PID 不命中 | 调用创建失败码 100 | 未命中时排入 CreateFail(100),不产生 name event |
MAPPED |
| DirectEnter 目标连接 | 设置 DirectEnter,连接角色槽 lAddr:wPort,重新走 CG_LOGIN2;DirectEnter 的 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.gd、test_intro_select_parity.gd、net_classic_session_test和net_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 起身队列构成角色选择后到游戏内的统一实例入口。当前端角色选择和NetWorldentity 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 的改名回包改为创建失败码 100;SendSelectEmpirePacket 对齐为写包成功后立即更新本地 empire;DirectEnter 的 PHASE_SELECT 只切换 Loading,不再额外发送普通 CG_PLAYER_SELECT。
新增/更新 native 边界回归覆盖 legacy 三槽固定槽、帝国本地状态时点、未知改名 PID 和 DirectEnter 包序。cmake --build build -j2、ctest --test-dir build --output-on-failure(23/23)、char_create_delete_test.gd、test_intro_select_parity.gd、p10_test.gd、app_flow_lifecycle_test.gd 和 git diff --check 均通过。
剩余 locale codepage 字节语义、创建/删除异常和双窗口回调、DirectEnter 重复进入/中断清理、角色实例管理器生命周期以及真实服务端包序仍未达到 40250 统一语义,合同保持 PARTIAL。
Audit round 2026-09-20 — Select-phase side-effect gate
继续核对 PythonNetworkStreamPhaseLogin.cpp::LoginPhase、
PythonNetworkStreamPhaseSelect.cpp::SelectPhase 与当前 ClassicSession::on_packet:40250 的角色创建、删除、强制改名回包只在 SelectPhase 的分支中消费;错误阶段不会进入固定槽位和 UI 回调。当前端此前由 phase-neutral parser 直接接收这些 header,延迟的创建/改名包可能在 Login/Loading 阶段改写角色列表。
本轮修复 ClassicSession::on_packet 的阶段门禁:
GC_PLAYER_CREATE_SUCCESS、GC_PLAYER_CREATE_FAILURE、GC_PLAYER_DELETE_SUCCESS、GC_PLAYER_DELETE_WRONG_SOCIAL_ID、GC_CHANGE_NAME只有CharSelect或 DirectEnter 尚处协议 SelectPhase 时才进入 parser。- 错误阶段返回明确错误并断开当前 phase owner,避免角色槽、改名状态和 UI 事件产生错误副作用。
- DirectEnter 的 UI stage 已是
Loading,但m_direct_enter仍作为协议 SelectPhase 标志保留,确保其合法的阶段边界不被误拒绝。
回归:net_classic_session_test、net_classic_stream_test、net_classic_wire_test、net_loopback_test、char_create_delete_test.gd、test_intro_select_parity.gd、p10_test.gd 和 git 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。