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

18 KiB
Raw Blame History

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 账本。

测试:

./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 节更新状态并提交。