ml 4e47e0a989
build / build (push) Successful in 2m11s
Fix missing-assets detection race, 16 KB page alignment, and Android audio backend
- QuestShockActivity now actually blocks the native engine from starting
  when game data is missing, closing three gaps found via on-device
  testing: super.onCreate() must run unconditionally first (Android
  throws SuperNotCalledException otherwise); SDLActivity.mBrokenLibraries
  is now set provisionally before the storage-permission check, since
  onWindowFocusChanged() closing the permission dialog could otherwise
  start the engine before the async onRequestPermissionsResult() callback
  ran; and a new GameSurface (SDLSurface subclass) closes the actual gap
  that let the init_popups NULL-deref crash through even with
  mBrokenLibraries set - SDLSurface.surfaceChanged() starts the native
  thread directly without ever checking that flag.
- Force Android to use SDL2's openslES audio backend instead of AAudio
  (android/engine-patches/05-android-audio-driver.patch): AAudio only
  allows one open playback device at a time, but the engine opens two
  (cutscene audio via SDL_OpenAudioDevice, SFX/MIDI via Mix_OpenAudio),
  hitting an assertion failure on real hardware.
- Add 16 KB ELF page-size alignment (-Wl,-z,max-page-size=16384) to every
  Android shared library - the four prebuilts (SDL2, SDL2_mixer,
  fluidsynth-lite, gl4es, in build-image/Dockerfile) and the engine's own
  libmain.so (build.gradle) - matching Google's Play Store requirement
  for Android 15+ and clearing Android Studio's compatibility warning.
- Add a stageEngine Gradle task that automatically re-stages the patched
  engine/ copy and prebuilt libraries before any Android Studio build
  (hooked into preBuild, with proper up-to-date checking), so source/
  patch changes can't silently go stale in the build/android-engine
  scratch copy - previously a manual, easy-to-forget step. Skips
  automatically inside the build-image container so make apk/CI are
  unaffected.
2026-07-23 19:20:22 +02:00

Questshock

questshock

An open source project to play the classic 1994 System Shock on a VR headset, built on top of Shockolate, a cross-platform port of the original game.

Layout

  • engine/ - a vendored snapshot of the Shockolate engine source. Built via Docker; see below.
  • build-image/ - the Dockerfile (and supporting scripts) for the engine build environment. Every third-party dependency the engine needs to compile (SDL2, SDL2_mixer, the fluidsynth-lite MIDI synth, a MIDI soundfont) is fetched and built once into this image - compiling engine/ itself needs no network access.
  • build-image.sh / run-image.sh / upload-image.sh - build the image, run it to compile the engine, and push it to a registry, respectively.
  • res/assets/ - where you place your own purchased copy of the game (see below); res/assets/extract_assets.sh extracts it into ss_ee/.
  • res/run.sh - the launcher script, copied into dist/ on build.
  • Makefile - assembles dist/, a self-contained runnable copy of the game, out of the compiled engine and the extracted assets. Also builds dist/shockolate-<version>-linux-<arch>.tar.gz, a redistributable package that omits the proprietary game assets (make package), and dist/questshock-<version>-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
    • applied to a scratch copy at build time; engine/ itself is never modified.

Building

# 1. Build the engine build-image (once, or after build-image/ changes)
./build-image.sh

# 2. Get your own copy of the game data (see "Game assets" below), then:
res/assets/extract_assets.sh

# 3. Compile the engine and assemble dist/
make dist

# 4. Play
dist/run.sh

make dist always recompiles the engine from the current engine/ source (via run-image.sh), so a fresh build-image plus a re-run of make dist is all that's needed after pulling engine changes.

Game assets

System Shock's game data is not included in this repository and cannot be redistributed - you need to own a copy. Buy System Shock: Enhanced Edition on gog.com, download the offline installer (a .exe), and drop it into res/assets/. Then run res/assets/extract_assets.sh, which pulls the classic game's data and sound files out of the installer (it's an Inno Setup package; the actual game data lives inside it in a zip-format sshock.kpf) into res/assets/ss_ee/. That script needs innoextract and unzip; if 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-<version>-linux-<arch>.tar.gz: the 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+<sha> 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 extra setup is needed in CI beyond the image itself), and publishes the resulting tarball to dl.ladkau.de.

Playing on Meta Quest

make apk builds dist/questshock-<version>-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 (or adb install).
  2. Launch it once. It'll ask for storage permission, then create /sdcard/questshock/ and extract its own bundled files (shaders, a default MIDI soundfont) there - res/data/ and res/sound/ are deliberately left missing, since that's the proprietary game data.
  3. With the Quest connected to a PC, use SideQuest's file browser (or any MTP file manager) to copy your own res/data/ and res/sound/ (see /sdcard/questshock/GET_ASSETS_QUEST.txt, extracted in step 2, for exactly what's needed and where it comes from) into /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:

./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 scripts, Makefile, asset extraction script, and the Quest app in android/ - aside from org/libsdl/app/, see below) is licensed under the MIT License.

android/app/src/main/java/org/libsdl/app/ is copied from SDL2's own android-project template and is zlib-licensed, same as SDL2 itself.

The Quest build also bundles GL4ES (lib/arm64-v8a/libGL.so in the APK, prebuilt unmodified into the build image), which translates the engine's desktop-style OpenGL calls into GLES/EGL and is MIT-licensed.

The vendored engine snapshot in engine/ is Shockolate, which is licensed under the GNU GPLv3 (see engine/LICENSE) - it is included unchanged and is not relicensed by this project's MIT license. Any build or distribution of the compiled engine must comply with the GPLv3.

The game assets extracted into res/assets/ss_ee/ are proprietary, copyrighted game data owned by their respective rightsholders - they are never committed to this repository (see .gitignore) and must be supplied by each user from their own legitimate purchase.

S
Description
An open source project to play the classic 1994 System Shock on a VR headset, built on top of Shockolate, a cross-platform port of the original game.
Readme MIT 67 MiB
Languages
C 85.2%
C++ 11.3%
Java 2.2%
Shell 0.4%
CMake 0.4%
Other 0.3%