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
202 lines
13 KiB
Markdown
202 lines
13 KiB
Markdown
# 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<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.
|