Files
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
  phases, EntityStore world model, ~all GC/CG headers. char create/delete,
  private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
  quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
  char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
  token), system-option + game-option + ESC system menu, private-shop 39-grid,
  party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
  dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.

Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.

Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).

ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
2026-08-31 20:02:12 +09:00

202 lines
13 KiB
Markdown
Raw Permalink 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.
# 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`.
## What each script does
| 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, ~23 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.