Files
mtgodot-poc/docs/PORT-PLAN.md
T
shenleiandClaude Opus 5 369a80fc10 port(2V0-a): EterPythonLib 窗口系统的头进镜像
2V0 的入口实测:audit/slices/2V0.json 的 212 个头里 164 个已经在镜像中,缺 48 个
(1 个 EterLib、5 个 EterPythonLib、42 个 UserInterface);10 个单元里已移植 2 个。
本轮补 EterPythonLib 这 5 个,外加必须先有的 EterPythonLib/StdAfx.h —— port_header_gate
按 40250 的做法把每个头放在所属库的 StdAfx.h 后面编译,没有它这五个既看不到
<Python.h> 也看不到 STL 和 CSingleton。

6 个文件全是 port_copy.py 的机械拷贝,0 处手改。

顺带修正清单的一处误报:EterLib/Dimm.h 不是 40250 的源文件,是 Windows SDK <dimm.h>
的 MIDL 产物,port/common/shim/win32/Dimm.h 已经顶掉了;镜像一份反而遮住 shim,
EterLib/IME.h 的 #include "Dimm.h" 会去找 rpc.h。拷进来验证后删掉。

只有头,没有 .cpp,所以没有 port-map 条目 —— 只有声明,没有可调用的东西。
port_header_gate 新增 6 个 TU 全绿;ctest 27/27。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 10:50:24 +09:00

