Files
mtgodot-poc/docs/PORT-PLAN.md
T
shenleiandClaude Opus 5.5 0137f852d3 docs: rewrite PORT-PLAN for the native SDL3 architecture
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>
2026-09-29 18:26:54 +09:00

218 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 节更新状态并提交。