An APK missing any asset GameAssets.kt opens by name installs and launches
fine, then fails on the device during staging — which is how two of them
reached test builds already. Fail the job instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GenerateBuilds runs on every push and builds six platforms, which is a long
wait when all you want is an APK to sideload. This builds ghostship.o2r and a
debug APK on its own, on demand or when something the APK contains changes.
Not for the upstream PR either — GenerateBuilds already has a build-android
job. This exists to shorten the device-test loop.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Not for the upstream PR. gfx_check_image_signature() rejects Android's tagged
heap pointers, so without this the skybox, title background and HUD render as
noise. Drop this commit once Kenix3/libultraship has the fix and the pin can
move back to an upstream commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An APK missing any asset GameAssets.kt opens by name installs and launches
fine, then fails on the device during staging — losing the game data to an
asset merge that had nothing to merge, or a source file to gitignore, both
left the build green. Fail the job instead of shipping it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mods/ is ignored repo-wide, which caught the placeholder the launcher copies
into the player's mods directory. It survived locally as an untracked file and
never reached a clean checkout, so CI built an APK without it.
The consequence was out of proportion to a hint file: staging opens it after
unpacking gamedata.zip, the missing asset threw, and the whole of
stageBundledAssets reported failure without writing its stamp — so a build
that had extracted everything correctly still came up unusable, and re-ran the
unpack on every launch. Writing the placeholder is now allowed to fail on its
own.
The gitignore negation follows the one already there for /src/port/mods/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Torch recipes and ghostship.o2r reached the APK as a source-set srcDir
pointing at a directory two tasks filled in. AGP takes the path from that but
not the dependency on the tasks producing it, so packGameData and
stampGameData were absent from the assembleDebug task graph entirely. A clean
checkout merged assets against an empty directory and shipped an APK holding
only gamecontrollerdb.txt and mods/, and the build stayed green because the
missing-archive check never ran to say otherwise. It only worked locally
because an earlier build had left the directory populated.
The two tasks become one PackGameData registered per variant and attached with
addGeneratedSourceDirectory, which is the wiring AGP does follow. It writes
gamedata.zip and the digest beside it into an AGP-owned assets directory,
sorted and at a fixed timestamp so an unchanged zip keeps its digest and the
device does not re-unpack on every update.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The player no longer needs a PC. LauncherActivity unpacks the bundled data,
takes a ROM through the document picker, checks it against the supported
SHA-1s, and runs Torch through JNI to produce sm64.o2r — about 27s on a
Pixel 10a. The archive is byte-identical to a host extraction.
GameExtractor gains a GenerateOTRTo() overload taking the two directories
explicitly: the launcher runs before SDL exists, so Ship::Context cannot
resolve a path yet.
The launcher runs in its own process and ends it once the archive is written.
Torch's Companion is deliberately never freed — every platform leaks it, and
its destructor does not survive being run — so ending the process is what
returns that memory before the game starts.
Unpacking the packaged data moves from the engine to the launcher, because
Torch needs config.yml and assets/ymls on disk before the game process starts,
not after. It is the same gamedata.zip from the same Gradle tasks, so the
packaging and the CI artifact are unchanged; AndroidUtils.cpp goes away.
Also adds mods and saves managers behind a button that appears while the
engine menu is open, and fixes edge-to-edge: with targetSdk 36 the system
ignores the old fullscreen flags, so the navigation bar was sitting on top of
the game.
Toolchain moves to NDK 30.0.15729638, SDK 36, Gradle 8.13, AGP 8.13.2 and the
Kotlin DSL, and builds arm64-v8a and armeabi-v7a. CMAKE_BUILD_TYPE=Release is
forced for every variant: the Android plugin builds the release variant as
RelWithDebInfo (-O2), but the root CMakeLists pins Release to -O1
-fno-strict-aliasing because the game's C code is miscompiled above that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An entry whose CRC resolved to nothing was silently replaced with a null
pointer, so a missing asset surfaced only as corrupted rendering much later.
A zero CRC is a legitimate gap — main_hud_lut is indexed by character code and
is mostly empty — so only a non-zero CRC that resolves to nothing is reported.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Torch emits ALIGN_ASSET(2) for individual texture declarations but not for
the table and ptrlist declarations, leaving 270 of them with alignment 1:
static const ALIGN_ASSET(2) char water_skybox_texture_00000[] = "__OTR__…/water.0";
static const char water_skybox_ptrlist[] = "__OTR__…/water_skybox_ptrlist";
The macro is there because gfx_check_image_signature() rejects odd addresses
before it looks for the marker. In an arm64 build 81 of the 270 landed on odd
addresses, where the path string is used as raw texture data instead of being
resolved. Which ones land badly depends only on how the linker packs .rodata,
so it varies by toolchain.
This fixes the generated headers; Torch's codegen needs the same change or
regenerating them reintroduces it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
savesPath and randomizerFolderPath call GetPathRelativeToAppDirectory() from
a global constructor. On Android the app directory comes from
SDL_AndroidGetExternalStoragePath(), which calls into Java — and a global
constructor runs at dlopen, before SDL's JNI is set up, so the process aborts
before main() with "CallStaticObjectMethod received NULL jclass".
Making them function-local statics defers the lookup to first use. It also
resolves an ODR violation: randomizerFolderPath was a non-static global
defined in two translation units.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
add_subdirectory() was given the submodule's own source directory as its
binary directory, so libImGui.a, libstb.a and libmonocypher.a were written
into the libultraship checkout.
With a single target that only pollutes the working tree. With more than one
it breaks the build: Android configures once per ABI, both write the same
archives, and whichever ran last wins — the other then fails to link with
"is incompatible with aarch64linux".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>