Files
mtgodot-poc/docs/PLATFORMS.md
T
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

13 KiB
Raw Blame History

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 Debugbuild/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.shbuild/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.zipOS.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_URLuser://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.