Files
mtgodot-poc/docs/CLIENT-ROADMAP.md
T
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
  phases, EntityStore world model, ~all GC/CG headers. char create/delete,
  private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
  quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
  char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
  token), system-option + game-option + ESC system menu, private-shop 39-grid,
  party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
  dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.

Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.

Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).

ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
2026-08-31 20:02:12 +09:00

353 lines
60 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 客户端玩法开发计划
> 把 [`CLIENT-GAP.md`](./CLIENT-GAP.md) 的缺口排成可执行的阶段计划。
> 网络栈进度见 [`CLIENT-PORT.md`](./CLIENT-PORT.md),渲染见 [`PARITY-GAP.md`](./PARITY-GAP.md) / [`BACKLOG.md`](./BACKLOG.md)。
>
> 规模:S ≤ 3 天 · M ≈ 1 周 · L ≈ 2–3 周 · XL ≈ 1 月+
> 标记:🌐 需服务器端到端 · 💻 离线可做(mock server / 合成封包 / 假 client
> 更新日期:2026-08-30 · 自用,无发布门禁
> **P0P10 首版全部完成**`192.168.21.203` 已联通,`net_e2e` 端到端到 PHASE_GAME。里程碑见文末。
## 已就绪的地基(这些不重复做)
- net 栈:握手 / auth / login / select / game 相位,`M2Client` 编排 + 生命周期
- `EntityStore`:实体模型 + 插值 + 战斗 / 状态封包(points / vitals / damage / motion / stun / dead / target
- `NetWorld``net_world.gd`):实体→场景桥,占位胶囊 + HP 条 + 伤害飘字 + 死亡倒地
- `M2Client` GDScript 面:`move/attack/set_target/say` + `get_entities/get_entity/get_points/get_target` + 12 个信号
- 渲染节点:`Metin2Model`(骨骼 + crossfade + 武器挂点 + LOD + 换肤)、`Metin2World``Metin2AnimPlayer`
- 资产:`mtproto`item/mob_proto)、`mtpack`eterpack)、`locale.gd``audio.gd`
## 依赖关系
```mermaid
graph TD
P0["P0 闭环骨架 💻"] --> P1["P1 UI 工具层 💻"]
P0 --> P4["P4 战斗表现 🌐"]
P1 --> P2["P2 物品/背包/装备 🌐"]
P1 --> P3["P3 聊天 + TextTail 💻"]
P1 --> P8["P8 社交/商店/仓库 🌐"]
P5["P5 EffectLib 特效引擎 💻→🌐"] --> P4
P5 --> P6["P6 技能系统 🌐"]
P2 --> P6
P1 --> P6
P1 --> P7["P7 任务/NPC 🌐"]
P2 --> P9["P9 世界系统 🌐"]
P0 --> P10["P10 网络补全 🌐"]
```
P1UI 工具层)是 P2/P3/P6/P7/P8 的共同前置。P5(特效)可**任何时候并行起**
`.mse` 解析器纯离线)。
---
## P0 — 闭环骨架(M,💻)—— ✅ 离线部分完成,待服务器端到端
**目标**:现有骨架变成「点怪→走过去→挥→掉血→切目标」的可玩手感,无新系统。
| 任务 | 规模 | 状态 | 落点 |
|---|---|---|---|
| 输入接线 | S | ✅ | `net_play.gd``pc.moved``M2Client.move(FUNC_MOVE)`(节流 >1m / >0.2s),`anim_state("wait")``move(FUNC_WAIT)`(一次),`pc.target_selected``set_target(vid)`vid 取节点 `vid` meta |
| 本地预测 + 服务器校正 | S | ✅ | `net_play._on_net_moved`:收到自己 vid 的 `entity_moved` → 差 >6m 瞬移、>0.3m lerp 0.25,高度本地贴地不信服务器 z |
| HUD 数据绑定 | S | ✅ | `hud.gd``set_vitals/set_exp/set_level/set_target/clear_target``net_play``points_changed`/`vitals_changed`。血条 / 蓝条 / 经验细条 / 等级 label |
| 目标血条 | S | ✅ | `hud._build_target_panel`(顶部居中,名字 + 180px 血条);`net_play``target_info(vid,hp%)` + 目标 `vitals_changed` 刷新;目标死亡 / despawn → `clear_target` |
| 攻击动作驱动 | S | 🟡 | `net_play`:进 2.5m 距离按 `attack_period`(0.6s) 自动 `attack(0, vid)` + 面向目标。真模型动作切换靠 `net_world``set_model_factory` + `set_anim_state("attack")`P4 细化 motion index / 攻速) |
| 坐标 / 朝向换算 | S | ✅ | Godot 米 ↔ 服务器 cm`position_to_godot` 逆)、Godot yaw ↔ Metin2 罗盘度(`net_world` 逆)|
| net_world 去重主角节点 | S | ✅ | `net_world.set_local_vid(vid)`:本地玩家由 `player_controller` 的节点代表,`net_world` 不再画一个 |
| 场景组装 | S | ✅ | `game_scene.gd`(简版):`setup(client, assets_root, map_path?)``Metin2World`(load 失败则平地兜底)+ 占位玩家 + `GameCamera` + `PlayerController` + `NetWorld` + `HUD` + `NetPlay` + `Audio` + `AppLifecycle``entity_main_set` → 主角定位出生点。`login.gd``entered_game` → 隐藏登录层 + 实例化(`--auto` 自动选槽 0)。`set_player_model()` / `set_entity_model_factory()` 留给 P2 |
**已验证**
- `netplay_test.gd` —— move 带正确 server cm + 节流、FUNC_WAIT 单发、set_target、攻击距离内自动 attack + 节流、points→HUD、远程校正瞬移、target_info→HUD、死亡清目标。
- `gamescene_test.gd` —— 装配 + 子系统接线 + 实体镜像(主角不重复画)+ 出生点定位(无地图兜底)。
- 项目导入无错,`netbridge_test` 仍过。
**未完成(仅剩服务器相关)**
- **端到端**:走真 `GC_MOVE`/`GC_PLAYER_POINTS`/`GC_TARGET_INFO``192.168.21.203` 可达。net 栈就绪,改 IP 即测。
- 攻击动作用真 `Metin2Model``set_player_model` / `model_factory` 把 race→gr2,P2 装备系统会带出来)。
- `login → charselect → game` 正式串场(现在 `--auto` 直接选槽 0`charselect.gd` 已存在,接一下即可,属 P10)。
## P1 — UI 工具层(L,💻)—— 🟡 核心完成(装载器打通全量语料)
**目标**:一层 Godot `Control` 封装对齐 `EterPythonLib`,让 P2+ 的窗口可批量搬。
| 任务 | 规模 | 状态 | 落点 |
|---|---|---|---|
| **uiscript 装载器** | M | ✅ | `ui/uiscript.gd`:解析原始 `assets/uiscript/*.py``window={...}` dict(子集解释器:str/int/float、NAME、`uiScriptLocale.X`、算术 `+ - * /``%` 格式化、相邻字符串拼接、`SCREEN_WIDTH/HEIGHT` + 文件顶层常量、`#`/`##` 注释)。防跑飞 guard。**实测 80/80 个真实 uiscript 文件解析 + 构建全绿** |
| 控件封装 | M | 🟡 | `ui/ui_build.gd`dict → Control 树,覆盖 ~20 种 typewindow/board/thinboard/board_with_titlebar/titlebar/text/button/toggle_button/radio_button/image/expanded_image/editline/line/listbox/gauge/grid_table/slotbar/sliderbar/scrollbar+ `horizontal_align`/`vertical_align`/`text_color`(0xAARRGGBB)/按钮 default/over/down 贴图。`ui/ui_assets.gd``.sub`/`.tga`/`.png`/`.jpg``.dds` 暂不支持→降级主题)。九宫格复用 `ui_kit.board`。⬜ 少见 typeani_image 动画、mark、grid_table 内容填充、真 scrollbar 行为)按窗口逐个细化 |
| 窗口管理器 | S | ✅ | `ui/ui_manager.gd``CanvasLayer` layer=10):窗口栈、`open/close/close_top/top`、ESC 关顶层、modal 变暗层、标题栏(`titlebar` / `board_with_titlebar` / meta `is_titlebar`)拖动、点击置顶。`open_script(path, assets)` 一步解析+构建+开 |
| 通用弹窗 | S | ✅ | `ui/dialogs.gd``confirm(text,on_ok,on_cancel)` / `alert(text)` / `input(prompt,on_submit,default)`,走 `UiManager.open(modal=true)` |
| 拖拽层(物品图标跟光标 + 落点命中 slot) | S | ⬜ | 归 P2(背包才用到)|
**已验证**`ui_test.gd` —— 解析器(算术 710、hex 色、locale 解析、常量、坏输入→{})、
构建器(节点类型 / 尺寸 / 位置 / center 对齐数学 / board 标题)、窗口管理器(栈 / 置顶 /
close_top / modal dim)、弹窗(confirm 打开 + modal + on_ok + 关闭)、真实文件
`popupdialog.py` name/children/.sub 按钮 + 构建)。全过。全量语料 sweep 80/80。
## P2 — 物品 / 背包 / 装备(L,🌐 解析+UI 💻)—— 🟢 主体完成(离线部分),端到端待服务器
**目标**:捡东西、开背包、穿脱装备(穿装备换模型 → 解锁 `PARITY §2.7` 高光 / `§2.3` 时装)。
| 任务 | 规模 | 状态 | 落点 |
|---|---|---|---|
| `GC_ITEM_*` 封包 + 模型 | S | ✅ | `wire.h``ItemPos`(3) / `ItemAttr`(3) + `GC_ITEM_SET`(54) / `DEL`(7) / `UPDATE`(41) / `USE`(19) / `GROUND_ADD`(24) / `GROUND_DEL`(8) / `GET`(75) + `CG_ITEM_MOVE`(11) / `USE`(7) / `DROP`(11) / `PICKUP`(8),全 `static_assert``EWindows` 常量。`EntityStore``m_inventory[90]` / `m_equipment[11]` / `m_ground` map + `apply()` 全部 7 包 + `drain_inv/drain_ground/drain_item_events``game_client.h` 路由 + `send_item_move/use/drop/pickup` |
| `Metin2Proto` 节点 | S | ✅ | 新 GDExtension 类:`load_item_proto/load_mob_proto` → vnum→index map`item(vnum)` / `mob(vnum)` → Dictname / locale_name / type / sub_type / wear_flags / prices / specular …)。实测载入 5748 真实 item,`item(1)`=Yang、`item(19)`=Sword+9 |
| `M2Client` 物品面 | S | ✅ | `get_inventory()` / `get_equipment()` / `get_item(win,cell)` / `get_ground_items()`(坐标转 Godot 米)+ `move_item/use_item/drop_item/pickup_item`;信号 `inventory_changed(win,cell)` / `ground_item_added(Dict)` / `ground_item_removed(vid)` / `item_picked_up(vnum,count,from)` / `item_used(vnum)` |
| 背包 / 装备窗 | M | ✅ | `ui/inventory_ui.gd``ui_manager.open_script("inventorywindow.py")`P1 装载器)→ 索引格子 → `M2Client.get_inventory/get_equipment` 填(icon `icon/item/<vnum>.tga` 或名字文本 + 数量 + tooltip 用 `Metin2Proto` 名字)→ `inventory_changed` 刷新 → 右键 `use_item` / 拖到别格 `move_item``ui/ui_build.gd` 加 slot-cell 构建(`grid_table`/`slot`/`slotbar``start_index`/`x_count`/`y_count`/`x_step`/`y_step` 网格 + 显式 `"slot"` 元组)。接进 `game_scene.gd`(I 键开关)。UI 格子编号:0..89 背包 / 90+ 装备(wear=idx-90) |
| item_list.txt 读取 | S | ✅ | `ui/item_list.gd`:解析 `locale/locale/common/item_list.txt`TSVvnum→type/icon/model)。用于背包图标(替代 vnum 猜路径)+ 武器模型解析 |
| 本地玩家真模型 | M | ✅ | `ui/player_view.gd`PlayerViewNode3D):`build(assets, race)` —— race 0..7 → class + 性别 → `pc(2)/<class>/<class>_novice.gr2` 身体 + `hair/` + `Metin2AnimPlayer``set_anim_state("wait/walk/run/attack/dead")``general/<s>.msa`crossfade)。`_set` 转发 `weapon_gr2`/`gr2_path`/`hair_*` 给内部 `Metin2Model``game_scene._on_main_set` 首次拿到 race 时 `set_player_model(pv)` + 接 `pc.anim_state` |
| 穿脱 → 换模型 | M | ✅ | `ui/equip_model.gd``inventory_changed(EQUIPMENT)``get_equipment()`**WEAR_WEAPON(4)** → `item_list.model(vnum)``player.set("weapon_gr2", …)`**WEAR_SHIELD(10)** → `shield_gr2``Metin2Model` 新增第二个刚体挂点,默认骨 `Bip01 L Hand` —— PC 骨架没 `equip_left_hand`,共用 `_load_attach` 抽出的加载器);**WEAR_BODY(0)** → `ui/race_spec.gd``<race>.msm``ShapeData`/`HairData` 组 → model + SourceSkin/TargetSkin)→ `armor_shape_of(vnum)`(默认 =vnum,可覆写)→ `gr2_path` 换整模 + 换肤;**WEAR_HEAD(1)** 有模型的头盔 → 覆盖 `hair_gr2` 槽(Metin2 约定:头防替发型),拆下回 `parts[CHR_EQUIPPART_HAIR]``race_spec.hair(idx)`。头发也随 `entity_info`GC_CHARACTER_UPDATE 改 parts)刷新。`equip_model_test`:武器 / 盾装拆、`armor_model_map` 注入、头盔覆盖 hair 槽 / 拆回 parts 发型;`race_spec_test`:真 `warrior.msm` 26 shape、shape 9 → `warrior_cheongrin.gr2` + 换肤 |
| 地面掉落物 + 拾取 | S | ✅ | `ui/ground_items.gd``ground_item_added(Dict)` → 旋转小方块 + billboard 名条(`Metin2Proto` locale 名);`_process` 近距离名条变绿;`try_pickup()`Z 键)→ 范围内最近的 → `pickup_item(vid)``ground_item_removed` → free。接进 `game_scene.gd` |
| belt inventory / 快捷栏 | S | ⬜ | `beltinventorywindow.py` `taskbar.py` |
**已验证**8 个 GDScript 测试套件全绿 + `ctest 9/9` + uiscript sweep 80/80 + iOS 编过):
`netbridge_test`(物品面 + `Metin2Proto` 5748 item)·
`inventory_ui_test`(真 `inventorywindow.py` 开窗 / 填充 / tooltip / 刷新 / use / move)·
`equip_model_test``item_list.txt` + 武器 vnum→真实 gr2)·
`p2b_test`(地面掉落 spawn/名条/拾取 + PlayerView 真 warrior 模型 + `set_anim_state`)·
`race_spec_test``warrior.msm` 解析 26 shape + 装 shape 9→真实 cheongrin.gr2 + 换肤)。
**未完成**belt / 快捷栏、`.epk` 内 icon(需 `AssetSource` → GDScript)、
拖动图标跟手视觉、`armor_shape_of` 用真 `item_proto` value[3](现 =vnum)、
**端到端**(真服务器 `192.168.21.203`)。
## P3 — 聊天 + TextTail(M,💻)—— 🟢 主体完成(离线部分)
**目标**:完整聊天窗 + 头顶信息。
| 任务 | 规模 | 状态 | 落点 |
|---|---|---|---|
| `GC_WHISPER` / `CG_WHISPER` | S | ✅ | `wire.h``GCWhisper`(70) / `CGWhisper`(69) + `EChatType` / `WHISPER_TYPE_*` 常量,`static_assert``EntityStore::apply(GC_WHISPER)``ChatMsg{type=WHISPER, from, sub, text}``ChatMsg``from`/`sub` 字段)。`game_client.h` 路由 + `send_whisper(name,text)``M2Client``whisper(to,text)` 方法 + `whisper_received(sub,from,text)` 信号(与方法不同名,避 GDScript 冲突)|
| 聊天窗 | M | ✅ | `ui/chat_ui.gd`:底部面板,4 标签(全部 / 私聊 / 系统 / 战斗)+ `RichTextLabel` 滚动 + `LineEdit` 输入。前缀 `/w 名 话``whisper` / `/g`→GUILD / `/p`→PARTY / `/s`→SHOUT / 其它→TALKING。消费 `chat`(按 type 分标签,名字取 `get_entity(vid).name`/ `whisper_received` / `item_picked_up`。每类型配色(bbcode)。接进 `game_scene.gd`Enter 聚焦输入) |
| 聊天气泡 | S | ✅ | `net_world.gd._bubble`:普通 / 队伍 / 公会 / 喊话 → 实体头顶 `Label3D`(3s + 1s 淡出);本地玩家 vid → `main_bubble` 信号 → `game_scene._player_bubble``player` 头顶 |
| 头顶信息完善 | S | 🟡 | 名字 + HP 条 + 伤害飘字已有(P2);⬜ 称号 / 公会名 / 目标高亮框 |
| 系统消息 / 战斗日志 | S | 🟡 | 拾取 → 系统 + 战斗标签;⬜ `GC_DAMAGE_INFO` / 经验 / 金钱变化的文本行(signal 已到位,接一下即可) |
**已验证**`net.entity_store` 扩展(whisper 包解析);`chat_test.gd`(4 标签路由:talking→全部带名 / info→系统+全部 / whisper→私聊+全部 / 系统 whisper→`[系统]` / 拾取→系统+战斗;前缀解析 `/w` `/g` `/s` + 本地回显;气泡:实体头顶节点 + 本地 vid→`main_bubble`)。9 个 GDScript 测试套件全绿,ctest 9/9iOS 编过。
## P4 — 战斗表现完善(L,🌐 表现 💻)—— 🟡 大部分完成(离线部分)
**目标**:战斗「有肉感」——拖尾、硬直、连段、状态、死亡复活、镜头。
| 任务 | 规模 | 状态 | 落点 |
|---|---|---|---|
| affect 封包 + 图标条 | M | ✅ | `wire.h``AffectElement`(21) / `GCAffectAdd`(25) / `GCAffectRemove`(9)`static_assert``EntityStore``m_affects` map + `drain_affects()` + `affects()``M2Client``get_affects()` + `affect_added(Dict)` / `affect_removed(type)` 信号。`hud.set_affects(array)` —— 顶部图标条(色块 by type + tooltip 显 type/point/value/duration)。接进 `game_scene` |
| 受击硬直 / 锁输入 | S | ✅ | `net_play._on_damage`:自己是 victim 且非 `DAMAGE_DODGE``_hitstun_until = now + 0.32s``is_stunned()` 期间 `_process` 不发攻击 + `pc.frozen = true``player_controller._process` 见 frozen 即 `anim_state("wait")` 早退)+ `player_view.set_anim_state("damage")` |
| combo 连段 | S | ✅ | `net_play._process` 攻击:`COMBO_WINDOW`(0.9s) 内连续攻击 → `_combo` 0→1→2 循环,`attack(attack_motion + _combo, vid)` + `set_anim_state("combo"/"attack")` |
| 相机抖动 | S | ✅ | `game_camera.shake(strength, decay)` —— `_process` 里加随机偏移,指数衰减。`net_play._on_damage` 命中 0.04m / 暴击 0.08m |
| 死亡 / 复活流程 | S | ✅ | `ui/death_ui.gd``entity_dead(main)``phase_changed("dead")` → 灰屏 + 窗(在此复活 / 回城复活 → `say(0, "/restart_here"/"/restart_town")`,同客户端走 quest 命令);`vitals_changed` hp>0 → 自动关。接进 `game_scene` |
| 相机锁定目标视角 | S | ⬜ | `game_camera` 锁定 target 时 yaw 跟随;低优先 |
| 武器挥砍拖尾 | S | ⬜ | 需 `Metin2Model` 暴露每帧武器骨骼世界变换 → `ImmediateMesh` 条带。纯视觉,延后 |
**已验证**`net.entity_store` 扩展(affect add/remove);`combat_fx_test.gd`(受击→硬直+抖屏+damage 动作,闪避不硬直,他人受击不影响;combo 0→1→2 循环;`hud.set_affects`;死亡窗弹出 + 在此复活→`say("/restart_here")` + 复活自动关)。10 个 GDScript 测试套件全绿,ctest 9/9iOS 编过。
## P5 — EffectLib 特效引擎(XL,💻 起 → 🌐 用)⏭ 独立立项 —— 🟡 首版打通
**目标**`.mse` 特效脚本 → Godot。见 `PARITY-GAP.md §1.2`
| 任务 | 规模 | 状态 | 落点 |
|---|---|---|---|
| `.mse` 解析器 | M | ✅ | `fx/mse.gd`CTextFileLoader 花括号树 + `List` 行表解析。`parse_file``{bsphere_r, bsphere_pos, particles:[{start_time, position, emitter, particle}], meshes:[...]}`。**实测真 `geompung_3_sword.mse` → 6 particle**。`.mse` 全资产集只有 3 种 groupParticle(514)/Mesh(22)/MeshElement,无 SimpleLight |
| Particle → GPUParticles3D | M | ✅ | `fx/effect_player.gd`:每 Particle 组 → `GPUParticles3D` + `ParticleProcessMaterial`。映射:`EmitterShape`(0点/3球+`EmittingRadius`)、`MaxEmissionCount`→amount、`TimeEventLifeTime`→lifetime、`TimeEventEmittingVelocity`→速度、`TimeEventGravity`→重力、`TimeEventSizeX`→scale、`TimeEventScaleX``scale_curve`(Curve)、`TimeEventColorRGB`+`TimeEventAlpha``color`+`color_ramp`(Gradient)、`RotationSpeed`→angular_velocity。绘制:`QuadMesh` + `StandardMaterial3D``BillboardType`→billboard_mode、`Src/DestBlendType`→blend_modedst=2→ADD)、程序化径向渐变代 `.dds`|
| EffectMesh / Light / 时间轴 | M | 🟡 | 时间轴:每组按 `StartTime` 延迟 `create_timer` 起 emitting`one_shot` 到寿命自动 `queue_free`。Mesh 组占位小方块(`.mde` 解码器待做)。Light:本资产集未用 |
| 挂点系统 | S | 🟡 | `spawn(name, parent_node)` 挂到任意节点下(跟随变换);`spawn_at(name, world, pos)` 世界定点。绑骨骼需 `Metin2Model` 暴露骨骼节点,待做 |
| 特效索引 + 服务器触发 | S | ✅ | `fx/effect_registry.gd`:名字 / 相对路径 / 盘符路径 → `.mse`(散包扫 + 裸名递归找 depth 7)+ 解析缓存。`wire.h` `GC_SPECIAL_EFFECT`(9) / `GC_SPECIFIC_EFFECT`(136) + `EntityStore::drain_effect_cues()` + `M2Client.effect_cue(vid, name, special)` 信号 → `game_scene` 在实体上 `fx.spawn(name)` |
**已验证**`net.entity_store` 扩展(specific/special effect cue 解析);`fx_test.gd``.mse` 解析:bsphere / 2 particle / List 行表 / 引号枚举 / 嵌套 groupEffectPlayer2 GPUParticles3D + amount + 球形发射 + 加法混合 + start_time meta + play()→emittingregistry:解析真 `.mse` 6 particle + spawn 挂载)。11 个 GDScript 测试套件全绿,ctest 9/9iOS 编过。
**未完成**`.dds` 粒子纹理(GDScript 不解 DDS,需暴露 `Metin2Image.load_dds` 静态方法)、`.mde` mesh 解码、SimpleLight(本资产集未用)、绑骨骼挂点、`GC_SPECIAL_EFFECT` 的 id→路径表、时间轴的 `MovingType` 位置动画、TexAni 帧动画。
## P6 — 技能系统(L,🌐)—— 🟢 主体完成(离线部分)
依赖 P1(技能窗)+ P2(技能书物品)+ P5(技能特效)。
| 任务 | 规模 | 状态 | 落点 |
|---|---|---|---|
| 技能表 | S | ✅ | `ui/skill_table.gd`:解析 `skilldesc.txt`(**列偏移修正** —— 0-idxid0 job1 name2-4 desc5-8 (9空) **attrs10** weapon11 **motion_name12** **motion_idx13** grades14;旧代码整体差 1 列,`motion_idx` 一直是 0)。`entry`/`name_of`/`motion_idx_of`/`is_attack`/`is_passive`/`is_toggle`/`can_level_up`/`category_of`/`for_category``for_job` = 别名)。JOB 列即分类:WARRIOR/ASSASSIN/SURA/SHAMAN(主动) · SUPPORT(辅助/被动) · HORSE · GUILD |
| 技能封包 | S | ✅ | `wire.h``GCSkillLevel`(259flat u8[255]) / `GCSkillCooltimeEnd`(5) / `GCCreateFly`(13)`static_assert``EntityStore``m_skills[255]` + `skills_dirty()` + `drain_cooldown_ends()` + `drain_fly_cues()``M2Client``get_skills()`[{id,level}]+ `skills_changed` / `skill_cooldown_end(skill)` / `fly_cue(type,start,end)` 信号 |
| 技能窗 + 加点 | M | ✅ | `ui/skill_ui.gd`K 键):**三个分类页** `[主动][辅助][坐骑]`(→ `for_category(job/SUPPORT/HORSE)`)。每行 = 名 + `Lv X`(+ `M`/`G`/`P``master_type`) + `[被动]`/`[切换]` 标签 + `[]``is_passive``CANNOT_LEVEL_UP` 时无 +)。```M2Client.skill_up(id)`。点名 → `drag_skill_id``skills_changed` 刷新等级 + master |
| 快捷栏施放 + 冷却 | S | ✅ | `ui/quickbar.gd`(数字键 1-8):`assign(slot, "skill"/"item", id)`。技能 → `M2Client.cast_skill(motion_idx, heading, x, y)` + 本地 2s 冷却占位;`GC_SKILL_COOLTIME_END` 提前解锁。冷却遮罩自上而下扫。`restore_from_server()``get_quickslots()` 恢复。施放时 emit `skill_activated(id)``game_scene` 播技能特效 |
| 技能特效表 | S | ✅ | `fx/skill_fx.gd``skill_effect_name(id, master)` = `<skilldesc.motion_name>_<clamp(2+master,2,4)>``PC/ymir work/pc/<cls>/effect/*.mse``_blow`/`_head`/… 子特效存在就一起播);`GC_SPECIAL_EFFECT` 内建 id → `SPECIAL_FX` 表(`drugup_red/blue/green``firecracker_1``fail` …)。`game_scene``effect_cue``special` 分支 + `quickbar.skill_activated` |
| 飞行道具 | S | 🟡 | `net_world._on_fly``GC_CREATE_FLY` → 从 start 实体飞向 end 实体的小发光球(tween,按距离算时长)。⬜ 真弹道 mesh / 命中特效 / `GC_FLY_TARGETING` 追踪 |
**已验证**`net.entity_store` 扩展(skill level / cooltime end / fly cue);`skill_test.gd`(真 `skilldesc.txt` >30 skill + WARRIOR 列 + skill 1 job/name/motion_idxskill 窗行数 = 职业技能数 + +→`skill_up(1)` + Lv 5 显示;quickbar assign→activate→`cast_skill` 用正确 motion_idx + 冷却中不重发 + `cooldown_end` 后可再发)。12 个 GDScript 测试套件全绿,ctest 9/9iOS 编过。
**未完成**:真弹道、加点等级公式 / 消耗校验(服务器管)。
被动 / 支援技能树 ✅(skilldesc 列偏移修 + skill_ui 三分类页 + `is_passive`/`master_type` 显示)。
技能特效表 ✅:`fx/skill_fx.gd` —— skill id + master → `<motion_name>_<2+master夹2..4>``skilldesc` col13 的
motion_name 就是特效基名,上一轮列偏移修好后自然接上);子特效 `_blow`/`_head`/`_foot`/`_yong` 存在就一起播。
`GC_SPECIAL_EFFECT` 内建 id → `SPECIAL_FX` 小表(`SE_HPUP_RED`=1→`drugup_red``SE_AUTO_HPUP`=19、
`SE_FAIL`=12→`fail` …)。`quickbar.skill_activated` 信号 → `game_scene` 播;`effect_cue``special` 分支
接上。实测真资产:skill 1/5/17/31/92/93 + SE 1/2/3/9/12/19/20 全部 resolve 到真 `.mse`
## P7 — 任务 / NPCL,🌐)
| 任务 | 规模 | 状态 | 落点 |
|---|---|---|---|
| NPC 交互 | S | ✅ | `wire.h``CGOnClick`(8)、`CG_ON_CLICK`(0x0A02)。`M2Client.click_npc(vid)``send_on_click``net_play._on_pick``ch_type ∈ {1,3,4}`NPC/石头/warp)→ `click_npc(vid)` 而非攻击 |
| 对话框 | 🟡 | 🟡 | `wire.h``GCScript`(7`skin`+`src_size`)、`GCQuestConfirm`(77)、`CGScriptAnswer`(5) / `CGScriptButton`(8) / `CGQuestInputString`(69) / `CGQuestConfirm`(9)`static_assert``EntityStore``ScriptCue{skin,text}` / `ConfirmCue{msg,timeout,pid}` + `drain_scripts()` / `drain_confirms()``ui/quest_dialog.gd``parse_script``[ENTER]`/`[CLEAR]`/`[NEXT]`/`[DONE]`/`[QUESTION arg(..)]`/`[LETTER]` → 文本 + 选项 + 继续钮;选项 → `script_answer(idx)`,无选项 → `script_answer(255)``GC_QUEST_CONFIRM` → 接受/拒绝 → `quest_confirm(yes,pid)`。⬜ `[INPUT]` 输入框、`SCRIPT_SELECT_ITEM` 选道具、左右立绘 |
| 任务日志 + 指引 | 🟡 | 🟡 | `wire.h``GCQuestInfo`(7 + flag 驱动变长尾)、`QUEST_SEND_*` flag 枚举。`EntityStore``QuestInfo{index,flag,begin,title,clock_*,counter_*,icon}` + `m_quests` map + `drain_quest_changes()` / `quest(idx)` / `quest_indices()``ui/quest_log.gd`J 键):列 `◆ title` + `counter_name: value` + `clock_name: value``quest_info` 信号刷新。⬜ 地图 / 世界箭头(`TARGET_CREATE` 位置目标)、`[QUESTBUTTON]``script_button` |
| quest 脚本执行 | — | ✅(选型②) | 采纳推荐方案:**客户端纯渲染 `GC_SCRIPT`,quest 逻辑全在服务器**。不内嵌 Python 解释器 |
**已验证**`net.entity_store` 扩展(`GC_SCRIPT``[QUESTION]``ScriptCue``GC_QUEST_CONFIRM``ConfirmCue``GC_QUEST_INFO` 带 TITLE+COUNTER flag → `QuestInfo` 变长尾解析,`length` 字段回填);`quest_test.gd``parse_script`:纯文本保留 / `[ENTER]`→换行 / `[QUESTION]`→2 选项 / `[DONE]`→继续钮;`script_dialog` 信号 → 弹窗 + 3 选项钮 + 选 B → `script_answer(1)` + 关窗;无选项 → 继续 → `script_answer(255)``quest_confirm_ask` → 接受 → `quest_confirm(true,999)`quest_log 只显示有标题的 quest + `quest_info` 刷新不炸)。13 个 GDScript 测试套件全绿,ctest 9/9iOS 编过。
**未完成**`[INPUT]` 文本输入 UI`CGQuestInputString` 封包已备)、`SCRIPT_SELECT_ITEM` 选道具、NPC 对话左右立绘、`[QUESTBUTTON]``script_button`、地图 / 世界任务箭头(`TARGET_CREATE_TYPE_LOCATION`)、端到端(见 `CLIENT-PORT.md` §3:握手 + 认证已联通,进游戏串场归 P10)。
## P8 — 社交 / 商店 / 仓库(XL,🌐)
依赖 P1。窗口多,首版覆盖组队 / 好友 / 商店 / 交易 / 仓库五件;公会 / 精炼 / 私人商店留后续(拍卖行 m2dev-client 无)。
| 子系统 | 规模 | 状态 | 落点 |
|---|---|---|---|
| 组队 | M | ✅ | `wire.h``GCPartyInvite/Add/Update/Remove/Link/Unlink/Parameter`(0x0710-16) + `CGPartyInvite/InviteAnswer/Remove/Parameter`(0x0701-06)`static_assert``EntityStore``m_party`(pid→`PartyMember{vid,name,state,hp_pct,affects[7]}`) + `party_dirty()` + `drain_party_invites()` + `party_distribute_mode()``M2Client``party_invite/party_answer/party_leave/party_set_distribute` + `get_party()` + `party_changed`/`party_invite_ask(leader_pid)` 信号。`ui/party_ui.gd`:左侧常驻队员面板(名字 + ★队长 + HP 条),收邀请 → `Dialogs.confirm``party_answer` |
| 私聊 / 好友 | M | ✅ | `wire.h``GC_MESSENGER`(0x0741) / `CG_MESSENGER`(0x0740) sub-header 包(`GCSubHeader`/`CGSubHeader`+ `MESSENGER_GC/CG_*` 子头枚举。`EntityStore``m_friends`(name→`Friend{online}`) + `friends_dirty()`,解 `LIST``{u8 connected,u8 len,name}` 循环)/ `LOGIN`/`LOGOUT`/`REMOVE_FRIEND``M2Client``add_friend/remove_friend``ADD_BY_NAME`/`REMOVE` 变长包)+ `get_friends()` + `friends_changed``ui/friend_ui.gd`(O 键):在线点 + 名字 + [×],点名 → `whisper_to``chat.start_whisper("/w 名 ")` |
| NPC 商店 | M | 🟡 | `wire.h``GC_SHOP`(0x0810) / `CG_SHOP`(0x0801) sub-header + `SHOP_GC/CG_*` 子头 + `ShopItem`(43`TShopItemData`)。`EntityStore``SHOP_GC_START``u32 owner_vid` + `ShopItem[]`)→ `m_shop_items` + `m_shop_open`/`m_shop_vid``END` 清;错误 → `drain_shop_errors()``M2Client``shop_buy(pos,cnt)``BUY` = `[head][u8 cnt][u8 pos]`/ `shop_sell(cell,cnt)``SELL2`/ `shop_close` + `get_shop_items()` + `shop_opened(vid)`/`shop_closed`/`shop_error(kind)``ui/shop_ui.gd`:名字 + 价格 + [买],背包右键 → `sell(cell)`,错误红字。⬜ `START_EX` 多标签商店、图标、批量 |
| 交易 | M | 🟡 | `wire.h``GC_EXCHANGE`/`CG_EXCHANGE`(0x051C/0x0508) + `EXCHANGE_SUB_*`START/ITEM_ADD/ITEM_DEL/ELK_ADD/ACCEPT/CANCELGC 侧 END/ALREADY/LESS_ELK+ `GCExchange`(50`is_me`+`arg1/2/3`+sockets+attrs)/`CGExchange`(13)。`EntityStore``ExchangeState{active,partner_vid,self/peer_items[12],self/peer_gold,self/peer_accept}` + `exchange_dirty()``M2Client``exchange_start/add_item/add_gold/accept/cancel` + `get_exchange()` + `exchange_changed``ui/exchange_ui.gd`:我方 / 对方两栏 + 金币 + [接受]/[取消],背包右键 → `offer(win,cell)`display pos 0..11 轮转)。⬜ 道具图标格、拒绝时回滚提示 |
| 仓库 | M | 🟡 | `wire.h``GC_SAFEBOX_SET/DEL`(0x0830-31body 复用 `GCItemSet`/`GCItemDel`) / `SIZE`(0x0833) / `MONEY_CHANGE`(0x0834) / `WRONG_PASSWORD`(0x0832)`CG_SAFEBOX_CHECKIN/CHECKOUT`(0x0820-21`{u8 safe_pos, ItemPos inv}`) / `ITEM_MOVE`(0x0822,复用 `CGItemMove`)。`EntityStore``m_safebox[135]` + `m_safebox_size/gold/open` + `safebox_dirty()``M2Client``safebox_checkin/checkout/move` + `get_safebox_items()`/`get_safebox_size()`/`get_safebox_gold()` + `safebox_changed``ui/safebox_ui.gd`:金币 + 道具行 + [取出],背包右键 → `deposit(win,cell)`(自动挑空位)。⬜ 密码框、商城页(`MALL_*`)、拖拽网格 |
| 公会 | L | 🟡 | `wire.h``GC_GUILD`(0x0730) sub-header + `GUILD_GC_*`/`GUILD_CG_*` 枚举 + `GuildInfoBody`(35)/`GuildSubMember`(13`name_flag` 后跟 name[65])/`GuildSubGrade`(10)`static_assert``EntityStore``GuildState` + `m_guild_members` map + `m_guild_grades[16]` + `guild_dirty()`,解 INFO/LIST/GRADE/GRADE_NAME/CHANGE_EXP/MONEY_CHANGE。`M2Client``get_guild`/`get_guild_members`/`get_guild_grades` + `guild_add_member`/`guild_remove_member`/`guild_offer`/`guild_answer_make` + `guild_changed` 信号。`ui/guild_ui.gd`(G 键):信息行 + 成员列表(按 grade 排序,grade 名解析,★官员)+ 16×12 会徽图(放大 3×,`NEAREST`)。**真机实测**`[GM-TEAM]` lvl 20 members 1/70。**会徽下载(独立连接)**:`wire.h` `GC_MARK_IDXLIST`(0x0C11)/`GC_MARK_BLOCK`(0x0C10)/`GC_MARK_UPDATE`(0x0B15) + `CG_MARK_LOGIN`(0x0C01)/`CG_MARK_IDXLIST`(0x0C04)/`CG_MARK_CRCLIST`(0x0C02)`GC_MARK_*` body 用 u32 `buf_size` 而非 u16 帧长。`NetStream``on_cipher_active()` + `on_raw()`(子类自定义分帧)钩子。`mark_image.{h,cpp}``MarkImageSet`guild_id→mark_id、LZO1X 解压 64×48 块拼进 512×512、`rect_of`/`mark_pixels`+ `parse_mark_idxlist`/`parse_mark_block``mark_client.h``MarkClient : NetStream`KX→`CG_MARK_LOGIN{handle,random_key}``CG_MARK_IDXLIST`→逐图 `CG_MARK_CRCLIST`(全 0 CRC = 整图)→`GC_MARK_BLOCK`)。`GameClient``mark_handle`/`mark_random_key`(来自 `GC_LOGIN_SUCCESS3/4`+ per-slot `guild_id`/`guild_name` + `drain_mark_updates()``M2Client``download_guild_marks(host,port)` / `get_guild_mark(gid)` / `get_guild_mark_image(gid)``Image` + `guild_marks_ready` / `guild_mark_updated` 信号。`serverinfo.gd` 第 8 列 `mark_port`0=跳过);`app_flow` 进游戏时按配置连、`guild_mark_updated` 1s 冷却重连。**公会战 + 公会技能页**:`wire.h` `GUILD_GC_SKILL_INFO`(12) `GuildSkillInfoBody`(17=`{u8 point, u8 lvl[12], u16 gp, u16 max_gp}`)、`GUILD_GC_WAR`(15) `GCGuildWar`(10)、`GUILD_GC_GUILD_WAR_LIST/END_LIST`(17/18) `GuildWarPair`(8)、`GUILD_GC_WAR_POINT`(19) `GuildWarPoint`(12)、`GUILD_GC_NAME`(16) `GuildNameEntry`(16,无 NUL)`EGuildWarState` 枚举;`GUILD_CG_USE_SKILL`(9)。`EntityStore``GuildSkillState` / `GuildWarStatus` / `m_guild_wars` / `m_guild_names` map + `guild_skill_dirty()`/`guild_war_dirty()` + `drain_guild_war_events()`/`drain_guild_war_scores()``GameClient.send_guild_use_skill(vnum, target_vid)``M2Client``get_guild_skill`/`get_guild_wars`/`get_guild_war`/`get_guild_name` + `use_guild_skill(vnum, target)` + `declare_guild_war(name)`(发 `/war <name>` 聊天命令)+ `guild_skill_changed`/`guild_war_changed`/`guild_war_event`/`guild_war_point` 信号。`ui/guild_ui.gd` 改三页 Tab(成员 / 技能 / 公会战):技能页显示技能点 + 公会点 + 12 技能等级(名字取 `skill_table.for_category("GUILD")`)+ 已学技能「施放」按钮;公会战页显示当前战状态 + 进行中 GvG 列表(我方标记)+ 宣战输入框。**会徽上传**:`wire.h` `CG_MARK_UPLOAD`(0x0C03) `CGMarkUpload`(776=`{gid, u8 image[16*12*4]}`)、`CG_GUILD_SYMBOL_UPLOAD`(0x0722) `CGSymbolUpload`(8) + 追加原始文件字节;`NetStream.send_pending()``MarkClient``Mode`Download / UploadMark / UploadSymbol+ `set_upload_mark/set_upload_symbol` + `upload_done()`(发缓冲排空即完成)。`M2Client``upload_guild_mark(host, port, gid, Image)`(自动转 RGBA8 / 缩放到 16×12/ `upload_guild_symbol(host, port, gid, PackedByteArray)` / `get_mark_server()` + `guild_mark_uploaded(ok)` 信号。`guild_ui` 成员页「上传会徽」按钮(配了 `mark_port` + 图源才出),`game_scene._guild_mark_upload_image()` 优先 `res://ui/default_guild_mark.png` 否则现造占位。⬜ 等级/日志页、公会战应答(`/war` 系聊天命令) |
| 精炼 / 强化 | M | 🟡 | `wire.h``GC_REFINE_INFO`(0x051D)/`_NEW`(0x051E) `GCRefineInfo`(63) = type+pos+`RefineTable`(57src/result vnum + cost + prob + `RefineMaterial[5]`)`CG_REFINE`(0x050C) `CGRefine`(6)=`{pos,type}``EntityStore``RefineCue` + `drain_refine_cues()``M2Client``refine(pos,type)` + `refine_ask(dict)` 信号。`ui/refine_ui.gd``refine_ask` → 弹框(src→result / 成功率 / 费用 / 材料)→ [精炼]→`refine(pos,type)` / [取消]。⬜ 精炼结果动画 |
| 龙魂精炼 | M | 🟡 | `wire.h``GC_DRAGON_SOUL_REFINE`(0x051F) `GCDragonSoulRefine`(8)=`{sub_type, ItemPos}``CG_DRAGON_SOUL_REFINE`(0x050D) `CGDragonSoulRefine`(50)=`{sub_type, ItemPos grid[15]}``DS_SUB_*` 枚举(OPEN / DO_UPGRADE·IMPROVEMENT·REFINE / REFINE_SUCCEED / REFINE_FAIL_*);`DRAGON_SOUL_MAX_NUM`(180) + `WINDOW_DRAGON_SOUL`(5) 走 `mut_slot``EntityStore``DragonSoulCue` + `drain_ds_cues()` + `m_dragon_soul[180]` + `dragon_soul_slot()``M2Client``ds_refine(mode, cells)`mode 0/1/2 → DO_UPGRADE/IMPROVEMENT/REFINEcells 为背包格,grid[0]=龙魂)+ `get_dragon_souls()` + `ds_window_open` / `ds_refine_result(ok, sub_type, cell)` 信号。`ui/dragon_soul_ui.gd`L 键 / `ds_window_open` 自动弹):3 模式按钮 + 15 格(背包右键填/取)+ [执行]→`ds_refine` + 成功/失败提示。⬜ 龙魂属性表 / 结果动画 |
| 私人商店 / 道具商城 / Cube | M | 🟡 | **首版**`CG_MYSHOP`(0x0802) `CGMyShopHead`(38)+`MyShopItem`(13)×N`GC_MALL_OPEN`(0x0841)/`SET`(0x0842)/`DEL`(0x0843) 路由进 `EntityStore.m_mall[135]``CG_MALL_CHECKOUT`(0x0840)。Cube 走 `GC_CHAT`/`CHAT_TYPE_COMMAND` 文本总线(`EntityStore.apply_server_command``cube open/close/info/success/fail/r_list/m_info`)→ `CubeState` + `drain_cube_events()``send_cube_make/material_info/result_list``/cube …``M2Client``open/close_private_shop``get_mall_items`/`mall_checkout``get_cube`/`cube_make`/`cube_request_*` + `mall_opened`/`mall_changed`/`cube_opened`/`cube_closed`/`cube_changed`/`cube_result``ui/{private_shop,mall,cube}_ui.gd``net.entity_store`+`net.loopback_flow`+`shop_cube_mall_test.gd` 覆盖。⬜ 能量条、头顶招牌 3D、`START_EX` 多标签、39 格拖放、send 命令字对真服校准 |
**已验证**`net.entity_store` 扩测(party ADD/UPDATE/LINK/PARAMETER/REMOVE + INVITE 入队;messenger LIST 2 好友 + 在线位 + LOGIN 翻转;shop START owner+item / 错误 / ENDexchange START/ITEM_ADD/ELK_ADD/ACCEPT/ENDsafebox SIZE/SET/MONEY/DEL);`net.loopback_flow` MockServer 断言 `CG_PARTY_INVITE{vid}` / `CG_SHOP BUY{cnt,pos}` / `CG_EXCHANGE START{vid}` / `CG_SAFEBOX_CHECKIN{safe,inv}` 字节到位;`p8_test.gd`5 个 UI 节点 + FakeClient:队员面板 2 行 + 自动接受邀请、好友行 + 点名 whisper + 添加、商店 2 行 + 买 + 卖 + 错误本地化 + 关、交易两栏 + offer + 放金币 + 非 active 自动关、仓库开 + 金币 + 取出 + 存入挑空位)。13 套 GDScript 测试全绿,ctest 9/9,项目导入干净,iOS 编过。
**已验证(公会 + 精炼)**`net.entity_store` 扩测(GC_GUILD INFO 名/等级/资金/容量 + LIST `name_flag` 变长成员 + GRADE `[count][idx][GuildSubGrade]` 名/权限;GC_REFINE_INFO src/result/cost/prob + materials[5]);`guild_refine_test.gd`(公会窗无公会提示 / 信息行 / 成员按 grade 排序 + grade 名 / ★官员;精炼框 `refine_ask` 弹出 + 成功率 + 费用 + 材料行 + [精炼]→`refine(7,1)` + [取消] 不发包)。真机 `net_e2e``[GM-TEAM]` 公会数据解析正确。
**已验证(龙魂精炼)**`net.entity_store` 扩测(GC_DRAGON_SOUL_REFINE OPEN cue / REFINE_SUCCEED 带 cell+window / REFINE_FAIL_* / drain 清空);`dragon_soul_test.gd``ds_window_open` 弹出 + 默认精炼模式 + 切模式禁用当前按钮 + 背包右键按序填 15 格 + 非背包窗忽略 + 再次右键取出/末尾重放 + [执行]→`ds_refine(mode, [cells])` + 成功清空/失败留原因码 + 清空/关闭按钮)。
**已验证(会徽下载)**:新 CTest `net.guild_mark``parse_mark_idxlist` 2 条 → guild→mark_id + `needed_images` 升序去重;`rect_of``mark_id/1280` 选图、`%1280` 定位;合成 LZO1X 压缩块 → `parse_mark_block``apply_block` 拼图 → `mark_pixels` 逐像素回读;坏压缩数据 / 越界索引 / 截断 body 全部安全拒绝);`guild_mark_test.gd`guild_ui 无图时 TextureRect 隐藏 → `guild_marks_ready` 出图 16×12 → 离开公会收起;`app_flow` `mark_port=0` 不下载 / 配置后 `download_guild_marks(host,11002)` / `guild_mark_updated` 冷却外重下、1s 内不重下)。**独立连接 KX + `MarkClient` 分帧逻辑离线覆盖,未接真机**(联调服未开会徽服端口)。附带修复:`game_scene.gd``_skill_master()` 之前被误插进 `setup()` 中段,截断了后半段(导致 `assets_root` 等未声明)——已把它移到 `setup()` 之后,多个依赖 `game_scene` 的 GDScript 套件恢复真绿。
**已验证(会徽上传)**`net.loopback_flow``MockServer::Mode::Mark`:真 loopback socket 跑完整 KX → `CG_MARK_LOGIN{handle,random_key}` 字节到位 → `CG_MARK_UPLOAD{gid=77, image[768]}` 服务端逐字节校验和一致 → `upload_done()`;同法验 `CG_GUILD_SYMBOL_UPLOAD{gid=88}` + 2000 字节原始附加。`guild_mark_test.gd`:未配 mark server 无「上传会徽」按钮 → 配好后按钮出现 → `upload_guild_mark("1.2.3.4",11002,88,img)` + `guild_mark_uploaded(true)` → 状态更新。23 套 GDScript 全绿,ctest 10/10,导入干净,iOS 编过。
**已验证(公会战 + 公会技能)**`net.entity_store` 扩测(GUILD_GC_NAME id→名注册、SKILL_INFO 点数/12 等级/公会点、WAR ON_WAR 状态 + 事件 drain、WAR_LIST 2 对 → WAR_END_LIST 删 1、WAR_POINT drain);`guild_war_skill_test.gd`(技能页无数据提示 → `guild_skill_changed` 出点数行 + GUILD 技能名 + 等级 + 第 12 技能配 `levels[11]`;已学技能有「施放」→ `use_guild_skill(4051,0)`;公会战页无战提示 + 宣战 → `declare_guild_war("Tigers")` + `guild_war_changed` → 「交战中」+ GvG 行「◀我方」)。23 套 GDScript 全绿,ctest 10/10,导入干净,iOS 编过。**真机(2026-08-31`net_e2e`**`[GM-TEAM]` 账号进游戏后服务器自动推 `GUILD_GC_SKILL_INFO`17B body 解析正确(skill_point=19 / guild_point 0/2300 / 12 等级);`GUILD_GC_WAR` 无战状态解析正确;trace `GC_GUILD` 子包 len=171/40/83/22/21/9/5 无 unknown。
**未完成**:私人商店打磨;商店 `START_EX`;交易 / 仓库道具图标网格;仓库密码框 + 商城页;公会战应答聊天命令;公会等级/日志页;龙魂属性表 + 精炼结果动画。
## P9 — 世界系统(L,🌐,分散)
| 任务 | 规模 | 状态 | 落点 |
|---|---|---|---|
| 换图 / 传送 | S | 🟡 | `wire.h` `GCWarp`(18x/y/addr/port)。`EntityStore` `WarpCue` + `drain_warps()``same_server()` = addr==0)。`M2Client` `warp(pos, same_server)` 信号(pos 已转 Godot 米)。`game_scene._on_warp`:同服 → 直接挪玩家 + 相机 snap;跨服(addr≠0)→ warning,归 P10 重连串场 |
| 频道 | S | ✅ | `wire.h` `GCChannel`(5)。`M2Client` `get_channel()` + `channel_changed(ch)` 信号。小地图角标显示 `CH n``hud.set_channel` 若有也调) |
| 游戏内时间 / 昼夜 | S | ✅ | `wire.h` `GCTime`(12unix 秒 i64)。`M2Client` `get_server_time()` + `time_changed(epoch)``world/world_time.gd`:epoch → 日内秒 → 太阳 pitch/azimuth + 光强/光色(夜黑冷蓝 → 黄昏橙 → 正午暖白)+ `Environment` ambient/背景色。无 GC_TIME 时退回系统本地时间 |
| 小地图 | M | 🟡 | `ui/minimap.gd`:右上角圆形,北朝上,`_draw` 画底圆 + 外环 + 玩家朝向箭头 + 实体点(怪红 / 其它蓝)+ NPC 点(传送青 / 其它黄)+ 任务标记橙方。世界→图 = `(d.x, d.z)*SCALE` 夹到半径。⬜ 缩放 / 拖动 / 大地图窗 / atlas 底图 |
| NPC 位置 / 世界任务标记 | M | 🟡 | `wire.h` `GCNPCPosition`(6 + `count`×`NPCPositionEntry`78) / `GCTargetCreate`(46) / `GCTargetUpdate`(16) / `GCTargetDelete`(8) + `CREATE_TARGET_TYPE_*``EntityStore` `m_npc_marks` + `m_markers` map + `npc_marks()`/`markers()` + dirty。`M2Client` `get_npc_marks()`/`get_world_markers()` + `npc_marks_changed`/`world_markers_changed`。小地图消费。⬜ 世界空间 3D 箭头 / 屏幕边缘指示 |
| 天气 / 环境粒子 | S | 🟡 | `fx/weather.gd`:跟随相机上方的 `GPUParticles3D` 盒发射,`set_weather("snow"/"rain"/"none")` 切重力 / 速度 / 尺寸 / 颜色 / 湍流。⬜ 由地图 `.msenv` 属性驱动、闪电 / 雾 |
| 坐骑 | S | 🟡 | `wire.h` `GCMount`(21)。`Entity.mount_vnum` + `drain_mount_changes()``M2Client` `mount_changed(vid)` 信号 + `entity_dict``mount_vnum`。⬜ 换骑乘模型 / 坐骑动作集 / 移速 |
| 地形贴花 | S | ⬜ | `GameLib/TerrainDecal.cpp` —— 血迹 / 施法阵 / 脚印(Godot `Decal` 节点) |
| 钓鱼 / 副本 | M | ⬜ | `GC_FISHING`(0x0B10) `DUNGEON`(0x0B11) |
**已验证**`net.entity_store` 扩测(warp 入队 + same_servertime set + dirty oncechannel set`GC_NPC_POSITION` 2 条 markmarker create→update→deletemount → `Entity.mount_vnum` + change);`p9_test.gd`(小地图:view + `CH` 角标 + `_to_map` 北朝上/东朝右/夹半径;world_time:正午 `day_fraction≈0.5` + 太阳亮,午夜 ≈0 + 太阳暗;weatheroff→snow→rain(雨落更快)→offwarp 同服信号)。14 套 GDScript 测试全绿,ctest 9/9,项目导入干净。
**未完成**:大地图窗 + atlas 底图、地形贴花、钓鱼 / 副本、坐骑换模型、世界空间任务箭头、天气按地图驱动、跨服换图(P10)。
## P10 — 网络补全(M,🌐)
| 任务 | 规模 | 状态 | 落点 |
|---|---|---|---|
| **LOADING → PHASE_GAME**(真机端到端的最后一跳)| S | ✅ | `wire.h` `CGClientVersion`(0x000D70Bfilename+timestamp)。`GameClient`:收到 `GC_MAIN_CHARACTER` 后**自动发 `CG_CLIENT_VERSION`**fork 用固定 timestamp `1215955205`)—— 这是服务器放行到 PHASE_GAME 的门槛,之前缺它就在 phase 4 断线。`net_e2e` 真机验证:`phase = 5 / IN GAME`,收到 121 个实体 + HP/SP/level + 20 格背包 + 好友列表 |
| Loading 相位 | S | ✅ | `ui/loading_screen.gd``CanvasLayer` layer 60):`phase_changed` → "login/select→连接中/载入角色"、"loading→载入地图" 显示转轮 + 阶段文案 + 可选进度条;"game" 隐藏。断线也收起 |
| 选服 / 选频道 | S | 🟡 | `net/serverinfo.gd``class_name ServerInfo`):解析 TSV 服务器表(`name / auth_host / auth_port / game_host / game_port / channels`),内置当前联调服默认项。`address(server, channel)` → 频道 N 的 game 端口 = base + (N-1)×10(经典 Metin2 约定)。`app_flow.gd` 登录表单里做服务器 / 频道下拉。⬜ 频道在线人数轮询(`ServerStateChecker` UDP 探针) |
| Offline / 断线重连 | S | ✅ | `ui/reconnect_ui.gd`layer 70):`disconnected(reason)` → 遮罩 + "N 秒后自动重连(次数/上限)" 倒计时 → `M2Client.reconnect()`(已有);[立即重连] / [停止] 按钮;`entered_game` 或非 offline 的 `phase_changed` 自动收起。最多重试 5 次 |
| charselect 串场 | M | ✅ | `app_flow.gd``Node` 场景根):一个 `M2Client` 贯穿全程,状态机 `LOGIN → SELECT → GAME``char_list` → 选人列表;点角色 → `select_character``entered_game` → 实例化 `game_scene``setup`LOGIN/SELECT 期间断线 → 回登录,GAME 期间断线 → 保留场景交给 `ReconnectUI``LoadingScreen` + `ReconnectUI` 常驻。`build_game_scene` 开关便于 headless 测状态机 |
| Metin2 风选人页 | M | ✅ | `ui/char_select_screen.gd` 取代裸按钮列表:全屏 `select.jpg` 背景 + 右侧 `SubViewport``PlayerView` 真模型(正面 + `wait` + 缓摆,**包围球** `r/sin(fovV/2)` 自适应机位,不再切远侧手)+ 左上职业名(locale `name_<class>.sub``select.dds`DDS 走 `Metin2World.load_dds`;退中文描边字)+ 左侧 `board` 板(帮会徽 / **国家名+国旗** `get_empire()` / 帮会名 / 名称 / 等级 / 游戏时间 / 体力·智力·力量·敏捷)+ 开始/创建/删除/退出 + ◀▶ 槽位切换(`_pad_slots` 补空槽)。**建号/删号 1:1**:`CG_CHARACTER_CREATE`(0x0201)/`CG_CHARACTER_DELETE`(0x0202) + `GC_PLAYER_CREATE_SUCCESS/FAILURE`(0x020C/D) + `GC_PLAYER_DELETE_SUCCESS`(0x020E)/`WRONG_SOCIAL_ID`(0x020F)`GameClient.create/delete_character` + `drain_char_events` + `replace_slot/clear_slot`(服务器补丁槽位不重发);`M2Client` 同名方法 + `char_created/char_create_failed/char_deleted/char_delete_failed` 信号 + `get_empire/get_slot_count`;选人页建号弹窗(职业+名称+各职业起始四维)、删号弹窗(secret 删除码);`app_flow._on_char_list` 在 SELECT 只 `set_chars` 不整屏重建。`net.loopback_flow` MockServer + `char_create_delete_test.gd` 覆盖。C++ `CharSlot`+dict 补 `play_minutes/st/ht/dx/iq/main_part/hair_part``ui_assets` 加运行时 DDS 解码 |
| attack CRC | S | ✅ | `net_e2e` 真机实测:进游戏后发 `CG_MOVE` + `CG_ATTACK``crc_proc=0 crc_file=0`),保持连接 10s 不被踢。**结论:0 CRC 服务器接受,不需要占位 CRC** |
| ESC 系统菜单 + 游戏设置窗(`uisystem.SystemDialog` + `uigameoption.OptionDialog`| M | ✅ | `system_menu_ui.gd``systemdialog.py`8 键:system_option / game_option 开子窗、`change_button``/phase_select``logout_button``/logout``mall_button``/in_game_mall``exit_button`→quit。ESC 改开此菜单。`game_option_ui.gd``gameoptiondialog.py`):`/setblockmode <mask^bit>`EBlockAction 位 1<<0..1<<5,本地 `_block_mode` + cfg 持久化)+ `/pkmode {0,1,2,4}` PK radio + 6 组显示开关持久化 `user://system_option.cfg [gameopt]`(渲染钩子待补)。`system_menu_ui_test.gd` 覆盖。C++ 无改动 |
| 组队成员信息板(`uiparty.PartyMemberInfoBoard`| M | ✅ | `wire.h` `CGPartySetState`(10) + `EPartyRole``send_party_set_state` / `M2Client.party_set_state``get_party()``state`+`affects[7]``party_ui.gd` 每员 strip = 角色状态按钮(队长弹菜单 `CG_PARTY_SET_STATE` 攻/坦/狂/辅/宗/防 + 踢人)+ 名字(★) + HP gauge + affect chip;顶部 EXP 分配开关 + 组队治疗。点名字 → `set_target``p8_test` 组队段扩覆盖;顺带修 `p8_test._init` 未 await 的隐患 + `shop_ui.refresh` queue_free 延迟 bug |
| 系统设置窗(`uisystemoption.OptionDialog`| S | ✅ | `ui/system_option_ui.gd`:装 `systemoptiondialog.py` 真 uiscript,绑定照 `uisystemoption.py`:音乐/音效音量滑条→`Audio.master_bgm/sfx`(含在播 BGM 即时)、`camera_short/long``game_camera.max_dist`(近 11/远 20)、`fog_level0/1/2`(浓/中/淡)→`Environment.fog_density``tiling_*` Godot 无对应保留占位。持久化 `user://system_option.cfg`= `systemSetting` 配置),setup 读回即时生效,`.msenv` 换 env 后重贴。`game_scene` ESC 呼出。`system_option_ui_test.gd` 覆盖 |
| 私人商店开설창(`PrivateShopBuilder`)| S | ✅ | 增量 36:布局改走真 uiscript `privateshopbuilder.py`40 格 grid + NameLine 盖 LineEdit + Ok/Close,候选面板挂窗右侧)。`itemStock` 交互(拿起/落位/价格弹窗/排序+display_pos/撤下/개설/철수)不变。测试拆出 `private_shop_ui_test.gd`。以下为原 | `private_shop_ui.gd` 重做成 `uiprivateshopbuilder.py``itemStock` 模型:左侧背包候选「拿起」+ 右侧 40 格(`shop.SHOP_SLOT_COUNT`);拿件点空格 → 价格弹窗(`MoneyInputDialog`)→ 落位(`AddPrivateShopItemStock`),点占用格 → 撤下(`DelPrivateShopItemStock`)。개설 把 stock 按格号排序、`display_pos=格号`、上限 39`PRIVATE_SHOP_ITEM_MAX_NUM`)→ `open_private_shop(sign, [{vnum,count,inv_cell,price,display_pos}])``TPacketCGMyShop`+`TShopItemTable`×N)。`shop_cube_mall_test` 重写覆盖。C++ 无改动 |
| 商店 `SHOP_GC_START_EX`(多货架)| S | ✅ | `entity_store` 解析 `SHOP_GC_START_EX`(10) = `{u32 owner_vid, u8 tab_count}` + `tab_count`×`{ShopTabHead(name[32]+coin_type), ShopItem[40]}``ShopEntry.pos` 保槽位,`m_shop_tabs``ShopTab{name,coin_type,items}`),`SHOP_GC_START` 也按 40-槽填 pos,END 清货架。`M2Client.get_shop()``{vid,open,tabs:[…]}``shop_ui.gd` 多货架按钮 + 买位置 `tabIdx*40+slot`1:1 `uishop.py GetIndexFromSlotPos`)。`net_entity_test` 双货架合成包 + `p8_test` 切页 / `buy pos 5` / `buy pos 43` |
| 选魔石窗(样板第 2 例)| S | ✅ | `ui/select_item_ui.gd`:装 `selectitemwindow.py`5×8 grid),填格照 `uiselectitem.py.RefreshSlot`(只 `ITEM_TYPE_METIN`=10 + `GetItemGrade`(内部名末位数字)≤2,≤54 个,升序映射背包格)。点格 → `client.script_select_item(invCell)``CG_SCRIPT_SELECT_ITEM` 0x0903 = `net.SendSelectItemPacket`+ 关窗;关闭 → `script_select_item(0)`。触发链:`quest_dialog.parse_script` 识别 `[SELECT_ITEM]` token → `select_item_requested` 信号(= `PythonEventManager EVENT_TYPE_SELECT_ITEM``BINARY_OpenSelectItemWindow`)→ `game_scene``select_item_ui.open``select_item_ui_test.gd` 覆盖 |
| 角色状态窗(1:1 窗口样板)| M | ✅ | `ui/char_status_ui.gd``UiScript`+`UiBuild` 装载 `characterwindow.py` 真布局,数值逐字对照 `uicharacter.py.RefreshStatus`Level/Exp/RestExp、HP/SP、STR/DEX/HTH/INT、ATT=`(min|max)+ATT_BONUS+ATTACKER_BONUS`、DEF、MATT=`MAG_ATT+(min|max)MAGIC_WEP`、MDEF、ASPD/MSPD/CSPD/EREPointTypes 索引取 m2dev `Packet.h`)。加点 /`client.say(0,"/stat ht"|"/stat- ht")`(同原 `statusPlusCommandDict`);`POINT_STAT>0` 才显加点按钮。4 页 tab `SetState` 切换。名称/帮会/头像取 `get_entity(get_main_vid())``game_scene` V/C 热键。`char_status_ui_test.gd` 覆盖。**后续窗口(选道具网格 / 商店 START_EX / 私人商店 39 格 / 系统设置 …)照抄此样板:uiscript 装载 + 逐字数值绑定 + 原聊天命令 / 封包** |
**已验证**`net.loopback_flow` MockServer 断言「`GC_MAIN_CHARACTER` 后客户端自动发 `CG_CLIENT_VERSION`」;`p10_test.gd`serverinfo 内置默认 + 文件解析 + 频道端口偏移;loading 遮罩 phase=loading 显示 / progress 条 / phase=game 隐藏;reconnect 断线 → 倒计时耗尽调 `reconnect()` + 手动重连 + `entered_game` 收起;app_flow 状态机 LOGIN→连接用 serverinfo 地址→`char_list`→SELECT→点角色→`select_character``entered_game`→GAMESELECT 断线→LOGIN)。15 套 GDScript 测试全绿,ctest 9/9。**`net_e2e` 真机:auth → game → 选人 → LOADING → PHASE_GAME 全程打通。**
**真机字节校准 + 收尾(2026-08-30,已做)**
- `GC_CHAR_ADD_INFO`(0x020797B) 补上 → 实体名/等级/公会/骑乘出(NPC「Old Man」实测正确);
`GC_CHARACTER_UPDATE`(0x0209) 按 `TPacketGCCharacterUpdate` 全解(parts/guild/alignment/pk/mount)。
- `ch_type` 改在 GDScript 按 race 查 `mob_proto.type` 分类(`net_play._entity_kind`/`_entity_name`),
`game_scene``mob_proto``GC_PLAYER_POINTS` 本来就对(gold 13.5 亿 = GM 号真实值,
exp/next_exp 按 unsigned 输出)。
- **3D 名字标签刷新**`ChangeKind::Info` + `entity_info` 信号 → `net_world._on_info` 刷 Label3D
`name_resolver` 让怪名走 `mob_proto`
- **跨服换图**`GC_WARP` addr≠0):`M2Client.warp_to_game_server` 拆连接、复用 `login_key` 直连新
game server。**断点快连**`reconnect()` 优先直连 game server 省一次 auth 往返。
- **频道负载查询**`net/channel_status.gd` = `CServerStateChecker` 端口(明文 TCP`CG_STATE_CHECKER`
0x000F → `GC_RESPOND_CHANNELSTATUS` 0x0010 → `{port: 0关/1正常/2拥挤/3爆满}`)。真机 `11011`
`{11011:1,11012:1,11013:1,11991:1}``serverinfo` 频道口步长改 +1`port_step` 可按服覆盖)。
- **怪 / NPC 真 3D 模型**`ui/mob_view.gd`race → `root/npclist.txt` 代号 → 目录逐段回退
`bear_brown→bear`)→ `Metin2Model`(网格 / 贴图用完整代号)+ `motlist.txt` 动作),
`game_scene._make_entity_model``net_world` 默认工厂,失败回退占位胶囊。
`net_world._attach_nameplate` 抽出,真模型也挂名字 + HP 条。翻译版 `mob_proto` `szName` 被本地化
"Brown Bear")解不出目录,故必须走 `npclist.txt`
- **已在局内 / 重连补拉实体**`net_world.catch_up()` 遍历 `client.get_entities()` 补 spawn
(协程化的 `setup()``entity_spawned` 接得晚,会漏初始 spawn 洪流);`game_scene`
`set_local_vid` + `net_play._on_main_set``catch_up`,避免本地玩家被画成压身的蓝胶囊。
详见 `CLIENT-PORT.md` §3。**23 套 GDScript + 10 CTest + 项目导入 + macOS/iOS 扩展全绿。**
**剩的不是代码问题**:字面在线人数(Metin2 协议只有 4 态负载,要数字得改服务器)、全量 monster 资产。
---
## 里程碑
**P0P10 首版全部完成**2026-08-30)。真机端到端(`net_e2e`)从 auth 握手一路打通到
PHASE_GAME —— 收到 121 个实体、HP/SP/等级、20 格背包、好友列表。18 套 GDScript + 9 CTest +
项目导入 + macOS/iOS 扩展全绿。
| 里程碑 | 含 | 首版状态 | 距「打磨完成」还差 |
|---|---|---|---|
| **M1 可打** | P0 闭环 + P4 硬直 / 连击 / 相机抖 / 死亡窗 | ✅ 首版 | 攻速 / motion index 按 `POINT_ATT_SPEED` 细化、武器拖尾、相机锁定 |
| **M2 角色养成** | P1 UI 层 + P2 物品 / 装备(武器 / 盾 / 身体 / 头盔 / 头发 / armor shape / 时装)+ P6 技能(分类页 + 技能特效表 + 内建特效)+ P3 聊天 | ✅ | 加点等级公式(服务器管)、真弹道 |
| **M3 世界** | P7 任务 / NPC + P9 世界系统 + P10 网络补全 | ✅ 首版 | `[INPUT]` / 选道具 / 立绘、大地图窗 + atlas、地形贴花、跨服换图、断点续连 |
| **M4 社交** | P8 组队 / 好友 / 商店 / 交易 / 仓库 / **公会**(技能页 + 公会战 + 会徽上/下载)/ **精炼** / **龙魂精炼** | 🟡 首版(12 件) | 龙魂属性表 + 结果动画、私人商店打磨、公会等级/日志页 |
| **特效** | P5 EffectLib`.mse` → GPUParticles | 🟡 首版 | `.dds` 纹理、`.mde` mesh、绑骨骼、skill / 命中特效表 |
### 下一步优先级(都是「首版 → 打磨」)
1. ~~真机字节校准第二轮~~ ✅ 已做(2026-08-30):`GC_SKILL_LEVEL_NEW`(0x021B,本服才发这个,
之前技能窗全空) + `GC_QUICKSLOT_*`(0x0519/1A/1B) 补上;`GC_NPC_POSITION` / `GC_ITEM_UPDATE` /
`GC_CHAT` / `GC_AFFECT_ADD` / `GC_SPECIAL_EFFECT` / `GC_QUEST_INFO` 逐字节确认全对。详见 `CLIENT-PORT.md` §3。
2. ~~换装完整~~ ✅ 已做(2026-08-30):`Metin2Model` 加第二刚体挂点(盾,`Bip01 L Hand``_load_attach`
抽公共加载器);`equip_model` 补 WEAR_SHIELD / WEAR_HEAD(头盔覆盖 hair 槽)/ 头发随 `parts[HAIR]`
**armor→shape 真表**proto 读器加 `alValues[6]`@191`equip_model._armor_shape_default` = `item_proto
values[3]`(非 0)否则 =vnum(对齐 `__ArmorVnumToShape`+ armor `specular``model.specular_power`
(§2.7)。**时装**Metin2 不发独立 costume 槽包,服务器把 costume vnum 写进主角 `parts[ARMOR/HAIR]`
`equip_model` 每槽改成 `_eff()``parts[]` 非 0 优先,否则回退 `get_equipment()`),大编号 `parts[HAIR]`
`item_list.model`(假发)。`GC_CHARACTER_UPDATE` parts 变 → 推 `Info` 变更触发全刷。也是渲染
远端玩家外观的同一条路。
3. ~~被动 / 支援技能树~~ ✅ 已做(2026-08-30):`skilldesc.txt` 列偏移修正(旧代码差 1 列,`motion_idx`
一直读成 0);`skill_ui` 三分类页 `[主动][辅助][坐骑]``for_category`),被动技能无 `[]``master_type`
显示为 `Lv X M/G/P`
4. ~~技能特效表~~ ✅ 已做(2026-08-30):`fx/skill_fx.gd`skill id + master → `<motion_name>_<N>``skilldesc`
col13),`GC_SPECIAL_EFFECT` id → `SPECIAL_FX` 小表;`quickbar.skill_activated` + `effect_cue.special` 接上。
5. **全量资产** —— 换完整 monster / npc / effect 包,`mob_view` / `effect_registry` / `skill_fx` 立刻全量。
6. ~~公会 + 精炼~~ ✅ 已做(2026-08-30):`GC_GUILD`(0x0730) INFO/LIST/GRADE 解析 + `ui/guild_ui.gd`G 键,
真机 `[GM-TEAM]` 验证);`GC_REFINE_INFO`(0x051D) + `ui/refine_ui.gd``refine_ask` 弹框 → `refine(pos,type)`)。
~~公会战 / 技能页~~ ✅ 已做(2026-08-30):`GUILD_GC_SKILL_INFO`(12)/`WAR`(15)/`GUILD_WAR_LIST`·`END_LIST`(17/18)/
`WAR_POINT`(19)/`NAME`(16) 解析 + `GUILD_CG_USE_SKILL`(9)`M2Client` `get_guild_skill`/`get_guild_wars`/
`use_guild_skill`/`declare_guild_war`(`/war` 命令) + 4 信号;`guild_ui` 改三页 Tab(成员/技能/公会战)。
9. ~~会徽上传~~ ✅ 已做(2026-08-30):`CG_MARK_UPLOAD`(0x0C03`{gid, u8 image[768]}`) / `CG_GUILD_SYMBOL_UPLOAD`
(0x0722) + 追加文件字节。`MarkClient``Mode`Download/UploadMark/UploadSymbol+ `set_upload_mark/_symbol`
+ `upload_done()``NetStream.send_pending()` 归零即完成)。`M2Client.upload_guild_mark(host,port,gid,Image)`(自动转
RGBA8 + 缩放 16×12/ `upload_guild_symbol` / `get_mark_server` + `guild_mark_uploaded(ok)``guild_ui` 成员页
「上传会徽」按钮。`net.loopback_flow``MockServer::Mode::Mark` 真 socket 校验字节。
7. ~~龙魂精炼~~ ✅ 已做(2026-08-30):`GC_DRAGON_SOUL_REFINE`(0x051F) / `CG_DRAGON_SOUL_REFINE`(0x050D
`grid[15]`) + `DS_SUB_*` 枚举;`EntityStore.DragonSoulCue` + `m_dragon_soul[180]``WINDOW_DRAGON_SOUL`
`mut_slot`);`M2Client.ds_refine(mode, cells)` + `ds_window_open` / `ds_refine_result``ui/dragon_soul_ui.gd`
(L 键 / 自动弹,3 模式 + 15 格背包右键填 + [执行])。剩龙魂属性表、精炼结果动画。
8. ~~会徽下载~~ ✅ 已做(2026-08-30):`MARK_*`0x0C0x/0x0B15)独立连接。`NetStream``on_cipher_active()`
+ `on_raw()` 钩子让子类自定义分帧(`GC_MARK_*` 用 u32 `buf_size`)。`mark_image.{h,cpp}` `MarkImageSet`
LZO1X 解 64×48 块 → 512×512 → `mark_pixels`+ `mark_client.h` `MarkClient`KX → `CG_MARK_LOGIN{handle,
random_key}``CG_MARK_IDXLIST` → 逐图 `CG_MARK_CRCLIST`(全 0) → `GC_MARK_BLOCK`)。`M2Client.download_guild_marks`
/ `get_guild_mark_image``Image``guild_ui` 显示 16×12 会徽;`serverinfo` `mark_port` 第 8 列(0=跳过);
`app_flow` 进游戏连、`guild_mark_updated` 冷却重连。CTest `net.guild_mark` + `guild_mark_test.gd`。**未接真机**
(联调服未开会徽服端口)。
9. **私人商店打磨**、商店 `START_EX`、道具图标网格 —— 按需。(拍卖行不在 m2dev-client 内)
## 通用做法
1. **每个 P 先做 💻 部分**`wire.h` 加封包 + `EntityStore` 解析 + 合成封包单测(`net_entity_test` 风格)+ 假 client 驱动 UI`netbridge_test` 风格)。
2. 🌐 端到端用 `build/extension/net_e2e`(真机跑 auth→game→选人→进游戏);`MT_NET_TRACE=1` 逐包 header/len/consumed、`MT_NET_DUMP=1` 逐包 hex,对字节用。
3. UI 一律走 P1 的 uiscript 装载器,不手搓布局。
4. 封包号 / 结构以 `../m2dev-client-src-main/src/UserInterface/Packet.h` 为准,`static_assert` 尺寸;**真机字节和参考头文件不符时以真机为准**(已踩:`GC_LOGIN_SUCCESS4``bType``CG_CLIENT_VERSION`)。
5. 玩法逻辑尽量**服务器驱动、客户端只渲染**(尤其 quest / 商店 / 公会),少搬业务规则。