76 closure headers (EterBase, EterLocale, EterPack, EterLib, GameLib, MilesLib, EffectLib, SphereLib, EterImageLib) copied by the new port_copy.py (CP949->UTF-8, LF, include-path case only); platform-class headers go to the mirror path too, as the interface platform/ implements. Six manual `// PORT:` edits (Random, Singleton, Pool, Stl, Utils, FlyTarget). - common/D3D8Types.h: D3D8 value types with SDK values/layout, opaque COM interfaces - common/shim/sdk (d3d8/d3dx8/mss) on every platform, common/shim/win32 off Windows - Win32Types/Win32Crt: HRESULT, GUID, LARGE_INTEGER, LOGFONT/TEXTMETRIC, _atoi64, CRITICAL_SECTION on std::recursive_mutex - header gate prepends each library's StdAfx.h (40250's precompiled header) port_gate: macOS, Android, iOS, Windows (mingw) PASS; Linux BLOCKED (no toolchain). ScriptLib/EterPythonLib/UserInterface wait for 2P (their StdAfx includes Python). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
310 lines
27 KiB
Markdown
310 lines
27 KiB
Markdown
# 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 | 迁移期新旧路径共存,新路径接通运行后才在同一提交删除旧逻辑;纵向切片拆成 2V0–2V3 | `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/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 位 Win32(ILP32):
|
||
`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 重新基线 | **下一步** |
|
||
| **2R** | 资源包能力盘点:EPK 类型/密钥/覆盖顺序/路径大小写/移动端交付 | **下一步**,可与 2A 并行(只读分析 + 独立工具) |
|
||
| 2P | CPython 2.7.18 编进 libmtgodot,五个平台分别配置和验证 | 2A 之后 |
|
||
| 2D | 数据源切到 40250(msm 路径、proto、资源根、严格资源测试) | proto 部分在 2A 之后;资源根在 2R 之后 |
|
||
| **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、Linux、Windows 五个平台分别编译。
|
||
某个平台的工具链暂不可用时,2A 对该平台保持 `BLOCKED`,不能用其他平台的通过结果替代。
|
||
4. `extension/src/platform/` 接口骨架:闭包里出现的平台类(`CGraphicThingInstance`、`CSoundManager` 等)只声明
|
||
40250 调用方用到的方法,先给空实现。
|
||
5. 门禁:`port/**` 下每个头文件单独编译通过(header self-containment),序列化结构的 `static_assert` 全部通过。
|
||
6. port-map 重新基线(见第 6 节)。
|
||
7. 用 `#include` 依赖图生成批次 2 的移植顺序(拓扑序),替换原来"互不共享实现文件即可并行"的假设:共享头文件的单元,
|
||
头文件由先做的那个单元负责,后面的单元只能在它合入后开始。
|
||
**已完成**:`port_deps.py`(显式 `#include` + 按符号解析的 StdAfx 隐式依赖)生成 `audit/slices/2V0–2V3.json`
|
||
和 `audit/slices/batch2-order.json`。结论:每个切片的头文件闭包约 210 个(其中平台层约 110 个,来自
|
||
`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 的库):闭包头文件照抄,76 个,覆盖 EterBase、EterLocale、EterPack、EterLib、
|
||
GameLib、MilesLib、EffectLib、SphereLib、EterImageLib。
|
||
- 门禁中每个头文件前先 include 本库的 `StdAfx.h`(40250 把它作为预编译头)。
|
||
- D3D8 的值类型(`D3DFORMAT`、`D3DCAPS8`、`D3DLIGHT8`、`D3DMATERIAL8` 等,按 SDK 取值和布局并加 `static_assert`)
|
||
放在 `port/common/D3D8Types.h`;COM 接口(`IDirect3DDevice8` 等)只做不透明声明,由 platform 持有。
|
||
- 手工 `// PORT:` 修改共 6 个文件:
|
||
- `Random.h`:`random`/`srandom` 与 POSIX 重名,改名;
|
||
- `Singleton.h`:`(int)` 指针运算改为 `intptr_t`;
|
||
- `Pool.h`:`operator new(unsigned int)` 改为 `size_t`;
|
||
- `Stl.h`:`<SSTREAM>` 改为小写;
|
||
- `Utils.h`:补 `<math.h>`;
|
||
- `FlyTarget.h`:补 `CFlyTarget` 前置声明(MSVC 会让 friend 声明可见)。
|
||
- 40250 StdAfx 里有影响语义的宏,移植逻辑时要注意:
|
||
- EterBase 的 `#define atoi _atoi64`;
|
||
- UserInterface 的 `_USE_32BIT_TIME_T`;
|
||
- `EterBase/ServiceDefs.h` 的 `_IMPROVED_PACKET_ENCRYPTION_`;
|
||
- 工程字符集是 MultiByte,所以 `TCHAR` 就是 `char`。
|
||
- ScriptLib、EterPythonLib、UserInterface 的 StdAfx 会 include Python 头,要等 2P 把 CPython 加进来之后再照抄。
|
||
- 未完成:platform 接口骨架(步骤 4);依赖 Python 的三个库的闭包头(等 2P)。
|
||
|
||
| 平台 | port_gate |
|
||
| --- | --- |
|
||
| macOS arm64 | PASS(含 `port.common` 测试) |
|
||
| Android arm64(脚本取已装的最新 NDK,本机 28.2;API 24) | PASS |
|
||
| iOS arm64 | PASS(Xcode,未签名) |
|
||
| Windows x64 | mingw-w64 PASS;MSVC 需在 Windows 主机上跑,**BLOCKED** |
|
||
| Linux x86_64 | **BLOCKED**(本机无 Linux 工具链;Linux 主机上直接运行脚本) |
|
||
|
||
已知的无关问题:完整 Android 扩展构建在 `extension/src/net/text_codec.cpp` 失败(bionic 从 API 28 才声明 `iconv`,
|
||
而构建用 API 24;c9389431 引入);仓库里的 `build-android/` 缓存还指向旧路径 `mtgodot-poc`。
|
||
|
||
### 批次 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` 读取 40250 的 root/uiscript。标准库沿用已验证的 `python27.zip + sys.path`:
|
||
- 桌面端把 zip 放在 CPython 可读的真实文件系统路径;
|
||
- Android/iOS 首次启动从应用资源复制到应用沙盒,校验提交在资源清单中的 sha256,再加入 `sys.path`;
|
||
- `zipimport` 静态编进解释器。`system.py` 的导入钩子只负责 pack 内的简单 root 模块,不能用来加载标准库 package、
|
||
dotted import 或 `encodings`。
|
||
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) |
|
||
|
||
### 批次 2V0–2V3:纵向切片
|
||
|
||
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,输入、焦点、裁剪、文本和图片至少各有一个运行测试。
|
||
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 用经典 TEA(32 轮,`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`:该模式下 `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. 进度与 port-map 基线
|
||
|
||
实时数字用 `port_map.py status` 查看。
|
||
|
||
本次方案修订已经重置旧的 6 个完成状态;2A 完成工具和基础目标后再次确认基线:
|
||
|
||
- 之前"完成"的 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 基线前的数字(仅供参考):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` 和其他审计脚本。
|
||
- Android:NDK 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 节)更新状态并提交。
|