Files
mtgodot-poc/docs/PORT-PLAN.md
T
shenleiandClaude Opus 5 0f2eee0b24 port/common: platform-independent D3DX8 math layer
D3DXVECTOR2/3/4, D3DXMATRIX, D3DXQUATERNION, D3DXPLANE, D3DXCOLOR with
d3dx8math.inl operator semantics, plus the D3DX* functions the 2V3 slice
logic uses. Covered by port.common; port_gate passes on macOS, Android,
iOS and Windows (mingw).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:02:15 +09:00

284 lines
24 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 | 迁移期新旧路径共存,新路径接通运行后才在同一提交删除旧逻辑;纵向切片拆成 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/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 可执行文件目标是 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 重新基线 | **下一步** |
| **2R** | 资源包能力盘点:EPK 类型/密钥/覆盖顺序/路径大小写/移动端交付 | **下一步**,可与 2A 并行(只读分析 + 独立工具) |
| 2P | CPython 2.7.18 编进 libmtgodot,五个平台分别配置和验证 | 2A 之后 |
| 2D | 数据源切到 40250msm 路径、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/2V02V3.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 的乘法顺序;
后续单元用到新函数时再按需补。
这一层不包含 D3D8 渲染枚举和结构(`D3DLIGHT8``D3DRS_*` 等),它们归 platform 层。测试在 `port.common` 里。
- 未完成:闭包头文件照抄(步骤 2)、platform 接口骨架(步骤 4)。
| 平台 | port_gate |
| --- | --- |
| macOS arm64 | PASS(含 `port.common` 测试) |
| Android arm64NDK 27.2API 24 | PASS |
| iOS arm64 | PASSXcode,未签名) |
| Windows x64 | mingw-w64 PASSMSVC 需在 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 |
### 批次 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,输入、焦点、裁剪、文本和图片至少各有一个运行测试。
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` 包 | 由 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` 和其他审计脚本。
- 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 节)更新状态并提交。