Adapt Android mobile controls and native client UI

This commit is contained in:
shen
2026-09-28 19:39:13 -07:00
parent a1ae69aa7a
commit 3c52ced02e
180 changed files with 688 additions and 23782 deletions
+15 -384
View File
@@ -1,393 +1,24 @@
<!-- generated-by: gsd-doc-writer -->
# Android 真机测试指南
# Android 原生客户端测试
本文说明如何在 Android 真机上安装本客户端(APK 产物名仍是 `mtgodot-poc.apk`,包名 `org.internal.mtgodotpoc`)、生成并导入资源包、验证资源挂载,以及进行 40250 客户端测试。
当前 APK 由 `build-android-native.sh` 构建,使用 SDL3 + Vulkan;应用包名 `org.metin2port.client`,ABI 为 `arm64-v8a`,最低 API 24。
## 1. 当前测试范围与已知前置条件
## 安装和资源
当前 Android 导出目标是 `arm64-v8a`,APK 包名为 `org.internal.mtgodotpoc`,最低 Android SDK 为 24。APK 只包含代码和 Godot 工程资源;Metin2 的 `assets/` 与 `bgm/` 不打进 APK,测试时必须单独导入 `assets.zip`。
当前仓库的资源部署存在一个需要特别注意的限制:
- `export-android.sh --install` 会把资源推到 `/sdcard/Android/data/org.internal.mtgodotpoc/files/assets.zip`。
- `project/asset_pack.gd` 的候选顺序是 `MT_ASSETS_ZIP` 环境变量 → `user://assets.zip` → `OS.get_user_data_dir()/assets.zip` → 可执行文件目录 → `res://../assets.zip`(见 `_find_local_zip()`);上述外部目录不在候选路径里。
- 因此,Debug 真机测试应把外部目录中的压缩包再复制到应用私有 `files/` 目录,确保客户端的 `user://assets.zip` 能命中。
另外,40250 classic 网络栈当前由进程环境变量 `MT_PROTOCOL=classic` 选择。Android 通过 Activity 启动 APK 时不能直接继承 Mac 终端的环境变量;如果 APK 尚未增加 Android 侧的协议配置入口或将 classic 设为移动端默认值,则只能完成 APK、资源和 UI 验证,不能据此宣称已经完成真实 40250 登录验收。
公会功能按当前项目范围暂缓;龙魂玩法不属于目标 40250 服务器功能,不纳入 Android 验收。
## 2. 主机环境
以下版本来自当前 Android Gradle 配置和构建脚本:
| 项目 | 要求 |
|---|---|
| 主机 | macOS,Apple Silicon 构建脚本已按当前机器验证 |
| Godot | `4.7.1.stable`,并安装对应 Android 导出模板 |
| Android SDK | compile/target SDK `36`,Build Tools `36.1.0` |
| Android NDK | `29.0.14206865` |
| Java | Gradle 配置最低 Java 17;当前脚本默认使用 OpenJDK 21 |
| Android 手机 | `arm64-v8a`,建议使用 Vulkan 设备 |
| 磁盘 | 至少预留 APK、资源包和临时构建空间;当前 `assets.zip` 约 2.1–2.2 GB |
进入仓库并设置环境变量。路径按本机安装位置调整:
```bash
cd /Users/shenlei/Work/mt/metin2-client
export ANDROID_SDK_ROOT="$HOME/Library/Android/sdk"
export ANDROID_HOME="$ANDROID_SDK_ROOT"
export ANDROID_NDK_ROOT="$ANDROID_SDK_ROOT/ndk/29.0.14206865"
export JAVA_HOME="/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home"
export GODOT="/opt/homebrew/bin/godot"
export ADB="$ANDROID_SDK_ROOT/platform-tools/adb"
```sh
./build-android-native.sh Release --install
./push-android-client.sh /path/to/40250/Client
adb shell am start -n org.metin2port.client/.MainActivity
```
先确认工具存在:
`push-android-client.sh` 将 40250 `Client` 目录复制到应用私有目录。APK 本身包含 `libmain.so`、`libSDL3.so`、着色器和 Python 2.7 标准库,不包含约 1.4 GB 的 40250 客户端资源。使用中文资源时运行 `./push-android-client.sh --locale zh`。
```bash
"$GODOT" --version
java -version
"$ADB" version
test -f "$ANDROID_NDK_ROOT/build/cmake/android.toolchain.cmake"
## 连服务器
```sh
adb shell am start -n org.metin2port.client/.MainActivity \
--es args "--live-server HOST:AUTH_PORT:GAME_PORT --login-screen"
```
如果 Android SDK/NDK 尚未安装,可在 Android Studio 的 SDK Manager 中安装,或使用 `sdkmanager` 安装与上表一致的版本。不要只安装一个较旧 NDK 后直接构建;Gradle 模板要求的 NDK 版本以 `project/android/build/config.gradle` 为准。
默认不传参数时连接进程内假服。测试真服前确认模拟器/设备能访问相应主机与端口。用 `adb logcat` 查看崩溃和启动错误。`adb devices -l` 没有设备时,先连接模拟器或开启真机 USB 调试。
Godot 还需要安装与 `4.7.1` 完全匹配的 Android 导出模板。可在 Godot 的“编辑器设置 → 导出 → 导出模板”中安装。
首次导出前生成 Debug keystore:
```bash
./gen-debug-keystore.sh
```
## 3. 连接 Android 真机
在手机上完成以下设置:
1. 设置 → 关于手机 → 连续点击版本号,开启开发者选项。
2. 开发者选项中开启“USB 调试”。
3. USB 连接模式选择“文件传输/MTP”。
4. 首次连接时,在手机上确认“允许 USB 调试”。
在 Mac 上检查设备:
```bash
"$ADB" kill-server
"$ADB" start-server
"$ADB" devices -l
```
期望看到:
```text
<设备序列号> device
```
状态为 `unauthorized` 时,解锁手机并接受 RSA 授权;没有设备时,优先检查 USB 线、USB 模式、开发者选项和手机是否允许该电脑调试。
## 4. 生成资源包
资源包必须从与 APK 同一份工作树生成。执行:
```bash
./pack-assets.sh
```
该脚本会:
1. 执行 `bake_asset_index.gd`,生成 `assets/asset_index.txt`。
2. 将 `assets/` 和 `bgm/` 打成 `build/export/assets.zip`。
3. 使用 `zip -0` 原样存储,不压缩 `.dds`、`.gr2` 等大型资源。
4. 排除 `.git`、`.DS_Store` 和 `assets/.gdignore`。
检查资源包:
```bash
ls -lh build/export/assets.zip
unzip -l build/export/assets.zip | rg 'assets/asset_index\.txt|assets/|bgm/' | head -n 20
```
不要手工解压到 APK,也不要把完整 `assets/` 强行放进 `res://`。客户端启动时通过 `ProjectSettings.load_resource_pack()` 挂载 zip,挂载成功后资源以 `res://assets/...` 和 `res://bgm/...` 访问。
资源发生变化后必须重新执行 `./pack-assets.sh`。只重新导出 APK 不会更新已经生成的 `assets.zip`。
## 5. 构建 APK
建议先单独构建,不要在第一次测试时直接使用 `--install`:
```bash
./export-android.sh Debug
```
构建成功后应得到:
```text
build/export/mtgodot-poc.apk
```
检查 APK 和 native library:
```bash
ls -lh build/export/mtgodot-poc.apk
unzip -l build/export/mtgodot-poc.apk | rg 'lib/arm64-v8a/.*\.so'
```
应至少包含 `lib/arm64-v8a/` 下的 Godot Android 库和 `libmtgodot` 扩展库。
## 6. 安装 APK 并导入资源
### 6.1 安装 APK
```bash
"$ADB" install -r build/export/mtgodot-poc.apk
```
`-r` 会保留应用数据。若要完全清理测试数据,可选执行:
```bash
"$ADB" shell pm clear org.internal.mtgodotpoc
```
清理后必须重新导入资源包。
### 6.2 先推送到 Android 外部目录
```bash
PKG=org.internal.mtgodotpoc
EXT_DIR="/sdcard/Android/data/$PKG/files"
"$ADB" shell mkdir -p "$EXT_DIR"
"$ADB" push build/export/assets.zip "$EXT_DIR/assets.zip"
"$ADB" shell ls -lh "$EXT_DIR/assets.zip"
```
### 6.3 Debug 测试复制到 Godot `user://`
当前 `Debug` APK 可使用 `run-as` 把外部文件复制到应用私有 `files/` 目录:
```bash
"$ADB" shell run-as "$PKG" sh -c \
"cp '$EXT_DIR/assets.zip' files/assets.zip"
"$ADB" shell run-as "$PKG" ls -lh files/assets.zip
```
最后一条命令能看到约 2.1–2.2 GB 的文件,才继续启动客户端。这个步骤是当前代码路径下的关键步骤;只执行 `adb push` 到 `/sdcard/Android/data/.../files/`,不能替代它。
如果 `run-as` 报告应用不可调试,确认安装的是 `Debug` APK,而不是 Release APK。Release 流程需要先修改资源查找/部署实现,使客户端明确支持外部目录,不能假设 `run-as` 可用。
### 6.4 资源更新
客户端退出后,只更新资源包时执行:
```bash
./pack-assets.sh
"$ADB" push build/export/assets.zip "$EXT_DIR/assets.zip"
"$ADB" shell run-as "$PKG" sh -c \
"cp '$EXT_DIR/assets.zip' files/assets.zip"
```
资源挂载只在 `client_main.gd::_ready()` 中执行,因此更新后必须重启客户端,不能只切回前台:
```bash
"$ADB" shell am force-stop "$PKG"
```
## 7. 启动客户端并确认资源挂载
清空旧日志后启动:
```bash
"$ADB" logcat -c
"$ADB" shell am start -n \
org.internal.mtgodotpoc/com.godot.game.GodotApp
```
实时查看日志:
```bash
"$ADB" logcat -v time -s godot GodotError AndroidRuntime
```
资源挂载成功的关键日志是:
```text
[AssetPack] mounted .../assets.zip
```
以下日志表示资源没有被找到或挂载失败:
```text
[AssetPack] 找不到 assets.zip
[AssetPack] load_resource_pack 失败
[client] 资源目录不存在
```
启动验收至少包含:
- 登录界面正常出现,而不是黑屏或立即退出。
- 登录背景、控件和字体资源正常显示。
- 选人页能加载职业模型、背景和界面资源。
- 进入游戏后能看到地图、地形、建筑、树木或角色模型。
- 日志中没有持续出现资源文件缺失、`FileAccess` 打开失败或 native library 加载失败。
可保存截图作为测试证据:
```bash
mkdir -p build/export/android-evidence
"$ADB" exec-out screencap -p > build/export/android-evidence/launch.png
```
## 8. 40250 服务器连接配置
### 8.1 手机网络要求
手机必须能直接访问认证服/游戏服的 IP 和端口:
- 手机与局域网测试服连接到同一网络,或服务器开放公网访问。
- 客户端不能使用 `127.0.0.1` 或 `localhost` 指向 Mac;在手机上它们指向手机自身。
- 防火墙和 FreeBSD jail 的端口映射必须允许手机来源地址访问。
- 服务器端口以实际 `CONFIG` 为准,不要照抄示例端口。
### 8.2 `serverlist.txt`
客户端启动时会优先读取 `res://serverlist.txt`;没有该文件时使用 `project/net/serverinfo.gd` 中的内置默认值。真实 40250 测试应在导出前准备 `project/serverlist.txt`,格式为 TSV:
```text
# 名称\t认证地址\t认证端口\t游戏地址\t游戏端口\t频道列表\t端口步长\t会徽端口
龙魂40250\t<服务器IP>\t<auth-port>\t<服务器IP>\t<channel1-port>\t1,2,3,4\t1\t0
```
例如 40250 仓库脚本中的默认 channel1 端口与当前客户端内置默认值并不相同,必须以目标 FreeBSD 服务器的 `CONFIG` 和实际监听结果为准。
<!-- VERIFY: 真实 40250 服务器 IP、auth/game 端口、频道数量和防火墙策略必须由当前 FreeBSD 部署确认。 -->
### 8.3 classic 协议选择限制
当前 native 代码仅在进程环境变量满足以下条件时切换 40250 classic 后端:
```text
MT_PROTOCOL=classic
```
在 Mac 上运行 Godot 时可以通过环境变量设置;但 Android APK 是由 Activity 启动的,普通的 `adb shell am start` 不会把 Mac 的环境变量传入 APK。因此,真实 Android 40250 登录测试前必须确认以下任一项已经完成:
1. Android 版本把 classic 设置为默认网络后端;
2. 客户端增加可持久化的协议配置并在 Android 导出中设为 `classic`;或
3. Android 测试包提供协议选择入口。
不要把下面的命令当作可靠解决方案:
```bash
adb shell MT_PROTOCOL=classic am start ...
```
它设置的是 `am` 命令进程的环境,不等于给已启动的 Android Activity 设置环境变量。
## 9. 真机功能验收清单
以下项目按实际 40250 测试账号、角色和 NPC 条件逐项记录:
### 启动与网络
- [ ] APK 安装成功,包名为 `org.internal.mtgodotpoc`。
- [ ] `assets.zip` 已复制到应用私有 `files/`,大小与主机一致。
- [ ] 日志出现 `[AssetPack] mounted`。
- [ ] 登录界面资源完整显示。
- [ ] 服务器/频道检测能得到预期结果。
- [ ] 认证成功后进入选人页。
- [ ] 选择角色后进入游戏场景。
### 世界与基础操作
- [ ] 地图、地形、建筑、树木、角色和纹理正常加载。
- [ ] 点击地面移动,角色位置与服务器同步。
- [ ] 普通攻击、目标选择、技能和动作没有异常断线。
- [ ] 聊天发送与接收正常。
- [ ] 切换前后台后连接和场景状态符合预期;断线时能看到重连提示。
### 已接入的常用功能
- [ ] 背包打开、物品使用、丢弃、移动和快捷栏操作。
- [ ] NPC 对话、脚本选项、输入框和确认框。
- [ ] 普通商店查看、切换货架和购买。
- [ ] 私人商店物品选择、定价、开设和撤收。
- [ ] 交易发起、物品/金币添加、确认和取消。
- [ ] 仓库密码、存入、取出和物品移动。
- [ ] 商城物品列表和取出。
- [ ] 组队邀请、接受、离队、成员状态和治疗入口。
- [ ] ESC 系统菜单、音量、镜头、雾效和显示设置持久化。
### 明确不纳入本轮
- 公会相关功能:按当前范围暂缓。
- 龙魂:目标 40250 服务器没有该功能,不进行功能验收。
- 真实 FreeBSD + MySQL 全流程验收:需要服务器环境和测试账号,不能用本机离线测试结果替代。
## 10. 测试证据保存
建议每次测试保留 APK、资源包校验值、设备信息和日志:
```bash
mkdir -p build/export/android-evidence
shasum -a 256 build/export/mtgodot-poc.apk \
build/export/assets.zip \
> build/export/android-evidence/artifacts.sha256
"$ADB" shell getprop ro.product.model \
> build/export/android-evidence/device-model.txt
"$ADB" shell getprop ro.build.version.release \
> build/export/android-evidence/android-version.txt
"$ADB" logcat -d -v time -s godot GodotError AndroidRuntime \
> build/export/android-evidence/logcat.txt
```
测试记录至少填写:构建时间、Git commit、APK SHA-256、`assets.zip` SHA-256、手机型号、Android 版本、服务器地址/端口、账号、测试结果和失败日志。
## 11. 常见问题
| 现象 | 处理 |
|---|---|
| `adb devices` 没有设备 | 开启 USB 调试、接受 RSA、切换 MTP、检查数据线;必要时重新执行 `adb kill-server` / `adb start-server` |
| 状态是 `unauthorized` | 解锁手机并接受“允许 USB 调试”,然后重新执行 `adb devices` |
| APK 安装失败 | 检查设备是否为 arm64;卸载旧包后再安装;查看 `adb install` 的具体错误 |
| `run-as` 报不可调试 | 安装 `Debug` APK;Release APK 不能依赖 `run-as` 导入资源 |
| 日志找不到 `assets.zip` | 确认外部 push 成功,再执行 `run-as ... cp ... files/assets.zip`,然后重启应用 |
| `load_resource_pack 失败` | 检查压缩包完整性、磁盘空间、是否由 `./pack-assets.sh` 生成,以及 zip 内是否存在 `assets/asset_index.txt` |
| 登录连接超时 | 手机不能使用 `localhost`;检查服务器 IP、端口、Wi-Fi/VPN、防火墙和 FreeBSD jail 映射 |
| 登录协议不对或立即断线 | 确认 Android 包实际选择了 `classic`,仅在 Mac 终端设置 `MT_PROTOCOL` 不会自动传入 APK |
| 黑屏或 Vulkan 崩溃 | 保存完整 `logcat`;当前 Android 移动 Vulkan 的 shader/GPU-skin 真机验证仍需在目标设备上进行 |
| 更新资源后画面不变 | 资源包挂载发生在启动阶段,执行 `am force-stop` 后重新启动;必要时清理应用数据并重新导入 |
## 12. 推荐的最短测试流程
首次测试按以下顺序执行:
```bash
cd /Users/shenlei/Work/mt/metin2-client
./pack-assets.sh
./export-android.sh Debug
export ADB="${ANDROID_SDK_ROOT:-$HOME/Library/Android/sdk}/platform-tools/adb"
PKG=org.internal.mtgodotpoc
EXT_DIR="/sdcard/Android/data/$PKG/files"
"$ADB" install -r build/export/mtgodot-poc.apk
"$ADB" shell mkdir -p "$EXT_DIR"
"$ADB" push build/export/assets.zip "$EXT_DIR/assets.zip"
"$ADB" shell run-as "$PKG" sh -c \
"cp '$EXT_DIR/assets.zip' files/assets.zip"
"$ADB" shell run-as "$PKG" ls -lh files/assets.zip
"$ADB" logcat -c
"$ADB" shell am start -n "$PKG/com.godot.game.GodotApp"
"$ADB" logcat -v time -s godot GodotError AndroidRuntime
```
只有在看到 `[AssetPack] mounted`、登录界面资源正常、并确认 Android 包使用正确的 40250 classic 后端后,才进入真实服务器功能验收。
更详细的构建依赖与命令见 [native_render/README.md](../native_render/README.md)。
+8 -203
View File
@@ -1,207 +1,12 @@
# Platform builds (macOS / iOS / Android)
# 平台构建
Phase 1 shipped the GDExtension for macOS only. As of 2026-08-30 the build system
is opened up for the two Phase 2 devices (BACKLOG F1/F2): **iPhone 16** (iOS,
Metal) and **OnePlus 13** (Android arm64, Vulkan). The native deps that used to
come from Homebrew are now vendored and cross-compile from the same tree — see
`docs/THIRD-PARTY.md`.
当前客户端使用 SDL3 + Vulkan 原生渲染,目标平台是 macOS arm64 和 Android arm64。核心 40250 移植代码在 `extension/src/port` 与 `extension/src/platform`,与 UI 引擎无关。
**Scope (decided 2026-09-23, PORT-PLAN §1):** the shipping targets are **macOS
arm64** and **Android arm64**. iOS arm64 stays cross-compile-clean but is not an
acceptance target. **Linux and Windows are not done** — the mingw-w64 cross
build is kept only as a portability gate (it catches real include/layering bugs),
not as Windows support, and MSVC is not built at all.
| 平台 | 构建入口 | 产物 |
| --- | --- | --- |
| macOS | `cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release -DMT_BUILD_NATIVE_RENDER=ON -DCMAKE_PREFIX_PATH=/opt/homebrew`,然后 `cmake --build build-release --target mt_native_render -j8` | `build-release/native_render/mt_native_render` |
| Android | `./build-android-native.sh Release` | `android-native/app/build/outputs/apk/debug/app-debug.apk` |
## What each script does
macOS 的 Vulkan loader 通过 MoltenVK 使用 Metal。Android APK 使用 SDLActivity 和 `libmain.so`。40250 `Client` 资源在 macOS 通过 `--live-client` 指定,在 Android 通过 `push-android-client.sh` 放进应用目录。
| Script | Target | Output |
|---------------------|---------------------------|---------------------------------------------------------|
| `./build.sh` | macOS (host) | `project/bin/libmtgodot.macos.template_{debug,release}.dylib` |
| `./build-ios.sh` | iOS arm64 device (`OS64`) | `project/bin/libmtgodot.ios.template_{debug,release}.a` + staged deps in `project/bin/ios/` |
| `./build-android.sh`| Android `arm64-v8a` | `project/bin/libmtgodot.android.template_{debug,release}.arm64.so` |
| `./export-android.sh [Debug\|Release] [--install]` | Android APK (wraps build-android + Godot export) | `build/export/mtgodot-poc[-release].apk` |
| `./gen-debug-keystore.sh` | Android debug keystore (once) | `~/Library/Application Support/Godot/keystores/debug.keystore` |
| `./pack-assets.sh` | Metin2 assets → mountable zip | `build/export/assets.zip` (`zip -0`, ~2.2 GB) |
All three run `git submodule update --init --recursive` if needed. The host tools
(`packtool`, `net_probe`) and CTest suite build only on a host build
(`CMAKE_SYSTEM_NAME == CMAKE_HOST_SYSTEM_NAME`), never when cross-compiling.
`project/bin/mtgodot.gdextension` has the `[libraries]` entries for all three
platforms and an `[dependencies]` block for iOS (see below).
## Status
### macOS — ✅ done
Unchanged. `./build.sh` → dylib, `ctest` 9/9.
### iOS — 🟡 compiles; export/sign/device pending (F1)
**Verified here (Xcode 26.4.1 + iPhoneOS 26.4 SDK):**
- `./build-ios.sh Debug` cross-compiles the whole surface to **arm64, minos 15.0**:
libsodium, libzstd, miniLZO, libgr2, xr_formats, mtnet, godot-cpp, and the
extension itself → `libmtgodot.ios.template_debug.a` (valid `ar` archive).
- iOS forces the extension to **STATIC** (`MT_LIB_KIND` in `extension/CMakeLists.txt`)
because the platform can't `dlopen`. Godot links the archive + its deps into the
app at export time, which is why `.gdextension` carries an `[dependencies]`
`ios.debug` / `ios.release` map listing every archive `build-ios.sh` stages
into `project/bin/ios/`.
**Not done (needs the device + Godot iOS export templates, ~2–3 days):**
- Install matching Godot 4.7 iOS export templates.
- Godot editor → Export → iOS preset; add `project/bin/ios/*.a` as extra link
libraries (or fold into an `.xcframework`).
- Free personal provisioning profile + signing for on-device install.
- First run on the iPhone 16; capture a frame for F3.
- Release build (`./build-ios.sh Release`) and size/strip pass.
### Android — ✅ APK builds; on-device run pending USB debugging (F2/F3)
**Done (2026-08-31):**
- `extension/CMakeLists.txt` handles `CMAKE_SYSTEM_NAME == Android` (SHARED `.so`,
`arm64` output-name suffix). `./build-android.sh` drives it through the NDK's own
`build/cmake/android.toolchain.cmake` (`ANDROID_ABI=arm64-v8a`, `android-24`).
- Toolchain on this machine: `sdkmanager "ndk;27.2.12479018" "platforms;android-35"
"build-tools;35.0.0"`; `brew install openjdk@21`; Godot 4.7.1 export templates
installed; `./gen-debug-keystore.sh`.
- `editor_settings-4.7.tres`: `export/android/android_sdk_path` = `~/Library/Android/sdk`,
`java_sdk_path` = `/opt/homebrew/opt/openjdk@21/.../Home`, `debug_keystore` = the
generated one (pass `android`).
- `project/export_presets.cfg` `[preset.1]` "Android": `arm64-v8a` only,
`gradle_build/use_gradle_build=false` (prebuilt template APK — no Gradle/JDK17 build),
`permissions/internet=true`, pkg `org.internal.mtgodotpoc`, minSdk 24.
- `./export-android.sh Debug` → `build/export/mtgodot-poc.apk` (~73 MB), signed with the
debug keystore via `build-tools/28.0.3/apksigner`. Contains
`lib/arm64-v8a/{libgodot_android.so, libmtgodot.android.template_debug.arm64.so, libc++_shared.so}`.
- Renderer stays `forward_plus` (OnePlus 13 = flagship Vulkan). No GLES3 / Compatibility
fallback — all target devices are modern.
**Asset IO portability (A1, 2026-08-31):** the extension used to read every asset with raw
`std::fopen` / `std::ifstream`, which cannot read a Godot PCK — so a bundled-assets APK
would launch to nothing. Fixed by routing all *running-extension* reads through
`godot::FileAccess` (works for `res://` PCK, `user://`, and absolute OS paths):
- `extension/src/asset_io.{h,cpp}` — `read_file()` / `dds_from_file()` / `gr2_from_file()`.
All `gr2::File::load_path` and `load_dds_path` call sites (model / anim / weapon / hair /
static objects / trees / terrain splat / water / shadowmap) now go through it.
- `formats/` funnels every read through `fmt::read_file`; added `fmt::set_file_reader()` and
`register_types.cpp` installs a `FileAccess`-backed reader → `height.raw` / `tile.raw` /
`water.wtr` / `.msenv` / `areadata.txt` / `map_setting` / `property` / `.spt` /
`texture_set` / `.msm` / `.msa` (textscript) all portable.
- `mtproto`: added `load_proto_bytes()`; `Metin2Proto` reads via `asset_io`.
- The standalone-lib `*_path()` functions are untouched, so the four non-Godot CTests
(`proto.item_mob`, `pack.roundtrip`, `formats.map_formats`, `libgr2.loader_errors`) stay green.
- **Phase 2b done (2026-08-31):** `fmt::AssetResolver` can no longer `std::filesystem`-scan a
PCK, so it's now a **baked index**: `AssetResolver::save_index()` / `load_index()` (line format
`MTIDX1` + `Y|R\t<key>\t<rel>`, paths relative to assets_root), and
`build_or_load(root)` → loads `<root>/asset_index.txt` via `fmt::read_file` if present, else
scans (desktop dev). `resolve()` now returns `assets_root + "/" + rel` so it's portable
(`res://assets/...` on device). Generate the index with
`godot --headless --path project --script bake_asset_index.gd` (→ `assets/asset_index.txt`,
~8 MB, 54k files) — `export-android.sh` does this automatically. `metin2_world.cpp` calls
`build_or_load`; `Metin2World.bake_asset_index(out)` is the bound builder method.
- **`eterpack` / `asset_source`** — unused while the dev asset tree is fully loose (no `.epk`).
### Bundling the assets (mobile) — the mount-a-zip approach
The APK/IPA is **code-only** (73 MB): `assets/` (2.1 GB) + `bgm/` (80 MB) are gitignored and
sit beside `project/`, not under `res://`. Getting them into `res://` via the exporter fails:
`.gdignore` blocks `include_filter` too, and without `.gdignore` Godot imports the ~10 k
`.dds`/`.tga`/`.jpg`/`.wav` → converts them to `.ctex`/etc → `FileAccess("res://.../x.dds")`
no longer yields raw DXT bytes → the C++ decoder breaks. (It also litters the asset tree with
`.import`/`.uid` sidecars and bloats `.godot/` to ~190 MB.)
Instead: **ship a plain `zip -0` and mount it at runtime.** All C++ asset IO now goes through
`godot::FileAccess` (see A1 above), so once the zip is mounted at `res://` everything reads
from it transparently.
- `./pack-assets.sh` → `build/export/assets.zip` (bakes `asset_index.txt` first; `zip -0`
store, no compression — DXT/gr2 don't compress and we want fast random access; paths stay
`assets/…` `bgm/…` so they mount as `res://assets/…` `res://bgm/…`).
- `project/asset_pack.gd` (`class_name AssetPack`) — `ensure()` finds the zip
(`MT_ASSETS_ZIP` env → `user://assets.zip` → `OS.get_user_data_dir()/assets.zip` → exe-dir →
`res://../assets.zip`) and `ProjectSettings.load_resource_pack()`s it. `client_main.gd`
calls it in `_ready()` before `AppFlow.start()`.
- `project/asset_root.gd` — adds `res://assets` as the first candidate **iff**
`res://assets/asset_index.txt` exists (the "pack mounted" sentinel; the empty
`project/assets/.gdignore` stub doesn't count).
- Deploy: `./export-android.sh Debug --install` pushes both the APK and (if present)
`assets.zip` → `/sdcard/Android/data/org.internal.mtgodotpoc/files/assets.zip` over USB.
iOS later: `ios-deploy --bundle_id … --upload assets.zip --to Documents/`. Same mount code.
- **Future (download instead of push):** only `AssetPack._find_local_zip()`'s failure branch
changes — HTTPRequest `ASSET_PACK_URL` → `user://assets.zip` (resume + sha256 + progress UI)
→ same `load_resource_pack`. Structurally additive.
- One more `std::filesystem` scan turned up: `fmt::PropertyRegistry::scan()` walks
`<root>/Property/` for `.pr*` CRC files (drives building/tree placement). Added
`PropertyRegistry::scan_list(root, rel_paths)` + `AssetResolver::all_rel()`; `metin2_world.cpp`
feeds it the resolver's file list (both index-loaded and dir-scanned modes). `scan()` kept
for the `formats.map_formats` CTest.
- **Verified end to end**: force-mount the real `assets.zip` (2.1 GB, 58183 entries) with the
loose tree hidden → `Metin2World.load_map("OutdoorA1/metin2_map_a1")` →
20/20 chunks, 20 splatted, **601 objects (0 missing), 368 trees, 1330 property CRCs**, water,
env — byte-for-byte the same result as the loose tree. Desktop unaffected. ctest 10/10,
GDScript 34/34. Android `.so` + iOS `.a` rebuilt.
**Pending:** the OnePlus 13 is on USB and macOS sees it (`ioreg` shows vendor "OnePlus"),
but `adb devices` is empty → enable **Developer options → USB debugging** on the phone and
accept the "Allow USB debugging?" RSA prompt (USB mode: File transfer / MTP). Then:
- `./export-android.sh Debug --install` (or `adb install -r build/export/mtgodot-poc.apk`
then `adb shell am start -n org.internal.mtgodotpoc/com.godot.game.GodotApp`).
- `adb logcat -s godot GodotError Godot` — first run WILL need shader / GPU-skin fixes
under Godot's mobile Vulkan path (never exercised); capture a frame for F3.
- adb note: a stale `sdk/platform-tools/adb` daemon can hold `:5037` ("Address already in
use"); `pkill -9 -f adb` then use one adb consistently (`/opt/homebrew/bin/adb`).
## Notes / gotchas
- The `ranlib: ... has no symbols` spam during a libsodium build is harmless: the
x86 SIMD translation units compile to empty objects on arm64 (their bodies are
guarded by CPU-feature macros). libsodium-cmake compiles the full file list on
every arch by design.
- The staged `project/bin/ios/libgodot-cpp.*.a` is ~430 MB in Debug (all classes,
unstripped). Fine for a dev link; Release + dead-strip shrinks it hard.
`project/bin/ios/` and `project/bin/android/` are gitignored.
- Shaders (`SRC_TERRAIN/WATER/LEAF/SKIN/MIX` + the §2.7 spec branch) and the
GPU-skin bone-texture path have **not** been exercised under Godot's Mobile
renderer yet — that's a separate verification pass once a device boots.
- `net_stream.cpp` is pure POSIX sockets → fine on iOS/Android/Linux/macOS; only
a Windows target would need a Winsock shim.
## GPU texture compression (F4)
`extension/src/texture_util.{h,cpp}` — `make_color_texture(w,h,rgba,len,mipmaps)`
is the single funnel for "decoded RGBA8 → `ImageTexture`". On a mobile OS (or
`MTGODOT_TEXCOMP=1`) it runs the decoded colour image through runtime **ASTC 8x8**
(~2 bpp vs 32 for RGBA8), falling back to RGBA8 if the encoder is unavailable.
Desktop default is **off** and byte-identical to the old per-site code.
Wired: character skins (`metin2_model::_load_dds`), building albedo
(`static_object`), tree bark / leaf-composite (`tree_placeholder`). **Not** wired
(exact texel values matter): splat control maps, `shadowmap`, water, HUD
`load_dds` / minimap.
`texcomp_set_enabled(bool)` overrides at runtime. Verified: macOS + iOS compile,
`ctest` 9/9, desktop character render unchanged, `MTGODOT_TEXCOMP=1` render OK.
Remaining: on-device quality check + VRAM/bandwidth numbers (needs F1/F2).
## App lifecycle (F5)
`project/app_lifecycle.gd` — an `AppLifecycle` node. Add it early (or as an
autoload); `bind(m2client, audio)` wires the common consumers. It turns the
SceneTree-forwarded MainLoop notifications into signals
(`paused` / `resumed` / `focus_changed(bool)` / `memory_warning` /
`back_requested` / `close_requested`) and, by default, on background: pauses the
tree, stops BGM, drops `Engine.max_fps` to 8; restores on foreground.
`pause_tree_on_background` / `background_max_fps` are tunable (login screen sets
`pause_tree_on_background = false`).
`M2Client` (C++) handles `NOTIFICATION_APPLICATION_PAUSED/RESUMED` itself →
`suspend()` / `resume()`. While suspended `_process()` doesn't pump the socket.
A mobile OS drops the TCP connection within ~30 s of backgrounding, so the first
pump after `resume()` surfaces `disconnected`; `reconnect()` redoes the full
auth→game login from stored credentials. `login.gd` auto-calls it on `resumed`
when the stage was in-game. New signals: `suspended`, `resumed`.
Rendering-context loss (Android Vulkan surface) is handled by Godot; every GPU
resource we hold (bone textures, MultiMesh, ShaderMaterials, ImageTextures) is
RenderingServer-managed and survives — nothing for the extension to do.
Verified: headless smoke (`suspend`/`resume`/`reconnect` bound, idempotent,
signals fire once), `ctest` 9/9, iOS compile. Remaining: on-device background/
resume/reconnect cycle and a real iOS memory-warning.
原生客户端的详细配置与验收步骤见 [native_render/README.md](../native_render/README.md) 和 [ANDROID-TESTING.md](ANDROID-TESTING.md)。
+2
View File
@@ -1,5 +1,7 @@
# 40250 1:1 移植:路线与方案
> 历史迁移记录:2026-09-28 起,客户端使用 `native_render/` 的 SDL3 + Vulkan 原生路径。本文关于 Godot 工程、GDExtension、`project/`、Godot 导出和相应门禁的描述仅记录过去的迁移过程;这些代码已删除。当前构建方法见根目录 README 和 `native_render/README.md`。
本文是移植工作的总入口,换一台电脑继续开发时先读这份。更新日期:2026-09-22。
- 方法细则:`.agents/skills/metin2-40250-parity-audit/SKILL.md`(每轮的做法、状态定义、工具)