Godot-era batch records move verbatim to docs/archive/PORT-HISTORY.md. README and THIRD-PARTY no longer mention submodules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
18 KiB
40250 1:1 移植:路线与方案
本文是移植工作的总入口,换一台电脑继续开发时先读这份。更新日期:2026-09-29。
当前架构:原生 SDL3 + Vulkan 客户端(native_render/),macOS 上经 MoltenVK 走 Metal,Android 上是 SDLActivity
外壳加原生 libmain.so。40250 的 C++ 逻辑逐文件照抄在 extension/src/port/,平台类实现在 extension/src/platform/,
40250 的 root/*.py、uiscript/ 由内嵌 CPython 2.7.18 原样运行,资源直接读 40250 的 EPK。运行时不依赖 Godot;
Godot 时期(2026-09-22 至 09-27)的批次记录已移到 docs/archive/PORT-HISTORY.md。
- 方法细则:
.agents/skills/metin2-40250-parity-audit/SKILL.md(每轮的做法、状态定义、工具) - 批次队列:
audit/remediation-roadmap.md - 逐函数进度:
audit/port-map/**;每轮一行记录:audit/history.jsonl - 构建与启动:根目录
README.md、native_render/README.md;Android 真机测试:docs/ANDROID-TESTING.md - 渲染差距与门禁:
docs/PARITY-GAP.md;中文语言包:docs/ZH-LOCALE.md;移动端 UI:docs/MOBILE-UI-IMPLEMENTATION.md - Python UI 评估:
docs/PYTHON-EMBED-EVAL.md - 旧方案
docs/CLIENT-REFACTOR-40250-PLAN.md("形式上不照搬类文件")已被本文取代
1. 目标与原则
metin2-client 是 40250 Windows 客户端的跨平台版本。首要目标是 Android arm64 原生客户端(2026-09-28 定),
macOS arm64 是开发和验收平台;iOS arm64 保持交叉编译干净,之后再做。Linux 和 Windows 不做——不验收、
不作为任何步骤的阻塞项。script/port_gate.sh 里的 mingw-w64 / Linux 交叉编译只留作可移植性门禁,不代表支持这些平台。
除渲染和平台 API 外,所有玩法算法、分支、常量、状态顺序、计时来源、数据来源、封包副作用和清理路径都以 40250 为准。40250 源码就是规格,不设计行为,只照抄。
- 工作单位是 40250 的一个源文件(或超大文件里的一组函数),不是某个行为分支。
- 当前代码里没有 40250 对应物的逻辑是缺陷,要删掉。例外是明确标为 PORT 的移动端/平台功能(见第 3 节"PORT 扩展"), 它们不改变 40250 的玩法语义。
- 运行时不会加载的代码不算实现,它的测试也不算证据。
2. 已定的决定
| 日期 | 决定 | 依据 |
|---|---|---|
| 2026-09-22 | 逻辑层按 40250 结构移植:C++ → C++,文件名、类名、方法名、成员名、语句顺序都保持一致 | 能逐文件机械核对,不再"找差异" |
| 2026-09-22 | UI 走内嵌 CPython 2.7.18,原样运行 40250 的 root/*.py 和 uiscript/,只移植 C++ 模块 |
脚本无需改动;macOS 和 Android 真机均已验证,见 docs/PYTHON-EMBED-EVAL.md |
| 2026-09-22 | 数据源统一为 40250(Client/pack 的 EPK 和 Client/Eternexus 的 root/uiscript/locale),不用 m2dev 资源 |
两者有实质差异,见 docs/archive/PORT-HISTORY.md |
| 2026-09-22 | 照抄时保持 40250 的宽度和溢出语义(Win32 下 long 为 32 位),序列化结构用定宽类型并 static_assert 大小 |
64 位平台 long 是 8 字节,机械照抄会破坏 proto/封包/EPK 布局和 TEA |
| 2026-09-23 | 网络层用 40250 经典协议(1 字节包头 + 包长表 + 序列字节 + Crypto++ DH2/CTR),m2dev 分支协议放弃 | 真服 192.168.21.203 跑的是 40250 服务端 |
| 2026-09-27/28 | 宿主换成原生 SDL3 + Vulkan(native_render/),Godot 工程、GDExtension、project/*.gd 全部删除 |
D3D8 固定管线按 40250 语义直译比经 Godot 适配更接近原版;去掉一层引擎,移动端体积和功耗更可控 |
| 2026-09-28 | Android 原生客户端优先,iOS 延后 | 目标用户在手机上 |
| 2026-09-28 | 代码页转换用内置 WindowsBestFit 表(mt_codepage),不再用 iconv |
Android API 24 没有可用的 iconv;CP949/GBK 结果与 Windows 一致 |
| 2026-09-29 | 移动端专有功能(触控、自动狩猎、账号注册、帧率设置)以 PORT 模块形式注入,不改 40250 脚本 | 40250 脚本字节保持不变,账本 RUN_AS_IS 仍成立 |
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/ + native_render/ |
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/src/platform/EterLib/RecordingDevice.cpp # IDirect3DDevice8 的记录实现:把 D3D8 调用录成绘制命令
extension/src/platform/ScriptLib/PythonBoot.cpp # 解释器启动、RunMainScript、脚本线程、PORT 模块注入
extension/third_party/cpython-2.7.18/ # 静态库
native_render/main.cpp # SDL3 宿主:窗口/输入/触控、VulkanWindow 渲染器、帧率策略
native_render/android_perf.h # Android 帧率、ADPF、电量/温度(JNI 到 MainActivity)
android-native/ # Gradle 工程:SDLActivity 外壳 MainActivity、APK 打包
运行结构(每帧):
SDL3 宿主主循环(native_render/main.cpp run_live_client)
├─ SDL 事件 → 40250 的窗口消息/输入接口(鼠标、键盘、IME 文本、触控摇杆/按钮)
├─ PythonBoot::AppFrame():交给脚本线程跑一次 CPythonApplication::Process()(宿主等待,两者不并发)
│ └─ system.py / app.Loop() → UpdateGame / RenderGame / 窗口系统 Render
│ → RecordingDevice 录下 Render3DDraw(含阴影等离屏目标 rt:<id>:<w>x<h>)和 UIRenderCommand
└─ VulkanWindow:上传纹理/几何,离屏 pass → 主 pass(MSAA)→ UI → present(2 帧在途)
逻辑层规则:
- 40250 调用平台类的地方,调用
extension/src/platform/下同名的适配接口,ported 代码里不直接写 SDL/Vulkan/JNI 调用。 闭包里的 40250 头文件不分逻辑层和平台层,一律照抄到镜像路径(port/EterLib/GrpBase.h等),作为平台类的接口, 这样原来的相对 include(../eterLib/StdAfx.h)保持不变;platform/只放这些类的.cpp实现,不另写同名头文件。 - 平台层对渲染的适配止于 D3D8 接口:40250 的
Grp*、地形、水、SpeedTree、特效照抄后调用IDirect3DDevice8, 由RecordingDevice记录,native_render按 D3D8 固定管线语义(光照、雾、纹理阶段、alpha 测试/混合、填充规则)直译成 Vulkan。 - 照抄只做机械转换,用
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)由移植代码持有,生命周期与 40250 相同;宿主(native_render)不持有玩法状态,只通过PythonBoot和平台接口交互。 - 保持 40250 的宽度和溢出语义,不是机械保留 C++ 类型名。 40250 可执行文件目标是 32 位 Win32(ILP32):
long/unsigned long和指针都是 32 位;移植目标则可能是 LP64 或 LLP64。port/common/Win32Types.h只为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的毫秒,角度用度),只在适配层换算。
PORT 扩展(40250 没有对应物,只为移动端/平台而加;不能改 40250 脚本和玩法语义):
- 由
PythonBoot::RunMainScript注入的 Python 模块:mt_autohunt(自动狩猎,AutoHuntScript.inc)、mt_register(客户端内账号注册)、mt_framerate(系统选项里的"画面帧率"行)。它们包装 40250 窗口类的方法,不修改脚本文件。 - 触控控制(
native_render/touch_controller.h):摇杆、攻击/技能按钮,转成 40250 的输入接口。 - 帧率与功耗(
platform/EterBase/FrameRateMode.h+main.cpp的FrameRatePolicy):30 / 60(默认)/ 显示器最高三档, 无输入一段时间降到 30 fps(自动狩猎时除外),Android 上配合setFrameRate和 ADPF。 - 性能遥测:
--perf-log写 CSV(native_render/perf_log.h、platform/EterBase/PerfCounters.h)。
4. 路线
| 批次 | 内容 | 状态 |
|---|---|---|
| 0–1 | 工具(参考根解析、port_map.py、构建/测试入口);删除运行时不加载的 GDScript 系统 |
完成(55f733fd、5afd5a4f、a989b8f1) |
| 2A / 2R / 2P / 2D | Win32 类型层与头文件闭包;资源包盘点(只有 NONE/COMPRESS/SECURITY,设备上直接读 EPK);内嵌 CPython;数据源切到 40250 | 完成(细节见 docs/archive/PORT-HISTORY.md) |
| 2V0–2V3、2、3 | 纵向切片(UI 壳 → 登录/选角 → 进游戏 → 本地移动)及其余 P0–P3 单元;logic 层 100% | 完成(2026-09-25) |
| — | 40250 Client 移入仓库管理的资源路径;GDScript UI 全部删除 |
完成(01990214、3981cbe5) |
| N1 | 原生宿主:SDL3 窗口/输入 + Vulkan 渲染器,RecordingDevice 记录 D3D8 调用 |
完成(25dd65f2) |
| N2 | 渲染对齐单元 A–K:D3D8 固定管线直译、光照、阴影纹理、SpeedTree、STP/HTP 地形、水、Gamma、DDS mip、视距 | 完成(5285ca85 … d2356d59);像素级对比受 PARITY-GAP.md §0 门禁约束 |
| N3 | 代码页:WindowsBestFit 表替代 iconv | 完成(0b701d88) |
| N4 | Android 原生 APK(SDLActivity、arm64、API 24、16 KB 页)、真服登录、CJK 字体、中文语言包 | 完成(fd051cc3、a1ae69aa、53d06c3c) |
| N5 | 移动端:触控操作、移动端 UI、自动狩猎、客户端内注册 | 完成(3c52ced0、3e3708ef) |
| N6 | 性能:遥测、2 帧在途、120 Hz 节拍、GPU 阴影贴图 | 完成(c2cc093e) |
| N7 | 功耗:帧率设置、空闲降帧、电量遥测 | 完成(82503315) |
| N8 | 清理:删除 Godot 构建树、m2dev 网络/资源包层、过时文档 | 完成(f970f958) |
| 4 | NEEDS_LIVE 真服验证 |
进行中(见下) |
下一步(按优先级):
- platform 层剩余 TODO(实时 54.8%,第 6 节):MilesLib(135,音频目前是简化适配)、UserInterface 平台部分(85)、
ScriptLib(38)、SphereLib(36)、SpeedTreeLib(36,
CSpeedTreeRT仍是替身)、EterLib/EterImageLib 余量。 每个单元按 SKILL 的流程照抄或按可观察输出核对后更新 port-map。 - 真服验证(批次 4):角色删除、龙魂石精炼、公会标志上传未测;Cube 和商城需要与 NPC 交互后再测。
已确认 1:1 的封包见
audit/history.jsonl。 - 渲染对齐:先按
PARITY-GAP.md§0 采集受控参考帧,之后才调光照/色调/相机/材质;已知缺口有 ExpandedImage 混合模式等。 - 性能与功耗第二批:3D 渲染缩放 0.75、MSAA 4→2;脚本线程
RenderGame仍占 8–10 ms;拔线(无线 adb)下的真实 功耗 A/B;空闲降帧后偶发被唤醒的原因未查。 - 发布:正式签名、字体随包、APK 与资源交付方式;iOS 之后再做。
5. 数据源
- 资源、proto、root/uiscript/locale 全部来自 40250
Client:Client/pack的经典.eix/.epk(103 个包加root, 52,609 个路径,先注册的包优先),经移植的EterPack/EterPackManager读取;item_proto/mob_proto用移植的CLZO+ TEA 读(记录 156 / 255 字节)。 - 中文:
push-android-client.sh --locale zh推送build/zh-locale生成的locale_zh,译名来源见docs/ZH-LOCALE.md。 - 环境变量:
MT_40250_CLIENT:40250 的Client目录(pack/、Eternexus/)。native 测试的 CMake 缓存变量默认读它, 再退回${MT_40250_SOURCE}/../../Client;MT_ASSETS_STRICT=1:native 测试在资源缺失时失败(返回 1),不按跳过(77)处理。
- 来源清单:
tools/asset_manifest.py generate|verify <Client>记录pack/Index、全部.eix/.epk和Eternexus/root/*.msm的路径、大小、sha256,结果在audit/assets/40250-client.json。 Client/root的RUN_AS_IS账本指向pack://<script>.py,script/verify_root_pack.py逐个解码root.epk核对参考源码和账本 SHA-256。
6. 进度与 port-map
实时数字用 port_map.py status 查看;原样运行的 Python 脚本函数记 RUN_AS_IS(不是 N_A),计入完成。
历次基线见 docs/archive/PORT-HISTORY.md。
现状(2026-09-29,port_map.py status;port_map.py check 0 错误):
| 层 | 函数 | 完成 | 完成度 | 备注 |
|---|---|---|---|---|
| logic | 3338 | 3337 | 100.0% | |
| python | 5175 | 5123 | 99.0% | 1760 PORTED,3350 RUN_AS_IS |
| platform | 2096 | 1148 | 54.8% | TODO 集中在 MilesLib、UserInterface、ScriptLib、SphereLib、SpeedTreeLib |
| 合计 | 10609 | 9608 | 90.6% |
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/ # pack/ 和 Eternexus/root, uiscript, locale(运行和 Python 参考)
- 参考源放在其他位置时,设置
MT_40250_SOURCE=<.../ClientVS22/source>,资源设置MT_40250_CLIENT=<.../Client>。 - macOS:
brew install cmake sdl3 vulkan-headers vulkan-loader molten-vk shaderc;构建见根目录 README (-DMT_BUILD_NATIVE_RENDER=ON,目标mt_native_render)。 - Android:NDK 27.2(
ANDROID_NDK_HOME指向它)、openjdk@21、Android SDK;adb用/opt/homebrew/bin/adb。./build-android-native.sh Release --install构建安装 APK,./push-android-client.sh [--locale zh] <Client>推送资源 (APK 只含代码,资源在应用数据目录)。启动前先唤醒手机。 - Python 3:用于
port_map.py和其他审计脚本。Python 2.7.18 只在复核py_embed_spike.py时需要。 - 真服账号、密码不写进仓库或 audit 账本。
测试:
./script/port_gate.sh macos # port_platform 构建 + ctest '^port\.';另有 android/ios/windows/linux 交叉编译门禁
ctest --test-dir build-native --output-on-failure
node script/native_mac_acceptance.mjs # macOS 原生客户端验收
性能/功耗:客户端加 --perf-log(可选 --show-fps、--fps-mode 0|1|2、--fps-cap N),Android 上用
tools/pull_perf_logs.sh 拉 CSV,tools/perf_summary.py 汇总(含按帧率档位/空闲分组的电流和功耗)。插着 USB 时电量读数
没有意义,真实功耗需无线 adb 并拔线。
新会话恢复工作:
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
然后按第 4 节"下一步"继续,每完成一步在第 4、6 节更新状态并提交。