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>
218 lines
18 KiB
Markdown
218 lines
18 KiB
Markdown
# 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` 里记录适配不变式。
|
||
- 单位保持 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 <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 账本。
|
||
|
||
测试:
|
||
|
||
```bash
|
||
./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 并拔线。
|
||
|
||
新会话恢复工作:
|
||
|
||
```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
|
||
```
|
||
|
||
然后按第 4 节"下一步"继续,每完成一步在第 4、6 节更新状态并提交。
|