Files
mtgodot-poc/docs/PORT-PLAN.md
T
shenleiandClaude Opus 5 dcefc83211 docs(port-plan): add 2A/2R/2V prerequisites after plan review
- keep 40250 widths/overflow semantics (Win32 long = 32-bit), fixed-width
  serialized structs with static_assert, instead of copying C type names
- 2A base batch (types, header closure, port_logic target, platform stubs,
  header gate, port-map re-baseline) and include-order porting
- 2R pack inventory: six EPK compression types, HybridCrypt key source,
  index override order, path case, mobile delivery
- 2P: shared source list, per-platform pyconfig.h, all five platforms
- 2V minimal vertical slice; old and new paths coexist until wired
- strict asset tests (MT_ASSETS vs M2_ASSETS), RUN_AS_IS instead of N_A,
  void the 6 pre-architecture 'done' functions

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 18:38:04 +09:00

213 lines
18 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.
# 40250 1:1 移植:路线与方案
本文是移植工作的总入口,换一台电脑继续开发时先读这份。更新日期:2026-09-22。
- 方法细则:`.agents/skills/metin2-40250-parity-audit/SKILL.md`(每轮的做法、状态定义、工具)
- 批次队列:`audit/remediation-roadmap.md`
- 逐函数进度:`audit/port-map/**`;每轮一行记录:`audit/history.jsonl`
- Python UI 评估:`docs/PYTHON-EMBED-EVAL.md`
- 旧方案 `docs/CLIENT-REFACTOR-40250-PLAN.md`("形式上不照搬类文件")已被本文取代
## 1. 目标与原则
metin2-client 是 40250 Windows 客户端的跨平台版本(macOS / Windows / Linux / Android / iOS)。
除渲染和平台 API 外,所有玩法算法、分支、常量、状态顺序、计时来源、数据来源、封包副作用和清理路径都以 40250
为准。**40250 源码就是规格,不设计行为,只照抄。**
- 工作单位是 40250 的一个源文件(或超大文件里的一组函数),不是某个行为分支。
- 当前代码里没有 40250 对应物的逻辑是缺陷,要删掉。
- 运行时不会加载的代码不算实现,它的测试也不算证据。
## 2. 已定的决定
| 日期 | 决定 | 依据 |
| --- | --- | --- |
| 2026-09-22 | **逻辑层按 40250 结构移植**:C++ → C++,文件名、类名、方法名、成员名、语句顺序都保持一致 | 能逐文件机械核对,不再"找差异" |
| 2026-09-22 | **UI 走内嵌 CPython 2.7.18**(方案 A),原样运行 40250 的 `root/*.py``uiscript/`,只移植 C++ 模块 | 脚本无需改动;macOS 和 Android 真机均已验证,见 `docs/PYTHON-EMBED-EVAL.md` |
| 2026-09-22 | 数据源统一改为 40250`Client/Eternexus` 的 root/uiscript/locale,以及 `Client/pack` 的资源),**不再用**仓库里 m2dev 版本的 `assets/root``assets/uiscript``assets/locale` | 两者有实质差异,见第 5 节 |
| 2026-09-22 | 现有 `project/ui/*.gd`(60 个文件,依据的是错误版本的脚本)不再维护,由 Python 层取代 | — |
| 2026-09-22 | 照抄时保持 40250 的**宽度和溢出语义**Win32 下 `long` 为 32 位),序列化结构用定宽类型并 `static_assert` 大小 | 64 位平台 `long` 是 8 字节,机械照抄会破坏 proto/封包/EPK 布局和 TEA |
| 2026-09-22 | 迁移期新旧路径共存,新路径接通运行后才在同一提交删除旧逻辑;先做最小纵向切片 2V | `main` 始终可玩;运行时不可达的新代码不算实现 |
## 3. 三个层和代码位置
| 层 | 40250 单元 | 做法 | 位置 |
| --- | --- | --- | --- |
| `logic` | `UserInterface/``GameLib/``EterLib` 的网络/计时/文本解析、`EterPack``EterLocale` | 逐文件照抄 C++ | `extension/src/port/<Lib>/<File>.{h,cpp}` |
| `python` | `root/*.py``uiscript/``UserInterface/*Module.cpp``EterPythonLib/``ScriptLib/` | 脚本原样运行;`*Module.cpp`、窗口系统、`CPythonLauncher` 照抄 | `extension/src/port/<Lib>/` + `extension/third_party/cpython-2.7.18/` |
| `platform` | Direct3D/`Grp*`、Granny、Miles、SpeedTree、特效/地形渲染、Win32 窗口/输入/IME、线程、反作弊 | 适配层,接口与 40250 调用方看到的一致,按可观察输出核对 | `extension/src/platform/` + 现有渲染代码 |
```
extension/src/port/UserInterface/InstanceBase.cpp # 40250 同名文件的照抄
extension/src/port/GameLib/ActorInstance.cpp
extension/src/port/ScriptLib/PythonLauncher.cpp
extension/src/port/EterPythonLib/PythonWindow*.cpp
extension/src/port/UserInterface/*Module.cpp # app/net/player/chr/... Python 模块
extension/src/platform/ # CGraphicThingInstance、CSoundManager 等同名适配接口
extension/third_party/cpython-2.7.18/ # 静态库(2P 批次加入)
```
逻辑层规则:
- 40250 调用平台类的地方,调用 `extension/src/platform/` 下同名的适配接口,ported 代码里不直接写 Godot 调用。
- 40250 的单例(`CPythonPlayer``CPythonCharacterManager``CPythonNetworkStream`)归扩展所有,GDScript 不持有玩法状态。
- **保持 40250 的宽度和溢出语义,不是机械保留 C++ 类型名。** 40250 是 Win32ILP32/LLP64):`long`/`unsigned long`
是 32 位,指针是 32 位。64 位的 macOS/Linux/Android/iOS 上 `long` 是 64 位,照抄会改变结构大小、TEA 的读取宽度和
溢出行为。统一由 `port/common/Win32Types.h`(批次 2A)把 `LONG`/`DWORD`/`long` 等映射到 `int32_t`/`uint32_t`
- 凡是参与序列化的结构(proto 记录、封包、EPK 索引、msa/msm 二进制)一律用定宽类型,`#pragma pack` 与 40250 相同,
并对 40250 已知大小加 `static_assert(sizeof(...) == N)`(如 `TItemTable` == 156);
- 纯计算里的 `long` 如依赖 32 位回绕,也换成 `int32_t`
- 指针存进 `DWORD` 的写法(句柄、`SetUserData` 等)改成 `uintptr_t`,并在 port-map 的 `note` 里记下。
- 单位保持 40250 的(`TPixelPosition` 用 cm,时间是 `ELTimer_GetMSec` 的毫秒,角度用度),只在适配层换算。
- 迁移来源:`net_play.gd``net_world.gd``game_scene.gd``player_controller.gd``entity_store.cpp`。**旧逻辑只在新路径
已接通运行时之后才删除**(见第 4 节"迁移方式"),删除与接通在同一次提交完成。
- 迁移结束后 Godot 这边只剩:场景节点、渲染适配、一个把输入转发给 `CPythonWindowManager` 并每帧调用 Update/Render 的宿主 Control。
## 4. 路线
| 批次 | 内容 | 状态 |
| --- | --- | --- |
| 0 | 工具:参考根解析、`port_map.py`、构建/测试入口 | 完成(55f733fd、5afd5a4f |
| 1 | 删除运行时不加载的 106 个 `*_system.gd` 及其测试 | 完成(a989b8f1 |
| — | 定下目录结构,评估内嵌 Python;Android 独立程序验证 | 完成(9d0e50de、b02c49bb |
| **2A** | 基础:Win32 类型层、参考公共头的最小闭包、`port_logic` CMake 目标、platform 接口骨架、头文件可编译门禁、port-map 重新基线 | **下一步** |
| **2R** | 资源包能力盘点:EPK 类型/密钥/覆盖顺序/路径大小写/移动端交付 | **下一步**,可与 2A 并行(只读分析 + 独立工具) |
| 2P | CPython 2.7.18 编进 libmtgodot,五个平台分别配置和验证 | 2A 之后 |
| 2D | 数据源切到 40250msm 路径、proto、资源根、严格资源测试) | proto 部分在 2A 之后;资源根在 2R 之后 |
| **2V** | 最小纵向切片:`CPythonLauncher``system.py``wndMgr` → 登录/选角 → GamePhase → 本地角色移动 | 需要 2A、2P、2D;**这是第一个可运行的新架构里程碑** |
| 2 | 其余 P0 角色/移动单元,按依赖拓扑移植 | 2V 之后 |
| 3 | P1 战斗/技能 → P2 游戏阶段封包 → P3 物品;Python 层(窗口系统 → `*Module.cpp` | 未开始 |
| 4 | `NEEDS_LIVE` 真服验证 | 未开始 |
### 迁移方式:始终保持可玩
旧 GDScript 路径和新的 ported 路径在切片接通前共存,由构建/运行开关选择(例如 `MT_PORT_PATH=legacy|port`,默认
`legacy`)。规则:
- 新单元先编译进 `port_logic`,但**只有被新路径的运行时调用到**,才算实现("运行时不可达不算实现"同样适用于新代码);
没有接通前,port-map 状态保持 `TODO``note` 写"已照抄,未接通"。
- 每个接通步骤的提交同时:切换调用方到新路径、删除被取代的旧逻辑、跑一遍能覆盖该路径的运行测试(离线 FakeClient
或真服 e2e)。
- `main` 在任意提交上都能进游戏走动;`legacy` 路径在 2V 完成、默认值切到 `port` 并稳定后整体删除。
### 批次 2A:基础
1. `extension/src/port/common/``Win32Types.h``BYTE`/`WORD`/`DWORD`/`LONG`/`BOOL`/`UINT`/`HANDLE` 等的定宽映射)、
40250 用到的 Win32/CRT 宏和函数(`ZeroMemory``_snprintf``stricmp``timeGetTime` 等)的最小实现、
`StdAfx.h` 等价物。
2. 参考公共头的最小闭包:从第一批要移植的单元出发(`PythonPlayerEventHandler.h` 依赖 `ActorInstance.h``FlyHandler.h`
`PythonNetworkStream.h``InstanceBase.h`),用脚本列出 `#include` 闭包,把闭包里的头文件先照抄为可编译的声明。
3. `port_logic` 静态库 CMake 目标,链接进 `libmtgodot`;在 macOS 和 Android 两个工具链上编译。
4. `extension/src/platform/` 接口骨架:闭包里出现的平台类(`CGraphicThingInstance``CSoundManager` 等)只声明
40250 调用方用到的方法,先给空实现。
5. 门禁:`port/**` 下每个头文件单独编译通过(header self-containment),序列化结构的 `static_assert` 全部通过。
6. port-map 重新基线(见第 6 节)。
7.`#include` 依赖图生成批次 2 的移植顺序(拓扑序),替换原来"互不共享实现文件即可并行"的假设:共享头文件的单元,
头文件由先做的那个单元负责,后面的单元只能在它合入后开始。
### 批次 2R:资源包能力盘点
40250 的读取链不只是 `EterPack.cpp`,还有 `EterPackManager.cpp`(多包覆盖顺序、路径归一化)、
`EterPackPolicy_CSHybridCrypt.cpp`HybridCrypt,密钥可能来自登录/握手阶段的服务器下发)、`CMappedFile``CLZO`
以及 TEA/Panama/Camellia/Twofish/XTEA。压缩类型有六种:`NONE``COMPRESS``SECURITY``PANAMA`
`HYBRIDCRYPT``HYBRIDCRYPT_WITHSDB``Client/pack` 有 217 个文件、约 1.3 GB。
1. 写一个只读的扫描工具(Python,放在 `tools/`),解析所有 `.eix`,统计每个包、每种 `compressed_type` 的文件数和字节数。
2. 按统计结果决定:
- 只有 `NONE/COMPRESS/SECURITY` 的包可以离线解出;
- `PANAMA`/`HYBRIDCRYPT*` 如存在,确认密钥来源:本地的 `Index`/配置,还是服务器 `GC_HYBRIDCRYPT_KEYS`/`SDB` 下发。需要服务器密钥的包,要么移植密钥链,要么在开发期用一次真服登录抓取密钥后离线解包(密钥不进仓库)。
3. 验证 `Index` 文件列出的包顺序和同名文件的覆盖优先级,与 `CEterPackManager` 一致。
4. 路径归一化:40250 在 Windows 上大小写不敏感,并把 `d:/ymir work/` 等前缀映射到包内路径。确定统一的小写化规则,扫描大小写冲突。
5. 移动端交付:决定最终形式(例如解包后重新打成我们自己的 `mtpack`,或直接在设备上读 EPK),给出 Android/iOS 的包体积、首包与按需下载的划分,以及更新方式。`MT_ASSETS` 只是开发期覆盖。
6. 输出写进本文第 5 节,再决定 2D 的资源根切换方式。
### 批次 2P:内嵌 Python 集成
1. CPython 2.7.18 源码放进 `extension/third_party/cpython-2.7.18/`。**共用源码清单和静态模块清单**`Modules/Setup` 中启用的 C 模块,见 `tools/py_embed_android/build-and-run.sh`);**每个平台各自一份 `pyconfig.h`**(由该平台的 configure 生成后提交,或 Windows 用 `PC/pyconfig.h`),因为它是对目标平台类型大小和系统 API 的探测结果,不能共用。已知平台差异:Android API 24 需关掉 `HAVE_LANGINFO_H`
2. 静态链接进 `libmtgodot`,照抄 `ScriptLib/PythonLauncher.cpp`
3. `pack` 模块通过 `asset_io` 读取,标准库的纯 Python 文件放进资源包,由 `system.py` 自带的导入钩子加载。
4. 五个平台分别验证:编译、链接无未定义符号、`Py_Initialize`、静态 C 模块逐个 `import`、在 app 进程里跑 `system.py``prototype.RunApp()`(对照结果:74 个模块中 66 个加载成功,引导期调用 33 个 C++ 函数)。
| 平台 | 状态 |
| --- | --- |
| macOS arm64 | 系统 Python 2.7.18 跑通 spike;静态嵌入未做 |
| Android arm64 | 独立可执行文件在真机跑通(b02c49bb);APK 进程内未做 |
| iOS arm64 | 未做 |
| Linux x86_64 | 未做 |
| Windows x64 | 未做(用 `PCbuild` 的源码清单和 `PC/pyconfig.h`,不走 configure |
## 5. 数据源切换:m2dev assets → 40250
现在的渲染和逻辑读的是 `assets/`,这是从 m2dev 的包解出来的。2026-09-22 与 40250 `Client/Eternexus` 逐项对比的结果:
| 数据 | 读取方 | 与 40250 的差异 | 处理 |
| --- | --- | --- | --- |
| `root/npclist.txt` | `mob_view.gd``m2_client.cpp` | 相同 | 无 |
| 8 个 `*.msm`(种族模型、挂点、染色) | `equip_model.gd``player_view.gd` | 内容相同,位置不同:m2dev 在 `root/msm/<cls>_<m/w>.msm`40250 在 `root/<cls>_<m/w>.msm``LoadLocalRaceData("warrior_m.msm")`) | 查找列表加上 40250 的位置 |
| `playersettingmodule.py` 的动作注册(`SetGeneralMotions``__LoadGame*Ex`)和连击表 | `motion_registry.gd``net_play.gd` | 相同 | 40250 的文件是 CP949 + CRLF,切换后跑一遍解析测试 |
| `playersettingmodule.py` 的特效注册 | 暂无 | 40250 多了 `EFFECT_LEVELUP_*_FOR_GERMANY``EFFECT_EMPIRE+1..3`,少了 `EFFECT_AGGREGATE_MONSTER` | 移植特效时按 40250 |
| `atlasinfo.txt``grpblk.txt` | — | 相同 | 无 |
| `uiscript/uiscript` | 现有 `project/ui/*.gd` | 40250 有 94 个,m2dev 有 80 个;共有的 71 个中,去掉空白差异后有 11 个不同(建角/选角/密码/信使/公会等) | 由 2P 直接运行 40250 的 uiscript,不单独处理 |
| `item_proto` | `proto.cpp` | **读不了**40250 每条 156 字节(`TItemTable`,含 `long` 字段,需按 32 位布局),m2dev 236 字节;40250 用经典 TEA32 轮,`0x9E3779B9`,按 32 位字处理),m2dev 用 XChaCha20 | 按 40250 移植(需 2A 的类型层) |
| `mob_proto` | `proto.cpp` | **读不了**:每条 255 字节 vs 335 字节,加密同上 | 同上 |
| `item_list.txt` | `item_list.gd` | 177 行不同(m2dev 有 dummy 条目等);40250 放在 `locale/en/`m2dev 在 `locale/common/` | 按 40250 路径读取 |
| `locale_interface.txt``itemdesc.txt``skilldesc.txt``skilltable.txt` | UI、技能表 | 20 行 / 2 行 / 2 行 / 位置不同 | 按 40250 路径读取 |
| 模型、贴图、地图(PC、Monster、Outdoor 等) | 渲染 | **未核对**40250 是经典 `.eix/.epk` 包 | 由 2R 决定 |
切换步骤(批次 2D):
1. msm 查找路径加上 `root/<cls>_<m/w>.msm`
2. proto 读取按 40250 移植:`EterBase/tea.cpp``GameLib/ItemData.h``TItemTable``CPythonNonPlayer` 的 mob 表,放进镜像文件(使用 2A 的定宽类型和 `static_assert`),替换 `extension/src/proto/proto.cpp` 里的 m2dev 格式。
3. 资源测试严格模式:
- 统一资源环境变量:C++ 测试(`extension/CMakeLists.txt` 目前只透传 `M2_ASSETS`)和 GDScript`MT_ASSETS`)统一读 `MT_ASSETS`,过渡期两者都透传;
- 新增 `MT_ASSETS_STRICT=1`:指定了 40250 资源时,缺文件必须失败,不能跳过;
- 为 40250 的 root、locale、proto 和关键包生成来源清单(路径 + sha256),提交到 `audit/`,测试开始时核对。
4. 资源根切换方式按 2R 的结论执行,跑渲染和解析测试,更新本节状态。
## 6. 进度与 port-map 基线
实时数字用 `port_map.py status` 查看。
2A 时做一次重新基线,之后的数字才代表新架构的进度:
- 之前"完成"的 6 个函数(`PythonPlayerEventHandler.cpp`)作废:OnMove/OnMoving/OnStop 的实现在 `net_play.gd`,属于迁移来源;
3 个 `N_A`(单例、构造、析构,理由是"由 NetPlay 场景节点持有")与"单例归扩展所有"冲突。全部回退为 `TODO`
- 原样运行的 Python 脚本函数**不标 `N_A`**`N_A` 只用于没有玩法语义的平台胶水)。新增状态 `RUN_AS_IS`
`impl` 指向随包运行的原脚本,`evidence` 要求该函数所在模块在目标平台的运行证据(导入成功且被运行路径调用到)。
需要同步修改 `port_map.py``references/audit-schema.md`
2026-09-22 基线前的数字(仅供参考):logic 3285 个函数、python 4890、platform 2229,完成数视为 0。
## 7. 换机器后的环境准备
目录布局(路径都相对 `~/Work/mt/`,工具按相对路径查找):
```
~/Work/mt/metin2-client/ # 本仓库(git remote origin
~/Work/mt/40250/Server Client TMP4/ # 40250 参考(不在 git 里,需自行复制)
ClientVS22/source/ # C++ 参考根(audit/manifest.json 的 reference_root
Client/Eternexus/root, uiscript, locale # Python 参考
~/Work/mt/metin2-client/assets/, bgm/ # 游戏资源(gitignored,需自行复制;或用 MT_ASSETS 指到别处)
```
- 参考源放在其他位置时,设置 `MT_40250_SOURCE=<.../ClientVS22/source>`
- Python 2.7.18:用于运行 `py_embed_spike.py`。macOS 安装 python.org 的 2.7.18 包,确认 `python2.7 --version`
- Python 3:用于 `port_map.py` 和其他审计脚本。
- AndroidNDK 27.2`ANDROID_NDK_HOME` 指向它;未设置时脚本选版本号最低的 NDK)、openjdk@21、Godot 导出模板;
`adb``/opt/homebrew/bin/adb`。APK 导出用 `./export-android.sh`
- 真服账号、密码不写进仓库或 audit 账本。
新会话恢复工作:
```bash
cd ~/Work/mt/metin2-client
git pull
python3 .agents/skills/metin2-40250-parity-audit/scripts/port_map.py status
python3 .agents/skills/metin2-40250-parity-audit/scripts/port_map.py queue --limit 20
python3 .agents/skills/metin2-40250-parity-audit/scripts/port_map.py check
python2.7 .agents/skills/metin2-40250-parity-audit/scripts/py_embed_spike.py --out /tmp/py_embed_spike.txt # 可选:复核 Python 原型
tools/py_embed_android/build-and-run.sh # 可选:连接 Android 真机复核
```
然后按第 4 节标为"下一步"的批次继续,每完成一步在第 4 节(及 2P 平台表、第 5 节)更新状态并提交。