543 lines
49 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 arm64 和 Android arm64**2026-09-23 定);
iOS arm64 保持交叉编译干净,但不作为验收目标。**Linux 和 Windows 不做**——不验收、不作为任何步骤的阻塞项。
仓库里的 mingw-w64 交叉编译只留作可移植性门禁(它这次抓到了 `UserInterface/StdAfx.h` 的真问题),
不代表支持 Windows,也不要为它投入额外工作。
除渲染和平台 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 | 迁移期新旧路径共存,新路径接通运行后才在同一提交删除旧逻辑;纵向切片拆成 2V0–2V3 | `main` 始终可玩;运行时不可达的新代码不算实现 |
## 3. 三个层和代码位置
| 层 | 40250 单元 | 做法 | 位置 |
| --- | --- | --- | --- |
| `logic` | `UserInterface/``GameLib/``EterLib` 的网络/计时/文本解析、`EterPack``EterLocale``EterBase` 的纯逻辑单元(`tea``lzo``cipher``Random``Stl``Timer``Poly/` | 逐文件照抄 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、线程、反作弊、`EterBase` 的文件/OS 单元(`CPostIt``CRC32``Debug``FileBase``FileDir``FileLoader``MappedFile``TempFile``Utils``error``CRC32``Utils` 里的纯函数在 platform 实现里照抄) | 适配层,接口与 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/port/EterLib/GrpBase.h # 平台类的 40250 头文件同样照抄在镜像路径(接口)
extension/src/platform/ # 这些平台类的实现(CGraphicThingInstance、CSoundManager 等)
extension/third_party/cpython-2.7.18/ # 静态库(2P 批次加入)
```
逻辑层规则:
- 40250 调用平台类的地方,调用 `extension/src/platform/` 下同名的适配接口,ported 代码里不直接写 Godot 调用。
闭包里的 40250 头文件**不分逻辑层和平台层,一律照抄到镜像路径**(`port/EterLib/GrpBase.h` 等),作为平台类的接口,
这样原来的相对 include`../eterLib/StdAfx.h`)保持不变;`platform/` 只放这些类的 `.cpp` 实现,不另写同名头文件。
- 照抄只做机械转换,用 `port_copy.py copy <Lib/File>`CP949 转 UTF-8、CRLF 转 LF、`#include "..."` 路径改成磁盘上的
大小写;含非 ASCII 字符串字面量的文件拒绝照抄,需手工处理。其他任何改动都是手工修改,行上标 `// PORT:` 并写原因,
`port_copy.py diff` 列出全部手工修改。
- 40250 源码里的系统头保持原样 include,由 `port/common/shim/` 提供替身:`shim/sdk/``d3d8.h``d3dx8.h``mss.h`
所有平台都用,只含值类型和不透明接口)、`shim/win32/``windows.h``winsock.h``mmsystem.h` 等,仅非 Windows 平台用)。
- 40250 的单例(`CPythonPlayer``CPythonCharacterManager``CPythonNetworkStream`)归扩展所有,GDScript 不持有玩法状态。
- **保持 40250 的宽度和溢出语义,不是机械保留 C++ 类型名。** 40250 可执行文件目标是 32 位 Win32ILP32):
`long`/`unsigned long` 和指针都是 32 位;移植目标则可能是 LP64 或 LLP64。`port/common/Win32Types.h`(批次 2A
只为 `BYTE`/`WORD`/`DWORD`/`LONG`/`BOOL`/`UINT` 等 Win32 **标量别名**提供定宽定义,不能重定义 C++ 关键字 `long`
- 凡是参与序列化的结构(proto 记录、封包、EPK 索引、msa/msm 二进制)一律用定宽类型,`#pragma pack` 与 40250 相同,
并对 40250 已知大小加 `static_assert(sizeof(...) == N)`(如 `TItemTable` == 156);
- 纯计算里的 `long` 按语义改成 `int32_t`/`uint32_t`;依赖 32 位回绕时使用无符号运算或显式 wrapping helper,不能依赖
C++ 有符号溢出的未定义行为;
- `HANDLE``HWND``WPARAM``LPARAM` 等句柄/指针类型保持指针宽度并隔离在 platform adapter;指针存进 `DWORD`
的原写法改成 `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 重新基线 | **进行中**(剩 Python 库闭包头,等 2P |
| 2R | 资源包能力盘点:EPK 类型/密钥/覆盖顺序/路径大小写/移动端交付 | 完成:只有 NONE/COMPRESS/SECURITY,不需要服务器密钥;设备上直接读 EPK |
| **2P** | CPython 2.7.18 编进 libmtgodotmacOS arm64 + Android arm64 分别配置和验证 | **进行中**step 1(静态库 + macOS/Android/iOS 配置)、step 2ScriptLib 进 `port_logic`)、step 3a/3bpython27.zip + `PythonHost` + `pack` 模块)完成 |
| 2D | 数据源切到 40250msm 路径、proto、资源根、严格资源测试) | 完成(5a26e93f、232d0461、240d2827、a9b31dec);资源根只切了 proto/pack 后端,其余按第 5 节由 2V 切片逐个切换 |
| **2V0** | UI 壳:真实 `PythonLauncher/wndMgr/grp/app` + platform UI,其他玩法模块用桩,显示并操作 Logo/Popup | 需要 2A、2P 和 root/uiscript 子集 |
| **2V1** | 登录/选角:真实 `net` phase、登录/选角脚本及所需模块,走到 Loading | 2V0 之后 |
| **2V2** | GamePhase:真实角色管理、主角创建和最小渲染,进入游戏并显示角色 | 2V1、2D 之后 |
| **2V3** | 本地移动:真实 player/input/event/movement 链,输入、位置和移动封包切到新路径 | 2V2 之后;**第一个可玩的新架构里程碑** |
| 2 | 2V3 未覆盖的其余 P0 角色/移动单元,按依赖拓扑移植 | 2V3 之后 |
| 3 | 剩余 Python 模块和窗口 → P1 战斗/技能 → P2 游戏阶段封包 → P3 物品 | 未开始 |
| 4 | `NEEDS_LIVE` 真服验证 | 未开始 |
### 迁移方式:始终保持可玩
旧 GDScript 路径和新的 ported 路径在切片接通前共存,由构建/运行开关选择(例如 `MT_PORT_PATH=legacy|port`,默认
`legacy`)。规则:
- 新单元先编译进 `port_logic`,但**只有被新路径的运行时调用到**,才算实现("运行时不可达不算实现"同样适用于新代码);
没有接通前,port-map 状态保持 `TODO``note` 写"已照抄,未接通"。
- 每个接通步骤的提交同时:切换调用方到新路径、删除被取代的旧逻辑、跑一遍能覆盖该路径的运行测试(离线 FakeClient
或真服 e2e)。
- 每次接通都同时跑 `legacy``port` 两套路由的相关回归;共享 extension 的改动不能破坏默认的 legacy 路径。
- `main` 在任意提交上都能进游戏走动;`legacy` 路径在 2V3 完成、默认值切到 `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` 等价物。GameLib 的逻辑头直接使用 D3DX 数学类型(`TPixelPosition` 就是 `D3DXVECTOR3`),所以还要一个
与平台无关的 D3DX 数学层(`D3DXVECTOR2/3/4``D3DXMATRIX``D3DXQUATERNION``D3DXCOLOR` 及逻辑层用到的 `D3DX*` 函数),
按 D3DX8 的 float 语义实现。
2. 参考公共头的最小闭包:从第一批要移植的单元出发(`PythonPlayerEventHandler.h` 依赖 `ActorInstance.h``FlyHandler.h`
`PythonNetworkStream.h``InstanceBase.h`),用脚本列出 `#include` 闭包,把闭包里的头文件先照抄为可编译的声明。
3. `port_logic` 静态库 CMake 目标,链接进 `libmtgodot`macOS 和 Android 分别编译(iOS、mingw-w64 作为附带的
可移植性门禁一起编)。某个目标平台的工具链暂不可用时,2A 对该平台保持 `BLOCKED`,不能用其他平台的通过结果替代。
4. `extension/src/platform/` 接口骨架:闭包里出现的平台类(`CGraphicThingInstance``CSoundManager` 等)只声明
40250 调用方用到的方法,先给空实现。
5. 门禁:`port/**` 下每个头文件单独编译通过(header self-containment),序列化结构的 `static_assert` 全部通过。
6. port-map 重新基线(见第 6 节)。**已完成**(2026-09-22)。
7.`#include` 依赖图生成批次 2 的移植顺序(拓扑序),替换原来"互不共享实现文件即可并行"的假设:共享头文件的单元,
头文件由先做的那个单元负责,后面的单元只能在它合入后开始。
**已完成**`port_deps.py`(显式 `#include` + 按符号解析的 StdAfx 隐式依赖)生成 `audit/slices/2V02V3.json`
`audit/slices/batch2-order.json`。结论:每个切片的头文件闭包约 210–220 个(其中平台层约 100 个,来自
`ThingInstance.h`/`GrpBase.h` 等);P0 单元里 `InstanceBase``PythonCharacterManager``PythonPlayer`
`PythonPlayerEventHandler` 的头文件互相包含,必须作为一组串行移植,`ActorInstance.cpp` 排在最前。
2A 当前进度(`script/port_gate.sh` 复现;构建目录 `build-port-gate/`):
- 已完成:`port/common/``Win32Types.h``Win32Crt.{h,cpp}`MSVC `_snprintf` 截断语义、`timeGetTime` 32 位回绕)、
`StdAfx.h`);`port_logic` 目标已链接进 libmtgodot,也可用 `-DMTGODOT_BUILD_EXTENSION=OFF -DMTGODOT_BUILD_PORT=ON`
单独构建;每个 `port/**.h` 独立编译两次的门禁 `port_header_gate``port.common` 测试。编译宏取 40250 发布版
`Metin2Distribute.exe``Distribute|Win32` 配置:所有库 `NDEBUG`GameLib 加 `USE_LOD`UserInterface 加
`USE_LOD;_DISTRIBUTE`;不定义 `WIN32`
- 已完成:D3DX 数学层 `port/common/D3DXMath.{h,cpp}`。内容包括:
- 基础结构 `D3DVECTOR`/`D3DCOLORVALUE`/`D3DMATRIX`
- 类型 `D3DXVECTOR2/3/4``D3DXMATRIX``D3DXQUATERNION``D3DXPLANE``D3DXCOLOR`,运算符照 `d3dx8math.inl`
(例如 `v / f` 实现为乘以 `1/f`);
- 2V3 切片用到的 `D3DXVec*``D3DXMatrix*``D3DXQuaternion*` 函数,采用行向量、左手旋转、D3DX 的乘法顺序;
后续单元用到新函数时再按需补。
测试在 `port.common` 里。
- 已完成(步骤 2,不含依赖 Python 的库):2V3 切片闭包里的头文件照抄,共 173 个,覆盖 EterBase(含 `Poly/`)、
EterLocale、EterPack、EterLib、GameLib、MilesLib、EffectLib、EterGrnLib、SpeedTreeLib、SphereLib、PRTerrainLib、
EterImageLib。
- 门禁中每个头文件前先 include 本库的 `StdAfx.h`40250 把它作为预编译头)。
- D3D8 的值类型(`D3DFORMAT``D3DCAPS8``D3DLIGHT8``D3DMATERIAL8`、渲染状态/变换/纹理阶段枚举、`D3DFVF_*` 等,
按 SDK 取值和布局并加 `static_assert`)放在 `port/common/D3D8Types.h`COM 接口(`IDirect3DDevice8`、纹理/表面/
缓冲区的继承层次等)只做不透明声明,由 platform 持有。
- 第三方 SDK 头由 `shim/sdk/` 提供不透明替身:`granny.h`Granny 2.11 的标量和 `granny_data_type_definition`)、
`SpeedTreeRT.h``CSpeedTreeRT` 只声明头文件里用到的嵌套类型和方法)、`d3dx8.h`(加 `ID3DXMatrixStack`
`D3DX_FILTER_*`)。`EterLib/Dimm.h` 是 SDK `dimm.h` 的 MIDL 输出,按 SDK 头处理:不进镜像,由 `shim/win32/Dimm.h`
(连同 `imm.h``dinput.h`)提供。
- `EterBase/cipher.h` 用 Crypto++`EterBase/lzo.h` 用 LZO`port_logic` 链接已 vendor 的 `mt3p::cryptopp`
`mt3p::minilzo`;单独构建时顶层 CMake 先加入 `extension/third_party`
- 手工 `// PORT:` 修改共 10 个文件(`port_copy.py diff` 列出):
- `Random.h``random`/`srandom` 与 POSIX 重名,改名;
- `Singleton.h``(int)` 指针运算改为 `intptr_t`
- `Pool.h``operator new(unsigned int)` 改为 `size_t`;补 `<algorithm>`MSVC 头文件间接带入 `std::find`);
- `Stl.h``<SSTREAM>` 改为小写;
- `Utils.h`:补 `<math.h>`
- `FlyTarget.h``EffectLib/ParticleSystemInstance.h`:补前置声明(MSVC 会让 friend 声明可见);
- `EffectLib/Type.h`:依赖名前补 `typename`
- `EffectLib/EffectUpdateDecorator.h`:指针转 `(DWORD)` 改为 `(uintptr_t)`
- `PRTerrainLib/StdAfx.h`:去掉 `#include "../ScriptLib/StdAfx.h"`PRTerrainLib 不用 Python,否则每个地形编译单元
都要 CPython 头)。
- 40250 StdAfx 里有影响语义的宏,移植逻辑时要注意:
- EterBase 的 `#define atoi _atoi64`
- UserInterface 的 `_USE_32BIT_TIME_T`
- `EterBase/ServiceDefs.h``_IMPROVED_PACKET_ENCRYPTION_`
- 工程字符集是 MultiByte,所以 `TCHAR` 就是 `char`
- 非 Windows 上 `WCHAR` 是 32 位 `wchar_t`(Win32 是 16 位),只影响 IME 路径,移植 IME 时按 UTF-16 处理。
- ScriptLib、EterPythonLib、UserInterface 的 StdAfx 会 include Python 头,要等 2P 把 CPython 加进来之后再照抄。
- 已完成(步骤 4):platform 骨架 `extension/src/platform/`,目标 `port_platform`(静态库,链接 `port_logic`
libmtgodot 改为链接它;从 `port/CMakeLists.txt` 加入以共用每个库的编译宏;`port_gate.sh` 构建它)。
- 平台层头文件(同名 40250 `.cpp` 属于 platform 层,见 `port_map.py``LAYER_PATTERNS`)对应
`platform/<Lib>/<File>.cpp` 95 个,1843 个函数桩(步骤 6 把 EterBase 的纯逻辑单元改为 logic 层后,删掉了
`Random``Stl``cipher``lzo``tea``Timer``Poly/Poly``Poly/SymTable` 的 8 个桩文件)。
- 其中 22 个桩对应 40250 只声明、从未定义的函数(如 `CEffectManager::GetRenderingEffectCount``Mutex::Trylock`),
40250 里没有调用方,不算待移植函数。
-`platform_stub.py gen` 生成:用 clang 对门禁编译单元按类名/函数名 `-ast-dump-filter` 导出声明,定义头文件里
声明了但没有定义的成员函数、构造/析构、自由函数和静态成员;返回类型写成尾置形式,使类内嵌套类型在类作用域里解析,
静态成员写成 `decltype(C::x) C::x{};`。已存在的文件不覆盖(除非 `--force`)。
- 函数体是 `MT_PLATFORM_STUB()` 加默认返回值(`platform/PlatformStub.h`);`grep -r MT_PLATFORM_STUB` 就是还没实现的
平台函数清单,设置环境变量 `MT_PLATFORM_STUB_TRACE` 后每个桩第一次被调用时打印到 stderr。
- 6 个构造函数手工补了与 40250 相同的初始化列表(基类 `CResource(c_szFileName)``CDecal` 的 const 成员)。
- 未覆盖:platform 层 `.cpp` 定义的**逻辑层头文件**成员(`GameLib/MapOutdoorRender*.cpp``MapOutdoorWater.cpp` 定义
`MapOutdoor.h` 的方法等)。这些在对应逻辑单元移植时手工补桩。
- 已完成(步骤 6):port-map 重新基线,见第 6 节。
- 未完成:依赖 Python 的三个库的闭包头(等 2P)。
| 平台 | port_gate |
| --- | --- |
| macOS arm64 | PASS(含 `port.common` 测试) |
| Android arm64(脚本取已装的最新 NDK,本机 28.2API 24 | PASS |
| iOS arm64 | PASSXcode,未签名) |
| Windows x64 | mingw-w64 PASS(只是可移植性门禁,不是目标平台;MSVC 不做) |
| ~~Linux x86_64~~ | 不做(2026-09-23 定,见第 1 节) |
已知的无关问题:完整 Android 扩展构建在 `extension/src/net/text_codec.cpp` 失败(bionic 从 API 28 才声明 `iconv`
而构建用 API 24;c9389431 引入);仓库里的 `build-android/` 缓存还指向旧路径 `mtgodot-poc`
### 批次 2R:资源包能力盘点(完成,2026-09-22)
工具:`tools/epk_scan/epk_scan.py`(只读)。`summary` 统计并写 `audit/packs/2R-scan.json``verify` 解码全部可本地解的条目。
索引密钥和 SECURITY 密钥运行时从参考树 `EterPack/EterPack.cpp` 读取,仓库里没有密钥。读取链照 40250:`PackInitialize`
`pack/Index`)→ `CEterPackManager::RegisterPack`(每个包再注册 `<name>_texcache`,最后 `RegisterRootPack("pack/root")`
`CEterPack::__BuildIndex``.eix` 全部是 `MCOZ`TEA + LZO 包着 `EPKD` v2,每条 192 字节)→ `Get2`
1. **类型统计**(Index 注册到的 103 个包加 `root`54,891 条,52,609 个不同路径):
| 类型 | 文件数 | 存储 | 解码后 |
| --- | --- | --- | --- |
| `NONE` | 20,727 | 737.2 MiB | 737.2 MiB |
| `COMPRESS`LZO | 29,812 | 592.9 MiB | 1296.8 MiB |
| `SECURITY`TEA + LZO | 4,352 | 2.0 MiB | 6.6 MiB |
| `PANAMA` / `HYBRIDCRYPT*` | **0** | — | — |
`verify --per-type 1000000` 全部 54,891 条解码成功。每个包的类型分布见 JSON 的 `per_pack`
2. **密钥来源**:这个客户端只用编译进客户端的两组 TEA 密钥(`s_adwEterPackKey` 解索引,`s_adwEterPackSecurityKey`
SECURITY 数据)。没有 PANAMA 和 HybridCrypt 条目,所以**不需要服务器下发 IV/密钥**,也不需要真服抓包。
`CAccountConnector``RegisterPack(..., abIV)``GC_HYBRIDCRYPT_KEYS/SDB` 照抄移植即可,但这批资源用不到。
移植后的 `EterPack.cpp` 需要这两组常量;按"密钥不进仓库",由构建时从参考树生成一个 gitignore 的头文件,不直接写进源码。
3. **注册顺序与覆盖**
- `Index` 第 0 行是 `PACK`,后面是 (目录, 名字) 对,共 124 个名字。
- 124 对里有 119 次 `RegisterPack` 找到了包(有几个名字重复出现),去重后 103 个包(含两个
`metin2_patch_dragon_rock*_texcache`),再加 `root`。2D 用移植后的 `CEterPackManager` 复核过(原先写的 136 有误)。
- 缺失的包:106 个 `_texcache`,以及 `locale_{ae,bg,ca,mx,pa,uk,us}`。40250 遇到缺失的包只记日志、继续执行。
- 目录里有、但没注册的文件,40250 永远不会读到:`Outdoor``Psyco`0 字节)、`outdoorempirebattle1`、两个 `.rdch`
`.rdch` 在源码里没有任何引用。
- 查找由 `CEterFileDict` 完成,它是以 `filename_crc` 为键的 `unordered_multimap``GetItem``equal_range` 里返回第一个
同名项。40250 用 VS2022`ClientVS22`)构建,MSVC STL 的 multi 插入会把新元素放在同键已有元素的后面,所以**先注册的
包优先**。`UpdateItem`(后注册的覆盖先注册的)只在带 IV 的注册里用到,这批资源没有。
- 同名条目共 2,282 个,其中 2,194 个内容不同。主要是 `patch1`/`patch2`/`season3_eu`/`metin2_patch_*` 覆盖后面的
`zone``pc*``season2``outdoor*``icon`。**覆盖顺序会影响画面,必须 1:1。**
- 包里有两种打包器留下的格式:
- Ymir 格式:`data_size` 不小于整个 CLZObject,常补到 256 字节的倍数;`data_crc` 按存储字节算。
- 另一种格式出现在 `metin2_patch_sd``metin2_patch_mineral``metin2_patch_mundi` 三个包里:`data_size` 比 CLZObject
短 16 字节,`data_crc` 按解码后的内容算。
- 在第二种格式下,40250 的 COMPRESS 条目仍能读出来:`CLZO::Decompress` 按 LZO 头里的长度读,不看 `data_size`,读到
映射视图里 `data_size` 之后的字节。但 SECURITY 条目过不了 `Get2` 的 CRC 检查。
- 结果是 4 个文件在原版里**加载失败**:`npc/mineral2/wait.msa``effect/background/metinstone_loop_redblack.mse`
`monster2/spider_spawn/{wait.msa,line1.mse}`。第 5 个 `effect/etc/buff/buff_symbol1.mse` 由先注册的 `metin2_patch_eu4`
提供,不受影响。
- 移植的读取器要照这个行为实现:按头部长度读,保留 CRC 拒绝。
4. **路径归一化**
- `ConvertFileName` 只做两件事:ASCII `tolower`,以及 `\``/`。CP949 字节原样保留。
- 包内名字全部已经是小写、只用 `/`52,609 个路径没有大小写冲突,`filename_crc``crc32(name)` 全部一致。
- `d:/ymir work/...` 是包内的真实路径,一共 26,955 个。40250 的相对路径映射已经注释掉,所以调用方必须使用 40250 原样的
路径名,不能再用 m2dev `assets/` 的相对路径。
- 包文件名与 `Index` 大小写不一致,例如 `Etc` 对应磁盘上的 `ETC.eix`。NTFS 下能打开;在移动端和 Linux 上,按包名查找
磁盘文件时要做大小写不敏感匹配。
5. **移动端交付**(已注册包的存储大小共 1337 MiB):
| 分组 | 大小 |
| --- | --- |
| `metin2_patch_*`/`patch1`/`patch2`/`season3_eu` | 692.7 MiB |
| 核心 `d:/ymir work`pc/monster/zone/effect/npc/item…)+ icon/property/textureset | 405.7 MiB |
| sound/bgm | 87.6 MiB |
| season1/2 | 71.5 MiB |
| 其他 14 种语言的 locale | 49.9 MiB |
| 地图包 | 25.9 MiB |
| `locale_en` | 3.5 MiB |
| root + uiscript | 0.6 MiB |
决定:
- **设备上直接读 EPK**,用移植的 `EterPack`/`EterPackManager``asset_io` 读取。不重新打包成 `mtpack`。这样覆盖顺序、
CRC 拒绝等行为天然 1:1,更新时直接替换包文件。
- 首包包含 Index 里除其他语言外的全部包,约 1.29 GB。其他语言按需下载。
- patch 包在覆盖链的最前面,按需下载会改变已有文件的内容,所以不做细粒度按需下载。
- 如果以后要按需下载,任何新到的包都必须**按 Index 顺序重建整个字典**,不能追加到末尾。否则先注册者优先的结果就变了。
- 更新方式是整包替换 `.eix/.epk`,由我们自己的清单记录版本号和 sha256。
6. **2D 的资源根切换**
- 新增 pack 后端:`asset_io``MT_ASSETS` 之外,接上移植的 `CEterPackManager`,按 `pack/Index` 注册。
- 渲染和 GDScript 的资源请求改用 40250 路径,经过 `ConvertFileName` 查找。
- m2dev `assets/` 的散文件保留作开发期覆盖,直到对应读取方切到新路径。
- 严格门禁(第 5 节步骤 3)的来源清单改为记录各个 `.eix/.epk` 的 sha256。
### 批次 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 生成后提交),因为它是对目标平台类型大小和系统 API 的探测结果,不能共用。已知平台差异:Android API 24 需关掉 `HAVE_LANGINFO_H`
**已完成**`mtpython` 静态库(`-DMTGODOT_EMBED_PYTHON=ON`133 个目标文件 = 参考 `libpython2.7.a` 的全集),
`tools/py_embed/gen_pyconfig.sh <平台>` 生成各平台 `config/<平台>/pyconfig.h` 并校验内建模块表一致,
`extension/tests/py_embed_test.cpp`ctest `python.embed`)。三处 vendor 补丁见 `docs/THIRD-PARTY.md`
目标平台是 macOS arm64 和 Android arm64Linux/Windows 不做(见第 1 节)。
2. 静态链接进 `libmtgodot`,照抄 `ScriptLib/PythonLauncher.cpp`
**已完成**`ScriptLib/{PythonLauncher,PythonUtils,PythonMarshal}.{h,cpp}` 进入 `port_logic``mt3p::python`),
`common/shim/sdk/Python-2.7/` 12 个转发头让 40250 的 `#include <Python-2.7/*>` 原样编译
`symtable.h` 自带 `mod_ty` 前向声明,绕开 `Python-ast.h``Delete`/`Module` 等单词宏与 `EterLib/Pool.h` 冲突)。
`extension/tests/port_python_launcher_test.cpp`ctest `port.python_launcher`)按 `UserInterface.cpp:241-434` 的顺序跑:
`Create()``__DEBUG__`/`__COMMAND_LINE__``TRUE/FALSE``RunMemoryTextFile`CRLF)→ `RunCompiledFile`.pyc
`RunFile("system.py")` 从真 pack 里取脚本,停在 `ImportError: No module named app`,即 2V0 的 C++ 模块。
`Py_NoSiteFlag` 等宿主标志目前在测试里设置,step 3 移进 platform 层。mingw-w64 可移植性门禁下没有 `mtpython`
`port_logic` 会自动排除 ScriptLib 和这些转发头(连带 `UserInterface/StdAfx.h``UserInterface/PythonPackModule.cpp`)。
3. `pack` 模块通过 `asset_io` 读取 40250 的 root/uiscript。标准库沿用已验证的 `python27.zip + sys.path`
- 桌面端把 zip 放在 CPython 可读的真实文件系统路径;
- Android/iOS 首次启动从应用资源复制到应用沙盒,校验提交在资源清单中的 sha256,再加入 `sys.path`
- `zipimport` 静态编进解释器。`system.py` 的导入钩子只负责 pack 内的简单 root 模块,不能用来加载标准库 package、
dotted import 或 `encodings`
**已完成(3a 标准库 + 3b pack 模块)**`tools/py_embed/make_stdlib_zip.py` 把 vendored `Lib/` 打成
`<build>/python27.zip`397 个模块 / 6.9MB,全部 STORED——没有 zlib 模块,zipimport 解不了 deflate
内容确定性写入,sha256 就是 3c 的校验值),CMake 目标 `mtpython_stdlib` 跟着 `Lib/*.py` 重建。
宿主引导收进 `extension/src/platform/ScriptLib/PythonHost.{h,cpp}``Configure()` 设四个标志和程序名,
`InstallStdLib()``Py_Initialize` 之后换掉 `sys.path` 并重新 `codecs.register(encodings.search_function)`
(2.7 的 codec 注册表只在启动时初始化一次)。`python.embed` 现在直接跑 zip。
`UserInterface/PythonPackModule.cpp` 已移植(`packExist`/`packGet`/`initpack`),经 `CEterPackManager`
读 pack,即 `asset_io``pack://` 用的同一套;`port.python_launcher` 验证 `pack.Get` 取到的
`system.py``CEterPackManager` 的字节数一致,并能读 `uiscript/selectcharacterwindow.py`
`locale/en/locale_game.txt`,非 `.py/.pyc/.txt` 和不存在的文件返回 `None`
`UserInterface/StdAfx.h` 只镜像了当前能编的部分(原文件 include 整个客户端和 DirectShow),
`PythonPackModule.cpp` 里属于 `CAccountConnector` 的登录密钥函数原样保留但编译时排除,随 2V1 回来。
**已完成(3c 落盘)**`make_stdlib_zip.py` 额外写出 `python27.zip.sha256`;构建把两个文件
`copy_if_different``project/`(目标 `mtpython_stdlib_project`,两文件 gitignore),
导出预设的 `include_filter` 列出它们,所以它们进 PCK——`--export-pack` 验证过两个条目都在。
`extension/src/python_stdlib.{h,cpp}`GDScript 侧 `Metin2Python.stdlib_path(force_stage)`):
桌面 `res://` 就是磁盘目录,`globalize_path` 得到真实路径直接用,不复制;导出后 `res://` 在 PCK 里,
于是读出字节 → 写 `user://python27.zip.part` → 按随包 sha256 校验**落盘后的文件**(不是内存里的 buffer,
磁盘满要在这里报,而不是三个 import 之后变成 zipimport 失败)→ rename 成 `user://python27.zip`
已经在沙盒里且 sha 相符就直接复用;被杀在半路的半成品或换了一版构建的旧 zip 都会重新落盘。
路径交给 `PythonHost::SetDefaultStdLibPath()``DefaultStdLibPath()` 从此答得出(`MT_PYTHON_STDLIB` 仍优先)。
测试 `project/python_stdlib_test.gd``godot --headless --path project --script python_stdlib_test.gd`)在
macOS 上用 `force_stage=true` 跑的就是移动端那条路径:落盘、sha 一致、字节与 `res://` 相同、`.part` 不残留、
`ZIPReader` 能打开且含 `encodings/__init__.py`,重复调用不重复复制,人为弄坏副本后重新落盘。
**未完成**:真机上跑(Android/iOS 的 `user://` 沙盒路径本身没验过),随 step 4 一起;
进程内的调用者见 step 4`Metin2PythonHost.start()` 空路径时就调 `Metin2Python.stdlib_path()`);
正式启动路径要到 2V0 才起解释器,现在只有 `MT_TEST_MODE=python` 和测试调。
`port_platform` 现在在没有 `mtpython` 时也会排除 `platform/ScriptLib/`(与 `port_logic` 同一条规则),
否则 Windows 门禁会编到 `PythonHost.cpp`
4. 两个目标平台分别验证:编译、链接无未定义符号、`Py_Initialize`、静态 C 模块逐个 `import`、在 app 进程里跑 `system.py``prototype.RunApp()`(对照结果:74 个模块中 66 个加载成功,引导期调用 33 个 C++ 函数)。
**进程内入口(2026-09-23**`extension/src/platform/ScriptLib/PythonBoot.{h,cpp}`
40250 `RunMainScript` 的移植(`audit/port-map/UserInterface/UserInterface.cpp.json`),
拆成 `Start` / `RunMainScript` / `RunLine` / `Stop`——40250 把 `CPythonLauncher` 放在 `Main()` 的栈上,
这里进程归 Godot,launcher 必须活过创建它的那次调用。37 个模块初始化函数里只调了已移植的 `initpack()`
GDScript 侧是 `Metin2PythonHost``extension/src/python_host_node.{h,cpp}`,唯一同时认识 godot-cpp 和
port 树的单元;`PythonBoot` 保持 godot-free)。脚本报错能传回 GDScript,是因为
`platform/EterBase/Debug.cpp` 实现了 `LogBox`/`LogBoxf``Traceback()``PyErr_Fetch` 之后只剩这一条路
`platform/EterBase/LogBox.h``LastLogBoxMessage()`)。
检查脚本 `project/python_host_check.gd` 分两段:解释器 + 标准库那段不需要 40250 的 pack,哪个平台都能跑;
`system.py` 那段需要 pack,没有就跳过。桌面入口 `project/python_host_test.gd`
`godot --headless --path project --script python_host_test.gd`),真机入口是
`MT_TEST_MODE=python`(导出模板不接受 `--script`)。
| 平台 | 状态 |
| --- | --- |
| macOS arm64 | step 1、2、3 完成,step 4 完成:`mtpython` 静态库 + `python.embed`(39 个内建模块全部 import,标准库 codec/pickle 通过)+ `port.python_launcher``CPythonLauncher` 跑到 `system.py` 的第一个 import+ **app 进程内**`python_host_test.gd` / `MT_TEST_MODE=python`):标准库可 import、codec 注册表可用,`system.py` 停在和 ctest 完全相同的 `ImportError: No module named app``prototype.RunApp()` 要等 2V0 |
| Android arm64 | step 1 完成:NDK API 24 交叉编译出 `libmtpython.a`;独立可执行文件在真机跑通(b02c49bb);APK 进程内未做。step 2 的 ScriptLib 只在 macOS 编译过 |
| iOS arm64 | step 1 完成:iphoneos arm64 编出 133 个目标文件,无 `_system`/`_fork`/`_popen` 未定义符号;真机未跑。step 2 的 ScriptLib 只在 macOS 编译过 |
| ~~Linux x86_64~~ | 不做(2026-09-23 定,见第 1 节) |
| ~~Windows x64~~ | 不做(2026-09-23 定,见第 1 节)。记录原因备查:上游 2.7 只支持 MSVC + `PC/pyconfig.h`,而本仓库的 Windows 门禁是 mingw-w64 交叉编译,`PC/pyconfig.h` 的 gnu-win32 分支不设 `HAVE_UNISTD_H``posixmodule.c`/`dynload_win.c` 与 MinGW 头文件冲突。因此 mingw 门禁下不建 `mtpython``port_logic`/`port_platform` 自动排除 ScriptLib |
### 已知问题:Android 的 GBK 编解码(挂起,2026-09-23
**优先级:macOS 先跑通,Android 后面再说**(用户 2026-09-23 定)。
`extension/src/net/text_codec.cpp:10``__has_include(<iconv.h>)` 探测 iconv。NDK 里这个头文件在
任何 API 等级都存在,但函数声明被 `__BIONIC_AVAILABILITY_GUARD(28)` 挡住,所以 `build-android.sh`
的默认 API 24`build-android.sh:14`)下编译报 `use of undeclared identifier 'iconv_open'`
**升到 API 28 不是解法**NDK `iconv.h` 原文写明 bionic 只支持
`utf8/ascii/usascii/utf16be/utf16le/utf32be/utf32le/wchart`**没有 GB2312/GBK**。升级后
`iconv_open("GB2312","UTF-8")` 运行期返回 `-1``encode_one()`/`decode_wire()` 对所有输入都
返回 false——连纯 ASCII 也被拒,比现在 `#else` 的 ASCII 回退分支(`text_codec.cpp:104-127`)更糟。
附带成本:APK `min_sdk` 必须跟着 ≥28,且 `config/android/pyconfig.h` 是 API 24 下 configure 出来的
`HAVE_LANGINFO_H``nl_langinfo` 需 API 26 被关掉),要重跑 `gen_pyconfig.sh android` 并重验内建模块表。
将来动手时分两步:
1. 探测条件从「头文件在不在」改成「这个平台的 iconv 能不能用」——`#if __has_include(<iconv.h>) && !defined(__ANDROID__)`
Android 一律走回退分支,先把交叉编译修绿。**不要**写成 `__ANDROID_API__ >= 28`
2. 补真正的转换:Android 需要自带 GBK↔Unicode 表(约 22k 码位)。40250 原客户端走 Windows
`MultiByteToWideChar(CP949/CP936)`,跨平台没有对应物;macOS 能用只是因为系统 libiconv 恰好带 GBK。
### 批次 2V02V3:纵向切片
2A 先根据真实 `#include` 和 Python import 生成每个切片的机器可读单元清单;清单提交到 `audit/`,进入切片前不得仍有
“后续批次才实现”的隐式依赖。未列出的 native 模块可以使用行为可观察、会记录调用的桩,但桩函数不能计为移植完成。
1. **2V0 — UI 壳**:真实 `ScriptLib/PythonLauncher.cpp``PythonPackModule.cpp`
`EterPythonLib/PythonWindow*.cpp``PythonWindowManager*.cpp``PythonGraphic*Module.cpp`
`PythonApplicationModule.cpp` 和所需 platform UI adapter;玩法模块先用桩。验收为 app 进程内运行 `system.py`,显示并可操作
Logo/Popup,输入、焦点、裁剪、文本和图片至少各有一个运行测试。
**入口状态(2026-09-23 实测)**`audit/slices/2V0.json` 的 212 个头里 164 个已在镜像中,缺 48 个;
10 个单元里 `ScriptLib/PythonLauncher.cpp``UserInterface/PythonPackModule.cpp` 已移植,缺 8 个。
(清单里的 `EterLib/Dimm.h` 是误报:那是 Windows SDK `<dimm.h>` 的 MIDL 产物,40250 把它放在 EterLib 下,
`port/common/shim/win32/Dimm.h` 已经顶掉了;镜像一份反而会遮住 shim,`IME.h` 就找不到 `rpc.h`。)
子步骤,按依赖从下往上:
| 子步骤 | 内容 | 状态 |
| --- | --- | --- |
| 2V0-a | `EterPythonLib/StdAfx.h` + 5 个窗口系统头进镜像,门禁绿 | 完成 |
| 2V0-b | `EterPythonLib/PythonWindow.cpp``PythonWindowManager.cpp`(窗口树逻辑) | 未做 |
| 2V0-c | `wndMgr` / `grp` / `grpImage` / `grpText` / `grpThing` 模块 | 未做 |
| 2V0-d | 42 个 `UserInterface` 头(都在 `PythonApplication.h` 后面)+ `PythonApplicationModule.cpp``app` | 未做 |
| 2V0-e | `dbg` 等桩模块补齐,`system.py` 跑到 `prototype.RunApp()` | 未做 |
| 2V0-f | Godot 渲染器上的 platform UI adapterLogo/Popup 可显示可操作 | 未做 |
2. **2V1 — 登录/选角**:加入 `PythonNetworkStream{,PhaseHandShake,PhaseLogin,PhaseSelect,PhaseLoading}.cpp`
`PythonNetworkStreamModule.cpp`、AccountConnector 及登录/选角脚本真实调用到的模块。验收为 FakeServer 离线完成 phase 顺序,
再以真服 smoke 证明登录、角色列表和选择进入 Loading;未验证的服务器分支记 `NEEDS_LIVE`
3. **2V2 — GamePhase/角色显示**:加入 GamePhase 的最小分派闭包、`PythonCharacterManager``InstanceBase``ActorInstance`
及角色模型/动画/资源 adapter。验收为进入游戏、创建主角并显示可辨认的静止角色;这个切片要求 2D 的 proto 和所需资源通过
strict gate。
4. **2V3 — 本地移动**:加入 `PythonPlayer``PythonPlayerInput*``PythonPlayerEventHandler`
`InstanceBaseMovement``ActorInstance{Motion,Event,Position,Rotation,CollisionDetection}` 的实际依赖闭包。验收为输入经过新路径
改变位置/朝向、发送与 40250 相同的移动封包,并在离线边界测试和真服 smoke 中通过。接通时删除对应 legacy 玩法逻辑。
每个切片都必须同时跑 legacy/port 双路回归;只有 2V3 稳定并把默认路由切到 `port` 后,才能删除总开关和剩余 legacy 宿主。
## 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`103 个包加 `root`、52,609 个路径,全部可以本地解码(2R) | 移植 `EterPack`/`EterPackManager` 接到 `asset_io`,请求改用 40250 路径(见 2R 第 6 条) |
切换步骤(批次 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`:该模式下 `MT_ASSETS` 未设置、来源清单缺失、sha256 不匹配或必需文件缺失都必须失败,不能跳过;
- 为 40250 的 root、locale、proto 和关键包生成来源清单(路径 + sha256),提交到 `audit/`,测试开始时核对。
4. 新增并在 CI/发布前强制运行 `script/run_40250_asset_gate.sh`;它设置/校验 `MT_ASSETS_STRICT=1`,运行 native proto/pack/formats
测试和 Godot root/locale/render 测试。脚本在没有 40250 资源时必须失败,不能把跳过报告成 PASS。
5. 资源根切换按 2R 第 6 条执行:移植 `EterPack``CLZObject`/TEA 已经在第 2 步)和 `EterPackManager`,接上
`asset_io` 的 pack 后端,读取方逐个改用 40250 路径,跑严格门禁,更新本节状态。
状态(2026-09-23):
| 步骤 | 状态 | 提交 |
| --- | --- | --- |
| 1. msm 路径 | 完成:`equip_model.gd` 先查 `root/<cls>_<m/w>.msm`m2dev 的 `root/msm/` 作后备;8 个种族都能从 `Eternexus/root` 找到 | 240d2827 |
| 2. proto | 完成:`proto.cpp` 用移植的 `CLZO`/TEA 读 MIPX/MMPT156 / 255 字节),m2dev 的 XChaCha20 / 236 / 335 读取已删除;名字按 `GetName`/`GetMonsterName``szLocaleName`cp1252)。`SMobTable` 暂时在 `proto.cpp` 里放一份 `#pragma pack(1)` 镜像(`static_assert == 255`),2P 移植 `GameLib/PythonNonPlayer` 后改用正式镜像 | 232d0461 |
| 3. 严格模式 | 完成(见下) | 本批最后一个提交 |
| 4. 门禁脚本 | 完成:`script/run_40250_asset_gate.sh` 在 macOS 上 PASS | 同上 |
| 5. 资源根 | 部分完成:pack 后端(`PackInitialize` + `asset_io``pack://` + `Metin2Pack` 节点)已接入,proto 经 `pack://locale/<lang>/` 读取(5a 1f1004da、5b 5a26e93f)。模型、贴图、地图和 root/locale 文本仍读 m2dev `assets/`,由 2V 切片逐个切换 | |
环境变量(取代步骤 3 里“两者都透传”的过渡方案):
- `MT_40250_CLIENT`40250 的 `Client` 目录(`pack/``Eternexus/`)。native 测试(CMake 缓存变量默认读同名环境变量,
再退回 `${MT_40250_SOURCE}/../../Client`)、`AssetRoot.client_path()` 和门禁脚本都用它;
- `MT_ASSETS`:只表示 m2dev 散文件目录(开发期覆盖);
- `M2_ASSETS`:测试不再使用;
- `MT_ASSETS_STRICT=1`:native 测试在资源缺失时返回 1(不是 ctest 的跳过码 77),门禁脚本把 Godot 测试输出里的
`(skip` 也算失败。
来源清单:`tools/asset_manifest.py generate|verify <Client>` 记录 `pack/Index`、全部 `.eix/.epk`
`Eternexus/root/*.msm` 的路径、大小、sha256,结果在 `audit/assets/40250-client.json`223 个文件,1,407,474,045
字节)。文件缺失、内容变化或清单外多出的文件都会让 verify 失败。
已知问题(不是本批引入的):完整 Android 链接在 API 24 上失败,原因是 `extension/src/net/text_codec.cpp` 用了 iconv
本批的 mtproto、`pack40250_node``asset_io``proto_node` 都能在 Android 上编译。
## 6. 进度与 port-map 基线
实时数字用 `port_map.py status` 查看。
本次方案修订重置了旧的 6 个完成状态;2A 完成工具和基础目标后,2026-09-22 重新确认了基线:
- 之前"完成"的 6 个函数(`PythonPlayerEventHandler.cpp`)已作废并回退为 `TODO`OnMove/OnMoving/OnStop 的实现在
`net_play.gd`,属于迁移来源;3 个 `N_A`(单例、构造、析构)与"单例归扩展所有"冲突。
- 原样运行的 Python 脚本函数**不标 `N_A`**`N_A` 只用于没有玩法语义的平台胶水)。状态 `RUN_AS_IS`
`impl` 指向随包运行的原脚本,`evidence` 要求目标平台的导入/运行证据;单元的 reference hash 与资源来源清单共同证明
运行的是完全相同的字节。`port_map.py`、SKILL 和 schema 已同步支持并把它计入 done。
基线(2026-09-22`port_map.py status`):
| 层 | 函数 | 完成 | 备注 |
| --- | --- | --- | --- |
| logic | 3343 | 0 | 1 个 `DIVERGENT``CPythonPlayerEventHandler::OnWaiting` |
| python | 5175 | 0 | 其中 `root/*.py` 3350 |
| platform | 2091 | 0 | |
| 合计 | 10609 | 0 | |
2D 之后(2026-09-23):logic 46 个 `PORTED`EterPack、EterPackManager、lzo、tea、Stl),platform 21 个 `ADAPTED`
PackInitialize、FileLoader、MappedFile、FileBase、CRC32)。只标了 proto_test + port_eterpack_test 覆盖率运行实际执行到
的函数。
与基线前的数字(logic 3285、python 4890、platform 2229,共 10404)相比的变化:
- 函数清单(`port_map.cpp_functions`)用 platform 骨架的 clang 声明逐个对照后修正:
- 新增 namespace 块内缩进的定义(`EterPythonLib``UI::CWindow` 等窗口类、`CWindowManager``NEffectUpdateDecorator`),
这是 python 层 +285 的主要来源;
- 新增初始化列表或参数表超过 8 行的定义、类名全大写的构造/析构(`CLZO::CLZO``CIME::CIME`);
- 去掉 `/* */` 里注释掉的旧代码(如 `PythonNetworkDatagram.cpp` 整个文件、`GameUtil.cpp` 的碰撞函数)和被误当成函数的
构造函数初始化项(`m_hFM(NULL),`)。
- 核对结果:platform 骨架里的每个函数桩都在清单里,只有 22 个 40250 未定义的声明,以及 namespace/typedef 写法不同的
同一函数(`NEffectUpdateDecorator::CAirResistanceDecorator::__Excute``CSkyObject::TSkyObjectFace``CSkyBox`)除外。
- EterBase 按单元分层(见第 3 节),不再整个库算 platform。`audit/slices/*.json` 已重新生成(`EterLib/Dimm.h` 同时改为
platform)。
## 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 节)更新状态并提交。