# 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//.{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/` + `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::x)和 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 `: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` 里记录适配不变式。 - 单位保持 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` 真服验证 | 进行中(见下) | **下一步**(按优先级): 1. **platform 层剩余 TODO**(实时 54.8%,第 6 节):MilesLib(135,音频目前是简化适配)、UserInterface 平台部分(85)、 ScriptLib(38)、SphereLib(36)、SpeedTreeLib(36,`CSpeedTreeRT` 仍是替身)、EterLib/EterImageLib 余量。 每个单元按 SKILL 的流程照抄或按可观察输出核对后更新 port-map。 2. **真服验证(批次 4)**:角色删除、龙魂石精炼、公会标志上传未测;Cube 和商城需要与 NPC 交互后再测。 已确认 1:1 的封包见 `audit/history.jsonl`。 3. **渲染对齐**:先按 `PARITY-GAP.md` §0 采集受控参考帧,之后才调光照/色调/相机/材质;已知缺口有 ExpandedImage 混合模式等。 4. **性能与功耗第二批**:3D 渲染缩放 0.75、MSAA 4→2;脚本线程 `RenderGame` 仍占 8–10 ms;拔线(无线 adb)下的真实 功耗 A/B;空闲降帧后偶发被唤醒的原因未查。 5. **发布**:正式签名、字体随包、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 ` 记录 `pack/Index`、全部 `.eix/.epk` 和 `Eternexus/root/*.msm` 的路径、大小、sha256,结果在 `audit/assets/40250-client.json`。 - `Client/root` 的 `RUN_AS_IS` 账本指向 `pack://