# 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, ~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\t`, paths relative to assets_root), and `build_or_load(root)` → loads `/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 `/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.