From ac640762d7d058975bb590106afd5c2fd8c57c22 Mon Sep 17 00:00:00 2001 From: ml Date: Thu, 23 Jul 2026 06:42:45 +0200 Subject: [PATCH] Fix Quest launch crashes, version the APK like the tarball, and support native builds in Android Studio - patchelf gl4es's embedded SONAME to libGL.so so AGP's jniLibs packaging (which drops any file not literally named "*.so") and the dynamic linker's NEEDED-entry resolution (by embedded SONAME, not filename) finally agree - fixes the "library \"libGL.so.1\" not found" crash seen on real Quest hardware. - QuestShockActivity now checks that res/data and res/sound exist before starting the native engine, showing an explanatory dialog instead of crashing on init_popups' unchecked NULL resource load when a fresh install has no game data copied in yet. - dist/questshock--android-arm64.apk is now versioned from the same git-tag-or-dev-placeholder scheme as the desktop tarball (build-image/version.sh, shared by both via the Makefile and build-apk.sh). - build-image/prepare-android-project.sh (prep logic extracted out of build-apk.sh) can now stage android/ for a native build directly in Android Studio (--host-paths), exporting the prebuilt SDL2/SDL2_mixer/ fluidsynth-lite/gl4es libraries and writing host-resolvable paths, instead of only ever building inside the Docker image. - Corrected GET_ASSETS.txt/GET_ASSETS_QUEST.txt, which wrongly described merging res/pc/hd and res/pc/cdrom trees out of the raw installer's sshock.kpf - an already-installed copy's res/data res/sound can just be copied directly, with extract_assets.sh only needed from the raw installer. --- .gitea/workflows/build.yml | 5 +- .gitignore | 3 + Makefile | 14 +-- README.md | 50 ++++++++-- android/app/build.gradle | 40 +++++--- .../ladkau/questshock/QuestShockActivity.java | 47 ++++++++++ .../engine-patches/02-android-opengl-es.patch | 4 +- build-image/Dockerfile | 21 ++++- build-image/build-apk.sh | 62 ++++--------- build-image/prepare-android-project.sh | 91 +++++++++++++++++++ build-image/version.sh | 28 ++++++ res/assets/GET_ASSETS.txt | 18 ++-- res/assets/GET_ASSETS_QUEST.txt | 15 ++- run-image.sh | 6 ++ 14 files changed, 304 insertions(+), 100 deletions(-) create mode 100755 build-image/prepare-android-project.sh create mode 100755 build-image/version.sh diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 99f1258..5489463 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -71,7 +71,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: questshock-quest-apk - path: dist/questshock-debug.apk + path: dist/questshock-*-android-*.apk - name: Publish to dl.ladkau.de # Uploads the tarball and APK over SFTP instead of using @@ -81,6 +81,7 @@ jobs: run: | set -euo pipefail TARBALL="$(ls dist/shockolate-*-linux-*.tar.gz)" + APK="$(ls dist/questshock-*-android-*.apk)" mkdir -p ~/.ssh echo "${{ secrets.DL_SFTP_KEY }}" > ~/.ssh/dl_sftp_key chmod 600 ~/.ssh/dl_sftp_key @@ -89,5 +90,5 @@ jobs: uploader@dl.ladkau.de </dev/null || true - @V="$(VERSION)"; \ - if [ -z "$$V" ]; then \ - if TAG=$$(git describe --tags --exact-match --match 'v[0-9]*.[0-9]*.[0-9]*' 2>/dev/null); then \ - V=$${TAG#v}; \ - else \ - V="0.0.0-dev+$$(git rev-parse --short HEAD)"; \ - echo "WARNING: HEAD is not on a vX.Y.Z tag - building placeholder version $$V (push a tag to drive a real release version)" >&2; \ - fi; \ - fi; \ - case "$$V" in \ - [0-9]*.[0-9]*.[0-9]*) ;; \ - *) echo "PREFLIGHT FAIL: VERSION '$$V' is not a semantic version (expected X.Y.Z, optionally with a -pre+meta suffix)" >&2; exit 1;; \ - esac; \ + @V="$$(VERSION="$(VERSION)" ./build-image/version.sh)"; \ PKG_NAME="shockolate-$$V-linux-$(ARCH)"; \ PKG_STAGE="$(BUILD_DIR)/package/$$PKG_NAME"; \ echo "Packaging $$PKG_NAME"; \ diff --git a/README.md b/README.md index 88defc7..b8dcc39 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ a cross-platform port of the original game. game, out of the compiled engine and the extracted assets. Also builds `dist/shockolate--linux-.tar.gz`, a redistributable package that omits the proprietary game assets (`make package`), and - `dist/questshock-debug.apk` for the Quest (`make apk`). + `dist/questshock--android-arm64.apk` for the Quest (`make apk`). - `android/` - the Quest app (Java `SDLActivity` glue, Gradle project). `android/engine-patches/` holds the one small patch needed to build `engine/` as an Android shared library instead of a desktop executable @@ -64,6 +64,13 @@ game data lives inside it in a zip-format `sshock.kpf`) into they aren't installed locally it falls back to running the extraction in a throwaway Docker container instead. +If you already have the game installed instead (on Windows, or via +Wine/Proton on Linux), you don't need the installer or the script at +all - just copy its `res/data/` and `res/sound/` folders directly into +`res/assets/ss_ee/data/` and `res/assets/ss_ee/sound/`. That's exactly +the same layout `extract_assets.sh` produces, so `make dist`/`make +package`/`make apk` pick it up the same way either way. + ## Packaging `make package` builds `dist/shockolate--linux-.tar.gz`: the @@ -71,7 +78,8 @@ compiled binary, its runtime libraries, shaders, a default MIDI soundfont, license information, and `res/GET_ASSETS.txt` in place of the actual game data (which the tarball never includes). Version comes from the current git tag (push a `vX.Y.Z` tag to drive a release); without one -it builds an untagged `0.0.0-dev+` placeholder. +it builds an untagged `0.0.0-dev+` placeholder. `make apk` (below) +is versioned identically, via the same `build-image/version.sh`. A Gitea Actions workflow (`.gitea/workflows/build.yml`) builds this package on every push, using the build-image as its container (so no @@ -80,11 +88,11 @@ resulting tarball to dl.ladkau.de. ## Playing on Meta Quest -`make apk` builds `dist/questshock-debug.apk` - a plain (non-VR) Android -app that runs as a flat, floating panel in the Quest's Home environment, -same as any other sideloaded Android app. It's not a head-tracked 6DoF VR -port (that's a much larger, separate undertaking); play with a Bluetooth -mouse/keyboard connected to the headset. +`make apk` builds `dist/questshock--android-arm64.apk` - a plain +(non-VR) Android app that runs as a flat, floating panel in the Quest's +Home environment, same as any other sideloaded Android app. It's not a +head-tracked 6DoF VR port (that's a much larger, separate undertaking); +play with a Bluetooth mouse/keyboard connected to the headset. 1. Install the APK with [SideQuest](https://sidequestvr.com/) (or `adb install`). @@ -99,6 +107,34 @@ mouse/keyboard connected to the headset. `/sdcard/questshock/res/`. 4. Launch it again. +### Building natively in Android Studio + +`make apk` always compiles the engine and links the APK inside the +Docker build-image - convenient for CI/CLI builds, but Android Studio +can't attach a debugger to (or get IDE code-intelligence for) a build +that happens inside a container it isn't running. + +To have Android Studio compile and deploy `android/` itself instead: + +```sh +./run-image.sh bash build-image/prepare-android-project.sh --host-paths +``` + +This stages everything `make apk` normally stages (a scratch, patched +copy of `engine/`; the Android SDL2/SDL2_mixer/fluidsynth-lite/gl4es +prebuilts; bundled assets) - the same as `build-apk.sh`'s own prep step - +except it writes `android/engine.properties` with paths that resolve on +your host filesystem, and additionally exports the prebuilt libraries +(otherwise only present inside the image, at `/opt/prebuilt/android`) to +`build/android-prebuilt/` so they're visible outside the container too. +Re-run it whenever `engine/`, `android/engine-patches/`, or the +prebuilt-library versions in `build-image/Dockerfile` change. + +Then open `android/` as a project in Android Studio (JDK 17, NDK +`26.1.10909125`, and SDK Platform/Build-Tools 34 installed, matching +`build-image/Dockerfile` and `android/app/build.gradle`) and build/run +normally - no Docker involved for this part. + ## License The original tooling in this repository (the Docker build image, build diff --git a/android/app/build.gradle b/android/app/build.gradle index 1f49ac7..342b605 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,15 +1,31 @@ apply plugin: 'com.android.application' -// Path to the (patched, at build time - see build-apk.sh and -// ../engine-patches/) scratch copy of engine/, written by build-apk.sh -// since it's only known at build time, not something a checked-in -// build.gradle can hardcode. +// Paths written by build-image/prepare-android-project.sh (called by +// build-apk.sh, or standalone with --host-paths to prep for a native +// build in Android Studio - see README) since they're only known at +// prep/build time, not something a checked-in build.gradle can hardcode. +// engineDir is the scratch, patched copy of engine/; prebuiltDir is the +// Android SDL2/SDL2_mixer/fluidsynth-lite/gl4es prebuilts - both point +// into this container's own paths for build-apk.sh's own gradlew call, +// or host-resolvable paths (plus a host-side export of prebuiltDir, since +// /opt/prebuilt/android only exists in the image) when prepared with +// --host-paths for Android Studio. def engineProps = new Properties() file("${projectDir}/../engine.properties").withInputStream { engineProps.load(it) } def engineDir = engineProps.getProperty('engineDir') if (engineDir == null) { - throw new GradleException("android/engine.properties is missing 'engineDir' - run via build-apk.sh, not gradlew directly") + throw new GradleException("android/engine.properties is missing 'engineDir' - run via build-apk.sh or prepare-android-project.sh, not gradlew directly") } +def prebuiltDir = engineProps.getProperty('prebuiltDir') +if (prebuiltDir == null) { + throw new GradleException("android/engine.properties is missing 'prebuiltDir' - run via build-apk.sh or prepare-android-project.sh, not gradlew directly") +} + +// Set by build-apk.sh (see build-image/version.sh, shared with the +// desktop tarball's versioning) via -PquestshockVersionName/Code. Default +// here only covers a direct, unsupported `gradlew` invocation. +def questshockVersionName = project.hasProperty('questshockVersionName') ? project.property('questshockVersionName') : '0.0.0-dev' +def questshockVersionCode = project.hasProperty('questshockVersionCode') ? project.property('questshockVersionCode').toInteger() : 1 android { namespace "de.ladkau.questshock" @@ -31,8 +47,8 @@ android { // targets - not just whatever's the current API level today. minSdkVersion 24 targetSdkVersion 29 - versionCode 1 - versionName "1.0" + versionCode questshockVersionCode + versionName questshockVersionName externalNativeBuild { cmake { @@ -42,8 +58,9 @@ android { // config, unlike the desktop build's older bundled copy); // SDL2_mixer and FluidSynth via the same build_ext/ BUNDLED // convention the desktop build already uses (populated in the - // scratch engine copy by build-apk.sh from - // /opt/prebuilt/android/*). + // scratch engine copy by prepare-android-project.sh from + // prebuiltDir). ANDROID_PREBUILT_DIR is also read directly by + // android/engine-patches/02-android-opengl-es.patch, for gl4es. // The CMAKE_FIND_ROOT_PATH* overrides below are needed // because the NDK toolchain file restricts find_package/ // find_path/find_library to its own sysroot by default, @@ -51,8 +68,9 @@ android { // build-image/Dockerfile's SDL2_mixer build, which hit the // exact same thing). arguments "-DENABLE_SDL2=ON", "-DENABLE_SOUND=BUNDLED", "-DENABLE_FLUIDSYNTH=BUNDLED", \ - "-DCMAKE_PREFIX_PATH=/opt/prebuilt/android/sdl2", \ - "-DCMAKE_FIND_ROOT_PATH=/opt/prebuilt/android/sdl2", \ + "-DANDROID_PREBUILT_DIR=${prebuiltDir}", \ + "-DCMAKE_PREFIX_PATH=${prebuiltDir}/sdl2", \ + "-DCMAKE_FIND_ROOT_PATH=${prebuiltDir}/sdl2", \ "-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH", \ "-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH", \ "-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH", \ diff --git a/android/app/src/main/java/de/ladkau/questshock/QuestShockActivity.java b/android/app/src/main/java/de/ladkau/questshock/QuestShockActivity.java index e81cf2a..3f63714 100644 --- a/android/app/src/main/java/de/ladkau/questshock/QuestShockActivity.java +++ b/android/app/src/main/java/de/ladkau/questshock/QuestShockActivity.java @@ -1,6 +1,7 @@ package de.ladkau.questshock; import android.Manifest; +import android.app.AlertDialog; import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.os.Bundle; @@ -96,6 +97,18 @@ public class QuestShockActivity extends SDLActivity { copyAssetFile("GET_ASSETS_QUEST.txt", marker); } + // Shockolate's engine/ is never patched to check this itself (see the + // class doc above) - it just does plain fopen("res/data/...", ...) + // and, on a fresh install with no game data copied in yet, that + // fails deep inside startup (init_popups(), which doesn't NULL-check + // the load) as a hard native crash instead of a message. Catch the + // missing-data case here instead, before super.onCreate() ever + // starts Shockolate's native main(). + if (!isNonEmptyDir(new File(gameDir, "res/data")) || !isNonEmptyDir(new File(gameDir, "res/sound"))) { + showMissingAssetsDialog(); + return; + } + // chdir() is process-wide, not per-thread - already in effect for // every thread (including the one that will run Shockolate's own // SDL_main) by the time super.onCreate() below starts it. @@ -104,6 +117,40 @@ public class QuestShockActivity extends SDLActivity { super.onCreate(mSavedInstanceState); } + private static boolean isNonEmptyDir(File dir) { + String[] entries = dir.list(); + return entries != null && entries.length > 0; + } + + // Same single-button, non-cancelable pattern as SDLActivity's own + // "broken libraries" dialog (org/libsdl/app/SDLActivity.java) - there's + // no game to start without this data, so the only way forward is to + // close, copy the assets, and relaunch. Message text mirrors + // res/assets/GET_ASSETS_QUEST.txt (also extracted to GAME_DIR) so the + // user isn't sent hunting for a second file just to read it the first + // time - it points back there at the end in case they need it again. + private void showMissingAssetsDialog() { + new AlertDialog.Builder(this) + .setTitle("Game data missing") + .setMessage("This app does not include System Shock's game data - it's " + + "copyrighted, proprietary content that can't be redistributed. To " + + "play, you need to own a copy of System Shock: Enhanced Edition (e.g. " + + "from gog.com), installed (on Windows, or via Wine/Proton on Linux).\n\n" + + "Using SideQuest (or any MTP file browser) with your Quest connected, " + + "copy its res/data/ and res/sound/ folders directly into:\n" + + GAME_DIR + "/res/data/\n" + + GAME_DIR + "/res/sound/\n" + + "They already contain everything needed, in the right layout - no " + + "merging or extraction required.\n\n" + + "Then relaunch.\n\n" + + "(These instructions are also in GET_ASSETS_QUEST.txt in the " + + "questshock folder on this device, if you need to read them again.)") + .setCancelable(false) + .setPositiveButton("Exit", (dialog, which) -> finish()) + .create() + .show(); + } + private void copyAssetFile(String assetPath, File dest) { if (dest.exists()) { return; diff --git a/android/engine-patches/02-android-opengl-es.patch b/android/engine-patches/02-android-opengl-es.patch index 4a459dc..1826ea8 100644 --- a/android/engine-patches/02-android-opengl-es.patch +++ b/android/engine-patches/02-android-opengl-es.patch @@ -12,8 +12,8 @@ if(ENABLE_OPENGL) - find_package(OpenGL REQUIRED) + if(ANDROID) -+ set(OPENGL_INCLUDE_DIRS /opt/prebuilt/android/gl4es/include) -+ set(OPENGL_LIBRARIES /opt/prebuilt/android/gl4es/lib/libGL.so) ++ set(OPENGL_INCLUDE_DIRS ${ANDROID_PREBUILT_DIR}/gl4es/include) ++ set(OPENGL_LIBRARIES ${ANDROID_PREBUILT_DIR}/gl4es/lib/libGL.so) + else() + find_package(OpenGL REQUIRED) + endif() diff --git a/build-image/Dockerfile b/build-image/Dockerfile index 92ebf08..bb94634 100644 --- a/build-image/Dockerfile +++ b/build-image/Dockerfile @@ -77,12 +77,17 @@ ENV DEBIAN_FRONTEND=noninteractive # build cache for this layer - and vice versa, adding a package here only # ever costs a rebuild of the (slow) layers below it, not a re-download # of packages that didn't change. +# patchelf: fixes up GL4ES's built-in ELF SONAME below (see the gl4es +# build step) - the Android APK packager (AGP) refuses to bundle a +# jniLibs file whose name doesn't literally end in ".so", but the +# dynamic linker resolves NEEDED entries by embedded SONAME, not +# filename - patchelf lets both agree on "libGL.so". RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential cmake make git curl ca-certificates pkg-config gosu \ libgl1-mesa-dev libglx-dev libxext-dev libx11-dev libxrandr-dev \ libxi-dev libxfixes-dev libxss-dev libxinerama-dev libxcursor-dev \ libogg-dev libvorbis-dev libasound2-dev openssh-client \ - openjdk-17-jdk-headless unzip \ + openjdk-17-jdk-headless unzip patchelf \ && rm -rf /var/lib/apt/lists/* WORKDIR /opt/prebuilt @@ -231,8 +236,17 @@ RUN git clone https://github.com/EtherTyper/fluidsynth-lite.git fluidsynth-lite- # gl4es's own CMakeLists.txt writes its output straight to # ${CMAKE_SOURCE_DIR}/lib (the source tree, not the build dir) and gives -# the GL target a ".so.1" suffix - stage explicitly rather than -# `cmake --install` (which it doesn't support for this target anyway). +# the GL target a ".so.1" suffix, both in filename and in its embedded +# ELF SONAME - stage explicitly rather than `cmake --install` (which it +# doesn't support for this target anyway). Renaming the *file* to +# "libGL.so" isn't enough on its own: the dynamic linker resolves +# libmain.so's NEEDED entry by the *embedded* SONAME, not by whatever +# filename it was linked against, so a bare rename would link fine but +# fail to dlopen on-device. And keeping the real "libGL.so.1" filename +# isn't an option either - AGP's jniLibs packaging silently drops any +# native library whose filename doesn't literally end in ".so". So +# patchelf the embedded SONAME to "libGL.so" too, making the rename +# consistent both at link time and at runtime. RUN git clone --branch "v${ANDROID_GL4ES_VERSION}" --depth 1 \ https://github.com/ptitSeb/gl4es.git gl4es-android \ && rm -rf gl4es-android/.git \ @@ -243,6 +257,7 @@ RUN git clone --branch "v${ANDROID_GL4ES_VERSION}" --depth 1 \ && cmake --build build-gl4es-android -j"$(nproc)" \ && mkdir -p /opt/prebuilt/android/gl4es/lib /opt/prebuilt/android/gl4es/include \ && cp -a gl4es-android/lib/libGL.so.1 /opt/prebuilt/android/gl4es/lib/libGL.so \ + && patchelf --set-soname libGL.so /opt/prebuilt/android/gl4es/lib/libGL.so \ && cp -a gl4es-android/include/. /opt/prebuilt/android/gl4es/include/ \ && rm -rf gl4es-android build-gl4es-android diff --git a/build-image/build-apk.sh b/build-image/build-apk.sh index 817feeb..775fb48 100755 --- a/build-image/build-apk.sh +++ b/build-image/build-apk.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash -# Builds the Quest APK from a scratch, patched copy of engine/ (see -# android/engine-patches/ - engine/ itself is never modified) plus the -# Android SDL2/SDL2_mixer/fluidsynth-lite prebuilt into this image at -# /opt/prebuilt/android/*. Run via ../run-image.sh (or directly, if -# already inside this image - see the Makefile's `apk` target). +# Builds the Quest APK - see build-image/prepare-android-project.sh (which +# this calls) for how android/ gets a scratch, patched copy of engine/, +# the Android SDL2/SDL2_mixer/fluidsynth-lite/gl4es prebuilts, and bundled +# assets staged in. Run via ../run-image.sh (or directly, if already +# inside this image - see the Makefile's `apk` target). # # Unlike build-engine.sh, this step needs network access: Gradle/AGP's # own dependency resolution isn't prebuilt into the image (see @@ -12,53 +12,29 @@ set -euo pipefail REPO_ROOT="$(pwd)" ANDROID_DIR="$REPO_ROOT/android" -SCRATCH_ENGINE="$REPO_ROOT/build/android-engine" -echo "== Preparing a scratch, patched copy of engine/ ==" -rm -rf "$SCRATCH_ENGINE" -mkdir -p "$(dirname "$SCRATCH_ENGINE")" -cp -a "$REPO_ROOT/engine" "$SCRATCH_ENGINE" -for p in "$ANDROID_DIR"/engine-patches/*.patch; do - patch -p1 -d "$SCRATCH_ENGINE" < "$p" -done +bash "$REPO_ROOT/build-image/prepare-android-project.sh" -echo "== Wiring up prebuilt Android SDL2_mixer/fluidsynth-lite (BUNDLED mode, like the desktop build) ==" -mkdir -p "$SCRATCH_ENGINE/build_ext/built_sdl_mixer" "$SCRATCH_ENGINE/build_ext/fluidsynth-lite" -cp -a /opt/prebuilt/android/sdl2_mixer/. "$SCRATCH_ENGINE/build_ext/built_sdl_mixer/" -mkdir -p "$SCRATCH_ENGINE/build_ext/fluidsynth-lite/src" -cp -a /opt/prebuilt/android/fluidsynth-lite/lib/. "$SCRATCH_ENGINE/build_ext/fluidsynth-lite/src/" -cp -a /opt/prebuilt/android/fluidsynth-lite/include/. "$SCRATCH_ENGINE/build_ext/fluidsynth-lite/include/" - -echo "engineDir=$SCRATCH_ENGINE" > "$ANDROID_DIR/engine.properties" - -echo "== Staging bundled assets (shaders, soundfont, get-assets text) ==" -ASSETS_DIR="$ANDROID_DIR/app/src/main/assets" -rm -rf "$ASSETS_DIR" -mkdir -p "$ASSETS_DIR/shaders" "$ASSETS_DIR/res" -# GLES ports of engine/shaders/ (see android/gles-shaders/ and -# android/engine-patches/04-android-opengl-es-render.patch) - not -# engine/shaders/ itself, which is desktop-only GLSL. -cp -a "$ANDROID_DIR/gles-shaders/." "$ASSETS_DIR/shaders/" -cp "/opt/prebuilt/soundfont/default.sf2" "$ASSETS_DIR/res/soundfont.sf2" -cp "$REPO_ROOT/res/assets/GET_ASSETS_QUEST.txt" "$ASSETS_DIR/GET_ASSETS_QUEST.txt" - -echo "== Staging prebuilt Android SDL2/SDL2_mixer/fluidsynth-lite/gl4es .so's into jniLibs ==" -JNI_LIBS_DIR="$ANDROID_DIR/app/src/main/jniLibs/arm64-v8a" -rm -rf "$ANDROID_DIR/app/src/main/jniLibs" -mkdir -p "$JNI_LIBS_DIR" -find /opt/prebuilt/android/sdl2/lib /opt/prebuilt/android/sdl2_mixer/lib /opt/prebuilt/android/fluidsynth-lite/lib \ - /opt/prebuilt/android/gl4es/lib \ - -name '*.so' -exec cp -a {} "$JNI_LIBS_DIR/" \; +echo "== Determining version (shared with 'make package', see build-image/version.sh) ==" +git config --global --add safe.directory "$REPO_ROOT" 2>/dev/null || true +VERSION="$("$REPO_ROOT/build-image/version.sh")" +# versionName (VERSION, above) can be anything, but Android's versionCode +# must be a positive, monotonically-increasing integer - the commit count +# is a simple, deterministic stand-in for that. +VERSION_CODE="$(git -C "$REPO_ROOT" rev-list --count HEAD)" +echo "Version: $VERSION (versionCode $VERSION_CODE)" echo "== Building the APK (gradlew assembleDebug) ==" cd "$ANDROID_DIR" -./gradlew --no-daemon assembleDebug +./gradlew --no-daemon assembleDebug \ + "-PquestshockVersionName=$VERSION" "-PquestshockVersionCode=$VERSION_CODE" APK="$ANDROID_DIR/app/build/outputs/apk/debug/app-debug.apk" [ -f "$APK" ] || { echo "BUILD FAIL: $APK not found after assembleDebug" >&2; exit 1; } mkdir -p "$REPO_ROOT/dist" -cp "$APK" "$REPO_ROOT/dist/questshock-debug.apk" +PKG_NAME="questshock-$VERSION-android-arm64.apk" +cp "$APK" "$REPO_ROOT/dist/$PKG_NAME" echo "== Done ==" -echo "APK: $REPO_ROOT/dist/questshock-debug.apk" +echo "APK: $REPO_ROOT/dist/$PKG_NAME" diff --git a/build-image/prepare-android-project.sh b/build-image/prepare-android-project.sh new file mode 100755 index 0000000..55ece99 --- /dev/null +++ b/build-image/prepare-android-project.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Prepares android/ to be built: a scratch, patched copy of engine/ (see +# android/engine-patches/ - engine/ itself is never modified), the +# Android SDL2/SDL2_mixer/fluidsynth-lite/gl4es prebuilts staged into +# jniLibs, bundled assets, and android/engine.properties (engineDir/ +# prebuiltDir, read by android/app/build.gradle). Always run inside this +# image (via ../run-image.sh, or directly if already inside it) - it +# reads from /opt/prebuilt/android/*, which only exists there. +# +# Two modes: +# (no args) - for build-apk.sh's own gradlew, invoked in this same +# container - engine.properties points at this +# container's own paths (e.g. /workspace/build/...). +# --host-paths - for building android/ natively in Android Studio on +# the HOST afterwards, instead of via build-apk.sh. +# Needs HOST_REPO_ROOT (set by run-image.sh) to know +# this container's bind-mounted repo root's path on +# the host, since a plain container-internal path +# like /workspace/... doesn't resolve there. Also +# exports /opt/prebuilt/android (host-invisible, +# image-only) to build/android-prebuilt so a host-side +# CMake configure can actually find it. +set -euo pipefail + +HOST_PATHS=0 +if [ "${1:-}" = "--host-paths" ]; then + HOST_PATHS=1 +fi + +REPO_ROOT="$(pwd)" +ANDROID_DIR="$REPO_ROOT/android" +SCRATCH_ENGINE="$REPO_ROOT/build/android-engine" +PREBUILT_EXPORT_DIR="$REPO_ROOT/build/android-prebuilt" + +echo "== Preparing a scratch, patched copy of engine/ ==" +rm -rf "$SCRATCH_ENGINE" +mkdir -p "$(dirname "$SCRATCH_ENGINE")" +cp -a "$REPO_ROOT/engine" "$SCRATCH_ENGINE" +for p in "$ANDROID_DIR"/engine-patches/*.patch; do + patch -p1 -d "$SCRATCH_ENGINE" < "$p" +done + +echo "== Wiring up prebuilt Android SDL2_mixer/fluidsynth-lite (BUNDLED mode, like the desktop build) ==" +mkdir -p "$SCRATCH_ENGINE/build_ext/built_sdl_mixer" "$SCRATCH_ENGINE/build_ext/fluidsynth-lite" +cp -a /opt/prebuilt/android/sdl2_mixer/. "$SCRATCH_ENGINE/build_ext/built_sdl_mixer/" +mkdir -p "$SCRATCH_ENGINE/build_ext/fluidsynth-lite/src" +cp -a /opt/prebuilt/android/fluidsynth-lite/lib/. "$SCRATCH_ENGINE/build_ext/fluidsynth-lite/src/" +cp -a /opt/prebuilt/android/fluidsynth-lite/include/. "$SCRATCH_ENGINE/build_ext/fluidsynth-lite/include/" + +if [ "$HOST_PATHS" -eq 1 ]; then + : "${HOST_REPO_ROOT:?--host-paths needs HOST_REPO_ROOT set - run via ./run-image.sh, not directly}" + echo "== Exporting prebuilt Android SDL2/SDL2_mixer/fluidsynth-lite/gl4es for host-side use ==" + rm -rf "$PREBUILT_EXPORT_DIR" + cp -a /opt/prebuilt/android "$PREBUILT_EXPORT_DIR" + ENGINE_DIR_PROP="$HOST_REPO_ROOT/build/android-engine" + PREBUILT_DIR_PROP="$HOST_REPO_ROOT/build/android-prebuilt" +else + ENGINE_DIR_PROP="$SCRATCH_ENGINE" + PREBUILT_DIR_PROP="/opt/prebuilt/android" +fi + +{ + echo "engineDir=$ENGINE_DIR_PROP" + echo "prebuiltDir=$PREBUILT_DIR_PROP" +} > "$ANDROID_DIR/engine.properties" + +echo "== Staging bundled assets (shaders, soundfont, get-assets text) ==" +ASSETS_DIR="$ANDROID_DIR/app/src/main/assets" +rm -rf "$ASSETS_DIR" +mkdir -p "$ASSETS_DIR/shaders" "$ASSETS_DIR/res" +# GLES ports of engine/shaders/ (see android/gles-shaders/ and +# android/engine-patches/04-android-opengl-es-render.patch) - not +# engine/shaders/ itself, which is desktop-only GLSL. +cp -a "$ANDROID_DIR/gles-shaders/." "$ASSETS_DIR/shaders/" +cp "/opt/prebuilt/soundfont/default.sf2" "$ASSETS_DIR/res/soundfont.sf2" +cp "$REPO_ROOT/res/assets/GET_ASSETS_QUEST.txt" "$ASSETS_DIR/GET_ASSETS_QUEST.txt" + +echo "== Staging prebuilt Android SDL2/SDL2_mixer/fluidsynth-lite/gl4es .so's into jniLibs ==" +JNI_LIBS_DIR="$ANDROID_DIR/app/src/main/jniLibs/arm64-v8a" +rm -rf "$ANDROID_DIR/app/src/main/jniLibs" +mkdir -p "$JNI_LIBS_DIR" +find /opt/prebuilt/android/sdl2/lib /opt/prebuilt/android/sdl2_mixer/lib /opt/prebuilt/android/fluidsynth-lite/lib \ + /opt/prebuilt/android/gl4es/lib \ + -name '*.so' -exec cp -a {} "$JNI_LIBS_DIR/" \; + +echo "== Done ==" +echo "engineDir=$ENGINE_DIR_PROP" +echo "prebuiltDir=$PREBUILT_DIR_PROP" +if [ "$HOST_PATHS" -eq 1 ]; then + echo "android/ is ready to open and build natively in Android Studio on the host." +fi diff --git a/build-image/version.sh b/build-image/version.sh new file mode 100755 index 0000000..e67b972 --- /dev/null +++ b/build-image/version.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Prints the release version used to name both dist/shockolate-*.tar.gz +# (see `make package`) and dist/questshock-*.apk (see build-apk.sh), so +# the two are always versioned identically. +# +# Defaults to the current git tag (vX.Y.Z, prefix stripped) if HEAD is +# exactly on one matching that pattern; otherwise a 0.0.0-dev+ +# placeholder (with a warning on stderr) - push a vX.Y.Z tag to drive a +# real release version. Override either way with VERSION=1.2.3 in the +# environment. +set -euo pipefail + +V="${VERSION:-}" +if [ -z "$V" ]; then + if TAG=$(git describe --tags --exact-match --match 'v[0-9]*.[0-9]*.[0-9]*' 2>/dev/null); then + V="${TAG#v}" + else + V="0.0.0-dev+$(git rev-parse --short HEAD)" + echo "WARNING: HEAD is not on a vX.Y.Z tag - building placeholder version $V (push a tag to drive a real release version)" >&2 + fi +fi + +case "$V" in + [0-9]*.[0-9]*.[0-9]*) ;; + *) echo "PREFLIGHT FAIL: VERSION '$V' is not a semantic version (expected X.Y.Z, optionally with a -pre+meta suffix)" >&2; exit 1;; +esac + +echo "$V" diff --git a/res/assets/GET_ASSETS.txt b/res/assets/GET_ASSETS.txt index 1b9e162..41260dd 100644 --- a/res/assets/GET_ASSETS.txt +++ b/res/assets/GET_ASSETS.txt @@ -1,16 +1,14 @@ This package does not include System Shock's game data - it's copyrighted, proprietary content that can't be redistributed. To play, -you need a copy of System Shock: Enhanced Edition (e.g. from gog.com). +you need to own a copy of System Shock: Enhanced Edition (e.g. from +gog.com), installed (on Windows, or via Wine/Proton on Linux). -From your Enhanced Edition install, you need its classic-game data and -sound files - the res/pc/hd/data and res/pc/cdrom/data trees merged -together (res/pc/hd's copies win the couple of filenames present in -both: intro.res, objprop.dat), and the res/pc/hd/sound tree, packed -inside the install's sshock.kpf (a zip file). - -Copy that merged data into place, alongside this file: - res/data/ <- res/pc/hd/data + res/pc/cdrom/data, merged - res/sound/ <- res/pc/hd/sound +Copy its res/data/ and res/sound/ folders directly into place here, +alongside this file: + res/data/ + res/sound/ +They already contain everything needed, in the right layout - no +merging or extraction required. Once res/data/ and res/sound/ exist next to this file, run ./run.sh from the root of this package to play. diff --git a/res/assets/GET_ASSETS_QUEST.txt b/res/assets/GET_ASSETS_QUEST.txt index ed9750a..bd49f8e 100644 --- a/res/assets/GET_ASSETS_QUEST.txt +++ b/res/assets/GET_ASSETS_QUEST.txt @@ -1,17 +1,14 @@ This app does not include System Shock's game data - it's copyrighted, -proprietary content that can't be redistributed. To play, you need a -copy of System Shock: Enhanced Edition (e.g. from gog.com). - -From your Enhanced Edition install, you need its classic-game data and -sound files - the res/pc/hd/data and res/pc/cdrom/data trees merged -together (res/pc/hd's copies win the couple of filenames present in -both: intro.res, objprop.dat), and the res/pc/hd/sound tree, packed -inside the install's sshock.kpf (a zip file). +proprietary content that can't be redistributed. To play, you need to +own a copy of System Shock: Enhanced Edition (e.g. from gog.com), +installed (on Windows, or via Wine/Proton on Linux). Using SideQuest (or any MTP file browser) with your Quest connected, -copy that merged data into: +copy its res/data/ and res/sound/ folders directly into: /sdcard/questshock/res/data/ /sdcard/questshock/res/sound/ +They already contain everything needed, in the right layout - no +merging or extraction required. (shaders/ and this app's default soundfont are already here, extracted automatically on first launch - only res/data/ and res/sound/ are diff --git a/run-image.sh b/run-image.sh index dbc7e19..acf6411 100755 --- a/run-image.sh +++ b/run-image.sh @@ -39,11 +39,17 @@ docker image inspect "$IMAGE" >/dev/null 2>&1 \ TTY_FLAGS="-i" [ -t 1 ] && TTY_FLAGS="-it" +# HOST_REPO_ROOT: lets scripts running in the container (e.g. +# build-image/prepare-android-project.sh --host-paths) write paths that +# resolve on the HOST filesystem instead of this container's own +# /workspace - needed so a gradlew/Android Studio running natively on the +# host afterwards can find them. # shellcheck disable=SC2086 docker run --rm $TTY_FLAGS \ -v "$ROOT:/workspace" \ -e HOST_UID="$(id -u)" \ -e HOST_GID="$(id -g)" \ + -e HOST_REPO_ROOT="$ROOT" \ "$IMAGE" \ "$@"