Android Build - DatMoshu/GodotUO GitHub Wiki
GUO exports as an ARM64 debug APK for an Android device (the build's
floor is Android 7.0, ADR-0017; only Android 13 has been run), without the
NDK. The export uses Godot's Gradle build: run.py installs the pinned
source template, and the first export needs access to Maven. Source of truth: ADR-0017 (Accepted) and
tools\android\README.md, whose run table records what actually ran on a
device. The first device run is commit "Android: first run on a device";
everything below has been exercised on one handheld with two screens, so a
second device is the next real test.
launchers\android\doctor.bat REM what is missing, and the fix for each
launchers\android\export.bat REM a debug APK, headless, into build\android
launchers\android\install.bat REM adb install onto the attached device
launchers\android\run.bat REM start it and stream its logcat
launchers\android\smoke.bat REM export, install, run, wait for the login gump, pull a screenshot and a log
launchers\android\dual_probe.bat REM the second-screen probe, see Dual Screen
launchers\android\portrait_probe.bat REM the portrait spike: turns the screen, photographs each state
launchers\android\device_manager.bat REM the Device Test Manager: device profiles, emulator, screenshotsUnderneath: python tools\android\run.py <doctor|templates|keystore|release-keystore|settings|preset|export|install|run|logcat|push|push-stage|smoke|avd|dual_probe|portrait_probe|displays>.
With no data on the device, the first-run screen's Choose folder opens the
system folder picker and copies the folder into the app's own files; see
tools\android\README.md.
| Part | Where it comes from | Setting |
|---|---|---|
| JDK 17 | your install |
UO_ANDROID_JDK (default %JAVA_HOME%) |
Android SDK with platform-tools and build-tools
|
Android Studio or the command-line tools | UO_ANDROID_SDK |
| Mono export templates for 4.7.2 |
run.py templates downloads them into %APPDATA%\Godot\export_templates\4.7.2.stable.mono
|
none |
| A debug keystore |
run.py keystore makes one with the standard androiddebugkey / android
|
UO_ANDROID_KEYSTORE* |
| .NET SDK 9 | the Android target of Godot 4.7's template is net9.0; GUO.csproj switches to it when GodotTargetPlatform is android
|
none |
| adb and a device with USB debugging on |
UO_ANDROID_DEVICE (empty picks the single ready device) |
Godot reads the SDK, the JDK and the keystore from its editor settings
file and nowhere else (not ANDROID_HOME, not JAVA_HOME), so
run.py settings writes the values from config.bat into
%APPDATA%\Godot\editor_settings-4.7.tres (a .bak is kept), and export
does so before every export. The export preset is rendered from
tools\android\export_presets.template.cfg into the gitignored
godot\GUO\export_presets.cfg, which carries the keystore path and password;
that is why the preset is never committed. The preset excludes
addons/guo_editor/* so the editor add-on's sources are not packed.
export.bat --release makes build\android\GUO-release.apk, signed with
your own key: UO_ANDROID_RELEASE_KEYSTORE, _USER and _PASSWORD, set in
config.local.bat or the environment, never in the repository. The keystore
must live outside the checkout. python tools\android\run.py release-keystore
makes one and doctor.bat --release checks it. The release workflow builds
one on a tag when the repository has the matching secrets. Details, and why the
key never reaches the preset file: tools/android/README.md, "A release APK,
signed with your key".
The UO install is never inside the APK. run.py push copies it to the app's
external files folder, UO_ANDROID_CLIENT_DATA
(/sdcard/Android/data/<package>/files/uo), and the export bakes
--play --client-data <that path> into the APK's command_line/extra_args,
so the app starts straight into the login screen. Exports are silent unless
--sound is in the baked arguments.
run.py run --args "--host <address of the PC running the shard>" points the
device at a shard on your LAN. The shard must listen on an address the phone
can reach, not only loopback; see Dev Shard.
Recorded in ADR-0017 and the first-run commit, in case you hit them elsewhere:
-
project.godotsetsimport_etc2_astc; the export refuses without it. - No console colour in the logger on Android (
PlatformNotSupportedException). - Managed zlib on Android: the system
libzbinding aborts. - The plugin host is a Windows-only build item; a publish for android-arm64
failed in
tools\plugin_hostbefore any GUO code was built. Assistant plugins are not part of the Android build. - The whole-number screen scale divides the OS scale out, so the client's own
integer
ScreenScaleis used instead of Godot stretch. Pixel art stays unfiltered. - Immersive mode only survives scene changes in
WindowMode.Fullscreen, so a mobile OS keeps that mode through the login and game windows. - The account field raises the e-mail keyboard and the password field the password keyboard: the two input types an Android IME must not autocorrect in. The IME's check mark arrives as Enter and logs in.
The export is locked to sensor-landscape. Portrait was tried on an Odin 2 Mini (2026-09-28) and turned down on one number:
| Landscape | Portrait | |
|---|---|---|
| Turning the screen | — | 95 ms, nothing lost |
| World in view (tiles across x down) | 15.3 x 8.6 | 11.5 x 20.4 |
| Command-bar button | 13.2 x 7 mm | 7.4 x 2.3 mm |
| Paperdoll, backpack, Modern gumps | fit | fit |
The world works in portrait, and shows twice as far north and south. The
command bar does not: its buttons shrink to 2.3 mm tall, which a thumb
cannot hit, and the login screen stays where landscape put it, half off the
screen. Both are fixable, so portrait could come back as an option later;
the measurements and the probe that took them are in the repository's
docs/android_portrait_spike.md and ADR-0017.
launchers\android\smoke.bat exports, installs, launches, waits for the
login probe's line on logcat ([GUO] prefix, the same line the web smoke
waits for), pulls a screenshot and the log into build\android\. It exits 0
only when the login gump was drawn. smoke.bat --avd guo_phone does the
same on GUO's own x86_64 emulator: it boots it headless (only with 10 GB of
memory free), installs, waits for the login screen, takes a screenshot and
cleans up, touching only the emulator it started. run.py displays lists the device's
displays for Dual Screen work.
From ADR-0017 and the README table: export, install, login by touch alone, shard list, character selection, arriving in the world, hold-to-walk, pinch zoom, zoom kept across relaunch, the soft keyboard typing an account letter for letter, and the smoke exiting 0. All on one Android 13 handheld against the dev shard over the LAN.
ADR-0017 still carries a "Written, not run" list from its first pass beside
its Accepted status; read the ADR's Validation section for what is actually
crossed off. Nothing has been run on a second device, on Android 12
specifically, or on a phone with one screen and no physical controls, so
treat those as written, not verified. CI publishes the .NET half of the
Android export on every push (.github\workflows\build.yml) but cannot run
the engine or export an APK, and says so in the workflow.
The touch layer and the per-platform profile that make a phone playable are on the Mobile UI page.