Files
shenleiandClaude Opus 5 1933a5ceda fix(combat): 战斗逐项对齐 40250,修复移动 / 技能释放与武器挂点
- 连击类型由 combo_changed 驱动(SetComboSkillFlag,技能 122 等级)
- 命中判定改为 .msa 刀尖 Z 圆柱 vs .msm 防御球(hit_collision.gd 逐行移植)
- _register_hit 改为 map::insert 语义,修正命中上限
- 攻速同时缩放 GetAttackingElapsedTime 与攻击动作速率;按武器动作模式目录绑定攻击段
- 去掉本地连击超时,motion_bound 清命中表 / 连击段号
- __ProcessDataAttackSuccess:InsertDelay 硬直、physics_push.gd 击退 + GetBlendingPosition 同步、
  命中特效、__HitGood / __HitGreate / __HitStone 受击动作链与抖动(ui/hit_reaction.gd)
- ClassicSession::send_use_skill 不再夹带 CG_FLY_TARGETING
- mac 前进 / 后退方向与技能释放修复;武器按职业骨骼挂到手上
- 新增 hit_collision / physics_push / hit_view / net_world_push / weapon_attach 测试;
  gpu_pose_bounds / race_motion_assembly 适配 damage 随机变体
- 文档:CLIENT-GAP.md、CLIENT-GAP-FIX.md §3.5 / §3.7 与 C.5 增量 130

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ft3kXpNdLbKEfg5uDLfqVF
2026-09-12 13:15:28 +09:00

394 KiB
Raw Permalink Blame History

40250/ClientVS22 → metin2-client 1:1 功能差距

对照范围:../40250/ClientVS22/(40250 客户端源码及其运行时模块)以及本工程当前实现。 当前工程默认运行 MT_PROTOCOL=classicextension/src/net/wire.h 仅属于显式选择的 m2dev 后端,当前 40250/ClientVS22 对照应优先使用 extension/src/net/classic/wire_classic.h

本文按“协议、数据、交互、表现均与原客户端等价”的严格 1:1 标准审计; CLIENT-ROADMAP.md 中的“P0–P10 首版完成”只表示主要纵向流程已有首版,不等于 1:1 完成。 渲染细节另见 PARITY-GAP.md,动画、材质和资产管线另见 BACKLOG.md,网络实现另见 CLIENT-PORT.md

本文末「ClientVS22 实现细节审计(2026-09-02)」每一条的具体修改细节见 CLIENT-GAP-FIX.md(逐条给出 40250/ClientVS22 的参考 源码位置、常量与算法,以及本工程要改的文件/函数;其附录 A 列出本节中 需要订正的条目,分「参考行为不符 / 表述需加限定 / 低估了当前差距」三档)。

更新日期:2026-09-04(对照 40250/ClientVS22 复核)

状态列图例 达到本文末“1:1 完成判据” · 🟡 有首版 / 部分语义 · 未实现 · 明确不做(超出 40250/ClientVS22 参考范围,除非另立需求)。

开发执行约定:并行工作流与进度同步

本文按“可并行开发”维护,不要求按章节号从 1 到 10 串行实现。章节号只表示差距 归属;实际开发按工作流拆分,使用稳定接口、测试替身和集成门禁降低互相等待。 所有工作流都以 40250/ClientVS22/ 的源码和当前 Classic 协议为唯一对照基线。

并行拆分

工作流 负责范围 主文件边界 依赖 / 集成点
W0 公共基础与集成 连接阶段、共享网络状态、实体公共接口、场景生命周期、文档 extension/src/net/m2_client.*entity_store.*classic_session.*project/game_scene.gd、本目录两份差距文档 先冻结信号、状态结构、reset / enter / warp 接口;其他工作流不得同时重写这些共享文件
W1 协议与账号会话 §1、Classic wire / parser、登录 / 选人 / DirectEnter / 断线、协议错误 extension/src/net/classic/*、对应 extension/tests/net_classic_*、登录和选人专属脚本 依赖 W0 的会话接口;用 loopback / mock 验证,真实服务端留在集成门禁
W2 实体、世界与战斗 §2–§3、实体两层模型、移动、动作、技能、目标和换图后的世界状态 project/net_world.gdnet_play.gdplayer_controller.gdproject/world/*、动画 / 战斗相关扩展及测试 依赖 W0 的 EntityStore / world_reset;UI 只消费公开信号,不直接修改网络核心
W3 玩法 UI、输入与本地化 §4–§5、§8、背包 / 快捷栏 / 任务 / 社交 / 输入 / IME / 键位 project/ui/*hud.gdlogin.gdcharselect.gd、UI 测试和本地化资源 依赖稳定的 M2Client 信号和数据快照;缺少服务端时使用 fixture,不阻塞 W1/W2
W4 渲染、资产与音频 §6–§7、§9、GR2 / EffectLib / SpeedTree、地图、材质、天气、BGM、生命周期表现 extension/src/{gr2_bridge,metin2_world,terrain_splat,m2_material,environment_builder}.*project/fx/*、音频和渲染专属脚本 / 测试 依赖 W0 的场景 reset / 地图切换钩子;不得把渲染占位逻辑写入协议层
W5 打包、平台与验收 §10、Mac App 自包含资源、架构 / 签名、发布配置、验收脚本 project/export_presets.cfg、打包 / 发布脚本、extension/CMakeLists.txt、发布验收文档 可与 W1–W4 并行;集成阶段消费最终构建产物,不能用“能启动”代替资源和签名验收

并行规则与集成门禁

  1. G0:接口冻结后再分工。 W0 先定义并记录 enter_gamewarpworld_reset、实体快照、 UI 信号和测试 fixture;接口变更必须在两份差距文档中先登记影响范围,再通知相关工作流。
  2. 按文件边界并行。 同一文件只能有一个工作流作为主维护者。需要改共享文件时,提交“接口变更” 给 W0 集成,不在多个分支中各自复制一份核心实现。
  3. 允许用替身解耦。 W1 可用 loopbackW2/W3 可用固定实体和包 fixture,W4 可用最小地图 / 资源, W5 可用 debug 构建先验证流程;替身通过后仍必须在集成门禁替换为真实链路。
  4. G1:接口集成。 每个工作流先通过自己的单元 / 回环 / GDScript 回归,再由 W0 合并共享接口, 执行 C++ 扩展构建、原生网络测试和受影响的 Godot 测试。
  5. G2:真实验收。 连接、DirectEnter、Warp 换图、双账号玩法、Mac App 资源包、架构 / 签名和 视觉 / 输入体验必须按对应验收项逐条记录;受限环境下的 skip 只能标为“未验证”,不能标为通过。
  6. 合并顺序不等于开发顺序。 只要依赖接口已经冻结,W1–W5 可以同时开发;只有触及共享状态或 集成门禁时才回到 W0 排队。

当前并行进度(2026-09-04

工作流 状态 当前已知进展 下一步
W0 公共基础 / 集成 🟡 DirectEnter、Classic Warp endpoint、world_reset、场景重建首版已落地;增量 49 接口冻结 + 增量 51 AppFlow 单一 AppLifecycle + 增量 52 net_bounds 已接入,GameScene 不再重复创建,当前 C++ CTest 15/15 与生命周期 / 场景回归通过 Phase 1 4 工作流并行完成后执行 G1 集成;真机生命周期 / 业务边界验收
W1 协议 / 账号会话 🟡 增量 50:§1.5 版本包按 locale 分支(EUROPE 发 CG_CLIENT_VERSION2 0xf1 + "1215955205",其余发 0xfd + MT_BUILD_TIMESTAMP)、可执行名 setter;§1.10 text_codec.h UTF-8 安全定长文本裁剪 + net_text_codec_test,会话侧所有定长 name/sign/comment 填充改走 to_wire();§2.2 GC_CHARACTER_ADD 按 invisible-race 丢弃 / PC·NPC 暂存 m_pending_actor / 其余即时 mut_spawn(名字取自 npclist.txt),GC_CHAR_ADDITIONAL_INFO 无暂存则记录并丢弃、命中则合并 9 字段 mut_spawn_full 后清表 真实 locale codepage 转码、真实双连接、enter delay;GDScript 名称长度校验统一(G1 已收)
W2 实体 / 世界 / 战斗 🟡 §2.1 两层模型(EntityStore 纯数据层 + net_world.gd 每帧 _update_visibility()CHAR_STAGE_VIEW_BOUND / AFFECT_SHOW_ALWAYS / IsWall 建 / 淡出节点)、§2.3mut_spawnmut_spawn_full 整行替换 + 节点销毁重建 + 上下坐骑保坐标)、§2.4(点数快照截断 / 尾部清零 + 单索引边界)、§2.5(mut_affect_flags 两段拼装 + 参考调用序节点刷新)已落地并回归(增量 50);增量 52 补齐 M2Client 公共动作 / 物品 / 交互入口的 wire 参数边界;增量 73(§3.7)删除 net_play.gd 的本地硬直计时器 _hitstun_until,「能否行动」统一走 _can_process_network_state() / _can_attack() / _can_use_skill()(对齐 InstanceBase.cpp:1247 / InstanceBaseBattle.cpp:355 / :310),Entity 增加 knock_down(受击 .msa 播放生命周期驱动、动作播完清零),dead / stunned 保持服务端包驱动;增量 74(§3.3)formats/msa 新增 ComboInputData / AttackingData 解析、metin2_anim.get_motion_data()net_play.gd 删除 ATTACK_RANGECLICK_DIST_DEFAULT/NPC/RESOURCE_CM(对齐 InstanceBaseBattle.cpp:294),普攻节奏改为 .msa DirectInputTime(缺组退化 duration*0.9)除以 bAttackSpeed/100;增量 75(§3.4net_play.gd 新增 enum ReservedMode + _update_auto_attack() + _reserve_process_click_actor() 严格 10 步(对齐 PythonPlayer.cpp:207 / PythonPlayerInput.cpp:877),出距离 pc.walk_to 靠近、NPC 走 click_npcIN_SAFE / EMPTY_ARROWsignal cannot_act;增量 76(§3.1player_controller.gd 去掉直线改坐标,改 _src_pos / _dst_pos / _is_going / _dst_rotwalk_to()_goto() 对齐 NEW_GotoInstanceBaseMovement.cpp:251:太近不动、三门 _can_translate() 未过 → __ReserveClickGround 0.1 s 重试、moving_skill 只转向),新增 player_move_test.gd;增量 77(§3.2 命令队列 part AEntityStore 把远端 Actor 位置改为 std::deque<StateCmd> 命令队列驱动(对齐 InstanceBase.cpp:1278 StateProcess),mut_move() 入队(≈ PushTCPState+ drain_state_queue() 释放到期项、删旧 start_move() 直改坐标、两套后端统一走 mut_move(...,arg,time),新增 process_states() / apply_state_cmd() 1:1 复刻 switchFUNC_WAIT 1.0、FUNC_COMBO/ATTACK/MOB_SKILL 50.0 硬阈值 + m_kMovAfterFunc 到达后动作 + skip_collision),释放门按 set_server_frame_ms()(为 0 = 帧钟未对齐即时释放)+ __CanProcessNetworkStatePacketdead / knock_down 挂队),新增 net_state_queue_test5 组)、C++ 20/20;增量 78(§3.2 part Bnet_play.gd 上行按 PythonPlayerEventHandler.cpp 拆成 6 回调(统一出口 _send_state):OnMove 立即 FUNC_MOVE + 重置 moving/waiting 节流窗、OnMoving 300 ms 节流、OnWaiting(新 _tick_on_waiting100 ms + 位移 ≥ 1 cm、OnStop FUNC_WAIT、OnAttack 额外 FUNC_COMBO、新公共入口 on_use_skill() → `FUNC_SKILL motion;删旧 SEND_INTERVAL/SEND_DIST 单一节流;net_world.gd去掉自有follow_lerp_process直接n.position = e.pos(插值全交 C++ tick());netplay_test 加第 11 段、netbridge_test near-move 断言改写,7 个 GDScript 回归 + C++ 20/20 全绿;增量 79(§3.2 part C 收官)ClassicStream::server_frame_ms()(≈ ELTimer_GetServerFrameMSec(),由 HDR_HANDSHAKE m_server_time_base = hs.time + lDelta/m_client_time_base算出)经ClassicSession::process()每帧set_server_frame_ms() 喂给命令队列(m2dev 恒 0 = 即时释放,无回归),drain_state_queue加防呆阀STATE_QUEUE_MAX_WAIT_MS=1000(异帧钟 dwTime 立即放行,有意偏离参考端),net_classic_stream_test+net_state_queue_test补覆盖,C++ 20/20 + 7 GDScript 全绿;增量 80(§3.5 part A)查明 PC 连击段表**不在**.msm 里(.msm只有BaseModelFileName+HairData),实为 assets/root/playersettingmodule.pychrmgr.ReserveComboAttackNew/RegisterComboAttackNew调用建在CRaceData上,新增formats/combo_table.{h,cpp}fmt::PlayerComboTables)按 def __LoadGameEx分四职业块解析为map<MAKE_COMBO_KEY(motionMode,comboType), vector<段号>>,语义对齐 CRaceData::Reserve/RegisterComboAttack(insert 不覆盖 / 越界跳过),新增 formats_combo_table_test(合成 + 真实 playersettingmodule.py 双覆盖),C++ 21/21 + 7 GDScript 全绿;增量 81(§3.5 part Bnet_play.gd用此表 +.msa ComboInputData 三时间实现连击状态机(_do_attack_swing/_run_next_combo/_combo_process/_on_end_combo/_clear_combo逐行对齐ActorInstanceBattle.cpp:157/213/256/297/308),替掉 (_combo + 1) % 3,段号经新静态绑定 Metin2AnimPlayer.parse_combo_tables()race & 3×combo_motion_mode<<16|type 取,缺段表退化单段普攻,game_scene装配时set_asset_root()combat_fx_testFakeAnim全路径断言,C++ 21/21 + 7 GDScript 全绿;增量 82(§3.5 part C,修改 1)导出.msa HitDataContainer 多命中窗:textscript.cppList Name { ... }解析(此前List HitPosition破坏后续Group),formats/msa.{h,cpp} AttackingDatastd::vectorMsa::HitWindow{start_time,end_time,bone_name,weapon_length,samples},吃老式内联 + 新式 HitDataCount N/Group HitData0i),新增 motion_type/hit_limit_countget_motion_data()hit_windows/motion_type/hit_limit_countformats_msa_test补合成 +M2_ASSETS真机断言(真实双命中窗assassin/dualhand_sword/combo_01.msa),C++ 21/21 + 7 GDScript 全绿;增量 83(§3.5 修改 4,收官)net_play.gd新增_begin_hit_windows/_attack_process(dt)/_hit_deduped/_register_hit/_hit_geometry/flush_victim_list:逐帧按动作时间窗与 hit_window [start,end]相交对client.get_entities() 可攻击 / 未死 / 非安全区实体做几何判定(m_HitDataMap查重 + 每窗命中上限 NORMAL/COMBO 16 / SKILLhit_limit_count),命中 → client.attack(swing_skill, vid)OnHit),被击退目标帧末 client.sync_positions()(≤16);emit_swing改为只有缺命中窗数据才在挥击瞬间发CG_ATTACK;几何判定 = reachmax WeaponLength / 采样水平偏移)+ 正面 ±60° 弧近似(真实动态圆柱扫掠随 §3.1 留桩),combat_fx_testFakeNetWorld + 6 段断言,C++ 21/21 + 7 GDScript 全绿;增量 84(§3.5 修改 6,§3.5 修改 1-6 全落地)net_play.gd补全MOTION_MODE*EMode17 项)+ITEM_TYPE*/WEAPON_SUB*常量,新增静态motion_mode_for(item_type, weapon_sub, mounted, poly, dress)逐分支复刻CInstanceBase::RefreshStateSetMotionModeInstanceBase.cpp:2851Poly→GENERAL / 婚纱→WEDDING_DRESS / 镐·钓竿→骑乘 HORSE 否则 GENERAL·FISHING / 骑乘 switch → HORSE_* / else switch → ONEHAND·TWOHAND·DUALHAND_SWORD·BOW·FAN·BELL·默认 GENERAL),_equipped_weapon_vnum()parts[WEAPON]优先,否则get_equipment()[WEAR_WEAPON].vnum+ _refresh_motion_mode()(读 mount_vnum/polymorph/wedding_dress+proto.item(vnum).{type,sub_type}set_combo_motion_mode(),换模式内部 _clear_combo()),由 _on_entity_info()末尾 /inventory_changed(EQUIPMENT) / mount_changed(主角) 三处触发,combat_fx_testFakeProto+ 15 段motion_mode_for+ 2 段端到端断言,C++ 21/21 + 7 GDScript(含equip_model_test)全绿;增量 85(§3.5 修改 7,§3.5 修改 1-7 全落地)net_play.gd _clickable_distance()逐分支对齐NEW_IsClickableDistanceDestInstanceInstanceBaseBattle.cpp:294:默认 150 → _is_bow_mode()MODE_BOW/MODE_HORSE_BOW)改用 _bow_range_cm()→ NPC 500 → 采集物 100,后写if 覆盖前面),_bow_range_cm()=BOW_RANGE_BASE_CM(2500-100) + _bow_distance_bonus1:1 复刻 CInstanceBase::__GetBowRange InstanceBase.cpp:694),_bow_distance_bonus_on_points()points["bow_distance"]wire.hPOINT_BOW_DISTANCE=34+get_points() 透出,combat_fx_test §3.5 段加 8 段断言,C++ 21/21 + 7 GDScript 全绿;增量 86(§3.6,修改 1-3 全落地)game_scene.gd单槽_pending_shoot_skill+create_timer(1.5)换成_pending_shots: Array[int]FIFO(上限 8):弓挥击 / 弓技能起手_queue_shot(uSkill) 压一个,.msa MOTION_EVENT_TYPE_FLYpop_front()client.shoot()AutoClear + 连射并发,对齐 ActorInstanceMotionEvent.cpp:290);OnSetFlyTarget移到net_play._emit_swing_send_fly_target()(目标节点 → MapCoord.to_server_cm()client.add_fly_targeting(),对齐 ActorInstanceMotion.cpp:372+ActorInstanceFly.cpp:4),新增信号 bow_shot_firedcombat_fx_test+6 段 /eterngrn_polish_testFLY 段改 FIFO 连射断言,C++ 21/21 + 9 GDScript 全绿;增量 87(§3.8 修改 2POC 无 C++ skill proto,故在ui/skill_table.gdRegisterSkillDescPythonSkill.cpp:280)把 skilldesc 第 11 列 attributes / 第 12 列 weapon 的
W3 玩法 UI / 输入 🟡 增量 53–62 已覆盖快捷栏、输入入口、物品窗口 / 拖放、tooltip 基础字段、任务条带、地图信号和帮助窗;增量 95 完成 Quest EventManager 常用标签分派;增量 96–98 完成 wear 槽位 / 装备门 / 部位遮挡;增量 99 完成远端 PC RemotePlayerView + awPart[4] 四字段换装;增量 101 完成 Quest 日志 QUEST_MAX_NUM=5 五行窗口、滚动起始索引、默认 icon、倒计时和任务行事件;增量 108 完成小地图任务信标越界夹圆周 + 方向箭头(世界箭头 / 屏幕边缘指示,对照基线 m2dev PythonMiniMap.cpp:441-462);增量 109 完成 quest 信件按钮条 __ArrangeQuestButton 多列定位 + 最新在前 + 组队窗 xPos 移位;增量 110 完成头顶名字颜色 CInstanceBase::GetNameColorIndex / SetEmpireNameMode 1:1name_color.gd 纯静态 + net_world.gd 事件驱动 / ~2Hz 兜底重染,帝国名色默认关);增量 111 完成头顶公会名 / 称号色 GetTitleColor / 等级子标签 1:1text_tail.gd 纯静态 RefreshTextTail / UpdateTextTailLevel / EnablePKTitle + net_world.gd GuildTag / TitleTag / LevelTagLabel3D 子节点);增量 112 完成头顶聊天 / 信息尾标 RegisterChatTail / RegisterInfoTail 1:1chat_tail.gd 纯静态 LivingTime 5s 硬删 / "名字: " 前缀剥离 / SHOUT·COMMAND 排除 + net_world.gd 每 vid 一条 "bubble" Label3D、白 / 淡红按 chat/info、新增 register_info_tail());增量 113 完成头顶名字「是否显示」1:1(name_show.gd 纯静态 IsAlwaysShowName / __IsShowName总是显示名字选项 or LALT / ShowAllTextTailfDistanceFromPlayer < 3500 平面距裁剪 / ShowCharacterTextTailisShow · !IsGuildWall · CanPickInstance 资格判定 + net_world._name_shown() 四路组装:uigameoption always_show_name radio(经 game_option_ui 新信号)/ LALT / net_play.target_changed 目标强显 / chat 尾标 bNameFlag 强显);增量 114 完成头顶伤害飘字 AddDamageEffect / ProcessDamage 1:1damage_effect.gd 纯静态:DODGE|BLOCKMISS/TARGETMISSCRITICAL/POISON 分支参考端注释掉走普通数字、bSelf"damage_"+SELFDAMAGESELFDAMAGE2 交替、!bTarget"nontarget_" 不渲染、否则 "target_"while(damage>0) 逐位 %10//10 index 0..7、IsShowDamage / SetShowDamageFlag 门 + net_world._on_damage/_process_damage_queue 每帧 pop 一条 + game_option_ui「伤害数字」radio 经 display_option_changednet_world.set_show_damageseam ⑪:单 Label3D 写整串 vs damagevalue dds 逐位特效、self 退化水平抖动、暴击不再放大 / 变色、全局单队列);增量 115 完成头顶目标框 RecvTargetPacket 收包分派 1:1target_board.gd 纯静态:RecvTargetPacket 判定树(实例缺失→CloseTargetBoard、目标已死→不动框、IsPC()|IsBuilding()CloseTargetBoardIfDifferentCanViewTargetHPIsStone()|IsWoodenDoor()|IsEnemy())→SetHPTargetBoard、否则→CloseTargetBoard+ game.py SetHPTargetBoard 换 VID 先 ResetTargetBoard + net_play._on_target_info 由「任意有名字 VID 都显框」改为按判定树分派(PC / building 不显血条框、NPC / warp 关框);seam ⑫:poc 无 building / wooden-door 实体类型、LCONTROL 悄悄话捷径未接、target_info 由 C++ EntityStore 侧抽出不改 C++ 收包);增量 116 完成 game.py OnRender Picking 块分派 1:1 + 悬停角色强显名字(pick_show.gd 纯静态第 27 个 canonicalIsPickedWindow 门内 chr.Pick() 命中角色 → ShowCharacterTextTailtargetBoard.GetTargetVID()ShowCharacterTextTail__IsShowName() 分叉 item.Pick()ShowAllTextTail + textTail.Pick()、末尾 SelectItemNameforced_name_vids[hover, target] 去重)+ net_world._hover_vid / set_hover_vid / _name_shown() forced 第 5 路 + player_controller.hover_entity_changed 信号(射线悬停拾取,压 UI / 悬停地面物品 → 0)+ game_scene 连线;seam ⑨:名字尾标随 chat 气泡下移 17px 未接;seam ⑩:ArrangeTextTail 屏幕空间去重叠仍缺(本增量只收口「悬停强显名字」一小块);增量 117 完成头顶名字尾标随聊天气泡上移 17px 1:1(name_tail_layout.gd 纯静态第 28 个 canonicalUpdateShowingTextTail :110-114 —— VID 有 chat 尾标且 bNameFlag TRUE 时名字屏幕 y = 气泡 y 17px;poc 3D 适配为名字 Label3D 局部 y 顶到「气泡 y + 17*pixel_size」,气泡消失或「信息」尾标时回默认 2.1;net_world._reposition_name_tag_apply_chat_tail / _expire_chat_tails 调用,seam ⑨ 收口);增量 118 完成目标框「看他国玩家」前置过滤 + §8.4 名字色 radio 接线 1:1target_board.hidden_as_other_empire(view_other_empire, same_empire) 照抄 uitarget.py TargetBoard.Open :189-193 —— game-option 关「看他国目标框」且目标异阵营 → net_play._on_target_infoclassify_clear_target() returnnet_play.set_view_other_empire_target() + net_world.set_empire_name_mode()game_scene._on_display_optiongame_option_uitarget_board / name_color radio,开局各自 display_value(...) == 1 回填;修 game_option_ui.gd target_board 默认 0→1 对齐 VIEW_OTHER_EMPIRE_PLAYER_TARGET_BOARD = 1;§8.4 三显示 radio + LALT 至此全部接线);增量 119 完成 seam ⑦ 收口 —— 头顶「公会名 / 称号 / 等级」子标签随名字尾标纵向位移 1:1(name_tail_layout.gdname_tail_lift_delta / subtag_world_y 照抄 CPythonTextTail::ArrangeTextTail :211/:228/:258/:283 —— 三子标签屏幕 y 全锚在已被 UpdateShowingTextTail :114 顶过的 pTextTail->y 上;net_world 把子标签默认局部 y 提为 SUBTAG_DEFAULT_Y 常量、_reposition_name_tag 顶完名字尾标后按同一 lift 平移 GuildTag / TitleTag / LevelTag,气泡过期 / 「信息」尾标时回默认;剩余 seam ⑦:子标签竖排 vs 参考屏幕空间逐像素水平排版仍近似);增量 120 完成 seam ⑩ 收口 —— ArrangeTextTail item 尾标去重叠 + isIn 1:1text_tail_arrange.gd 纯静态第 29 个 canonicalboxes_overlapisIn AABB 含边界 + arrange_item_tails 外层逐条 / 内层比对 / 命中顶到 pCompareTextTail->y + yEnd + 5 / 从头重跑 / MAX_ITERS 20 封顶 + box_* 四边 -w/2-2 w/2+2 -2 hground_items._arrange_tags 每帧把所有掉落名条锚点 cam.unproject_position 到屏幕、按字符数估框、跑去重叠、按 Label3D.pixel_size 折回局部 y,可见名条 < 2 回默认;game_scene 传相机 getter;剩余 seam ⑩:名条框按「字符数 * 7px」估宽 vs 参考 GetTextSize 精确像素、只对掉落 item 名条做);增量 121 完成 seam ⑦ 水平深化 —— 头顶「公会名 / 称号 / 等级」子标签逐像素水平排版 1:1(name_tail_layout.gdname_x_shiftfxAdd 8/4/0/ title_x_offsetnameW/2 8/ level_x_offset(有称号再 titleW/ guild_name_x_offset / mark_x_offset / subtag_world_x 照抄 ArrangeTextTail :211-320 非 EUROPE 分支;net_world._layout_subtags_x_apply_text_tail_to 尾部设名字 / TitleTag / LevelTag.position.xGuildTag 留 0(poc 无公会徽记),文本宽按 字符数 * 7px 估;纵向位移(增量 117/119)与横向排布只分别写 .position.y / .position.x 互不干扰;剩余 seam ⑦:无公会徽记 mark 实体、字符数估宽 vs GetTextSize、三子标签竖排 + x 偏移的折衷 vs 参考同一屏幕行);增量 122 完成 seam ⑫ 收口 —— 目标框点击 LCONTROL 悄悄话捷径接 whisper 1:1net_playwhisper_requested 信号 + _lcontrol_down + set_lcontrol_down()_on_pickKIND_PC 分支轮询 target_board.whisper_on_ctrl_clicklcontrol_down and same_empire and not is_main and not is_building,谓词增量 118 已备)→ 发 whisper_requestedgame_scene._unhandled_input KEY_CTRL 按下 / 松开喂 set_lcontrol_down(e.pressed)net_play.whisper_requestedchat.start_whisperpoc 版 OpenWhisperDialog=聊天输入预填 "/w 名 ");C++ picking 只对 PC 派发 SetPCTargetBoard 故 poc 也只在 PC 分支查;is_building 实参恒 false(poc 无门实体、:814 分支永不触发)、无独立 whisper dialog 窗口,seam ⑫ 末尾);增量 123 完成 seam ⑦ / ⑩ 收口 —— 头顶子标签 / 掉落名条排版的文本宽度从「字符数 * 约 7px」估算换成 CGraphicTextInstance::GetTextSize 的真实字体度量 1:1(新增 text_metrics.gd 第 30 个 canonical 纯静态 text_size / width_of / label_widthnet_world._layout_subtags_xname_w / title_wground_items._arrange_tags 的名条框 w 改走 TextMetrics.label_widthSUBTAG_CHAR_W_PX / TAG_CHAR_W_PX 保留仅作无字体兜底;name_tail_layout.gd / text_tail_arrange.gd 排布公式不动;剩余:单行文本高仍 15px 估、无公会徽记 mark 实体、三子标签竖排 vs 参考同一屏幕行);增量 124 完成 seam ⑦ / ⑩ 末尾 —— 掉落名条屏幕空间去重叠框的「单行文本高」从硬编码 15px 换成 CGraphicTextInstance::GetTextSize 的 h 真实字体度量 1:1text_metrics.gdconst FALLBACK_LINE_H_PX := 15.0 + height_of / label_heightground_items._arrange_tags 的框高 h 改走 maxf(1.0, TextMetrics.label_height(tag, TAG_LINE_H_PX))TAG_LINE_H_PX 保留仅作无字体兜底;text_tail_arrange.gdbox_end_y(h) = h/ name_tail_layout.gd 排布公式不动;剩余:掉落名条 owner 未拆成独立 text instance、无公会徽记 mark 实体、三子标签竖排 vs 参考同一屏幕行);增量 125 完成 seam ⑩ 末尾 —— 带 owner 的掉落名条屏幕空间去重叠框按 CPythonTextTail::SetItemTextTailOwnerPythonTextTail.cpp:706-737)的「owner 独立度量 + yEnd += yOwnerSize + 4」1:1text_tail_arrange.gd 扩第 29 个 canonical 加纯静态 item_box_with_owner——无 owner -> 纯名字框、有 owner -> {maxf(name_w, owner_w-2.0), name_h+owner_h+4.0} 使 box_end_x(w) 恰等于参考 :737 fMAXtext_metrics.gd 扩第 30 个 canonical 加 string_width_like / string_height_like 量某 Label3D 字体下的任意子串;ground_items._on_addeditem_name / item_owner meta、_arrange_tags 把名字子串与 owner + 's 子串分别度量后走 item_box_with_owner;渲染仍单个合并 "name (owner)" Label3D;剩余:owner 未独立渲染成 CENTER 黄字、strOwnership 未接本地化语素、只对掉落 item 名条);相关离线回归通过 CJK IME、窗口视觉 / .sub 资源、完整交易事务、复杂 mousemodule 规则、EventManager 逐帧 / .msc / 相机消费端、任务日志窗体自身多列 / 分页、tooltip 特殊分支 / 装备对比、text_tail.gd 屏幕空间血条整合、三子标签并到名字同一屏幕行 / 接公会徽记 mark(seam ⑦ 末尾;文本宽 / 高已分别由增量 123 / 124 换真实字体度量 GetTextSize 的 w / h 1:1)/ 名字与气泡合成单尾标链表(seam ⑨ 遗留)、掉落名条 owner 独立渲染成 CENTER 黄字 + strOwnership 接本地化语素(seam ⑩ 末尾;去重叠框度量已由增量 125 换 1:1)、伤害飘字 damagevalue dds 逐位特效(seam ⑪)、目标框 building / wooden-door 实体类型分类(is_building 恒 falseLCONTROL 悄悄话捷径已由增量 122 接 whisper)(seam ⑫ 末尾)、真服和真机仍未验收
W4 渲染 / 资产 / 音频 🟡 增量 50:§9.1 地图 BGM 选曲、§9.2 后台音量存恢复已落地;增量 51:AppFlow 绑定当前场景音频;增量 53EffectLib 已读取 .mse 首个 TextureFiles 的实际 .dds,并映射位置、发射形状、方向 / 速度;增量 55:audio.gd 已接入 MilesLib 的监听器、200/1000 缩放、音量曲线、普通 / 环境 / 角色 3D 声道、角色 0.3 秒与 5000cm 限频、3D stop 和限幅淡出;增量 63:按 MilesLib/Type.cpp 解析 .mssSoundDataCount/SoundData%02d,转换为 60 FPS 声音实例并接到 PlayerView / MobView 动作帧;增量 64:按 EffectMesh.cpp 解析 .mde EffectData/MDEData002,展开几何帧、UV、.ifl 贴图和 MeshElement 材质;增量 65:解析 .mse Light 组并生成范围 / 生命周期曲线,粒子多贴图支持帧延迟与顺逆序,旋转类型区分 NONE / TIME_EVENT / CW / CCW;增量 66:按 EffectElementBase::GetPosition 接入粒子 / 网格 / Light 的 MovingType 直接段与二次 Bezier 段,并映射球面边缘发射;增量 67:按 Area.cpp / MapType.cpp 接入 AreaAmbienceData + .pra,独立于对象渲染加载环境声源,Audio 实现 LOOP / ONCE / STEP、距离音量和卸载停止;增量 68:按 MapUtil.cpp 补齐 .msenv 参考缺省值、IsDensityFilterBTextureRenderMode、六面天空文件名和 Reserved,通过 AssetResolver 接入真实云层贴图到 ProceduralSkyMaterial.sky_cover,并修复 macOS 大小写敏感环境路径;增量 69:背景 / 角色方向光按参考端分成 Sun layer 1 与 CharacterLight layer 2,静态场景和 Metin2Model 分层,换图时清理两盏光;增量 70:真实 CapeDragonHead 五面天空贴图按 ClientVS22 面顺序接入 Godot sky shader,空 bottom 面回退渐变;增量 71:树 proxy 使用参考 fWindStrength=0.2 / fWindRandom=0 缺省;增量 72:真实 DawnMistWood 验证 texture-mode 五面天空与有限云层平面、CloudScale / Height / TextureScale / Speed 和参考颜色合成;bgm_testaudio_miles_testambience_testenvironment_testskybox_testworld_probeplayer_motion_testmob_view_testfx_test + 生命周期 / 场景回归全绿 TexAni 的随机起始帧 / 每粒子语义、完整 emitter advanced / cycle、完整时间轴 / Actor motion、Direct3D 材质 stage、SpeedTree、云层渐变过渡、滤色实际合成、风参数完整语义、lens flare、昼夜天气联动、声音资源逐项和真机验收
W5 打包 / 发布 / 验收 🟡 增量 53 已把 assets / bgm 复制进 Contents/Resources,支持 MT_MAC_ARCHES=universallipo 架构门禁,嵌套扩展先签名、App 后签名;默认凭据改为环境变量;脚本语法 / diff 检查通过 尚未执行完整 2.2G 资源导出;当前已检查的 dylib 仍为 arm64;正式 Developer ID / notarization、跨机启动和 Mac App 验收

最新进展(增量 1272026-09-04:本地可验证的四项尾部缺口已完成。net_world.gd 现用 CanvasLayer + ScreenHpBar 做屏幕空间 HP 条,按相机投影更新并在死亡、镜头后方、屏幕外和换图时 隐藏 / 清理;entity_rules.gd / net_play.gd 支持建筑与木门分类(显式 kind / buildingbType=4、race 13000 / 30111..30119),目标框不会再将它们误作 PC;ground_items.gd 的 owner 行继承掉落物显式颜色或品阶颜色;chat_ui.gd 提供独立 WhisperDialogLCONTROL 点同阵营 其他 PC 后可发送 / 取消 / ESC 关闭。screen_hp_bar_test.gdentity_rules_test.gdtarget_board_test.gdnetplay_test.gdchat_test.gdp2b_test.gd 已覆盖这些行为;全量 C++/CTest 21/21、Godot 脚本 71/71 通过。剩余的是公会徽记 / 单尾标等表现 seam、完整世界玩法和 真实服务器 / 真机 / 发布包验收,不能把离线通过当作端到端验收。

W3→M2 最新进展(增量 1282026-09-04net_world.gd 已消费客户端公会会徽缓存的 guild_marks_ready / guild_mark_updated 事件,为已存在角色动态创建 / 刷新 / 删除头顶 GuildMark Sprite3D,并按参考 mark_x_offset / guild_name_x_offset 排布,聊天气泡 上移时会徽同步跟随;guild_mark_tail_test.gd 覆盖延迟下载、纹理更新、排布和公会移除。 quest_dialog.gd[DONE] 解析已无副作用,done_event 只在用户点击关闭按钮时触发, quest_test.gd 覆盖解析与点击语义。当前离线回归为 CTest 22/22、Godot 74/74;仍未完成的 是单尾标链表、完整 .msc 电影播放器、Quest/fishing/dungeon/mount/observer 完整玩法、 真实服务器、真机、纯 pack、双架构和正式签名验收。

W2 最新进展(增量 1302026-09-12:战斗逐项对齐 40250。

  • 命中判定改为 .msa 刀尖 Z 圆柱 vs .msm 防御球(hit_collision.gd)。
  • __ProcessDataAttackSuccess 补齐:InsertDelay 硬直、physics_push.gd 击退 + GetBlendingPosition 同步、命中特效,以及 __HitGood / __HitGreate / __HitStone 受击动作链与抖动 ui/hit_reaction.gd)。
  • 攻速同时缩放命中时间与动作速率。
  • 连击类型改由 combo_changed 驱动;连击段号和命中表随 motion_bound 清零。
  • send_use_skill 不再夹带 CG_FLY_TARGETING
  • 测试:新增 hit_collision_test / physics_push_test / hit_view_test / net_world_push_test 扩充 combat_fx_test
  • seam:击退没有世界 / 角色碰撞;防御球未跟骨骼;远端角色不跑 AttackProcessowner 时间取客户端观测。

详见 CLIENT-GAP-FIX.md §3.5 / §3.7 / C.5 增量 130。

W4 最新进展(增量 100EffectLib 已完成缺省 cycle、有限 LoopCount 发射停止、重播时钟复位和 粒子寿命隔离;OUTER / INNER 当前仍是显式标记的宽方向 fallback,不计作精确径向速度完成。完整 状态以本文变更记录和 CLIENT-GAP-FIX.md §6.3 / C.5 为准。

W3→M2 最新进展(增量 99:远端 ch_type==0 PC 已由 game_scene.gd 创建 RemotePlayerView,复用真实 PlayerView race / GR2 / MSA 模型;远端换装改为消费完整 awPart[ARMOR,WEAPON,HEAD,HAIR]net_world.gd 的四字段调用序已覆盖。所有职业 / race、 变身字段、远端真实服务器洪流和精确视觉仍待验收。详见 CLIENT-GAP-FIX.md §4.1 / C.5。

W3→M2 最新进展(增量 101quest_log.gd 已按 ClientVS22 的 QUEST_MAX_NUM=5 固定显示 5 条任务;超过 5 条显示滚动条并映射 questShowingStartIndex,任务行点击发送 -2147483648 + questIndex,空 icon 路径使用 season1/icon/scroll_open.tga 默认图标。 quest_test.gd 已覆盖 7 条任务、首尾滚动和事件参数。多列定位、世界箭头、屏幕边缘指示、 完整 .sub 图标视觉和真实任务生命周期仍待验收。详见 CLIENT-GAP-FIX.md §5.2 / C.5。

W3→M2 最新进展(增量 102:§5.3 钓鱼输入状态机已接入:钓鱼模式攻击键发送 CG_FISHING 方向、普通攻击不再误发;方向键边沿和地面点击按参考顺序取消钓鱼,并保留 NEW_CancelFishing 的 500ms 节流;特殊技能 123 已从快捷栏特殊技能门进入钓鱼入口。 GC_FISHING START/REACT/STOP/SUCCESS/FAIL/FISH 会更新客户端意图状态。水面角度扫描、 wrong-place 文案、鱼获物品落袋,以及副本 / 坐骑 / 观战者完整实现仍待验收。详见 CLIENT-GAP-FIX.md §5.3 / C.5。

W3→M2 最新进展(增量 103Metin2World 已按 ClientVS22 的 InstanceBaseMotion::GetFishingRot 通过 attr.atr 水面属性,在角色前方 600cm 对左右 0..180°、每 10° 扫描,并由 NetWorld 桥接给 NetPlay;选中的 heading 会发送给 CG_FISHING,没有水面时发出 wrong-place 状态。fishing_water_test.gd 加载 A1 全 20 区块 找真实水面并通过。鱼获物品落袋、副本 / 坐骑 / 观战者完整玩法仍待验收。详见 CLIENT-GAP-FIX.md §5.3 / C.5。

W3→M2 最新进展(增量 104ChatUI 已按 ClientVS22 的 RecvFishing 分支接入 FISHING_WRONG_PLACEFISHING_FAILUREFISHING_UNKNOWN,以及 item proto 类型驱动的 FISHING_NOTIFY1/2FISHING_SUCCESS1/2 文案;GC_FISHING(SUCCESS) 只切换动画状态, 不再错误打印“鱼钩命中”。chat_test.gdgamescene_test.gd 通过。参考端的真正背包增加 仍由独立 GC_ITEM_* 包驱动,本批次没有伪造本地落袋。详见 CLIENT-GAP-FIX.md §5.3 / C.5。

W3→M2 最新进展(增量 105:坐骑攻击门按 ClientVS22 的 SHORSE::GetLevel/CanAttackCPythonPlayer::__CanAttack 接入:普通坐骑 level 1 禁止攻击,新坐骑要求骑乘术 level 11 或 master; 自动攻击路径同时改用实体 mount_vnum 判断,不再只检查不存在的 mounting 标志。netplay_test.gd 已覆盖普通 / 新坐骑门禁。坐骑模型 / MODE_HORSE* 完整动作、移速和真实服务器验收仍待完成。详见 CLIENT-GAP-FIX.md §5.3 / C.5。

W3→M2 最新进展(增量 106:小地图已按 ClientVS22 CPythonMiniMap::AddObserver / MoveObserver / RemoveObserver 接入观战者紫色标记的一秒位置插值,初始快照可播种轨迹, world_reset 清理观战者轨迹。p9_test.gd 已覆盖 ADD / MOVE / REMOVE / reset;副本入口、 观战自由视角及真实服务器完整观战生命周期仍待完成。详见 CLIENT-GAP-FIX.md §5.3 / C.5。

W3→M2 最新进展(增量 107:副本 GC_DUNGEON DESTINATION_POSITION 已按 PythonNetworkStreamPhaseGame.cpp:3875-3901PythonPlayer.cpp:147-167,1255-1285 接入 dungeon_state.gd:保存全局厘米坐标、立即发出目标提示、按参考端 Manhattan <10000cm 清除目标,并在 world_reset 时清理。HUD 增加目标距离 / 朝向提示;TIME_ATTACK_START 保持 参考端空分支,不虚构本地计时器。dungeon_state_test.gdgamescene_test.gdp9_test.gdnetplay_test.gd 均通过。DungeonBlock 的 .mdatr 碰撞、真实副本入口 / 结算窗口及真服验收仍待完成。 详见 CLIENT-GAP-FIX.md §5.3 / C.5。

总览表同步说明(增量 107:P1“世界玩法”中“副本入口 / 计时”仍是旧快照的概括;当前已完成的 是 GC_DUNGEON 目的地消费、参考端到达判据和 HUD 罗盘提示,TIME_ATTACK_START 在参考端本身为空分支, 因此不实现本地计时器。仍待的是 DungeonBlock .mdatr 碰撞、真实副本入口 / 结算窗和真服验收。

W3→M2 最新进展(增量 108:§5.2 小地图任务信标已补齐世界箭头 / 屏幕边缘指示。信标越出 小地图半径时,minimap.gdm2dev PythonMiniMap.cpp:441-462atan2 + radius*cos/sin 夹到圆周(留边常量 SIGNAL_RIM_INSET=9.0 对齐 c_fMiniMapWindowRadius- 9.0f),并改画 指向真实目标的方向箭头(signal_marker_geometry + _draw_signal_arrow)。p9_test.gd 加 8 断言覆盖夹边朝向 / 半径 / 增删计数。夹边逻辑的对照基线落在 m2dev 完整版(40250/ClientVS22 的 PythonMiniMap.cpp 为裁剪版,缺该实现);多列定位、真实 .sub 图标、Atlas 同一信标夹边 仍待。详见 CLIENT-GAP-FIX.md §5.2 / §8.9 / C.5。

W3→M2 最新进展(增量 109:§5.2 quest 信件按钮条已按 assets/root/interfacemodule.py:1357-1386__ArrangeQuestButton 1:1 多列定位:yPos = 170*H//600yCount = (H-330)//63、第 i 个 = (xPos + (i//yCount)*100, yPos + (i%yCount)*63)xPos 组队窗显示时由 20 移到 130。 quest_log.gd 新增有序表 _quest_button_order 复刻 questButtonList.insert(0, btn)(最新在前、 同 index 提前),_button_list 从 VBox 单列改绝对定位 Control,组队窗可见性经 set_party_probe_process 轮询。quest_test.gd 加 16 断言覆盖换列坐标 / 顺序 / 组队窗移位 / 清空隐藏。 剩余:任务日志窗体 _list 自身多列 / 分页、真实 .sub 信件图标 / Flash 视觉。详见 CLIENT-GAP-FIX.md §5.2 / §8.9 / C.5 增量 109。

W3→M2 最新进展(增量 110:§8.8 头顶名字颜色按 CInstanceBase::GetNameColorIndex InstanceBaseEffect.cpp:525+ GetIndexedNameColor:75+ SetEmpireNameMode:52 1:1 落地。新建 project/name_color.gd(纯静态 RefCounted):NAMECOLOR_* 枚举 InstanceBase.h:216EMPIRE_NUM = 4)、introloading.py:264 注册的 RGB 槽表、 chrmgr.NAMECOLOR_MOB/NPC/PCNORMAL_*PythonCharacterManagerModule.cpp:767 重映射, name_color_index(self_e, main_e, ctx) 逐行照抄(IsPC → killer→PK → 存在主角实例且非本人时 决斗 DUEL_CANNOTATTACK→本色 / DUEL_START 命中对局→PVP、同阵营查 __FindPVPKey / __FindGVGKey(guild≠0) → PVP、异阵营→PVP → 组队→PARTY → PC+GetEmpireIDNPC→NPC、 敌对 / 变身→MOB、其余 fallthrough→白)。ctxentity_rules.gd / net_play._attack_ctx() 同款(main_vid / duel_mode / duel_opponents / pvp_pairs / gvg_pairs / party_vids)。net_world.gdLabel3D.modulate 从非 1:1 的 _state_tint 占位(已删)改为专用名字色:spawn / info / pvp / duel / main_set 事件即时 _apply_name_color(vid) + _process 每 0.5 s _retint_all_names() 兜底(覆盖组队增减 / set_empire_name_mode 切换),kind_resolver = net_play._entity_kindgame_scene.gd 注入以补 m2dev fork「NPC / 怪 bType 均为 0」的分类缺口(未注入则回退裸 ch_typeseam ⑥)。 帝国名色默认关(constinfo.CHRNAME_COLOR_INDEX == 0),运行期开关 net_world.set_empire_name_mode(bool) 尚未接 game_option UI。name_color_test.gd(第 21 个 canonical,~55 断言)+ 全量离线回归通过。无 seam(40250 源含 GetNameColorIndex ISO-8859 编码,与 m2dev 逐字节一致;更正增量 109「对照基线 m2dev 完整版」的措辞)。 剩余:text_tail.gd 称号色 GetTitleColor / 公会名 / 屏幕空间血条 / 聊天气泡整合、 目标框 / 可攻击光标按 ENameColor + is_attackable_instance 驱动。详见 CLIENT-GAP-FIX.md §8.8 / §5.4 / C.5 增量 110。

W3→M2 最新进展(增量 111:§8.8 头顶「公会名 / 称号 / 等级」子标签按 CInstanceBase::GetTitleColorInstanceBaseEffect.cpp:609+ RefreshTextTail:664+ AttachTextTail:621+ UpdateTextTailLevel:654+ CPythonTextTail::RegisterCharacterTextTail 的公会名分支(PythonTextTail.cpp:509+ EnablePKTitle:9381:1 落地。新建 project/text_tail.gd(纯静态 RefCounted):TITLE_NUM=9 / TITLE_NONE=4g_akD3DXClrTitle 的 9 色槽(introloading.py:287 TITLE_COLOR_DICT + colorInfo.py:29)、s_kLevelColor = Color(152/255, 255/255, 51/255)、公会名色 0xFFEFD3FF"Noname" 兜底; title_color(grade)grade≥TITLE_NUM → 白)、refresh_title(alignment)(复用 entity_rules.alignment_gradegrade==TITLE_NONEEnablePKTitle 关时不显示)、 refresh_level(level)"Lv %d"level>0 且 title 开)、refresh_guild(id, name) id==0 不显示 / 空名→"Noname")。net_world.gd_attach_nameplateGuildTag / TitleTag / LevelTag 三个 Label3D 子节点,_apply_text_tail(vid)spawn / info 事件)+ _process 每 0.5 s _retint_all_text_tails() 兜底,title_name_resolvergame_scene.gd 注入 Locale.t("PVP_LEVEL%d")(称号名 localeInfo.TITLE_NAME_LIST)。 text_tail_test.gd(第 22 个 canonical~50 断言)+ 全 62 个离线回归通过。无 seam (40250 源含 GetTitleColor / RefreshTextTail / UpdateTextTailLevel / RegisterTitleColor); seam ⑦:billboard 3D 竖排 vs 参考屏幕空间像素排版;seam ⑧:title_name_resolver 未注入 回退裸 PVP_LEVEL%d key。剩余:伤害数字 / 聊天气泡 / 屏幕空间血条整合、头顶信息显示开关 (LALT + §8.4 三 radio)。详见 CLIENT-GAP-FIX.md §8.8 / §5.4 / C.5 增量 111。

W3→M2 最新进展(增量 112:§8.8 头顶「聊天 / 信息」尾标按 CPythonTextTail::RegisterChatTailPythonTextTail.cpp:581+ RegisterInfoTail:624+ TextTail_GetLivingTime / SetLivingTime:26/:28/:34gs_TextTail_LivingTime = 5000 assert > 1000+ UpdateAllTextTailLivingTime < now 硬删(:328+ IsChatTextTail:684+ RecvChatPacket 的派发规则(PythonNetworkStreamPhaseGame.cpp:1301/:1321/:13721:1 落地。新建 project/chat_tail.gd(纯静态 RefCounted):CHAT_COLOR=白 / INFO_COLOR=Color(1,.785,.785)LIVING_TIME_DEFAULT=5000EChatType 0..8strip_name_prefixstrchr(buf,':') ? p+2 : buf)、 line_for_tailTALKING/PARTY/GUILD/SHOUT/WHISPER 去 "名字: " 前缀,INFO/NOTICE 原样)、 wants_chat_tail(除 SHOUT / COMMAND 全进)、register_chat / register_info(共用 _chat_tails 同 vid 互相覆盖,bNameFlag TRUE/FALSE)、is_chat_text_tailexpirenet_world.gd_bubble 改走 chat_tail,每 vid 一条 "bubble" Label3D(白 / 淡红按 chat/info), LivingTime 到点由 _expire_chat_tails_process 每 0.5 s)硬删;新增 register_info_tail(vid, text) 公开方法对齐 RegisterInfoTailchat 派发从 t in [0,3,4,6] 换成 ChatTail.wants_chat_tail(t) —— 行为变化:SHOUT 不再弹头顶气泡(对齐 :1372),WHISPER / INFO / NOTICE 带 VID 时现在会弹。 game_scene._player_bubble 寿命对齐 5s。chat_tail_test.gd(第 23 个 canonical~45 断言)+ 全 63 个离线回归通过。无 seam(40250 源含 RegisterChatTail / RegisterInfoTail / RecvChatPacket); seam ⑨:气泡与名字 Label3D 各自 billboard 并存,名字不强制顶出 / 不随气泡下移 17px, bNameFlag 只存不用。剩余:伤害数字(GC_DAMAGE_INFO / AddDamageEffect/ 屏幕空间血条整合、 TextTail_SetLivingTime 运行期钩子。详见 CLIENT-GAP-FIX.md §8.8 / §5.4 / C.5 增量 112。

W3→M2 最新进展(增量 113:§8.4 / §8.6 头顶名字「是否显示」按 40250/ClientVS22/source 补 1:1。新建 project/name_show.gd(第 24 个 canonical 起点,纯静态 extends RefCounted,与 name_color.gd / text_tail.gd / chat_tail.gd 同套路)照抄 CPythonSystem::IsAlwaysShowName / SetAlwaysShowNameFlagiFlag == 1)、配置行 ALWAYS_VIEW_NAMEDEFAULT_VALUE_ALWAYS_SHOW_NAME = true)、game.py::__IsShowName() IsAlwaysShowName() or ShowNameFlagCPythonTextTail::ShowAllTextTailfDistanceFromPlayer < 3500.0f 平面距裁剪、ShowCharacterTextTailpOwner->isShow() / !IsGuildWall() / CanPickInstance() 资格判定、UpdateAllTextTail:74 的 chat 尾标 bNameFlag 强显(不受 3500 限制)。net_world.gd_names_visible(默认 true 拆成 _always_show_name(默认 true,对齐参考默认值与 game_option_ui 默认 1+ _alt_held

  • _target_vid;新增 _name_shown() 四路组装(_always_show_name / _alt_held / _target_vid / chat bNameFlag+ set_always_show_name() / set_target_vid()_process ~2Hz 节流兜底 距离变化,LALT / 选项 / 选中 / 尾标增删即时刷新。game_option_ui.gddisplay_option_changed(key, value) 信号 + display_value()(对齐 uigameoption RefreshAlwaysShowName);game_scene.gdnet_play.target_changedset_target_viddisplay_option_changedset_always_show_namename_show_test.gd(第 24 个 canonical~35 断言)+ 全 64 个离线回归通过。 行为变化:3500–20000cm 的实体有节点但无名字(原先全显);主角位置未知时 fail-open(同 _is_visible_entity);配置总写 ALWAYS_VIEW_NAME 键(参考仅在非默认时落盘)。 seam ⑩:悬停 chr.Pick() 强显、屏幕空间 ArrangeTextTail 防重叠、CanPickInstance 仅按 死透近似、参考 UpdateDistance-ownerY 是渲染帧 / 像素帧混用产物(本移植统一 pos_cm 帧)。 ShowName()playerGauge.EnableShowAlways() / SetQuickPage 两步未移植(blob / quickbar UI)。 详见 CLIENT-GAP-FIX.md §8.4 / §8.6 / §8.8 / §5.4 / C.5 增量 113。

W3→M2 最新进展(增量 114:§8.8 / §5.4 头顶伤害飘字按 40250/ClientVS22/source 补 1:1。新建 project/damage_effect.gd(第 25 个 canonical,纯静态 extends RefCounted,与 name_color.gd / text_tail.gd / chat_tail.gd / name_show.gd 同套路) 照抄 enum DamageFlagInstanceBase.h:300NORMAL 1<<0CRITICAL 1<<5)、 TPacketGCDamageInfoPacket.h:1943header / dwVID / flag / int damage)、 RecvDamageInfoPacketPythonNetworkStreamPhaseGame.cpp:2401bSelf = 目标是主实例bTarget = 目标是 m_pInstTargetdamage >= 0AddDamageEffect< 0TraceError))、 CInstanceBase::AddDamageEffectInstanceBaseEffect.cpp:87:仅 CPythonSystem::IsShowDamage()m_DamageQueue.push_back)、ProcessDamage:100InstanceBase.cpp:1910 每帧调一次, m_DamageQueue 每帧 pop_front 一条:(flag & DODGE) || (flag & BLOCK)bSelf ? EFFECT_DAMAGE_MISS : EFFECT_DAMAGE_TARGETMISS + returnCRITICAL / POISONCreateEffect 参考端均注释掉 → 落普通数字(无独立暴击视觉);bSelf"damage_" + EFFECT_DAMAGE_SELFDAMAGE / SELFDAMAGE2m_bDamageEffectType 交替;bTarget == false"nontarget_" + return不渲染);否则 "target_"while (damage > 0) 逐位 damage % 10 / damage /= 10index 0..7> 7 TraceError break),每位一张 d:/ymir work/effect/affect/damagevalue/<type><digit>.ddsFONT_WIDTH(30) * index 相机右向铺开)、 CPythonSystem::IsShowDamage / SetShowDamageFlagPythonSystem.cpp:337 / :342 bShowDamage = iFlag == 1,默认 true,配置行 SHOW_DAMAGE = atoi == 1,仅非默认落盘)。 net_world.gd_on_damageAddDamageEffectdamage_number 信号仍无条件发=本移植扩展 供战斗日志 / 测试;飘字本体 _show_damage 门后入 _damage_queue,元素 {vid, amount, flag, self, target})、新增 _process_damage_queue()_process 每帧调一次照 ProcessDamage 分类,show == false(非 self 非 target 数字)直接 return,其余一块 billboard Label3Dstr(amount) / "MISS"set_show_damage(on)clear_for_map_change_damage_queuegame_scene.gdgame_option_ui.display_option_changed("show_damage")net_world.set_show_damage 初值 display_value("show_damage") == 1 回填。damage_effect_test.gd(第 25 个 canonical ~50 断言)+ 全 65 个离线回归通过。 行为变化:BLOCK 也算 MISS(原占位只判 DODGE);非 self 非 target 的数字伤害不再显示; 暴击不再金色 / 放大(对齐该 40250 build);受 IsShowDamage() 门控;每帧只 pop 一条。 seam ⑪:单 Label3D 写整串 vs damagevalue dds 逐位特效 / 相机右向像素铺排;self SELFDAMAGESELFDAMAGE2 退化为落字水平抖动符号交替(±0.3);_damage_queue 全局单条 vs 参考每实例一条(同帧多 VID 受击会分帧显示);_show_damage 配置总写 SHOW_DAMAGE 键。 详见 CLIENT-GAP-FIX.md §8.8 / §5.4 / C.5 增量 114。

W3→M2 最新进展(增量 115:§5.4 / §8.8 头顶目标框(TargetBoard)收包分派按 40250/ClientVS22/source 补 1:1。新建 project/target_board.gd(第 26 个 canonical,纯静态 extends RefCounted,与 name_color.gd / text_tail.gd / chat_tail.gd / name_show.gd / damage_effect.gd 同套路)照抄 TPacketGCTargetPacket.h:1937header / dwVID / bHPPercent)、 CPythonNetworkStream::RecvTargetPacketPythonNetworkStreamPhaseGame.cpp:2424)的判定树: pInstPlayer && pInstTarget 都在且 !IsDead() 时 —— IsPC() || IsBuilding()CloseTargetBoardIfDifferent(dwVID):2440)、否则 pInstPlayer->CanViewTargetHP(*pInstTarget)SetHPTargetBoard(dwVID, bHPPercent):2442)、否则 CloseTargetBoard():2444); m_pInstTarget = pInstTarget 仅在该 !IsDead() 块内(:2447);实例缺失 → CloseTargetBoard() (:2453);目标已死透 → 整块跳过(不动框)。CInstanceBase::CanViewTargetHP InstanceBase.cpp:2293)= victim.IsStone() || victim.IsWoodenDoor() || victim.IsEnemy()game.pySetHPTargetBoard:829vid != GetTargetVID()ResetTargetBoard() + SetEnemyVID(vid),再 SetHP + Show)、CloseTargetBoardIfDifferent:837vid != GetTargetVID()Close())、CloseTargetBoard:841)、SetPCTargetBoard:804,按住 LCONTROLIsSameEmpire 且非本人且非 building → OpenWhisperDialog)。target_board.gd API enum Action { CLOSE, CLOSE_IF_DIFFERENT, SET_HP, NONE }can_view_target_hp / classify / updates_current_target / needs_reset / should_close_if_different / whisper_on_ctrl_click / clamp_hp_pctnet_play.gd_on_target_info(vid, hp%) 由「任意 vid != 0 且有名字都 hud.set_target」改为按 classify 分派 —— is_pc = _entity_kind == KIND_PCis_building = falseseam ⑫)、can_view = KIND_STONE 或 KIND_MONSTERCLOSE / CLOSE_IF_DIFFERENTvid != _target_vid 才) → _clear_target()SET_HP → 换 VID 先 _target_vid = vid + target_changed.emithud.set_target(名字, clamp_hp_pct(hp%)) NONE → 不动。target_board_test.gd(第 26 个 canonical~35 断言)+ 全 66 个离线回归通过。 行为变化:此前对任意有名字 VID(含 NPC)都显目标框;现按判定树 —— PC / building 目标不再显 血条框(只在框上是别的 VID 时清旧框)、NPC / warp 等不可看血目标一律关框、目标已死透不动框、 hp% 钳到 0..100。_change_target_to_picked_instance(点击选目标,PythonPlayerInput 路径) 不在本批次范围,维持原逻辑。 seam ⑫:poc 无 building / wooden-door / stone-door 实体类型(is_building 恒 false、木门分类 缺);target_info(vid, hp%) 由 C++ extension/src/net/m2_client.cpp:3129/3633 从 EntityStore 抽出,本移植不改 C++ 收包(对齐「纯 GDScript」约束);LCONTROL 悄悄话捷径谓词 whisper_on_ctrl_click 已备,net_play 暂不接(无 whisper dialog 钩子)。 详见 CLIENT-GAP-FIX.md §5.4 / §8.8 / C.5 增量 115。

W3→M2 最新进展(增量 116:§8.8 / §5.4 game.py GameWindow.OnRender() 的「Picking」块 :1519)分派按 40250/ClientVS22/source 补 1:1,并把「悬停角色强显名字」接进 poc。新建 project/pick_show.gd(第 27 个 canonical,纯静态 extends RefCounted,与 name_color.gd / text_tail.gd / chat_tail.gd / name_show.gd / damage_effect.gd / target_board.gd 同套路) 照抄:textTail.UpdateAllTextTail():1531)→ wndMgr.IsPickedWindow(self.hWnd) 门(:1533)内 chr.Pick()(:1535)命中角色(!= -1)→ textTail.ShowCharacterTextTail(idx):1537)、 targetBoard.GetTargetVID() != 0 → ShowCharacterTextTail(targetVID):1539)、not __IsShowName()item.Pick()ShowItemTextTail:1543/:1546);门外 __IsShowName()ShowAllTextTail()

  • PickingItemIndex = textTail.Pick(x, y):1552/:1554);末尾 UpdateShowingTextTail + ArrangeTextTail:1557/:1558)、PickingItemIndex != -1SelectItemName:1560)。 pick_show.gd APINO_PICK := -1char_pick_active(is_picked_window)forced_name_vids(is_picked_window, pick_char_vid, target_vid)(收 chr.Pick() 命中 + 目标框 VID,去重、顺序 [hover, target]、非窗口 → [])、should_pick_item(is_picked_window, is_show_name) and notshould_show_all_names / should_pick_text_tail is_show_nameshould_select_item_name != NO_PICK。poc 侧仿增量 113 的 _target_vid forced 路数给 net_world._name_shown() 加第 5 路 forced _hover_vidnet_world.set_hover_vid(vid)(值变才刷 prev / vid 两节点名字可见性,仿 set_target_vid)由 player_controller._update_hover_cursor() 三出口喂 —— 鼠标压 UI → 0、悬停地面物品 → 0、射线命中 pickablebest.get_meta("vid")、 无命中 → 0;新增 hover_entity_changed(vid) 信号(值变去抖),game_scene 紧挨 net_play.target_changed → net_world.set_target_vid 那行连 pc.hover_entity_changed → net_world.set_hover_vidpick_show_test.gd(第 27 个 canonical~24 断言)+ 全 67 个离线回归 通过(两次运行 PASS=67 FAIL=0)。行为变化:此前只有 targetBoard 目标(增量 113)forced 强显 头顶名字;现悬停到的角色(player_controller 射线拾取,pickablesvid meta)也 forced 强显 —— 即便未开「总显名字」且超 3500 平面距。悬停命中沿用既有 _ray_pick_t(cursor 用的同 一套),无新增射线开销。net_play._on_target_info(增量 115)不受影响。 seam ⑨:名字尾标 y = chat 气泡 y 17pxPythonTextTail.cpp:112-114)未接。seam ⑩: ArrangeTextTail 的屏幕空间尾标去重叠排布仍缺 —— poc 名字牌是 3D billboard,靠深度天然分层, 未做 2D 投影后的挤开;本增量只收口其中「悬停角色强显名字」一小块。近似:参考里 chr.Pick() 强显与 targetBoard 强显都在 :1533 的 IsPickedWindow 门内,poc 把 _target_vid / _hover_vid 都当作无条件 forced_hover_vidplayer_controller 压 UI / 悬停物品时置 0 部分等效该门。 详见 CLIENT-GAP-FIX.md §8.8 / §5.4 / C.5 增量 116。

W3→M2 最新进展(增量 117:§8.8 seam ⑨ 收口 —— 头顶名字尾标「有聊天气泡时上移贴气泡」 按 40250/ClientVS22/source 补 1:1。CPythonTextTail::UpdateShowingTextTail PythonTextTail.cpp:88)在角色名字尾标投影完屏幕坐标后(:104),m_ChatTailMap.find(dwVirtualID) 命中(:106/:110)且那条 bNameFlag 为 TRUE:112,= RegisterChatTail 的「聊天」气泡,非 RegisterInfoTail 的「信息」气泡)→ pTextTail->y = pChatTail->y - 17.0f:114,屏幕 y 向下 减 17px = 名字上移一行贴气泡上沿)。名字与气泡默认同世界高(GetHeight()+10:519/:581),全靠 这条 17px 分开。新建 project/name_tail_layout.gd(第 28 个 canonical,纯静态 extends RefCounted,与 name_color.gd / text_tail.gd / chat_tail.gd / name_show.gd / damage_effect.gd / target_board.gd / pick_show.gd 同套路):NAME_OFFSET_ABOVE_BUBBLE_PX := 17.0name_follows_bubble(has_chat_tail, chat_name_flag) and:110 命中 && :112 bNameFlag)、name_tail_screen_y(default_y, bubble_y, follows) 参考端字面 bubble_y 17name_tail_world_y(default_world_y, bubble_world_y, follows, px_size)= poc 3D 适配(屏幕 y 向下 −17px → 世界 y 向上 bubble_world_y + 17*px_size)。poc 侧 net_world._reposition_name_tag(n)_apply_chat_tail(冒气泡)/ _expire_chat_tails(气泡过期)调用,读 _chat_tails[vid].name_flag RegisterChatTail TRUE / RegisterInfoTail FALSE)决定名字 Label3D 局部 y 在 NAME_TAG_DEFAULT_Y2.1)与「BUBBLE_TAG_Y 2.35 + 17pixel_size」之间切换;_attach_nameplateNAME_TAG_DEFAULT_Y_apply_chat_tailBUBBLE_TAG_Yname_tail_layout_test.gd(第 28 个 canonical+ chat_test.gd 扩断言(冒气泡后名字 y == 气泡 y + 17pixel_size 且 > 气泡 y register_info_tail 覆盖 → 名字回 2.1)+ 全 68 个离线回归通过(PASS=68 FAIL=0)。剩余 seam ⑨: 气泡与名字仍是两个并存 Label3D 节点(非参考单尾标链表)、子标签 GuildTag / TitleTag / LevelTag 不随名字上移 = seam ⑦ 遗留;换算按名字 Label3Dpixel_size(0.006),与参考的 字体无关常量 17 对齐。seam ⑩:ArrangeTextTail 屏幕空间去重叠仍缺。 详见 CLIENT-GAP-FIX.md §8.8 / §5.4 / C.5 增量 117。

W3→M2 最新进展(增量 118:§5.4 / §8.8 / §8.4 目标框「看他国玩家」前置过滤 + 名字色 radio 接线按 40250/ClientVS22/source + assets/root 补 1:1。root/uitarget.py TargetBoard.Open(vid, name):189-193)在整个 RecvTargetPacket 判定之前先过滤: if not constInfo.GET_VIEW_OTHER_EMPIRE_PLAYER_TARGET_BOARD(): if not player.IsSameEmpire(vid): self.Hide(); returnconstinfo.VIEW_OTHER_EMPIRE_PLAYER_TARGET_BOARD = 1 默认, uigameoption.pytarget_board_view / target_board_no_view radio 经 constInfo.SET_VIEW_OTHER_EMPIRE_PLAYER_TARGET_BOARD(index))。project/target_board.gd 加纯静态谓词 hidden_as_other_empire(view_other_empire, same_empire) not view_other_empire and not same_empire(不按 is_pc 收窄 —— 忠于参考;empire 0 的 怪 / NPC / 石头在 EntityRules._is_same_empire 里恒「同阵营」故永不被挡)。net_play_view_other_empire_target(默认 true+ set_view_other_empire_target(on) _on_target_infoTargetBoard.classify(...) 之前 if exists and TargetBoard.hidden_as_other_empire(_view_other_empire_target, EntityRules._is_same_empire(_main_entity(), e)): _clear_target(); return。同批把 §8.4 「名字色」radio(name_color_normal / name_color_empire)接进 game_scene._on_display_optionnet_world.set_empire_name_mode(v == 1)(帝国名色调色板 算法增量 110 已 1:1),并修 game_option_ui.gdtarget_board 默认值 0 → 1(对齐 VIEW_OTHER_EMPIRE_PLAYER_TARGET_BOARD = 1)。game_scene 开局两处 display_value(...) 回填。target_board_test.gd+4 断言)/ netplay_test.gd(+8b 段:看他国框开→异阵营 PC 目标框保留、关→前置过滤清框、关不影响怪)/ system_menu_ui_test.gd+name_color_empire / target_board_no_view radio → _display + cfg 持久化 + display_option_changed 信号)+ 全 68 个离线回归通过(PASS=68 FAIL=0,无新增测试文件)。§8.4 三个显示 radio + §8.6 LALT 至此全部接线完毕。seam ⑫(延续):poc 无 building / wooden-door 实体类型、目标框点击 LCONTROL 悄悄话捷径未接。 详见 CLIENT-GAP-FIX.md §5.4 / §8.8 / §8.4 / C.5 增量 118。

W3→M2 最新进展(增量 119:§8.8 seam ⑦ 收口 —— 头顶「公会名 / 称号 / 等级」子标签 随名字尾标纵向位移按 40250/ClientVS22/source 补 1:1。CPythonTextTail::ArrangeTextTail PythonTextTail.cpp:151)逐条角色名字尾标(:211)把公会名(:228,pTextTail->y - c_fyGuildNamePositionc_fyGuildNamePosition = 15.0f :21/ 称号(:258,同 pTextTail->y)/ 等级(:283,同 y)三个子标签的屏幕 y 全部锚在 pTextTail->y 上 —— 而这个 y 已被 UpdateShowingTextTail :114 顶过 −17(增量 117)。所以名字尾标因「聊天」气泡上移时,三个 子标签整体跟随同一段纵向位移;增量 117 只让名字尾标本身跟随,子标签当时留作 seam ⑦ 遗留。 project/name_tail_layout.gdGUILD_NAME_ABOVE_NAME_PX := 15.0name_tail_lift_delta(default_world_y, bubble_world_y, follows, px_size)(= name_tail_world_y(...) - default_world_y,follows 时为净上移量、否则 0)、 subtag_world_y(default_subtag_y, name_tail_lift_delta)(=二者相加)。net_worldGuildTag / TitleTag / LevelTag 默认局部 y 提为 SUBTAG_DEFAULT_Y 2.34 / 1.86 / 1.74)常量,_reposition_name_tag 顶完名字尾标后按同一 lift 平移三个 子标签,气泡过期 / 「信息」尾标(bNameFlag FALSE)时回默认。name_tail_layout_test.gd +「4. seam ⑦ 收口」段:name_tail_lift_delta / subtag_world_y 数值、平移后相对间距不变、 lift 0 回默认)/ chat_test.gd+真 NetWorld 断言:冒聊天气泡后三子标签 position.y == 默认 + lift、覆盖成信息尾标后回默认)+ 全 68 个离线回归通过(PASS=68 FAIL=0,无新增测试文件)。 剩余 seam ⑦:子标签竖排 vs 参考屏幕空间逐像素水平排版(公会名上方 15px、称号 / 等级同 水平行左偏)仍近似。剩余 seam ⑨:气泡与名字仍是两个并存 Label3D(非参考单尾标链表)。 详见 CLIENT-GAP-FIX.md §8.8 / §5.4 / C.5 增量 119。

W3→M2 最新进展(增量 120:§8.8 seam ⑩ 收口 —— CPythonTextTail::ArrangeTextTail 的 item 尾标去重叠 + isIn40250/ClientVS22/source 补 1:1。PythonTextTail.cpp:153m_ItemTextTailList 块(:160-190):逐条掉落名条与其它每条比 isIn:489-507 [x+xStart,x+xEnd]×[y+yStart,y+yEnd] 的 AABB<=/>= 含边界),命中就把「自己」顶到 pCompareTextTail->y + pCompareTextTail->yEnd + yTempyTemp = 5),内层从头重跑、 LimitCount >= 20 封顶。新增第 29 个 canonical project/text_tail_arrange.gd extends RefCounted 全静态):PUSH_GAP := 5.0 / MAX_ITERS := 20box_start_x/end_x/ start_y/end_y-w/2-2 / w/2+2 / -2 / hPythonTextTail.cpp:810-813)、 boxes_overlap(a, b)(=isIn)、arrange_item_tails(tails: Array) -> Array(外层逐条 i、 内层逐条 j 跳过自身、命中 ys[i] = ys[j] + box_end_y(h_j) + PUSH_GAP 后 j 归零 limit++、 limit >= MAX_ITERS breakpCompare 用已排布后的 ys[j])。poc 侧 project/ui/ground_items.gd_camera_getter + 常量 TAG_BASE_Y 0.75 / TAG_CHAR_W_PX 7 / TAG_LINE_H_PX 15_process 尾部 _arrange_tags() 把所有掉落名条世界锚点 cam.unproject_position 到屏幕、按字符数估框、跑 arrange_item_tailsposition.y = TAG_BASE_Y - (arranged_y - y0) * pixel_size,可见名条 < 2 直接回默认; project/game_scene.gdfunc() -> Camera3D: return cam 传进 ground_items.setup 第 6 参。 text_tail_arrange_test.gd(新:常量、box_* 四边、boxes_overlap x/y 都叠 / 边界相接 / x·y 分离、arrange_item_tails 单条 / 不叠 / 两条同位 → [121, 100] / 三条同位 → [121, 142, 100] 且两两不叠 / 40 条全同位不死循环)/ p2b_test.gd+真 Camera3D + 真 GroundItems:同位两 掉落名条投影后靠前一条按 (15+5)*0.005 = 0.1 下推、靠后留默认、拉开距离回默认)+ 全 69 个 离线回归通过(PASS=69 FAIL=0)。剩余 seam ⑩:名条框按「字符数 * 7px」估宽 vs 参考 GetTextSize 精确像素;只对掉落 item 名条做(与参考 m_ItemTextTailList / m_CharacterTextTailList 分表一致,角色名字尾标不参与)。 详见 CLIENT-GAP-FIX.md §8.8 / §5.4 / C.5 增量 120。

W3→M2 最新进展(增量 121:§8.8 seam ⑦ 水平深化 —— CPythonTextTail::ArrangeTextTail 角色块(PythonTextTail.cpp:211-320 非 EUROPE 分支)的「公会名 / 称号 / 等级」子标签逐像素 水平排版按 40250/ClientVS22/source 补 1:1。参考端:fxAdd = 0(无称号无等级,:236)/ 8 (有称号,:242)/ 4(仅等级无称号,:288);名字文本 CENTER 对齐画在 x + fxAdd:320); 称号 CENTER 对齐画在 x iNameWidth/2 fxAdd(:258);等级 RIGHT 对齐,有称号时 x iNameWidth/2 fxAdd iTitleWidth:282)、仅等级时 x iNameWidth/2 fxAdd:312); 公会名 / 徽记仅当 pMarkInstance && pGuildNameInstance 时才由 ArrangeTextTail 重定位 :220),iImageHalfSize = markW/2 + c_fxMarkPosition(1.5):224),公会名 x + iImageHalfSize :228)、徽记 x guildW/2 iImageHalfSize:227)。project/name_tail_layout.gd 增 4 常量 NAME_X_ADD_TITLE 8.0 / NAME_X_ADD_LEVEL_ONLY 4.0 / MARK_X_PAD 1.5 / MARK_ABOVE_NAME_PX 26.0+ 6 静态方法(name_x_shift(has_title, has_level)fxAdd 8/4/0、 title_x_offset(name_w)name_w/2 8level_x_offset(name_w, title_w, has_title)=有称号 再减 title_wguild_name_x_offset(mark_w)mark_w/2 + 1.5mark_x_offset(guild_w, mark_w)subtag_world_x(px_offset, px_size)px_offset * px_size 线性折回、符号保留左负右正)。 poc 侧 net_world.gdconst SUBTAG_CHAR_W_PX := 7.0(无字体度量 -> 按「字符数 * 约 7px」 估文本宽,与 ground_itemsTAG_CHAR_W_PX 同源)、_apply_text_tail_to 尾部调 _layout_subtags_x(n, ttag, ltag):由 .text != ""has_title / has_level,设名字 Label3D.position.x = subtag_world_x(name_x_shift(...), pixel_size)TitleTag.position.x / LevelTag.position.x 同法(无则回 0)、GuildTag.position.x = 0poc 无公会徽记实体,参考的 pMarkInstance && pGuildNameInstance 门恒不满足)。纵向位移(增量 117/119 写 .position.y 与横向排布(增量 121 写 .position.x)互不干扰。name_tail_layout_test.gd(+「5. seam ⑦ 水平排版」段:4 常量、name_x_shift 四组、title_x_offset(40)==28< 0level_x_offset(40,30,true)==58 / (40,30,false)==24 且有称号时比称号更左、 guild_name_x_offset(20)==11.5 / (0)==1.5mark_x_offset(60,20)==41.5subtag_world_x 线性 / 符号 / 0、组合 lx(58) < tx(28) < nx(+8)/ chat_test.gd+真 NetWorldCharlie vid 3000 有公会 7 / alignment 15000 / level 42 -> GuildTag.text=="Camelot"position.x≈0、名字 position.x≈8*pixel_sizeTitleTag.position.x<0 且精确 ≈((7*7/2)8)*pixel_sizeLevelTag.position.x < TitleTag.position.x;改 alignment=0 后 TitleTag.text=="" 且 position.x≈0、名字 position.x≈4*pixel_sizeLevelTag.position.x 精确 ≈((7*7/2)4)*pixel_size)+ 全 69 个离线回归通过(PASS=69 FAIL=0,无新增测试文件)。 剩余 seam ⑦:poc 无公会徽记 / 徽记 mark 实体(参考 pMarkInstance && pGuildNameInstance 门 恒不满足,GuildTag x 恒 0);文本宽按「字符数 * 7px」估 vs 参考 GetTextSize 精确像素; 三子标签仍竖排叠 x 偏移 vs 参考同一屏幕行(poc 可读性折衷)。 详见 CLIENT-GAP-FIX.md §8.8 / §5.4 / C.5 增量 121。

W3→M2 最新进展(增量 122:§5.4 / §8.8 seam ⑫ 收口 —— 目标框点击的 LCONTROL 悄悄话捷径按 40250/ClientVS22/source + assets/root/game.py 补 1:1。参考端 CGameWindow::SetPCTargetBoardgame.py:804-817):self.targetBoard.Open(vid, name) 之后, if app.IsPressed(app.DIK_LCONTROL):807)→ if not player.IsSameEmpire(vid): return:809if player.IsMainCharacterIndex(vid): return:812)→ elif chr.INSTANCE_TYPE_BUILDING == chr.GetInstanceType(vid): return:814)→ self.interface.OpenWhisperDialog(name):817);C++ picking 只对 PC 命中派发 SetPCTargetBoard(怪 / NPC 走别的分派)。谓词 target_board.whisper_on_ctrl_click( lcontrol_down, same_empire, is_main, is_building)lcontrol_down and same_empire and not is_main and not is_building 增量 118 已备好没接,本增量纯接线:project/net_play.gdsignal whisper_requested(name) + var _lcontrol_down + func set_lcontrol_down(down) _on_pick_change_target_to_picked_instance(vid) 之后、_entity_kind(e) == KIND_PC 分支内调 TargetBoard.whisper_on_ctrl_click(_lcontrol_down, EntityRules._is_same_empire(_main_entity(), e), vid == _main_vid, false),真且 _entity_name(e) != ""whisper_requested.emit(name)project/game_scene.gd _unhandled_inputKEY_SHIFT 块后加 KEY_CTRL 按下 / 松开 → net_play.set_lcontrol_down(e.pressed)(一处轮询态,对齐参考端 IsPressed 语义),chat 装配处(fishing_feedback 接线之后)连 net_play.whisper_requestedchat.start_whisper poc 版 OpenWhisperDialog=聊天输入预填 "/w 名 " 并抢焦点)。netplay_test.gd 加 「8c) seam ⑫」段(无新增测试文件):主角 empire=1Bobvid 6100 / ch_type 0 / empire 1 没按 Ctrl 点 → whispers 空但 set_target 6100、按 Ctrl 点 → whispers == ["Bob"]、改 empire=2 再点 → 空(:809)、Wolf2ch_type 2)按 Ctrl 点 → 空(仅 PC 分派)+ 全 69 个 离线回归通过(PASS=69 FAIL=0)。剩余 seam ⑫:poc 无 building / wooden-door / stone-door 实体类型(_entity_kind 只到 0 PC / 1 NPC / 2 MONSTER / 3 STONE / 4 WARP),is_building 实参恒 false —— 参考端 :814 INSTANCE_TYPE_BUILDING 分支在 poc 永不触发;poc 悄悄话=聊天 输入预填,非独立 whisper dialog 窗口。 详见 CLIENT-GAP-FIX.md §5.4 / §8.8 / C.5 增量 122。

W3→M2 最新进展(增量 123:§8.8 seam ⑦ / ⑩ 收口 —— 头顶子标签 / 掉落名条排版的 文本宽度从「字符数 * 约 7px」估算换成 CGraphicTextInstance::GetTextSize 的真实字体度量 1:1。 参考端 CPythonTextTail::ArrangeTextTailPythonTextTail.cpp)里 iNameWidth / iTitleWidth / iLevelWidth / guildW:225 / :233 / :240 / :267 / :297)与 item 尾标框 xSize / ySize :733 / :748 / :809)全部取自 pTextInstance->GetTextSize(&w, &h)——即字体的实际逐字形排版 度量(含字距、非等宽),不是「字符数 * 定宽」。新增纯静态模块 project/text_metrics.gd(第 30 个 canonical,同 name_color.gd / text_tail.gd / name_tail_layout.gd / text_tail_arrange.gd 一套 extends RefCounted 全 static + *_test.gd 头less 回归的镜像套路):text_size(font, text, font_size)(空 / null / font_size<=0 -> Vector2.ZERO,否则 Font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size))、width_of(font, text, font_size, fallback_char_px := 7.0)(有字体量得正宽 -> 精确像素;否则 -> text.length() * fallback_char_px)、 label_width(label: Label3D, fallback_char_px := 7.0)(从节点自身取 font ?? ThemeDB.fallback_fontfont_size)。调用方换源不换排布:net_world._layout_subtags_xname_w / title_w 改走 TextMetrics.label_width(label, SUBTAG_CHAR_W_PX)(原 字符数 * SUBTAG_CHAR_W_PX), ground_items._arrange_tags 的名条框 w 改走 TextMetrics.label_width(tag, TAG_CHAR_W_PX) (原 text.length() * TAG_CHAR_W_PX);name_tail_layout.gd / text_tail_arrange.gdname_w / title_w / 框 w 作入参,排布公式(增量 117 / 119 / 120 / 121)不动。SUBTAG_CHAR_W_PX / TAG_CHAR_W_PX(均 7.0)保留仅作无字体兜底。新增 text_metrics_test.gd(头less):常量、退化输入 -> ZERO、真实度量随串长 / 字号单调、width_of 有字体 == text_size.x、无字体回退 字符数 * pxlabel_width 回落 fallback_font / null -> 0、Label3D 实测。离线回归 PASS=70 FAIL=0(新增 text_metrics_test)。剩余 seam ⑦ / ⑩:单行文本高仍按 15px 估(未走 GetTextSize 的 h)、poc 无公会 徽记 mark 实体、三子标签竖排 vs 参考同一屏幕行、掉落名条为唯一 item 尾标来源。 详见 CLIENT-GAP-FIX.md §5.4 / §8.8 / C.5 增量 123。

W3→M2 最新进展(增量 124:§8.8 seam ⑦ / ⑩ 末尾 —— 掉落名条屏幕空间去重叠所用文本框的 「单行文本高」从硬编码 15px 换成 CGraphicTextInstance::GetTextSize 的 h 真实字体度量 1:1。 参考端 CPythonTextTail::ArrangeTextTail 的 item 尾标去重叠把「自己」顶到 pCompareTextTail->y + pCompareTextTail->yEnd + yTempPythonTextTail.cpp:182),而 yEnd = ySize:751 / :812),ySize 与增量 123 换过的 xSize 同出 pTextInstance->GetTextSize(&xSize, &ySize)(:749 / :809)——单行文本的 ySize ≈ 字体行高 ascent + descent),与字形无关,Font.get_string_size(...).y 是 1:1 对应物。text_metrics.gd (仍第 30 个 canonical,扩非新增)加 const FALLBACK_LINE_H_PX := 15.0(=旧 ground_items.TAG_LINE_H_PX)、height_of(font, text, font_size, fallback_line_px := 15.0) (空串 -> 0;量得正高 -> text_size(...).y;否则 -> 回退)、label_height(label: Label3D, fallback_line_px := 15.0)label.font ?? ThemeDB.fallback_font + label.font_sizenull -> 0)。 ground_items._arrange_tags 的名条框 h 由硬编码 TAG_LINE_H_PX 改走 maxf(1.0, TextMetrics.label_height(tag, TAG_LINE_H_PX))TAG_LINE_H_PX15.0)保留仅作 无字体兜底。text_tail_arrange.gdbox_end_y(h) = h,对齐参考 yEnd = ySize/ name_tail_layout.gd 收框高作入参、排布公式不动。text_metrics_test.gd 扩「5. height_of / label_height」段(FALLBACK_LINE_H_PX = 15、空串 -> 0、无字体 / font_size<=0 -> 回退、 有字体高为正且与串内容无关、随字号单调、height_of == text_size(...).ylabel_height(null) -> 0 / 空 Label3D -> 0),离线回归 PASS=70 FAIL=0(无新增测试文件)。剩余 seam ⑦:poc 无公会徽记 mark 实体、三子标签竖排 vs 参考同一屏幕行;剩余 seam ⑩:掉落名条 owner 未拆成独立 text instance(参考端 yEnd += yOwnerSize + 4:733-736poc 仍按单行 "name (owner)" 处理)、 只对掉落 item 名条做。详见 CLIENT-GAP-FIX.md §5.4 / §8.8 / C.5 增量 124。

W3→M2 最新进展(增量 125:§8.8 seam ⑩ 末尾 —— 带 owner 的掉落名条,其屏幕空间去重叠 框按参考 CPythonTextTail::SetItemTextTailOwnerPythonTextTail.cpp:706-737)的「owner 独立 度量 + yEnd += yOwnerSize + 4」1:1。参考端 owner 后缀是与名字分开的 CGraphicTextInstance (文本 = c_szName + strOwnershipIDS_POSSESSIVE_MORPHENE 为空取字面 "'s";黄 1,1,0 / CENTER),单独 GetTextSize(&xOwnerSize, &yOwnerSize),据此 yEnd += float(yOwnerSize + 4) :735)、xStart = fMIN(xStart, -xOwnerSize/2 - 1):736)、`xEnd = fMAX(xEnd, xOwnerSize/2

  • 1):737);owner 为空则删 owner 实例、框复位纯名字(:739-751)。text_tail_arrange.gd(扩第 29 个 canonical)加纯静态item_box_with_owner(name_w, name_h, owner_w := 0.0, owner_h := 0.0)owner_w<=0 或 owner_h<=0->{name_w, name_h}(对齐参考 else 分支); 否则 -> {maxf(name_w, owner_w - 2.0), name_h + owner_h + 4.0}——令 box_end_x(w) = w/2+2恰好等于 :737 的fMAX(nameW/2+2, ownerW/2+1),框 x 两边对称故 box_start_x自动 = :736 的fMINtext_metrics.gd(扩第 30 个 canonical)加 string_width_like(label, text, fallback_char_px := 7.0)/string_height_like(label, text, fallback_line_px := 15.0)——用某 Label3D 的字体 / 字号量任意子串(null-> 字符数 * 兜底 / 空串 -> 0),对应参考端两个独立 text instance 各自GetTextSizeground_items._on_addeditem_name/item_ownermeta_arrange_tags把「名字子串」「owner +'s子串」分别度量后走item_box_with_owner合成 去重叠框;渲染仍是单个合并"name (owner)"billboardLabel3Dseam ⑩ 记录在案的折中)。 text_tail_arrange_test.gd 扩「5. item_box_with_owner」段、text_metrics_test.gd 扩 「6. string_*_like」段、p2b_test.gdFakeClient.add_ground加可选 owner 入参 + 带 owner 掉落被顶(15+15+4+5)*0.005 段,离线回归 PASS=70 FAIL=0(无新增测试文件)。剩余 seam ⑩: owner 未真正独立渲染成 CENTER 黄字、strOwnership未接ApplicationStringTable的本地化语素、 只对掉落 item 名条做。详见CLIENT-GAP-FIX.md` §5.4 / §8.8 / C.5 增量 125。

W3→M2 最新进展(增量 126:§8.8 seam ⑩ 末尾 —— 带 owner 的掉落名条把 owner 后缀拆成 一个独立的 CENTER 黄字(1,1,0text instance,并按参考 ArrangeTextTail 把名字实例 下移 15px。参考 CPythonTextTail::SetItemTextTailOwnerPythonTextTail.cpp:712-745):owner 非空时新建 pOwnerTextInstance:715)、文本 = c_szName + strOwnership:723strOwnership = ApplicationStringTable_GetString(IDS_POSSESSIVE_MORPHENE) == "" ? "'s" : ...:722)、 SetHorizonalAlign(HORIZONTAL_ALIGN_CENTER):727)、SetColor(1.0f, 1.0f, 0.0f) 黄(:729); 排布 pOwnerTextInstance->SetPosition(x, y, z):195)、pTextInstance->SetPosition(x, y + 15.0f, z):200);owner 为空 CGraphicTextInstance::Delete(pOwnerTextInstance):743)。 ground_itemsdrop 节点下加第二个 Label3D "owner_tag"horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER / modulate = Color(1,1,0) / billboard / no_depth_test / pixel_size 0.005,文本 = owner + _possessive),_apply_owner_tag(node, owner) 建 / remove_child + queue_free 删;名字 tag 只放 _name_for(vnum)_tag_text 删,不再拼 "name (owner)")。新增 const OWNER_NAME_GAP_PX := 15.0 + _place_tail(n, tag, d_px):有 owner -> owner_tag.y = TAG_BASE_Y - d_px*pxtag.y = TAG_BASE_Y - (d_px + 15)*px;无 -> tag.y = TAG_BASE_Y - d_px*px_arrange_tags behind-camera / 单条 / 多条三处落点都改走它)。 setup() 加可选 possessive := ""_possessive = possessive if possessive != "" else OWNER_POSSESSIVE1:1 对齐 :722 的 == "" ? "'s" : 语义),game_sceneEterLocalePOSSESSIVE_MORPHENE 键解析后注入(poc 的 locale/*.txt 无此键 -> 回落 "'s",与 40250 西文 UserInterface.rcIDS_POSSESSIVE_MORPHENE "'s" 一致)。去重叠框度量仍是增量 125 的 item_box_with_owneryEnd += ownerH + 4)不变。p2b_test.gd seam ⑩ 末尾段重写 (名字实例文本 = "掉落28"owner_tag 文本 "Alice's" / CENTER / 黄 / 落基点,名字实例 下移 15 * pixel_size;无 owner 的 drop 不建 owner_tagsetup(possessive="@") -> owner_tag 文本 "Bob@"),离线回归 PASS=70 FAIL=0(无新增测试文件)。剩余 seam ⑩:owner 行颜色语义固定黄(参考名字实例取 pInsertTextTail->Color 的道具品阶色、poc 名字走 _process 远近着色)、去重叠只对掉落 item 名条做。详见 CLIENT-GAP-FIX.md §5.4 / §8.8 / C.5 增量 126。

表格旧快照更正:W3 行中若仍写“远端 PC 仍是占位胶囊”或“任务日志缺滚动 / 定位”,以本节 增量 99 / 101 记录及 CLIENT-GAP-FIX.md 对应条目为准;后续编辑总览表时须同步替换旧描述。

进度同步是交付条件

开发过程中的每个批次都必须同步本文和 CLIENT-GAP-FIX.md,包括“开始、完成、 阻塞、重新验证”四类状态变化。代码已经提交但文档没有同步时,该批次仍视为未完成

每次同步至少记录:

  • 工作流 / 条目编号、当前状态( 未开始、🟡 开发中、 已完成、⚠️ 阻塞、🔎 待真实验收);
  • 参考源码依据、实际修改文件和公开接口变化;
  • 自动化测试命令及结果;无法运行或被 skip 的测试和原因;
  • 真实服务端 / Mac App 验收证据,仍缺什么;
  • 已知风险、与其他工作流的冲突 / 依赖、下一批次和负责人。

同步位置固定为:本文对应差距表 / 变更记录负责“全局状态”,CLIENT-GAP-FIX.md 对应条目和实施进度 负责“实现细节”。文档由 W0 集成维护者统一合并,工作流提交者只更新自己负责的条目,避免并行编辑造成状态 互相覆盖。

当前 40250 范围调整(增量 45):真实 FreeBSD + MySQL 服务端的完整登录 / 功能验收暂缓, 不作为本阶段交付门槛;公会相关功能暂缓。当前目标 40250 服务端不提供龙魂玩法,龙魂不纳入本阶段 支持范围,已有兼容代码不代表服务端可用;运行时龙魂入口已禁用,但保留 ds_refine 等协议兼容代码。

不做清单(

  • 拍卖行 / auction house:当前 40250 默认构建未发现完整的 ClientVS22 拍卖行 UI / handlerPacket.h 和服务端仍保留 __AUCTION__ 条件编译协议(客户端 CG_AUCTION_CMD=205、 服务端列表包 =206)。当前服务端默认关闭该宏,因此本阶段标注为不做;不能表述为“全源码、 协议和资产均不存在”。参考:Packet.h:158-160
  • 反作弊(anti-cheatCG_HACK(105 / 0x69) 客户端异常上报,以及桌面外壳的 ProcessCRC / ProcessScanner / 内存扫描等。自用客户端不需要,标注为不做, 除非服务端强制校验才回头做最小上报。
  • 龙魂精炼(当前 40250 目标服务端):目标服务端不提供该玩法,本阶段不实现 / 不验收; 客户端保留的兼容代码不计入当前支持范围。

变更记录

以下记录保留当时的增量快照;其中旧协议缺口数量、旧路径名称以及 0x 协议编号可能来自当时的 m2dev / 旧实现,不代表当前 Classic 审计口径。当前结论以“协议覆盖审计”和 MT_PROTOCOL 变体 说明为准;历史记录中的 CG_QUEST_CANCEL 也不代表 Classic 存在同名独立 header。

  • 2026-09-03 增量 99W3→M2 / §4.1 修改 5 远端 PC 真模型工厂)game_scene.gdch_type==0 创建 RemotePlayerView,复用 PlayerView 的 race / GR2 / MSA;新增远端专用 EquipModel.setup_remote(),只消费参考端 Packet.h:1282-1289 定义的 awPart[ARMOR=0,WEAPON=1,HEAD=2,HAIR=3],不读取本地 get_equipment()net_world.gdset_armor(parts[0])→set_weapon(parts[1])→set_head(parts[2])→set_hair(parts[3]) 对齐 NetworkActorManager.cpp:473remote_player_test.gd 以真实 warrior 资源覆盖建模、换装和字段序, gamescene_test / equip_model_test / net_world_vis_test / netplay_test / player_motion_test 全部通过。其他职业 / race、变身、真服多角色换装和精确视觉仍待验收。 详见 CLIENT-GAP-FIX.md §4.1 / C.5。
  • 2026-09-03 增量 100W4 / §6.3 EffectLib emitter cycle 生命周期首版):按 40250/ClientVS22/source/EffectLib/ParticleSystemData.cpp:38-60 对齐缺省 CycleLength=0.05ParticleSystemInstance.cpp:280-300 接入有限 cycle / LoopCount 的发射停止;重播时复位 CPU 时间门控,并移除把 cycle 长度错误写入粒子寿命的逻辑。fx_test 已覆盖 cycle、LoopCount、 独立粒子寿命和 OUTER fallback。OUTER / INNER 精确径向速度、每周期 emission residue / 时间曲线 重置、随机起始帧和每粒子随机语义仍待实现。详见 CLIENT-GAP-FIX.md §6.3 / C.5。

  • 2026-09-02 增量 73W2 / §3.7 受击状态统一):删除 net_play.gd 里参考端不存在的 _hitstun_until / HITSTUN 本地硬直计时器及全部引用。「能否行动」的判定统一走 _can_process_network_state() / _can_attack() / _can_use_skill() 三个函数:对齐 InstanceBase.cpp:1247(≈ ActorInstanceSync.cpp__IsSyncing = IsDead || IsStun || IsPushing)、InstanceBaseBattle.cpp:355NEW_Attack 多一个 IsUsingSkill)、:310。 C++ Entity 增加 knock_downentity_dict 暴露、mut_knockdown(vid,on) 落位并标脏); 本地玩家的击退态由受击 .msa 播放生命周期驱动——_on_damage 只置位 + 播 damage 动作 + 抖屏,_process 轮询 player_view.is_in_hit_reaction() 在动作播完时清零(player_view_on_playback_finished 增加 damage 一次性 clip 分支)。dead / stunned 保持由服务端 包驱动。combat_fx_test 断言改写并通过;C++ 19/19、netplay_test / netbridge_test / gamescene_test / player_motion_test 回归全绿。远端实体 knock_down 的接线、真实 40250 受击 / 击退 / AFFECT_STUN 清除时序仍待后续增量 / 真服验收。 详见 CLIENT-GAP-FIX.md §3.7 / C.5。

  • 2026-09-02 增量 74W2 / §3.3 普攻节奏硬编码 → 动作数据驱动)formats/msa.{h,cpp} 新增 Group ComboInputDataPreInputTime / DirectInputTime / InputLimitTime / LinkTime)与 Group AttackingDataAttackingType / HittingType / AttackingStartTime / AttackingEndTime / StiffenTime / InvisibleTime / ExternalForce)解析,对齐 EterGrnLib CGrannyMotion::LoadMotionDataCRaceMotionData::TComboInputData / TAttackingDatametin2_anim 新增并绑定 get_motion_data() 导出 duration / next_comboDirectInputTimeduration*0.9)等。net_play.gd 删除 ATTACK_RANGE 常量,改为 CLICK_DIST_DEFAULT_CM=150 / NPC_CM=500 / RESOURCE_CM=100 + _clickable_distance(te) (对齐 InstanceBaseBattle.cpp:294 NEW_IsClickableDistanceDestInstance);普攻节奏改为 _current_attack_period() = (.msa next_combo 或 DEFAULT_ATTACK_PERIOD) / (bAttackSpeed/100),收 info 包时 _apply_attack_speed() 只写 _atk_speed_factor 不再写死间隔。弓箭 __GetBowRange()AttackingData / HitPosition 驱动的命中帧 留待 §3.5。C++ 19/19formats.msa_loop_data 扩展断言)、combat_fx_test / netplay_test / netbridge_test / gamescene_test / player_motion_test / p2b_test 回归全绿。详见 CLIENT-GAP-FIX.md §3.3 / C.5。

  • 2026-09-02 增量 75W2 / §3.4 自动攻击目标预留状态机)net_play.gd 新增 enum ReservedMode {NONE, CLICK_ITEM, CLICK_ACTOR, CLICK_POSITION, USE_SKILL} 与预留字段 (对齐 PythonPlayerInput.cpp:820-865)。_process 改为每帧 _update_auto_attack()(照抄 PythonPlayer.cpp:207 __Update_AutoAttack 的目标存在 / 死亡 / 骑乘 / charge-skill / 睡眠分支)→ _refresh_reserved() 模式分派。 _reserve_process_click_actor() 严格 10 步对齐 PythonPlayerInput.cpp:877:出可点击距离 pc.walk_to 继续靠近、不可攻击走 client.click_npcIN_SAFE / DEST_IN_SAFE / EMPTY_ARROW 经新 signal cannot_act(code)、第 10 步按 §3.3 节奏 gate _do_attack_swing 后清预约并于下帧重新预约。安全区 / 弓箭模式 / 箭矢数 / 骑乘等级 / dash affect 等服务端 未下发的判定位先用实体字典键 + 本地占位变量接线(做法同 §3.7 knock_down)。CLICK_ITEM / CLICK_POSITION / USE_SKILL 分派留桩。 —— 增量 91 已补齐(见该增量条目)。netplay_test 新增 5b 段(出距离→walk_to 且不发包、 进距离→攻击且清预约、IN_SAFEcannot_act 且不发包、目标死→auto-attack 清零); C++ 19/19、combat_fx_test / netbridge_test / gamescene_test / player_motion_test / p2b_test / input_key_test 回归全绿。详见 CLIENT-GAP-FIX.md §3.4 / C.5。

  • 2026-09-02 增量 76W2 / §3.1 本地移动 Src/Dst 模型)player_controller.gd 去掉 _move_target / _has_target 的直线改坐标,改为 _src_pos / _dst_pos / _is_going / _dst_rot 模型。walk_to()_goto(dst_flat) 对齐 CInstanceBase::NEW_Goto() InstanceBaseMovement.cpp:251):flat.length() < MOVABLE_GROUND_DISTANCE_M(1.0 m) → 返回 true 不动(≈ __IsMovableGroundDistance);三门 _can_translate() = not frozen and not locked and not moving_skill 未过 → 存 _reserved_ground + RESERVED_GROUND_DELAY 0.1 s_process 里递减归 0 后自动重跑 _goto(≈ __ReserveClickGround / NEW_IsEmptyReservedDelayTime);moving_skill 只转向不平移。_process 每帧朝 _dst_pos 推进、抵达清 _is_goingWASD 分支置 _is_going = false(≈ NEW_MoveToDirectionm_isGoing = FALSE)。新增 project/player_move_test.gd5 组);C++ 19/19、netplay_test / combat_fx_test / gamescene_test / eterngrn_polish_test(既有无关 motion_event: sound FAIL/ player_motion_test / p2b_test / netbridge_test / mouse_controller_test 回归全绿。 未做:Actor 间碰撞(TestActorCollision / BlockMovement 的 Area3D 近似)、坐骑 MODE_HORSE*(依赖 §6.4 + §3.2)。详见 CLIENT-GAP-FIX.md §3.1 / C.5。

  • 2026-09-02 增量 77W2 / §3.2 远端同步命令队列 —— C++ 核心 part A)EntityStore 把远端 Actor 位置改为命令队列驱动(对齐 InstanceBase.cpp:1278 CInstanceBase::StateProcess), 不引入预测回滚 / 抖动缓冲。Entity 新增 std::deque<StateCmd> state_queue + mov_after_func / mov_after_arg / mov_after_rot / bool skip_collisionmut_move() 改为把 {chk_time,dst,rot,func,arg,dur_ms} 入队(≈ PushTCPState)后 drain_state_queue() 释放 到期项;删掉旧 start_move() 直改坐标路径,m2dev apply() 的 GC_MOVE 与 classic parser 的 HDR_GC_MOVE 都统一走 mut_move(vid, angle, func, tx, ty, dur, arg, time)。新增 process_states() + apply_state_cmd() 1:1 实现 StateProcess 的 switchFUNC_WAIT dir_len>1.0 → 走 + m_kMovAfterFunc=WAITelse SCRIPT_SetPixelPosition 贴齐;FUNC_MOVE 恒走;FUNC_COMBO/ATTACK/MOB_SKILL dir_len>=50.0 → 走到位再动作,else 贴齐 + 动作;所有 「走过去」分支置 skip_collision(≈ __EnableSkipCollision),tick() 到达帧据 mov_after_func 落地 COMBO/ATTACK/MOB_SKILL 动作并清标志。释放门按新 set_server_frame_ms() m_server_frame_ms == 0 = 帧钟未对齐 → 立即释放 = 退回增量 76 时序,无回归)+ __CanProcessNetworkStatePacketdead / knock_down 时挂队不应用,恢复即释放)。 SNetworkActorData::UpdatePosition 式线性插值只保留给 §2.1 可见性、不再直接写节点位置的 工作留给增量 78。新增 extension/tests/net_state_queue_test.cpp5 组:帧钟释放门 / FUNC_WAIT 1.0 阈值 / FUNC_COMBO 50.0 阈值 + 到达后动作 / knock_down·dead 门 / 多条到期按序 drain); net_entity_test 的 FUNC_ATTACK 断言按新语义拆成近(贴齐)/ 远(先走后打)两例。 ctest --test-dir build → 20/20(新增 net.state_queue);netbridge_test / gamescene_test / netplay_test / player_move_test / p2b_test / combat_fx_test 回归全绿。 留给增量 78net_play.gd 上行 6 事件回调 + 节流常量(100/300 ms + disp≥1.0)、net_world.gd 去掉自有 lerp、从 GC_HANDSHAKE lDelta 接真实服务器帧钟;__IsEnableTCPProcess 表情锁与 Actor 碰撞近似另计。详见 CLIENT-GAP-FIX.md §3.2 / C.5。

  • 2026-09-02 增量 78W2 / §3.2 上行 6 事件回调 + net_world 去 lerp —— part B net_play.gd 的上行按 PythonPlayerEventHandler.cpp 拆成 6 个回调,统一走新出口 _send_state(func,arg,pos)(≈ SendCharacterStatePacket / TPacketCGMove):OnMove pc.moved 首帧)立即发 FUNC_MOVE 并重置 moving/waiting 两个节流窗,OnMovingSEND_MOVING_INTERVAL=0.3s 节流发 FUNC_MOVEOnWaiting(新 _tick_on_waiting() _process 每帧)按 SEND_WAITING_INTERVAL=0.1s 节流且相对上次上行位移 ≥ 1 cm 才发 FUNC_WAITOnStopanim_state("wait") 且刚在移动)不节流收尾 FUNC_WAITOnAttackclient.attack()(CG_ATTACK) 外额外 _send_state(FUNC_COMBO, attack_motion+_combo, pos),新增公共入口 on_use_skill(motion,arg)_send_state(FUNC_SKILL|(motion&0x7F), arg, pos)(真实调用点属 §3.8 player_skill.gd,此处仅留 1:1 出口)。删掉旧的 SEND_INTERVAL / SEND_DIST / _last_sent_t 单一节流。net_world.gd_process 改为 n.position = _grounded(e.pos) 直接跟随,去掉自有 follow_lerp 二次平滑——位置 插值现在完全在 C++ EntityStore::tick()(增量 77 命令队列 + SNetworkActorData:: UpdatePosition 式线性插值)里做,只朝向仍在 GDScript 侧 lerp_angle 平滑;follow_lerp / snap_dist 字段保留仅为兼容。netplay_test 新增第 11 段(OnMove / OnMoving 节流 / OnStop / on_use_skill)、观战段基线改「相对计数不变」,netbridge_test 的 near-move 断言改「node follows e.pos directly」。C++ 20/20 不变;netbridge_test / gamescene_test / netplay_test / player_move_test / p2b_test / combat_fx_test / player_motion_test 回归全绿。留给增量 79:从 GC_HANDSHAKE lDelta 接真实服务器毫秒帧钟 ClassicStream 已存 m_server_time_base / m_client_time_base,需暴露访问器并穿到 EntityStore::set_server_frame_ms());__IsEnableTCPProcess 表情锁、GC_SYNC_POSITION 是否清队列另计。详见 CLIENT-GAP-FIX.md §3.2 / C.5。

  • 2026-09-03 增量 79W2 / §3.2 真实服务器毫秒帧钟 —— part C,§3.2 收官) ClassicStream 暴露 server_frame_ms()classic_stream.h:107 / .cpp:48,≈ ELTimer_GetServerFrameMSec())——HDR_HANDSHAKE 处理里早已存的 m_server_time_base = hs.time + lDelta / m_client_time_base = now_ms() classic_stream.cpp:266-267),帧钟 = now_ms() - m_client_time_base + m_server_time_base,首个握手前返回 0;另加 has_server_clock()ClassicSession::process()m_world.tick()m_world.set_server_frame_ms(m_stream.server_frame_ms())classic_session.cpp:307), classic 后端 drain_state_queue() 遂按服务器时间放行命令队列而非即时释放。m2dev 后端无握手 lDelta 对应物,m_server_frame_ms 恒 0 = 即时释放 = 增量 76 时序,无回归。 EntityStore::drain_state_queue 加防呆阀 STATE_QUEUE_MAX_WAIT_MS = 1000 entity_store.h)——chk_time 领先帧钟超过 1 s 视为异帧钟 dwTime,立即放行以免 远端 Actor 永久冻结;参考端 StateProcess 无此上限,属有意偏离,已在 CLIENT-GAP-FIX.md §3.2 记录。net_classic_stream_test 加帧钟段(握手前 0 + !has_server_clock()、握手后 ≈ hs.time + lDelta、单调不减),net_state_queue_test 加第 6 段(防呆阀边界)。 C++ 20/20、7 个 GDScript 回归全绿。§3.2 至此完成,真实服务器 GC_MOVE dwTime 时基是否与握手帧钟一致仍待联机核验。详见 CLIENT-GAP-FIX.md §3.2 / C.5。

  • 2026-09-03 增量 80W2 / §3.5 PC 连击段表解析 —— part A):查明参考端 PC 的连击 段表 不在 .msm 里(.msm 只有 BaseModelFileName + HairData),而是由 assets/root/playersettingmodule.pychrmgr.ReserveComboAttackNew / RegisterComboAttackNew 调用在 CRaceData 上建起来(PythonCharacterManagerModule.cppCRaceData::ReserveComboAttack / RegisterComboAttackRaceData.cpp:405)。新增 formats/combo_table.{h,cpp}fmt::PlayerComboTables):按 def __LoadGame<Class>Ex 分四个职业块(男女同函数体 → 表按职业收),解析那批调用为 map<MAKE_COMBO_KEY(motionMode, comboType), vector<段号>>(段号 = NAME_COMBO_ATTACK_* = 14..21),语义对齐 CRaceDataReserve 建长 count 的 0 向量(map::insert 重复 key 不覆盖)、Register 按下标写(key 未 Reserve 或下标越界即跳过)。认得 chr.MOTION_MODE_*= CRaceMotionData::EMode)、chr.MOTION_COMBO_ATTACK_N= 13+N)、 chr.MOTION_NORMAL_ATTACK= 13)、COMBO_TYPE_N / COMBO_INDEX_N= N1)、裸整数。 新增 formats_combo_table_test(合成片段 + M2_ASSETS 时对真实 playersettingmodule.py 断言 warrior/sura 1H、shaman FAN),C++ 21/21 + 7 个 GDScript 回归全绿。留给增量 81: player_controller.gd 连击状态机(_cur_combo_index / _is_pre_input / _is_next_pre_input)改用此表 + .msa ComboInputData 三时间,替掉 net_play.gd _do_attack_swing(_combo + 1) % 3;之后 §3.5 命中窗(.msa HitDataContainer

    • 命中判定 / OnHitCG_ATTACK / FlushVictimList 帧末 CG_SYNC_POSITION。 详见 CLIENT-GAP-FIX.md §3.5 / C.5。
  • 2026-09-03 增量 81W2 / §3.5 PC 连击状态机 —— part B)net_play.gd 用增量 80 的 连击段表 + .msa ComboInputData 三时间实现连击状态机,替掉 _do_attack_swing(_combo + 1) % 3。新增 _do_attack_swing / _run_next_combo / _combo_process / _on_end_combo / _clear_combo,逐行对齐 CActorInstance::InputComboAttackCommand / __RunNextCombo / ComboProcess / __OnEndCombo / __ClearCombo ActorInstanceBattle.cpp:157/213/256/297/308):_combo_index==0 直接推进;否则按 pre_input_timeInputStartTime/ direct_input_timeNextComboTime/ input_limit_timeInputEndTime)判 _is_pre_input 挂起 / 直接推进 / 弓模式 _is_next_pre_input;无 combo 输入组用 duration*0.9 兜底;_combo_process() 每帧触发 挂起输入、动作回 Wait 时 _clear_combo();段号越界不推进;末段仅骑乘复位到 1。段号取自 新静态绑定 Metin2AnimPlayer.parse_combo_tables(py_path)extension/src/metin2_anim.{h,cpp} 转发 fmt::parse_player_combo_tables_file),按 race & 3 选职业块、 combo_motion_mode<<16|type 取段序列(combo_motion_mode 默认 GENERAL,武器→mode 映射 留给增量 82+);缺段表退化为单段普攻。_emit_swing 保持增量 78 的两包 CG_ATTACK{bType=0} + CG_CHARACTER_MOVE{FUNC_COMBO, 段号})。game_scene.gd 装配时 net_play.set_asset_root(_assets)combat_fx_test.gdFakeAnim + 连击状态机全路径断言。 C++ 21/21 + 7 个 GDScript 回归全绿。留给增量 82+:.msa HitDataContainer 多命中窗 导出、THitData 窗口命中判定 + OnHit/CG_ATTACK + FlushVictimList 帧末 CG_SYNC_POSITION、武器种类 → combo_motion_mode 映射。 详见 CLIENT-GAP-FIX.md §3.5 / C.5。

  • 2026-09-03 增量 82W2 / §3.5 .msa 多命中窗导出 —— part C,修改 1)formats/msa.{h,cpp}Group AttackingDataGameLib GameType.cpp NRaceData::LoadMotionAttackData 的两种 写法解析成 std::vector<Msa::HitWindow>:老式(组内直接 AttackingStartTime/…/List HitPositionHitDataCount= 1 窗;新式(AttackType + MotionType + HitDataCount N + Group HitData00..0N)= N 窗(多段挥击 / 双持左右手 = 2 窗)。每窗带 start_time/end_time/bone/weapon_length + List HitPosition 采样({time, last_pos, pos} 7 浮点一组);新增 motion_type / hit_limit_countattacking_type 兼容 AttackType/AttackingTypeattack_start_time/attack_end_time 保留为首窗起止(旧调用兼容)。 formats/textscript.cppList Name { ... } 块解析(原来 List 会把 { 吞进普通行、 还串掉其后的 Group——palbang.msaMotionEventData 就丢了)。 extension/src/metin2_anim.cpp get_motion_data() 新增 hit_windows 数组 {start_time,end_time,bone,weapon_length,samples:[{time,last_pos:Vector3,pos:Vector3}]}

    • motion_type / hit_limit_countformats/tests/msa_test.cpp 加内联单窗 / 新式双窗合成 断言 + M2_ASSETS 时对真实 warrior/general/attacktwohand_sword/combo_05assassin/dualhand_sword/combo_01R Hand + L Hand 双窗)、warrior/skill/palbang 断言。 C++ 21/21 + 7 个 GDScript 回归全绿。留给增量 83+:修改 4(hit_window [start,end] 内 几何判定 + OnHitCG_ATTACK + FlushVictimList 帧末 CG_SYNC_POSITION)、 武器种类 → combo_motion_mode 映射。 详见 CLIENT-GAP-FIX.md §3.5 / C.5。
  • 2026-09-03 增量 83W2 / §3.5 命中窗几何判定 → OnHit,修改 4 / §3.5 收官) net_play.gd 新增 _begin_hit_windows() / _attack_process(dt) / _hit_deduped() / _register_hit() / _hit_geometry() / _flush_victim_list(),逐帧按动作时间落在某个 hit_window [start,end] 内对 client.get_entities() 里可攻击 / 未死 / 双方非安全区的实体 做几何判定(对齐 InstanceBaseBattle.cpp:413 AttackProcess + ActorInstanceCollisionDetection.cpp:333 __NormalAttackProcess):_hit_deduped 复刻 m_HitDataMapMOTION_TYPE_COMBO 同窗同目标一次、其余按 invisible_time 冷却), _register_hit 复刻每窗命中上限(NORMAL/COMBO 16、SKILL hit_limit_count),命中 → client.attack(_swing_skill, vid)OnHitSendAttackPacket,普攻 _swing_skill=0), 被击退目标帧末 client.sync_positions()(≤16,对齐 PythonPlayerEventHandler.cpp:194 FlushVictimList)。_emit_swing() 因此改为只有拿不到命中窗数据headless / 缺资源) 才在挥击瞬间立即发 CG_ATTACK。~~几何判定用 reach = max(WeaponLength, 窗内采样最大水平偏移)

    • 正面 ±60° 弧近似(真实动态圆柱扫掠需骨骼矩阵 + defending sphere,随 §3.1 留桩)。~~ 增量 130 已换成 Z 圆柱 vs .msm 防御球的逐行判定。 combat_fx_test.gdFakeNetWorld + 6 段断言。C++ 21/21 + 7 个 GDScript 回归全绿。 留给增量 84:武器种类 → combo_motion_mode 映射。 详见 CLIENT-GAP-FIX.md §3.5 / C.5。
  • 2026-09-03 增量 84W2 / §3.5 武器种类 → combo_motion_mode,修改 6 / §3.5 修改 1-6 全落地) net_play.gd 补全 MOTION_MODE_*CRaceMotionData::EMode 全 17 项)+ ITEM_TYPE_WEAPON/ROD/PICK + WEAPON_SUB_* 常量,新增静态 motion_mode_for(item_type, weapon_sub, mounted, poly, dress) 逐分支复刻 CInstanceBase::RefreshStateSetMotionMode InstanceBase.cpp:2851,顺序:Poly→GENERAL / 婚纱→WEDDING_DRESS / 镐→骑乘 HORSE 否则 GENERAL / 钓竿→骑乘 HORSE 否则 FISHING / 骑乘 switch(subType)HORSE_* / else switch(subType)ONEHAND/TWOHAND/DUALHAND_SWORD·BOW·FAN·BELL·默认 GENERAL), _equipped_weapon_vnum()(主角 parts[WEAPON] 非 0 优先,否则 get_equipment()[WEAR_WEAPON].vnum

    • _refresh_motion_mode()(读 mount_vnum / polymorph / wedding_dress + proto.item(vnum)type/sub_typeset_combo_motion_mode(...),换模式内部 _clear_combo()),由 _on_entity_info() 末尾、inventory_changedwindow==2 EQUIPMENT)、mount_changed(主角)三处触发。 combat_fx_test.gdFakeProto + 15 段 motion_mode_for 断言 + 2 段端到端(换匕首 → DUALHAND_SWORD_combo_index 归零;再上马 → HORSE_DUALHAND_SWORD)。C++ 21/21 + 7 个 GDScript 回归全绿(equip_model_test 亦 PASS)。留给增量 85+:弓箭 __GetBowRange() (§3.5 命中 / 点击距离)、§3.6 箭矢 MOTION_EVENT_TYPE_FLY 事件。 详见 CLIENT-GAP-FIX.md §3.5 / C.5。
  • 2026-09-03 增量 85W2 / §3.5 弓箭 __GetBowRange() 可点击 / 攻击距离,§3.5 修改 1-7 全落地) net_play.gd _clickable_distance() 逐分支对齐 CInstanceBase::NEW_IsClickableDistanceDestInstanceInstanceBaseBattle.cpp:294): 默认 150 cm → _is_bow_mode()combo_motion_mode ∈ {MODE_BOW, MODE_HORSE_BOW} 对齐 CActorInstance::IsBowMode ActorInstance.cpp:258)时改用 _bow_range_cm() → 目标 IsNPC() 覆盖为 500 → IsResource() 覆盖为 100(后写的 if 覆盖前面)。 _bow_range_cm() = BOW_RANGE_BASE_CM2500-100+ _bow_distance_bonus1:1 复刻 CInstanceBase::__GetBowRangeInstanceBase.cpp:6942500.0f-100.0f + 主角 GetStatus(POINT_BOW_DISTANCE),参考端按 cm 直加不 ×100)。_bow_distance_bonus_on_points()points["bow_distance"] 填充;wire.hPOINT_BOW_DISTANCE = 34M2Client::get_points()d["bow_distance"]combat_fx_test.gd §3.5 段加 8 段断言。 C++ 21/21 + 7 个 GDScript 回归全绿(equip_model_test 亦 PASS)。留给增量 86:§3.6 箭矢 .msa MOTION_EVENT_TYPE_FLYCG_FLY_TARGETING / CG_SHOOT,替掉约 1.5 s 定时器兜底。 详见 CLIENT-GAP-FIX.md §3.5 / §3.3 / C.5。

  • 2026-09-03 增量 86W2 / §3.6 弓箭 FLY 事件 OnSetFlyTarget / OnShoot,§3.6 修改 1-3 全落地) 拆掉 game_scene.gd 单槽 _pending_shoot_skill + get_tree().create_timer(1.5) 兜底,换成 _pending_shots: Array[int]FIFO,上限 MAX_PENDING_SHOTS = 8,满丢最旧),1:1 对齐 CNormalBowAttack_FlyEventHandler_AutoClear:每次弓挥击 / 弓技能起手 _queue_shot(uSkill) 压一个,.msa MOTION_EVENT_TYPE_FLY 帧(_on_local_motion_eventtype 6pop_front() 弹一个 client.shoot()pop 即自清 = AutoClear,连射多箭在途各自一发;对齐 CActorInstance::ProcessMotionEventFly ActorInstanceMotionEvent.cpp:290)。OnSetFlyTarget ActorInstanceMotion.cpp:372__SetMotion__IsNeedFlyTargetMotion() 为真即触发)放在 net_play.gd._emit_swing_is_bow_mode() 时先 _send_fly_target()_target_vid 回退 _vid_reservednet_world 节点 → MapCoord.to_server_cm()client.add_fly_targeting(vid, x, y) 对齐 SendFlyTargetingPacket(targetVID, target->OnGetFlyTargetPosition())OnGetFlyTargetPosition = 目标包围球中心世界坐标,平面分量与脚点一致,CG_FLY_TARGETING 只带平面坐标, ActorInstanceFly.cpp:4),再 bow_shot_fired.emit(_swing_skill)game_scene 入队。 新增 net_play 信号 bow_shot_fired(skill) + _send_fly_target()game_scene._queue_shot()combat_fx_test.gdFakeClient.add_fly_targeting/shoot + 6 段 §3.6 断言(非弓无 FLY / 弓挥击发 CG_FLY_TARGETING(vid) / 坐标 = 目标 server-XY / bow_shot_fired(0) / 无目标不发), eterngrn_polish_test.gd FLY 段改 FIFO 连射断言。C++ 21/21 + 9 个 GDScript 回归全绿。 留给后续:飞行物实体本身(CFlyingManager / FlyingInstance 生成 / 轨迹 / 命中)、__CanShot 弓箭合法性、真服 CG_FLY_TARGETING z 分量。详见 CLIENT-GAP-FIX.md §3.6 / C.5。

  • 2026-09-03 增量 87W2 / §3.8 TSkillData 判定位解析,修改 2POC 无 C++ skill proto extension/src/proto 只有 item/mob),技能数据全在 GDScript project/ui/skill_table.gdlocale/locale/<lang>/skilldesc.txt TSV。按 RegisterSkillDescPythonSkill.cpp:280)把 第 11 列 attributes / 第 12 列 weapon 的 | 分隔记号折成 attr_bits / need_weapon_bits 位掩码(_fold_bits() 静态方法;记号名 1:1 抄 PythonSkill.h:92-128SKILL_ATTRIBUTE_* / SKILL_NEED_WEAPON_*,未知记号忽略 = 参考端“map 查不到就跳过”;DOUBLE_SWORD 复用 DAGGER 位;weapon 位 = 1 << CItemData::WEAPON_*GameLib/ItemData.h:63-75),load_file 时连同 skill_typeJOB 列经 m_SkillTypeIndexMapSKILL_TYPE_{NONE,ACTIVE,SUPPORT,GUILD,HORSE}) 一起预存进 entry。谓词逐条对齐 CPythonSkillis_can_use_skill=非 PASSIVE:1407)、 is_standing / is_need_target / is_need_corpse / is_auto_search_target / can_change_direction / is_horse_skill / is_moving_skill / is_melee / is_charge_skill / is_use_hp / is_time_increase / is_need_empty_bottle / is_need_poison_bottle / can_use_for_me / is_only_for_alliance / can_use_if_not_enough / is_only_for_guild_war / is_fan_range / is_circle_range / has_weapon_limitation / can_use_weapon_type(id,type) =无限制恒真否则 dwNeedWeapon & (1<<type):1143/ is_need_bow / skill_type_of / is_guild_skill;旧 is_attack / is_passive / is_toggle / is_ranged / can_level_up 改为查位(is_ranged 现含 ARROW 位)。skill_test.gd +34 条 §3.8 断言。C++ 21/21 + 9 个 GDScript 回归全绿。留给增量 88+:player_skill.gd 移植 (a)(b)(c)(d) 三层校验、 M2Client::use_skill() 带符号 + 范围校验(§3.9)。详见 CLIENT-GAP-FIX.md §3.8 / C.5。

  • 2026-09-03 增量 88W2 / §3.8 player_skill.gd 三层合法性校验,修改 1:新建 project/player_skill.gdextends RefCounted,纯校验器,不发包 / 不驱动动画,返回 {"ok", "code", "target_vid"}),逐行移植 PythonPlayerSkill.cpp 的四层:(a)click_skill_slot() 分派(:106)、(b)can_use_skill() 全局可用性(:439,硬编码骑乘术 vnum 109:骑乘中且 grade<1 && level<20 → 禁)、(c)check_skill_usable() 逐项前置(:166 完整顺序:骑乘⇄马术 NOT_HORSE_SKILL / HAVE_TO_RIDE → 安全区攻击技 IN_SAFE → 被动 → 空瓶 27995 → 毒瓶 27996 → 钓鱼 REMOVE_FISHING_ROD → 未学 NOT_YET_LEARN → 武器不匹配 NOT_MATCHABLE_WEAPON → 无箭 EMPTY_ARROW → dash / 蓄力 / 冷却 WAIT_COOLTIME → HP / SP)、(d)use_skill() 施放 (私人商店静默、__CheckSpecialSkill 钓鱼 123 / 连击 122、切换技 TOGGLE_OFF、施法中 ALREADY_CASTING、目标矩阵 _resolve_target() 同盟 / 安全区 / 可攻击 / 尸体 → CANNOT_USE_SELF / ONLY_FOR_ALLIANCE / CANNOT_ATTACK_ENEMY_IN_SAFE_AREA / CANNOT_ATTACK / ONLY_FOR_CORPSE / NEED_TARGET)。命名常量 SKILL_MAX_NUM=255Packet.h:2041/ SKILL_TYPE_GUILD=3 / RIDING_SKILL_INDEX=109 / ITEM_EMPTY_BOTTLE=27995 / ITEM_POISON_BOTTLE=27996 / SKILL_INDEX_COMBO=122 / SKILL_INDEX_FISHING=123 PythonPlayerModule.cpp:6-7);SILENT_CODES[] 13 个无文案码 + static is_silent_code()。 运行期状态尽量走 clientget_main_vid / get_entity / get_skills / get_target / get_inventory),其余经公有 context 字段由 net_play.skill_context() 每次注入(观察 / 骑乘 / 安全区 / 钓鱼 / 施法中 / 可行动 / dash / weapon_sub_type / arrow_count)。ui/quickbar.gd activate() 技能分支施放前跑三层,TOGGLE_OFF → 关闭切换技 + skill_activated,非静默码 → 新信号 skill_rejectedgame_scenenet_play.cannot_act.emit(code)HUD 走同一条 OnCannotUseSkill 文案通道),gate 解析出的 target_vid 覆盖点击目标。__CheckShortMana / __CheckShortLife 打桩恒 falsePOC 无 .msk NeedSP/NeedHP 公式解析,留给增量 89)。 新增 project/player_skill_test.gd(读真 skilldesc.txt,覆盖 (a)(b)(c)(d) 每条分支 + 目标矩阵 + is_silent_code)。C++ 21/21 + 12 个 GDScript 回归全绿。留给增量 89+: 修改 3(M2Client::use_skill() 带符号 + 范围校验,§3.9)+ HP/SP 校验落地。 详见 CLIENT-GAP-FIX.md §3.8 / C.5。

  • 2026-09-03 增量 89W2 / §3.8 修改 3 —— use_skill 边界收紧 + 短蓝短血校验落地,§3.8 三层全落地)extension/src/net/net_bounds.h 新增 SKILL_MAX_NUM=255Packet.h:2041+ skill_index(v) = 0 < v < 255 谓词;extension/src/net/m2_client.cpp::use_skill(int skill_id, int target_vid)(形参增量 52 起即带符号)的 skill_id 护栏从 skill_id <= 0 || !bounds::u32() 收紧成 !mtnet::bounds::skill_index(skill_id),对齐 ClickSkillSlotslot >= SKILL_MAX_NUM 门;target_vid 保留 >= 0 && bounds::u32cast_skill()motion_idx ∈ [0,0x7f] 不动(7 位 wire 位段)。net_bounds_test.cpp +4 断言。② ui/skill_table.gd _load_cooltime 更名 _load_msk,加解析 .msk NeedSPFormula;新增 _SKILL_POWER[] LocaleService_GetSkillPower INTERNATIONAL 表,Locale.cpp:126level >= 50 → 0+ _skill_point(level) + need_sp(id, level)(≈ GetNeedSPPythonSkill.cpp:1426POC 约定: 无 NeedSPFormula 返回 -1,参考返回 0,避免误挡缺字段技能)+ need_hp()(照抄 __CheckShortLife:359 复用 GetNeedSP 的怪癖);cooldown_ofSkillPoint 也改走 _skill_point()(与参考同一份 fcurEfficientPercentage)。③ project/player_skill.gd 新增 SKILL_INDEX_SUMMON=131PythonPlayerModule.cpp:15+ 上下文字段 cur_sp / cur_hp -1 = 未知不判),_check_short_mana 逐条移植 __CheckShortMana:320summon 131 豁免、 非开关技 need_sp == 0 → 坏数据拦、can_use_if_not_enoughcur_sp <= 0 拦、否则 need_sp != -1 && need_sp > cur_sp 拦),_check_short_life 移植 __CheckShortLife:354 !is_use_hp() → false 放行、need_hp > cur_hp 拦;shipped skilldesc.txt 无 USE_HP 技能故 对现有技能等价放行),二者转 NOT_ENOUGH_SP / NOT_ENOUGH_HP。④ net_play.gd::skill_context()client.get_points()hp / spcur_hp / cur_sp。⑤ player_skill_test.gd +“(c) §3.8 修改 3”段(_skill_point(20)==0.5 / need_sp(1,5)==52 / need_hp==need_sp / SP 不足→NOT_ENOUGH_SP / SP 未知→ok / 无公式技 + SP 0→不挡 / 非 USE_HP 技 + HP 1→不报), _resetcur_hp=-1 / cur_sp=-1。C++ 21/21 + 13 个 GDScript 回归全绿。§3.8 三层校验(修改 1/2/3)至此全部落地。留给增量 90+:§3.10 技能升级门(audit 10)、 __ChangeTargetToPickedInstance 拾取回退、扇形 / 圆形多目标 fly-targeting。 详见 CLIENT-GAP-FIX.md §3.8 / §3.9 / C.5。

  • 2026-09-03 增量 90W2 / §3.10 技能升级门 —— 加号按钮门控 + skill_up 边界收紧): 审计条目 10 的门控空缺补齐。① extension/src/net/wire.h EPointTypesPOINT_SUB_SKILL=27 / POINT_SKILL=28 / POINT_HORSE_SKILL=113UserInterface/Packet.h 对应 uicharacter.py skillPageStatDict);m2_client.cpp::get_points() 新增 skill_active / skill_support / skill_horse 三键(镜像增量 85 的 bow_distance)。 ② m2_client.cpp::skill_up(int skill_id) 护栏 skill_id >= POINT_COUNT 收紧成 !mtnet::bounds::skill_index(skill_id)= 0 < v < 255,与 use_skill §3.9 同一谓词); 发包 /skillup <idx> 不变。③ project/ui/skill_table.gdload_fileRegisterSkillDescLEVEL_LIMIT(列 26/ MAX_LEVEL(列 27)解析(.msk maxlevel 优先;shipped 22 列 数据两列都缺,逻辑为兼容第三方数据);新增 max_level_of(id)= SSkillData::GetMaxLevel() PythonSkill.cpp:1392,缺值默认 20 = 构造默认 byMaxLevel:1462)、skill_level_limit(id) = skillGetSkillLevelLimitPythonSkill.cpp:1741 = byLevelLimit本 40250 数据里 skilldesc 无 LEVEL_LIMIT 列、.msklevellimit 记号 → 恒 0,马术页等级前置门恒真)、 can_level_up_skill(id, level)= skillCanLevelUpSkillPythonSkill.cpp:1833本 build 里 isRequirement / strRequireSkillName / RequireStatDataVector 从不被填充(构造默认,:1471) → 1:1 化简为 level < GetMaxLevel() && !(attr & CANNOT_LEVEL_UP))。④ project/ui/skill_ui.gd refresh() 里逐行移植 uicharacter.pyRefreshSkillPlusButtonList:984/ __RefreshSkillPlusButton:937/ CanShowPlusButton:925)——_can_use_skill_now() = client.get_skill_group() != 0,对应 __CanUseSkillNow :1270)、_stat_point_for(page) SUPPORTskill_support / HORSEskill_horse / 其余→skill_active,三来源分开取)、 _can_show_plus(id, level, grade, page, …)(技能组门 → 辅助页 HIDE_SUPPORT_SKILL_POINT 整页隐 → stat_point<=0 隐 → skillGrade!=0 隐 → 马术页 plv >= skill_level_limit(id)level < 20 才显 → 主动页 table.can_level_up_skill(id, level));按钮仍在 _rebuild() 为 非被动 / 非 CANNOT_LEVEL_UP 技能创建,refresh() 只切 visible= HideAllSlotButton() + ShowSlotButton());常量 HIDE_SUPPORT_SKILL_POINTuicharacter.py:21,本资源为 True → 辅助页整页不出加号)/ RIDING_SKILL_MAX_LEVEL=20。升级结果仍由 GC_SKILL_LEVELskills_changed 驱动 refresh(),不本地伪造。⑤ skill_test.gd FakeClient 补 get_skill_group() / get_points();扩 §3.10 段(max_level_of / skill_level_limit / can_level_up_skill 未满级 / 满级 / 超级 / CANNOT_LEVEL_UP / 未知 id + 加号显隐:主动页 可见 / skillGrade!=0 隐 / skill_group==0 隐 / 该页点数 0 隐 / 辅助页整页隐 / 马术页 skill_horse>0 && level<20 可见 / 点数 0 隐)。C++ 21/21 + 13 个 GDScript 回归全绿。 留给增量 91+:__ChangeTargetToPickedInstance 拾取回退、扇形 / 圆形多目标 fly-targeting、 §3.6 飞行物实体(CFlyingManager / FlyingInstance)。 详见 CLIENT-GAP-FIX.md §3.10 / C.5。

  • 2026-09-03 增量 91W2 / §3.4 其余三种预约模式 + __ChangeTargetToPickedInstance + 扇形 / 圆形补 fly-targeting: § 3.4 自增量 75 起留桩的 MODE_CLICK_ITEM / MODE_CLICK_POSITION / MODE_USE_SKILL 三个 分派补齐。① project/net_play.gd _refresh_reserved() 由「只跑 CLICK_ACTOR」改为四路 match,逐行对齐 NEW_RefreshMouseWalkingDirectionPythonPlayerInputMouse.cpp:287): _reserve_process_click_item()(按 IID 查 client.get_ground_items() 世界坐标, < 20cmCLICK_ITEM_PICKUP_CM)到手 → _send_state(FUNC_WAIT) + client.pickup_item() + 解冻 + _clear_reserved(),否则 pc.walk_to();物品消失 → _clear_reserved())、 _reserve_process_click_position()pc._can_translate() 未锁且 _reserved_delay <= 0 _process 逐帧递减 ≈ NEW_IsEmptyReservedDelayTime)→ pc.walk_to(_ppos_reserved) + _clear_reserved())、_reserve_process_use_skill()dist_cm_is_huge_race-200cmHUGE_RACE_DIST_FIX_CM)修正,< _skill_range_reservedset_target + use_skill_hook(slot) 成功才 _clear_reserved(),否则 pc.walk_to(tnode);目标丢失 → _clear_reserved())。② 预约入口 reserve_click_item(iid) / reserve_click_ground(pos) _reserved_delay = 0.1/ reserve_use_skill(vid, slot, range_cm)= __ReserveUseSkill PythonPlayerInput.cpp:854range > 100 先减 RESERVE_SKILL_RANGE_TRIM_CM(10)/ pick_ground_item(iid)= __OnPressItem——清预约 + auto-attack 归零,CLICK_DIST_RESOURCE_CM 内直接 pickup_item + 停,否则预约)。_clear_reserved() 追加复位 _ppos_reserved / _skill_range_reserved。③ _on_pick()__OnPressActor 特例(PythonPlayerInput.cpp:324): _reserved_mode == USE_SKILL && _dash_active 时只改 _vid_reserved 不选中;战斗目标改走 _change_target_to_picked_instance(vid)= __ChangeTargetToPickedInstance :186 → SetTarget :130——IsTargetableInstance 限定 PC / 怪,否则 set_target(0) + clear_target NPC / 石头 / warp 仍由 _on_pick 先分流到 click_npc)。_is_huge_race(race)——IS_HUGE_RACE 的 vnum 白名单未随源码发布,改用 proto.mob(race)size >= MOBSIZE_HUGE(3)rank >= BOSS(4) 近似,查不到 → false(文档化 seam)。④ 扇形 / 圆形补 fly-targeting PythonPlayerSkill.cpp:680-746 if (dwTargetMaxCount>0 …) 块):net_play.send_fly_targeting( target_vid, range_cm, max_count, shape)——主目标 CG_FLY_TARGETING + CG_USE_SKILL 仍由 M2Client::use_skill 发,这里只补附加目标——_instances_in_fan_range()= NEW_GetInstanceVectorInFanRange InstanceBaseBattle.cpp:192:随距离收窄半角 (MAX-MIN) - RPM·min(dist,1000) + MINFAN_HALF_ROT_MIN=20 / MAX=40 / MIN_DISTANCE=1000/ _instances_in_circle_range()(:243,无方位过滤)内其它可攻击敌人经 client.add_fly_targeting(vid, sx, sy)= SendAddFlyTargetingPacket)补齐;名额不足时 _random_pos_in_fan_range()= NEW_GetRandomPositionInFanRange :124:目标方位 ±10° FAN_RANDOM_JITTER_DEG)抖动、沿该方向 FAN_RANDOM_REACH_CM(8000cm) 取点)→ add_fly_targeting(0, sx, sy)。方位角在 Godot world 帧统一用 _bearing_deg(角差与坐标系无关)。 ⑤ project/ui/skill_table.gd_load_msk.msk TargetCountFormula / Range 解析 CMotionSkillDatastrTargetCountFormula / dwTargetRange);新增 target_count(id, level)= GetTargetCount(fSkillPoint) PythonSkill.cpp:1361——空公式 → 0 否则 SkillPoint 代入后 floor)、target_range(id)= __GetSkillTargetRange 基础项 PythonPlayerSkill.cpp:391,弓距加成留 net_play 叠加)、fly_shape(id)FAN_RANGE→1 / CIRCLE_RANGE→2 / 否则 0,对齐 NetPlay.FlyShape)。⑥ project/ui/quickbar.gdactivate()client.use_skill 成功后,若 target_vid != 0 且技能为 FAN_RANGE / CIRCLE_RANGE → 调 net_play.send_fly_targeting(...);新增 _skill_level(sid)(扫 client.get_skills())、 activate_reserved(global_slot)use_skill_hook 回调,MODE_USE_SKILL 进射程后执行槽位施法)。 ⑦ project/game_scene.gdnet_play.use_skill_hook = func(slot): return quickbar.activate_reserved(slot)。⑧ 测试:netplay_test.gd FakeClient 补 get_entities / pickup_item / add_fly_targeting / get_ground_items / get_skills,新增「5c」段 CLICK_ITEM 远→walk_to / 到手→pickup+FUNC_WAIT+清、物品消失→清;CLICK_POSITION 延时门 + walk_toUSE_SKILL range>100→-10、射程外walk_to、射程内hook+set_target+清、目标丢失→清; _change_target_to_picked_instance NPC→清目标 / 怪→选中;send_fly_targeting FAN 2 侧翼入扇 + 背后排除、CIRCLE 背后计入、随机落点补齐、max_count<=1 / SINGLE → 0);skill_test.gd 补 skill 47gwangyeokFAN + Range 2500 + target_count(lv1)=2 / (lv20)=5/ skill 91 bipabuFAN + Range 1800 + 无公式→0/ skill 1fly_shape=SINGLE)。C++ 21/21(无 C++ 改动)+ 13 个 GDScript 回归全绿。留给增量 92+:§3.6 飞行物实体(CFlyingManager / FlyingInstance)生成 / 命中、__CanShot 弓箭合法性。 详见 CLIENT-GAP-FIX.md §3.4 / C.5。

  • 2026-09-03 增量 92W2 / §3.6 飞行物实体 CFlyingManager / FlyingInstance 生成 / 命中 + __CanShot 弓箭合法性) ① 新增 project/fly_object.gdFlyData / FlyInstance / FlyManager),对齐 GameLib/FlyingObjectManager.{h,cpp}CreateFlyingInstanceFlyTargetUpdate()while 迭代 + eraseCreateIndexedFlyNORMAL / FIRE_CRACKER / AUTO_FIRE 分支常量)+ FlyingInstance.{h,cpp}Create__SetDataPointer__SetTargetDirectionUpdate() 逐帧 vel += accel*dtvel.z += gravity*dtremain_range -= |movement| → 线段-点 square_distance_between_linesegment_and_point < m_fBombRange² 命中 → OnShootDamage(本 POC 转 shoot_damage(vid) 信号)/ pierce -- + __Bomb 续飞 / __Explode + return false m_fRemainRange<0OnExplodingOutOfRange + __Explode(false)(不 __Bomb);位置目标同 线段判定;m_bHitOnBackgroundGetTerrainHeight(x,-y) > z+ FlyingData.cpp __Initialize() 默认值(米制换算)。线段判定 _seg_point_sq_dist 1:1 移植 EterLib/GrpMath.h:80。 ② project/net_world.gdsetup()FlyManager 子节点并 setup(parent, world),新增 signal fly_exploded(world_pos, cause);重写 _on_fly(type, start_vid, end_vid)——原 tween 「发光球」占位换成 _fly_mgr.spawn(start_w, target_node|goal_w, false, FlyData, type) 真实 弹道 + 线段命中(合成箭参数 init_vel=45 / flat_range=max(dist*1.5, 30) / bomb_range=0.6—— 无 .fly 资源加载器)。③ project/net_play.gd:新增 _can_shot(target_vid := -1) -> String __CanShot 在本发布源码树中 grep 无命中——据 IFlyEventHandler::OnNoTarget / OnNoArrow FlyHandler.hNEW_Attack InstanceBaseBattle.cpp:355IsDead 门、__GetBowRange InstanceBase.cpp:694 防御式重建):非弓 → "NOT_BOW_MODE"_arrow_count == 0"EMPTY_ARROW"-1 = 未跟踪 / 无限,放行);_target_vid / _vid_reserved 均 ≤ 0 → "NO_TARGET";目标实体已知且不可攻击 / 已死 → "NO_TARGET"te.is_empty() 不拒——headless 容差,与 _send_fly_target「有节点即可」一致);目标世界距离 * CM > _bow_range_cm()"OUT_OF_RANGE";否则 ""_emit_swing 弓分支据此加门:_can_shot() 非空 → _emit_cannot(why) (走 OnNoArrow / OnNoTarget 文案),不发 CG_FLY_TARGETING、不排 OnShoot;其余起手动作 照常(seam:参考端整挥击中止)。④ 新增 project/fly_test.gd(第 14 个 GDScript 回归):直线 命中对象目标 / 超程不 __Bomb / can_attackshoot_damage(vid) / 穿透后存活续飞再超程 / 位置目标 / 触地 / _can_shot 六分支 / _seg_point_sq_dist 三段。C++ 21/21(无 C++ 改动)

    • 14 个 GDScript 回归全绿。已文档化 seam:GC_CREATE_FLY 在本 40250 客户端无包处理器 (仅 NetStream.cpp 头串索引 70),实际驱动走 POC 自有 fly_cue__CanShot 防御式重建; 重力参考端作用于 .z=上、本 POC 映射到 -Y;怪物球体碰撞 / CullingMgr / 建筑体素判定未 移植(客户端飞行物只做命中时机,伤害服务端裁决);.fly 数据文件加载器缺失 → 合成箭参数。 留给增量 93+:§3.1 Actor-Actor 碰撞(TestActorCollision / BlockMovement+ 坐骑 MODE_HORSE*。 详见 CLIENT-GAP-FIX.md §3.6 / C.5。
  • 2026-09-03 增量 93W2 / §3.1 收尾:Actor-Actor 碰撞 + 坐骑转向速度)project/player_controller.gd 新增 _actor_blocked(next_pos)——CActorInstance::TestActorCollision ActorInstanceCollisionDetection.cpp+ CInstanceBase::CheckAdvancingInstanceBaseBattle.cpp:469 近似:距离门 ACTOR_COLLIDE_MAX_DIST_M = 8.0 m(≈ LengthSq(victim - self) > 800² 直接不检)→ body 球重叠(ACTOR_BODY_RADIUS_M = 0.55,自身 + 对方相加 = 判定阈值;固定半径近似真实 race 碰撞球)→「趋近」判定 d_next < combined && d_next <= d_now(对齐 TestActorCollision 末尾 dist(cur) <= dist(last)return TRUE,远离时放行 → 能从重叠里走出来)。候选 = pickables (每个已生成的远端 NPC / 怪 / 他人节点),dead meta 跳过(≈ rVictim.IsDead())。 ② _process 平移分支改逐轴门控:block_x = _actor_blocked(仅动 X) or _blocked(...)block_z 同理——单个 Actor 逐轴放行 = 滑移(≈ AdjustDynamicCollisionMovement,该函数不在发布源码树), 两轴都挡 = 停在上一帧位(≈ uCollisionCount==2 / 调整后仍撞 → BlockMovement() = __InitializeMovement); 随后既有 disp 判定把顶人磨蹭切回 _is_going = false + wait。③ skip_actor_collision (≈ CActorInstance::CanSkipCollision() / __EnableSkipCollision,§3.2 网络状态包驱动的移动整段 跳过)——net_playsetup() / _on_observer_mode_observer_mode 置位。④ 坐骑转向速度: player_controller 新增 rotation_speed_deg(默认 ROT_SPEED_DEFAULT_DEG = 1200 = c_fDefaultRotationSpeed

    • _turn_toward(target_yaw, dt)SetAdvancingRotation 的逐帧限幅收敛,移动方向不受限,仅朝向变慢), 替换原来两处 player.rotation.y = atan2(...) 瞬转;net_play._refresh_motion_mode()mounted 时把 rotation_speed_deg 切到 ROT_SPEED_HORSE_DEG = 300= c_fDefaultHorseRotationSpeed,对齐 CInstanceBase::MountHorseSetRotationSpeedInstanceBase.cpp:954),下马恢复 1200。坐骑动作 模式(MODE_HORSE*)已由增量 84 的 motion_mode_for + mount_changed_refresh_motion_mode 覆盖,本增量不重复。⑤ project/player_move_test.gd 5 组 → 8 组(正前方 Actor 挡下且 is_going 归零、dead Actor 不挡、skip_actor_collision 直穿、坐骑单帧转向 ≤ 300 deg/s 且仍朝 Dst 平移)。 C++ 21/21(无 C++ 改动)+ 14 个 GDScript 回归全绿。seam:真实 body sphere 半径需 race 碰撞数据(.col / CollisionData.h);AdjustDynamicCollisionMovement 精确滑移向量 + 门 / Actor 复合的 uCollisionCount==2 用逐轴近似代替;EnableSkipCollision 尚未接到 §3.2 网络状态包驱动的 本地预测玩家;MOVABLE_GROUND_DISTANCE 参考默认值仍是估计(1.0 m)。留给增量 94+:§3.2 / §3.5 收尾(多为待真实服务器);转 M2(W3 玩法)。 详见 CLIENT-GAP-FIX.md §3.1 / C.5。
  • 2026-09-03 增量 94W3→M2 / §5.4 PVP·PK·决斗可攻击判据 IsAttackableInstance 1:1 ① 新建 project/entity_rules.gd(纯静态 extends RefCounted)——is_attackable_instance(self_e, victim_e, ctx) 逐行照抄 CInstanceBase::IsAttackableInstanceREF/UserInterface/InstanceBase.cpp:2147): 主角 + 观战 → false;自打自 → falseIsStone / IsPC / IsEnemy / IsPoly 四分支。PC 分支内 victim.IsPC 时按序:决斗三态(DUEL_CANNOTATTACK → false / DUEL_START__FindDUELKey ? true : false)、 PK_MODE_GUILD && 同公会 → false、victim.IsKiller() && !同队 → true、PK_MODE_PROTECT != GetPKMode()PK_MODE_FREEvictim 非 PROTECT && !同队 → true/ PK_MODE_GUILD(再加公会不同 → true)、 IsSameEmpireIsPVPInstance → true / PK_MODE_REVENGE && !同队 && IsConflictAlignmentInstance → true,异帝国直接 → true;随后 victim.IsEnemy() / victim.IsWoodenDoor() → true。判据 helper 全 1:1alignment_grade:502 九档)、alignment_type:524)、is_conflict_alignment_instance:2116)、 _is_same_empire:470)、_is_pvp_instanceInstanceBaseEffect.cpp:505)、_find_duel_key:495)、 _is_same_partyPythonPlayer.cpp:1445)、_is_wooden_door:2390vnum 13000 / 30111..30119); _is_pc / _is_enemy / _is_polyCActorInstanceActorInstance.cpp:270/282/298)的 m_eActorType 单值语义——变身玩家仍是 TYPE_PCelse if (IsPC()) 先命中,IsPoly() 分支只服务真正的 TYPE_POLY)。 ② project/net_play.gd_is_attackable(e) 从「_entity_kind(e) in [0,2]」桩改为拼 self_e _main_entity() 快照 + kind/ victim_e+ kind/ 新 _attack_ctx()(从 client.get_duel() / get_pvp_relations() / get_guild_wars() / get_party() 装配 observer / pk_mode / duel_mode / duel_opponents / pvp_pairs / gvg_pairs / party_vids),委托 EntityRules.is_attackable_instance _main_entity() 为空(headless)退化旧近似。5 处调用点签名不变。③ 新增 project/entity_rules_test.gd (第 15 个 canonical25 组断言);combat_fx_test / netplay_test / fly_test 的怪物夹具补 "ch_type": 2(此前无 ch_type_entity_kind 归 PC,旧桩按 [0,2] 也「可打」;1:1 规则下和平同 帝国 PC 不可攻击 → 夹具须明确怪类别,与真实服务器一致)。C++ 21/21(无 C++ 改动)+ 15 个 GDScript 回归全绿。seam__FindPVPKeyGC_PVP 关系表 setter/ __FindGVGKey(公会战对局 setter/ IsGameMaster 标记 / IsBuilding·IsWall 的 race 白名单(源码未随发布)/ m2dev 后端 IsKiller 状态位 / _entity_kind 无法分类时怪被当和平 PC(生产 mob_proto 恒加载);名字颜色 §8.8、IsInSafeATTRIBUTE_BANPK 地形属性仍未接。留给增量 95+:§5.1 quest 38 tag、§5.3 fishing/dungeon/mount/observer UI、§4.1 costume/belt。详见 CLIENT-GAP-FIX.md §5.4 / C.5。

  • 2026-09-03 增量 95W3→M2 / §5.1 Quest EventManager CPythonEventManager 1:1 project/ui/quest_dialog.gd 由旧的单遍手写 parse_script 重写为 EventTypeMap + 状态机 + 分派:① const EVENT_TYPE_MAP42 键,tag 字符串 → EVT_*+ enum { EVT_LETTER .. EVT_SELECT_ITEM } (顺序 = REF/UserInterface/PythonEventManager.h:35EEventType+ _get_script_event_index() = GetScriptEventIndex PythonEventManager.cpp:1093:前导 /EVENT_POSITION_END + 剥离, 未知名字 → ok=false / 跳过、不进 match)。② class EventSet extends RefCounted= SEventSet :98,字段名 1:1ix/iy/iWidth/iyLocal/isLock/lLastDelayTime/iCurrentLetter/CurrentColor/ strCurrentLine/isConfirmWait/iConfirmEndTime/DiffuseColor/lWaitingTime/iRestrictedCharacterCount=30/ iVisibleStartLine/iVisibleLineCount/iAdjustLine/isTextCenterMode/isWaitFlag/nAnswer+ init_event_set()__InitEventSet :72,常量 c_lNormal_Waiting_Time=10 / c_fLine_Temp=16 / BOX_VISIBLE_LINE_COUNT=5)、insert_line()__InsertLine :976)、clear_line()ClearLine :953)。 ③ _process_event_set(es, cmd) = ProcessEventSet() :377switchcase 对拷:DELAY / COLOR / COLOR256 的 START vs /ENDLETTER / ITEM / MOBiCurrentLetter >= iRestrictedCharacterCount → insert_line 断行、NEXT / DONE / INSERT_IMAGEiAdjustLine += 2WAITiyLocal=0; isLock=true)、QUESTION= MakeQuestion :884)、 CLEAR / CLEAR_TEXT 均走 clear_line、image family + TITLE_IMAGEADDMAPSIGNAL / CLEARMAPSIGNAL / SETCMAPPOSSETMSGPOS / ADJMSGPOSpass(引 :562/:566 空实现)、 SLEEPSET_CAMERA_camera_setting 九参 = GetCameraSettingFromArgList :48/ BLEND_CAMERA +blendtime/ RESTORE_CAMERAFADE_* / WHITE_*isWaitFlag=trueDUNGEON_RESULT 9 参、 QUESTBUTTON / QUESTBUTTON_CLOSEWINDOW_SIZEINPUTCONFIRM_WAIT / END_CONFIRM_WAITSELECT_ITEMRUN_CINEMApassProcessEventSet 无此 case)。④ _tokenize() 拆混排文本 / [tag ...];裸网络文本折成合成 LETTERis_raw=true → 跳过 30 字断行,reference 脚本只有显式 [LETTER])。arg 提取 _tag_kv()key(v) / key("v") / key;v+ _arg_s/_arg_i/_arg_f (keyword 优先、位置回退,兼容旧测试 value;255 / value(1) 写法)。⑤ parse_script() 返回字典 =旧契约全键不变,新增 insert_images / dungeon_result / adjust_line / waiting_time / is_locked / wait_flag / answer_count / cinema_file / end_confirm;首命令 RUN_CINEMARegisterEventSetFromString :142 特例,仅当 cmds[0]RUN_CINEMA、无前导文本)→ es.cinema_file + 新 signal cinema_requested_emit_side_effects 透出)。新增 project/quest_event_test.gd(第 16 个 canonical 23 组断言);既有 quest_test / select_item_ui_test 仍绿。C++ 21/21(无 C++ 改动)+ 18 个 GDScript 回归全绿。seam:逐帧 UpdateEventSet :312 的时间推进(lLastDelayTime 递减 / isWaitFlag 暂停 / isConfirmWait 倒计时刷新 m_strLeftTimeString)、Skip / EndEventProcessiConfirmEndTimetimeGetTime.msc 事件文件加载(RUN_CINEMA 换文件)、MakeNextButton NEXT vs DONE 文案区分 + DoneEvent() 回调、camera_event_requested / fade_event_requested / cinema_requested 消费端与 §7.6 相机模式、game_scene 接线、物品名颜色 (1.0,0.2,0.2) 均未接(一次性跑完 tokenisLock / isWaitFlag 仅作状态保留并透出)。留给增量 96+:§5.2 任务日志窗口、§5.3 fishing/dungeon/mount/observer UI、§4.1 costume/belt 窗口、§4.2 拖放鼠标物品态。详见 CLIENT-GAP-FIX.md §5.1 / C.5。

  • 2026-09-03 增量 96W3→M2 / §4.1 wear 槽位版图 + costume/ring/belt 发包坐标 GameType.h 1:1 project/ui/inventory_ui.gd._to_wire()REF/UserInterface/GameType.h:40-112 把装备格拆两路—— c_Equipment_Start+0..10EWearPositions BODY..SHIELDWEAR_MAX_NUM=11)走 WINDOW_EQUIPMENT(2) + 零基 wear cellENABLE_COSTUME_SYSTEM / ENABLE_NEW_EQUIPMENT_SYSTEMc_Costume_Slot_Start(90+19) / c_Costume_Slot_Hair(90+20) / c_Equipment_Ring1(90+21) / c_Equipment_Ring2(90+22) / c_Equipment_Belt(90+23) 在客户端没有独立窗,继续用 WINDOW_INVENTORY(1) 的全局 cell 109..113——修掉了 inventory_test.gd_to_wire(113) == [1, 113] 一直红的断言(旧实现返回 [2, 23])。新增常量 WEAR_MAX_NUM=11 / WEAR_COSTUME_BODY=19 / WEAR_COSTUME_HAIR=20 / WEAR_RING1=21 / WEAR_RING2=22WEAR_BELT=23 值不变)/ NEW_EQUIP_WEAR_START=19 / WINDOW_EQUIPMENT=2 全部 1:1 自 GameType.hItemData.h EWearPositions_add_extended_cells 由「wear 11..34 连续 24 格」收窄为「19..23 这 5 个真实新 装备位 + 16 格腰带背包」(EWearPositions 11..18 无位置,refresh() 对空槽自动跳过)。 inventory_test.gd 增 7 条 wire 坐标断言(_to_wire(90)→[2,0] / (100)→[2,10] / (109..112)→ [1,109..112]);inventory_ui_test.gdmove_to(5,91) → [1,5,2,1,1],基础 wear 仍走 window=2 保持绿。C++ 21/21net.loopback_flow 首跑 localhost bind 抖动,--rerun-failed 即过;无 C++ 改动)+ 55 个 GDScript 回归全绿。seam:§4.1 修改 2(antiflag→wearable_flag→limit 装备校验链 + CANNOT_EQUIP_* 文案)——40250 客户端不做 CanEquipNow 前置门(PythonItemModule 只暴露 IsWearableFlag / GetLimit,仲裁在服务器 char_item.cpp),需按 §3.9 __CanShot 方式防御性重建; 修改 4(穿 costume 隐藏本体 armor / 穿头盔隐藏发型,InstanceBase.cpp ChangeArmor / SetHair)、 修改 5(远端角色 armor/weapon/hair 三字段接换模)render 侧未接。留给增量 97+:§4.1 修改 2/4/5、 §5.2 任务日志窗口、§5.3 fishing/dungeon/mount/observer UI、§4.2 拖放鼠标物品态。详见 CLIENT-GAP-FIX.md §4.1 / C.5。

  • 2026-09-03 增量 97W3→M2 / §4.1 修改 2 —— CanEquipNow 前置门 ItemData.h 1:1 新增 project/equip_rules.gdextends RefCounted 纯校验器),枚举 1:1 自 REF/GameLib/ItemData.h ——EItemAntiFlagclassic 排列:FEMALE=1<<0SHAMAN=1<<5,帝国位 EMPIRE_A/B/R=1<<9..1<<11)、 EItemWearableFlagBODY=1<<0ARROW=1<<9)、EWearPositions0..10+ GameType.h 新装备槽 19..23、ELimitTypesLIMIT_LEVEL=1LIMIT_CON=5PCBANG=6REAL_TIME=7)、EItemType WEAPON=1/ARMOR=2/COSTUME=28/RING=33/BELT=34+ EArmorSubTypes/ECostumeSubTypescan_equip(item, wear, ctx) -> {ok, code, need},语义顺序对齐服务器 char_item.cpp::CanEquipNow 目标槽解析(wear<0default_wear_for())→ anti-flag 性别(ANTI_SEX)→ 职业(ANTI_JOB JOB_ANTIFLAG[race%4])→ 帝国(ANTI_EMPIREGC_EMPIRE 1/2/3)→ slot_accepts()(新槽按 item type、旧 11 槽按 wear_flags & wear_to_wearable(wear)NOT_WEARABLE_HERE)→ limit 逐条 LIMIT_LEVELctx.level<vneed=vLIMIT_STR/DEX/INT/CON:对应属性 >=0 且 <v 才判)。 SEX_BY_RACE / job_of_race=race%4 / sex_of_race 表 1:1 自 MAIN_RACE_*inventory_ui.gd.use() 对「常规背包格 + 可穿戴物品」跑此门,拦下则 _set_hint() + 新 signal equip_rejected(code, need) + 不发 use_item;新增公开 can_equip_item()_equip_ctx() get_points().level + get_entities() 主角 race/empire/levelst/dx/ht/iq 未由 M2Client 按名 暴露 → 传 -1 = seam)、_equip_reject_hint() 中文文案。新增 canonical 测试 #17 project/equip_rules_test.gd33 断言);inventory_ui_test.gdFakeProto.item()sub_type/anti_flags/wear_flags/limitsiv.use(5) 仍过门。C++ 21/21(无 C++ 改动)+ 56 个 GDScript 回归全绿。seam:精确 tie-break 顺序 / 已穿同槽替换 / LIMIT_PCBANG·REAL_TIME* / 属性 4 项按名读取留待接服务器比对;§4.1 修改 4(部位隐藏遮挡)+ 修改 5(远端换装接字段)为 render 侧,留给增量 98+。详见 CLIENT-GAP-FIX.md §4.1 / C.5。

  • 2026-09-03 增量 98W3→M2 / §4.1 修改 4 —— 变身 / 时装部位遮挡 InstanceBase.cpp 1:1 + 修改 5 字段序确认) 新增 project/part_hiding.gdextends RefCounted 纯函数),常量 1:1 自 REF/UserInterface/InstanceBase*.cppPOLY_SHAPE=0IsPoly()SetShape(0)SetWeapon/SetParts 提前 return)、ANIMAL_WEAR_SHAPES=[100,101,102,103]__IsShapeAnimalWear:2834 → SetWeapon:2747 / SetParts 返回 false)、WEDDING_DRESS_SHAPE=201IsWearingDress)、BOUQUET_VNUMS=[50201..50204] c_iBouquets__IsChangableWeapon:901 除捧花外一律 false)。函数 is_animal_wear / is_wearing_dress / is_bouquet / is_changable_weapon(婚纱→仅捧花)/ weapon_hidden(变身 ∨ 动物时装 ∨ 婚纱非捧花)/ hair_hidden(变身 ∨ 动物时装;婚纱不影响头发)/ body_shape(变身→0,动物时装 100-103 保留)/ effective_weaponequip_model.gd.refresh() 刷新序改为对齐 ChangeArmorSetArmor→SetHair→SetWeapon) = 身体→头发→武器→盾;解出身体 shape(armor_shape_of=__ArmorVnumToShapevalues[3]/=vnum 后过 PartHiding:动物 / 怪物时装隐藏 weapon_gr2+shield_gr2+hair_gr2,婚纱非捧花隐藏武器+盾、 头发保留;新增 _is_poly() seam40250 实体表无变身字段,恒 false)。net_world._apply_field_updates() 确认 set_armor(parts[0])→set_weapon(parts[1])→set_head(parts[2])→set_hair(parts[3]) 字段序已 1:1 于 NetworkActorManager.cpp:473(增量 46 落地),补注遮挡走模型侧 part_hiding.gd。新增 canonical 测试 #18 project/part_hiding_test.gd45 断言)+ equip_model_test.gd 补 7 段遮挡断言。C++ 21/21 (无 C++ 改动)+ 57 个 GDScript 回归全绿。seam:变身态(等 GC_CHARACTER_UPDATE 带变身 race); 远端 PC 真模型工厂已在增量 99 首版落地(RemotePlayerView + 只消费 awPart[4] 的远端 EquipModel),所有职业 / race、远端真服换装和精确视觉仍待验收。详见 CLIENT-GAP-FIX.md §4.1 / C.5。

  • 2026-09-03 增量 101W3→M2 / §5.2 Quest 日志窗口):按参考端 QUEST_MAX_NUM=5 固定渲染 5 行,超过 5 条显示滚动条并映射 questShowingStartIndex;空 icon 使用 season1/icon/scroll_open.tga,任务行点击发送 -2147483648 + questIndexquest_test.gd 已覆盖 7 条任务、首尾滚动和事件参数。多列定位、世界箭头、屏幕边缘指示、完整 .sub 视觉 和真实任务生命周期仍待验收。详见 CLIENT-GAP-FIX.md §5.2 / C.5。

  • 2026-09-03 增量 102W3→M2 / §5.3 钓鱼输入状态机):钓鱼模式攻击键改发 CG_FISHING 方向并阻断普通攻击;方向键边沿、地面点击接入 NEW_CancelFishing,地面点击 先清预约 / 自动攻击再取消,取消保留 500ms 节流;快捷栏特殊技能 123 接入钓鱼入口; GC_FISHING 五类事件维护钓鱼意图状态。netplay_testplayer_move_testskill_testgamescene_testcombat_fx_test 全部 PASS。尚缺 GetFishingRot 水面像素扫描、 wrong-place 文案、鱼获物品落袋及副本 / 坐骑 / 观战者完整玩法。详见 CLIENT-GAP-FIX.md §5.3 / C.5。

  • 2026-09-03 增量 103W3→M2 / §5.3 GetFishingRot 水面扫描):新增 Metin2World.get_fishing_rotation() / can_fishing_position(),读取已加载区块的 ATTRIBUTE_WATER,按参考端前方 600cm、左右 0..180° 每 10° 扫描并返回选中 heading NetWorld 将地图层查询桥接给 NetPlay,无地图 provider 时保留协议 fallback。扩展重新编译通过, fishing_water_test.gd 真实 A1 20 区块回归 PASS。仍缺 wrong-place locale 文案、鱼获 item proto / inventory 落袋和副本 / 坐骑 / 观战者完整玩法。详见 CLIENT-GAP-FIX.md §5.3 / C.5。

  • 2026-09-03 增量 104W3→M2 / §5.3 钓鱼消息分支)ChatUI 加载 locale_game.txtRecvFishing 的状态与 item proto 类型分派 FISHING_NOTIFY1/2FISHING_SUCCESS1/2FISHING_FAILUREFISHING_UNKNOWNFISHING_WRONG_PLACESUCCESS 只作为动画事件, 不再误报“鱼钩命中”。chat_test.gdgamescene_test.gd PASS。真正物品数量仍等待 独立 GC_ITEM_* 包验证,副本 / 坐骑 / 观战者完整玩法仍未完成。详见 CLIENT-GAP-FIX.md §5.3 / C.5。

  • 2026-09-03 增量 105W3→M2 / §5.3 坐骑攻击等级门):按 40250/ClientVS22/source/UserInterface/InstanceBase.cpp:124-216InstanceBaseBattle.cpp:623-628 接入坐骑 vnum→level 映射和 SHORSE::CanAttacklevel 1 禁攻); 按 PythonPlayerInput.cpp:737-764 接入新坐骑 GetSkillGrade(109) < 1 && GetSkillLevel(109) < 11 门禁。net_play.gd 的自动攻击改用 mount_vnumnetplay_test.gd PASS。坐骑真实模型 / MODE_HORSE* 动作 / 移速及真服坐骑数据仍待验收。详见 CLIENT-GAP-FIX.md §5.3 / C.5。

  • 2026-09-03 增量 106W3→M2 / §5.3 观战者小地图插值):按 40250/ClientVS22/source/UserInterface/PythonMiniMap.cpp:16-64,168-180 接入 GC_OBSERVER_ADD/REMOVE/MOVE 的独立轨迹和 1 秒线性插值;新增初始快照播种和 world_reset 清理。p9_test.gd 覆盖 ADD / MOVE / REMOVE / reset 并 PASS。观战自由视角、真服完整生命周期 和副本 / 坐骑剩余项仍待验收。详见 CLIENT-GAP-FIX.md §5.3 / C.5。

  • 2026-09-03 增量 107W3→M2 / §5.3 副本目的地状态):按 40250/ClientVS22/source/UserInterface/PythonNetworkStreamPhaseGame.cpp:3875-3901PythonPlayer.cpp:147-167,1255-1285 接入 GC_DUNGEON 目的地状态:保存全局厘米坐标、 收包立即提示、按 Manhattan <10000cm 清除目标、每 20 秒重新提示,并在 world_reset 清理。 hud.gd 增加目标距离 / 朝向提示;TIME_ATTACK_START 保持参考端空分支,不虚构本地计时器。 dungeon_state_test.gdgamescene_test.gdp9_test.gdnetplay_test.gd PASS。DungeonBlock .mdatr 碰撞、真实副本入口 / 结算窗和真服验收仍待完成。详见 CLIENT-GAP-FIX.md §5.3 / C.5。

  • 2026-09-02 增量 46W0/W1 公共基础第一批实现)Classic 角色槽已保存 lAddr/wPort 新增 M2Client.enter_game(index) 并接入 ClientVS22 DirectEnter 的第二次游戏连接入口; 选人 / 登录 / 进场发送结果已透传,未知 header 不再 Loading 启发式重同步,已知未处理包不再 静默成功;已建立游戏连接后的断线回到 LOGIN。net_classic_stream_testnet_classic_session_testnetbridge_testp10_test 已通过(受限环境下本地 TCP bind 子测试自动 skip)。真实双连接 E2E、 MT_CLASSIC_ENTER_DELAY 移除和版本包 0xf1 仍未完成。

  • 2026-09-02 增量 47W0/W2 公共基础第一批实现)Classic GC_WARP 已改为严格按 lAddr:wPort 重连,不再用 addr==0 推断“同服原地移动”;连接会保留 login key / 已选角色槽, 新增 world_reset 信号,并在 Godot 侧清理网络实体、地面掉落、PVP / 决斗、目标、交互窗和 地图局部状态后重建地图、HUD 小地图、Atlas、环境和玩家位置。net_entity_testnet_classic_session_testgamescene_testp9_testp2b_testnetbridge_testp8_test 已通过;受限环境下 localhost TCP 目标测试仍自动 skip。真实 40250 换图 / 第二次 连接 E2E、地图 BGM 选择和 MT_CLASSIC_ENTER_DELAY 仍待完成。

  • 2026-09-02 增量 50Phase 1 W1:落地 CLIENT-GAP-FIX.md §1.5 / §1.10 / §2.2。 §1.5ClassicSession::send_client_version() 改为 locale 分支——EUROPE(本项目 locale)发 CG_CLIENT_VERSION2header 0xf1CGClientVersion2 67Btimestamp 固定 "1215955205"), 非 EUROPE 发 CG_CLIENT_VERSION0xfd+ CMake 注入的 MT_BUILD_TIMESTAMPC __TIMESTAMP__ 形态);新增 set_executable_name()(默认 "metin2.bin",已由 M2Client 传入 OS::get_executable_path().get_file());版本包只在 send_fixed() 成功后置 m_version_sentwire_classic.hCGClientVersion2 结构 + 尺寸表项。§1.10:新增 extension/src/net/text_codec.h —— to_wire()/from_wire_str() 做 UTF-8 安全的按字节裁剪(不切多字节序列,硬字节上限), godot::String 重载用 __has_include 守卫使 mtnet 仍无 godot 依赖;classic_session.cpp 里 create/change name、whisper、私店招牌、公会等级名 / 公告、ANSWER_MAKE_GUILD、好友增删共 8 处 定长填充改走 to_wire()parser 侧 GC_CHAR_ADDITIONAL_INFO 名字读取走 from_wire_str() 新增 net_text_codec_testCJK 24B 收下 / 27B 截到 24B / 多字节边界 / ASCII 往返 / 读端悬挂字节)。 真实 locale codepageCP949 / CP1252)转码仍为后续项,当前 wire 字节即 UTF-8ASCII 往返一致)。 §2.2ClassicParser GC_CHARACTER_ADD —— is_invisible_race20025/20038/20039)直接丢弃; bType==PC||NPC 仅把裸 Entityvid/race/type/坐标/朝向/速度/state·affect flag)暂存进 m_pending_actor,不碰世界模型;其余(怪 / 石 / 门 / 物件)即时 mut_spawn(),名字取自 root/npclist.txtrace→code,新增 npc_names.h + set_npclist_path() setterM2Client 已按 MT_ASSETS 传入资源根,未配置时回退 $M2_ASSETS,无表时名字为空)。GC_CHAR_ADDITIONAL_INFO —— 无对应暂存则写 m_last_error + m_unhandled_headers 并丢弃(绝不半创建);命中则把暂存 Entity 与附加信息 9 字段(name/guild/level/alignment/pk_mode/empire/parts[armor/weapon/hair]/ mount_vnum)合并为一条,mut_spawn_full() 落地后 erase 暂存项。新增 pending_actor_count() 测试钩子;net_classic_session_test 更新(版本包断言改 0xf1/CGClientVersion2;新增两包 暂存 → 合并 → 落地、怪即时生成、invisible-race 丢弃、无暂存附加信息为空操作的覆盖)。 依赖 W2 §2.2 步骤 4–5(mut_char_info touch-create / mut_shop_signEntity 清理)—— 本次合并逻辑与当前 W2 代码兼容。C++ ctest 17/17(原 16 + net.text_codec)、netbridge_testp10_test 全绿。

  • 2026-09-02 增量 50Phase 1 W2:实体两层模型 + 可见性 / 单次覆盖 / 点数边界 / 应用顺序) 把可见性从 EntityStore 彻底移到 net_world.gd§2.1EntityStore 只做数据层—— 仅 mut_spawn / mut_spawn_full / mut_spawn_main 建行,mut_mount / mut_char_info / mut_shop_sign 遇未知 VID 只 dbg_ignored()MT_NET_TRACE 门控)后返回,不再 touch() 出半行、不做 replay 缓冲;net_world.gd 每帧 _update_visibility()is_main / AFFECT_SHOW_ALWAYS(bit7) / IsWall(1420114204) / 平面距主角 < CHAR_STAGE_VIEW_BOUND(200*100 cm) 决定建节点,超 +10 淡出并保留数据行(_fading 集合 防淡出中重建),只在主角坐标已知时启用、否则沿用 entity_spawned 建节点。§2.3 mut_spawn() 改为委托 mut_spawn_full(),后者整行值替换(m_ents[vid] = e,不再保留旧 move 状态,对齐 SNetworkActorData::__copy__);net_world._on_spawn 对已有节点的 vid 先销毁再重建,上/下坐骑触发的重建复用旧世界坐标。§2.4mut_set_points() 全量快照 > POINT_MAX_NUM(255) 截断、< 255 清零尾部;mut_set_point() 索引 >= 255 拒绝。 §2.5:新增 EntityStore::mut_affect_flags(vid, lo, hi)v = lo | (hi<<32) 两段拼装), mut_char_update() 的 guild/alignment/pk 变化也触发 Infonet_world._apply_field_updates() 按参考调用序(NetworkActorManager.cpp:470Armor→Weapon→Hair→Guild→AffectFlag→MoveSpeed→ AttackSpeed→Alignment→PKMode→StateFlags)刷新节点。C++ ctest 16/16net.entity_store 加 §2.1 数据层断言)、新增 net_world_vis_test.gdnetbridge_test / gamescene_test / netplay_test / p9_test / combat_fx_test / player_motion_test 全绿。待 W1 跟进: wire_classic.hGCPoints 已有 static_assert(sizeof == 1 + 255*4)ClassicParser 需在 GC_CHARACTER_ADD[2] / GC_CHARACTER_UPDATE 后调用 mut_affect_flags()affect_flag[2] 送进实体(mut_char_update 无 affect 形参,未接线)。G1 集成已接线(见下)。

  • 2026-09-02 增量 50Phase 1 W3 / §8.3 帮助窗):新增 project/ui/help_ui.gd —— 1:1 迁移 assets/root/uihelp.pyHelpWindow(单页版 ENABLE_HELP_MULTIPAGE = 0LoadDialogSinglePage),布局走真 assets/uiscript/uiscript/helpwindow.pyUiScript → UiBuild),19 条浮动帮助行 + 10 个任务栏贴纸标签 + close_button 的文本逐字对齐 uiScriptLocale.HELP_*locale/locale/en/locale_interface.txt);UiManager 未接 locale 回调时 helpwindow.py 里的 uiScriptLocale.X 被解析成 <X> 占位,_relabel() 用本工程 Locale 表 + 英文兜底补回(含原脚本重名的两个 help_02)。入口:game_scene.gd _unhandled_input 加单条 elifH 且非 Ctrl / 非 Meta → system_menu_ui.toggle_help() Ctrl+H 仍留给 /user_horse_ride);system_menu_ui.gdhelp_button 由占位提示改为 close() + open_help(),新增 _ensure_help() 懒建 / open_help() / toggle_help() 与 可注入的 help_ui 成员(setup() 追加可选第 6 参,旧 5 参调用不受影响)。新增 project/help_ui_test.gdhelpwindow.py 装载 + 19 行逐字绑定 + 任务栏标签 + close_button + open/close/toggle + ESC 栈顶 + SystemMenuUI.open_help() 懒建)。help_ui_test / system_menu_ui_test / gamescene_test / ui_test(附加 system_option_ui_test / char_status_ui_test)全绿;本 worktree 无 build/,未跑 C++ ctest。视觉 / .sub 贴图 与真服未验证。

  • 2026-09-02 增量 50Phase 1 W4:地图 BGM / 音频):落地 CLIENT-GAP-FIX.md §9.1 §9.2。§9.1bgm_director.gd 补齐「地图 → 曲目」解析——服务端 bgm_changed(name, volume) 名非空即播;名为空回落 musicInfo.fieldMusicMETIN2THEMA = "M2BG.mp3");volume < 0(服务端未下发, GC_MAIN_CHARACTER3_BGM)用默认 1/5*0.1 = 0.02,否则用服务端值;应用音量在 play_bgm() 淡入之前(对标 game.py:190SetMusicVolume/SetSoundVolumeFadeInMusic);连 M2Client.world_reset 在换图时重跑选曲。新增 musicinfo.gd METIN2THEMA / loginMusic / createMusic / selectMusic / fieldMusic 五常量 + save/load_last_play_field_musicuser://BGM/lastplay.inf)。§9.2Audio 新增 save_volume() / restore_volume()(字段名对齐参考 m_fBackupMusicVolume / m_fBackupSoundVolume)与 set_music_volume()app_lifecycle.gd 进后台改为 save_volume() + 静音(不再 stop_bgm()),回前台 restore_volume()_bgm_name 全程不清空,同一首继续播。新增 bgm_test.gdbgm_testgamescene_testnetplay_testp9_test 全绿;真服 BGM 验收待 40250 服务端。§9.3 后续由增量 51 单独落地。

  • 2026-09-02 增量 52W0-W2 / §3.9 公共网络 API 参数边界):新增 extension/src/net/net_bounds.h 统一封装 u8/u16/u32/i32、36 快捷栏槽、12 交易槽、24 Cube 槽、39 私人商店项、15 龙魂精炼格等 已核实边界;M2Client 在窄类型转换前校验连接端口、角色创建、坐标 / VID、动态文本包长度、聊天 / 技能、社交、公会、商店 / 交易 / 仓库 / 私人商店 / Cube、精炼、物品与会徽参数,超限直接返回 false, 不再静默截断或把负数转为无符号值。新增 net_bounds_test.cppC++ ctest 18/18,通过相关 Godot 离线回归;真实 40250 业务合法性仍待验收。详见 CLIENT-GAP-FIX.md §3.9 / C.5。

  • 2026-09-02 增量 53W3/W4/W5 / §4.8 §6.3 §8.6 §10:W3 将快捷栏改为参考端的服务器 36 槽 / 客户端 4 页 × 8 格,类型改为 INVENTORY=1/SKILL=2/EMOTION=3,表情槽调用 send_emoticon();补齐 LSHIFT 翻页、F1F4、Ctrl/Command+数字、空格按住攻击、Alt 显示名字、 相机按住 / 释放、截图、帮助 / 坐骑 / 角色窗口 / 小地图 / 聊天等键位入口。W4 的 .mse 粒子现在 按参考 EmitterShape 读取位置、形状、方向 / 速度,并通过 UiAssets + 原生 DDS 解码加载首个 TextureFiles.mde 等仍明确保留为未完成项。W5 将 assets / bgm 自包含复制进 App,支持 MT_MAC_ARCHES=universallipo 门禁、嵌套扩展签名顺序,移除发布代码中的硬编码凭据。离线 fx_testinput_key_testskill_testnetplay_testnetbridge_testgamescene_test 全绿; 2.2G 完整导出、当前机器双架构产物、正式证书 / notarization 和真服 / 真机仍未验证。详见 CLIENT-GAP-FIX.md §4.8 / §6.3 / §8.6 / §10 / C.5。

  • 2026-09-02 增量 54W3 / §4.2 §4.9:新增 cursor_manager.gdmouse_controller.gd 对齐 mousemodule.py 的全局物品拖放边界:背包物品可拖到其他背包格或快捷栏,释放到无效区域、 右键、ESC、失焦均取消;快捷栏物品槽类型和装备窗口 wire 坐标保持原语义。UiManager 增加窗口 键盘焦点屏蔽,PlayerController / GroundItems / NetWorld 接入 Talk / Attack / Pick 等光标 状态。新增 mouse_controller_test.gd,并重跑 inventory_ui_testinput_key_testgamescene_test 全绿。CJK IME 候选窗、复杂窗口逐项 cursor 资源和真服 / 真机仍未验证。

  • 2026-09-02 增量 55W4 / §9.4 MilesLib 音频子集)project/audio.gd 对齐 CSoundManagerSOUND_SCALE=200、环境音 1000、监听器位置 / 方向、世界坐标相对监听器 缩放、PlaySound3D / PlayAmbienceSound3D / PlayCharacterSound3DSetSoundVolume3D / StopSound3D / StopAllSound3Dr<0.1 ? r : pow(10,r-1) 音量曲线、角色声音 5000cm 距离和 0.3 秒同文件限频;.msa 本地动作声音已走角色声音入口,GameScene 每帧更新监听器。新增 audio_miles_test.gd,回归通过。.mss 解析、真实声音节点数据和真机 / 真服仍未完成。

  • 2026-09-02 增量 56W3 / §4.2 §4.4 §4.5 §4.6 §4.7:仓库接入 5×9 网格分页、指定格存取和 仓库内移动;商城接入分页网格与商城→背包;交易接入 12 个指定投放格;NPC 商店接入 BUY/SELL toggle、出售投放区和 BUY/SELL 光标联动;私人商店 40 格接入全局物品态。新增 complex_item_drop_test.gd,覆盖仓库 / 商城 / 交易 / 商店落点,并确认购买模式拒绝出售投放; 回归通过。金钱附着、stack split、anti-flag / 售价确认、完整交易事务、真实 .sub 资源和真服 / 真机 仍未完成。详见 CLIENT-GAP-FIX.md §4.2 / §4.4 / §4.6 / C.5。

  • 2026-09-02 增量 60W1/W3 / §4.5 §5.1 §5.2:交易协议状态补齐 GC_EXCHANGEsockets[3] / attrs[7]m2dev / Classic 两条解析路径、EntityStore::ExchangeSlotM2Client.get_exchange() 保持同一实例数据;quest_log.gd 接入任务 icon(资源存在时显示)和 按参考格式递减的分钟 / 秒倒计时;quest_dialog.gd 识别 COLOR/COLOR256ITEM/MOBWINDOW_SIZE、居中、CONFIRM_WAIT、图片元数据和 DELAY/WAIT。新增交易字段断言与任务日志 回归;C++ 构建、受影响 Godot 测试通过。完整交易事务、38 项逐事件 EventManager、真实资源和 真服 / 真机仍待验收。

  • 2026-09-02 增量 65W4 / §6.3:按 40250/ClientVS22/source/EffectLib/SimpleLightData.cpp / SimpleLightInstance.cpp 接入 .mse Light 组:解析 MaxRange、ambient/diffuse、三项衰减、 duration、loop / loop count 和 TimeEventRange,在 effect_player.gd 中生成 OmniLight3D, 按参考范围曲线和生命周期更新位置、可见性和范围。粒子多贴图接入 TexAniDelay 及顺 / 逆序, RotationType 区分 NONE / TIME_EVENT / CW / CCWfx_test 增加 Light 组范围 / 生命周期 断言并通过;随机起始帧、每粒子随机语义、完整 MovingType / 时间轴、SpeedTree、.msenv 和 真机视觉仍未完成。详见 CLIENT-GAP-FIX.md §6.3 / C.5。

  • 2026-09-02 增量 66W4 / §6.3:按 40250/ClientVS22/source/EffectLib/EffectElementBase.cpp:5-60 接入效果元素位置时间曲线:直接段按前后关键帧线性插值,MOVING_TYPE_BEZIER_CURVEp0 + controlPoint 做二次 Bezier,边界时间按参考端钳制;粒子、.mde 网格和 SimpleLight 都消费同一位置曲线。EmitterEmitFromEdgeFlag 的球体映射到 Godot 球面发射,并保存 EmitterAdvancedType / edge 元数据;新增直接段 / Bezier / sphere-edge 断言,fx_test 通过。 随机起始帧、每粒子随机方向 / 帧语义、完整 emitter advanced / cycle、完整时间轴和真机视觉仍未完成。 详见 CLIENT-GAP-FIX.md §6.3 / C.5。

  • 2026-09-02 增量 67W4 / §9.4:按 40250/ClientVS22/source/GameLib/Area.cppMapType.cpp 接入地图 AreaAmbienceData.pra:位置 / 范围 / 播放类型 / 间隔 / 声音向量 由 C++ 世界层提供,且与对象渲染、splat 开关解耦;audio.gd 对齐 LOOP / ONCE / STEP 的严格 范围判定、距离音量、进入边沿播放和离开区域停止;修正 Miles 3D 坐标的厘米换算。新增真实 A3 地图 ambience_test,并通过 C++ 构建、audio_miles_testfx_test 回归。.msenv 的 云层 / 滤色 / 双方向光 / 风参数 / 昼夜天气联动、声音资源逐项听感和真机验收仍未完成。详见 CLIENT-GAP-FIX.md §9.4 / C.5。

  • 2026-09-02 增量 72W4 / §7.5:按 40250/ClientVS22/source/EterLib/SkyBox.cpp:552-557,869-927 补上 texture-mode 天空与云层的组合:有限水平云面按 CloudScale / CloudHeight 投影, CloudTextureScale / CloudSpeed 映射到 Godot sky shader 的 UV 缩放 / 秒级滚动,并保留 ClientVS22 的 MODULATEINVALPHA_ADDCOLOR + ONE/INVSRCCOLOR 颜色合成;真实 DawnMistWood 环境验证通过。云层渐变过渡、滤色、风、lens flare、昼夜联动和真机视觉仍待完成。 详见 CLIENT-GAP-FIX.md §7.5 / C.5。

  • 2026-09-02 增量 70W4 / §7.5:按 40250/ClientVS22/source/EterLib/SkyBox.cpp:483-542 对齐 texture mode 的六面顺序和 UVenvironment_builder.cpp 用 Godot sky shader 接入 Front/Back/Left/Right/Top/BottomFaceFileName;真实 CapeDragonHead 的五个非空面可解析并渲染, 空 bottom 面回退 .msenv 渐变。新增 skybox_test.gd,验证五面资源、空面回退和 shader 安装。 纹理天空与云层同时存在时的叠加、云层 UV / 高度动画、滤色、风、lens flare 和昼夜联动仍待实现; 后续增量 72 已补上前两项的首版 shader 映射。 详见 CLIENT-GAP-FIX.md §7.5 / C.5。

  • 2026-09-02 增量 69W4 / §7.5:按 40250/ClientVS22/source/GameLib/MapOutdoor.cpp:246-264 对齐背景 / 角色两套方向光:Sun 只照 layer 1 静态场景,CharacterLight 只照 layer 2 的 Metin2Model、装备和 LOD;换图清理两盏光。environment_testplayer_motion_test 和 C++ 构建通过。 SpeedTree 风参数、滤色、lens flare、昼夜联动和真机视觉仍未完成。详见 CLIENT-GAP-FIX.md §7.5 / C.5。

  • 2026-09-02 增量 68W4 / §7.5:按 40250/ClientVS22/source/GameLib/MapUtil.cppMapType.cpp 补齐 .msenv 的参考缺省值、IsDensityFilterBTextureRenderMode、六面 天空文件名和 Reserved 字段;environment_builder.cpp 通过 AssetResolver 解析真实资源, 将云层贴图接入 ProceduralSkyMaterial.sky_cover,并把 Godot 暂无等价渲染载体的原始参数保存 到 WorldEnvironment 元数据。同步修复已解析环境文件路径被强制转小写导致的 macOS 加载风险。 formats_map_test(内联 + 真实 A1)、environment_testworld_probe 和 C++ / Godot 回归通过。 六面天空、云层 UV / 高度动画、滤色实际合成、双方向光分层、风参数、lens flare 和昼夜天气联动 在该增量记录时仍未宣称完成;后续增量 69/70/71/72 已分别补上双光层、纹理天空首版、参考风缺省和 纹理天空云层组合首版。 详见 CLIENT-GAP-FIX.md §7.5 / C.5。

  • 2026-09-02 增量 64W4 / §6.3:按 40250/ClientVS22/source/EffectLib/EffectMesh.cpp 实现 .mdeEffectData v1 / MDEData002 v2 读取:校验几何体 / 帧 / 数量边界,读取每帧 visibility 与顶点、索引、UV,按参考展开 TPTVertex、翻转 UV,并将厘米坐标换为 Godot 米坐标。 effect_player.gd 改为构建真实 ArrayMesh,播放几何帧,接入 MeshElement 的 billboard、混合、 ColorFactor / TimeEventAlpha 和 .ifl 贴图帧;新增 project/fx/mde.gd 与真实 geompung.mde 解析 / 构建断言。fx_test 通过,并修复了真实 .mse 触发的 Gradient 单点移除运行时错误。 SimpleLight、TexAni 完整帧语义、MovingType / 骨骼挂点和资源逐项视觉 / 真机验收仍未完成。详见 CLIENT-GAP-FIX.md §6.3 / C.5。

  • 2026-09-02 增量 63W4 / §9.4:按 40250/ClientVS22/source/MilesLib/Type.cpp:17-74,118-136 实现 .mssSoundDataCount / SoundData%02d 解析和 DWORD(time / (1/60)) 帧换算; 修正 sound/ 虚拟前缀到各 patch sound 根的查找,audio.gd 提供 load_mss_for_motion() / update_sound_instances_3d()PlayerView / MobView 在动作帧进入时播放角色声音。新增 .mss 解析、虚拟路径、60 FPS 换算和玩家 / 怪物动作配对断言;audio_miles_testplayer_motion_testmob_view_test 通过。SimpleLight、TexAni、SpeedTree、.msenv、完整音频资源和真机 / 真服听感仍待验收。

  • 2026-09-02 增量 62W3 / §4.3:按 uitooltip.py 的技能书、遗忘书、变身道具分支,把 item_tooltip.gdsockets[0] 接到本地技能名 / 怪物原型名,并对限制类型 7/8/9 对应的 结束时间或持续时长生成 剩余分钟 / 秒提示;新增技能书、变身和限时提示断言,item_tooltip_test 通过。CJK 文案 / 字体切换、未覆盖的其他特殊分支、装备对比、真实资源和真服 / 真机仍待验收。

  • 2026-09-02 增量 61W3 / §5.1 §5.2 §9.5:按 PythonEventManager.cpp:554-614 / 579-602 接入任务脚本的 ADDMAPSIGNALCLEARMAPSIGNALSETCMAPPOSQUESTBUTTONQUESTBUTTON_CLOSE side effect;任务日志增加独立的任务信件条带,点击后发送已有的 script_button(index),对话打开期间按参考隐藏条带,关闭后恢复。Minimap 保存并绘制任务信号点, AtlasUI 接入原版 screen-space center adjustIMAGE / LEFTIMAGE / TOPIMAGE / BGIMAGE / TITLE_IMAGE 在资源存在时实际创建图片节点,缺资源安全跳过。新增 quest 事件 / 条带断言并重跑 quest_testp9_testgamescene_test,均通过。完整 EventSet 逐事件等待 / 淡入淡出 / 相机 / 电影、真实 .sub 资源和真服 / 真机仍待验收。

  • 2026-09-02 增量 59W3 / §4.6shop_ui.gd 按参考 shopdialog.py 改为 5×8、40 格 货位,空位保持不可用,已放货物显示名称 / 数量并接入统一 tooltip;多货架仍按 tabIdx * SHOP_SLOT_COUNT + slot 计算全局槽号。BUY 模式点击非空货位进入确认,确认后固定 发送数量 1,SELL 模式继续只接受出售投放。p8_test 已更新并通过;真实 .sub 槽位 / 按钮 贴图、图标和非金币货币显示仍待完成。

  • 2026-09-02 增量 58W1/W3 / §4.2 §4.3:按 40250 GameLib/ItemData.h 的 236 字节 TItemTable 布局补齐 limits[2]applies[3]values[6]sockets[3]、精炼关联、 魔法属性概率、球面高光和开孔概率字段;proto_item_layout_test 锁定字段偏移。新增统一 project/ui/item_tooltip.gd,解析 itemdesc.txt,并接入背包、装备查看、NPC 商店、仓库、 商城、交易、私人商店,生成基础数值 / 限制 / applies / 实例 attributes / sockets / refine 提示。 item_tooltip_test、库存回归和 C++ 构建通过。鱼竿 / 镐经验、龙魂计时、技能书 / 变身等特殊 分支,CJK 文案 / 字体、装备对比、真实资源和真服 / 真机仍未完成。

  • 2026-09-02 增量 57W3 / §4.2 §4.5 §4.6 §4.7 + W1 item 实例字段):全局物品态现在保留 anti_flagsflagssockets 等实例元数据;普通背包拖放携带整组数量,Shift+左键堆叠物品可先 选择附着数量,金币使用 ITEM_MONEY=-1 独立拖放态。交易按参考端补齐服务端 accept 镜像、1000 距离退出、ANTIFLAG_GIVE、7 位金币上限和已放金币不可编辑;NPC 商店补齐实例 ANTIFLAG_SELLIsValuableItem / Is1GoldItem 售价公式和首次确认框;私人商店补齐 ANTIFLAG_GIVE/MYSHOP。 新增 proto_item_layout_testshop_rules_test,并重跑库存 / 鼠标 / 交易 / 摆摊 / 复杂窗口回归。 代码与离线回归完成;参考端 safebox money 发送函数确认主动 return false,未伪造 /safebox_money。 完整交易事务、真实 .sub 资源、40250 真服和真机仍待验收。详见 CLIENT-GAP-FIX.md §4.2 / §4.4 / §4.5 / §4.6 / §4.7 / C.5。

  • 2026-09-02 增量 51W0 / §9.3 生命周期单一属主):生产入口 client_main.gd → AppFlow 创建并持有唯一 AppLifecycle,登录 / 选人 / 游戏切屏不重复创建;GameScene 删除本地 AppLifecycle,只通过 lifecycle_audio() 交给 AppFlow 绑定当前 AudioM2Client 删除 NOTIFICATION_APPLICATION_PAUSED/RESUMED 监听,只保留 suspend()/resume()。旧兼容启动器 login.gd 也不再在进游戏时销毁自己的生命周期节点。新增 app_flow_lifecycle_test.gd 扩展 gamescene_test.gd 验证单一属主;两项 Godot 回归使用独立 --log-file 串行通过。 真机前后台 / 内存告警 / 后台 TCP 回收后的重连仍未验证。

  • 2026-09-02 增量 49W0 接口冻结批次):为 Phase 1 的 4 个并行工作流一次性落地跨工作流 共享文件的骨架,冻结接口后再拆分。EntityStoreEntityempire / affect_flags / owner_vid / state_flags 字段;新增 mut_spawn_full()(§2.3 两包合并的单次落地入口)、 mut_ownership()(§2.5)、mut_map_bgm() + take_bgm_dirty()/bgm_name()/bgm_volume() (§9.1)、drain_dirty()(§2.1/§2.5 裸字段更新的可见性重镜像);mut_char_info() 不再 丢弃 empirereset_for_map_change() 一并清 m_dirtyM2Client:新增 bgm_changed 信号,classic / m2dev 两条 pump 回路都消费 drain_dirty()entity_infotake_bgm_dirty()bgm_changedentity_dict()empire / affect_flags / owner_vid / state_flags 键。ClassicParserBGM 名 / 音量从 GC_MAIN_CHARACTER3_BGM / GC_MAIN_CHARACTER4_BGM_VOL 路由到 mut_map_bgm()(此前直接 丢弃),新增 m_pending_actor 暂存表成员(W1 在 §2.2 填充两包合并逻辑)。Godot 侧新增 project/bgm_director.gdM2Client.bgm_changedAudio.play_bgm,把地图 BGM 消费点 从 net_world.gd 拆出,避免 W2/W4 抢改同一文件),game_scene.gd 装配并 bind() game_scene.gd 键位按 §8.6 / §4.8 收敛:数字键 1–4 → 快捷栏 0–3、F1–F4 → 快捷栏 47、 Ctrl+1..9 → _emote() 统一入口(表情包与本地表情动作由 W3 在 §8.6 补全)。 C++ ctest 16/16、netbridge_test / gamescene_test / p9_test / netplay_test / system_menu_ui_test / skill_test / combat_fx_test / skill_fx_test / player_motion_test 全绿。BGM→曲目解析与音量应用、§2.2 合并逻辑、§8.6 剩余键位为 Phase 1 各工作流范围。

  • 2026-09-02 增量 48(并行开发协议):本文件与 CLIENT-GAP-FIX.md 改为“公共基础 + W1–W5 并行工作流 + 集成门禁”的执行格式,不再把附录 C 的章节顺序当成唯一开发顺序;明确 共享文件主维护者、接口冻结、测试替身、真实验收和冲突处理规则。新增硬性要求:每个批次在开始、 完成、阻塞或重新验证时,都必须把状态、改动文件、测试证据、风险和下一步同步回两份文档。

  • 2026-09-01 增量 45(P8 交互边界与龙魂运行时范围):补齐 NPC 点击路由、Cube NPC 前置 / 配方材料 / 金币校验与制作状态、商店购买 / 出售数量和错误反馈、交换重复 / 满槽 / 金币边界、仓库 / 商城密码与满包回滚、个人摊位标准 Godot 拖放路径;缺失可选 .sub 贴图 安全降级。龙魂入口运行时禁用,公会和真实服务器验收继续按当前范围暂缓。

  • 2026-09-01 增量 44(交付范围调整):真实 FreeBSD + MySQL 完整验收、公会相关功能均 暂缓;当前目标 40250 服务端不提供龙魂玩法,龙魂从本阶段支持范围排除。保留本地协议 / 状态 / UI 首版与离线回归结果,后续恢复服务端条件后再单独推进 deferred 项。

  • 2026-09-01 增量 43(队伍角色 / 能量 / 私店招牌复核):对照 40250 服务端 CParty::Update 修正队伍角色 6 为 HASTE(迅捷),补齐 affects[7] 的服务端槽位语义, 并在 party_ui.gd 按 Leadership 10/20/25/35/40 门控攻击/坦克/迅捷/辅助/宗师/防御, Leadership 18 门控组队治疗;p8_test.gd 覆盖低 Leadership 的禁用路径。Cube HUD 已有 POINT_ENERGY 快照显示;GC_SHOP_SIGN 已解析并在 NetWorldShopSign Label3D 作为无 .sub 资源 fallbackGC_SYMBOL_DATA 也已有会徽下载链路。剩余仍是原版资源、真 .sub 拖放 / 视觉和 Cube NPC 交互;live FreeBSD + MySQL E2E 暂缓。

  • 2026-09-01 增量 42(选魔石窗口闭环复核):确认 GC_SCRIPT[SELECT_ITEM] 已由 QuestDialog 转发到 SelectItemUI;选择窗按原 uiselectitem.RefreshSlot 过滤魔石 / grade、 映射背包格并发送 CG_SCRIPT_SELECT_ITEM,关闭发送 0select_item_ui_test.gd 已覆盖。仍需 完整原版资源、图标和 CJK 输入环境下的视觉验收。

  • 2026-09-01 增量 41(创建公会 UI 闭环)GC_REQUEST_MAKE_GUILD 触发公会创建窗口, 对应 CG_ANSWER_MAKE_GUILD 已接入名称输入、12 字上限、空名 / 控制字符校验和发送失败提示; 新增 guild_creation_ui_test.gd 覆盖请求、校验和合法名称应答。服务端敏感词 / 重名错误及实际建会 状态仍需真服验收。

  • 2026-09-01 增量 40(强制改名 UI 闭环)SimplePlayer.change_name 已从 classic / ClientVS22 角色列表透传到 M2Client.char_list;选人页按 40250 原版语义,在角色带该标志时点击“开始” 弹出改名窗,校验名称后发送 CG_CHANGE_NAME,成功的 GC_CHANGE_NAME 刷新槽位并清除标志, AppFlow 接通请求、成功和失败回传,新增 char_create_delete_test.gd 回归覆盖。P1 行不再把 改名 UI 列为未实现;仍需真服验证强制改名、重名 / 非法名错误码和服务端状态。

  • 2026-08-31 增量 30EterGrnLib 对齐复核):查代码确认以下已落地,表内相关行同步。 ① 材质 blend / two-sided 改读 GR2 材质信息metin2_model::decide_blend/decide_two_sided 优先 gr2::MaterialInfo.alpha_blend/two_sided = dump_materials 端口 Material.cpp:233 body + weapon 两处;m2_materialcull_disabled shader 变体)。 ② LOD 淡入LodGhost 冻结旧 mesh + lod_fade uniform0.18s,距离阈值 15% 迟滞)。 ③ crossfade 权重改 ease-in 2w²−w³,对齐 GrannySetControlEaseInCurve(0,0,1,1)。 ④ .msa motion event 类型分派game_scene._on_local_motion_eventsound / effect / SCREEN_WAVING 震屏 / SCREEN_FLASHING 屏闪 / FLY·WARP 钩子)。 ⑤ 角色接触阴影project/ui/char_shadow.gd:强制 cast_shadow=ON + 脚下渐变 Decal player_view / mob_view 调用)。 ⑥ 点选精度player_controller._ray_pick_t:射线 vs 子 mesh 合并世界 AABB slab 相交, 无 mesh 回退胶囊)。 ⑦ 武器挂点复核:隔离探针实测 weapon MI 原点 == equip_right_hand 骨骼原点 Δ≤0.06 m(旧「偏 10 m」为 cpu_skin / MapCoord 修复前的失效结论);新增 grip pre-transform weapon_pre = mul4x3(weaponInvWorld[0], weaponLocal[0])(对齐 ModelInstanceUpdate.cpp:171weaponComposite[0]),30 cm 位移夹断退回原始手部位姿。 03150 类网格在 .gr2 里就离骨骼原点 ~3 m(资产缺陷,非变换)。 新增测试 eterngrn_polish_test.gdctest 10/10 + 29 套 GDScript + iOS 全绿。

  • 2026-08-31 增量 38ESC 系统菜单 + 游戏设置窗)system_menu_ui.gdsystemdialog.py 真 uiscriptthinboard 8 键)= uisystem.py SystemDialog.__LoadSystemMenu_Default 的 1:1system_option_buttonsystem_option_ui.open() game_option_buttongame_option_ui.open()change_buttonclient.say(0,"/phase_select") = net.ExitGame()SendChatPacket("/phase_select")),logout_button"/logout" = net.LogOutGame()),mall_button"/in_game_mall"exit_buttonget_tree().quit() help_button → 占位提示,cancel_button / 标题栏 → 关。ESC 现在开这个菜单(game_scene 把 ESC 从直接开 system_option 改成开 system_menu)。 ② game_option_ui.gdgameoptiondialog.py 真 uiscript= uigameoption.py block_{exchange,party,guild,whisper,friend,party_request}_button(toggle) → client.say(0,"/setblockmode " + str(mask ^ bit))EBlockAction 位 1<<0..1<<5,本地 _block_mode 跟踪 + 持久化,服务器回包同步待补);pvp_{peace,revenge,guild,free}(radio) → "/pkmode {0,1,4,2}"__OnClickPvPMode*);name_color/target_board/view_chat/ always_show_name/show_damage/salestext radio → 持久化 user://system_option.cfg [gameopt] systemSetting.Set*Flag 等价;渲染侧钩子待补)。 新增 system_menu_ui_test.gd(两窗装载 + /setblockmode 位运算 + /pkmode + /phase_select/ /logout//in_game_mall + 显示开关持久化 + 二次实例读 cfg)。C++ 无改动;ctest 10/10 + 34 套 GDScript + 导入 + iOS 全绿。

  • 2026-08-31 增量 37(组队成员信息板 + CG_PARTY_SET_STATEC++wire.hCGPartySetState{u16 hdr,u16 len,u32 pid,u8 role,u8 on}(断言 10

    • EPartyRole 枚举(NORMAL 0/LEADER 1/ATTACKER 2/TANKER 3/BUFFER 4/SKILL_MASTER 5/ HASTE 6/DEFENDER 7);game_client.send_party_set_state(pid,role,on) M2Client.party_set_state(pid,role,on) + bindget_party() dict 补 state(完整角色字节) 和 affects[7]。 ② party_ui.gd 从「名字 + 一条 HP」重做成 1:1 迁移 assets/root/uiparty.py PartyMemberInfoBoard + partymemberinfoboard.py 布局:每员 strip = 角色状态按钮 (队长可点 → 弹角色菜单 普通/攻击/坦克/迅捷/辅助/宗师/防御 → party_set_state;普通 = 清当前角色 on=false+ 踢出 → party_leave = SendPartyRemovePacket+ 名字(+★队长)
    • HP gauge + affects[7] 非零 → 附加效果 chip(tooltip 带值,槽位名暂定)。点名字 → set_target(vid)。顶部:EXP 分配开关(party_set_distribute 0 不均分 / 1 均分)+ 组队治疗(party_use_skill(PARTY_SKILL_HEAL=1, 0))。队长判定 = get_party() 里本地 get_main_vid() 那条的 leader。角色菜单按 40250 服务端 Leadership 门控:攻击≥10、坦克/迅捷≥20、 辅助≥25、宗师≥35、防御≥40;治疗要求 Leadership≥18。 ③ p8_test 隐患_init 原来 _run()await_run 第一个 await process_frame 之后的所有断言(含增量 34 的 shop_ex 多货架)从未真正生效 —— 改成 await _run() 后 暴露 shop_ui.refresh()queue_free 延迟 bug(旧 row 当帧未消失 → get_child_count 多计),已改 remove_child + queue_freectest 10/10 + 33 套 GDScript + 导入 + iOS 全绿。
  • 2026-08-31 增量 36(系统设置窗 + 私人商店/选魔石补真 uiscript)系统设置窗 project/ui/system_option_ui.gd —— UiScript+UiBuildassets/uiscript/uiscript/systemoptiondialog.py;控件绑定逐字对照 assets/root/uisystemoption.pymusic/sound_volume_controller(sliderbar) → snd.SetMusicVolume/SetSoundVolume(接 Audio.master_bgm/master_sfx + 即时改在播 BGM 声道); camera_short/long(radio) → game_camera.max_dist(近 11 / 远 20,并夹当前 dist); fog_level0/1/2(浓/中/淡 radio) → Environment.fog_enabled + fog_density [0.055/0.018/0.004]tiling_cpu/gpu+apply(原 CPU/GPU 分块渲染)Godot 渲染器无对应, 保留占位。设置持久化到 user://system_option.cfg= 原 systemSetting 配置文件), setup() 时读回即时生效,.msenvEnvironment_on_main_set 重新 _apply_allgame_sceneESC 呼出(ui_manager 先关最顶层窗并吃掉事件 → 到 game_scene 说明无窗打开)。 新增 system_option_ui_test.gd(装载 + 音量/镜头/雾绑定 + cfg 持久化 + 二次实例读 cfg)。 ② 私人商店窗改走真 uiscript privateshopbuilder.pyboard + TitleBar + NameLine + ItemSlot 5×8=40 grid + Ok/Close):40 格从 _win.nodes 索引,NameLine(text) 上盖 LineEdit,背包候选面板挂窗右侧(原客户端靠主背包拖拽,此处点选)。itemStock 交互模型 (拿起 / 落位 / 价格弹窗 / 排序 + display_pos / 撤下 / 개설 / 철수)不变。测试拆出独立 private_shop_ui_test.gduiscript,缺资产跳过),shop_cube_mall_test 私人商店段移除。 ③ 选魔石窗 select_item_ui.gd 早已走真 uiscript selectitemwindow.py(增量 33),无需改。 C++ 无改动;ctest 10/10 + 33 套 GDScript + 导入 + iOS 全绿。

  • 2026-08-31 增量 35(私人商店开设窗 = PrivateShopBuilder 交互模型)private_shop_ui.gd 从「前 5 行 checkbox」重做成 1:1 复刻 assets/root/uiprivateshopbuilder.pyitemStock 模型:左侧背包候选(点一件“拿起”)+ 右侧 40 格(shop.SHOP_SLOT_COUNT,5×8); 拿着件点空格 → 价格输入(uiCommon.MoneyInputDialog)→ 落位(AddPrivateShopItemStock + itemStock[targetSlot]=(src));点已占用格 → 撤下(OnSelectItemSlot / DelPrivateShopItemStock)。개설(OkButton) 把 stock 按格号排序、display_pos = 格号、 上限 39PRIVATE_SHOP_ITEM_MAX_NUM = TPacketCGMyShop::bCount cap)打包成 M2Client.open_private_shop(sign, [{vnum,count,inv_cell,price,display_pos}]) = TPacketCGMyShop + TShopItemTable×N,字段 1:1)。shop_cube_mall_test 私人商店段 重写(拿起 / 落位 / 价格弹窗 / 排序 + display_pos / 撤下 / 철수)。C++ 无改动; ctest 10/10 + 31 套 GDScript + 导入 + iOS 全绿。 NOTE: 面板仍手绘(与 mall_ui / cube_ui 一致),真 .subprivateshopbuilder.py uiscript 待后续。

  • 2026-08-31 增量 34SHOP_GC_START_EX 多货架商店)entity_store 新增 SHOP_GC_START_EX(10) 解析 —— TPacketGCShopStartEx{u32 owner_vid, u8 tab_count}

    • tab_count × {ShopTabHead(char name[32]+u8 coin_type=33B), ShopItem[40]}wire.hShopTabHead + 断言 33SHOP_TAB_NAME_MAX=32/SHOP_TAB_COUNT_MAX=3)。 ShopEntrypos(货架内槽位,买位置用),ShopTab{name,coin_type,items} + m_shop_tabsSHOP_GC_START 也改成按固定 40 槽数组填 posSHOP_GC_END 清货架。 shop_items() 仍是 tab 0 的镜像(旧调用不动)。M2Client.get_shop(){vid, open, tabs:[{name, coin_type, items:[{pos,vnum,price,count}]}]}shop_ui.gdtabs>1 显示 货架按钮,买位置 = tabIdx*SHOP_SLOT_COUNT(40)+slotPos1:1 对齐 uishop.py GetIndexFromSlotPos)。net_entity_test 加双货架合成包断言(名称 / coin_type / 槽位保留 / tab0 镜像 / END 清空),p8_test 加多货架 UI + buy pos 5 / buy pos 43 断言。 ctest 10/10 + 31 套 GDScript + 导入 + iOS 全绿。
  • 2026-08-31 增量 33(选魔石窗,样板第 2 例):新增 project/ui/select_item_ui.gd ——UiScript+UiBuild 装载 assets/uiscript/uiscript/selectitemwindow.py5×8 grid_table);填格逻辑逐字照 assets/root/uiselectitem.py.RefreshSlot:遍历背包前 INVENTORY_PAGE_SIZE*2 格,只留 item.IsMetinproto.type == ITEM_TYPE_METIN = 10GetItemGrade <= 2= 物品内部名最后一位数字,非数字按 0)的物品,最多 54 个。 点格 → client.script_select_item(inventoryCell)= net.SendSelectItemPacket CG_SCRIPT_SELECT_ITEM 0x0903+ 关窗;ExitButton / 关闭 → script_select_item(0) uiselectitem.Close)。触发链 1:1quest_dialog.parse_script 识别 EventManager [SELECT_ITEM] token → 新增 select_item_requested 信号(对应 PythonEventManager EVENT_TYPE_SELECT_ITEMinterfacemodule.BINARY_OpenSelectItemWindow)→ game_scene 接到 select_item_ui.open()。新增 select_item_ui_test.gdtoken 解析 + 过滤 + 选包 + 关窗发 0);ctest 10/10 + 31 套 GDScript + 导入 + iOS 全绿。

  • 2026-08-31 增量 32(角色状态窗 = 1:1 窗口迁移样板):新增 project/ui/char_status_ui.gd——直接用 UiScript+UiBuild 装载 assets/uiscript/uiscript/characterwindow.py 真布局;数值绑定逐字对照 assets/root/uicharacter.py.RefreshStatusLevel/Exp/RestExp、HP/SP、 STR/DEX/HTH/INT、ATT=(min|max)+ATT_BONUS+ATTACKER_BONUS、 DEF=DEF_GRADE(+DEF_BONUS)、MATT=MAG_ATT+(min|max)MAGIC_WEP、MDEF、 ASPD/MSPD/CSPD/EREPointTypes 索引取自 40250/ClientVS22 Packet.h)。加点 / 按钮发 client.say(0,"/stat ht"|"/stat- ht" …),与原 statusPlusCommandDict / statusMinusCommandDict 完全一致;POINT_STAT>0 才显示加点按钮。STATUS/SKILL/ EMOTICON/QUEST 四页做 SetState 页签切换(技能 / 表情 / 任务的数据仍在各自专用窗)。 名称 / 帮会 / 职业头像取 get_entity(get_main_vid())game_scene 挂 V / C 热键。 新增 char_status_ui_test.gdctest 10/10 + 30 套 GDScript + 导入 + iOS 全绿。 这是「原 interfacemodule.py 逐窗口迁移」的样板,后续选道具网格 / 商店 START_EX / 私人商店 39 格照抄这套(uiscript 装载 + 逐字数值绑定 + 原聊天命令 / 封包)。

  • 2026-08-31 增量 31(真服 E2E 二轮扫,单会话):以 MT_NET_TRACE=1 MT_E2E_SWEEP=1 ./build/extension/net_e2e 完成一次 auth→选人→PHASE_GAME 会话;出站明文 / 加密字节均已落盘。10 个安全探针全部发送成功、 450ms 后仍在线、unknown_header=0CG_CHARACTER_POSITION(5B)、 CG_SYNC_POSITION(16B)、CG_SCRIPT_SELECT_ITEM(8B)、脚本取消(Classic 为 CG_SCRIPT_ANSWER=29 + sentinel 255)、 CG_PARTY_USE_SKILL(9B)、CG_FLY_TARGETING/CG_ADD_FLY_TARGETING(16B)、 CG_FISHING(5B)、CG_SHOOT(5B)、CG_USE_SKILL(12B)。脚本取消触发 1 条 quest-change 钓鱼收到真服 Please choose a Fishing Pole.(无鱼竿的预期业务拒绝),其余 无业务事件但均无断连 / 无解析错误。被动 GC_LAND_LIST 436B 在登录洪流中真服收到并解析。 扫描暴露一个代码缺口:GC_FLY_TARGETING/GC_ADD_FLY_TARGETINGGC_CHANGE_SKILL_GROUP 已有 EntityStore 实现却未列入 GameClient 世界分派,现已补路由并由 net_entity_test 复核。CG_DUNGEONCG_WARP、改名 / 符号 / 物品变更等破坏性或独立 连接协议本轮未发送,仍保持“待真服验”。

  • 2026-08-31 增量 5(实现复核):修正文档中已经落后的功能状态。 情侣状态已不再是“整体未做”:GC_LOVER_INFO(70B) / GC_LOVE_POINT_UPDATE(5B) 已接入 EntityStore,并通过 M2Client.get_lover() / lover_changed 驱动常驻 love_uiC++ 状态测试和 love_ui_test.gd 均有覆盖;仍缺完整情侣交互与真服验收。 角色 / 装备渲染已补本地主角的身体盔甲 / 时装 parts、头盔 / 假发 / 发型、右手武器、 左手盾牌、item_proto.specular/100 球面高光、角色 LOD 迟滞 + 0.18s 淡入、动作切换 ease-in、.msa 音效 / 特效 / 震屏 / 屏闪事件和角色接触阴影首版。地图 / 材质行同步 记录 16 层 splat、原 UV / 源分辨率、水纹序列 / 水深 alpha、terrain patch 剔除和物体 Y-up 旋转修复。协议审计总缺口仍为 24 个 header,但 GC_LOVER_* 已从“声明未消费” 清单移除;输入行修正为已支持 F1–F4 与数字键 1–9。 本次同时补充“参考模块覆盖索引”,把协议表之外的聊天、TextTail、HUD、UI 运行时、输入 / 相机、 登录串场和桌面外壳差距单独列出。

  • 2026-08-31 增量 6(位置同步上行):补齐 CG_CHARACTER_POSITION(0x0A60) / CG_SYNC_POSITION(0x0303) 的 wire 结构、尺寸断言、GameClient 发送接口和 M2Client.character_position() / sync_positions() API;回环测试覆盖姿态包、最多 16 个 坐标元素、字段值和空批次 / 超限拒绝。两项已从“缺失 header”移入“已声明但待核”——仍需 接入原版的自动碰撞纠偏时机、姿态动作和真服务器端到端验收。

  • 2026-08-31 增量 7(任务应答协议):补齐 CG_SCRIPT_SELECT_ITEM(0x0903) / CG_QUEST_CANCEL(0x0906) 的 wire 结构、尺寸断言、GameClient 发送接口和 M2Client.script_select_item() / quest_cancel() API;回环测试验证选择值与取消包。 同时让 QuestDialog 支持基础 [INPUT] 文本框和 Escape 取消;缺失 header 清单由 22 项 降为 20 项。任务窗口仍需接入选道具网格、原版 IME / 输入校验和完整 EventManager 标签语义。

  • 2026-08-31 增量 8(组队技能上行):补齐 CG_PARTY_USE_SKILL(0x0705) 的 wire 结构、尺寸断言、GameClient.send_party_use_skill()M2Client.party_use_skill(); 回环测试验证技能索引 / 目标 VID。缺失 header 清单由 20 项降为 19 项;队伍技能的 冷却、权限和队员世界标记仍待补齐。

  • 2026-08-31 增量 9(飞行目标广播):补齐 GC_FLY_TARGETING(0x0411) / GC_ADD_FLY_TARGETING(0x0412) 的 20B wire 结构;EntityStore 持久化 shooter 的目标 VID / 坐标并产生追加标记,M2Client.fly_targeting 信号向 Godot 暴露;C++ 实体测试覆盖 设置目标、追加目标和坐标回退。缺失 header 清单由 19 项降为 17 项。

  • 2026-08-31 增量 10(技能组切换):补齐 GC_CHANGE_SKILL_GROUP(0x021D) 的 5B wire 结构、EntityStore 技能组状态 / dirty 标记、M2Client.get_skill_group()skill_group_changed 信号;切换会同时使技能快照失效,C++ 实体测试覆盖状态更新。 缺失 header 清单由 17 项降为 16 项。

  • 2026-08-31 增量 11(角色改名):补齐 CG_CHANGE_NAME(0x010B) / GC_CHANGE_NAME(0x010C) 的固定长度 wire 结构、GameClient 改名发送与角色槽位更新、 M2Client.change_name()char_name_changed 信号;回环测试覆盖名称字节和 pid 广播。 缺失 header 清单由 16 项降为 14 项。

  • 2026-08-31 增量 12(公会创建请求):补齐 GC_REQUEST_MAKE_GUILD(0x0731) 的 无负载包解析,GameClient 产生请求事件,M2Client.guild_make_requested 向 Godot 暴露, 回环测试覆盖事件消费。缺失 header 清单由 14 项降为 13 项。

  • 2026-08-31 增量 13(主动传送请求):补齐 CG_WARP(0x0305) 的 4B wire 结构、 GameClient.send_warp()M2Client.request_warp();回环测试验证上行请求及现有 GC_WARP 目标回传。缺失 header 清单由 13 项降为 12 项。

  • 2026-08-31 增量 14(挖矿动作广播):补齐 GC_DIG_MOTION(0x0308) 的 13B wire 结构、EntityStore 挖矿动作事件、M2Client.dig_motion 信号和 NetWorld 朝向 / 动画 元数据处理;实体、回环和 Godot 桥接测试覆盖。缺失 header 清单由 12 项降为 11 项。

  • 2026-08-31 增量 15(公会符号下载):补齐 CG_SYMBOL_CRC(0x0723) / GC_SYMBOL_DATA(0x0732) 的 16B / 8B 结构和原始数据流;MarkClient 支持 CRC 请求与变长符号文件接收, M2Client.download_guild_symbol() / get_guild_symbol() / guild_symbol_ready 已接入, 回环测试覆盖 CRC 字段、guild id 和数据长度。缺失 header 清单由 11 项降为 9 项。

  • 2026-08-31 增量 16(钓鱼协议):补齐 CG_FISHING(0x0B01) / GC_FISHING(0x0B10) 的 5B / 10B wire 结构、旋转方向换算、EntityStore 事件和 M2Client.fishing() / fishing_event;回环与 Godot 桥接测试覆盖。缺失 header 清单由 9 项降为 7 项。

  • 2026-08-31 增量 17(副本协议):补齐 CG_DUNGEON(0x0B02) / GC_DUNGEON(0x0B11) 的 4B / 5B wire 结构,支持目的地坐标变长 body,接入 M2Client.request_dungeon() / dungeon_event;实体、回环和 Godot 桥接测试覆盖。缺失 header 清单由 7 项降为 5 项。

  • 2026-08-31 增量 18(领地与观战者):补齐 GC_LAND_LIST(0x0B12) 的 4B 头 + 24B 元素变长解析,以及 GC_OBSERVER_ADD/REMOVE/MOVE(0x0B200x0B22) 的 12B / 8B wire 结构;EntityStore 保存领地和观战者状态,M2Client.get_land_areas() / get_observers()land_areas_changed / observer_event 已接入;小地图现在消费观战者 状态并绘制紫色菱形标记。实体、回环、Godot 桥接和 p9_test.gd 覆盖。缺失 header 清单由 5 项降为 1 项(仅剩 CG_TEXT,参考端未发现实际发送方)。

  • 2026-09-01 增量 39CG_TEXT wire 尺寸覆盖):按 40250 TPacketCGText / CInputHandshake::Analyze 补齐 CG_TEXT 的 1 字节固定前缀和 classic packet_size_cg() 表项,并加入 wire 回归断言。该包后续文本由服务端按换行特殊解析,仅作为握手阶段管理通道; Cube 继续使用已验证的 CG_CHAT/CHAT_TYPE_COMMAND,不把管理命令暴露为普通游戏 API。 主网络层缺失 header 清单由 1 项降为 0 项。

  • 2026-08-31 增量 19(钓鱼动作消费)NetWorldNetPlay 现在消费 fishing_event 的 START / STOP / REACT / SUCCESS / FAIL 子头,记录方向并尝试驱动远端及 本地主角的钓鱼动作;FISH 子头按参考客户端语义保留为鱼获物品事件,不误当作角色 VID。 combat_fx_test.gdnetbridge_test.gd 覆盖动作分发。

  • 2026-08-31 增量 20(钓鱼结果日志)ChatUI 消费 GC_FISHING 的 SUCCESS / FAIL / FISH 事件,将命中、失败和鱼获物品写入系统 / 战斗日志;chat_test.gd 覆盖结果路由。

  • 2026-08-31 增量 21(小地图领地标记):小地图继续消费 GC_LAND_LIST,把全局厘米坐标 的领地矩形绘制为公会金色 / 中性青色边框;p9_test.gd 补充领地数据供 headless 重绘路径 自检。观战者紫色菱形与领地边框均已接入,仍缺原版 minimap 贴图和 Atlas 交互。

  • 2026-08-31 增量 22(小地图缩放)minimap.gd 新增 set_scale() / get_scale() 对齐原 CPythonMiniMap::SetScale 的像素 / 米缩放语义;鼠标滚轮在小地图区域内调整缩放, p9_test.gd 验证缩放会改变世界坐标投影。仍缺原版贴图、Atlas 拖动和坐标查询窗口。

  • 2026-08-31 增量 23(飞行目标消费)NetWorld 为每个 shooter 保存 FLY_TARGETING / ADD_FLY_TARGETING 目标队列;GC_CREATE_FLY 在终点 VID 缺失时回退到 队列中的目标 VID 或全局厘米坐标,补上原版 SetFlyTarget / AddFlyTarget 的关键语义。 netbridge_test.gd 覆盖无终点 VID 的投射物创建;真实弹道模型、命中时机和特效仍待补齐。

  • 2026-08-31 增量 24(弓技能发射时机)SkillTable.is_ranged() 根据 weapon_limit=BOW 识别远程技能;GameScene 为弓技能暂存 CG_SHOOT,在 .msa 的 FLY motion event 到达时发送,超时自动清理,避免近战技能误发。skill_test.gdeterngrn_polish_test.gd 覆盖远近程识别及一次性发射。

  • 2026-08-31 增量 25Atlas 大地图窗口):新增 AtlasUI,复用地图区块 minimap.dds 构建 Atlas 底图;支持 M 键打开 / 关闭、地图名称、玩家位置(全局厘米)显示、 左键拖动和右键复位。无底图资源时仍保留可交互坐标查询;atlas_test.gdgamescene_test.gd 覆盖窗口装配和输入路径。

  • 2026-08-31 增量 26(队伍地图标记):圆形小地图和 Atlas 从 get_party() / 实体快照 区分队员,绘制绿色队员点、队长环和 Atlas 队员标记;p9_test.gd / atlas_test.gd 覆盖 队伍标记绘制路径。队伍世界标记已不再是纯 UI 列表,仍需按原版颜色和离线状态逐项校准。

  • 2026-08-31 增量 27(输入差距复核):根据 game_camera.gd 当前实现修正文档:桌面右键 拖拽环绕、滚轮缩放和触屏单指 / 双指手势已经存在,不再列为缺失功能;剩余差距改为相机 模式、灵敏度 / 键位持久化及窗口冲突规则。

  • 2026-08-31 增量 28(移动速度校准)GC_CHANGE_SPEED 已暴露实体的 moving_speedNetPlay 在主角 entity_info 到达时调用 PlayerController.set_server_speed(), 以原版 100 为基准缩放本地步行 / 奔跑预测(0.25–3.0 限幅),netplay_test.gd 覆盖 150 → 1.5×。

  • 2026-08-31 增量 29(攻击速度校准)GC_CHARACTER_ADD[2] / GC_CHARACTER_UPDATEattack_speed 已进入 EntityStore / M2Client.entity_dict();本地主角的 POINT_ATT_SPEED 也通过 get_points() 暴露。NetPlay 按 40250/ClientVS22 的 attack_speed / 100.0 动作速率换算自动攻击间隔(0.25–3.0 倍限幅,100 → 0.6 秒),并由 netplay_test.gd / net_entity_test 覆盖实体字段和 150 → 0.4 秒。仍需用真实 .msa motion duration、武器 / 坐骑 规则和服务端攻击合法性逐帧校准。

  • 2026-08-31 增量 4(真机验证)net_e2e 加 post-2026-08-30 协议探针(env 门禁 MT_E2E_CHARCREATE / MT_E2E_MYSHOP / MT_E2E_CUBE)。真服 192.168.21.203 确认 1:1CG_CHARACTER_CREATE(77B/u16 job) + GC_PLAYER_CREATE_FAILURE(5B) + GC_EMPIRE(5B) + GUILD_GC_SKILL_INFO(17B body) + GUILD_GC_WAR + CG_MYSHOP (38B+13B×N) 全部被真服接受 / 解析正确,无 unknown header。Cube/MALL 送出路径通但 需 NPC 交互;CG_CHARACTER_DELETE / 龙魂精炼 / 会徽上传(破坏性)仍未真机验。

  • 2026-08-31 增量 3:两个 项落地首版 —— 私人商店 / 道具商城 CG_MYSHOP / GC_MALL_OPEN|SET|DEL / CG_MALL_CHECKOUT + private_shop_ui / mall_ui)、Cube 제작GC_CHAT/COMMANDcube … 总线 → CubeState + cube_ui)。能量值已由 POINT_ENERGY 快照接入 HUD;仍缺原版拖放 UI 细节。 拍卖行默认构建未见完整 UI / handler,但协议受 __AUCTION__ 条件编译控制;当前目标服务端 默认关闭 → 标注 不做(见上「不做清单」)。 反作弊CG_HACK + ProcessCRC/ProcessScanner)→ 标注 不做。 「协议覆盖审计」段改为逐条清单:当时按 wire.h vs Packet.h 核对,主网络层 仍缺 24 个 header(位置同步上行 / 战斗广播 / 任务 / 社交 / 改名 / 世界玩法整组; CG_HACK 已移入「不做」),另有 3 个「已声明未消费 / 待核」。

  • 2026-08-31 增量 2

    • 角色创建 / 删除流程完成首版并接近 1:1(原为“暂不纳入”)——见新表行 「选人 / 建号 / 删号」。
    • 怪物 / NPC 真模型渲染修复:root/npclist.txtvnum→代号→目录)解析 + net_world.catch_up() 补拉协程化 setup() 漏掉的初始 spawn 洪流。世界玩法行 的“NPC / 怪物首版”由“能出占位胶囊”提升为“能出真模型 + 贴图 + 动作 + 名字牌 / 血条”。
    • GC_EMPIRE(0x0109) 已暴露为 M2Client.get_empire();选人页显示国家名 + 国旗。
    • 关键 1:1 差距表新增“状态”列。

结论

metin2-client 已经不是早期“只能登录、移动和攻击”的骨架:登录到 PHASE_GAME、基础战斗、 背包 / 装备、技能 UI、任务、聊天、组队 / 好友、商店 / 交易 / 仓库、精炼, 地图和部分特效都已有可运行首版。

当前的主要差距已从“有没有功能”转为:

  1. 部分关键协议只实现了表现包或收包,没有完成原客户端的完整上行语义;
  2. 数据模型已扩到 24 个 wear 位置和独立 16 格腰带,并接入部分装备 / 时装换模,槽位版图与 发包坐标已按 GameType.h 对齐(增量 96),装备校验链(修改 2:antiflag→wearable_flag→limit 已由 equip_rules.gd 在客户端防御性重建(增量 97),变身 / 动物时装 / 婚纱的部位遮挡(修改 4) 已由 part_hiding.gd + equip_model.gd 落地(增量 98),但原版 costume / belt 窗口、拖放、tooltip、 远端 PC 换模(修改 5,远端仍是占位胶囊)、变身态字段和校验链的精确 tie-break(接服务器)仍不完整;
  3. 多数玩法窗口是简化实现,未复现原 Python UI 的完整行为;
  4. EffectLib、SpeedTree、材质和世界表现仍有明确占位或近似;
  5. 打包资产路径、桌面配置和输入体验尚未形成原客户端等价闭环。

因此当前应称为“核心功能纵切完成”,不能称为 40250/ClientVS22 的 1:1 客户端。

已具备的首版能力

  • 登录、服务器 / 频道选择、频道负载状态、断线回登录、选人、建号 / 删号、进入游戏;
  • Metin2 风选人页(背景 / 3D 角色 / 职业书法字 / 国家 + 国旗 / 四维条 / ◀▶ 槽位 / 建删弹窗);
  • 基础移动、攻击、选目标、生命 / 法力 / 经验、伤害飘字、affect、死亡与复活窗口;
  • 背包、基础装备换模、地面物品、技能窗口和快捷栏读取;
  • 聊天、私聊、基础任务对话和任务日志;
  • 组队、好友、情侣名 / 爱意值状态、NPC 商店、交易、仓库;
  • 公会、公会技能、公会战、会徽上 / 下载、精炼(公会相关验收暂缓);
  • 地形、建筑、水体、NPC / 怪物真模型(vnum→npclist.txt→目录)、小地图、频道、昼夜和天气首版;
  • GR2 模型 / 动画、换装首版、武器 / 盾挂点、距离 LOD / 淡入、角色接触阴影、 item_proto 驱动的球面高光和 .mse 粒子首版。

这些项目只说明已有入口和基础闭环;是否 1:1 仍以下表为准。

关键 1:1 差距

优先级 状态 模块 当前实现 需要迁移的原客户端语义
P1 🟡 选人 / 建号 / 删号 / 改名 Classic 当前使用 CG_CHARACTER_CREATE=4 / CG_CHARACTER_DELETE=5 / CG_CHANGE_NAME=106 / GC_PLAYER_CREATE_SUCCESS=8 / GC_PLAYER_CREATE_FAILURE=9 / GC_PLAYER_DELETE_SUCCESS=10 / GC_PLAYER_DELETE_WRONG_SOCIAL_ID=11 / GC_CHANGE_NAME=107;全部有尺寸断言;GameClient 发送接口、角色槽位补丁和 drain_*_events()M2Client 同名建删改 API、4 个建删结果信号及 char_name_changednet.loopback_flow 覆盖建删和改名名称字节 / pid 广播。真机验证(2026-08-31net_e2e MT_E2E_CHARCREATE=1:建号、GC_PLAYER_CREATE_FAILUREGC_EMPIRE 已验。选人页有建号 / 删号弹窗、强制改名弹窗与国家国旗 CG_CHARACTER_DELETE 破坏性,真机未验;强制改名、重名 / 非法名错误码和服务端状态未真服验收;建号缺外形 / 发型选择与属性再分配 UI(现发 shape=0 + 职业基础四维);原版选人台座、镜头动画、OnCreateFailure 完整错误码文案
P0 🟡 技能施放协议 CG_USE_SKILLCG_FLY_TARGETINGCG_ADD_FLY_TARGETINGCG_SHOOT 已有尺寸断言、原生发送接口和 loopback 字节验证;服务端 GC_FLY_TARGETING / GC_ADD_FLY_TARGETING 已进入 EntityStore,通过 M2Client.fly_targeting 暴露,且已补入 GameClient 世界分派;GC_CHANGE_SKILL_GROUP 会更新 get_skill_group() 并使技能快照失效;快捷栏先发 use_skill(skill_id,target_vid),再同步 CG_MOVE 技能动作;弓技能会在 .msa FLY 事件发送 CG_SHOOT(增量 86:单槽 + 1.5 s 定时器改成 FIFO _pending_shots 队列,支持连射多箭在途各自一发;弓普攻也走同一路径,并在挥击起手由 net_play._send_fly_target()CG_FLY_TARGETING)。真服单会话已发送 CG_USE_SKILL / CG_FLY_TARGETING / CG_ADD_FLY_TARGETING / CG_SHOOT,均无断连 / unknown 仍需实现范围 / 多目标选择器来自动驱动 ADD_FLY_TARGETING;冷却、目标合法性、真实弹道和客户端表现也须按技能类型细分;尚未在真服观察到对应 GC 飞行目标推送
P0 🟡 快捷栏持久化 服务器保存 36 槽,客户端本地显示 4 页 × 8 格;类型统一为 INVENTORY(1) / SKILL(2) / EMOTION(3),清除 / 交换 / 回包恢复和技能冷却已有;数字 1–4、F1–F4、LSHIFT 翻页、Ctrl/Command+19 表情已按参考接入,inventory → quickslot 有全局拖放首版 仍需完整刷新死亡 / 变身 / 换武器后的服务端状态、跨窗口快捷栏拖放和真实服务端验收;macOS F1–F12 受系统 Fn / 功能键设置影响
P0 🟡 物品操作 已有移动、使用、丢弃、拾取;Shift 选“来源→目标”发 ITEM_USE_TO_ITEMCtrl+右键向当前目标 ITEM_GIVEAlt+右键以数量框发 ITEM_DROP2;增量 54 增加全局 item payload、鼠标跟随图标、inventory / equipment / belt → inventory / quickbar 投放、无效区域 / 右键 / ESC / 失焦取消和 PICK / TALK / ATTACK 光标;增量 56 接入 safebox 45 格分页 / 仓库内移动、mall 分页、exchange 12 格、shop 出售目标和 private_shop 40 格;增量 57 使 payload 携带 anti_flags / flags / sockets 等实例字段,普通拖放保留整组数量,Shift+左键支持 stack split,金币使用 ITEM_MONEY=-1 独立态;交易、NPC 商店和私人商店的关键 anti-flag / 确认边界已接入;增量 58 统一基础 tooltip;增量 60 让 m2dev / Classic 交易包保留 sockets[3]attrs[7]GC_VIEW_EQUIP 已保存 11 格检查快照并弹出装备查看窗 仍需完整交易事务、宝石专用规则,以及装备查看窗的原版槽位图标、3D 纸娃娃与交互;CJK IME、真实 .sub 资源、tooltip 特殊分支、真服 / 真机输入仍待验收
P0 🟡 装备 / 背包数据模型 常规背包 90 格、运行时 24 wear 位、腰带 16 格、基础 tooltip 已有;增量 96 按 GameType.h 对齐基础 wear / costume / ring / belt 发包坐标;增量 97 按 ItemData.h 防御性重建 CanEquipNow 的 antiflag→wearable→limit 校验;增量 98 完成变身 / 动物时装 / 婚纱部位遮挡;增量 99 让远端 PC 创建 RemotePlayerView 并消费 awPart[ARMOR,WEAPON,HEAD,HAIR] 四字段换装 变身字段、所有职业 / race 的资源、真服远端换装、精确 tie-break / 同槽替换 / LIMIT_PCBANG·REAL_TIME* / 属性读取、原版 costume / belt 视觉、装备对比和真机仍待验收
P0 🟡 移动与战斗同步 GC_MOVE、本地预测、攻击;M2Client.character_position() 可发送姿态回报,M2Client.sync_positions() 可按原包批量发送最多 16 个坐标,M2Client.request_warp() 可发送 CG_WARP 主动传送请求;GC_SYNC_POSITION 批量坐标纠偏、GC_CHANGE_SPEEDGC_WALK_MODEGC_CHARACTER_POSITIONGC_DIG_MOTION 已恢复到实体状态,NetPlaymoving_speed/100 校准主角步行 / 奔跑预测,挖矿广播可驱动目标朝向 / dig 动画,WALK_MODE 会驱动远端 walk/run 动画选择;真服单会话已发送 CG_CHARACTER_POSITION(5B) 与 CG_SYNC_POSITION(16B),均无断连 / unknown本地移动改为 Src/Dst 模型(增量 76,§3.1player_controller.gdwalk_to()_goto() 对齐 CInstanceBase::NEW_Goto()InstanceBaseMovement.cpp:251),太近(< MOVABLE_GROUND_DISTANCE_M 1.0 m)不动、三门 _can_translate()frozen/locked/moving_skill__IsSyncing/isLock/IsUsingMovingSkill)未过时按 __ReserveClickGround 存点 + 0.1 s 延迟重试,moving_skill 只转向,WASD 分支 _is_going = false远端同步改为命令队列(增量 77,§3.2 part AEntityStore 远端 Actor 位置由 std::deque<StateCmd> 驱动(1:1 复刻 StateProcessInstanceBase.cpp:1278),mut_move() 入队 + drain_state_queue()set_server_frame_ms()(为 0 即时释放)+ __CanProcessNetworkStatePacket 逐条释放,apply_state_cmd()FUNC_WAIT 1.0 / FUNC_COMBO/ATTACK/MOB_SKILL 50.0 硬阈值 + m_kMovAfterFunc 到达后动作 + skip_collision,线性插值只保留给 §2.1 可见性;上行 6 事件回调(增量 78,§3.2 part Bnet_play.gdPythonPlayerEventHandler.cpp 拆成 OnMove(立即 FUNC_MOVE + 重置 moving/waiting 节流窗)/ OnMoving300 ms 节流)/ OnWaiting_tick_on_waiting100 ms + 位移 ≥ 1 cm/ OnStopFUNC_WAIT/ OnAttackCG_ATTACK 外额外 FUNC_COMBO/ on_use_skill()`FUNC_SKILL motion),统一走 _send_statenet_world.gd去掉自有follow_lerp,节点位置直接跟随 e.pos(插值全交 C++ tick());**真实服务器毫秒帧钟(增量 79,§3.2 收官)**:ClassicStream::server_frame_ms()(≈ ELTimer_GetServerFrameMSec())由 HDR_HANDSHAKE存的m_server_time_base = hs.time + lDelta/m_client_time_base 算出,ClassicSession::process()每帧set_server_frame_ms() 后 classic 后端命令队列按服务器时间放行(m2dev 恒 0 = 即时释放,无回归);drain_state_queue加防呆阀STATE_QUEUE_MAX_WAIT_MS=1000chk_time 领先帧钟超 1 s 立即放行,参考端无此上限,有意偏离);**Actor-Actor 碰撞 + 坐骑转向速度(增量 93,§3.1 收尾)**:player_controller._actor_blocked()近似TestActorCollision(8 m 距离门 + body 球重叠 ACTOR_BODY_RADIUS_M=0.55+ 趋近判定)+_process 逐轴门控(单 Actor 滑移 / 两轴挡=BlockMovement),skip_actor_collision(≈ CanSkipCollision())观战置位;rotation_speed_deg+_turn_toward() 逐帧限幅收敛,net_play._refresh_motion_mode()坐骑切ROT_SPEED_HORSE_DEG=300c_fDefaultHorseRotationSpeed/ 常态 1200,替换两处 rotation.y = atan2() 瞬转;player_move_test.gd` 5→8 组
P0 🟡 PVP / 决斗 / 阵营 GC_PVPDUEL_START 已进入状态层,并以 pvp_changed / duel_started 信号提供给 Godot;挑战 / 战斗 / 复仇关系会在世界实体上显示标签;实体已有部分 empire / alignment / pk 字段;可攻击判定 IsAttackableInstance 已 1:1(增量 94entity_rules.gd + net_play._is_attackable / _attack_ctx,含 alignment grade/type、IsSameEmpireIsConflictAlignmentInstanceIsPVPInstance__FindDUELKeyIsSamePartyMemberIsWoodenDoor 可攻击判定(增量 94 落地);原版名字颜色(ENameColor §8.8(增量 110 落地:name_color.gd 1:1 GetNameColorIndex / SetEmpireNameModenet_world 事件 + ~2Hz 兜底重染 Label3D.modulate,帝国名色默认关、开关 net_world.set_empire_name_mode() 已由增量 118 接 §8.4「名字色」radio);头顶称号色 / 公会名 / 等级(增量 111 落地:text_tail.gd 1:1 GetTitleColor / RefreshTextTail / UpdateTextTailLevel / EnablePKTitlenet_world 驱动 GuildTag / TitleTag / LevelTag 子标签、开关 net_world.set_pk_title_enabled() 未接 UI);头顶聊天 / 信息气泡(增量 112 落地:chat_tail.gd 1:1 RegisterChatTail / RegisterInfoTail / TextTail_GetLivingTime / IsChatTextTailnet_world 每 vid 一条 "bubble" Label3D + 5s 硬删 + "名字: " 前缀剥离 + SHOUT·COMMAND 排除、register_info_tail() 公开方法);头顶名字「是否显示」/「总是显示名字」/ LALT 名字显示 / 目标强显(增量 113 落地:name_show.gd 1:1 IsAlwaysShowName / SetAlwaysShowNameFlag / __IsShowName / ShowAllTextTailfDistanceFromPlayer < 3500 平面距裁剪 / ShowCharacterTextTailisShow · !IsGuildWall · CanPickInstance 资格 / UpdateAllTextTail 的 chat bNameFlag 强显,net_world._name_shown() 四路:_always_show_namegame_option_ui radio/ _alt_heldLALT/ _target_vidnet_play.target_changed/ chat bNameFlag);头顶伤害飘字 AddDamageEffect / ProcessDamage(增量 114 落地:damage_effect.gd 1:1 enum DamageFlag / RecvDamageInfoPacketbSelf / bTarget / damage >= 0 门 / AddDamageEffectIsShowDamage() 门 / ProcessDamage 分类(DODGE|BLOCK→MISS、CRITICAL·POISON 参考端注释掉走普通数字、bSelf"damage_"!bTarget"nontarget_" 不渲染、否则 "target_"while(damage>0) 逐位)/ SetShowDamageFlagnet_world._on_damage / _process_damage_queue 每帧 pop 一条、game_option_ui「伤害数字」radio 接 net_world.set_show_damage);头顶目标框 RecvTargetPacket 收包分派 / CanViewTargetHP(增量 115 落地:target_board.gd 1:1 RecvTargetPacket 判定树(实例缺失→CloseTargetBoard、目标已死→不动框、IsPC()|IsBuilding()CloseTargetBoardIfDifferentCanViewTargetHPIsStone()|IsWoodenDoor()|IsEnemy())→SetHPTargetBoard、否则→CloseTargetBoard+ game.py SetHPTargetBoard 换 VID 先 ResetTargetBoardnet_play._on_target_info 由「任意有名字 VID 都显框」改为按判定树分派:PC / building 不显血条框、NPC / warp 关框);目标框「看他国玩家」前置过滤(uitarget.py TargetBoard.Open :189-193(增量 118 落地:target_board.hidden_as_other_empire + net_play._on_target_infoclassify 前判 —— game-option 关「看他国目标框」且目标异阵营 → _clear_target() returnnet_play.set_view_other_empire_target()game_scene._on_display_option 接 §8.4 target_board radio、开局 display_value 回填、game_option_ui.gd target_board 默认修 0→1);仍需 PK 模式上行、保护区 ATTRIBUTE_BANPK、原版目标板视觉 / 决斗 UI、可攻击光标按 ENameColor 驱动;seam__FindPVPKey / __FindGVGKey / IsGameMaster / IsWall race 白名单 / m2dev IsKiller 位的 packet setter、name_colorkind_resolver 未注入时回退裸 ch_typeseam ⑥)、text_tail 子标签 billboard 竖排 vs 屏幕空间排版(seam ⑦;逐像素水平排版增量 121 已 1:1、纵向位移 119、文本宽换真实字体度量 GetTextSize 123 已 1:1、单行文本高换 GetTextSize 的 h 124 已 1:1、剩余:无公会徽记 mark 实体 / 三子标签竖排叠 x 偏移 vs 同一屏幕行)、title_name_resolver 未注入回退裸 key(seam ⑧)、chat_tail 气泡与名字 Label3D 并存、名字与气泡未合成单尾标链表(seam ⑨ 遗留;名字随气泡下移 17px 增量 117 落地:name_tail_layout.gd 纯静态第 28 个 canonical UpdateShowingTextTail :110-114 —— chat 尾标 bNameFlag TRUE 时名字 Label3D 局部 y 顶到「气泡 y + 17*pixel_size」,net_world._reposition_name_tag_apply_chat_tail / _expire_chat_tails 调用)、子标签竖排 vs 参考屏幕空间逐像素水平排版(seam ⑦ 遗留)(增量 119 落地纵向:name_tail_layout.gdname_tail_lift_delta / subtag_world_y 照抄 ArrangeTextTail :211/:228/:258/:283 —— 三子标签屏幕 y 全锚在已被 UpdateShowingTextTail :114 顶过的 pTextTail->y 上,net_world SUBTAG_DEFAULT_Y 常量 + _reposition_name_tag 顶完名字尾标后按同一 lift 平移 GuildTag / TitleTag / LevelTag;增量 121 落地横向:name_x_shiftfxAdd 8/4/0/ title_x_offset / level_x_offset / subtag_world_x 照抄 ArrangeTextTail :211-320 非 EUROPE 分支,net_world._layout_subtags_x_apply_text_tail_to 尾部设名字 / TitleTag / LevelTag.position.xGuildTag 恒 0;增量 123 落地文本宽:text_metrics.gd 纯静态第 30 个 canonical text_size / width_of / label_widthFont.get_string_size = CGraphicTextInstance::GetTextSize 1:1),_layout_subtags_xname_w / title_wground_items._arrange_tags 的名条框 w 改走 TextMetrics.label_widthSUBTAG_CHAR_W_PX / TAG_CHAR_W_PX 保留仅作无字体兜底;增量 124 落地单行文本高:text_metrics.gdFALLBACK_LINE_H_PX := 15.0 + height_of / label_heightground_items._arrange_tags 的框高 h 改走 TextMetrics.label_height= GetTextSize 的 h)、TAG_LINE_H_PX 保留仅作无字体兜底;seam ⑦ 剩余:无公会徽记 mark 实体 / 三子标签竖排叠 x 偏移 vs 同一屏幕行)、悬停 chr.Pick() 强显(增量 116 落地:pick_show.gd + net_world._hover_vid forced 第 5 路 + player_controller.hover_entity_changed)、屏幕空间 ArrangeTextTail 防重叠 增量 120 落地:text_tail_arrange.gd 纯静态第 29 个 canonical boxes_overlapisIn+ arrange_item_tails(外层逐条 / 内层比对 / 命中顶到 pCompareTextTail->y + yEnd + 5 / 从头重跑 / MAX_ITERS 20 封顶),ground_items._arrange_tags 每帧投影所有掉落名条、跑去重叠、按 pixel_size 折回局部 ygame_scene 传相机 getterCanPickInstance 仅按死透近似(seam ⑩:名条框宽 / 单行文本高已分别由增量 123 / 124 换真实字体度量 GetTextSize 的 w / h 1:1、带 owner 的去重叠框已由增量 125 按 SetItemTextTailOwnerPythonTextTail.cpp:706-737)的「owner 独立度量 + yEnd += yOwnerSize + 4」换 1:1text_tail_arrange.gd 加纯静态 item_box_with_ownertext_metrics.gdstring_width_like / string_height_likeground_items._on_addeditem_name / item_owner meta 后 _arrange_tags 把名字子串与 owner + 's 子串分别度量),剩余:owner 未独立渲染成 CENTER 黄字 / strOwnership 未接本地化语素、只对掉落 item 名条)、伤害飘字单 Label3D 写整串 vs damagevalue dds 逐位特效 / self 双资源交替 / 全局单队列(seam ⑪)、目标框 poc 无 building / wooden-door 实体类型(is_building 恒 false、木门分类缺)/ LCONTROL 悄悄话捷径未接(增量 122 已接:net_play._on_pick KIND_PC 分支轮询 whisper_on_ctrl_clickwhisper_requestedgame_scenechat.start_whispergame_scene KEY_CTRL_lcontrol_down/ target_info 由 C++ EntityStore 侧抽出不改 C++ 收包(seam ⑫ 末尾:门实体分类、独立 whisper dialog 窗口)
P1 🟡 Actor 战斗运行时 有骨骼动画、基础 combo / 相机抖动;受击不再用固定硬直秒数(增量 73 删除 _hitstun_until),「能否行动」统一走 _can_process_network_state() / _can_attack() / _can_use_skill()(对齐 InstanceBase.cpp:1247 / InstanceBaseBattle.cpp:355 / :310),击退态由受击 .msa 播放生命周期驱动、动作播完清零;动作切换按 Granny EaseInCurve(0,0,1,1)2w²−w³ 逐骨 crossfade;角色 mesh 支持距离 LOD、15% 迟滞和约 0.18s 新旧 mesh 淡入(LodGhost + lod_fade uniform);右手武器 / 左手盾按 weaponComposite[0] = weaponInvWorld[0]·weaponLocal[0] grip 复合挂 equip_right_hand(探针实测 Δ≤0.06 m,非标准米级偏移文件夹断回退);.msa motion event 已分派声音、EffectPosition 特效、震屏、屏闪和弓箭 FLY(增量 86:type 6 FIFO _pending_shotsclient.shoot() = OnShootOnSetFlyTargetnet_play._emit_swingclient.add_fly_targeting();WARP 留钩子);玩家 / 怪物强制实时投影并附脚底接触阴影 Decal;点击 / 攻击距离按目标类型分 150 / 500 / 100 cm(增量 74,对齐 InstanceBaseBattle.cpp:294);普攻节奏改由 .msa ComboInputData.DirectInputTimeget_motion_data().next_combo,缺组退化 duration*0.9)除以 bAttackSpeed/100 得出,attack_speed 只缩放动作播放速率不再写死间隔;自动攻击走 ReservedMode 状态机(增量 75):_update_auto_attack() + _reserve_process_click_actor() 严格 10 步(照 PythonPlayer.cpp:207 / PythonPlayerInput.cpp:877:出距离 pc.walk_to 靠近、NPC 走 click_npcIN_SAFE / DEST_IN_SAFE / EMPTY_ARROWsignal cannot_act、第 10 步按节奏 _do_attack_swing PC 连击段表已解析(增量 80formats/combo_table.{h,cpp}playersettingmodule.pyReserveComboAttackNew / RegisterComboAttackNewmap<MAKE_COMBO_KEY, vector<段号>>,非 .msm),并已接进 net_play.gd 连击状态机替掉 (_combo + 1) % 3(增量 81_do_attack_swing / _run_next_combo / _combo_process / _on_end_combo / _clear_combo 逐行对齐 ActorInstanceBattle.cpp:157/213/256/297/308,按 .msa ComboInputData 三时间驱动 _combo_index / _is_pre_input / _is_next_pre_input,段号经新静态绑定 Metin2AnimPlayer.parse_combo_tables()race & 3 取);.msa HitDataContainer 多命中窗已导出(增量 82formats/msa.{h,cpp} Msa::HitWindow + get_motion_data().hit_windows / motion_type / hit_limit_counttextscript.cppList 解析,含真实双命中窗 assassin/dualhand_sword/combo_01.msa);命中帧已落地(增量 83net_play.gd _begin_hit_windows / _attack_process(dt) / _register_hit / _flush_victim_list 逐帧按动作时间与 hit_window [start,end] 相交对 client.get_entities() 可攻击 / 未死 / 非安全区实体做判定,增量 130 起为 _normal_attack_process 的刀尖 Z 圆柱 vs .msm 防御球逐行判定,m_HitDataMap 每窗查重 + 命中上限 NORMAL/COMBO 16 / SKILL hit_limit_count,命中 → client.attack(_swing_skill, vid)OnHit),被击退目标帧末 client.sync_positions() ≤16_emit_swing 改为只有缺命中窗数据才在挥击瞬间发 CG_ATTACK);武器种类 → combo_motion_mode 映射已落地(增量 84net_play.gd motion_mode_for() 逐分支复刻 CInstanceBase::RefreshStateSetMotionModeInstanceBase.cpp:2851),_refresh_motion_mode()parts[WEAPON] / get_equipment()[WEAR_WEAPON].vnum + proto.item().{type,sub_type} + mount_vnum / polymorph / wedding_dress,由 _on_entity_info / inventory_changed / mount_changed 三处触发,换模式内部 _clear_combo()combat_fx_testFakeProto + 15 段映射 + 2 段端到端断言);弓箭 __GetBowRange() 已落地(增量 85_clickable_distance()_is_bow_mode() 改用 _bow_range_cm() = BOW_RANGE_BASE_CM(2500-100) + _bow_distance_bonusPOINT_BOW_DISTANCE=34_on_points()points["bow_distance"]),NPC/采集物分支覆盖之,逐分支对齐 NEW_IsClickableDistanceDestInstance + __GetBowRangecombat_fx_test §3.5 段加 8 段断言);命中判定与命中效果已逐行对齐(增量 130:hit_collision.gd detect_z_cylinder + .msm 防御球;_process_attack_successInsertDelay 硬直、physics_push.gd 击退 + GetBlendingPosition 同步、命中特效、__HitGood / __HitGreate / __HitStone 受击动作链与抖动;攻速同时缩放 GetAttackingElapsedTime 与动作速率;combo_changedSetComboSkillFlagmotion_boundm_HitDataMap / 连击段号;send_use_skill 不再夹带 CG_FLY_TARGETING);剩余:击退无地形 / 角色碰撞,防御球未跟骨骼矩阵,远端角色不跑 AttackProcess;坐骑移速规则、POINT_BOW_DISTANCE 真服下发路径和服务端合法性校准;技能合法性判定位数据层已落地(增量 87:ui/skill_table.gdRegisterSkillDesc 把 skilldesc 第 11/12 列折成 attr_bits / need_weapon_bits + skill_typeIsCanUseSkill / IsStandingSkill / IsHorseSkill / IsNeedTarget / IsNeedCorpse / CanChangeDirection / IsChargeSkill / CanUseWeaponType 等谓词逐条对齐 CPythonSkill);三层校验已落地(增量 88project/player_skill.gd extends RefCounted 纯校验器逐行移植 PythonPlayerSkill.cpp 的 (a)click_skill_slot:106/ (b)can_use_skill:439,硬编码骑乘术 109/ (c)check_skill_usable:166 完整前置顺序)/ (d)use_skill:460 + _resolve_target 目标矩阵),返回 {ok, code, target_vid}net_play.skill_context() 注入运行期状态,ui/quickbar.gd activate() 施放前跑三层、非静默码 → 信号 skill_rejectednet_play.cannot_act(同一 OnCannotUseSkill 通道),新增 player_skill_test.gd);边界收紧 + 短蓝短血校验已落地(增量 89,§3.8 修改 3 → 三层全落地:net_bounds.h SKILL_MAX_NUM=255 + skill_index() 谓词,M2Client::use_skill()skill_id 护栏收紧成 !bounds::skill_index() 对齐 ClickSkillSlotslot >= SKILL_MAX_NUM 门;skill_table.gd 解析 .msk NeedSPFormula + _SKILL_POWER[] 功率表 → need_sp / need_hp / _skill_pointplayer_skill.gd _check_short_mana / _check_short_life 从桩改为移植 __CheckShortMana:320/ __CheckShortLife:354),HP/SP 经 net_play.skill_context()get_points() 供入、-1 不判);技能升级门已落地(增量 90,§3.10:wire.h +POINT_SKILL=28 / POINT_SUB_SKILL=27 / POINT_HORSE_SKILL=113M2Client::get_points() 透出 skill_active / skill_support / skill_horseskill_up() 入参收紧为 !bounds::skill_index()skill_table.gd +max_level_of / skill_level_limit(本 40250 数据无 LEVEL_LIMIT 列 / .msk 记号 → 恒 0/ can_level_up_skill(id, level)= skillCanLevelUpSkill,本 build 前置技能 / 前置属性字段无人填充 → 化简为 level < GetMaxLevel() && !CANNOT_LEVEL_UP);skill_ui.gd refresh() 逐行移植 uicharacter.pyRefreshSkillPlusButtonList / __RefreshSkillPlusButton / CanShowPlusButton——技能组门 get_skill_group()!=0 + 三技能点来源分开取 + skillGrade==0 + 马术页 level<20 + 主动页 can_level_up_skill + 辅助页 HIDE_SUPPORT_SKILL_POINT 整页隐;按钮 _rebuild() 建 / refresh()visible,升级结果由 GC_SKILL_LEVELskills_changed 驱动、不本地伪造;skill_test.gd +§3.10 段);ReservedMode 的 CLICK_ITEM / CLICK_POSITION / USE_SKILL 分派已落地(增量 91_refresh_reserved() 四路 match 逐行对齐 NEW_RefreshMouseWalkingDirection,含 CLICK_ITEM 的 20cm 拾取阈值、CLICK_POSITION 的 0.1s ReservedDelay 门、USE_SKILL 的 IS_HUGE_RACE 200cm 修正 + use_skill_hook 进射程施法;__ChangeTargetToPickedInstance 拾取回退 = IsTargetableInstance 限 PC/怪、否则清目标;扇形 / 圆形补 fly-targeting = send_fly_targeting()_instances_in_fan_range / _instances_in_circle_range + _random_pos_in_fan_rangeCG_ADD_FLY_TARGETING,几何逐行对齐 NEW_GetInstanceVectorInFanRange / InCircleRange);安全区 / 弓箭 / 骑乘 / dash 判定位的服务端来源、IS_HUGE_RACE vnum 白名单(现用 proto.mob size>=3 / rank>=4 近似)仍留桩;另补 combo 取消窗口、IsResistFallen / 击退碰撞、角色碰撞、同步碰撞、武器拖尾、剩余 motion event / 骨骼挂点语义、GPU skin LOD、双持 / 左手武器和目标锁定相机;03150 类偏移网格需 per-weapon 偏移数据或修 .gr2
P1 🟡 飞行物与命中特效 GC_FLY_TARGETING / GC_ADD_FLY_TARGETING 保存 shooter 的目标队列和 VID / 坐标;GC_CREATE_FLY 可消费队列并生成 tween 发光球增量 92project/fly_object.gdFlyManager / FlyInstance / FlyData 对齐 GameLib/FlyingObjectManager.{h,cpp} + FlyingInstance.{h,cpp}——Update() 逐帧真实弹道(vel += accel*dt + gravity+ 线段-点命中判定(_seg_point_sq_dist 1:1 移植 EterLib/GrpMath.h:80 square_distance_between_linesegment_and_point < m_fBombRange²+ OnShootDamage / pierce -- 续飞 / 超程 __Explode(false) / m_bHitOnBackground 触地;FlyData 默认值取 FlyingData.cpp __Initialize()net_world._on_fly 换成 _fly_mgr.spawn(...) + signal fly_explodednet_play._can_shot() 防御式重建 __CanShotgrep 全树无定义)——非弓 / 无箭 EMPTY_ARROW / 无目标 / 死目标 / 出弓程 OUT_OF_RANGE_emit_swing 弓分支据此跳过 CG_FLY_TARGETING / OnShoot seamGC_CREATE_FLY 在本 40250 客户端无包处理器(仅 NetStream.cpp 头串索引 70)→ 实际驱动走 POC 自有 fly_cue;重力参考端作用于 .z=上、本 POC 映射到 -Y;怪物球体碰撞 / CullingMgr.ForInRange / 建筑体素 r=1 判定未移植(客户端飞行物只做命中"时机",伤害服务端裁决);无 .fly 数据文件加载器 → 合成箭参数(init_vel=45 / flat_range=max(dist*1.5,30) / bomb_range=0.6);FIRE_CRACKER / AUTO_FIRECreateIndexedFly 分支已在常量层预留但本 build 无驱动来源;bomb effect / 声音 / EffectLib 特效仍是占位闪光
P1 🟡 任务 / NPC 增量 95 已完成 CPythonEventManager 常用标签解析 / 分派;增量 101 完成 Quest 日志 QUEST_MAX_NUM=5 五行窗口、离散滚动、默认 / 服务端 icon、clock 和任务行事件;增量 108 完成小地图任务信标越界夹圆周 + 指向目标的方向箭头(世界箭头 / 屏幕边缘指示首版,minimap.signal_marker_geometry + _draw_signal_arrow);增量 109 完成 quest 信件按钮条 interfacemodule.__ArrangeQuestButton 多列定位(yCount=(H-330)//63、换列 +100px、最新在前、组队窗 xPos→130);SelectItemUI、QuestButton、地图信号和对话图片已有首版 逐帧 UpdateEventSet、Skip / EndEventProcess、.msc、NEXT / DONE 文案、相机 / fade / cinema 消费端、CJK IME、任务日志窗体 _list 自身分页 / 多列、Atlas 同一信标夹边、完整 .sub 图标、立绘 / 电影、真实资源和真服仍待验收
P1 🟡 商店 / 交易 / 仓库 普通 NPC 商店、交易状态和仓库列表已有首版;SHOP_GC_START_EX 多货架已解析(m_shop_tabsget_shop()),shop_ui 带 5×8/40 格货位、货架按钮 + tabIdx*40+slot 买位置、空位禁用和 BUY 确认(固定 count=1;增量 56 增加仓库 5×9 分页 / 指定格 / 仓库内移动、交易 12 格指定投放和商店 BUY/SELL toggle / 出售投放区;增量 57 补齐出售 anti-flag、首次贵重物品确认、普通 / COUNT_PER_1GOLD 价格公式和失败音效;增量 58 接入统一物品 tooltip;增量 60 交易物品状态补齐 sockets[3] / attrs[7] 补交易事务回滚提示、仓库金钱 / 改密码、完整音效和 coin_type 非金币货币显示;真实 .sub / 图标和真服回包仍待验收
P1 🟡 私人商店 / 道具商城 首版CG_MYSHOP=55CG_MALL_CHECKOUT=69GC_MALL_OPEN=122 / GC_MALL_SET=128 / GC_MALL_DEL=129CGMyShopHead(38) + MyShopItem(13)×N 有尺寸断言;增量 56 增加商城 45 格分页 / 商城→背包以及私人商店 40 格全局物品态投放;增量 57 补齐 ANTIFLAG_GIVE/MYSHOP;增量 58 接入统一物品 tooltip;商城状态路由进 EntityStoreprivate_shop_ui / mall_ui 和真服探针均已接入。 仍缺真 .subprivateshopbuilder.py / shopdialog.py uiscript、资源视觉、完整价格 / 确认回包流程和真实鼠标拖放物品态验收
拍卖行 / auction house 不做 当前 40250 默认构建未发现完整 ClientVS22 拍卖行 UI / handler;但客户端 Packet.h 保留 __AUCTION__ 下的 CG_AUCTION_CMD=205,服务端也保留列表包 =206。当前默认服务端关闭该宏,因此本阶段不做;若启用需另行验收,不能称为“协议不存在”
P1 🟡 Cube (제작) / 能量系统 首版Cube 走 GC_CHAT/CHAT_TYPE_COMMAND 文本总线(对齐 ServerCommand()),EntityStore::apply_server_command 解析 cube open/close/info/success/fail/r_list/m_info(配方 v,c 列表 + @/&/` //分层材料 + 金币)→CubeState+drain_cube_events()GameClient::send_cube_make/material_info/result_list/cube make
P1 🟡 组队 邀请、接受、离队;成员信息板(party_ui.gd1:1 迁移 uiparty.PartyMemberInfoBoard):每员角色状态按钮(队长弹菜单 → CG_PARTY_SET_STATE=75 分配 攻/坦/迅捷/辅/宗/防 或踢人)+ 名字(★队长) + HP gauge + affects[7] 附加效果 chip;顶部 EXP 分配开关(party_set_distribute+ 组队治疗(party_use_skill(1,0);角色 / 治疗控件已按 40250 服务端 Leadership 规则本地门控(攻击≥10、坦克/迅捷≥20、辅助≥25、宗师≥35、防御≥40,治疗≥18);真服单会话 CG_PARTY_USE_SKILL(9B) 后仍在线;小地图 / Atlas 绿色队员点 + 队长环 仍需真版 .sub 图标 / 颜色、队伍技能冷却 / 权限、离线颜色语义和真实两账号组队验收
P1 🟡 好友 / 情侣 好友列表、上线状态和私聊入口已有;GC_LOVER_INFO(70B) / GC_LOVE_POINT_UPDATE(5B) 已有尺寸断言、EntityStore::LoverInfo 状态、M2Client.get_lover() / lover_changedlove_ui 常驻显示伴侣名和 0–100 爱意值;net.entity_storelove_ui_test.gd 覆盖 增量 129 补 messenger 三固定组(好友 / 公会 / 情侣,对齐 uimessenger.py:274-295)与 lover_login/logout/near/far/divorce 命令事件 → 情侣在线 / 在身边状态(love_ui 后缀 + friend_ui 分组行);仍需情侣 affect 表现、断线 / 切图生命周期、原版文案和真服务器端到端验收
P1 🟡 公会 信息、成员、技能、公会战、会徽上 / 下载和部分操作已有首版;GC_REQUEST_MAKE_GUILD=82 已解析并以 M2Client.guild_make_requested 通知;公会窗已弹出创建名称输入并发送 CG_ANSWER_MAKE_GUILDCG_GUILD_SYMBOL_CRC=113 / GC_GUILD_SYMBOL_DATA=133 已支持原始符号文件下载。增量 129 恢复公会范围:权限门(GUILD_AUTH_* 位控踢人 / 公告 / 技能升级 / 施放,grade 0 = 会长,数据未同步回退全开交服务器权威)、成员页资金行与踢人按钮、! 前缀公告发布、/gskillup <vnum> 技能升级、公会战宣战应答弹窗(state 3 → /war·/nowar 仍缺公会日志页、积分板、领地和公会建筑;完整真服验收待进行
龙魂精炼 当前目标 40250 服务端不提供该玩法;本阶段不纳入支持或验收,已有兼容代码仅保留作协议参考 若未来服务端增加该玩法,再另立协议、UI 和验收需求
P1 🟡 世界玩法 CG_WARP=65 主动请求、GC_WARP=65 处理、NetWorld / GameScene 的无条件 lAddr:wPort 重连与换图清理首版、CG_FISHING=82 / GC_FISHING=89 钓鱼方向与状态事件、NetPlay 钓鱼动作分发、ChatUI 鱼获 / 失败日志、CG_DUNGEON=110 / GC_DUNGEON=110 副本请求与状态事件、GC_DIG_MOTION=134 挖矿动作广播、GC_LAND_LIST=130 领地状态、GC_OBSERVER_ADD/REMOVE/MOVE=9698 观战者状态、频道、时间、NPC marker、天气粒子和 mount_vnum 字段;真服登录洪流已收到并解析 GC_LAND_LIST(436B),单会话发送 CG_FISHING(5B) 后收到“Please choose a Fishing Pole.”业务拒绝且连接保持 补完整钓鱼 UI / 鱼获背包落地、副本计时 / 入口 UI、坐骑真模型 / 骑乘动作 / 移速、地图 BGM / 天气配置、地形贴花、DungeonBlock;换图首版已落地,仍需真实 40250 端到端验收
P1 🟡 小地图 / Atlas 可画玩家、实体、NPC、任务标记、观战者紫色菱形和 GC_LAND_LIST 领地边框;支持 set_scale() / 鼠标滚轮缩放;增量 61 接入 EventManager 任务信号点、清除和 Atlas center adjust;增量 108 任务信号点越出小地图半径时按 m2dev PythonMiniMap.cpp:441-462 夹到圆周并改画指向目标的方向箭头(SIGNAL_RIM_INSET=9.0);Atlas 支持底图、地图名称、玩家坐标、拖动和坐标查询 补原 minimap / Atlas 贴图细节、提示、队员 / NPC 分类、Atlas 侧同一信标夹边、完整 .sub 边缘指示图标、地图驱动天气配置和原版窗口视觉
P1 🟡 EffectLib .mse Particle 已映射到 GPUParticles3D;增量 53 已读取首个 TextureFiles 的实际 .dds,并接入位置、发射形状、方向 / 速度;增量 64 已解析 .mde v1/v2、展开三角形帧、翻转 UV、接入 .ifl 贴图和 MeshElement 材质 / 透明度;增量 65 已接入 Light 组、范围曲线、多贴图 TexAni 的帧延迟 / 顺逆序和旋转类型 TexAni 随机起始帧 / 每粒子语义、MovingType / 时间曲线、Direct3D 材质 stage、骨骼挂点和资源逐项视觉验收
P1 🟡 SpeedTree .spt 只嗅探真实 bark / composite 贴图,几何为程序化 proxy 迁移或离线转换真实树干、叶片、frond、LOD、billboard、风和阴影;当前不能作为 1:1 树木
P1 🟡 地图 / 材质渲染 A1 地形、静态物、水和 .msenv 环境已有首版;splat 已支持每区块最多 16 个活动层、原客户端 UV / offset 与最大源分辨率,地形拆 patch 做视锥 / 距离剔除;水使用原 30 帧 DDS 序列、水深 alpha 和轻微高度动画;静态物 Y-up 旋转共轭已修正。GR2 blend / two-sided 优先读材质信息,球面高光管线已接 item_proto.specular;角色有实时投影 + 接触阴影;.msenv 双光层和纹理天空首版已接入 补其余固定功能材质与多贴图分支、精确 alpha-test / blend / cull、透明排序、逐 patch 几何 LOD、baked / realtime 阴影 mask、血迹 / 施法阵等 Decal、云层渐变过渡、滤色 / 风完整语义 / lens flare;按受控参考帧校准水深、LOD、阴影、光照和材质
P1 🟡 UI 控件语义 80 个 uiscript 可解析和构建,覆盖约 20 类基础节点 解析成功不等于窗口完成;需补动画图片、slot 拖放、scrollbar/listbox 行为、tooltip、radio group、候选窗、原层级 / 锚点 / 焦点 / modal 规则
P1 🟡 UI 窗口行为 背包、技能、任务、聊天、社交和商店等有简化窗口;腰带背包、扩展装备和装备查看已有兼容窗口;选人页已按 Metin2 风重做角色状态窗(char_status_ui.gd)已按迁移资产中的 characterwindow.py 布局与 uicharacter.py.RefreshStatus 逻辑实现,并使用原 /stat 加点命令作为迁移样板 选魔石窗 select_item_ui.gd系统设置窗 system_option_ui.gd游戏设置窗 game_option_ui.gd/setblockmode + /pkmode 真命令 + 显示开关持久化)、ESC 系统菜单 system_menu_ui.gdsystemdialog.py + /phase_select//logout//in_game_mall)、私人商店窗 private_shop_ui.gd组队成员板 party_ui.gd 均已按迁移样板做成;这些 Python 文件不在本次 ClientVS22/source/ 树中,仍需用对应 C++ 常量 / 行为和实际资源验收
P2 🟡 输入 / 快捷键 WASD、点地、触屏、右键拖拽环绕、滚轮缩放、双指捏合、I/K/J/O/G/L/Z、数字 14、F1F4、LSHIFT 翻页、Ctrl/Command+数字表情、空格连续攻击、Alt 显示名字、截图、帮助 / 坐骑 / 角色窗口和实体 PICK / TALK / ATTACK 光标已接;窗口键盘不会穿透到世界;增量 56 已接商店 BUY / SELL 光标 仍需 CJK IME、完整复杂窗口 mousemodule、不可达 / DOOR / CHAIR / MAGIC / camera resize 等光标状态、真实 .sub 光标资源、macOS Fn / 功能键实机验收;键位重绑定 / 相机灵敏度持久化不是参考端已有能力
P2 🟡 CJK 输入与本地化 支持 KEY→VALUE 查表和基础字体注入 PythonIME 中 / 日 / 韩候选窗、复数 / 性别规则、字体自动切换、完整 item / mob 名称与 tooltip 本地化
P2 🟡 音频 BGM crossfade、UI 音效池和 MilesLib 子集已接入:监听器、200/1000 缩放、音量曲线、普通 / 环境 / 角色 3D 播放、角色限频、停止与限幅淡出;本地 .msa 动作声音和 .mss 60 FPS 动作声音表走角色入口;地图 AreaAmbienceData + .pra 已接入 LOOP / ONCE / STEP、距离音量和卸载停止 补 EffectInstance 完整声音节点生命周期、完整武器 / 技能 / 怪物 / NPC 资源逐项验收和真机空间听感校准;.msenv 剩余项归入地图环境渲染与天气联动
P2 🟡 客户端外壳 Godot 工程已有 macOS / iOS / Android 构建脚本 config.exe 对应的分辨率、显示、音量、阴影、语言和鼠标配置;Movie / Web / Logo、补丁器约束、Discord RPC 按发布目标取舍
P0 发布 🟡 Pack 资产统一读取 PackMount / AssetSource 已实现并有 roundtrip 测试 Metin2World / Metin2Model 等仍有直接 AssetResolver::resolve() 的散文件路径;必须统一切到 AssetSource::read/to_path(),否则 .epk/.eix 打包客户端不等价

参考模块覆盖索引

下表以 40250/ClientVS22/source/UserInterface/ 的模块职责为索引,补充上表未展开的基础能力。 “已有”只表示当前有可运行首版;仍需满足本文末的 1:1 完成判据。

参考模块 / 能力域 当前实现 与 40250/ClientVS22 的差距 优先级
AccountConnector / PythonNetworkStreamPhase* 登录与相位 auth → game → 选人 → LOADING → PHASE_GAME 已打通;LoadingScreen、频道状态探针、Classic DirectEnter 和 Classic GC_WARP 重连 / 换图清理均有首版 登录 / 选服 / 选频道的原版错误码、表单校验、超时与会话失效文案,真实双连接 / 换图验收、服务器列表刷新和所有相位边界清理仍未逐项对齐 P1
PythonChat / PythonTextTail / PythonPlayer 聊天四标签、私聊 / 组队 / 公会 / 喊话路由,实体气泡、名字、HP 条、伤害飘字和本地气泡已有;头顶名字颜色已 1:1(增量 110name_color.gd GetNameColorIndex / SetEmpireNameModenet_world 驱动 Label3D.modulate);头顶公会名 / 称号色 GetTitleColor / 等级子标签已 1:1(增量 111,text_tail.gd RefreshTextTail / UpdateTextTailLevel / EnablePKTitlenet_world 驱动 GuildTag / TitleTag / LevelTag);头顶聊天 / 信息尾标已 1:1(增量 112,chat_tail.gd RegisterChatTail / RegisterInfoTail / TextTail_GetLivingTime / IsChatTextTailnet_world 每 vid 一条 "bubble" + 5s 硬删 + 前缀剥离 + SHOUT·COMMAND 排除);头顶名字「是否显示」IsAlwaysShowName / __IsShowName / ShowAllTextTail 的 3500 平面距裁剪 / ShowCharacterTextTail 资格已 1:1(增量 113name_show.gd 纯静态,net_world._name_shown() 四路 + game_option_ui display_option_changed 信号 + net_play.target_changed 强显);头顶伤害飘字 GC_DAMAGE_INFO / AddDamageEffect / ProcessDamage 分类 + IsShowDamage 门已 1:1(增量 114damage_effect.gd 纯静态,net_world._on_damage / _process_damage_queue 每帧 pop 一条 + game_option_ui「伤害数字」radio 接 net_world.set_show_damage);头顶目标框 GC_TARGET / RecvTargetPacket 收包分派 + CanViewTargetHP 已 1:1(增量 115target_board.gd 纯静态 Action { CLOSE, CLOSE_IF_DIFFERENT, SET_HP, NONE } 判定树,net_play._on_target_info 按判定树分派:PC / building 不显血条框、NPC / warp 关框、石头 / 敌对怪 SetHPTargetBoard、换 VID 先 Reset);game.py OnRender Picking 块分派 + 悬停角色强显名字已 1:1(增量 116,pick_show.gd 纯静态第 27 个 canonicalIsPickedWindow 门 + chr.Pick() / targetBoard.GetTargetVID()ShowCharacterTextTail + __IsShowName() 分叉 item.Pick()ShowAllTextTail + textTail.Pick() + SelectItemNamenet_world._hover_vid forced 第 5 路 + player_controller.hover_entity_changed);头顶名字尾标随聊天气泡上移 17px 已 1:1(增量 117name_tail_layout.gd 纯静态第 28 个 canonical UpdateShowingTextTail :110-114 —— chat 尾标 bNameFlag TRUE 时名字 Label3D 局部 y 顶到「气泡 y + 17*pixel_size」,net_world._reposition_name_tag_apply_chat_tail / _expire_chat_tails 调用;seam ⑨ 收口);目标框「看他国玩家」前置过滤 + §8.4 名字色 radio 接线已 1:1(增量 118target_board.hidden_as_other_empire + net_play._on_target_info 前置过滤 + net_play.set_view_other_empire_target / net_world.set_empire_name_modegame_scene._on_display_optiongame_option_uitarget_board / name_color radiogame_option_ui.gd target_board 默认修 0→1;§8.4 三显示 radio + LALT 全部接线);头顶「公会名 / 称号 / 等级」子标签随名字尾标纵向位移已 1:1(增量 119,name_tail_layout.gd name_tail_lift_delta / subtag_world_y 照抄 ArrangeTextTail :211/:228/:258/:283net_world SUBTAG_DEFAULT_Y 常量 + _reposition_name_tag 顶完名字尾标后按同一 lift 平移 GuildTag / TitleTag / LevelTag,气泡过期 / 「信息」尾标时回默认);掉落物名条屏幕空间去重叠 ArrangeTextTail item 块 + isIn 已 1:1(增量 120text_tail_arrange.gd 纯静态第 29 个 canonicalboxes_overlapisIn AABB 含边界 + arrange_item_tails 外层逐条 / 内层比对 / 命中顶到 pCompareTextTail->y + yEnd + 5 / 从头重跑 / MAX_ITERS 20 封顶 + box_* 四边 -w/2-2 w/2+2 -2 hground_items._arrange_tags 每帧把所有掉落名条锚点 cam.unproject_position 到屏幕、按字符数估框、跑去重叠、按 Label3D.pixel_size 折回局部 y、可见名条 < 2 回默认,game_scene 传相机 getter);头顶「公会名 / 称号 / 等级」子标签逐像素水平排版(ArrangeTextTail 角色块 PythonTextTail.cpp:211-320 非 EUROPE 分支)已 1:1(增量 121name_tail_layout.gd name_x_shiftfxAdd 8/4/0/ title_x_offsetnameW/28/ level_x_offset(有称号再 titleW/ guild_name_x_offset / mark_x_offset / subtag_world_xnet_world const SUBTAG_CHAR_W_PX := 7.0 + _layout_subtags_x_apply_text_tail_to 尾部设名字 / TitleTag / LevelTag.position.xGuildTag 恒 0(poc 无公会徽记实体);纵向 .position.y117/119)与横向 .position.x121)互不干扰);目标框点击 LCONTROL 悄悄话捷径已 1:1(增量 122net_play signal whisper_requested(name) + var _lcontrol_down + set_lcontrol_down()_on_pickKIND_PC 分支(C++ picking 只对 PC 派发 SetPCTargetBoard)轮询 target_board.whisper_on_ctrl_clicklcontrol_down and same_empire and not is_main and not is_building,谓词增量 118 已备)→ 发 whisper_requestedgame_scene._unhandled_input KEY_CTRL 按下 / 松开喂 set_lcontrol_down(e.pressed) + whisper_requestedchat.start_whisperpoc 版 OpenWhisperDialog=聊天输入预填 "/w 名 ");is_building 实参恒 false(poc 无门实体、:814 分支永不触发)、无独立 whisper dialog 窗口 = seam ⑫ 末尾);头顶子标签 / 掉落名条排版的文本宽换真实字体度量 CGraphicTextInstance::GetTextSize 已 1:1(增量 123text_metrics.gd 纯静态第 30 个 canonical text_size / width_of / label_width = Font.get_string_sizenet_world._layout_subtags_xname_w / title_wground_items._arrange_tags 的名条框 w 改走 TextMetrics.label_widthSUBTAG_CHAR_W_PX / TAG_CHAR_W_PX 保留仅作无字体兜底、name_tail_layout.gd / text_tail_arrange.gd 排布公式不动);头顶掉落名条屏幕空间去重叠框的「单行文本高」换真实字体度量 CGraphicTextInstance::GetTextSize 的 h 已 1:1(增量 124text_metrics.gdconst FALLBACK_LINE_H_PX := 15.0 + height_of / label_height = Font.get_string_size(...).yground_items._arrange_tags 的名条框 h 由硬编码 TAG_LINE_H_PX 改走 maxf(1.0, TextMetrics.label_height(tag, TAG_LINE_H_PX))TAG_LINE_H_PX15.0)保留仅作无字体兜底、text_tail_arrange.gdbox_end_y(h) = h/ name_tail_layout.gd 排布公式不动);带 owner 的头顶掉落名条屏幕空间去重叠框按 CPythonTextTail::SetItemTextTailOwnerPythonTextTail.cpp:706-737)的「owner 独立度量 + yEnd += yOwnerSize + 4」已 1:1(增量 125text_tail_arrange.gd 扩第 29 个 canonical 加纯静态 item_box_with_owner——无 owner -> 纯名字框、有 owner -> {maxf(name_w, owner_w-2.0), name_h+owner_h+4.0} 使 box_end_x(w) 恰等于参考 :737 fMAX(nameW/2+2, ownerW/2+1)text_metrics.gd 扩第 30 个 canonical 加 string_width_like / string_height_like 量某 Label3D 字体下的任意子串;ground_items._on_addeditem_name / item_owner meta_arrange_tags 把名字子串与 owner + 's 子串分别度量后走 item_box_with_owner;渲染仍单个合并 "name (owner)" Label3D 缺屏幕空间血条(名字颜色 / 称号色 / 公会名 / 等级 / 聊天气泡增量 110–112 已 1:1,名字显示开关 113,伤害数字 114,目标框 115OnRender Picking + 悬停强显 116,名字尾标随气泡上移 17px 117,看他国目标框 + 名字色 radio 接线 118,子标签随名字尾标纵向位移 119,掉落物名条屏幕空间去重叠 120,子标签逐像素水平排版 121,目标框 LCONTROL 悄悄话捷径 122,头顶子标签 / 掉落名条文本宽换真实字体度量 GetTextSize 123,掉落名条去重叠框单行文本高换 GetTextSize 的 h 124,带 owner 的掉落名条去重叠框换 SetItemTextTailOwner 1:1 125);名字与气泡合成单尾标链表未做(seam ⑨ 遗留,名字随 chat 气泡下移 17px 已由 117 收口、子标签随名字纵向位移已由 119 收口、ArrangeTextTail item 尾标去重叠已由 120 收口、子标签逐像素水平排版已由 121 收口、文本宽换真实字体度量 GetTextSize 已由 123 收口、单行文本高换 GetTextSize 的 h 已由 124 收口 —— seam ⑦ 剩余:poc 无公会徽记 mark 实体、三子标签竖排叠 x 偏移 vs 参考同一屏幕行);掉落名条框宽 / 单行文本高已分别由增量 123 / 124 换真实字体度量 GetTextSize 的 w / h 1:1、带 owner 的去重叠框已由增量 125 按 SetItemTextTailOwnerPythonTextTail.cpp:706-737)的「owner 独立度量 + yEnd += yOwnerSize + 4」换 1:1seam ⑩ 剩余:owner 未真正独立渲染成 CENTER 黄字、strOwnership 未接 ApplicationStringTable 本地化语素、只对掉落 item 名条);伤害飘字 damagevalue dds 逐位特效 / self 双资源交替未做(seam ⑪);目标框 poc 无 building / wooden-door 实体类型(is_building 恒 false/ LCONTROL 悄悄话捷径已由增量 122 接 whispernet_play.whisper_requestedchat.start_whisper/ 无独立 whisper dialog 窗口(seam ⑫ 末尾);经验 / 金钱变化的原版日志行、notice / big-notice 排版、历史滚动和 InsultChecker 过滤未完整迁移 P1
PythonPlayer HUD / affect / target HP、SP、经验、等级、目标血条、死亡复活窗和 affect 色块条已绑定 原版状态栏布局、图标贴图、持续时间倒计时、目标板按钮 / 交互、所有 point 类型和战斗日志格式仍是简化版 P1
PythonPlayerInput* / CameraProcedure / PythonApplicationCursor WASD、点地、触屏点选 / 单指相机拖动 / 双指捏合、桌面右键拖拽与滚轮缩放、网格 AABB 点选、相机遮挡淡出和碰撞、首版 TALK / ATTACK / PICK 光标和窗口键盘焦点已有;GC_CHANGE_SPEED 会校准本地移动速度 缺 BUY / SELL / CANT_GO 等完整光标状态、真实 .sub 光标资源、相机模式与灵敏度配置持久化;macOS F1–F12 仍需实机验收 P1
EterPythonLib / PythonApplicationModule UI 运行时 80 个真实 uiscript 可解析;约 20 类控件、窗口栈、modal、拖动标题栏、全局物品 attach / drop / cancel 和通用弹窗已有 ani_imagemark、真实 grid_table / scrollbar / list 行为、.dds UI 贴图、复杂窗口的落点分支、焦点 / 锚点 / 层级和原版九宫格视觉仍未完全对齐 P1
PythonEffect / PythonFly / EffectLib .mse Particle → GPUParticles3D、首个 .dds、位置 / 形状 / 方向 / 速度、技能 / 服务器特效触发、基础飞行球和动作事件已有;.mde v1/v2 几何帧、UV、.ifl 贴图和基础 MeshElement 材质已接入;Light 组、范围曲线、多贴图 TexAni 基础帧控件、旋转类型和 MovingType 直接 / 二次 Bezier 位置曲线已接入 TexAni 随机 / 每粒子语义、完整 emitter advanced / cycle、完整时间轴、Direct3D 材质 stage、骨骼挂点、真实弹道 / 命中事件和声音仍缺 P1
PythonMiniMap / PythonBackground 圆形小地图、实体 / NPC / 任务点、观战者、领地边框、队员 / 队长标记、鼠标滚轮缩放;Atlas 已有区块底图、拖动、坐标查询、地图名称和玩家 / 队员标记;地形 / 建筑 / 水体 / 天气和昼夜首版已有 缺原 minimap / Atlas 贴图细节、世界箭头、地图驱动天气配置和原版窗口视觉 P1
PythonSystem / MovieMan / PythonApplicationWebPage / Discord Godot macOS / iOS / Android 构建脚本、基础窗口和音频设置入口已有 config.exe 的分辨率 / 阴影 / 语言 / 鼠标配置持久化、Movie / Logo / Web 页面、补丁器约束及按发布目标取舍的 Discord RPC P2
ProcessCRC / ProcessScanner / PythonProfiler 反作弊明确列为不做 若服务端未来强制校验,只补最小协议上报;性能采样、崩溃上报和诊断工具不属于当前 1:1 交付范围

协议覆盖审计

40250/ClientVS22/source/UserInterface/Packet.h 是一个按 “To Server / From Server” 分段的全局 enum,不存在 namespace CG / namespace GC。本文此前的“68 + 109 = 177”以及“当前 0 个 header” 是把旧版 wire.hm2dev)与 Classic 实现混在一起后的历史统计,不能继续作为当前覆盖结论。

当前协议审计必须按运行时变体分开:

  • 当前默认 Classic:以 extension/src/net/classic/wire_classic.hclassic_parser.cppGameClient 分派和 40250/Server/.../packet.h 逐包核对。Classic 的核心编号与参考端一致, 例如 CG_CHARACTER_CREATE=4CG_MYSHOP=55CG_WARP=65CG_FISHING=82CG_DUNGEON=110GC 侧 GC_PLAYER_CREATE_SUCCESS=8GC_FISHING=89GC_LAND_LIST=130
  • m2dev:只有显式使用 MT_PROTOCOL=m2dev 时才核对 extension/src/net/wire.h;其中的 0x02010x08020x0B01 等编号不是当前 Classic/40250 对照值。
  • CG_HACKCG_STATE_CHECKERGC_RESPOND_CHANNELSTATUS 等条件能力应单独列为“已排除 / 待 服务器变体验证”,不能从一个总数中静默扣除。
  • “header 已声明”只代表 wire / size 层存在,不代表 parser、实体状态、UI 消费或真实服务器流程 已完成;本文以这些层级分别记录状态,不再用单一缺失数量表示全部协议覆盖。

账号 / 命令通道

header 说明
CG_TEXT 64 (0x40) 40250 Server 的管理文本包,当前 Classic 已有 1 字节固定前缀尺寸表;后续文本由服务端按换行特殊解析。它不在 ClientVS22 客户端 Packet.h 的普通 To Server 命名项中,因此不计入 ClientVS22 客户端 header 覆盖数。Cube 继续使用 CG_CHAT / CHAT_TYPE_COMMAND,不伪造管理命令入口。

已接线但仍需验收

  • CG_CHARACTER_POSITION(28 / 0x1C) / CG_SYNC_POSITION(8 / 0x08)wire、发送接口和 loopback 字节测试已覆盖;真服单会话已分别发送 5B / 16B,均保持在线且无 unknown;仍缺原版 自动触发语义、姿态动作链和服务端位置纠偏回包验收。
  • CG_SCRIPT_SELECT_ITEM(114 / 0x72)wire、发送接口和 loopback 字节测试已覆盖;真服单会话 已发送 8B 且无 unknown;基础 [INPUT] / [SELECT_ITEM] 已接入。Classic 没有独立的 CG_QUEST_CANCEL headerEscape 取消通过 CG_SCRIPT_ANSWER=29 发送 sentinel 255, 当前实现和测试均覆盖该路径;选择窗的背包魔石网格、grade 过滤和选择回包已由 select_item_ui_test.gd 覆盖;仍缺原版输入校验、CJK IME 和有脚本时的真实任务流程验收。
  • CG_PARTY_USE_SKILL(76 / 0x4C)wire、发送接口和 loopback 字节测试已覆盖;真服单会话已发送 9B 且保持在线 / 无 unknown,但无队伍技能业务回包;仍缺冷却 / 权限、UI 入口和有效队伍真服 验收。
  • GC_FLY_TARGETING(71 / 0x47) / GC_ADD_FLY_TARGETING(69 / 0x45)wire、EntityStore 状态、 M2Client.fly_targeting 信号和 NetWorld 目标队列消费已覆盖;本轮发现并修复 GameClient 未分派这两个 GC header 的 bug;真服本轮未主动推送,仍缺真实弹道模型、命中时机 和服务端广播验收。
  • GC_CHANGE_SKILL_GROUP(112 / 0x70)wire、EntityStore 状态、dirty 失效和 M2Client.skill_group_changed 已覆盖;本轮发现并修复 GameClient 未分派该 header 的 bug; 仍缺技能组选择 UI、服务端权限和真服推送验收。
  • CG_CHANGE_NAME(106 / 0x6A) / GC_CHANGE_NAME(107 / 0x6B)wire、角色槽位更新、 M2Client.change_name()char_name_changed 已覆盖;选人页按 change_name 标志接入 原版“点击开始先改名”流程,并对重名 / 非法名错误码给出状态文案;仍缺真服验收。
  • GC_REQUEST_MAKE_GUILD(82 / 0x52):无负载 wire 解析和 guild_make_requested 信号已覆盖;公会窗已 完成创建窗口、12 字上限、空名 / 控制字符校验和 CG_ANSWER_MAKE_GUILD 应答;仍缺服务端敏感词 / 重名错误回包、实际建会状态和真服验收。
  • CG_GUILD_SYMBOL_CRC(113 / 0x71) / GC_GUILD_SYMBOL_DATA(133 / 0x85)16B / 8B wire、MarkClient CRC 请求、 变长数据接收和 M2Client.guild_symbol_ready 已覆盖;仍缺本地符号缓存失效策略、UI 预览和 真服验收。
  • CG_FISHING(82 / 0x52) / GC_FISHING(89 / 0x59)5B / 10B wire、旋转方向换算、 EntityStore 事件、M2Client.fishing_event、远端 / 本地主角动作分发和 ChatUI 结果日志已 覆盖;真服单会话已发送 5B,收到“Please choose a Fishing Pole.”业务拒绝且无断连 / unknown, 证明包布局和错误路径可达;仍缺持竿后的完整钓鱼 UI、鱼获背包落地和成功流程验收。
  • CG_DUNGEON(110 / 0x6E) / GC_DUNGEON(110 / 0x6E)4B / 5B wire、目的地坐标变长 body、 EntityStore 事件和 M2Client.dungeon_event 已覆盖;仍缺副本计时 / 入口 UI、地图生命周期 清理和真服验收。
  • GC_LAND_LIST(130 / 0x82)4B 固定头 + 24B LandPacketElement 变长解析、领地状态、 M2Client.get_land_areas() / land_areas_changed 和小地图领地矩形边框已覆盖;真服登录洪流 已收到并解析 436B;仍缺领地贴图 / 建筑和有实际领地变更时的推送验收。
  • GC_OBSERVER_ADD/REMOVE/MOVE(9698 / 0x600x62)12B / 8B wire、持久观战者状态、 M2Client.get_observers() / observer_event 和小地图紫色菱形标记已覆盖;仍缺真服验收。
  • CG_WARP(65 / 0x41)4B wire、GameClient.send_warp()M2Client.request_warp() 和回环 请求 / GC_WARP 回传测试已覆盖;仍缺传送确认 UI、冷却 / 失败提示和真服验收。
  • GC_DIG_MOTION(134 / 0x86)13B wire、EntityStore 事件、M2Client.dig_motionNetWorld 朝向 / 动画元数据处理已覆盖;仍缺真实挖矿动作资源、音效 / 特效和真服验收。
  • GC_ITEM_DROP(23 / 0x17)ClientVS22 仍保留声明,但客户端没有注册 / 消费;实际掉落流程使用 GC_ITEM_GROUND_ADD(26 / 0x1A)40250 Server 的掉落发送路径也使用后者。当前可以不实现 GC_ITEM_DROP,但应标记为“保留声明 / 未消费”,不能宣称两个包已经逐字节等价。
  • GC_LOGIN_KEY(118 / 0x76):游戏阶段登录 key 包;net_e2e 真机 auth→PHASE_GAME 全程已打通, 但仍需将 auth 成功、游戏连接和登录 key 的实际变体在真服矩阵中单独记录,不能用旧的 0x0107 / GC_AUTH_SUCCESS=0x0108 表述替代。

“header 已声明”也不能直接视为完成:还需检查封包尺寸、变长 body、所有 sub-header、 EntityStore 状态、GDScript 信号、UI 消费和真服务器端到端行为。

建议迁移顺序

M1:服务器语义正确

  1. 把远程 SHOOT 时机、范围目标与技能类型冷却接入现有技能协议;
  2. 快捷栏的 INVENTORY / SKILL / EMOTION 类型映射、数字键 / Ctrl 表情 / F1–F4 槽位语义、 物品拖放与特殊冷却规则;
  3. 细化 24 wear / 腰带 / 装备查看的原版窗口表现,并完成完整物品操作;
  4. 移动同步、PVP / 决斗和 Actor 战斗状态机;
  5. 用 loopback + 真服抓包为每类协议建立双向验收。

M2:主要玩法完整

  1. 任务完整标签 / 输入 / 选道具 / 指引;
  2. 商店 START_EX、仓库密码、商城、私人商店、Cube(拍卖行 40250/ClientVS22 无,除非另起协议);
  3. 组队角色、情侣完整交互、公会日志 / 积分板、领地与公会建筑;公会完整页面已随增量 129 恢复;
  4. 钓鱼、副本、坐骑、观战者和 Atlas。

M3UI 与表现 1:1

  1. 以原 interfacemodule.py 为窗口清单,逐窗口迁移行为;
  2. 完成 tooltip、拖放、光标、IME、快捷键和系统设置;
  3. 完成 EffectLib、SpeedTree、材质、地图表现和声音事件;
  4. 固定原客户端参考机位、角色、地图、时间和 UI 状态,建立截图 / 录屏签核。

M4:发布闭环

  1. 所有资产调用方统一使用 AssetSource,验证纯 pack 运行;
  2. 覆盖目标平台构建、前后台、输入和性能;
  3. 按服务器要求决定 Discord / Web / Movie 等桌面能力是否迁移(反作弊 ProcessCRC / ProcessScanner 已标注 不做,除非服务端强制校验)。

1:1 完成判据

一个功能只有同时满足以下条件才标记为完成。当前 40250 阶段对已明确暂缓的真服、公会和龙魂项不作完成承诺:

  1. 原客户端涉及的 CG / GC 包和 sub-header 全部覆盖,并有尺寸断言;
  2. 数据状态能正确恢复、重连、切图和清理;
  3. UI 操作、错误提示、拖放、快捷键和窗口互斥与原客户端一致;
  4. 模型、动画、特效、声音和名字 / tooltip 表现一致;
  5. loopback、真服务器端到端、受控截图或交互录屏至少有一种自动门禁和一种人工签核。

在达到以上判据前,应继续使用“首版”“部分完成”描述,避免把“能解析布局”“有一个按钮”或 “能播放动作”写成 1:1 完成。


ClientVS22 实现细节审计(2026-09-02

审计口径

本节以当前 metin2-client 源码和 40250/ClientVS22 源码做静态对照,重点检查 UserInterfaceGameLibEffectLibMilesLibEterLibEterGrnLibSpeedTreeLibPRTerrainLib 中承担实际运行时语义的模块。 它不是简单的功能勾选表,而是把问题分成四类:

  • 明确缺陷:当前代码存在可定位的错误或错误状态转移;
  • 未实现:当前只有接口、占位或协议接线,没有原客户端对应行为;
  • 近似实现:能够运行,但和 ClientVS22 的数据、时序或表现语义不同;
  • 未验证:代码路径存在,但还没有真实 40250 服务器、多账号或受控画面证据。

本次指定的 40250/ClientVS22/source/ 树中没有 game.pyuicharacter.pyuiparty.py 等 Python 脚本;文档中出现的 assets/root/*.py 均属于本工程迁移资产 / 历史脚本依据,不应单独 当作 ClientVS22 源码证据。凡只能由这些脚本确认、而在 ClientVS22/source/ 没有对应 C++ 实现或常量的内容,统一视为“迁移资产依据 / 待验证”,不宣称已完成 1:1 源码对照。

参考客户端的核心差异在于:它不是只有网络收发,而是由 CPythonNetworkStreamNetworkActorManagerCPythonPlayerActorInstanceCPythonEventManagerPythonIMEMapOutdoorEffectLibMilesLib 共同维护 阶段、Actor、动作、地图、脚本、输入、特效和声音状态。

1. P0:连接、协议和生命周期

  1. Classic DirectEnter 已接入,但仍未完成真实双连接验收。 登录入口仍是 Auth → Game; 角色列表保存每槽 lAddr/wPort 后,M2Client.enter_game(index) 会断开第一条游戏连接, 按角色槽地址重连,并在 DirectEnter 的 SELECT 阶段发送 CG_CHARACTER_SELECTMT_CLASSIC_ENTER_DELAY 和真实两次 TCP 连接验收仍待完成。见 classic_session.cppm2_client.cpp

  2. 游戏中断线的回登录链路已接入,真实 TCP 断线仍待验收。 已建立游戏连接后的 Classic 失败发 disconnectedAppFlow 无条件回 LOGIN;认证 / 游戏连接建立前的失败仍发 login_failedReconnectUI 已从 AppFlow 主流程移除,但独立文件仍保留作旧测试 / 可选增强。 见 m2_client.cppapp_flow.gd

  3. 跨服或换图的场景重建已完成首版,但还没有真实服务器验收。 Classic GC_WARP 已按 lAddr:wPort 重连;world_reset 会清理旧实体、掉落物、PVP / 决斗、目标和交互窗, GameScene 会重建地图、HUD 小地图、Atlas、环境和玩家位置。仍缺真实换图服务器矩阵、 地图 BGM 自动选择、飞行物 / 特效的完整清理和加载阶段逐项截图验收。

  4. 选人和进入游戏吞掉底层发送失败(已修复第一批)。 select_char() / DirectEnter 选人 / enter_game() 现在透传 send_fixed() 结果,CG_ENTERGAME 成功后执行 flush GDScript 入口优先调用 enter_game(index)。失败提示和真实发送失败注入仍待补充。 见 classic_session.cpp

  5. 客户端版本包是硬编码的。 当前固定发送 metin2.bin 和旧 timestamp,未读取实际 构建版本、文件时间或资源包版本。服务端开启版本校验时,更新客户端后仍可能被当成旧客户端。 见 classic_session.cpp:335

  6. Passpod、HackShield、XTrap、StateChecker 等兼容路径没有实现。 当前选择了不伪造 这些回应;如果目标 40250 服务端启用了其中任意一项,登录或进场可能被断开。应把“不支持” 变成明确的服务器兼容矩阵,而不是让连接表现为随机失败。

  7. 未知包的 Loading 重同步问题已修复第一批。 当前不再扫描后续字节寻找 GC_CHARACTER_ADD 边界;未知 header 记录最近包、清空缓冲并断开,保留 header == 0 填充跳过。真实加密流验收仍待完成。见 classic_stream.cpp

  8. 已知但没有业务处理的包静默丢弃问题已修复第一批。 ClassicParser 现在记录阶段 / header 并返回失败,连接层清空缓冲并断开;可选集成包不会再静默成功。按阶段拆分处理表和 全量业务覆盖仍待完成。见 classic_parser.cpp

  9. 封包覆盖仍停留在结构和尺寸层。 还需要逐包核对 sub-header、动态 body、实体状态、 UI 消费、发送后的锁定状态、失败回滚和重连恢复,不能仅以 Packet.h 中存在 header 作为完成。

  10. 文本编码和固定长度字段没有统一策略。 当前大量使用 UTF-8 Stringstd::stringmemcpy,没有完整复现 ClientVS22 的本地编码转换。CJK 名称、聊天、私聊、公会名可能 按 UTF-8 字节截断或超过服务端字段边界;角色名校验还存在 < 24<= 24 两种规则。

  11. 登录错误、超时、会话失效和服务器列表状态没有完整映射。 当前更多是把底层字符串 直接显示出来,没有复现 ClientVS22 的错误码、阶段专用文案、服务器刷新和超时处理。

2. P0/P1:实体、世界和状态一致性

  1. 未知 VID 的普通 Actor 更新包会直接丢失。 mut_movemut_set_positionmut_change_speedmut_deadmut_stun、物品更新等路径遇到未知实体直接返回,没有 pending state 或重放机制。 角色添加顺序变化时,位置、HP、死亡、动作和装备信息会丢失;GC_SHOP_SIGN 当前有独立的 招牌 fallback,不能与普通 Actor 更新混为一谈。见 entity_store.cpp:123

  2. GC_CHAR_ADDITIONAL_INFO 早于 Spawn 时状态不会被待合并缓存。 mut_char_info() 可以 创建 / 更新 Entity,但新建时不发送 Spawn change;如果后续 Spawn 正常到达,NetWorld 仍会 创建节点,若 Spawn 丢失或顺序异常则实体会长期只有数据没有表现层节点。见 entity_store.cpp:458net_world.gd:128

  3. VID 重用时旧状态可能污染新实体。 mut_spawn() 没有完整重置 dead、stunned、mount、 guild、pk、alignment、parts、shop sign、affect 和上一动作状态,只覆盖了基础出生字段。 见 entity_store.cpp:75

  4. 点数组 API 对部分更新可能残留旧值。 mut_set_points() 只复制本次传入数量,不清理 上一次更长数组的尾部;不过当前固定长度 Classic 包路径传入 POINT_MAX_NUM,该路径不会 产生部分更新。后续 UI 或其他部分更新调用者仍可能读到旧状态。

  5. 实体数据字段不完整。 empire、称号、公会名、公会徽章、名字颜色、alignment 颜色、 target highlight、attackable、weapon mode、sex/job/race motion mode、affect 图标和 剩余时间没有形成完整统一的实体状态。mut_char_info() 当前还明确忽略 empire。 见 entity_store.cpp:477

  6. 一次性事件没有可靠的生命周期缓存。 聊天、脚本、特效、飞行物、任务提示等事件主要 通过 drain queue 即时消费;如果 UI 或 GameScene 尚未完成装配,事件可能被消费后无法补发。

  7. Classic Warp 的无条件重连和换图清理已落地首版,仍缺真实 E2E。 ClientVS22 的 RecvWarpPacket() 对所有 GC_WARP 都按 lAddr:wPort 重连;当前 Classic parser 透传 lX/lYM2Client 在重连前发 world_reset,并保留 login key / 已选角色槽。GameScene 已重建地图和局部 UI;仍需真实服务确认第二次连接、重连后的 GC_MAIN_CHARACTER 定位、 地图 BGM / 任务 / 飞行物 / EffectLib 清理。m2dev 后端仍保留自己的 addr==0 原地移动语义, 不应与 Classic 对照混用。

3. P1:移动、战斗和技能

  1. 本地移动改为 Src/Dst 模型(增量 76)。 当前控制器直接修改 X/Z,只做地形阻挡检查 —— player_controller.gd 去掉 _move_target / _has_target,改 _src_pos / _dst_pos / _is_going / _dst_rotwalk_to()_goto() 对齐 CInstanceBase::NEW_Goto() InstanceBaseMovement.cpp:251):太近(< MOVABLE_GROUND_DISTANCE_M 1.0 m)不动、三门 _can_translate()not frozen and not locked and not moving_skill__IsSyncing / isLock / IsUsingMovingSkill)未过时存 _reserved_ground + RESERVED_GROUND_DELAY 0.1 s 延迟重试(≈ __ReserveClickGround / NEW_IsEmptyReservedDelayTime),moving_skill 只转向。 WASD 分支置 _is_going = false(≈ NEW_MoveToDirection)。见 player_controller.gd + project/player_move_test.gd增量 93 补完 Actor-Actor 碰撞 + 坐骑转向速度 player_controller._actor_blocked(next_pos) 近似 CActorInstance::TestActorCollision ActorInstanceCollisionDetection.cpp+ CInstanceBase::CheckAdvancing InstanceBaseBattle.cpp:469)—— 8 m 距离门(≈ LengthSq > 800² 直接不检)+ body 球 重叠(ACTOR_BODY_RADIUS_M = 0.55 相加)+「趋近」判定(d_next < combined && d_next <= d_now, 远离放行,能从重叠里走出来),候选 = pickablesdead meta 跳过(≈ rVictim.IsDead()); _process 平移逐轴门控(单 Actor 逐轴放行 = 滑移 ≈ AdjustDynamicCollisionMovement,两轴都 挡 = 停在上一帧位 ≈ uCollisionCount==2BlockMovement() / __InitializeMovement); skip_actor_collision(≈ CActorInstance::CanSkipCollision() / __EnableSkipCollision)由 net_playsetup() / _on_observer_mode_observer_mode 置位。坐骑转向速度:新增 rotation_speed_deg(默认 ROT_SPEED_DEFAULT_DEG = 1200 = c_fDefaultRotationSpeed+ _turn_toward(target_yaw, dt) 逐帧限幅收敛,替换原两处 player.rotation.y = atan2(...) 瞬转; net_play._refresh_motion_mode()mounted 时切到 ROT_SPEED_HORSE_DEG = 300 = c_fDefaultHorseRotationSpeed,对齐 CInstanceBase::MountHorseSetRotationSpeed InstanceBase.cpp:954),下马恢复 1200MODE_HORSE* 动作模式已由增量 84 覆盖。 player_move_test.gd 5 组 → 8 组。仍未接Actor 之间碰撞(TestActorCollision / BlockMovement 的 Area3D 近似,需远端 Actor 碰撞体)、坐骑 MODE_HORSE* 移速与动作模式 (依赖 §6.4 + §3.2)、真实 body sphere 半径需 race 碰撞数据(.col / CollisionData.h)、 AdjustDynamicCollisionMovement 精确滑移向量 + 门 / Actor 复合的 uCollisionCount==2 (现用逐轴近似)、MOVABLE_GROUND_DISTANCE 参考默认值校准、路径寻路 / 坡度处理、网络驱动 移动的 __EnableSkipCollision()(§3.2 网络状态包驱动的本地预测玩家尚未接)。

  2. 远端同步改为命令队列 + 硬阈值(增量 77 / 78 / 79,§3.2 完成)。 当前按固定时间或距离发送移动, 回包后按距离阈值 snap/lerp —— EntityStore 的远端 Actor 位置改为 std::deque<StateCmd> 命令队列驱动,1:1 复刻 CInstanceBase::StateProcessInstanceBase.cpp:1278):mut_move() 入队(≈ PushTCPState)后 drain_state_queue()m_server_frame_msset_server_frame_ms(), 为 0 = 帧钟未对齐 → 立即释放,退回增量 76 时序)+ __CanProcessNetworkStatePacket dead / knock_down 挂队)逐条释放;apply_state_cmd()FUNC_WAIT 1.0 / FUNC_COMBO/ATTACK/MOB_SKILL 50.0 硬阈值 + m_kMovAfterFunc 到达后动作 + skip_collision (≈ __EnableSkipCollision)。SNetworkActorData::UpdatePosition 式线性插值保留但只喂 §2.1 可见性。增量 78part Bnet_play.gd 上行按 PythonPlayerEventHandler.cpp 拆成 6 个回调(统一出口 _send_state)—— OnMove 立即发 FUNC_MOVE 并重置 moving/waiting 两个节流窗、OnMoving 300 ms 节流、OnWaiting_tick_on_waiting100 ms 节流且位移 ≥ 1 cm、 OnStop 收尾 FUNC_WAIT、OnAttack 额外 FUNC_COMBO、新公共入口 on_use_skill()FUNC_SKILL|motionnet_world.gd 去掉自有 follow_lerp_process 直接 n.position = e.pos(位置插值全交 C++ tick(),只朝向仍 GDScript 平滑)。 增量 79part C,§3.2 收官)ClassicStream::server_frame_ms()(≈ ELTimer_GetServerFrameMSec())由 HDR_HANDSHAKE 存的 `m_server_time_base = hs.time

    • lDelta/m_client_time_base = now_ms() 算出(握手前返回 0);ClassicSession::process()m_world.tick()set_server_frame_ms(m_stream.server_frame_ms()),classic 后端 命令队列遂按服务器时间放行(m2dev 无 lDelta对应物,恒 0 = 即时释放,无回归)。drain_state_queue加防呆阀STATE_QUEUE_MAX_WAIT_MS = 1000——chk_time领先帧钟超过 1 s 立即放行以免远端 Actor 冻结(参考端无此上限,有意偏离)。 仍未接:完整 sequence / authoritative tick**抖动缓冲不做、预测回滚不做**(§3.2 / §6 明确排除);真实服务器GC_MOVE dwTime 时基核验、__IsEnableTCPProcess表情锁、GC_SYNC_POSITION` 是否清队列随对应 §3.x 处理。见 §3.1 / §3.2 修复批次。
  3. 普攻节奏部分对齐动作数据(增量 74)。 范围、攻击间隔、硬直和连击窗口均为固定值或简单按攻速缩放 —— 点击 / 攻击距离改为 CLICK_DIST_DEFAULT_CM=150 / NPC_CM=500 / RESOURCE_CM=100 (对齐 InstanceBaseBattle.cpp:294);普攻节奏改为读 .msa ComboInputData.DirectInputTime metin2_anim.get_motion_data().next_combo,缺组退化 duration*0.9 / headless 兜底 DEFAULT_ATTACK_PERIOD)再除以 bAttackSpeed/100。弓箭 __GetBowRange() 已在增量 85 接入 _clickable_distance()_is_bow_mode() 改用 _bow_range_cm() = 2500-100 + _bow_distance_bonus,NPC/采集物分支覆盖之,逐分支对齐 NEW_IsClickableDistanceDestInstance

    • CInstanceBase::__GetBowRange InstanceBase.cpp:694)。仍未接: AttackingData / HitPosition 驱动的命中帧 / 硬直 / 无敌帧的真实动态圆柱扫掠(§3.5, 当前 reach + 正面弧近似)、mount 与武器规则、服务端合法性校准。见 net_play.gd
  4. 自动攻击目标预留逻辑已移植(增量 75)。 当前主要是检测距离后发攻击包 —— net_play.gd 新增 enum ReservedMode + _update_auto_attack()(照 PythonPlayer.cpp:207,检查目标存在 / 死亡 / 骑乘 / charge skill / 睡眠后 _reserve_click_actor()+ _reserve_process_click_actor() 严格 10 步(照 PythonPlayerInput.cpp:877,出距离 pc.walk_to 靠近 / NPC 走 click_npc / IN_SAFE / DEST_IN_SAFE / EMPTY_ARROWsignal cannot_act / 第 10 步按 §3.3 节奏 _do_attack_swing 后清预约)。仍未接:CLICK_ITEM / CLICK_POSITION / USE_SKILL 分派、安全区 / 弓箭 / 骑乘等级 / dash 等判定位的服务端来源、OnCannot* 文案接 HUD。 参考:ClientVS22/PythonPlayer.cpp:207

  5. 攻击动作、命中时机和网络包没有严格绑定 —— §3.5 修改 1-5 已全部落地(增量 80/81/82/83)。 当前 combo 仍只在 0/1/2 之间循环(net_play.gd _do_attack_swing(_combo + 1) % 3 —— 增量 81 已替换:_do_attack_swing / _run_next_combo / _combo_process / _on_end_combo / _clear_combo 逐行对齐 CActorInstance::InputComboAttackCommand / __RunNextCombo / ComboProcess / __OnEndCombo / __ClearCombo ActorInstanceBattle.cpp:157/213/256/297/308),按 .msa ComboInputData 三时间 pre_input_time / direct_input_time / input_limit_time)驱动 _combo_index / _is_pre_input / _is_next_pre_input,段号取自增量 80 的段表 (新静态绑定 Metin2AnimPlayer.parse_combo_tables(),按 race & 3 × combo_motion_mode<<16|type)。 hit frame、范围、角度、武器轨迹、击退和受击动作仍未由真实 motion data 驱动。 —— 增量 83 已接命中帧(见下);武器轨迹拖尾仍留桩。 RaceData 的 combo 段表需从 .msm 解析 —— 更正PC 连击段表不在 .msm.msm 只有 BaseModelFileName + HairData),而是 assets/root/playersettingmodule.pychrmgr.ReserveComboAttackNew / RegisterComboAttackNew 调用建在 CRaceDataPythonCharacterManagerModule.cppCRaceData::ReserveComboAttack / RegisterComboAttack)。增量 80 新增 formats/combo_table.{h,cpp}fmt::PlayerComboTables 把那批调用解析为按职业分块的 map<MAKE_COMBO_KEY(motionMode, comboType), vector<段号>>,语义 1:1 对齐 CRaceData Reserve insert 不覆盖、Register 越界 / 未登记跳过),formats_combo_table_test 合成 + 真实资源双覆盖。增量 82(part C,修改 1)导出 .msa HitDataContainer 多命中窗: formats/textscript.cppList Name { ... } 解析(此前 List HitPosition 会破坏后续 Grouppalbang.msaMotionEventData 因此丢失),formats/msa.{h,cpp}AttackingData 重写为 std::vector<Msa::HitWindow>HitWindow{start_time, end_time, bone_name, weapon_length, vector<HitSample{time, last_pos[3], pos[3]}>}),同时吃老式内联(无 HitDataCount)与新式 AttackType + MotionType + HitDataCount N + Group HitData0i 两种写法,新增 motion_type / hit_limit_countattack_start_time / attack_end_time 保留为窗 0 别名;get_motion_data() 增加 hit_windows / motion_type / hit_limit_countformats_msa_test 补合成 + M2_ASSETS 真机断言(含真实双命中窗 assassin/dualhand_sword/combo_01.msa:右手窗 Bip01 R Hand + 左手窗 Bip01 L Hand)。增量 83(修改 4,§3.5 收官)在 net_play.gd 落地命中帧: _begin_hit_windows() 挥击起手缓存 get_motion_data().hit_windows / motion_type / hit_limit_count / invisible_time_attack_process(dt) 每帧按动作时间窗 [t-dt, t] 与某个 hit_window [start,end] 相交时,遍历 client.get_entities() 里可攻击 / 未死 / 双方 非安全区的实体,net_world.node_for(vid) 取世界位做几何判定(对齐 InstanceBaseBattle.cpp:413 AttackProcess + ActorInstanceCollisionDetection.cpp:333 __NormalAttackProcess):_hit_deduped() 复刻 m_HitDataMapMOTION_TYPE_COMBO 同窗同目标 一次、其余按 invisible_time 冷却),_register_hit() 复刻每窗命中上限(NORMAL/COMBO 16、 SKILL hit_limit_count),命中 → client.attack(_swing_skill, vid)OnHitSendAttackPacket 普攻 _swing_skill=0),被击退目标帧末 _flush_victim_list()client.sync_positions()(≤16 对齐 PythonPlayerEventHandler.cpp:194 FlushVictimList)。_emit_swing() 因此改为只有拿不到 命中窗数据(headless / 缺资源)才在挥击瞬间立即发 CG_ATTACK。~~几何判定为近似: reach = max(WeaponLength, 窗内采样最大水平偏移) + 正面 ±60° 弧(真实动态圆柱扫掠需骨骼矩阵

    • defending sphere,随 §3.1 actor 碰撞一起留桩)。~~ 增量 130 已换成 HitCollision.detect_z_cylinder 刀尖 Z 圆柱 vs .msm 防御球,并补全 __ProcessDataAttackSuccess:硬直、击退、命中特效、受击动作链、抖动。combat_fx_test.gdFakeNetWorld + 6 段断言。 剩余(增量 84:武器种类 → combo_motion_mode 映射(当前恒 MOTION_MODE_GENERAL)。 —— 增量 84 已落地net_play.gd 补全 MOTION_MODE_*CRaceMotionData::EMode 全 17 项)
    • ITEM_TYPE_WEAPON/ROD/PICK + WEAPON_SUB_* 常量,新增静态 motion_mode_for(item_type, weapon_sub, mounted, poly, dress) 逐分支复刻 CInstanceBase::RefreshStateSetMotionModeInstanceBase.cpp:2851,顺序 Poly→GENERAL / 婚纱→WEDDING_DRESS / 镐→骑乘 HORSE 否则 GENERAL / 钓竿→骑乘 HORSE 否则 FISHING / 骑乘 switch(subType)HORSE_{ONEHAND,TWOHAND,DUALHAND}_SWORD·HORSE_{FAN,BELL,BOW}·默认 HORSE / else switch(subType){ONEHAND,TWOHAND,DUALHAND}_SWORD·BOW·FAN·BELL· ARROW/默认 GENERAL),_equipped_weapon_vnum()(主角 parts[WEAPON] 非 0 优先,否则 get_equipment()[WEAR_WEAPON].vnum+ _refresh_motion_mode()(读 mount_vnum / polymorph / wedding_dress + proto.item(vnum).{type,sub_type}set_combo_motion_mode(...),换模式 内部 _clear_combo()),由 _on_entity_info() 末尾、inventory_changedwindow==2 EQUIPMENT)、 mount_changed(主角)三处触发。combat_fx_test.gdFakeProto + 15 段 motion_mode_for 断言 + 2 段端到端(换匕首 → DUALHAND_SWORD_combo_index 归零;再上马 → HORSE_DUALHAND_SWORD)。 仍留桩:弓箭 __GetBowRange()(§3.5 命中 / 点击距离,增量 85+) —— 增量 85 已落地 _clickable_distance()_is_bow_mode()combo_motion_mode ∈ {MODE_BOW,MODE_HORSE_BOW} 对齐 CActorInstance::IsBowMode ActorInstance.cpp:258)改用 _bow_range_cm() = BOW_RANGE_BASE_CM2500-100+ _bow_distance_bonus_on_points()points["bow_distance"] = POINT_BOW_DISTANCE 34),NPC 500 / 采集物 100 分支覆盖之, 逐分支对齐 NEW_IsClickableDistanceDestInstanceInstanceBaseBattle.cpp:294+ CInstanceBase::__GetBowRangeInstanceBase.cpp:694);wire.hPOINT_BOW_DISTANCE=34 + M2Client::get_points() 透出,combat_fx_test.gd §3.5 段加 8 段断言。 wedding_dress 实体标志本工程暂无(读 e.wedding_dress,缺则 false)。 OnAttackCG_CHARACTER_MOVE{FUNC_COMBO} 已在增量 78 落地。
  6. 弓箭发射仍有固定超时兜底。 当前优先消费 .msa 的 FLY 事件,但 pending skill 仍由约 1.5 秒定时器兜底;不同技能、攻速、动作长度和网络延迟下可能偏离真实事件,连续释放还可能 覆盖 pending skill。 —— 增量 86 已落地:删掉 game_scene.gd 的单槽 _pending_shoot_skill + get_tree().create_timer(1.5),换成 _pending_shots: Array[int] FIFO(上限 8,满丢最旧)。每次弓挥击 / 弓技能起手 _queue_shot(uSkill) 压一个,.msa MOTION_EVENT_TYPE_FLYpop_front()client.shoot()pop 即自清 = _AutoClear 连射多箭在途各自一发;对齐 CActorInstance::ProcessMotionEventFly ActorInstanceMotionEvent.cpp:290)。OnSetFlyTargetActorInstanceMotion.cpp:372)改在 net_play._emit_swing_is_bow_mode()_send_fly_target()(目标节点 → MapCoord.to_server_cm()client.add_fly_targeting(vid, x, y)OnGetFlyTargetPosition = 目标包围球中心,平面分量与脚点一致,ActorInstanceFly.cpp:4+ bow_shot_fired.emit仍待:飞行物实体(CFlyingManager / FlyingInstance)、__CanShot 弓箭合法性 —— 增量 92 已落地:新增 project/fly_object.gdFlyData / FlyInstance / FlyManager), 对齐 GameLib/FlyingObjectManager.{h,cpp}CreateFlyingInstanceFlyTargetUpdate()while 迭代 + eraseCreateIndexedFlyNORMAL / FIRE_CRACKER / AUTO_FIRE 分支 常量)+ FlyingInstance.{h,cpp}Create__SetDataPointer__SetTargetDirection Update() 逐帧 vel += accel*dtvel.z += gravity*dtremain_range -= |movement| → 线段-点 square_distance_between_linesegment_and_point < m_fBombRange² 命中 → if (m_canAttack) OnShootDamage()(本 POC 转 shoot_damage(vid) 信号)→ m_iPierceCount-- + __Bomb() 续飞、否则 __Explode() + return falsem_fRemainRange<0OnExplodingOutOfRange + __Explode(false)(不 __Bomb);位置目标同线段判定; m_bHitOnBackgroundGetTerrainHeight(x,-y) > z+ FlyingData.cpp __Initialize() 默认值(米制换算)。_seg_point_sq_dist 1:1 移植 EterLib/GrpMath.h:80net_world.setup()FlyManager 子节点,_on_fly() 把原 tween「发光球」占位换成 _fly_mgr.spawn(start_w, target_node|goal_w, false, FlyData, type) 真实弹道,新增 signal fly_exploded(world_pos, cause)net_play._can_shot(target_vid := -1) -> String 防御式重建 __CanShotgrep -rn "CanShot|__CanShot|CanShoot" 全树无命中——据 IFlyEventHandler::OnNoTarget / OnNoArrow FlyHandler.hNEW_Attack InstanceBaseBattle.cpp:355IsDead 门、 __GetBowRange InstanceBase.cpp:694 重建):非弓 → "NOT_BOW_MODE"_arrow_count == 0"EMPTY_ARROW"-1 = 未跟踪 / 无限,放行);_target_vid / _vid_reserved 均 ≤ 0 → "NO_TARGET";目标实体已知且不可攻击 / 已死 → "NO_TARGET"te.is_empty() 不拒—— headless 容差,与 _send_fly_target「有节点即可」一致);目标世界距离 * CM > _bow_range_cm()"OUT_OF_RANGE";否则 ""_emit_swing 弓分支据此加门:_can_shot() 非空 → _emit_cannot(why)(走 OnNoArrow / OnNoTarget 文案),不发 CG_FLY_TARGETING、不排 OnShoot;其余起手动作照常(seam:参考端整挥击中止)。新增 project/fly_test.gd(第 14 个 GDScript 回归)。C++ 21/21(无 C++ 改动)+ 14 GDScript 全绿。已文档化 seamGC_CREATE_FLY 在本 40250 客户端无包处理器(仅 NetStream.cpp 头串索引 70)→ 实际驱动走 POC 自有 fly_cue__CanShot 防御式重建;重力参考端作用于 m_v3Velocity.z=上、本 POC 映射到 vel.y -Y;怪物球体碰撞 / CullingMgr.ForInRange / 建筑体素 r=1 判定未移植(客户端 飞行物只做命中"时机",伤害服务端裁决);无 .fly 数据文件加载器 → 合成箭参数。仍待:真服 CG_FLY_TARGETING z 分量、.fly 数据文件解析。见 fly_object.gd / game_scene.gd _queue_shot / _on_local_motion_event

  7. 受击状态分裂。 EntityStore.stunnedNetPlay._hitstun_until 是两套互不统一的状态 —— 增量 73 已删除 _hitstun_until 本地硬直计时器,「能否行动」统一走 _can_process_network_state() / _can_attack() / _can_use_skill()(对齐 InstanceBase.cpp:1247 / InstanceBaseBattle.cpp:355 / :310IsDead || IsStun || IsPushing)。Entity.knock_down 由本地受击 .msa 播放生命周期驱动,dead / stunned 保持服务端包驱动。剩余:远端实体 knock_down 填充、真实 40250 受击 / 击退 / AFFECT_STUN 清除时序、mut_stun 目前无独立清除包。

  8. 技能缺少统一合法性检查。 当前主要检查 skill id 是否大于零,没有统一验证技能归属、等级、 master、cooldown、消耗、目标类型、目标数量、距离、角度、当前动作、PK 和坐骑限制。 target_vid 等参数还可能先被转换为无符号整数。 —— §3.8 修改 1/2/3 于增量 87/88/89 全部落地。 —— 增量 87 落地判定位数据层(§3.8 修改 2)POC 无 C++ skill proto,技能数据只在 GDScript project/ui/skill_table.gd,现按 RegisterSkillDescPythonSkill.cpp:280)把 skilldesc 第 11/12 列折成 attr_bits / need_weapon_bits 位掩码 + skill_typeCPythonSkillIsCanUseSkill / IsStandingSkill / IsHorseSkill / IsAttackSkill / IsNeedTarget / IsNeedCorpse / IsAutoSearchTarget / CanChangeDirection / IsNeedEmptyBottle / IsNeedPoisonBottle / IsChargeSkill / CanUseWeaponType / IsOnlyForGuildWar 等谓词逐条落地,skill_test.gd +34 断言。 —— 增量 88 落地三层校验(§3.8 修改 1):新建 project/player_skill.gdextends RefCounted, 纯校验器,不发包 / 不驱动动画,返回 {ok, code, target_vid}),逐行移植 PythonPlayerSkill.cpp (a)click_skill_slot():106 入口分派:越界 / 无数据 / 公会技路由 / 被动 / 切换技)、 (b)can_use_skill():439 全局:观察态 / 骑乘中且骑乘术 109 grade<1 && level<20 / 不可行动)、 (c)check_skill_usable():166 完整前置顺序:骑乘⇄马术 NOT_HORSE_SKILL / HAVE_TO_RIDE → 安全区攻击技 IN_SAFE → 被动 → 空瓶 27995 → 毒瓶 27996 → 钓鱼 REMOVE_FISHING_ROD → 未学 NOT_YET_LEARN → 武器不匹配 NOT_MATCHABLE_WEAPON → 无箭 EMPTY_ARROW → dash / 蓄力 / 冷却 WAIT_COOLTIME → HP / SP)、(d)use_skill():460:私人商店静默、 __CheckSpecialSkill 钓鱼 123 / 连击 122、切换技、施法中 ALREADY_CASTING_resolve_target() 目标矩阵:同盟 / 安全区 / 可攻击 / 尸体 → CANNOT_USE_SELF / ONLY_FOR_ALLIANCE / CANNOT_ATTACK_ENEMY_IN_SAFE_AREA / CANNOT_ATTACK / ONLY_FOR_CORPSE / NEED_TARGET)。命名常量 SKILL_MAX_NUM=255Packet.h:2041/ SKILL_TYPE_GUILD=3 / RIDING_SKILL_INDEX=109 / ITEM_EMPTY_BOTTLE=27995 / ITEM_POISON_BOTTLE=27996 / SKILL_INDEX_COMBO=122 / SKILL_INDEX_FISHING=123PythonPlayerModule.cpp:6-7); SILENT_CODES[] 13 个无文案码 + static is_silent_code()。运行期状态尽量走 client get_main_vid / get_entity / get_skills / get_target / get_inventory),其余经 net_play.skill_context() 每次注入(观察 / 骑乘 / 安全区 / 钓鱼 / 施法中 / 可行动 / dash / weapon_sub_type / arrow_count)。ui/quickbar.gd activate() 技能分支施放前跑三层, TOGGLE_OFF → 关闭切换技 + skill_activated,非静默码 → 新信号 skill_rejectedgame_scenenet_play.cannot_act.emit(code)(同一条 OnCannotUseSkill 文案通道), gate 解析出的 target_vid 覆盖点击目标。新增 project/player_skill_test.gd(读真 skilldesc.txt,覆盖 (a)(b)(c)(d) 每条分支 + 目标矩阵 + is_silent_code)。 —— 增量 89 落地边界收紧 + 短蓝短血校验(§3.8 修改 3,三层全落地)net_bounds.h 新增 SKILL_MAX_NUM=255 + skill_index(v)=0<v<255 谓词,M2Client::use_skill(int skill_id, int target_vid)(形参增量 52 起即带符号)的 skill_id 护栏从 skill_id <= 0 || !bounds::u32() 收紧成 !mtnet::bounds::skill_index(),对齐 ClickSkillSlotslot >= SKILL_MAX_NUM 门(net_bounds_test.cpp +4 断言)。skill_table.gd _load_cooltime_load_msk,加解析 .msk NeedSPFormula + _SKILL_POWER[]LocaleService_GetSkillPower INTERNATIONAL 表,Locale.cpp:126+ _skill_point() / need_sp()(≈ GetNeedSP PythonSkill.cpp:1426;无公式返回 -1,POC 约定与参考的 0 相异以免误挡)/ need_hp() (照抄 __CheckShortLife 复用 GetNeedSP 的怪癖)。player_skill.gd +SKILL_INDEX_SUMMON=131

    • 上下文 cur_sp / cur_hp-1=未知不判),_check_short_mana / _check_short_life 从桩 改为逐条移植 __CheckShortMana:320/ __CheckShortLife:354),转 NOT_ENOUGH_SP / NOT_ENOUGH_HPHP/SP 由 net_play.skill_context()get_points() 供入。player_skill_test.gd +“(c) §3.8 修改 3”段。C++ 21/21 + 13 个 GDScript 回归全绿。见 CLIENT-GAP-FIX.md §3.8 / §3.9 / C.5。
  9. 公共网络 API 的 wire 参数边界已补齐,但业务合法性仍未完成。 增量 52 已在 M2Client 所有相关窄类型转换前拒绝负数、溢出、错误 Variant、超限槽位 / 数量 / 文本,覆盖 attack、交易、仓库、物品、技能、社交、公会、Cube 和私人商店;剩余是技能目标 / 冷却、背包 规则、权限和地图坐标等业务语义,不能由通用整数边界代替。详见 CLIENT-GAP-FIX.md §3.9。

  10. 技能升级的传输方式已与参考端一致,但门控和状态流程不完整。 ClientVS22 本身也通过 /skillup <skillIndex> 发包;当前缺少参考端的可升级条件门控(技能组、等级上限、技能点、 死亡 / 变身 / 骑乘等状态)、加号按钮显示规则、失败提示和升级后的技能状态刷新。 —— 增量 90 落地加号按钮门控(§3.10)skill_ui.gd refresh() 逐行移植 uicharacter.pyRefreshSkillPlusButtonList / __RefreshSkillPlusButton / CanShowPlusButton —— 技能组门(get_skill_group() != 0,对应 __CanUseSkillNow)、 三个技能点来源分开取(skill_active / skill_support / skill_horsewire.hPOINT_SKILL=28 / POINT_SUB_SKILL=27 / POINT_HORSE_SKILL=113get_points() 透出)、 skillGrade==0、马术页 skillLevel < 20、主动页 skill_table.can_level_up_skill(id, level) = skillCanLevelUpSkill,本 40250 build 里前置技能 / 前置属性字段无人填充 → 化简为 level < GetMaxLevel() && !CANNOT_LEVEL_UPGetSkillLevelLimit 无源数据列 / 记号 → 恒 0)、 辅助页 HIDE_SUPPORT_SKILL_POINT 整页隐藏。skill_tablemax_level_of / skill_level_limit / can_level_up_skillM2Client::skill_up() 入参收紧为 mtnet::bounds::skill_index(与 use_skill 同一谓词)。升级结果由 GC_SKILL_LEVELskills_changed 驱动刷新,不本地伪造。skill_test.gd +§3.10 段。 C++ 21/21 + 13 GDScript 全绿。见 m2_client.cpp:961、 参考 PythonSkill.cpp:1833CLIENT-GAP-FIX.md §3.10 / C.5。

4. P1:物品、装备、商店和社交

  1. 24 个 wear 位置进入数据层,但没有完整视觉和规则。 costume、翅膀、饰品、双持、左手、anti-flag、遮挡、职业/性别变体、部位隐藏、卸装复位、 远程角色换装和逐部位高光仍不完整。 增量 96(§4.1 修改 1 + 修改 3proto 层此前 (§3.5)已导出 type / sub_type / anti_flags / wear_flags / limits(校验数据层齐备); ui/inventory_ui.gd._to_wire()REF/UserInterface/GameType.h:40-112 分流 wear 槽—— 基础 11 位(EWearPositions BODY..SHIELDWEAR_MAX_NUM=11)走 WINDOW_EQUIPMENT(2) + 零基 wear cellENABLE_COSTUME_SYSTEMc_Costume_Slot_Start(90+19) / Hair(90+20)ENABLE_NEW_EQUIPMENT_SYSTEMRing1(90+21) / Ring2(90+22) / Belt(90+23) 在客户端 无独立窗,继续用 WINDOW_INVENTORY(1) 全局 cell 109..113;新增常量 WEAR_COSTUME_BODY/HAIRWEAR_RING1/RING2NEW_EQUIP_WEAR_START=19WINDOW_EQUIPMENT=2 全部 1:1 自 GameType.h _add_extended_cells 由「wear 11..34 连续 24 格」收窄为「19..23 五个真实新装备位 + 16 腰带格」; inventory_test.gd 增 7 条 wire 断言(原 _to_wire(113)==[1,113] 转绿)。 增量 97(§4.1 修改 240250 客户端不做 CanEquipNow 前置门(PythonItemModule 只暴露 IsWearableFlag / GetLimit,仲裁在服务器 char_item.cpp),故按 §3.9 __CanShot 方式在 GDScript 侧防御性重建——新增 project/equip_rules.gdextends RefCounted),枚举 1:1 自 REF/GameLib/ItemData.hEItemAntiFlag classic 排列 / EItemWearableFlag / EWearPositions + GameType.h 19..23 / ELimitTypes / EItemType + EArmorSubTypes·ECostumeSubTypes)。 can_equip(item, wear, ctx) -> {ok, code, need}:目标槽解析 → anti-flag 性别(ANTI_SEX)→ 职业 ANTI_JOBrace%4)→ 帝国(ANTI_EMPIREGC_EMPIRE 1/2/3)→ slot_accepts() NOT_WEARABLE_HERE)→ limit 逐条(LIMIT_LEVEL / LIMIT_STR/DEX/INT/CON,属性 <0 跳过)。 inventory_ui.gd.use() 对常规背包格的可穿戴物品跑此门,拦下 → _set_hint() + 新 signal equip_rejected(code, need) + 不发 use_item;加公开 can_equip_item() / _equip_ctx() get_points().level + get_entities() 主角 race/empire/levelst/dx/ht/iq = -1 seam/ _equip_reject_hint() 中文文案。新增 canonical 测试 #17 equip_rules_test.gd33 断言)。 增量 98(§4.1 修改 4 + 修改 5 字段序):穿头盔覆盖发型槽、costume(parts[ARMOR]) 压过本体 armor 在增量 96/97 已由 equip_model.gd 落地;本次补齐变身 / 动物时装 / 婚纱的整件遮挡—— 新增 project/part_hiding.gdextends RefCounted 纯函数),常量 1:1 自 REF/UserInterface/InstanceBase*.cppPOLY_SHAPE=0IsPoly()SetShape(0))、 ANIMAL_WEAR_SHAPES=[100..103]__IsShapeAnimalWear:2834 → SetWeapon:2747 / SetParts 返回 false)、WEDDING_DRESS_SHAPE=201IsWearingDress)、BOUQUET_VNUMS=[50201..50204] c_iBouquets__IsChangableWeapon:901 除捧花外 false)。equip_model.gd.refresh() 刷新序改为 对齐 ChangeArmorSetArmor→SetHair→SetWeapon)= 身体→头发→武器→盾;解出身体 shape 后过 PartHiding:动物 / 怪物时装隐藏 weapon_gr2+shield_gr2+hair_gr2,婚纱非捧花隐藏武器+盾、 头发保留。net_world._apply_field_updates() 确认 set_armor→set_weapon→set_hair 字段序已 1:1 于 NetworkActorManager.cpp:473。新增 canonical 测试 #18 part_hiding_test.gd45 断言)+ equip_model_test.gd 补 7 段。 仍为 seam:精确 tie-break 顺序 / 已穿同槽替换 / LIMIT_PCBANG·REAL_TIME* / 属性 4 项按名读取 留待接服务器 char_item.cpp 比对;变身态(40250 实体表无 poly_race 字段,_is_poly()false,等 GC_CHARACTER_UPDATE 带变身 race)。 增量 129(§4.1 装备校验链精确化): 已对照 40250 服务器源码逐行核实链路并重排 equip_rules.gd.can_equip()—— char_item.cpp:7462 CanEquipNowJOB antiflag → LIMIT_LEVEL/STR/INT/DEX/CON → WEARABLE_UNIQUE 同组去重,IsSameSpecialGroup 客户端退化为同 vnum 判定)→ item.cpp:472 FindEquipCell(新增 1:1 复刻 find_equip_cell()else-if 优先级 BODY→HEAD→FOOTS→WRIST→WEAPON→SHIELD→NECK→EAR→ ARROWRING→RING1 空则 RING2UNIQUE→UNIQUE1 空则 UNIQUE2COSTUME 按 sub_typeBELT 固定, DS/SPECIAL_DS 走龙魂专用路径返回 -1)→ char_item.cpp:6100 EquipItem 后置检查(礼服 vnum 11901..11904 + 骑乘禁止 → 变身中除 WEAR_ARROW 一律禁止 → FN_check_item_sex 性别 → IsRideItem 已骑乘禁止)。帝国 antiflag 从装备门降级为 tooltip 提示empire_warning(), 服务器装备路径从不检查、仅显示层语义);LIMIT_PCBANG(6) 不是门禁(仅标记), LIMIT_REAL_TIME(7) / LIMIT_REAL_TIME_START_FIRST_USE(8) 由 socket 驱动到期,客户端职责只有 tooltip 倒计时(item_tooltip.gd 补「PC房专用道具」「首次使用后计时」与到期文案);变身门读 实体 polymorph/poly_vnum + affect 220_equip_ctx() 同步采集 st/dx/ht/iq 四属性与穿戴表 0..10 + 新装备槽 90+19..90+23)。equip_rules_test.gdfind_equip_cell 与后置检查断言。 远端 PC 换模链首版已在增量 99 落地—— game_scenech_type==0 创建 RemotePlayerView,复用真实 PlayerView race / GR2 / MSA 远端 EquipModel 只消费 awPart[ARMOR,WEAPON,HEAD,HAIR]net_world 按四字段调用 setter, 不读取本地 get_equipment()。所有职业 / race、变身字段、远端真服换装和精确视觉仍待验收。见 inventory_ui.gd / equip_rules.gd / part_hiding.gd / equip_model.gd,详见 CLIENT-GAP-FIX.md §4.1 / C.5。

  2. 物品拖放已有全局首版,但还不是完整原版鼠标物品态。 已有全局持有物品、鼠标跟随图标、 背包 / 装备 / 腰带到背包 / 快捷栏投放、safebox / mall / shop / exchange / private_shop 的 首版落点、无效区域取消、stack split、实例元数据传递和统一失败恢复;仍缺多格占用、装备预览、 全部窗口的原版光标 / 音效和真服回滚语义。

  3. 物品 tooltip 已覆盖固定字段与部分特殊分支,但仍未完整迁移。 增量 58 已接入 limits[2]applies[3]values[6]、实例 attrs[7] / sockets[3]、精炼关联和英文 itemdesc.txt;增量 60 已补鱼竿 / 镐、鱼长度、彩票、混合物品和龙魂基础提示;增量 62 已补 技能书 / 遗忘书 / 变身道具标题和基于限制字段的实时剩余时间。仍缺 CJK 文案 / 字体、其他 特殊分支、装备对比(参考源码未发现对应方法)和完整计时来源语义。

  4. Safebox 金币 / 改密已按 40250 真实能力定界(增量 129),网格仍为首版。 金币存取经核实 40250 无 wire 协议HEADER_CG_SAFEBOX_MONEY 服务端不存在、GC_SAFEBOX_MONEY_CHANGE(84) 从不发送、ClientVS22 SendSafeBoxMoneyPacket 为 assert-stub、原版 RefreshSafeboxMoneypass——故 UI 金币只读展示(不伪造协议包,避免状态失步)。close() 补发聊天命令 /safebox_closecmd.cpp:350,对齐原版关窗即服务器侧关闭);改密走 /safebox_change_password <old> <new>cmd.cpp:354,各 1..6 位)三段密码 ConfirmationDialog mall_ui.close() 同步补 /mall_close。仍缺 45 格网格、分页、指定目标格、仓库内移动、重试 限制和原版互斥规则。见 safebox_ui.gd:7

  5. 交易结束 / 失败提示已补(增量 129),事务状态仍缺。 已核实 GC_END 对成功与取消发同码 exchange.cpp EXCHANGE_ENDCancel()),包级不可区分——EntityStore.ExchangeState 新增 last_notice 捕获 EXCHANGE_GC_ALREADY(6) / EXCHANGE_GC_LESS_GOLD(7)(新协议 wire 名 LESS_ELK),exchange_ui 结束时 5s 浮层提示(「对方已经在交易中」/「金币不足」)+ 交易中 状态行内联提示;最终结果以 CHAT_TYPE_INFO 为准。仍缺锁定、接受后修改自动取消接受、物品 占用、交易失败恢复和交易中的背包锁定。

  6. NPC 商店已有货位网格与购买确认,但仍非完整原版商店。 增量 59 已接入 5×8/40 格货位、 空位禁用、货架全局槽号、tooltip 和 BUY 确认(固定数量 1);仍缺真实图标 / .sub、非金币货币、 busy 状态、完整库存限制、精确回包和对应音效。

  7. 私人商店是兼容实现。 虽然 CG_MYSHOP 已能发送,但原版库存模型、拖放、价格编辑、 开店前检查、库存锁定、撤下回滚和其他玩家查看流程尚未完整迁移。

  8. 快捷栏死亡 / 变身 / 失配刷新已补(增量 129),特殊技能冷却仍缺。 参考端可执行类型只有 INVENTORY(1)SKILL(2)EMOTION(3),当前已统一该编号、接通表情执行、4×8 显示、 inventory → quickslot 投放以及数字 / F1F4 / Shift 入口;增量 129 接 entity_dead(死亡全槽 置灰 + 激活守卫)、affect_added/affect_removedaffect 220 = 变身,技能槽置灰)、 inventory_changed(物品槽 get_item 为空 / vnum==0 时置灰并显示 ×vnum,覆盖换武器 / 服务器回收)。仍缺特殊技能冷却。见 quickbar.gd

  9. 组队状态是近似定义。 affect 标签仍有暂定映射,队伍技能冷却、队长权限动态变化、离线、 死亡、距离状态和多账号行为还未完成真实验收。

  10. 好友 / 公会 / 情侣 messenger 与公会权限已有完整首版(增量 129)。 friend_ui.gd 按 原版三固定组(好友 / 公会 / 情侣,uimessenger.py:274-295)分列:好友行、公会行(在线点 / 会长标记 / 点击私聊)、情侣行(♥ 在线点 + 「在身边」标记);情侣在线 / 在身边 / 离婚由 CHAT_TYPE_COMMAND 事件 lover_login/logout/near/far/divorcemarriage.cpp)驱动 LoverInfo.online/nearlove_ui 状态后缀与自动隐藏。公会:_my_auth() 按自身 grade 查 auth 位(guild.h:94-97 ADD/REMOVE/NOTICE/USE_SKILL)门控踢人按钮、! 前缀公告、技能施放 / /gskillup 升级,成员数据未同步时回退全开(服务器权威,对齐原版无前端门);公会战宣战 应答弹窗(state 3 = GUILD_WAR_RECV_DECLARE/war <name> 接受 / /nowar <name> 拒绝); 成员页资金行(存 / 取包保留 1:1,40250 服务端已禁用 CG 13/14 处理器)。仍缺:私聊生命 周期细节、情侣 affect 表现、公会日志页、积分板、领地和公会建筑、真服端到端验收。

5. P1:任务、NPC 和世界玩法

  1. Quest EventManager 已从少量标签扩展为常用标签的解析和部分界面 side effect,但不是完整事件机—— 标签表 / 索引解析 / 命令分派 / 状态字段已 1:1(增量 95)。 当前支持 ENTERCLEARNEXTDONEQUESTIONINPUTSELECT_ITEMLETTER,并识别 COLOR/COLOR256ITEM/MOBWINDOW_SIZE、居中、CONFIRM_WAIT、图片资源加载、DELAY/WAITQUESTBUTTON/QUESTBUTTON_CLOSEADDMAPSIGNAL/CLEARMAPSIGNAL/SETCMAPPOS;仍未按参考端逐事件推进等待、分页…… 增量 95 把 project/ui/quest_dialog.gd 由单遍手写 parse_script 重写为 CPythonEventManager 1:1 const EVENT_TYPE_MAP42 tag,键 = tag 字符串)+ enum { EVT_LETTER .. EVT_SELECT_ITEM } (顺序 = REF/UserInterface/PythonEventManager.h:35EEventType+ _get_script_event_index() = GetScriptEventIndex PythonEventManager.cpp:1093:前导 /EVENT_POSITION_END + 剥离, 未知名字 → 跳过不进 match+ class EventSet extends RefCounted= SEventSet :98,字段名 1:1iyLocal/isLock/lLastDelayTime/iCurrentLetter/CurrentColor/strCurrentLine/isConfirmWait/ iConfirmEndTime/lWaitingTime/iRestrictedCharacterCount=30/iAdjustLine/isTextCenterMode/isWaitFlag/ nAnswer 等)+ init_event_set()__InitEventSet :72/ insert_line()__InsertLine :976/ clear_line()ClearLine :953+ _process_event_set()= ProcessEventSet :377switchcase 对拷:DELAY/COLOR/COLOR256 的 START vs /ENDLETTER/ITEM/MOBiCurrentLetter >= iRestrictedCharacterCount 断行、NEXT/DONE/INSERT_IMAGEiAdjustLine += 2WAITQUESTIONMakeQuestion)、image family、SETMSGPOS/ADJMSGPOS 空实现(:562/:566)、 SET_CAMERAGetCameraSettingFromArgList :48/BLEND_CAMERA/RESTORE_CAMERAFADE_*/WHITE_*DUNGEON_RESULTCONFIRM_WAIT/END_CONFIRM_WAITRUN_CINEMApass+ _tokenize() / _tag_kv() / _arg_s/_arg_i/_arg_fkeyword 优先、位置回退)。parse_script() 返回字典保旧契约 全键不变,新增 insert_images/dungeon_result/adjust_line/waiting_time/is_locked/wait_flag/ answer_count/cinema_file/end_confirm;首命令 RUN_CINEMARegisterEventSetFromString :142 特例)→ es.cinema_file + 新 signal cinema_requested。裸网络文本折成合成 LETTER is_raw=true → 跳过 30 字断行,参考脚本只有显式 [LETTER])。自检 project/quest_event_test.gd(第 16 个 canonical23 组断言),quest_test / select_item_ui_test 仍绿。仍为 seam:逐帧 UpdateEventSet :312 时间推进(lLastDelayTime 递减 / isWaitFlag 暂停 / isConfirmWait 倒计时刷新 m_strLeftTimeString)、Skip / EndEventProcessiConfirmEndTimetimeGetTime.msc 事件文件加载(RUN_CINEMA 换文件)、MakeNextButton NEXT vs DONE 文案区分 + DoneEvent() 回调、camera_event_requested / fade_event_requested / cinema_requested 消费端与 §7.6 相机模式对接、物品名颜色 (1.0,0.2,0.2)。见 quest_dialog.gd / quest_event_test.gd

  2. 任务日志已补 5 行窗口、滚动、默认图标、倒计时和独立 QuestButton 条带,但不是完整 Quest 系统。 增量 101 按参考端 QUEST_MAX_NUM=5 固定显示 5 行;超过 5 条滚动到离散的 questShowingStartIndex,任务行点击发送 -2147483648 + questIndex,空 icon 使用 season1/icon/scroll_open.tga。QuestButton 的开始 / 删除 / 点击发送、小地图任务信号已接入; 仍缺多列定位、世界箭头、屏幕边缘指示、完整任务资源视觉和真实任务生命周期。

  3. 钓鱼输入、水面位置判定和反馈消息已完成首版,但钓鱼、副本、坐骑和观战者仍不是完整玩法。 增量 102 已接入 钓鱼模式攻击键 → CG_FISHING、方向 / 地面点击取消和 500ms 节流、特殊技能 123 快捷栏入口, 以及 GC_FISHING 五类事件的客户端状态维护;增量 103 已接 GetFishingRot 等价的 ATTRIBUTE_WATER 扫描和选中 heading;增量 104 已接 wrong-place / failure / unknown / notify / success locale 消息和 item proto 类型判定;增量 105 已接坐骑 vnum→level 攻击门和新坐骑技能门。 仍缺真实 GC_ITEM_* 鱼获事务 / 背包校验、副本入口 / 计时 / DungeonBlock、坐骑模型 / MODE_HORSE* 动作 / 移速、观战自由视角 / 完整 ADD/REMOVE/MOVE 生命周期和换图后的完整清理。

  4. PVP、PK 和决斗没有完整规则——可攻击判据已 1:1(增量 94)。 当前有部分关系和字段, 但缺……可攻击判断…… 增量 94 新建 project/entity_rules.gdis_attackable_instance(self_e, victim_e, ctx) 逐行照抄 CInstanceBase::IsAttackableInstanceREF/UserInterface/InstanceBase.cpp:2147)—— IsStone / IsPC / IsEnemy / IsPoly 四分支、决斗三态、PK_MODE_GUILD 同公会提前放行、 victim.IsKiller() && !同队、PK_MODE_FREE/GUILD!= PROTECT && !同队分支、IsSameEmpireIsPVPInstance / PK_MODE_REVENGE && 冲突善恶、异帝国直接可打、victim.IsEnemy() / IsWoodenDoor()alignment_grade:502/ alignment_type:524/ is_conflict_alignment_instance:2116/ _is_same_empire:470/ _is_pvp_instanceInstanceBaseEffect.cpp:505/ _find_duel_key:495/ _is_same_partyPythonPlayer.cpp:1445/ _is_wooden_door:2390)全部 1:1。net_play.gd_is_attackable(e) 从「_entity_kind(e) in [0,2]」桩改为拼 self_e / victim_e / _attack_ctx() get_duel / get_pvp_relations / get_guild_wars / get_party)委托 EntityRules;自检 project/entity_rules_test.gd(第 15 个 canonical,25 组断言)。名字颜色(ENameColor,§8.8 已由增量 110 的 name_color.gdGetNameColorIndex / SetEmpireNameMode 1:1+ net_world 驱动补齐;称号色(GetTitleColor)/ 头顶公会名 / 等级已由增量 111 的 text_tail.gd RefreshTextTail / UpdateTextTailLevel / EnablePKTitle 1:1+ net_world 三子标签补齐; 头顶聊天 / 信息尾标已由增量 112 的 chat_tail.gdRegisterChatTail / RegisterInfoTail / TextTail_GetLivingTime / IsChatTextTail / RecvChatPacket 派发 1:1+ net_world 每 vid 一条 "bubble" Label3D5s 硬删、"名字: " 前缀剥离、SHOUT·COMMAND 排除)补齐。 头顶名字「是否显示」已由增量 113 的 name_show.gd + net_world._name_shown() 补齐 __IsShowName() _always_show_nameIsAlwaysShowNamegame_option_ui always_show_name radio 经 display_option_changed 信号)or _alt_heldLALT);ShowAllTextTailfDistanceFromPlayer < 3500 平面距裁剪;net_play.target_changed 目标 VID / chat 尾标 bNameFlag 强显绕过 3500 但仍过 isShow · !IsGuildWall · CanPickInstance 资格)。 头顶伤害飘字已由增量 114 的 damage_effect.gd(纯静态)+ net_world._on_damage / _process_damage_queue() 补齐:RecvDamageInfoPacketPythonNetworkStreamPhaseGame.cpp:2401bSelf(目标是主实例)/ bTarget(目标是 m_pInstTarget/ damage >= 0 门、 CInstanceBase::AddDamageEffectInstanceBaseEffect.cpp:87)的 CPythonSystem::IsShowDamage() 门、ProcessDamage:100InstanceBase.cpp:1910 每帧调一次,m_DamageQueue 每帧 pop_front 一条)的分类((flag & DODGE) || (flag & BLOCK)bSelf ? MISS : TARGETMISS 并 return CRITICAL / POISONCreateEffect 参考端均注释掉 → 落普通数字;bSelf"damage_" + SELFDAMAGE / SELFDAMAGE2 交替;bTarget == false"nontarget_" + return(不渲染); 否则 "target_"while (damage > 0) 逐位 % 10 / /= 10index 0..7,每位一张 damagevalue/<type><digit>.ddsFONT_WIDTH(30) * index 相机右向铺开)、 CPythonSystem::SetShowDamageFlagPythonSystem.cpp:342bShowDamage = iFlag == 1 默认 true)全部 1:1game_option_ui「伤害数字」radio 经 display_option_changednet_world.set_show_damagenet_world.clear_for_map_change_damage_queue。 自检 project/damage_effect_test.gd(第 25 个 canonical~50 组断言)。 seam ⑪:本移植用一块 billboard Label3Dstr(amount) / "MISS",无 damagevalue dds 逐位特效 / 相机右向像素铺排;self SELFDAMAGESELFDAMAGE2 退化为落字水平抖动符号 交替(±0.3);暴击不再放大 / 变色(对齐该 40250 build 本就无独立暴击视觉);_damage_queue 全局单条 vs 参考每实例一条(同帧多 VID 受击分帧显示);damage_number 信号仍无条件发 (本移植扩展供战斗日志 / 测试)而参考端整个 AddDamageEffectIsShowDamage() 门控。 seam ⑩悬停 chr.Pick() 强显(增量 116 收口,见下)、屏幕空间 ArrangeTextTail 防重叠 (增量 120 收口,见下)、CanPickInstance 仅按死透近似;ShowName()playerGauge.EnableShowAlways() / SetQuickPage 未移植。 头顶目标框(TargetBoard)收包分派已由增量 115 的 target_board.gd(纯静态)+ net_play._on_target_info 补齐:CPythonNetworkStream::RecvTargetPacket PythonNetworkStreamPhaseGame.cpp:2424)的判定树 —— 实例缺失 → CloseTargetBoard:2453); 目标 !IsDead() 时:IsPC() || IsBuilding()CloseTargetBoardIfDifferent:2440)、 否则 pInstPlayer->CanViewTargetHP(*pInstTarget)InstanceBase.cpp:2293 IsStone() || IsWoodenDoor() || IsEnemy())→ SetHPTargetBoard:2442)、否则 CloseTargetBoard:2444);m_pInstTarget = pInstTarget 仅在 !IsDead() 块内(:2447); 目标已死透 → 不动框。game.pySetHPTargetBoard:829,换 VID 先 ResetTargetBoard + SetEnemyVID/ CloseTargetBoardIfDifferent:837/ CloseTargetBoard:841/ SetPCTargetBoardLCONTROL 悄悄话捷径(:804)全部 1:1。net_play._on_target_info(vid, hp%) 由「任意 vid != 0 且有名字都 hud.set_target」改为按 classify 分派(is_pc = _entity_kind == KIND_PCcan_view = KIND_STONE 或 KIND_MONSTER):CLOSE / CLOSE_IF_DIFFERENT vid != _target_vid 才)→ _clear_target()SET_HP → 换 VID 先 _target_vid = vid + target_changed.emithud.set_target(名字, clamp_hp_pct(hp%))NONE → 不动。 自检 project/target_board_test.gd(第 26 个 canonical~35 组断言)。 seam ⑫poc 无 building / wooden-door / stone-door 实体类型(is_building 恒 false、 木门分类缺,做出来后再补 IsWoodenDoor / IsBuilding);target_info(vid, hp%) 由 C++ extension/src/net/m2_client.cpp:3129/3633 从 EntityStore 抽出,本移植不改 C++ 收包 (对齐「纯 GDScript」约束);LCONTROL 悄悄话捷径谓词 whisper_on_ctrl_click 已备, net_play 暂不接(无 whisper dialog 钩子) 增量 122 已接线:net_play._on_pick_entity_kind(e) == KIND_PC 分支(C++ picking 只对 PC 派发 SetPCTargetBoard)调 TargetBoard.whisper_on_ctrl_click(_lcontrol_down, EntityRules._is_same_empire(_main_entity(), e), vid == _main_vid, false),真且 _entity_name(e) != ""whisper_requested.emit(name) net_playsignal whisper_requested(name) + var _lcontrol_down + set_lcontrol_down(down) game_scene._unhandled_input KEY_CTRL 按下 / 松开喂 set_lcontrol_down(e.pressed) net_play.whisper_requestedchat.start_whisperpoc 版 OpenWhisperDialog=聊天输入 预填 "/w 名 ");is_building 实参恒 false(:814 分支 poc 永不触发)、无独立 whisper dialog 窗口 = seam ⑫ 末尾;netplay_test.gd 加「8c) seam ⑫」段(无新增测试文件,PASS=69)。 行为变化:此前对任意有名字 VID(含 NPC)都显 目标框;现 PC / building 目标不显血条框(只在框上是别的 VID 时清旧框)、NPC / warp 关框、 目标已死透不动框、hp% 钳到 0..100。 game.py GameWindow.OnRender() 的「Picking」块(:1519)分派已由增量 116 的 pick_show.gd (纯静态第 27 个 canonical)补齐:textTail.UpdateAllTextTail():1531)→ wndMgr.IsPickedWindow(self.hWnd) 门(:1533)内 chr.Pick():1535)命中角色(!= -1)→ textTail.ShowCharacterTextTail(idx):1537)、targetBoard.GetTargetVID() != 0 → ShowCharacterTextTail(targetVID):1539)、not __IsShowName()item.Pick()ShowItemTextTail:1543/:1546);门外 __IsShowName()ShowAllTextTail() + PickingItemIndex = textTail.Pick(x, y):1552/:1554);末尾 UpdateShowingTextTail + ArrangeTextTail:1557/:1558)、PickingItemIndex != -1SelectItemName:1560)全部 1:1。 pick_show.gd APINO_PICK := -1char_pick_active(is_picked_window)forced_name_vids( is_picked_window, pick_char_vid, target_vid)(收 chr.Pick() 命中 + 目标框 VID,去重、顺序 [hover, target]、非窗口 → [])、should_pick_item(is_picked_window, is_show_name) and notshould_show_all_names / should_pick_text_tail is_show_nameshould_select_item_name != NO_PICK。poc 侧仿增量 113 的 _target_vid forced 路数给 net_world._name_shown() 加第 5 路 forced _hover_vidnet_world.set_hover_vid(vid)(值变才刷 prev / vid 两节点名字可见性,仿 set_target_vid)由 player_controller._update_hover_cursor() 三出口喂 —— 鼠标压 UI → 0、 悬停地面物品 → 0、射线命中 pickablebest.get_meta("vid")、无命中 → 0;新增 hover_entity_changed(vid) 信号(值变去抖),game_scene 紧挨 net_play.target_changed → net_world.set_target_vid 那行连 pc.hover_entity_changed → net_world.set_hover_vid。 自检 project/pick_show_test.gd(第 27 个 canonical~24 组断言)+ 全 67 个离线回归通过。 seam ⑩ArrangeTextTail 屏幕空间尾标去重叠排布仍缺(poc 名字牌是 3D billboard,靠深度 天然分层,未做 2D 投影后的挤开)—— 本增量只收口「悬停角色强显名字」一小块。近似:参考里 chr.Pick() 强显与 targetBoard 强显都在 :1533 的 IsPickedWindow 门内,poc 把 _target_vid / _hover_vid 都当无条件 forced_hover_vidplayer_controller 压 UI / 悬停物品时置 0 部分等效该门。行为变化:此前只有 targetBoard 目标(增量 113)forced 强显头顶名字;现悬停到 的角色(pickablesvid meta)也 forced 强显 —— 即便未开「总显名字」且超 3500 平面距。 悬停命中沿用既有 _ray_pick_t,无新增射线开销。 CPythonTextTail::UpdateShowingTextTailPythonTextTail.cpp:88)里「名字尾标随聊天气泡上移 17px」已由增量 117 的 name_tail_layout.gd(纯静态第 28 个 canonical)收口:角色名字尾标投影完 屏幕坐标后(:104),m_ChatTailMap.find(dwVirtualID) 命中(:106/:110)且那条 bNameFlag 为 TRUE:112,= RegisterChatTail 的「聊天」气泡,非 RegisterInfoTail 的「信息」气泡)→ pTextTail->y = pChatTail->y - 17.0f:114)。name_follows_bubble(has_chat_tail, chat_name_flag) andname_tail_screen_y(default_y, bubble_y, follows) 参考端字面 bubble_y 17name_tail_world_y(default_world_y, bubble_world_y, follows, px_size)= poc 3D 适配(屏幕 y 向下 −17px → 世界 y 向上 bubble_world_y + 17*px_size)。poc 侧 net_world._reposition_name_tag(n)_apply_chat_tail / _expire_chat_tails 调用,读 _chat_tails[vid].name_flag 决定名字 Label3D 局部 y 在 NAME_TAG_DEFAULT_Y2.1)与「BUBBLE_TAG_Y 2.35 + 17*pixel_size」之间 切换。自检 project/name_tail_layout_test.gd(第 28 个 canonical+ chat_test.gd 扩断言 + 全 68 个离线回归通过。 增量 119 收口 seam ⑦(子标签随名字尾标纵向位移)CPythonTextTail::ArrangeTextTail PythonTextTail.cpp:151)逐条角色名字尾标(:211)把公会名(:228,pTextTail->y - c_fyGuildNamePositionc_fyGuildNamePosition = 15.0f :21/ 称号(:258,同 pTextTail->y)/ 等级(:283,同 y)三个子标签屏幕 y 全部锚在 pTextTail->y 上 —— 而这个 y 已被 UpdateShowingTextTail :114 顶过 17。name_tail_layout.gdGUILD_NAME_ABOVE_NAME_PX := 15.0name_tail_lift_delta(default_world_y, bubble_world_y, follows, px_size)(= name_tail_world_y(...) - default_world_y)、subtag_world_y(default_subtag_y, name_tail_lift_delta)(=相加)。net_worldGuildTag / TitleTag / LevelTag 默认 局部 y 提为 SUBTAG_DEFAULT_Y2.34 / 1.86 / 1.74)常量,_reposition_name_tag 顶完名字 尾标后按同一 lift 平移三个子标签,气泡过期 / 「信息」尾标时回默认。自检 name_tail_layout_test.gd(+「4. seam ⑦ 收口」段)+ chat_test.gd+真 NetWorld 断言: 冒气泡后三子标签 position.y == 默认 + lift、覆盖成信息尾标后回默认)+ 全 68 个离线回归通过。 seam ⑦ 剩余子标签竖排 vs 参考屏幕空间逐像素水平排版(公会名上方 15px、称号 / 等级 同水平行左偏) 逐像素水平排版已由增量 121 收口(见下)、文本宽按字符数估 vs GetTextSize 已由增量 123 收口(见下)、单行文本高仍 15px 估 已由增量 124 换 GetTextSize 的 h 收口(见下),剩余:poc 无公会徽记 mark 实体、三子标签仍竖排叠 x 偏移 vs 参考同一屏幕行。 seam ⑨ 剩余:气泡与名字仍是两个并存 Label3D 节点(非参考单尾标链表)。 增量 120 收口 seam ⑩(ArrangeTextTail item 尾标去重叠 + isInPythonTextTail.cpp:153m_ItemTextTailList 块(:160-190)—— 逐条掉落名条 pInsertTextTail 与其它每条 pCompareTextTailisIn:489-507[x+xStart,x+xEnd]×[y+yStart,y+yEnd] 的 AABB <=/>= 含边界),命中就把「自己」顶到 pCompareTextTail->y + pCompareTextTail->yEnd + yTemp yTemp = 5),itorComparebegin() 从头重跑、LimitCount >= 20 封顶。新增第 29 个 canonical project/text_tail_arrange.gdextends RefCounted 全静态):PUSH_GAP := 5.0 / MAX_ITERS := 20box_start_x(w)-w/2-2 / box_end_x(w)w/2+2 / box_start_y()-2 / box_end_y(h)hPythonTextTail.cpp:810-813)、boxes_overlap(a, b)(=isIn a/b{x,y,w,h})、arrange_item_tails(tails: Array) -> Array(外层逐条 i、内层逐条 j 跳过自身、boxes_overlap 命中则 ys[i] = ys[j] + box_end_y(h_j) + PUSH_GAP、j 归零 limit++、 limit >= MAX_ITERS breakpCompare 用「已排布后」的 ys[j],与参考端共用一张链表就地改写 一致)。poc 侧 project/ui/ground_items.gd_camera_getter + 常量 TAG_BASE_Y 0.75 / TAG_CHAR_W_PX 7 / TAG_LINE_H_PX 15setup() 尾部加可选 camera-getter 参数,_process 尾部 _arrange_tags():按 vid 排序遍历掉落 → cam.unproject_position 各名条世界锚点 (背面 / 无相机跳过并回默认)→ 组 {x, y, w = max(1, len*7), h = 15}arrange_item_tailsposition.y = TAG_BASE_Y - (arranged_y - y0) * pixel_size,可见名条 < 2 直接回默认; project/game_scene.gdfunc() -> Camera3D: return cam 传进 ground_items.setup 第 6 参。 自检 project/text_tail_arrange_test.gd(第 29 个 canonicalbox_* 四边、boxes_overlap x/y 都叠 / 边界相接含边界 / x·y 分离、arrange_item_tails 单条 / 不叠 / 两条同位 → [121, 100] / 三条同位 → [121, 142, 100] 且两两不叠 / 40 条全同位不死循环)+ p2b_test.gd+真 Camera3D + 真 GroundItems:同位两掉落名条投影后靠前一条按 (15+5)*0.005 = 0.1 下推、靠后留默认、 拉开距离回默认)+ 全 69 个离线回归通过(PASS=69 FAIL=0)。seam ⑩ 剩余名条框按 「字符数 * 7px」估宽 vs 参考 GetTextSize 精确像素 已由增量 123 换真实字体度量 GetTextSize 1:1(见下);单行文本高仍 15px 估 已由增量 124 换 GetTextSize 的 h 1:1(见下);只对掉落 item 名条做(与参考 m_ItemTextTailList / m_CharacterTextTailList 分表一致,角色名字尾标不参与)。 增量 121 深化 seam ⑦(子标签逐像素水平排版)CPythonTextTail::ArrangeTextTail 角色块 PythonTextTail.cpp:211-320 非 EUROPE 分支)—— fxAdd = 0(无称号无等级,:236)/ 8 (有称号,:242)/ 4(仅等级无称号,:288);名字文本 CENTER 对齐画在 x + fxAdd:320)、 称号 CENTER 对齐画在 x iNameWidth/2 fxAdd(:258)、等级 RIGHT 对齐画在有称号 x iNameWidth/2 fxAdd iTitleWidth:282/ 仅等级 x iNameWidth/2 fxAdd:312); 公会名 / 徽记仅当 pMarkInstance && pGuildNameInstance 才由 ArrangeTextTail 重定位(:220), iImageHalfSize = markW/2 + c_fxMarkPosition(1.5):224),公会名 x + iImageHalfSize:228)、 徽记 x guildW/2 iImageHalfSize:227)。name_tail_layout.gdNAME_X_ADD_TITLE := 8.0 / NAME_X_ADD_LEVEL_ONLY := 4.0 / MARK_X_PAD := 1.5 / MARK_ABOVE_NAME_PX := 26.0 + 6 静态 方法 name_x_shift(has_title, has_level)fxAdd 8/4/0/ title_x_offset(name_w)name_w/28 / level_x_offset(name_w, title_w, has_title)(有称号再减 title_w/ guild_name_x_offset(mark_w)mark_w/2+1.5/ mark_x_offset(guild_w, mark_w) / subtag_world_x(px_offset, px_size)px_offset * px_size 线性、符号保留左负右正)。 net_worldconst SUBTAG_CHAR_W_PX := 7.0(无字体度量 → 文本宽按「字符数 * 约 7px」估, 与 ground_itemsTAG_CHAR_W_PX 同源)、_apply_text_tail_to 尾部调 _layout_subtags_x(n, ttag, ltag):由 .text != ""has_title / has_level,设名字 Label3D / TitleTag / LevelTag.position.x(无则回 0),GuildTag.position.x 恒 0(poc 无公会 徽记实体、参考的 pMarkInstance && pGuildNameInstance 门恒不满足)。纵向位移(增量 117/119 写 .position.y)与横向排布(增量 121 写 .position.x)互不干扰。自检 name_tail_layout_test.gd(+「5. seam ⑦ 水平排版」段)+ chat_test.gd+真 NetWorld Charlie vid 3000 有公会 / 称号 / 等级 → 三子标签 x 偏移方向与精确值断言、去称号后名字回移 4)+ 全 69 个离线回归通过(PASS=69 FAIL=0,无新增测试文件)。seam ⑦ 剩余poc 无公会徽记 mark 实体(GuildTag x 恒 0)、文本宽按字符数估 vs 参考 GetTextSize 精确像素 已由增量 123 换真实字体度量 GetTextSize 1:1(见下)、三子标签仍竖排叠 x 偏移 vs 参考同一屏幕行 (poc 可读性折衷)、单行文本高仍 15px 估 已由增量 124 换 GetTextSize 的 h 收口(见下)。 增量 123 收口 seam ⑦ / ⑩(文本宽换真实字体度量 GetTextSizeCPythonTextTail::ArrangeTextTailiNameWidth / iTitleWidth / iLevelWidth / guildWPythonTextTail.cpp:225 / :233 / :240 / :267 / :297)与 item 尾标框 xSize / ySize:733 / :748 / :809)全部取自 pTextInstance->GetTextSize(&w, &h) —— 字体的实际逐字形排版度量(含字距、非等宽),不是 「字符数 * 定宽」。新增第 30 个 canonical project/text_metrics.gdextends RefCounted 全静态): text_size(font, text, font_size)(空 / null / font_size<=0Vector2.ZERO,否则 Font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size) = GetTextSize 1:1)、 width_of(font, text, font_size, fallback_char_px := 7.0)(有字体量得正宽 → 精确像素,否则 → text.length() * fallback_char_px)、label_width(label: Label3D, fallback_char_px := 7.0) (从节点自身取 font ?? ThemeDB.fallback_fontfont_size)。net_world._layout_subtags_xname_w / title_w 改走 TextMetrics.label_width(label, SUBTAG_CHAR_W_PX)(原 字符数 * SUBTAG_CHAR_W_PX)、ground_items._arrange_tags 的名条框 w 改走 TextMetrics.label_width(tag, TAG_CHAR_W_PX)(原 text.length() * TAG_CHAR_W_PX); SUBTAG_CHAR_W_PX / TAG_CHAR_W_PX(均 7.0)保留仅作无字体(headless / 未配主题)兜底; name_tail_layout.gd / text_tail_arrange.gdname_w / title_w / 框 w 作入参,排布公式 (增量 117 / 119 / 120 / 121)不动。自检 project/text_metrics_test.gd(第 30 个 canonical:常量、 退化输入 → ZERO、真实度量随串长 / 字号单调、width_of 有字体 == text_size.x、无字体回退 字符数 * pxlabel_width 回落 ThemeDB.fallback_font / null → 0、Label3D 实测)+ 全 70 个 离线回归通过(PASS=70 FAIL=0,新增 text_metrics_test)。seam ⑦ / ⑩ 剩余单行文本高仍按 15px 估(未走 GetTextSize 的 h 已由增量 124 收口(见下)、poc 无公会徽记 mark 实体、 三子标签竖排叠 x 偏移 vs 参考同一屏幕行、掉落名条为唯一 item 尾标来源。 增量 124 末尾 seam ⑦ / ⑩(单行文本高换真实字体度量 GetTextSize 的 hArrangeTextTail 的 item 尾标去重叠把「自己」顶到 pCompareTextTail->y + pCompareTextTail->yEnd + yTemp PythonTextTail.cpp:182),而 yEnd = ySize:751 / :812),ySize 与增量 123 换过的 xSize 同出 pTextInstance->GetTextSize(&xSize, &ySize):749 / :809)—— 单行文本的 ySize ≈ 字体行高(ascent + descent),与字形无关,Font.get_string_size(...).y 是 1:1 对应物。 text_metrics.gd(仍第 30 个 canonical,扩非新增)加 const FALLBACK_LINE_H_PX := 15.0 (=旧 ground_items.TAG_LINE_H_PX)、height_of(font, text, font_size, fallback_line_px := 15.0)(空串 → 0;量得正高 → text_size(...).y;否则 → 回退)、label_height(label: Label3D, fallback_line_px := 15.0)label.font ?? ThemeDB.fallback_font + label.font_sizenull → 0)。 ground_items._arrange_tags 的名条框 h 由硬编码 TAG_LINE_H_PX 改走 maxf(1.0, TextMetrics.label_height(tag, TAG_LINE_H_PX))TAG_LINE_H_PX15.0)保留仅作无字体 兜底。text_tail_arrange.gdbox_end_y(h) h,对齐参考 yEnd = ySize/ name_tail_layout.gd 收框高作入参、排布公式不动。自检 text_metrics_test.gd+「5. height_of / label_height」段: FALLBACK_LINE_H_PX = 15、空串 → 0、无字体 / font_size<=0 → 回退、有字体高为正且与串内容无关、 随字号单调、height_of == text_size(...).ylabel_height(null) → 0 / 空 Label3D → 0)+ 全 70 个 离线回归通过(PASS=70 FAIL=0,无新增测试文件)。seam ⑦ 剩余:poc 无公会徽记 mark 实体、 三子标签竖排叠 x 偏移 vs 参考同一屏幕行。seam ⑩ 剩余掉落名条 owner 未拆成独立 text instance(参考端 yEnd += yOwnerSize + 4:733-736poc 仍按单行 "name (owner)" 处理) 增量 125 已把去重叠框度量按 SetItemTextTailOwner 换 1:1(见下)、掉落名条为唯一 item 尾标来源。 增量 125 末尾 seam ⑩(owner 后缀框度量:owner 独立度量 + yEnd += yOwnerSize + 4:参考 CPythonTextTail::SetItemTextTailOwner(dwVID, c_szName)PythonTextTail.cpp:706-737)—— 带 owner 的掉落名条,owner 后缀挂成一个与名字分开的 CGraphicTextInstanceSetTextPointer(ms_pFont) 文本 = c_szName + strOwnershipstrOwnershipIDS_POSSESSIVE_MORPHENE == "" 时取字面 "'s":722);SetColor(1.0f, 1.0f, 0.0f) 黄、HORIZONTAL_ALIGN_CENTER),单独 GetTextSize(&xOwnerSize, &yOwnerSize):733),据此把去重叠框扩张:yStart = -2.0f:734)、 yEnd += float(yOwnerSize + 4):735)、xStart = fMIN(xStart, float(-xOwnerSize/2 - 1)) :736)、xEnd = fMAX(xEnd, float(xOwnerSize/2 + 1)):737);owner 为空则 Delete(pOwnerTextInstance) 并把框复位纯名字 xStart=-xSize/2-2 / xEnd=xSize/2+2 / yEnd=ySize :739-751)。代入 poc 的 {w, h} 框(box_start_x(w) = -w/2-2 / box_end_x(w) = w/2+2 / box_end_y(h) = h):h = name_h + owner_h + 4box_end_x(w) 要等于参考 :737fMAX(nameW/2+2, ownerW/2+1)w = maxf(name_w, owner_w - 2.0)(框 x 两边对称,故 box_start_x 自动 = 参考 :736fMIN);owner_w<=0owner_h<=0(无 owner)→ {name_w, name_h} 与参考 else 分支一致。text_tail_arrange.gd(扩第 29 个 canonical)加纯静态 item_box_with_owner(name_w, name_h, owner_w := 0.0, owner_h := 0.0) -> Dictionarytext_metrics.gd(扩第 30 个 canonical)加 string_width_like(label: Label3D, text, fallback_char_px := 7.0) / string_height_like(label: Label3D, text, fallback_line_px := 15.0) —— 用某 Label3Dfont ?? ThemeDB.fallback_font + font_size 量任意子串(label 为 null → 宽 text.length() * fallback_char_px / 高 空串 0 否则 fallback_line_px;空串宽恒 0),对应 参考端「名字 instance」「owner instance」各自 GetTextSizeground_items._on_added 新建 / 复用 drop 节点时都 set_meta("item_name", _name_for(vnum)) + set_meta("item_owner", owner) _arrange_tags 的去重叠框改为 iname = meta item_nameowner_txt = item_owner + "'s"(空则 ""const OWNER_POSSESSIVE := "'s"),TextTailArrange.item_box_with_owner(string_width_like(tag, iname), string_height_like(tag, iname), string_width_like(tag, owner_txt), string_height_like(tag, owner_txt)) 合成 {w, h};渲染仍是单个合并 "name (owner)" billboard Label3D(seam ⑩ 记录 在案的折中)。自检 text_tail_arrange_test.gd+「5. item_box_with_owner」段:无 owner / owner_w<=0 / owner_h<=0 → 退化、有 owner → h += ownerH + 4、owner 更宽 → w = ownerW-2 使 box_end_x == ownerW/2+1、顶到带 owner 框底下 → push = (name_h+owner_h+4) + PUSH_GAP+ text_metrics_test.gd+「6. string_*_like」段:label null → 回退 / 空串 → 0 / 有字体量的是 给定子串而非 label.text+ p2b_test.gdFakeClient.add_ground 加可选 owner 入参 + 新增 mount3/fc3/gi3 段:301 无 owner 靠前、302 带 owner "Alice" 靠后同位 → 无字体度量下 302 框 yEnd = 15+15+4 = 34、靠前 301 被顶 (34+5)*0.005 = 0.195)+ 全 70 个离线回归通过(PASS=70 FAIL=0,无新增测试文件)。seam ⑩ 剩余:owner 未真正独立渲染成 CENTER 黄字(poc 仍单条 "name (owner)")、strOwnership 只取了字面 "'s"(未接 ApplicationStringTable IDS_POSSESSIVE_MORPHENE 的本地化语素)、掉落名条为唯一 item 尾标来源。 root/uitarget.py TargetBoard.Open(vid, name):189-193)在整个 RecvTargetPacket 判定之前的 异阵营前置过滤(if not constInfo.GET_VIEW_OTHER_EMPIRE_PLAYER_TARGET_BOARD(): if not player.IsSameEmpire(vid): self.Hide(); returnconstinfo.VIEW_OTHER_EMPIRE_PLAYER_TARGET_BOARD = 1 默认)已由增量 118 补齐:target_board.gd 加纯静态 hidden_as_other_empire(view_other_empire, same_empire) not view_other_empire and not same_empire(不按 is_pc 收窄 —— empire 0 的 怪 / NPC / 石头在 EntityRules._is_same_empire 里恒「同阵营」故永不被挡);net_play_view_other_empire_target(默认 true+ set_view_other_empire_target(on)_on_target_infoTargetBoard.classify(...) 之前 if exists and TargetBoard.hidden_as_other_empire( _view_other_empire_target, EntityRules._is_same_empire(_main_entity(), e)): _clear_target(); return。同批把 §8.4「名字色」radio(name_color_normal / name_color_empire)接进 game_scene._on_display_optionnet_world.set_empire_name_mode(v == 1)(算法增量 110 已 1:1)、 「看他国玩家目标框」radio → net_play.set_view_other_empire_target(v == 1)game_scene 开局 两处 game_option_ui.display_value(...) == 1 回填;修 game_option_ui.gdtarget_board 默认值 0 → 1(对齐 VIEW_OTHER_EMPIRE_PLAYER_TARGET_BOARD = 1)。§8.4 三个显示 radio + §8.6 LALT 至此全部接线完毕。自检 target_board_test.gd+4 断言)/ netplay_test.gd+8b 段)/ system_menu_ui_test.gd+ name_color / target_board radio → _display + cfg + 信号)+ 全 68 个 离线回归通过。seam ⑫(延续)poc 无 building / wooden-door 实体类型(is_building 恒 false)、LCONTROL 悄悄话捷径未接(增量 122 已接 whisper)。 仍缺PK 模式上行(/pkmode 增量 38 已有)、保护区 IsInSafeATTRIBUTE_BANPK 地形属性、原版目标板视觉 / 决斗 UI;seam:__FindPVPKeyGC_PVP/ __FindGVGKey(公会战对局)/ IsGameMaster / IsBuilding·IsWall race 白名单 / m2dev 后端 IsKiller 状态位的 packet setter 均未接。

6. P1/P2:模型、动画和特效

  1. 本地角色先显示胶囊占位体。 GameScene 初始化阶段先创建 Capsule,真实角色稍后加载, 会造成模型闪烁、相机目标高度变化、碰撞体和阴影重新挂接。见 game_scene.gd:136game_scene.gd:811

  2. SpeedTree 仍是程序化代理。 当前只嗅探 bark/composite 贴图,叶簇 UV 和枝干使用 proxy, 没有真实 .spt 几何、leaf table、LOD、impostor、风参数和原始阴影。见 tree_placeholder.cpp:190PARITY-GAP.md:77

  3. EffectLib 已从粒子近似推进到 .mde 网格子集。 .mse Particle 已映射到 GPUParticles3D,现在会读取首个 TextureFiles 的实际 .dds、位置、发射形状、方向 / 速度; 增量 64 又按参考 EffectMesh.cpp 读取 .mdeEffectData / MDEData002,展开索引三角形、 翻转 UV、播放几何帧,并接入 .ifl 贴图帧和 MeshElement 的混合 / billboard / alpha;增量 65 接入 SimpleLight 的范围 / 生命周期曲线和 TexAni 基础帧延迟 / 顺逆序;增量 66 接入统一的 MovingType 直接段 / 二次 Bezier 位置曲线,并让粒子、网格、Light 共用。仍缺 TexAni 随机起始帧 / 每粒子语义、完整 emitter advanced / cycle、完整时间轴、骨骼挂点和真实资源逐项验收。见 effect_player.gdmde.gd

  4. GR2 运行时不是完整 ActorInstance。 缺 race/weapon/sex motion mode、loop/interrupt/ accumulation、action reservation、mount motion、武器轨迹、击退、倒地、起身和完整死亡动作。 参考:ClientVS22/ActorInstanceMotion.cpp

  5. 武器和特效骨骼挂点不完整。 当前主要覆盖右手武器和部分盾牌,缺双持、左手武器、职业挂点、 武器拖尾、骨骼特效、坐骑挂点、轨迹碰撞和装备切换清理。

  6. 材质是固定功能近似。 缺精确 alpha-test ref、多贴图 stage、color operation、透明排序、 角色专属光照和逐 surface 状态;sphere-map 默认 spec_power = 0,没有完整由装备数据驱动。 见 m2_material.h:39

7. P1/P2:地图、环境和相机

  1. 静态地图只完整接入 Building / DungeonBlock。 Effect、Ambience、地图机关、透明物排序、 portal/area、对象脚本状态和对象 LOD 没有完整迁移。见 metin2_world.cpp:367

  2. Pack 资源读取没有统一。 虽然 AssetSource/PackMount 已存在,World、terrain、tree、 water 和 static object 仍有直接 AssetResolver::resolve() 的路径,散文件和 .epk/.eix 运行 结果可能不同。见 metin2_world.cpp:380terrain_splat.cpp:133

  3. 地形 splat 虽已支持 16 层,仍非完整固定功能复现。 alpha 含义、texture wrap/filter、 多 stage、patch geometry LOD、skirt、patch 碰撞和 baked shadow mask 叠加规则仍未完成。

  4. 水面是序列纹理加近似深度。 反射、折射、岸线泡沫、法线、精确 blend/filter/wrap、透明 排序和水深标定仍缺。当前浅水透明度已被项目文档标记为未校准。

  5. .msenv 已完成参考字段解析、静态云层、双方向光分层和 texture-mode 六面天空 / 云层组合首版, 但完整环境表现仍缺。 当前已覆盖参考缺省值、IsDensityFilterBTextureRenderMode、 六面天空文件名、Reserved,并将真实云层贴图接入渐变天空的 sky_cover 或 texture-mode 的 有限云面 shader;云层渐变过渡、滤色实际合成、SpeedTree 精确风常量 / 随机风语义、lens flare、 日夜 / 天气插值、颜色曲线和原始 ambient/diffuse/specular 关系仍没有完整实现;树 proxy 只采用 参考的 0.2/0.0 缺省。

  6. 相机不是固定基线机位。 当前依赖脚本中的 FOV、yaw、pitch、距离和 AABB 射线,缺 CameraProcedure 等价模式、灵敏度持久化、平滑碰撞、树木遮挡淡出、目标 Actor 状态和可回放 的受控机位。见 PARITY-GAP.md:195

8. P1/P2UI、输入和本地化

  1. uiscript 可解析不等于控件语义完成。 当前约 80 个脚本可解析,但 ani_imagemark、 真实 grid/list/scroll、tooltip、item cursor、focus、anchor、modal、layer 和完整 nine-patch 语义仍缺。

  2. 大量窗口是手工 Panel。 交易、仓库、商店、公会、任务、好友、聊天和系统窗口仍大量使用 固定坐标和固定尺寸,没有完整复现原 Interface.MakeInterface().sub 资源、字体描边、 rect/anchor/z-index 和分辨率缩放。

  3. 帮助窗口已有首版,仍缺视觉等价验收。 help_ui.gd 已装载 helpwindow.py 的布局和帮助 文案,H 键及 ESC 菜单入口已接通;仍需 .sub 贴图、字体描边和真分辨率视觉验收。见 help_ui.gd

  4. 系统设置部分只是保存数值。 CPU/GPU tiling 不会改变实际渲染,BGM 更换按钮 disabled, 部分画质设置没有完整接入渲染侧;名字颜色本身已 1:1(增量 110)、头顶称号色 / 公会名 / 等级已 1:1(增量 111)、头顶聊天 / 信息气泡已 1:1(增量 112)、头顶名字「是否显示」/ 「总是显示名字」/ LALT 名字显示已 1:1(增量 113,name_show.gd + net_world._name_shown())、 「伤害数字」radio 已接到渲染侧(增量 114,damage_effect.gd + net_world.set_show_damagegame_option_ui.display_option_changed("show_damage")GC_DAMAGE_INFO / AddDamageEffect / ProcessDamage 整合),但 game_option 的 「empire 名字颜色」开关尚未接到 net_world.set_empire_name_mode(bool)(默认关)、 EnablePKTitle 开关尚未接到 net_world.set_pk_title_enabled(bool)(默认开)、 TextTail_SetLivingTime(气泡停留秒数)尚无运行期钩子(默认 5000)。见 system_option_ui.gd:143

  5. 鼠标物品态和光标已有首版,复杂窗口语义仍不完整。 mouse_controller.gd 已提供鼠标跟随 物品、投放到背包 / 快捷栏、无效区域 / 右键 / ESC / 失焦取消;cursor_manager.gd 已提供 NORMAL / TALK / ATTACK / PICK / BUY / SELL / ITEM 状态,窗口键盘事件不会穿透到世界。仍缺 CJK / .sub 光标资源、商店 / 交易等复杂窗口的逐项 mousemodule 行为和真机体验验收。

  6. 键位主入口已按参考端收敛,平台 / 上下文仍需验收。 game_scene.gd 已接数字 1–4 激活 本地槽 0–3、F1–F4 激活本地槽 47、Ctrl/Command+19 表情、LSHIFT 翻页、空格按住攻击、 Alt 显示名字、截图、帮助 / 坐骑 / 角色窗口 / 小地图 / 聊天 / 相机入口;剩余差距是窗口焦点 屏蔽、物品态和 macOS F1–F12 的系统 Fn / 功能键设置验收。键位重绑定、冲突检测及独立相机 灵敏度设置不是 ClientVS22 已有能力,不应伪装成参考端缺失项。

  7. CJK 输入不等价。 当前使用 Godot LineEdit,没有 PythonIME 候选窗、composition、 候选确认、输入法切换、特殊文本框行为、复数/性别规则和自动字体切换。

  8. 聊天和头顶信息不完整。 缺 notice/big notice、经验和金币日志、私聊窗口生命周期、历史 滚动、频道过滤、InsultChecker、屏幕空间血条。 empire 名字颜色 / PK 颜色 增量 110 已 1:1name_color.gd GetNameColorIndex / SetEmpireNameModenet_world 事件 + ~2Hz 兜底重染 Label3D.modulate,帝国名色默认关); 称号色 GetTitleColor / 公会名 / 头顶等级 增量 111 已 1:1text_tail.gd RefreshTextTail / UpdateTextTailLevel / EnablePKTitlenet_world 驱动 GuildTag / TitleTag / LevelTag); 头顶聊天 / 信息气泡 增量 112 已 1:1chat_tail.gd RegisterChatTail / RegisterInfoTail / TextTail_GetLivingTime / IsChatTextTail + RecvChatPacket 派发,net_world 每 vid 一条 "bubble" Label3D + 5s 硬删 + "名字: " 前缀剥离 + SHOUT·COMMAND 排除 + register_info_tail() seam ⑨:气泡与名字 Label3D 并存;名字随气泡下移 17px 增量 117 收口、子标签随名字 尾标纵向位移 增量 119 收口(ArrangeTextTail :211/:228/:258/:283 —— 三子标签屏幕 y 锚在 pTextTail->yname_tail_lift_delta / subtag_world_y + net_world SUBTAG_DEFAULT_Y 常量 + _reposition_name_tag 按同一 lift 平移 GuildTag / TitleTag / LevelTag),见下; 剩余 seam ⑦:子标签竖排 vs 参考屏幕空间逐像素水平排版 逐像素水平排版增量 121 收口,见下)。 总是显示名字 / LALT 名字显示 / 目标强显 增量 113 已 1:1name_show.gd IsAlwaysShowName / SetAlwaysShowNameFlag / __IsShowName / ShowAllTextTail 的 3500 平面距裁剪 / ShowCharacterTextTail 资格 / UpdateAllTextTail 的 chat bNameFlag 强显;net_world._name_shown() 四路 + game_option_ui display_option_changed 信号 + net_play.target_changedseam ⑩:悬停 chr.Pick() 强显 (增量 116 收口)/ 屏幕空间 ArrangeTextTail 防重叠(增量 120 收口)/ CanPickInstance 仅按死透近似未做)。 GC_DAMAGE_INFO 伤害数字整合 增量 114 已 1:1damage_effect.gd enum DamageFlag / RecvDamageInfoPacketbSelf / bTarget / damage >= 0 门 / AddDamageEffectIsShowDamage() 门 / ProcessDamage 分类(DODGE|BLOCK→MISS、CRITICAL·POISON 参考端 注释掉走普通数字、bSelf"damage_"!bTarget"nontarget_" 不渲染、否则 "target_"while(damage>0) 逐位 %10 / /10 index 0..7/ SetShowDamageFlagnet_world._on_damageAddDamageEffect_damage_queue_process_damage_queue() 每帧 pop 一条照 ProcessDamagegame_option_ui「伤害数字」radio 经 display_option_changednet_world.set_show_damage seam ⑪:单 Label3D 写整串 vs damagevalue dds 逐位特效 / 相机右向像素铺排、self SELFDAMAGESELFDAMAGE2 退化为落字水平抖动符号交替、暴击不再放大 / 变色(对齐该 build)、 _damage_queue 全局单条 vs 参考每实例一条)。 目标框(TargetBoard)收包分派 增量 115 已 1:1target_board.gd Action { CLOSE, CLOSE_IF_DIFFERENT, SET_HP, NONE }RecvTargetPacketPythonNetworkStreamPhaseGame.cpp:2424 判定树 —— 实例缺失→CloseTargetBoard、目标已死→不动框、IsPC()|IsBuilding()CloseTargetBoardIfDifferentCanViewTargetHPInstanceBase.cpp:2293 IsStone()|IsWoodenDoor()|IsEnemy())→SetHPTargetBoard、否则→CloseTargetBoard game.py SetHPTargetBoard 换 VID 先 ResetTargetBoard + SetEnemyVIDnet_play._on_target_info 由「任意有名字 VID 都显框」改为按判定树分派:PC / building 不显血条框(只在框上是别的 VID 时清旧框)、NPC / warp 关框、石头 / 敌对怪 SetHPTargetBoardhp% 钳 0..100 seam ⑫:poc 无 building / wooden-door / stone-door 实体类型(is_building 恒 false、木门分类缺)、 target_info 由 C++ EntityStore 侧抽出不改 C++ 收包、LCONTROL 悄悄话捷径未接 增量 122 已接(net_play._on_pick KIND_PC 分支轮询 whisper_on_ctrl_clickwhisper_requestedgame_scene KEY_CTRL_lcontrol_down + whisper_requestedchat.start_whisper is_building 实参恒 false、无独立 whisper dialog 窗口 = seam ⑫ 末尾))。 game.py OnRender Picking 块分派 + 悬停角色强显名字 增量 116 已 1:1pick_show.gd 纯静态第 27 个 canonicaltextTail.UpdateAllTextTail():1531)→ wndMgr.IsPickedWindow:1533)内 chr.Pick() 命中角色 → ShowCharacterTextTail:1537)、targetBoard.GetTargetVID()ShowCharacterTextTail:1539)、not __IsShowName()item.Pick():1543);门外 __IsShowName()ShowAllTextTail() + textTail.Pick(x, y):1552/:1554);末尾 UpdateShowingTextTail + ArrangeTextTail:1557/:1558)、PickingItemIndex != -1SelectItemName:1560);forced_name_vids[hover, target] 去重。poc 侧仿增量 113 的 _target_vid forced 路数给 net_world._name_shown() 加第 5 路 forced _hover_vid net_world.set_hover_vidplayer_controller._update_hover_cursor() 三出口喂 (压 UI / 悬停地面物品 → 0、射线命中 pickablevid meta、无命中 → 0+ hover_entity_changed 信号(值变去抖)+ game_scene 连线;seam ⑩:ArrangeTextTail 屏幕空间 去重叠本增量未做(只收口「悬停强显名字」一小块),已由增量 120 收口,见下)。 名字尾标随聊天气泡上移 17px 增量 117 已 1:1name_tail_layout.gd 纯静态第 28 个 canonical CPythonTextTail::UpdateShowingTextTailPythonTextTail.cpp:88)里角色名字尾标投影完屏幕坐标后 (:104),m_ChatTailMap.find(dwVirtualID) 命中(:106/:110)且那条 bNameFlag 为 TRUE:112 RegisterChatTail 的「聊天」气泡,非 RegisterInfoTail 的「信息」气泡)→ pTextTail->y = pChatTail->y - 17.0f:114)。name_follows_bubble / name_tail_screen_y (参考端字面 bubble_y 17/ name_tail_world_ypoc 3D:屏幕 y 向下 −17px → 世界 y 向上 bubble_world_y + 17*px_size);net_world._reposition_name_tag(n)_apply_chat_tail / _expire_chat_tails 调用,名字 Label3D 局部 y 在 NAME_TAG_DEFAULT_Y2.1)与「BUBBLE_TAG_Y 2.35 + 17*pixel_size」之间切换;seam ⑨ 收口 —— 剩余:气泡与名字仍是两个并存 Label3D)。 公会名 / 称号 / 等级子标签随名字尾标纵向位移 增量 119 已 1:1name_tail_layout.gd CPythonTextTail::ArrangeTextTailPythonTextTail.cpp:151)逐条角色名字尾标(:211)把公会名 (:228,pTextTail->y - c_fyGuildNamePosition 15px/ 称号(:258,同 pTextTail->y)/ 等级 (:283,同 y)三子标签屏幕 y 全锚在已被 UpdateShowingTextTail :114 顶过的 pTextTail->y 上; 新增 GUILD_NAME_ABOVE_NAME_PX := 15.0name_tail_lift_delta(= name_tail_world_y(...) - default_world_y)、subtag_world_y(= default_subtag_y + lift_delta);net_world 把三 子标签默认局部 y 提为 SUBTAG_DEFAULT_Y2.34 / 1.86 / 1.74)常量,_reposition_name_tag 顶完名字尾标后按同一 lift 平移 GuildTag / TitleTag / LevelTag,气泡过期 / 「信息」 尾标时回默认;name_tail_layout_test.gd + chat_test.gd 扩断言 + 全 68 个离线回归通过; seam ⑦ 收口 —— 剩余:子标签竖排 vs 参考屏幕空间逐像素水平排版仍近似 逐像素水平排版增量 121 收口,见下)。 掉落物名条屏幕空间去重叠 ArrangeTextTail item 块 + isIn 增量 120 已 1:1 text_tail_arrange.gd 纯静态第 29 个 canonicalCPythonTextTail::ArrangeTextTailm_ItemTextTailList 块(PythonTextTail.cpp:153-190)逐条 pInsertTextTailyTemp = 5 / LimitCount 上限 20,内层遍历全表跳过自己,isIn(pInsert, pCompare):489-507 标准 AABBxStart=-xSize/2-2 / yStart=-2 / xEnd=xSize/2+2 / yEnd=ySize,含边界 <=/>=)命中则 pInsert->y = pCompare->y + pCompare->yEnd + yTemp 并把内层游标重置回表头、++LimitCount arrange_item_tails(tails) 在共享 y 表上原地推、boxes_overlap = isInground_items.gd _arrange_tags() 把每条掉落名条世界锚点经 unproject_position 投到屏幕、按字符数 *7px 估框、 跑 arrange_item_tails 得下推屏幕 Δy 再按 Label3D.pixel_size 折回世界局部 y,无相机 / 少于 2 条回默认;game_sceneground_items.setupcamera_gettertext_tail_arrange_test.gd

    • p2b_test.gd 扩断言 + 全 69 个离线回归通过;seam ⑩ 收口 —— 剩余:名条框按字符数估宽 vs 参考 GetTextSize 精确像素 名条框宽 / 单行文本高已分别由增量 123 / 124 换真实字体度量 GetTextSize 的 w / h 1:1(见下),剩余:掉落名条 owner 未拆成独立 text instance yEnd += yOwnerSize + 4 带 owner 的掉落名条去重叠框已由增量 125 按 CPythonTextTail::SetItemTextTailOwnerPythonTextTail.cpp:706-737owner 独立 GetTextSizeyEnd += float(yOwnerSize + 4) / xStart = fMIN(xStart, -xOwnerSize/2-1) / xEnd = fMAX(xEnd, xOwnerSize/2+1))换 1:1text_tail_arrange.item_box_with_owner + text_metrics.string_width_like / string_height_like 把「名字」「owner + 所有格」两子串 分开度量再合成框),剩余:owner 未独立渲染成 CENTER 黄字 text instance、strOwnership 未接 ApplicationStringTableIDS_POSSESSIVE_MORPHENE、只对掉落 item 名条不含角色名条)。 公会名 / 称号 / 等级子标签逐像素水平排版 ArrangeTextTail 角色块 增量 121 已 1:1 name_tail_layout.gdCPythonTextTail::ArrangeTextTailPythonTextTail.cpp:211-320 非 EUROPE 分支)—— fxAdd = 0(无称号无等级 :236)/ 8(有称号 :242/ 4(仅等级 :288),名字 CENTER x + fxAdd:320)、称号 CENTER x iNameWidth/2 fxAdd:258)、等级 RIGHT 有称号 x iNameWidth/2 fxAdd iTitleWidth:282/ 仅等级 x iNameWidth/2 fxAdd:312), 公会名 / 徽记仅 pMarkInstance && pGuildNameInstance 才重定位(:220`iImageHalfSize = markW/2
    • 1.5:224,公会名x + iImageHalfSize:228 / 徽记x guildW/2 iImageHalfSize:227); 新增NAME_X_ADD_TITLE 8.0/NAME_X_ADD_LEVEL_ONLY 4.0/MARK_X_PAD 1.5/MARK_ABOVE_NAME_PX 26.0+name_x_shift/title_x_offset/level_x_offset/guild_name_x_offset/mark_x_offset/subtag_world_xpx_offset * px_size 线性、 符号保留左负右正);net_worldconst SUBTAG_CHAR_W_PX := 7.0(无字体度量 → 文本宽按 「字符数 * 7px」估)、_apply_text_tail_to尾部_layout_subtags_x(n, ttag, ltag)设名字 /TitleTag/LevelTag.position.x(无则回 0)、GuildTag.position.x恒 0(poc 无公会 徽记实体);纵向(增量 117/119 写.position.y)与横向(增量 121 写 .position.x)互不干扰; name_tail_layout_test.gd+「5. seam ⑦ 水平排版」段 +chat_test.gd扩真NetWorld 断言 + 全 69 个离线回归通过(无新增测试文件);seam ⑦ 收口 —— 剩余:poc 无公会徽记 mark 实体 (GuildTagx 恒 0)、~~文本宽字符数估 vsGetTextSize~~ 已由增量 123 换真实字体度量 GetTextSize1:1(见下)、三子标签仍竖排叠 x 偏移 vs 参考同一屏幕行、~~单行文本高仍 15px 估~~ 已由增量 124 换GetTextSize的 h 1:1(见下))。 ~~名条框 / 子标签文本宽按「字符数 * 约 7px」估 vs 参考GetTextSize 精确像素~~ 增量 123 已 1:1 (text_metrics.gd 纯静态第 30 个 canonicaltext_size(font, text, font_size)=GetTextSize 1:1Font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size),空 / null / font_size<=0Vector2.ZERO/ width_of(...)(有字体 → 精确像素、否则 → 字符数 * fallback_char_px/ label_width(label: Label3D, ...)font ?? ThemeDB.fallback_font); net_world._layout_subtags_xname_w/title_wground_items._arrange_tags的名条框w改走TextMetrics.label_widthSUBTAG_CHAR_W_PX/TAG_CHAR_W_PX(均 7.0)保留仅作无字体 兜底,name_tail_layout.gd/text_tail_arrange.gd 排布公式不动;text_metrics_test.gd第 30 个 canonical + 全 70 个离线回归通过(PASS=70 FAIL=0);剩余:~~单行文本高仍 15px 估(未走GetTextSize的 h)~~ 已由增量 124 收口(见下)、poc 无公会徽记 mark 实体、掉落名条为唯一 item 尾标来源)。 ~~名条框 / 子标签单行文本高按硬编码 15px 估 vs 参考GetTextSize 的 h~~ 增量 124 已 1:1 text_metrics.gd第 30 个 canonical 加const FALLBACK_LINE_H_PX := 15.0+height_of(font, text, font_size, fallback_line_px := 15.0)(空串 → 0;量得正高 → text_size(...).y;否则 → 回退)+ label_height(label: Label3D, fallback_line_px := 15.0) label.font ?? ThemeDB.fallback_font+label.font_sizenull → 0);参考端 item 尾标框 yEnd = ySizePythonTextTail.cpp:751/:812),ySize与增量 123 换过的xSize同出pTextInstance->GetTextSize(&xSize, &ySize):749/:809),单行文本的 ySize ≈ 字体行高 ascent + descent),与字形无关,Font.get_string_size(...).y是 1:1 对应物;ground_items._arrange_tags的名条框h由硬编码TAG_LINE_H_PX改走maxf(1.0, TextMetrics.label_height(tag, TAG_LINE_H_PX))TAG_LINE_H_PX15.0)保留仅作 无字体兜底;text_tail_arrange.gdbox_end_y(h)h,对齐参考 yEnd = ySize/ name_tail_layout.gd 收框高作入参、排布公式不动;text_metrics_test.gd扩「5. height_of / label_height」段 + 全 70 个离线回归通过(PASS=70 FAIL=0,无新增测试文件);剩余:掉落名条 owner 未拆成独立 text instance(参考端yEnd += yOwnerSize + 4:733-736)、poc 无公会徽记 mark 实体、掉落名条为唯一 item 尾标来源)。 ~~目标框「看他国玩家」前置过滤 + §8.4 名字色 radio 接线~~ 增量 118 已 1:1root/uitarget.py TargetBoard.Open(vid, name):189-193)在 RecvTargetPacket 判定之前:if not constInfo.GET_VIEW_OTHER_EMPIRE_PLAYER_TARGET_BOARD(): if not player.IsSameEmpire(vid): self.Hide(); returnconstinfo.VIEW_OTHER_EMPIRE_PLAYER_TARGET_BOARD = 1 默认;target_board.gd加纯静态hidden_as_other_empire(view_other_empire, same_empire) not view_other_empire and not same_empire(不按 is_pc 收窄 —— empire 0 的怪 / NPC / 石头恒同阵营故永不被挡);net_play_view_other_empire_target(默认 true+ set_view_other_empire_target(on)_on_target_infoclassifyif exists and hidden_as_other_empire(...): _clear_target(); return。同批把 §8.4 「名字色」radio(name_color_normal/name_color_empire)接进 game_scene._on_display_optionnet_world.set_empire_name_mode(v == 1)(算法增量 110 已 1:1)、「看他国玩家目标框」radio → net_play.set_view_other_empire_target(v == 1)game_scene开局两处display_value(...) == 1回填;修game_option_ui.gd target_board默认 0 → 1(对齐VIEW_OTHER_EMPIRE_PLAYER_TARGET_BOARD = 1)。§8.4 三个显示 radio + §8.6 LALT 至此全部接线完毕;seam ⑫(延续):poc 无 building / wooden-door 实体类型(is_building 恒 false)、~~LCONTROL悄悄话捷径未接~~(增量 122 已接:net_play._on_pickKIND_PC 分支 →whisper_requestedgame_scenechat.start_whisper`))。
  9. 任务导航和完整资源视觉仍未完成。 增量 101 已接入默认 / 服务端 icon、5 行滚动、倒计时和 QuestButton 事件参数;仍缺世界箭头、屏幕边缘指示、多列定位、真实 .sub 状态图和服务端 iStartTime 生命周期。见 quest_log.gd

9. P1/P2:音频和应用生命周期

  1. 地图 BGM 已有首版消费,仍缺真服 / 完整 MilesLib 验收。 GC_MAIN_CHARACTER3_BGMGC_MAIN_CHARACTER4_BGM_VOL 已路由到 bgm_changedbgm_director.gd 按服务端名称、 musicInfo.fieldMusic 回落和默认音量选曲;仍缺完整声音事件模型及真实 40250 地图验证。见 bgm_director.gd

  2. 后台 BGM 恢复已有首版,仍缺真机验收。 AppLifecycle 后台保存并静音音量,回前台恢复 音量和当前曲目状态,不再清空 _bgm_nameAndroid/iOS 前后台切换和系统回收 TCP 后的恢复 仍未在真机验证。见 app_lifecycle.gd

  3. 生命周期单一属主代码已完成,仍缺平台验收。 生产入口由 AppFlow 持有唯一 AppLifecycleGameScene 不再创建,M2Client 不再直接处理应用暂停 / 恢复通知;旧 login.gd 兼容启动器也保持自己的单一节点贯穿全流程。仍需真机验证暂停、恢复、内存告警和 断线重连。见 CLIENT-GAP-FIX.md §9.3。

  4. MilesLib 事件模型已迁移可复用子集,完整事件数据仍未迁移。 audio.gd 已实现 ambience / character / 普通 3D 三类入口、监听器位置 / 方向、200/1000 缩放、参考音量曲线、 角色声音的 5000cm + 0.3 秒限频、实例停止和音乐限幅淡出;增量 63 已按 MilesLib/Type.cpp 解析 .mss,以 60 FPS 帧实例接入 PlayerView / MobView 的动作声音。增量 67 又按 Area.cpp / MapType.cpp 接入 AreaAmbienceData + .pra,并实现 LOOP / ONCE / STEP 和区域卸载停止; 增量 68/72 又按 MapUtil.cpp / SkyBox.cpp 补齐 .msenv 解析字段与参考缺省值,并将真实云层贴图接入 ProceduralSkyMaterial.sky_cover 或 texture-mode sky shader,未等价承载的参数保存在 WorldEnvironment 元数据。EffectLib 的 .mde 基础网格、SimpleLight 范围 / 生命周期和 MovingType 基础曲线已接入。仍缺 TexAni 随机 / 每粒子语义、SpeedTree、完整 .msenv 环境渲染 / 天气联动、完整 UpdateSoundData/UpdateSoundInstance 节点数据、全部武器 / 技能 / 怪物 / NPC 资源逐项验收和真实设备听感。参考:ClientVS22/MilesLib/SoundManager.h

10. 发布和验证问题

  1. Mac App 自包含脚本已修复,但完整产物尚未验收。 build-macos-client.sh 现在把 assetsbgm 复制到 Contents/ResourcesMT_ASSETS 只作为开发覆盖;仍需用完整导出产物跨机启动, 不能把脚本通过等同于 App 验收。

  2. 双架构构建入口已补齐,当前产物仍未证明 Universal。 build.sh / build-macos-client.sh 支持 MT_MAC_ARCHES=universal 并在打包时用 lipo 拒绝单架构扩展;当前已检查的扩展仍为 arm64, Intel Mac 兼容性待实际构建与运行验收。

  3. 签名入口已补齐,默认仍是 Ad-hoc。 脚本支持 CODESIGN_IDENTITY,按嵌套扩展先于 App 的 顺序签名并启用 hardened runtime 选项;当前没有 Developer ID、notarization 或 stapled ticket 证据,不能当作正式分发包。

  4. Demo 默认账号密码已移除。 AppFlow、登录和选人页只从 MT_ACCOUNT / MT_PASSWORD 读取,自动登录还要求 MT_AUTOLOGIN=1;正式 PCK / 导出包仍应加入字符串扫描门禁。

  5. 补丁器、Logo、Movie、Web、Discord 和反作弊策略没有形成发布决策。 当前只是“能启动 并进游戏”的工程入口,不是完整的 ClientVS22 发布外壳。

  6. 缺少真实验收门禁。 当前已有单元测试、loopback 和部分单会话验证,但仍缺:完整 40250 真服矩阵、双账号交易/组队/PVP、断线重连、跨服换图、纯 pack 运行、Intel Mac、受控机位 截图和移动端真机性能验证。

当前结论

当前版本应继续定义为:可登录、可进入地图、具备部分玩法和表现的核心功能纵切; 连接骨架和换图清理已经开始实现,但尚未完成真实双连接、换图和完整生命周期验收。 它还不能定义为:ClientVS22 的跨平台等价客户端

当前已完成 G0 公共接口的首版冻结,正在按附录 C 的并行波 A 持续交付;增量 49–128 的完成项、 自动化证据和剩余项以本文顶部看板及 CLIENT-GAP-FIX.md“实施进度 / 进度看板 / C.5 批次记录”为准。W3 已完成远端 PC 真模型工厂、任务日志五行 / 滚动 / 事件和 钓鱼输入 / 水面扫描 / 反馈消息首版、坐骑攻击等级门、观战者小地图插值和副本目的地罗盘首版;W4 已完成环境格式解析、静态云层和 texture-mode 云层组合及 EffectLib emitter cycle 生命周期首版接入,但均不代表完整玩法 / 环境渲染完成。 后续仍需要完成以下闭环:

  1. 完成 Classic 直连/Auth/Warp 的真实双连接验收,并补齐阶段清理和回登录后的完整生命周期;
  2. 未知 VID、VID 重用、事件缓存和完整实体生命周期;
  3. 真实 Actor 战斗状态机、动作命中时序和技能合法性;
  4. 物品拖放、装备规则、Safebox、交易和快捷栏事务状态;
  5. 所有资源调用统一切换到 AssetSource,并验证纯 pack 运行;
  6. ClientVS22 的 UI、EffectLib、SpeedTree、MilesLib 模块建立逐项验收;
  7. 完成受控地图/机位/角色/时间/天气/分辨率契约,再进行截图签核;
  8. 完成 Mac 自包含资源、双架构扩展、正式签名和 notarization。