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
13 KiB
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 Debugcross-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(validararchive).- iOS forces the extension to STATIC (
MT_LIB_KINDinextension/CMakeLists.txt) because the platform can'tdlopen. Godot links the archive + its deps into the app at export time, which is why.gdextensioncarries an[dependencies]ios.debug/ios.releasemap listing every archivebuild-ios.shstages intoproject/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/*.aas 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.txthandlesCMAKE_SYSTEM_NAME == Android(SHARED.so,arm64output-name suffix)../build-android.shdrives it through the NDK's ownbuild/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 (passandroid).project/export_presets.cfg[preset.1]"Android":arm64-v8aonly,gradle_build/use_gradle_build=false(prebuilt template APK — no Gradle/JDK17 build),permissions/internet=true, pkgorg.internal.mtgodotpoc, minSdk 24../export-android.sh Debug→build/export/mtgodot-poc.apk(~73 MB), signed with the debug keystore viabuild-tools/28.0.3/apksigner. Containslib/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(). Allgr2::File::load_pathandload_dds_pathcall sites (model / anim / weapon / hair / static objects / trees / terrain splat / water / shadowmap) now go through it.formats/funnels every read throughfmt::read_file; addedfmt::set_file_reader()andregister_types.cppinstalls aFileAccess-backed reader →height.raw/tile.raw/water.wtr/.msenv/areadata.txt/map_setting/property/.spt/texture_set/.msm/.msa(textscript) all portable.mtproto: addedload_proto_bytes();Metin2Protoreads viaasset_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::AssetResolvercan no longerstd::filesystem-scan a PCK, so it's now a baked index:AssetResolver::save_index()/load_index()(line formatMTIDX1+Y|R\t<key>\t<rel>, paths relative to assets_root), andbuild_or_load(root)→ loads<root>/asset_index.txtviafmt::read_fileif present, else scans (desktop dev).resolve()now returnsassets_root + "/" + relso it's portable (res://assets/...on device). Generate the index withgodot --headless --path project --script bake_asset_index.gd(→assets/asset_index.txt, ~8 MB, 54k files) —export-android.shdoes this automatically.metin2_world.cppcallsbuild_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(bakesasset_index.txtfirst;zip -0store, no compression — DXT/gr2 don't compress and we want fast random access; paths stayassets/…bgm/…so they mount asres://assets/…res://bgm/…).project/asset_pack.gd(class_name AssetPack) —ensure()finds the zip (MT_ASSETS_ZIPenv →user://assets.zip→OS.get_user_data_dir()/assets.zip→ exe-dir →res://../assets.zip) andProjectSettings.load_resource_pack()s it.client_main.gdcalls it in_ready()beforeAppFlow.start().project/asset_root.gd— addsres://assetsas the first candidate iffres://assets/asset_index.txtexists (the "pack mounted" sentinel; the emptyproject/assets/.gdignorestub doesn't count).- Deploy:
./export-android.sh Debug --installpushes both the APK and (if present)assets.zip→/sdcard/Android/data/org.internal.mtgodotpoc/files/assets.zipover 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 — HTTPRequestASSET_PACK_URL→user://assets.zip(resume + sha256 + progress UI) → sameload_resource_pack. Structurally additive. - One more
std::filesystemscan turned up:fmt::PropertyRegistry::scan()walks<root>/Property/for.pr*CRC files (drives building/tree placement). AddedPropertyRegistry::scan_list(root, rel_paths)+AssetResolver::all_rel();metin2_world.cppfeeds it the resolver's file list (both index-loaded and dir-scanned modes).scan()kept for theformats.map_formatsCTest. - 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.arebuilt.
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(oradb install -r build/export/mtgodot-poc.apkthenadb 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/adbdaemon can hold:5037("Address already in use");pkill -9 -f adbthen use one adb consistently (/opt/homebrew/bin/adb).
Notes / gotchas
- The
ranlib: ... has no symbolsspam 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.*.ais ~430 MB in Debug (all classes, unstripped). Fine for a dev link; Release + dead-strip shrinks it hard.project/bin/ios/andproject/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.cppis 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.