- 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>
18 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 | 迁移期新旧路径共存,新路径接通运行后才在同一提交删除旧逻辑;先做最小纵向切片 2V | 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 是 Win32(ILP32/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里记下。
- 凡是参与序列化的结构(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 重新基线 |
下一步 |
| 2R | 资源包能力盘点:EPK 类型/密钥/覆盖顺序/路径大小写/移动端交付 | 下一步,可与 2A 并行(只读分析 + 独立工具) |
| 2P | CPython 2.7.18 编进 libmtgodot,五个平台分别配置和验证 | 2A 之后 |
| 2D | 数据源切到 40250(msm 路径、proto、资源根、严格资源测试) | proto 部分在 2A 之后;资源根在 2R 之后 |
| 2V | 最小纵向切片:CPythonLauncher → system.py → wndMgr → 登录/选角 → GamePhase → 本地角色移动 |
需要 2A、2P、2D;这是第一个可运行的新架构里程碑 |
| 2 | 其余 P0 角色/移动单元,按依赖拓扑移植 | 2V 之后 |
| 3 | P1 战斗/技能 → P2 游戏阶段封包 → P3 物品;Python 层(窗口系统 → *Module.cpp) |
未开始 |
| 4 | NEEDS_LIVE 真服验证 |
未开始 |
迁移方式:始终保持可玩
旧 GDScript 路径和新的 ported 路径在切片接通前共存,由构建/运行开关选择(例如 MT_PORT_PATH=legacy|port,默认
legacy)。规则:
- 新单元先编译进
port_logic,但只有被新路径的运行时调用到,才算实现("运行时不可达不算实现"同样适用于新代码); 没有接通前,port-map 状态保持TODO,note写"已照抄,未接通"。 - 每个接通步骤的提交同时:切换调用方到新路径、删除被取代的旧逻辑、跑一遍能覆盖该路径的运行测试(离线 FakeClient 或真服 e2e)。
main在任意提交上都能进游戏走动;legacy路径在 2V 完成、默认值切到port并稳定后整体删除。
批次 2A:基础
extension/src/port/common/:Win32Types.h(BYTE/WORD/DWORD/LONG/BOOL/UINT/HANDLE等的定宽映射)、 40250 用到的 Win32/CRT 宏和函数(ZeroMemory、_snprintf、stricmp、timeGetTime等)的最小实现、StdAfx.h等价物。- 参考公共头的最小闭包:从第一批要移植的单元出发(
PythonPlayerEventHandler.h依赖ActorInstance.h、FlyHandler.h、PythonNetworkStream.h、InstanceBase.h),用脚本列出#include闭包,把闭包里的头文件先照抄为可编译的声明。 port_logic静态库 CMake 目标,链接进libmtgodot;在 macOS 和 Android 两个工具链上编译。extension/src/platform/接口骨架:闭包里出现的平台类(CGraphicThingInstance、CSoundManager等)只声明 40250 调用方用到的方法,先给空实现。- 门禁:
port/**下每个头文件单独编译通过(header self-containment),序列化结构的static_assert全部通过。 - port-map 重新基线(见第 6 节)。
- 用
#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。
- 写一个只读的扫描工具(Python,放在
tools/),解析所有.eix,统计每个包、每种compressed_type的文件数和字节数。 - 按统计结果决定:
- 只有
NONE/COMPRESS/SECURITY的包可以离线解出; PANAMA/HYBRIDCRYPT*如存在,确认密钥来源:本地的Index/配置,还是服务器GC_HYBRIDCRYPT_KEYS/SDB下发。需要服务器密钥的包,要么移植密钥链,要么在开发期用一次真服登录抓取密钥后离线解包(密钥不进仓库)。
- 只有
- 验证
Index文件列出的包顺序和同名文件的覆盖优先级,与CEterPackManager一致。 - 路径归一化:40250 在 Windows 上大小写不敏感,并把
d:/ymir work/等前缀映射到包内路径。确定统一的小写化规则,扫描大小写冲突。 - 移动端交付:决定最终形式(例如解包后重新打成我们自己的
mtpack,或直接在设备上读 EPK),给出 Android/iOS 的包体积、首包与按需下载的划分,以及更新方式。MT_ASSETS只是开发期覆盖。 - 输出写进本文第 5 节,再决定 2D 的资源根切换方式。
批次 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读取,标准库的纯 Python 文件放进资源包,由system.py自带的导入钩子加载。- 五个平台分别验证:编译、链接无未定义符号、
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)
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):
- 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:指定了 40250 资源时,缺文件必须失败,不能跳过; - 为 40250 的 root、locale、proto 和关键包生成来源清单(路径 + sha256),提交到
audit/,测试开始时核对。
- 统一资源环境变量:C++ 测试(
- 资源根切换方式按 2R 的结论执行,跑渲染和解析测试,更新本节状态。
6. 进度与 port-map 基线
实时数字用 port_map.py status 查看。
2A 时做一次重新基线,之后的数字才代表新架构的进度:
- 之前"完成"的 6 个函数(
PythonPlayerEventHandler.cpp)作废:OnMove/OnMoving/OnStop 的实现在net_play.gd,属于迁移来源; 3 个N_A(单例、构造、析构,理由是"由 NetPlay 场景节点持有")与"单例归扩展所有"冲突。全部回退为TODO。 - 原样运行的 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. 换机器后的环境准备
目录布局(路径都相对 ~/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 节)更新状态并提交。