# 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`、`EterBase` 的纯逻辑单元(`tea`、`lzo`、`cipher`、`Random`、`Stl`、`Timer`、`Poly/`) | 逐文件照抄 C++ | `extension/src/port//.{h,cpp}` | | `python` | `root/*.py`、`uiscript/`、`UserInterface/*Module.cpp`、`EterPythonLib/`、`ScriptLib/` | 脚本原样运行;`*Module.cpp`、窗口系统、`CPythonLauncher` 照抄 | `extension/src/port//` + `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 `: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 重新基线 | **进行中**(剩 Python 库闭包头,等 2P) | | 2R | 资源包能力盘点:EPK 类型/密钥/覆盖顺序/路径大小写/移动端交付 | 完成:只有 NONE/COMPRESS/SECURITY,不需要服务器密钥;设备上直接读 EPK | | **2P** | CPython 2.7.18 编进 libmtgodot,五个平台分别配置和验证 | **进行中**:step 1(静态库 + macOS/Android/iOS 配置)、step 2(ScriptLib 进 `port_logic`)、step 3a/3b(python27.zip + `PythonHost` + `pack` 模块)完成 | | 2D | 数据源切到 40250(msm 路径、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、Linux、Windows 五个平台分别编译。 某个平台的工具链暂不可用时,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/2V0–2V3.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`;补 ``(MSVC 头文件间接带入 `std::find`); - `Stl.h`:`` 改为小写; - `Utils.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//.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.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:资源包能力盘点(完成,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`(每个包再注册 `_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 生成后提交,或 Windows 用 `PC/pyconfig.h`),因为它是对目标平台类型大小和系统 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`。 Linux/Windows 见下表。 2. 静态链接进 `libmtgodot`,照抄 `ScriptLib/PythonLauncher.cpp`。 **已完成**:`ScriptLib/{PythonLauncher,PythonUtils,PythonMarshal}.{h,cpp}` 进入 `port_logic`(`mt3p::python`), `common/shim/sdk/Python-2.7/` 12 个转发头让 40250 的 `#include ` 原样编译 (`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 层。Windows 没有 `mtpython`, `port_logic` 会自动排除 ScriptLib 和这些转发头。 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/` 打成 `/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 一起; app 的启动流程还没有调用它——解释器要到 2V0 才在进程里起来,现在只有测试调。 `port_platform` 现在在没有 `mtpython` 时也会排除 `platform/ScriptLib/`(与 `port_logic` 同一条规则), 否则 Windows 门禁会编到 `PythonHost.cpp`。 4. 五个平台分别验证:编译、链接无未定义符号、`Py_Initialize`、静态 C 模块逐个 `import`、在 app 进程里跑 `system.py` → `prototype.RunApp()`(对照结果:74 个模块中 66 个加载成功,引导期调用 33 个 C++ 函数)。 | 平台 | 状态 | | --- | --- | | macOS arm64 | step 1、2 完成:`mtpython` 静态库 + `python.embed`(39 个内建模块全部 import,标准库 codec/pickle 通过)+ `port.python_launcher`(`CPythonLauncher` 跑到 `system.py` 的第一个 import);`system.py` 在 app 进程内未做 | | 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 | 未做(`gen_pyconfig.sh linux` 需要 Linux 主机,当前无) | | Windows x64 | 未做:上游 2.7 只支持 MSVC + `PC/pyconfig.h`,而本项目 Windows 门禁是 MinGW 交叉编译,`PC/pyconfig.h` 的 gnu-win32 分支不设 `HAVE_UNISTD_H`,`posixmodule.c`/`dynload_win.c` 与 MinGW 头文件冲突,需要一套补丁——在本步骤(step 4)决定走 MSVC 还是打补丁 | ### 批次 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/_.msm`,40250 在 `root/_.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`,103 个包加 `root`、52,609 个路径,全部可以本地解码(2R) | 移植 `EterPack`/`EterPackManager` 接到 `asset_io`,请求改用 40250 路径(见 2R 第 6 条) | 切换步骤(批次 2D): 1. msm 查找路径加上 `root/_.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/_.msm`,m2dev 的 `root/msm/` 作后备;8 个种族都能从 `Eternexus/root` 找到 | 240d2827 | | 2. proto | 完成:`proto.cpp` 用移植的 `CLZO`/TEA 读 MIPX/MMPT(156 / 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//` 读取(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 ` 记录 `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` 和其他审计脚本。 - 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 节)更新状态并提交。