tools/asset_manifest.py records path/size/sha256 of pack/Index, every .eix/.epk and Eternexus/root/*.msm (audit/assets/40250-client.json). The gate script sets MT_ASSETS_STRICT=1, verifies the manifest, runs the native pack/proto tests and four Godot tests, and fails on a missing client, a mismatch or a skip. MT_40250_CLIENT now comes from the environment for the native tests too. remote_player_test accepts both weighted WAIT variants (wait/wait_1). PORT-PLAN §5 records the 2D status. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
39 KiB
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/<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 位 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里记录适配不变式。
- 凡是参与序列化的结构(proto 记录、封包、EPK 索引、msa/msm 二进制)一律用定宽类型,
- 单位保持 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,五个平台分别配置和验证 | 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:基础
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 语义实现。- 参考公共头的最小闭包:从第一批要移植的单元出发(
PythonPlayerEventHandler.h依赖ActorInstance.h、FlyHandler.h、PythonNetworkStream.h、InstanceBase.h),用脚本列出#include闭包,把闭包里的头文件先照抄为可编译的声明。 port_logic静态库 CMake 目标,链接进libmtgodot;macOS、Android、iOS、Linux、Windows 五个平台分别编译。 某个平台的工具链暂不可用时,2A 对该平台保持BLOCKED,不能用其他平台的通过结果替代。extension/src/platform/接口骨架:闭包里出现的平台类(CGraphicThingInstance、CSoundManager等)只声明 40250 调用方用到的方法,先给空实现。- 门禁:
port/**下每个头文件单独编译通过(header self-containment),序列化结构的static_assert全部通过。 - port-map 重新基线(见第 6 节)。已完成(2026-09-22)。
- 用
#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截断语义、timeGetTime32 位回绕)、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是 SDKdimm.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 处理。
- EterBase 的
- ScriptLib、EterPythonLib、UserInterface 的 StdAfx 会 include Python 头,要等 2P 把 CPython 加进来之后再照抄。
- 门禁中每个头文件前先 include 本库的
-
已完成(步骤 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>.cpp95 个,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的方法等)。这些在对应逻辑单元移植时手工补桩。
- 平台层头文件(同名 40250
-
已完成(步骤 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(每个包再注册 <name>_texcache,最后 RegisterRootPack("pack/root"))
→ CEterPack::__BuildIndex(.eix 全部是 MCOZ:TEA + LZO 包着 EPKD v2,每条 192 字节)→ Get2。
-
类型统计(Index 注册到的 103 个包加
root,54,891 条,52,609 个不同路径):类型 文件数 存储 解码后 NONE20,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。 -
密钥来源:这个客户端只用编译进客户端的两组 TEA 密钥(
s_adwEterPackKey解索引,s_adwEterPackSecurityKey解 SECURITY 数据)。没有 PANAMA 和 HybridCrypt 条目,所以不需要服务器下发 IV/密钥,也不需要真服抓包。CAccountConnector的RegisterPack(..., abIV)和GC_HYBRIDCRYPT_KEYS/SDB照抄移植即可,但这批资源用不到。 移植后的EterPack.cpp需要这两组常量;按"密钥不进仓库",由构建时从参考树生成一个 gitignore 的头文件,不直接写进源码。 -
注册顺序与覆盖:
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按解码后的内容算。
- Ymir 格式:
- 在第二种格式下,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 拒绝。
-
路径归一化:
ConvertFileName只做两件事:ASCIItolower,以及\转/。CP949 字节原样保留。- 包内名字全部已经是小写、只用
/;52,609 个路径没有大小写冲突,filename_crc与crc32(name)全部一致。 d:/ymir work/...是包内的真实路径,一共 26,955 个。40250 的相对路径映射已经注释掉,所以调用方必须使用 40250 原样的 路径名,不能再用 m2devassets/的相对路径。- 包文件名与
Index大小写不一致,例如Etc对应磁盘上的ETC.eix。NTFS 下能打开;在移动端和 Linux 上,按包名查找 磁盘文件时要做大小写不敏感匹配。
-
移动端交付(已注册包的存储大小共 1337 MiB):
分组 大小 metin2_patch_*/patch1/patch2/season3_eu692.7 MiB 核心 d:/ymir work(pc/monster/zone/effect/npc/item…)+ icon/property/textureset405.7 MiB sound/bgm 87.6 MiB season1/2 71.5 MiB 其他 14 种语言的 locale 49.9 MiB 地图包 25.9 MiB locale_en3.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。
- 设备上直接读 EPK,用移植的
-
2D 的资源根切换:
- 新增 pack 后端:
asset_io在MT_ASSETS之外,接上移植的CEterPackManager,按pack/Index注册。 - 渲染和 GDScript 的资源请求改用 40250 路径,经过
ConvertFileName查找。 - m2dev
assets/的散文件保留作开发期覆盖,直到对应读取方切到新路径。 - 严格门禁(第 5 节步骤 3)的来源清单改为记录各个
.eix/.epk的 sha256。
- 新增 pack 后端:
批次 2P:内嵌 Python 集成
- 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。 - 静态链接进
libmtgodot,照抄ScriptLib/PythonLauncher.cpp。 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。
- 五个平台分别验证:编译、链接无未定义符号、
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 模块可以使用行为可观察、会记录调用的桩,但桩函数不能计为移植完成。
- 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,输入、焦点、裁剪、文本和图片至少各有一个运行测试。 - 2V1 — 登录/选角:加入
PythonNetworkStream{,PhaseHandShake,PhaseLogin,PhaseSelect,PhaseLoading}.cpp、PythonNetworkStreamModule.cpp、AccountConnector 及登录/选角脚本真实调用到的模块。验收为 FakeServer 离线完成 phase 顺序, 再以真服 smoke 证明登录、角色列表和选择进入 Loading;未验证的服务器分支记NEEDS_LIVE。 - 2V2 — GamePhase/角色显示:加入 GamePhase 的最小分派闭包、
PythonCharacterManager、InstanceBase、ActorInstance及角色模型/动画/资源 adapter。验收为进入游戏、创建主角并显示可辨认的静止角色;这个切片要求 2D 的 proto 和所需资源通过 strict gate。 - 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,103 个包加 root、52,609 个路径,全部可以本地解码(2R) |
移植 EterPack/EterPackManager 接到 asset_io,请求改用 40250 路径(见 2R 第 6 条) |
切换步骤(批次 2D):
- msm 查找路径加上
root/<cls>_<m/w>.msm。 - proto 读取按 40250 移植:
EterBase/tea.cpp、GameLib/ItemData.h的TItemTable、CPythonNonPlayer的 mob 表,放进镜像文件(使用 2A 的定宽类型和static_assert),替换extension/src/proto/proto.cpp里的 m2dev 格式。 - 资源测试严格模式:
- 统一资源环境变量:C++ 测试(
extension/CMakeLists.txt目前只透传M2_ASSETS)和 GDScript(MT_ASSETS)统一读MT_ASSETS,过渡期两者都透传; - 新增
MT_ASSETS_STRICT=1:该模式下MT_ASSETS未设置、来源清单缺失、sha256 不匹配或必需文件缺失都必须失败,不能跳过; - 为 40250 的 root、locale、proto 和关键包生成来源清单(路径 + sha256),提交到
audit/,测试开始时核对。
- 统一资源环境变量:C++ 测试(
- 新增并在 CI/发布前强制运行
script/run_40250_asset_gate.sh;它设置/校验MT_ASSETS_STRICT=1,运行 native proto/pack/formats 测试和 Godot root/locale/render 测试。脚本在没有 40250 资源时必须失败,不能把跳过报告成 PASS。 - 资源根切换按 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/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/<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)除外。
- 新增 namespace 块内缩进的定义(
- 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 账本。
新会话恢复工作:
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 节)更新状态并提交。