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>
This commit is contained in:
shenlei
2026-09-22 18:38:04 +09:00
co-authored by Claude Opus 5
parent 67a62e3390
commit dcefc83211
3 changed files with 115 additions and 49 deletions
@@ -26,13 +26,14 @@ The 40250 source is the specification. Do not design behavior; transcribe it. Co
## Code layout: mirror 40250 ## Code layout: mirror 40250
The logic layer has the 40250 structure, not a new architecture. `port_map.py` assigns every unit The logic layer has the 40250 structure, not a new architecture. Route, batch order and
prerequisites (2A base, 2R pack inventory, 2V vertical slice) are in `docs/PORT-PLAN.md`. `port_map.py` assigns every unit
one of three layers: one of three layers:
| Layer | 40250 units | How to port | Where | | Layer | 40250 units | How to port | Where |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `logic` | `UserInterface/`, `GameLib/`, `EterLib` net/timer/text parsing, `EterPack`, `EterLocale` (everything not listed below) | Copy C++ -> C++. Same file name, class name, method names, member names and statement order; one reference file = one implementation file | `extension/src/port/<Lib>/<File>.{h,cpp}` | | `logic` | `UserInterface/`, `GameLib/`, `EterLib` net/timer/text parsing, `EterPack`, `EterLocale` (everything not listed below) | Copy C++ -> C++. Same file name, class name, method names, member names and statement order; one reference file = one implementation file | `extension/src/port/<Lib>/<File>.{h,cpp}` |
| `python` | `Client/Eternexus/root/*.py`, `uiscript/`, `UserInterface/*Module.cpp`, `EterPythonLib/`, `ScriptLib/` | Being evaluated as embedded CPython 2.7 running the scripts unchanged (`docs/PYTHON-EMBED-EVAL.md`). **Do not port or translate this layer until that evaluation is decided.** Reference is 40250 `Eternexus/root`, **not** `assets/root` | `extension/src/port/<Lib>/` (bindings), scripts from pack | | `python` | `Client/Eternexus/root/*.py`, `uiscript/`, `UserInterface/*Module.cpp`, `EterPythonLib/`, `ScriptLib/` | Decided: embedded CPython 2.7.18 runs the scripts unchanged (`docs/PYTHON-EMBED-EVAL.md`, batch 2P); port only the C++ side. Never translate the scripts. Script functions get `RUN_AS_IS` (after the 2A re-baseline), never `N_A`. Reference is 40250 `Eternexus/root`, **not** `assets/root` | `extension/src/port/<Lib>/` (bindings), scripts from pack |
| `platform` | Direct3D/`Grp*`, Granny (`EterGrnLib`), Miles, SpeedTree, `EffectLib`/terrain rendering, Win32 window/input/IME, threads, anti-cheat | Adapter behind the interface the 40250 caller uses; equivalence by observable output | `extension/src/platform/` + existing render code (`metin2_model`, `metin2_anim`, `gr2_bridge`, ...) | | `platform` | Direct3D/`Grp*`, Granny (`EterGrnLib`), Miles, SpeedTree, `EffectLib`/terrain rendering, Win32 window/input/IME, threads, anti-cheat | Adapter behind the interface the 40250 caller uses; equivalence by observable output | `extension/src/platform/` + existing render code (`metin2_model`, `metin2_anim`, `gr2_bridge`, ...) |
Rules for the `logic` layer: Rules for the `logic` layer:
@@ -43,11 +44,18 @@ Rules for the `logic` layer:
- 40250 singletons (`CPythonPlayer::Instance()`, `CPythonCharacterManager`, `CPythonNetworkStream`) - 40250 singletons (`CPythonPlayer::Instance()`, `CPythonCharacterManager`, `CPythonNetworkStream`)
stay singletons owned by the extension. GDScript does not hold gameplay state. stay singletons owned by the extension. GDScript does not hold gameplay state.
- GDScript (`net_play.gd`, `net_world.gd`, `game_scene.gd`, `player_controller.gd`) and - GDScript (`net_play.gd`, `net_world.gd`, `game_scene.gd`, `player_controller.gd`) and
`extension/src/net/entity_store.cpp` are migration sources. In the commit that ports a unit, delete `extension/src/net/entity_store.cpp` are migration sources. Old and new paths coexist behind a
the old logic it replaces there and leave only glue: node creation, forwarding Godot input to the switch until the new path is wired into the runtime; delete the old logic in the same commit that
ported input handlers, and reading ported state to place nodes. switches its callers to the ported code, leaving only glue (node creation, forwarding Godot input,
- Keep 40250 types and units (`TPixelPosition` in cm, `DWORD` ms from `ELTimer_GetMSec`, degrees). reading ported state to place nodes). A ported function nothing calls at runtime stays `TODO`.
- Keep 40250 **widths and overflow semantics**, not its C type names: 40250 is Win32, where `long` and
`unsigned long` are 32-bit. Use the fixed-width types from `port/common/Win32Types.h`; every
serialized struct (proto records, packets, EPK index, msa/msm) gets 40250's `#pragma pack` and a
`static_assert(sizeof(T) == N)`; pointers stored in `DWORD` become `uintptr_t` with a port-map note.
- Keep 40250 units (`TPixelPosition` in cm, `DWORD` ms from `ELTimer_GetMSec`, degrees).
Convert to Godot space only in the adapter. Convert to Godot space only in the adapter.
- Port in `#include`-dependency order: a shared header belongs to the first unit that needs it, and
units depending on it start only after it lands. Separate `.cpp` files do not make units independent.
- `port_map.py check` prints `LEGACY` for a `logic` function whose `impl` is outside its mirror file. - `port_map.py check` prints `LEGACY` for a `logic` function whose `impl` is outside its mirror file.
Mark each ported function with a one-line tag at its definition: Mark each ported function with a one-line tag at its definition:
@@ -67,7 +75,8 @@ optional, because the file and method name already map one to one.
constants and units, state writes and their order, timing/event source, data source constants and units, state writes and their order, timing/event source, data source
(proto/msa/msm/txt, never hardcoded), packets sent, cleanup. Then: (proto/msa/msm/txt, never hardcoded), packets sent, cleanup. Then:
- keep the 40250 version; record each old behavior that differed as a divergence found; - keep the 40250 version; record each old behavior that differed as a divergence found;
- delete the old logic it replaces and rewire callers to the mirror class; - when the ported path is wired into the runtime, delete the old logic it replaces and rewire
callers in the same commit (see `docs/PORT-PLAN.md`, "迁移方式");
- mark `N_A` only for pure platform plumbing (D3D state, Python binding glue, Win32), with a reason. - mark `N_A` only for pure platform plumbing (D3D state, Python binding glue, Win32), with a reason.
4. **Test what changed**: a focused test using the reference's own boundary values for each changed 4. **Test what changed**: a focused test using the reference's own boundary values for each changed
formula/branch (fails before, passes after). Do not write tests for unchanged or trivial code. formula/branch (fails before, passes after). Do not write tests for unchanged or trivial code.
+8 -5
View File
@@ -16,16 +16,19 @@ python3 .agents/skills/metin2-40250-parity-audit/scripts/port_map.py queue --lim
| --- | --- | --- | | --- | --- | --- |
| 0 | 工具:参考根解析、缺失文件报错、`port_map.py`、构建/测试入口 | 完成 2026-09-22 | | 0 | 工具:参考根解析、缺失文件报错、`port_map.py`、构建/测试入口 | 完成 2026-09-22 |
| 1 | 删除只被自身测试引用的 `project/*_system.gd` 与其 `test_*_parity.gd`,清除 manifest 中对应证据 | 完成 2026-09-22106+106 个文件) | | 1 | 删除只被自身测试引用的 `project/*_system.gd` 与其 `test_*_parity.gd`,清除 manifest 中对应证据 | 完成 2026-09-22106+106 个文件) |
| 2 | P0 角色/移动单元,移植到 `extension/src/port/<Lib>/<File>`(并行 worktree | 未开始 | | 2A | 基础:Win32 定宽类型层、参考公共头最小闭包、`port_logic` CMake、platform 接口骨架、头文件可编译门禁、port-map 重新基线 | 下一步 |
| 2P | CPython 2.7.18 静态编入 libmtgodotmacOS → Android APK → iOS),见 `docs/PYTHON-EMBED-EVAL.md` | 未开始(Android 独立程序已验证 b02c49bb | | 2R | 资源包盘点:EPK 压缩/加密类型、密钥来源、覆盖顺序、路径大小写、移动端交付 | 下一步(可与 2A 并行 |
| 2D | 数据源切到 40250msm 路径、protoTEA + `TItemTable`)、EPK 解包比对、切资源根,见 `docs/PORT-PLAN.md` 第 5 节 | 未开始 | | 2P | CPython 2.7.18 编进 libmtgodot,五个平台各自 `pyconfig.h` 并分别验证 | 2A 之后(Android 独立程序已验证 b02c49bb |
| 2D | 数据源切到 40250msm 路径、protoTEA + `TItemTable`)、严格资源测试、资源根 | proto 在 2A 之后,资源根在 2R 之后 |
| 2V | 最小纵向切片:PythonLauncher → system.py → wndMgr → 登录/选角 → GamePhase → 本地移动 | 需要 2A、2P、2D |
| 2 | 其余 P0 角色/移动单元,按 `#include` 依赖拓扑移植 | 2V 之后 |
| 3 | P1 战斗/技能 → P2 游戏阶段封包 → P3 物品;Python 层按 2P 的结论进行 | 未开始 | | 3 | P1 战斗/技能 → P2 游戏阶段封包 → P3 物品;Python 层按 2P 的结论进行 | 未开始 |
| 4 | `NEEDS_LIVE` 真服验证批 | 未开始 | | 4 | `NEEDS_LIVE` 真服验证批 | 未开始 |
## 批次 2 单元(互不共享实现文件的可并行 ## 批次 2 单元(顺序由 2A 的依赖图重新生成;下表仅为候选
实现位置一律是镜像文件 `extension/src/port/<Lib>/<File>.{h,cpp}`;下面列出的是需要删减旧逻辑的迁移来源。 实现位置一律是镜像文件 `extension/src/port/<Lib>/<File>.{h,cpp}`;下面列出的是需要删减旧逻辑的迁移来源。
第一个单元负责建立 `extension/src/port/``extension/src/platform/` 目录和 CMake 目标 目录、CMake 目标和公共头由 2A 建立
- [ ] `UserInterface/PythonPlayerEventHandler.cpp`OnMove/OnMoving/OnStop 已在 `net_play.gd` 对齐,需迁到镜像文件;OnWaiting DIVERGENT)— `net_play.gd` - [ ] `UserInterface/PythonPlayerEventHandler.cpp`OnMove/OnMoving/OnStop 已在 `net_play.gd` 对齐,需迁到镜像文件;OnWaiting DIVERGENT)— `net_play.gd`
- [ ] `UserInterface/InstanceBaseMovement.cpp` + `UserInterface/InstanceBase.cpp`(同一 owner,串行)— `player_controller.gd``entity_store.cpp` - [ ] `UserInterface/InstanceBaseMovement.cpp` + `UserInterface/InstanceBase.cpp`(同一 owner,串行)— `player_controller.gd``entity_store.cpp`
+91 -37
View File
@@ -26,6 +26,8 @@ metin2-client 是 40250 Windows 客户端的跨平台版本(macOS / Windows /
| 2026-09-22 | **UI 走内嵌 CPython 2.7.18**(方案 A),原样运行 40250 的 `root/*.py``uiscript/`,只移植 C++ 模块 | 脚本无需改动;macOS 和 Android 真机均已验证,见 `docs/PYTHON-EMBED-EVAL.md` | | 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 | 数据源统一改为 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 | 现有 `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. 三个层和代码位置 ## 3. 三个层和代码位置
@@ -49,8 +51,16 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次
- 40250 调用平台类的地方,调用 `extension/src/platform/` 下同名的适配接口,ported 代码里不直接写 Godot 调用。 - 40250 调用平台类的地方,调用 `extension/src/platform/` 下同名的适配接口,ported 代码里不直接写 Godot 调用。
- 40250 的单例(`CPythonPlayer``CPythonCharacterManager``CPythonNetworkStream`)归扩展所有,GDScript 不持有玩法状态。 - 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` 的毫秒,角度用度),只在适配层换算。 - 单位保持 40250 的(`TPixelPosition` 用 cm,时间是 `ELTimer_GetMSec` 的毫秒,角度用度),只在适配层换算。
- `net_play.gd``net_world.gd``game_scene.gd``player_controller.gd``entity_store.cpp` 是迁移来源:移植某个单元的那次提交,同时删掉它们里面被取代的逻辑,只留胶水(建节点、转发输入、读状态摆节点)。 - 迁移来源:`net_play.gd``net_world.gd``game_scene.gd``player_controller.gd``entity_store.cpp`。**旧逻辑只在新路径
已接通运行时之后才删除**(见第 4 节"迁移方式"),删除与接通在同一次提交完成。
- 迁移结束后 Godot 这边只剩:场景节点、渲染适配、一个把输入转发给 `CPythonWindowManager` 并每帧调用 Update/Render 的宿主 Control。 - 迁移结束后 Godot 这边只剩:场景节点、渲染适配、一个把输入转发给 `CPythonWindowManager` 并每帧调用 Update/Render 的宿主 Control。
## 4. 路线 ## 4. 路线
@@ -59,35 +69,72 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次
| --- | --- | --- | | --- | --- | --- |
| 0 | 工具:参考根解析、`port_map.py`、构建/测试入口 | 完成(55f733fd、5afd5a4f | | 0 | 工具:参考根解析、`port_map.py`、构建/测试入口 | 完成(55f733fd、5afd5a4f |
| 1 | 删除运行时不加载的 106 个 `*_system.gd` 及其测试 | 完成(a989b8f1 | | 1 | 删除运行时不加载的 106 个 `*_system.gd` 及其测试 | 完成(a989b8f1 |
| — | 定下目录结构,评估内嵌 Python | 完成(9d0e50de | | — | 定下目录结构,评估内嵌 PythonAndroid 独立程序验证 | 完成(9d0e50de、b02c49bb |
| — | 内嵌 Python Android 真机验证 | 完成(b02c49bb | | **2A** | 基础:Win32 类型层、参考公共头的最小闭包、`port_logic` CMake 目标、platform 接口骨架、头文件可编译门禁、port-map 重新基线 | **下一步** |
| 2 | P0 角色/移动单元照抄到镜像文件;第一个单元负责建立 `port/``platform/` 和 CMake 目标 | **下一步,未开始** | | **2R** | 资源包能力盘点:EPK 类型/密钥/覆盖顺序/路径大小写/移动端交付 | **下一步**,可与 2A 并行(只读分析 + 独立工具) |
| 2P | CPython 2.7.18 编进 libmtgodotmacOS → Android APK → iOS | **下一步,未开始**,可与 2 并行 | | 2P | CPython 2.7.18 编进 libmtgodot,五个平台分别配置和验证 | 2A 之后 |
| 2D | 数据源切到 40250第 5 节的 4 步) | **下一步,未开始**,可与 2、2P 并行;2P 跑 40250 脚本前必须完成 | | 2D | 数据源切到 40250msm 路径、proto、资源根、严格资源测试) | proto 部分在 2A 之后;资源根在 2R 之后 |
| 3 | P1 战斗/技能 → P2 游戏阶段封包 → P3 物品;Python 层(`PythonLauncher`窗口系统 → `*Module.cpp` | 未开始 | | **2V** | 最小纵向切片:`CPythonLauncher``system.py``wndMgr` → 登录/选角 → GamePhase → 本地角色移动 | 需要 2A、2P、2D;**这是第一个可运行的新架构里程碑** |
| 2 | 其余 P0 角色/移动单元,按依赖拓扑移植 | 2V 之后 |
| 3 | P1 战斗/技能 → P2 游戏阶段封包 → P3 物品;Python 层(窗口系统 → `*Module.cpp` | 未开始 |
| 4 | `NEEDS_LIVE` 真服验证 | 未开始 | | 4 | `NEEDS_LIVE` 真服验证 | 未开始 |
### 批次 2:P0 单元(每个单元一个 worktree,互不共享实现文件的可并行) ### 迁移方式:始终保持可玩
1. `UserInterface/PythonPlayerEventHandler.cpp`OnMove/OnMoving/OnStop 已在 `net_play.gd` 对齐,需迁到镜像文件;OnWaiting 标为 DIVERGENT 旧 GDScript 路径和新的 ported 路径在切片接通前共存,由构建/运行开关选择(例如 `MT_PORT_PATH=legacy|port`,默认
2. `UserInterface/InstanceBaseMovement.cpp` + `InstanceBase.cpp`(串行) `legacy`)。规则:
3. `UserInterface/PythonPlayerInput*.cpp`(排在第 2 项之后)
4. `GameLib/ActorInstanceMotion.cpp` + `ActorInstanceEvent.cpp`
5. `GameLib/ActorInstance.cpp` + `Position`/`Rotation`/`CollisionDetection`
6. `UserInterface/PythonCharacterManager.cpp`
7. `UserInterface/PythonPlayer.cpp`
每轮的步骤(详见 SKILL.md):`port_map.py init/show` → 通读参考文件 → 照抄到镜像文件,逐函数与旧实现对比、 - 新单元先编译进 `port_logic`,但**只有被新路径的运行时调用到**,才算实现("运行时不可达不算实现"同样适用于新代码);
记录差异 → 删旧逻辑、改调用方 → 只为改动的公式/分支写测试 → 更新 port-map 和 history → 一个单元一次提交 没有接通前,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 集成 ### 批次 2P:内嵌 Python 集成
1. CPython 2.7.18 源码放进 `extension/third_party/cpython-2.7.18/`,用我们自己的 CMake 编译。macOS/Android/iOS 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`
共用一份源码清单和 `pyconfig.h`Android 需关掉 `HAVE_LANGINFO_H`),C 模块清单见 `tools/py_embed_android/build-and-run.sh` 2. 静态链接进 `libmtgodot`,照抄 `ScriptLib/PythonLauncher.cpp`
2. 静态链接进 `libmtgodot`,照抄 `ScriptLib/PythonLauncher.cpp` 3. `pack` 模块通过 `asset_io` 读取,标准库的纯 Python 文件放进资源包,由 `system.py` 自带的导入钩子加载。
3. `pack` 模块通过 `asset_io` 读取,标准库的纯 Python 文件放进资源包,由 `system.py` 自带的导入钩子加载 4. 五个平台分别验证:编译、链接无未定义符号、`Py_Initialize`、静态 C 模块逐个 `import`、在 app 进程里跑 `system.py``prototype.RunApp()`(对照结果:74 个模块中 66 个加载成功,引导期调用 33 个 C++ 函数)。
4. 先用桩 C++ 模块在 APK 进程里跑通 `system.py``prototype.RunApp()`(对照结果:74 个模块中 66 个加载成功,引导期调用 33 个 C++ 函数)
5. 同样的流程在 iOS 上跑一遍 | 平台 | 状态 |
| --- | --- |
| 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 ## 5. 数据源切换:m2dev assets → 40250
@@ -101,28 +148,35 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次
| `playersettingmodule.py` 的特效注册 | 暂无 | 40250 多了 `EFFECT_LEVELUP_*_FOR_GERMANY``EFFECT_EMPIRE+1..3`,少了 `EFFECT_AGGREGATE_MONSTER` | 移植特效时按 40250 | | `playersettingmodule.py` 的特效注册 | 暂无 | 40250 多了 `EFFECT_LEVELUP_*_FOR_GERMANY``EFFECT_EMPIRE+1..3`,少了 `EFFECT_AGGREGATE_MONSTER` | 移植特效时按 40250 |
| `atlasinfo.txt``grpblk.txt` | — | 相同 | 无 | | `atlasinfo.txt``grpblk.txt` | — | 相同 | 无 |
| `uiscript/uiscript` | 现有 `project/ui/*.gd` | 40250 有 94 个,m2dev 有 80 个;共有的 71 个中,去掉空白差异后有 11 个不同(建角/选角/密码/信使/公会等) | 由 2P 直接运行 40250 的 uiscript,不单独处理 | | `uiscript/uiscript` | 现有 `project/ui/*.gd` | 40250 有 94 个,m2dev 有 80 个;共有的 71 个中,去掉空白差异后有 11 个不同(建角/选角/密码/信使/公会等) | 由 2P 直接运行 40250 的 uiscript,不单独处理 |
| `item_proto` | `proto.cpp` | **读不了**40250 每条 156 字节(`TItemTable`),m2dev 236 字节;40250 用经典 TEA32 轮,`0x9E3779B9`),m2dev 用 XChaCha20 | 按 40250 移植 | | `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 字节,加密同上 | 按 40250 移植 | | `mob_proto` | `proto.cpp` | **读不了**:每条 255 字节 vs 335 字节,加密同上 | 同上 |
| `item_list.txt` | `item_list.gd` | 177 行不同(m2dev 有 dummy 条目等);40250 放在 `locale/en/`m2dev 在 `locale/common/` | 按 40250 路径读取 | | `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 路径读取 | | `locale_interface.txt``itemdesc.txt``skilldesc.txt``skilltable.txt` | UI、技能表 | 20 行 / 2 行 / 2 行 / 位置不同 | 按 40250 路径读取 |
| 模型、贴图、地图(PC、Monster、Outdoor 等) | 渲染 | **未核对**40250 是经典 `.eix/.epk` 包 | 见第 3 步 | | 模型、贴图、地图(PC、Monster、Outdoor 等) | 渲染 | **未核对**40250 是经典 `.eix/.epk` 包 | 由 2R 决定 |
切换步骤(批次 2D): 切换步骤(批次 2D):
1. msm 查找路径加上 `root/<cls>_<m/w>.msm` 1. msm 查找路径加上 `root/<cls>_<m/w>.msm`
2. proto 读取按 40250 移植:`EterBase/tea.cpp``GameLib/ItemData.h``TItemTable``CPythonNonPlayer` 的 mob 表,放进镜像文件,替换 `extension/src/proto/proto.cpp` 里的 m2dev 格式。 2. proto 读取按 40250 移植:`EterBase/tea.cpp``GameLib/ItemData.h``TItemTable``CPythonNonPlayer` 的 mob 表,放进镜像文件(使用 2A 的定宽类型和 `static_assert`,替换 `extension/src/proto/proto.cpp` 里的 m2dev 格式。
3. 移植 40250 的 EPK 读取(`EterPack/EterPack.cpp`),解出 `Client/pack`,与 `assets/` 抽样比对模型/贴图/地图;有差异的以 40250 为准。 3. 资源测试严格模式:
4. 资源根切到 40250 数据(`MT_ASSETS` 指向解出的目录),跑渲染和解析测试,更新本节状态。 - 统一资源环境变量: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. 当前进度 ## 6. 进度与 port-map 基线
实时数字用 `port_map.py status` 查看。2026-09-22 的状态: 实时数字用 `port_map.py status` 查看。
| 层 | 函数数 | 已完成 | 2A 时做一次重新基线,之后的数字才代表新架构的进度:
| --- | --- | --- |
| logic | 3285 | 6 | - 之前"完成"的 6 个函数(`PythonPlayerEventHandler.cpp`)作废:OnMove/OnMoving/OnStop 的实现在 `net_play.gd`,属于迁移来源;
| python | 4890 | 0(其中 `Client/root` 的 3350 个在 2P 完成后记为 `N_A`,属于原样运行) | 3 个 `N_A`(单例、构造、析构,理由是"由 NetPlay 场景节点持有")与"单例归扩展所有"冲突。全部回退为 `TODO`
| platform | 2229 | 0 | - 原样运行的 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. 换机器后的环境准备 ## 7. 换机器后的环境准备
@@ -155,4 +209,4 @@ python2.7 .agents/skills/metin2-40250-parity-audit/scripts/py_embed_spike.py --o
tools/py_embed_android/build-and-run.sh # 可选:连接 Android 真机复核 tools/py_embed_android/build-and-run.sh # 可选:连接 Android 真机复核
``` ```
然后按第 4 节"下一步"继续,一个单元完成后在第 4、6 节更新状态并提交。 然后按第 4 节标为"下一步"的批次继续,每完成一步在第 4 节(及 2P 平台表、第 5更新状态并提交。