From 66f02a0c612313d19baab6ac0da186240c865e57 Mon Sep 17 00:00:00 2001 From: ml Date: Mon, 20 Jul 2026 12:52:34 +0200 Subject: [PATCH] The Android/Quest CMakeLists.txt patch previously reimplemented desktop immediate-mode GL (glBegin/glVertex3f/glEnd and friends) from scratch on top of GLES, since GLES has none of it. That code compiled but was never actually exercised - no on-screen verification was possible without real Quest hardware - making it the biggest unverified risk in the port. Replace it with GL4ES (MIT-licensed, prebuilt into the build image), a mature desktop-GL-on-GLES translation library used by other Quest/Android game ports for exactly this problem. This cuts 04-android-opengl-es-render.patch by half, deleting the hand-written vertex-accumulation emulation entirely; alpha test, point sprites, and point size stay on their existing GLES-native fixes rather than trusting GL4ES's shakier custom-shader interop for those. Bump build-image/VERSION to 3 - a fresh image with GL4ES cross-compiled for arm64-v8a was built and make apk verified successfully against it. --- ...ject-is-to-play-classic-system-shock-o.txt | 3131 ----------------- NOTICE.txt | 3 + README.md | 5 + .../engine-patches/02-android-opengl-es.patch | 10 +- .../04-android-opengl-es-render.patch | 158 +- build-image/Dockerfile | 21 + build-image/VERSION | 2 +- build-image/build-apk.sh | 3 +- 8 files changed, 75 insertions(+), 3258 deletions(-) delete mode 100644 2026-07-19-212644-ok-this-project-is-to-play-classic-system-shock-o.txt diff --git a/2026-07-19-212644-ok-this-project-is-to-play-classic-system-shock-o.txt b/2026-07-19-212644-ok-this-project-is-to-play-classic-system-shock-o.txt deleted file mode 100644 index 5a12dab..0000000 --- a/2026-07-19-212644-ok-this-project-is-to-play-classic-system-shock-o.txt +++ /dev/null @@ -1,3131 +0,0 @@ -╭─── Claude Code v2.1.215 ─────────────────────────────────────────────────────╮ -│ │ Tips for getting │ -│ Welcome back Matthias! │ started │ -│ │ Ask Claude to create a… │ -│ ▐▛███▜▌ │ ─────────────────────── │ -│ ▝▜█████▛▘ │ What's new │ -│ ▘▘ ▝▝ │ Claude no longer runs … │ -│ Sonnet 5 · Claude Pro · │ Fixed single-segment `… │ -│ matthias.ladkau@gmail.com's Organization │ Fixed a permission-che… │ -│ ~/krotik/questshock │ /release-notes for more │ -╰──────────────────────────────────────────────────────────────────────────────╯ - - -❯ ok this project is to play classic system shock on a vr headset. the project - should be an open source project licensed under the MIT license. the base - engine should be Shockolate (https://github.com/Interrupt/systemshock) we - should store a snapshop of the engine in this repo. the building should be - done via a docker build image take inspiration from - /home/ml/krotik/deck_in_a_dash/ (please do not mention this project in the - documentation) with a build, run and upload image script. building via a - build image was already attempted here /home/ml/krotik/ss_port. It would be - good if the prerequisites for fluidsynth are already prebuild on the build - docker image i.e. ideally no download from the internet should be necessary - just the build docker image and the shockolate source code. the result of the - build image should be the system shock executable in the dist folder. the - game requires assets which should be copied from a bought version of system - shock. the installer from the enhanced edition bought from gog.com can be - found in /home/ml/krotik/questshock/res/assets/ it would be good to place a - script in the same folder which is able to extract the assets and place it in - a subfolder called ss_ee. the assets with a run script should be copied from - the res folder into the dist folder during build process (this could be done - via a Makefile or something similar if it would be more suitable). - -● I'll start by exploring the existing project structure, the previous build - attempt, and the reference Docker setup to understand what's already there - before designing this. - - Searched for 2 patterns, read 9 files, listed 1 directory, ran 1 shell command (ctrl+o to expand) - -● Now let me look at the previous ss_port build attempt in detail. - - Read 8 files, ran 6 shell commands (ctrl+o to expand) - -● Let me look at the full CMakeLists and check the actual upstream Shockolate - repo since we have internet access. - - Ran 10 shell commands (ctrl+o to expand) - -❯ source code and scripts should all go into the res folder - -● That interjection changes the layout significantly — let me confirm what you - mean before I structure the repo. - -● User answered Claude's questions: - ⎿  · You said "source code and scripts should all go into the res folder" — - how far does that extend? → Only the asset-extraction script (Recommended) - -❯ makefile or equivalent should be at the root level - -● Good, that confirms the layout I had planned — Makefile at root, only the - asset extraction script under res/assets/. Let me continue investigating the - GOG package format. - - Ran 13 shell commands (ctrl+o to expand) -● I now have a complete picture of how Shockolate builds and where its - dependencies come from. Let me set up task tracking and start building this - out. - -● API Error: 522 - {"type":"https://developers.cloudflare.com/support/troubleshooting/htt - p-status-codes/cloudflare-5xx-errors/error-522/","title":"Error 522: - Connection timed out","status":522,"detail":"Cloudflare could not - establish a TCP connection to the origin server. The TCP handshake - timed out, which may indicate the origin is overloaded, firewalling - Cloudflare, or unreachable at the network - level.","instance":"a1d967a29fc9d406","error_code":522,"error_name":"c - onnection_timeout","error_category":"origin","ray_id":"a1d967a29fc9d40 - 6","timestamp":"2026-07-19T11:32:41Z","zone":"api.anthropic.com","clou - dflare_error":true,"retryable":true,"retry_after":120,"owner_action_re - quired":true,"what_you_should_do":"**Wait and retry.** Back off for at - least 120 seconds. If the error persists, the website operator should - verify firewall rules and ensure the origin accepts connections from - Cloudflare IP ranges.","footer":"This error was generated by - Cloudflare on behalf of the website owner."}. This i… - (ctrl+o to expand) - -✻ Brewed for 11m 2s - -❯ are you still working? - -● Yes — still working through this. Quick status: I've finished investigating - (confirmed Shockolate's build system, the GOG installer's internal layout, and - how the previous ss_port attempt worked), and I'm now moving into actually - building the repo. Task 1 in progress: - - Ran 3 shell commands (ctrl+o to expand) - -● Good, clean snapshot in place, no .git remnants. Task 1 done — moving to the - Dockerfile. - - Ran 1 shell command (ctrl+o to expand) - -● Write(build-image/Dockerfile) ⎿  Wrote 91 lines to build-image/Dockerfile - 1 # Build environment for the Shockolate (System Shock) engine sna - 2 # engine/. Everything the engine build needs from the network - - 3 # SDL2_mixer, the fluidsynth-lite MIDI synth, and a General MIDI - soundfont - - with - 6 # - : the - 8 # in-container build script (build-image/build-engine.sh) copies - 9 # prebuilt pieces below straight into engine/build_ext/, matchin - 10 # engine/CMakeLists.txt's BUNDLED dependency mode expects. - … +111 lines (ctrl+o to expand) - -● Write(build-image/VERSION) - ⎿  Wrote 1 line to build-image/VERSION - 1 1 - -● Now the entrypoint and in-container build script (task 3). - -● Write(build-image/docker-entrypoint.sh) - ⎿  Wrote 14 lines to build-image/docker-entrypoint.sh - 1 #!/usr/bin/env bash - 2 # Drops from root (needed to read the prebuilt toolchain under /o - pt) to a - 3 # user matching the host's UID/GID, so anything written into the - 4 # bind-mounted repo (engine/build_ext, engine/.build-output, ...) - 5 # by the host user instead of root. - 6 set -euo pipefail - 7 - 8 USER_UID="${HOST_UID:-1000}" - 9 USER_GID="${HOST_GID:-1000}" - … +7 lines (ctrl+o to expand) - -● Write(build-image/build-engine.sh) - ⎿  Wrote 42 lines to build-image/build-engine.sh - 1 #!/usr/bin/env bash - e - 3 # dependencies prebuilt into this image at /opt/prebuilt - no ne - 4 # access needed. Run via ../run-image.sh, which mounts the repo - 5 # /workspace. - 6 # - the - 8 # shared libraries it needs at runtime (lib/), and a default MID - 9 # soundfont - everything the root Makefile needs to assemble dis - 10 set -euo pipefail - … +37 lines (ctrl+o to expand) - - Ran 1 shell command (ctrl+o to expand) - -● Write(build-image.sh) - ⎿  Wrote 34 lines to build-image.sh - 1 #!/usr/bin/env bash - 2 # Builds the engine build-image (build-image/Dockerfile) locally - , tagged - with - 4 # ./run-image.sh first, then publish with ./upload-image.sh. - 5 set -euo pipefail - 6 - 8 ROOT="$(pwd)" - 9 - 10 fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; } - … +26 lines (ctrl+o to expand) - -● Write(run-image.sh) - ⎿  Wrote 44 lines to run-image.sh - 1 #!/usr/bin/env bash - vendored - 3 # Shockolate snapshot), entirely offline - every dependency it n - 4 # (SDL2, SDL2_mixer, fluidsynth-lite, a MIDI soundfont) was alre - ady built - 6 # - 7 # Output lands in engine/.build-output/ - run `make dist` afterw - 8 # assemble it (together with the game assets from res/assets/ss_ - ee/) into - 10 # - … +40 lines (ctrl+o to expand) - -● Update(run-image.sh) - ⎿  Added 5 lines, removed 1 line - 36 docker image inspect "$IMAGE" >/dev/null 2>&1 \ - 37 || fail "$IMAGE not found locally - run ./build-image.sh fi - rst" - 38 - 39 -docker run --rm -it \ - 39 +TTY_FLAGS="-i" - 40 +[ -t 1 ] && TTY_FLAGS="-it" - 41 + - 42 +# shellcheck disable=SC2086 - 43 +docker run --rm $TTY_FLAGS \ - 44 -v "$ROOT:/workspace" \ - 45 -e HOST_UID="$(id -u)" \ - 46 -e HOST_GID="$(id -g)" \ - -● Now upload-image.sh and registry.env.example. - -● Write(upload-image.sh) - ⎿  Wrote 40 lines to upload-image.sh - 1 #!/usr/bin/env bash - 2 # Pushes the build-image - already built locally with ./build-im - 3 # and ideally verified with ./run-image.sh - to a container regi - stry, so - 4 # CI or other machines can reuse it without rebuilding the whole - 5 set -euo pipefail - 6 - 7 cd "$(dirname "${BASH_SOURCE[0]}")" - 9 - 10 fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; } - … +36 lines (ctrl+o to expand) - -● Write(registry.env.example) - ⎿  Wrote 9 lines to registry.env.example - 1 # Copy to registry.env (gitignored, never commit the real values) - and fill - .sh, and - 3 # upload-image.sh. Only REGISTRY_IMAGE is needed for local builds - /runs; - h with - 5 # upload-image.sh. - 6 REGISTRY= - 8 REGISTRY_USER= - 9 REGISTRY_PASSWORD= - … +3 lines (ctrl+o to expand) - - Ran 1 shell command (ctrl+o to expand) - - Ran 1 shell command (ctrl+o to expand) -● Write(res/assets/extract_assets.sh) - ⎿  Wrote 93 lines to res/assets/extract_assets.sh - 1 #!/usr/bin/env bash - tem Shock: - 3 # Enhanced Edition installer from gog.com, into ./ss_ee/{data,so - und}. - 5 # Usage: - 6 # 1. Buy System Shock: Enhanced Edition on gog.com and downloa - 7 # offline installer (a Windows .exe, e.g. - 8 # setup_system_shock_enhanced_edition_1.2.16_(64bit)_(44378 - ).exe). - 10 # 3. Run this script: ./extract_assets.sh - … +104 lines (ctrl+o to expand) - - Ran 1 shell command (ctrl+o to expand) - -● Task 5 done. Now the root Makefile and res/run.sh (task 6). - -● Write(res/run.sh) - ⎿  Wrote 7 lines to res/run.sh - 1 #!/usr/bin/env bash - 2 # Launches System Shock. Copied next to the systemshock binary in - dist/ - 3 # by the root Makefile. - 4 set -euo pipefail - 5 cd "$(dirname "${BASH_SOURCE[0]}")" - 6 export LD_LIBRARY_PATH="$(pwd)/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY - _PATH}" - 7 exec ./systemshock "$@" - -● Write(Makefile) - ⎿  Wrote 53 lines to Makefile - 1 # Assembles dist/ - a self-contained, runnable copy of System Sho - ck - - 3 # run-image.sh) and the game assets extracted from a purchased co - py (see - 4 # res/assets/extract_assets.sh). Building the engine needs the bu - 5 # (./build-image.sh, once); everything else here is plain file co - pying. - 6 - 8 ENGINE_OUT := engine/.build-output - 9 ASSETS_DIR := res/assets/ss_ee - … +53 lines (ctrl+o to expand) - - Ran 1 shell command (ctrl+o to expand) - -● Task 6 done. Now .gitignore and README.md (task 7). - - Read 1 file (ctrl+o to expand) - -● Write(.gitignore) - ⎿  Added 18 lines - 1 # Game assets are not included in the repo - 2 /res/assets/setup_system_shock_enhanced* - 3 +/res/assets/ss_ee/ - 4 + - 5 +# Build output - 6 +/dist/ - 7 + - 8 +# Engine build artifacts (engine/ is committed as a source snap - +shot; these - 9 +# are generated by run-image.sh / build-image/build-engine.sh) - 10 +/engine/build_ext/ - 11 +/engine/.build-output/ - 12 +/engine/CMakeCache.txt - 13 +/engine/CMakeFiles/ - 14 +/engine/cmake_install.cmake - 15 +/engine/Makefile - 16 +/engine/systemshock - 17 +/engine/src/Libraries/CMakeFiles/ - 18 + - 19 +# Local registry credentials (see registry.env.example) - 20 +/registry.env - -● Write(README.md) - ⎿  Wrote 72 lines to README.md - 1 # questshock - 2 - a VR - 4 headset, built on top of [Shockolate](https://github.com/Interru - pt/systemshock), - 6 - 7 ## Layout - 8 - e. Built - 10 via Docker; see below. - … +82 lines (ctrl+o to expand) - -● Now let's actually validate this end to end, starting with building the Docker - image (this will take a few minutes since it compiles - SDL2/SDL2_mixer/fluidsynth-lite). - - Ran 1 shell command (ctrl+o to expand) - -● The build image succeeded. Let me verify the soundfont actually downloaded - correctly (that URL is a bit sketchy), then run the actual engine build. - - Ran 1 shell command (ctrl+o to expand) - -● Good — soundfont downloaded properly (31MB, not an error page), and all three - prebuilt libraries look correct. Now let's compile the engine. - - Ran 1 shell command (ctrl+o to expand) - -● The engine compiled successfully offline, entirely from the prebuilt image. - Let's check ownership and the output, then run the asset extraction on the - real GOG installer. - - Ran 2 shell commands (ctrl+o to expand) -● All dynamic libraries resolve correctly with LD_LIBRARY_PATH set, no missing - libs. Now let's extract the real GOG assets and assemble dist/. - - Ran 1 shell command (ctrl+o to expand) - -● Found and need to fix a bash scoping bug: the trap ... EXIT set inside the - function fires when the shell process exits, by which point the local work - variable has already gone out of scope, causing an unbound-variable error - under set -u. Let me check what state things are in and fix it. - - Read 1 file, listed 1 directory (ctrl+o to expand) - -● Good — files ended up owned correctly (this Docker setup apparently remaps - root to the host user), so no real damage. But let's fix the trap bug properly - for portability, then verify the merge is correct against the known-good - reference. - -● Update(res/assets/extract_assets.sh) - ⎿  Added 6 lines, removed 2 lines - 42 run_extraction() { - 43 set -euo pipefail - 44 local installer="$1" out_dir="$2" - 45 - local work; work="$(mktemp -d)" - 46 - trap 'rm -rf "$work"' EXIT - 45 + # Deliberately not `local`: an EXIT trap set here fires whe - +n the whole - 46 + # shell process exits, which is after this function - and t - +his - 47 + # function's own scope - has already returned, so a `local` - + var would - 48 + # already be unset by then (unbound-variable error under `s - +et -u`). - 49 + work="$(mktemp -d)" - 50 + trap 'rm -rf "${work:-}"' EXIT - 51 - 52 echo "== Extracting sshock.kpf from installer ==" - 53 innoextract --silent --include sshock.kpf -d "$work/install - er" "$installer" - -● Now let's verify the extracted assets exactly match the known-good reference - layout from the earlier ss_port attempt. - - Ran 2 shell commands (ctrl+o to expand) - -● objprop.dat (the one file both variants should agree on if the reference - matches) confirms my "hd wins" merge precedence is correct. intro.res differs - from the old reference in all directions, which just means that reference was - assembled from a slightly different installer version — not a bug in my logic. - Now let's assemble dist/ and smoke-test the binary. - - Ran 1 shell command (ctrl+o to expand) - -● make dist succeeded end-to-end. Now let's smoke-test the actual binary - headlessly. - - Listed 1 directory, ran 1 shell command (ctrl+o to expand) - -● Found a real gap — the engine needs a shaders/ directory at runtime (relative - shaders/main.vert etc.) which I never copied into dist/. Let me check for - other runtime-relative paths I might have missed, then fix the Makefile. - - Ran 2 shell commands (ctrl+o to expand) - -● That's just a comment, not live code. Only shaders/ needs adding. Let me fix - the Makefile. - -● Update(Makefile) - ⎿  Added 1 line - 40 cp "$(ENGINE_OUT)/systemshock" "$(DIST_DIR)/" - 41 cp -a "$(ENGINE_OUT)/lib/." "$(DIST_DIR)/lib/" - 42 cp "$(ENGINE_OUT)/soundfont.sf2" "$(DIST_DIR)/res/" - 43 + cp -a engine/shaders "$(DIST_DIR)/shaders" - 44 cp -a "$(ASSETS_DIR)/data/." "$(DIST_DIR)/res/data/" - 45 cp -a "$(ASSETS_DIR)/sound/." "$(DIST_DIR)/res/sound/" - 46 cp res/run.sh "$(DIST_DIR)/run.sh" - - Ran 1 shell command (ctrl+o to expand) - -● Now let's re-run the headless smoke test to confirm OpenGL/shaders load - correctly this time. - - Ran 1 shell command (ctrl+o to expand) - -● OpenGL now initializes correctly ("OpenGL Resize 640 480") instead of falling - back to software mode, and it proceeds cleanly through splash screen setup. - The only errors are ALSA complaining about no sound card — expected in this - bare test container with no audio hardware, not a bug in dist/. This validates - the whole pipeline works end to end. - -● Let's do a final sanity check of the repo state before wrapping up. - - Ran 2 shell commands (ctrl+o to expand) - -● Clean — no leaks of the purchased installer, extracted assets, dist/, build - artifacts, or registry.env. Let me clean up the scratchpad and remove - ShockMac.sit size check just to confirm final repo size is sane. - Listed 1 directory (ctrl+o to expand) - -● One more improvement worth making: I pinned fluidsynth-lite to a floating - master ref in the Dockerfile, which undermines reproducibility. Let me fix - that to the actual commit we built against. - - Ran 1 shell command (ctrl+o to expand) - -● Update(build-image/Dockerfile) - ⎿  Added 1 line, removed 1 line - 14 ARG SDL2_MIXER_VERSION=2.0.4 - 15 # EtherTyper/fluidsynth-lite has no releases/tags; pin by commi - t so the image - 16 # is reproducible instead of silently picking up upstream chang - es. - 17 -ARG FLUIDSYNTH_LITE_REF=master - 17 +ARG FLUIDSYNTH_LITE_REF=c539a8d9270ba5a3f7d6e460606483fc2ab1eb6 - +1 - 18 # Soundfont used for MIDI music, matching what engine/build_dep - s.sh itself - 19 # fetches (a free substitute for the Windows default GM soundfo - nt). - 20 ARG SOUNDFONT_URL=http://rancid.kapsi.fi/windows.sf2 - - Ran 1 shell command (ctrl+o to expand) - -● Rebuilds cleanly with the pinned commit. Everything is validated end to end. - Here's a summary of what's in place: - - Searched for 1 pattern (ctrl+o to expand) - -● Everything is built and validated. Summary: - - Repo layout - - engine/ — a clean vendored snapshot of Shockolate (Interrupt/systemshock, - current HEAD), GPLv3 license preserved as-is. - - build-image/ — Dockerfile + entrypoint + in-container build script. At - image-build time it downloads and builds SDL2 2.0.9, SDL2_mixer 2.0.4, - fluidsynth-lite (pinned to a specific commit), and fetches a MIDI soundfont — - that's the only point where internet access is needed. - - build-image.sh / run-image.sh / upload-image.sh — build the image, compile - the engine offline against it, and push to a registry. - - res/assets/extract_assets.sh — pulls the game's data/sound files out of your - GOG installer (it's an Inno Setup package; the actual game data is a zip - called sshock.kpf inside it) into res/assets/ss_ee/. - - res/run.sh + root Makefile — assembles dist/ (binary, runtime libs, shaders, - soundfont, extracted assets, launcher script). - - What I actually verified, not just wrote: - - Built the Docker image for real (SDL2/SDL2_mixer/fluidsynth-lite/soundfont - all compiled/fetched successfully). - - Compiled the engine inside it with zero network access — clean full offline - build. - - Ran the real extraction script against your actual purchased installer in - res/assets/ and diffed the result against a known-good reference layout to - confirm the hd-wins merge logic is correct. - - Ran make dist and launched the actual binary headlessly (Xvfb) — it - initializes OpenGL, loads the real extracted game data, and reaches the splash - screen with no errors (the only warnings are ALSA complaining about no sound - card in that bare test container, not a real bug). - - Caught and fixed two real bugs along the way: a missing shaders/ copy into - dist/ (silently degraded to software rendering) and a bash scoping bug in the - extraction script's cleanup trap. - - One thing worth flagging clearly: Shockolate itself is GPLv3-licensed. I kept - your MIT LICENSE for the original tooling (Docker scripts, Makefile, - extraction script) but the vendored engine/ snapshot keeps its own GPLv3 - license unchanged — I did not and could not relicense it. This is called out - in the README's License section. VR support itself isn't implemented yet — - this was purely the build/asset infrastructure the task asked for. - -✻ Brewed for 29m 2s - -※ recap: Building the questshock repo to play System Shock in VR via Shockolate, with a Docker-based build pipeline for the engine and asset extraction. Everything's built, tested, and working end to end — next step is your review before anything gets committed. (disable recaps in /config) - -❯ cool works really well. the result of a build should also be a tar.gz file - which includes all the libraries, license information and a text file in the - res folder instead of the actual assets explaining where to get the assets. - all of this should be build with a gitea action and the tar.gz file should be - uploaded to dl.ladkau.de similar to deck-in-a-dash (again please don't - mention this project in the documentation). - - Read 1 file (ctrl+o to expand) - -● This needs a few real design decisions to make CI work cleanly (avoiding - docker-in-docker), plus a redistributable "package" target. Let me implement - it. - -● Let's start with fixing the build script's path handling and the Dockerfile - marker/openssh-client together. - - Read 1 file (ctrl+o to expand) - -● Update(build-image/build-engine.sh) - ⎿  Added 8 lines, removed 3 lines - 1 #!/usr/bin/env bash - 2 # Compiles engine/ (the vendored Shockolate snapshot) against t - he - 3 # dependencies prebuilt into this image at /opt/prebuilt - no n - etwork - 4 -# access needed. Run via ../run-image.sh, which mounts the repo - - at - 5 -# /workspace. - 4 +# access needed. Must be run with the repo root as the working - +directory - 5 +# - either via ../run-image.sh (which docker-runs this image wi - +th the - 6 +# repo bind-mounted at /workspace and WORKDIR set there), or di - +rectly - 7 +# when already inside this image (e.g. a CI job using this imag - +e as its - 8 +# container - see the Makefile's `engine` target, which picks w - +hichever - 9 +# of these applies). - 10 # - 11 # Output lands in engine/.build-output/: the systemshock binary - , the - 12 # shared libraries it needs at runtime (lib/), and a default MI - DI - 13 # soundfont - everything the root Makefile needs to assemble di - st/. - 14 set -euo pipefail - 15 - 12 -ENGINE_DIR="/workspace/engine" - 16 +REPO_ROOT="$(pwd)" - 17 +ENGINE_DIR="$REPO_ROOT/engine" - 18 OUT_DIR="$ENGINE_DIR/.build-output" - 19 - 20 cd "$ENGINE_DIR" - -● Now the Dockerfile: add the QUESTSHOCK_BUILD_IMAGE marker and openssh-client. - - Read 1 file (ctrl+o to expand) - -● Update(build-image/Dockerfile) - ⎿  Added 2 lines, removed 1 line - 35 # backend (X11 + GL). - 36 # libogg-dev/libvorbis-dev: SDL2_mixer's OGG Vorbis music decod - er. - 37 # libasound2-dev: engine/CMakeLists.txt's optional native ALSA - MIDI output. - 38 +# openssh-client: the CI workflow's `sftp` publish step. - 39 RUN apt-get update && apt-get install -y --no-install-recommend - s \ - 40 build-essential cmake make git curl ca-certificates pkg - -config gosu \ - 41 libgl1-mesa-dev libglx-dev libxext-dev libx11-dev libxr - andr-dev \ - 42 libxi-dev libxfixes-dev libxss-dev libxinerama-dev libx - cursor-dev \ - 42 - libogg-dev libvorbis-dev libasound2-dev \ - 43 + libogg-dev libvorbis-dev libasound2-dev openssh-client - +\ - 44 && rm -rf /var/lib/apt/lists/* - 45 - 46 WORKDIR /opt/prebuilt - -● Update(build-image/Dockerfile) ⎿  Added 7 lines - 87 COPY build-image/build-engine.sh /usr/local/bin/build-engine.sh - 88 RUN chmod +x /usr/local/bin/docker-entrypoint.sh /usr/local/bin - /build-engine.sh - 89 - 90 +# Marks a shell as already running inside this image (with ever - +y engine - 91 +# build dependency prebuilt above) - lets the Makefile's `engin - +e` target - 92 +# compile directly instead of shelling out to ./run-image.sh's - +`docker - 93 +# run`, which matters for CI jobs that already run inside this - +image - 94 +# (nested docker-in-docker isn't available there). - 95 +ENV QUESTSHOCK_BUILD_IMAGE=1 - 96 + - 97 WORKDIR /workspace - 98 ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] - 99 CMD ["/usr/local/bin/build-engine.sh"] - -● Task 9/11 done. Now let's write the res/assets/GET_ASSETS.txt placeholder and - the NOTICE.txt/license bundling, and the package target. - -● Write(res/assets/GET_ASSETS.txt) - ⎿  Wrote 17 lines to res/assets/GET_ASSETS.txt - 1 This package does not include System Shock's game data - it's - 3 - 4 To play, you need your own purchased copy: - 5 - e - 7 offline installer. - e was - 9 built from) and, in it, drop the installer into res/assets/ a - nd run - sound - … +11 lines (ctrl+o to expand) - -● Write(NOTICE.txt) - ⎿  Wrote 9 lines to NOTICE.txt - 1 This package's original tooling (build scripts, Makefile, launche - r) is - 3 - 4 The systemshock binary and its shared libraries in lib/ are built - from - icensed - 6 under the GNU GPLv3 - see LICENSE.Shockolate. It is included unch - anged - 8 - 9 Game assets are not included - see res/GET_ASSETS.txt. - … +3 lines (ctrl+o to expand) - - Read 1 file (ctrl+o to expand) - -● Write(Makefile) - ⎿  Added 67 lines, removed 8 lines - 3 # run-image.sh) and the game assets extracted from a purchased - copy (see - 4 # res/assets/extract_assets.sh). Building the engine needs the - build-image - 5 # (./build-image.sh, once); everything else here is plain file - copying. - 6 +# - 7 +# `make package` instead builds a redistributable tarball that - +omits the - 8 +# proprietary game assets entirely (see res/assets/GET_ASSETS.t - +xt, which - 9 +# it ships in their place) - this is what CI publishes. - 10 - 11 DIST_DIR := dist - 12 ENGINE_OUT := engine/.build-output - 13 ASSETS_DIR := res/assets/ss_ee - 14 +BUILD_DIR := build - 15 +ARCH := $(shell uname -m) - 16 - 11 -.PHONY: all dist build-image engine assets clean - 17 +.PHONY: all dist build-image engine assets package clean - 18 - 19 all: dist - 20 - ... - 23 build-image: - 24 ./build-image.sh - 25 - 20 -# Compiles engine/ (the vendored Shockolate snapshot) inside th - -e - 21 -# build-image - offline, since every dependency it needs was al - -ready baked - 22 -# into the image. Always re-run so `make dist` reflects the cur - -rent - 23 -# engine/ source, same as a fresh checkout would. - 26 +# Compiles engine/ (the vendored Shockolate snapshot) - offline - +, since - 27 +# every dependency it needs was already baked into the build-im - +age. - 28 +# Always re-run so `make dist`/`make package` reflect the curre - +nt engine/ - 29 +# source, same as a fresh checkout would. - 30 +# - 31 +# If we're already running inside the build-image (QUESTSHOCK_B - +UILD_IMAGE, - 32 +# set by its Dockerfile - true for a CI job using it as its con - +tainer, - 33 +# which has no nested docker available), compile directly; othe - +rwise - 34 +# shell out to ./run-image.sh, which docker-runs the image agai - +nst this - 35 +# checkout. - 36 engine: - 25 - ./run-image.sh - 37 + @if [ -n "$$QUESTSHOCK_BUILD_IMAGE" ]; then \ - 38 + bash build-image/build-engine.sh; \ - 39 + else \ - 40 + ./run-image.sh; \ - 41 + fi - 42 - 43 # Fails with a pointer to extract_assets.sh if the purchased ga - me assets - 44 # haven't been extracted yet. - ... - 51 - 52 dist: engine assets - 53 @echo "== Assembling $(DIST_DIR) ==" - 38 - rm -rf "$(DIST_DIR)" - 54 + rm -rf "$(DIST_DIR)/systemshock" "$(DIST_DIR)/lib" "$(DIST_DI - +R)/res" \ - 55 + "$(DIST_DIR)/shaders" "$(DIST_DIR)/run.sh" - 56 mkdir -p "$(DIST_DIR)/lib" "$(DIST_DIR)/res/data" "$(DIST_DIR - )/res/sound" - 57 cp "$(ENGINE_OUT)/systemshock" "$(DIST_DIR)/" - 58 cp -a "$(ENGINE_OUT)/lib/." "$(DIST_DIR)/lib/" - ... - 64 chmod +x "$(DIST_DIR)/run.sh" - 65 @echo "== Done - run $(DIST_DIR)/run.sh to play ==" - 66 - 67 +# Builds a versioned, redistributable Linux release tarball at - 68 +# dist/questshock--linux-.tar.gz - everything n - +eeded to - 69 +# run except the proprietary game assets (res/GET_ASSETS.txt e - +xplains how - 70 +# to get those instead of shipping res/data/, res/sound/). Ver - +sion - 71 +# defaults to the current git tag (vX.Y.Z, tag prefix stripped - +) - push a - 72 +# tag to drive a release. Override with `make package VERSION= - +1.2.3`, or - 73 +# just run it untagged for a local dev build (gets a 0.0.0-dev - ++ - 74 +# placeholder version, with a warning). - 75 +package: engine - 76 + @V="$(VERSION)"; \ - 77 + if [ -z "$$V" ]; then \ - 78 + if TAG=$$(git describe --tags --exact-match --match 'v[0-9 - +]*.[0-9]*.[0-9]*' 2>/dev/null); then \ - 79 + V=$${TAG#v}; \ - 80 + else \ - 81 + V="0.0.0-dev+$$(git rev-parse --short HEAD)"; \ - 82 + echo "WARNING: HEAD is not on a vX.Y.Z tag - building pl - +aceholder version $$V (push a tag to drive a real release vers - +ion)" >&2; \ - 83 + fi; \ - 84 + fi; \ - 85 + case "$$V" in \ - 86 + [0-9]*.[0-9]*.[0-9]*) ;; \ - 87 + *) echo "PREFLIGHT FAIL: VERSION '$$V' is not a semantic v - +ersion (expected X.Y.Z, optionally with a -pre+meta suffix)" > - +&2; exit 1;; \ - 88 + esac; \ - 89 + PKG_NAME="questshock-$$V-linux-$(ARCH)"; \ - 90 + PKG_STAGE="$(BUILD_DIR)/package/$$PKG_NAME"; \ - 91 + echo "Packaging $$PKG_NAME"; \ - 92 + rm -rf "$$PKG_STAGE"; \ - 93 + mkdir -p "$$PKG_STAGE/lib" "$$PKG_STAGE/res"; \ - 94 + cp "$(ENGINE_OUT)/systemshock" "$$PKG_STAGE/"; \ - 95 + cp -a "$(ENGINE_OUT)/lib/." "$$PKG_STAGE/lib/"; \ - 96 + cp -a engine/shaders "$$PKG_STAGE/shaders"; \ - 97 + cp "$(ENGINE_OUT)/soundfont.sf2" "$$PKG_STAGE/res/"; \ - 98 + cp res/assets/GET_ASSETS.txt "$$PKG_STAGE/res/GET_ASSETS.txt - +"; \ - 99 + cp res/run.sh "$$PKG_STAGE/run.sh"; \ - 100 + chmod +x "$$PKG_STAGE/run.sh"; \ - 101 + cp LICENSE "$$PKG_STAGE/LICENSE"; \ - 102 + cp engine/LICENSE "$$PKG_STAGE/LICENSE.Shockolate"; \ - 103 + cp NOTICE.txt "$$PKG_STAGE/NOTICE.txt"; \ - 104 + mkdir -p "$(DIST_DIR)"; \ - 105 + tar -czf "$(DIST_DIR)/$$PKG_NAME.tar.gz" -C "$(BUILD_DIR)/pa - +ckage" "$$PKG_NAME"; \ - 106 + rm -rf "$(BUILD_DIR)/package"; \ - 107 + echo "Wrote $(DIST_DIR)/$$PKG_NAME.tar.gz" - 108 + - 109 clean: - 51 - rm -rf "$(DIST_DIR)" "$(ENGINE_OUT)" \ - 110 + rm -rf "$(DIST_DIR)" "$(BUILD_DIR)" "$(ENGINE_OUT)" \ - 111 engine/build_ext engine/CMakeCache.txt engine/CMakeFiles \ - 112 engine/cmake_install.cmake engine/Makefile engine/systemsh - ock \ - 113 engine/src/Libraries/CMakeFiles - -● Now update .gitignore for /build/. - - Read 1 file (ctrl+o to expand) - -● Update(.gitignore) - ⎿  Added 1 line - 4 - 5 # Build output - 6 /dist/ - 7 +/build/ - 8 - 9 # Engine build artifacts (engine/ is committed as a source snap - shot; these - 10 # are generated by run-image.sh / build-image/build-engine.sh) - -❯ the GET_ASSETS text file should just explain which data is needed from the - enhanced edition. It doesn't make sense if a user who wants to play the game - extracts assets from a git repository. they should just copy the files in - place. - -● Write(res/assets/GET_ASSETS.txt) - ⎿  Added 10 lines, removed 11 lines - 1 This package does not include System Shock's game data - it's - 2 -copyrighted, proprietary content that can't be redistributed. - 2 +copyrighted, proprietary content that can't be redistributed. T - +o play, - 3 +you need a copy of System Shock: Enhanced Edition (e.g. from go - +g.com). - 4 - 4 -To play, you need your own purchased copy: - 5 +From your Enhanced Edition install, you need its classic-game d - +ata and - 6 +sound files - the res/pc/hd/data and res/pc/cdrom/data trees me - +rged - 7 +together (res/pc/hd's copies win the couple of filenames presen - +t in - 8 +both: intro.res, objprop.dat), and the res/pc/hd/sound tree, pa - +cked - 9 +inside the install's sshock.kpf (a zip file). - 10 - 6 -1. Buy System Shock: Enhanced Edition on gog.com and download t - -he - 7 - offline installer. - 8 -2. Get the questshock source repository (the project this packa - -ge was - 9 - built from) and, in it, drop the installer into res/assets/ - -and run - 10 - res/assets/extract_assets.sh. That pulls the game's data and - - sound - 11 - files out of the installer into res/assets/ss_ee/. - 12 -3. Copy the contents of res/assets/ss_ee/data/ into this packag - -e's - 13 - res/data/, and res/assets/ss_ee/sound/ into this package's - 14 - res/sound/ (both alongside this file). - 11 +Copy that merged data into place, alongside this file: - 12 + res/data/ <- res/pc/hd/data + res/pc/cdrom/data, merged - 13 + res/sound/ <- res/pc/hd/sound - 14 - 15 Once res/data/ and res/sound/ exist next to this file, run ./ru - n.sh from - 16 the root of this package to play. - -● Good, that's clearer and self-contained regardless of whether the user has the - source repo. Now let's write the Gitea Actions workflow (task 12). - - Ran 1 shell command (ctrl+o to expand) - -● Write(.gitea/workflows/build.yml) - ⎿  Wrote 76 lines to .gitea/workflows/build.yml - 1 name: build - 3 # Builds a versioned Linux release tarball (see `make package`) - 4 # push/PR, plus on-demand via the Gitea "Run workflow" button. R - 5 # inside the build-image (see ../../build-image/Dockerfile, buil - 6 # via ../../build-image.sh and ../../upload-image.sh), which bun - dles - needed - ld-image - site - ich - … +79 lines (ctrl+o to expand) - -● Now let's update the README to document the new package/CI flow, then validate - everything. - - Read 1 file (ctrl+o to expand) - -● Update(README.md) - ⎿  Added 3 lines, removed 1 line - 19 below); `res/assets/extract_assets.sh` extracts it into `ss_e - e/`. - 20 - `res/run.sh` - the launcher script, copied into `dist/` on bu - ild. - 21 - `Makefile` - assembles `dist/`, a self-contained runnable cop - y of the - 22 - game, out of the compiled engine and the extracted assets. - 22 + game, out of the compiled engine and the extracted assets. Al - +so builds - 23 + `dist/questshock--linux-.tar.gz`, a redistribu - +table - 24 + package that omits the proprietary game assets (`make package - +`). - 25 - 26 ## Building - 27 - -● Update(README.md) ⎿  Added 14 lines - 56 they aren't installed locally it falls back to running the extr - action in - 57 a throwaway Docker container instead. - 58 - 59 +## Packaging - 60 + - 61 +`make package` builds `dist/questshock--linux-.t - +ar.gz`: the - 62 +compiled binary, its runtime libraries, shaders, a default MIDI - 63 +soundfont, license information, and `res/GET_ASSETS.txt` in pla - +ce of the - 64 +actual game data (which the tarball never includes). Version co - +mes from - 65 +the current git tag (push a `vX.Y.Z` tag to drive a release); w - +ithout one - 66 +it builds an untagged `0.0.0-dev+` placeholder. - 67 + - 68 +A Gitea Actions workflow (`.gitea/workflows/build.yml`) builds - +this - 69 +package on every push, using the build-image as its container ( - +so no - 70 +extra setup is needed in CI beyond the image itself), and publi - +shes the - 71 +resulting tarball to dl.ladkau.de. - 72 + - 73 ## License - 74 - 75 The original tooling in this repository (the Docker build image - , build - - Ran 1 shell command (ctrl+o to expand) - -● The image rebuild is taking a bit longer this time (probably network - variance); it's been moved to background and I'll get notified when it's done. - I'll wait for that rather than poll. - - Ran 1 shell command (ctrl+o to expand) - -● I'll wait for the background build notification rather than poll. - -✻ Crunched for 15m 36s - -● Background command "./build-image.sh 2>&1 | tail -20" completed (exit -code 0) - -● The image rebuild finished. Let me check the output and proceed with - validation. - - Read 1 file (ctrl+o to expand) - -● Image built successfully with openssh-client and the QUESTSHOCK_BUILD_IMAGE - marker. Now let's test make package via the normal docker path (host machine, - no marker set). - - Ran 1 shell command (ctrl+o to expand) - -● make package works via the docker path (host machine, no - QUESTSHOCK_BUILD_IMAGE set → used ./run-image.sh). Let's inspect the tarball - contents. - - Ran 1 shell command (ctrl+o to expand) - -● Now let's simulate the CI code path — running make package directly inside the - image (as Gitea Actions would, bypassing our custom entrypoint since Actions - container jobs override it). - - Ran 1 shell command (ctrl+o to expand) - -● Good catch on a subtle issue: git describe/git rev-parse silently failed - inside the container due to git's "dubious ownership" safety check (root - running git against files owned by a different uid), producing a malformed - version string (0.0.0-dev+ with no sha). Let me harden make package against - that. - - Read 1 file (ctrl+o to expand) - -● Update(Makefile) - ⎿  Added 1 line - 73 # just run it untagged for a local dev build (gets a 0.0.0-dev+ - - 74 # placeholder version, with a warning). - 75 package: engine - 76 + @git config --global --add safe.directory "$$(pwd)" 2>/dev/nu - +ll || true - 77 @V="$(VERSION)"; \ - 78 if [ -z "$$V" ]; then \ - 79 if TAG=$$(git describe --tags --exact-match --match 'v[0-9] - *.[0-9]*.[0-9]*' 2>/dev/null); then \ - - Ran 1 shell command (ctrl+o to expand) - -● That test left root-owned files in the user's actual repo since I bypassed the - gosu entrypoint. Let me fix that immediately with a chown before doing - anything else. - - Ran 3 shell commands (ctrl+o to expand) - -● Fixed. Let me clean up properly now and redo the CI-path simulation running as - my own user (via --user) so it can't leave root-owned files again. - - Ran 1 shell command (ctrl+o to expand) - -● Clean now. Let's re-run the CI-path simulation properly as my own uid this - time. - - Ran 1 shell command (ctrl+o to expand) -● The version string now correctly includes the sha (0.0.0-dev+3ed6400), - confirming the safe.directory fix works, and no root-owned files this time. - - Ran 1 shell command (ctrl+o to expand) - -● Let's do one more smoke test — extract the package tarball standalone and - confirm the binary launches correctly from that location (with no assets - present, as a real downloader would have). - - Ran 1 shell command (ctrl+o to expand) - -● That's the expected, correct failure mode — it clearly logs the missing - res/data/*.res files (proving run.sh/library paths are all still correct) - before segfaulting on the totally-absent game data, which is an inherent - engine limitation, not a packaging bug. A user who follows GET_ASSETS.txt and - drops in res/data/res/sound gets exactly the working setup already validated - earlier. - - Ran 1 shell command (ctrl+o to expand) - -● Everything's clean and validated. Summary of what changed: - - New make package target — builds - dist/questshock--linux-.tar.gz: the binary, its runtime .so - libraries, shaders, a MIDI soundfont, LICENSE (MIT) + LICENSE.Shockolate - (GPLv3, verbatim) + NOTICE.txt, run.sh, and res/GET_ASSETS.txt in place of the - actual game data — explaining exactly which files are needed from an Enhanced - Edition install (res/pc/hd/data + res/pc/cdrom/data merged, res/pc/hd/sound) - and where to drop them, with no dependency on this source repo's own tooling. - Version comes from git describe --tags (push a vX.Y.Z tag), falling back to a - 0.0.0-dev+ placeholder. - - CI: .gitea/workflows/build.yml runs on every push/PR using the build-image as - its container, builds the package, uploads it as a build artifact, and (on - push only) publishes the tarball via SFTP to dl.ladkau.de/files/questshock/. - - Two real bugs I found and fixed while validating, not just assumed away: - 1. The engine build script assumed it always runs via docker run with the repo - at /workspace. Gitea Actions container jobs run inside the build-image - directly (no nested docker available), so I added a QUESTSHOCK_BUILD_IMAGE - marker the Dockerfile sets, and the Makefile's engine target now compiles - directly when it detects that, instead of trying (and failing) to shell out to - run-image.sh. - 2. git describe/git rev-parse silently failed inside the container due to - git's dubious-ownership check, producing a broken version string (0.0.0-dev+ - with no sha). Fixed by registering the workspace as a safe directory in the - package recipe itself. - - I validated the whole thing three ways: the normal host→docker path, a direct - simulation of the CI code path (running as a container job would, no nested - docker), and extracting the resulting tarball standalone and confirming it - launches and correctly reports missing assets rather than doing anything - worse. - -✻ Brewed for 7m 15s - -❯ hmm, running run-image no longer produces a dist directory - - Ran 2 shell commands (ctrl+o to expand) - -● User answered Claude's questions: - ⎿  · Just so I fix the right thing: what exactly did you run? → Ran - ./run-image.sh directly - -● That's expected behavior, not a regression — run-image.sh only ever compiled - the engine into engine/.build-output/; dist/ needs make dist afterward. Let me - make that clearer by having the script say so. - - Read 1 file (ctrl+o to expand) - -● Update(run-image.sh) - ⎿  Added 7 lines - 46 -e HOST_GID="$(id -g)" \ - 47 "$IMAGE" \ - 48 "$@" - 49 + - 50 +if [ "$#" -eq 0 ]; then - 51 + echo - 52 + echo "This only compiled the engine (engine/.build-output/) - + - it does not" - 53 + echo "create dist/ by itself. Run 'make dist' next to assem - +ble a runnable" - 54 + echo "copy (or 'make package' for a redistributable tarball - +)." - 55 +fi - -● Let's verify the new hint actually shows up. - - Ran 1 shell command (ctrl+o to expand) - -● Confirmed — the hint prints correctly now. To be clear: this wasn't a - regression, ./run-image.sh was never responsible for producing dist/, only for - compiling the engine into engine/.build-output/. make dist (or make package) - is the step that assembles the runnable/distributable output, and that still - works fine (I re-ran it above). I've added a printed reminder at the end of - run-image.sh so this doesn't trip anyone up again. - -✻ Churned for 2m 55s - -❯ cool, what would be a good commit message - - Ran 2 shell commands (ctrl+o to expand) - -● Since it looks like everything's already staged, here's a message that matches - the "why"-focused, plain-sentence style of your existing commit: - - Subject: - Add Docker build pipeline, asset extraction, and CI release packaging - - Body: - Vendor the Shockolate engine and build it via a Docker image with every - dependency (SDL2, SDL2_mixer, fluidsynth-lite, a MIDI soundfont) - prebuilt, so compiling the engine needs no network access - just the - image and the engine source. Add res/assets/extract_assets.sh to pull - the game's data files out of a purchased GOG installer, and a Makefile - that assembles a runnable dist/ from the two. - - Also add `make package`, which builds a redistributable tarball that - omits the proprietary game assets (shipping res/GET_ASSETS.txt instead) - plus license information for both the MIT tooling and the GPLv3 engine, - and a Gitea Actions workflow that builds and publishes it to - dl.ladkau.de on every push. - - Want me to go ahead and commit this (as-is or tweaked), or would you rather - split it into two commits (engine/build pipeline vs. packaging/CI)? - -✻ Cogitated for 1m 4s - -❯ please do not commit - I will do commit and push -● Understood — I won't commit or push anything. Let me know if you want any - changes before you commit. - -✻ Cooked for 12s - -❯ please include the logo in the res folder as part of the readme.md file right - under the heading. running the action in gitea gives an error: Run - actions/checkout@v4 - with: - fetch-depth: 0 - OCI runtime exec failed: exec failed: unable to start container process: - exec: "node": executable file not found in $PATH - ❌ Failure - Main actions/checkout@v4 - exitcode '127': command not found, please refer to - https://github.com/nektos/act/issues/107 for more information - - - Searched for 1 pattern, read 1 file, ran 1 shell command (ctrl+o to expand) - -● Logo looks great — I'll add it under the heading. Now let's fix both things. - - Read 1 file (ctrl+o to expand) - -● Update(README.md) - ⎿  Added 2 lines - 1 # questshock - 2 - 3 +![questshock](res/logo_small.png) - 4 + - 5 An open source project to play the classic 1994 System Shock on - a VR - 6 headset, built on top of [Shockolate](https://github.com/Interru - pt/systemshock), - 7 a cross-platform port of the original game. - - Ran 1 shell command (ctrl+o to expand) - -● Now let's fix the Gitea Actions failure — actions/checkout@v4 is a - Node.js-based action, and our build-image has no Node.js installed, so it - can't run inside the container job. Let's add it. - - Read 1 file (ctrl+o to expand) - -● Update(build-image/Dockerfile) - ⎿  Added 5 lines - 18 # Soundfont used for MIDI music, matching what engine/build_dep - s.sh itself - 19 # fetches (a free substitute for the Windows default GM soundfo - nt). - 20 ARG SOUNDFONT_URL=http://rancid.kapsi.fi/windows.sf2 - 21 +# Gitea/GitHub Actions' JS-based actions (actions/checkout, - 22 +# actions/upload-artifact, ...) need a node binary in the conta - +iner job's - 23 +# PATH - this image is otherwise pure C toolchain, so it isn't - +pulled in - 24 +# by anything else. - 25 +ARG NODE_VERSION=20.18.1 - 26 - 27 ENV DEBIAN_FRONTEND=noninteractive - 28 - -● Update(build-image/Dockerfile) - ⎿  Added 7 lines - 88 RUN mkdir -p soundfont \ - 89 && curl -sSL -o soundfont/default.sf2 "${SOUNDFONT_URL}" - 90 - 91 +# Node.js: needed only so Gitea/GitHub Actions' JS-based actio - +ns can run - 92 +# when this image is used as a CI job's container - see NODE_V - +ERSION above. - 93 +RUN curl -sSL -o /tmp/node.tar.xz \ - 94 + "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE - +_VERSION}-linux-x64.tar.xz" \ - 95 + && tar -xJf /tmp/node.tar.xz -C /usr/local --strip-compone - +nts=1 \ - 96 + && rm /tmp/node.tar.xz - 97 + - 98 COPY build-image/docker-entrypoint.sh /usr/local/bin/docker-en - trypoint.sh - 99 COPY build-image/build-engine.sh /usr/local/bin/build-engine.s - h - 100 RUN chmod +x /usr/local/bin/docker-entrypoint.sh /usr/local/bi - n/build-engine.sh - - Read 1 file (ctrl+o to expand) - -● Write(build-image/VERSION) - ⎿  Added 1 line, removed 1 line - 1 -1 - 1 +2 - -● Now let's rebuild the image and verify Node.js works, plus confirm the engine - build still succeeds. - - Ran 1 shell command (ctrl+o to expand) - -❯ maybe leave the image version as 1 until we have a first successful run - -● Write(build-image/VERSION) - ⎿  Added 1 line, removed 1 line - 1 -2 - 1 +1 - -● Reverted — VERSION stays at 1. Summary of what's in place now: - - - README logo: res/logo_small.png added right under the heading. - - Dockerfile fix: added a Node.js install step (needed because - actions/checkout@v4 and actions/upload-artifact@v3 are JS-based actions, and - Gitea/act_runner executes them inside the job's container — our build-image - had no node in PATH, causing that exact error). - - Since I didn't rebuild/push, the image at - cr.ladkau.de/questshock/builder:1/:latest still lacks Node.js. When you're - ready: ./build-image.sh to rebuild locally, verify it, then ./upload-image.sh - to push — after that a re-run of the Gitea Action should get past the checkout - step. Once that's confirmed working, bump build-image/VERSION to 2 (or just - re-push over 1/latest, your call). - -✻ Sautéed for 30s - -❯ since the build image never ran successfully it should stay as version 1. - -● That makes sense — no version bump until a build with that image has actually - succeeded once. I'll keep that in mind for this project. - - Recalled 1 memory, wrote 2 memories (ctrl+o to expand) - -● Noted and saved for future reference. Version stays at 1 — you'll bump it once - a CI run actually succeeds against it. -✻ Baked for 3m 13s - -❯ now the next step is to run the game on the meta quest 2 headset. I have - installed sidequest and connected it to the quest 2 headset. I should be able - to install an apk image and copy over the assets into a folder. Most other - games which require external assets seem to require a specific root folder - like for example QuestZDoom. The folder is created on the first run of the - app where the app displays that asset files are missing. it should be - possible to build the apk locally using again the build image (this time with - a higher version) - -● This is a much bigger step than the Linux build — Shockolate has no Android - support at all right now (no NDK toolchain, no Java/SDL2 Android glue, no VR - display code), so before we can even talk about "build an APK," there's real - engine porting work involved. Let me check whether anyone's already done part - of this (similar to how QuestZDoom exists for GZDoom) before I scope this out. - -● Web Search("Shockolate System Shock Android port Quest VR") - ⎿  Did 1 search in 6s - -● Web Search("QuestZDoom how it works SDL2 Android Quest VR panel flat screen") - ⎿  Did 1 search in 10s - -● Good news and bad news from that research: - - Bad news: there's no existing Android or Quest port of Shockolate to build on. - And the closest reference point, QuestZDoom, isn't just "an APK build" — it's - a full 6-DoF VR engine port (stereo rendering, Touch controller mapping, head - tracking, teleport/smooth locomotion), which is a much bigger lift than a - Linux Docker build. Getting there from Shockolate's current desktop-only - C/SDL2 code involves several genuinely separate pieces of work: an Android NDK - cross-compile toolchain (SDL2/SDL2_mixer/fluidsynth-lite all rebuilt for - arm64-v8a), an actual Android app project (AndroidManifest.xml, Java - SDLActivity glue — none of which exist in this repo today), - Android-appropriate asset-path handling (the engine currently just does - fopen("res/data/...") relative to cwd, which doesn't work the same way in an - Android app), and — only for real VR immersion — OpenXR/Oculus Mobile SDK - integration for stereo rendering and controller input. - - Good news: what you described (APK installs, runs, detects missing assets, - creates a folder, you copy files in via SideQuest) doesn't require any of the - VR-specific work — that's exactly the pattern for getting a plain 2D Android - app running on Quest first (Quest natively runs sideloaded non-VR Android apps - as a flat floating panel in Home). That's a much smaller, achievable first - milestone, with full VR head-tracking as a distinct later phase. - -● User answered Claude's questions: - ⎿  · Given the gap between "runs on Quest" and "full VR like QuestZDoom", what - should this step actually target? → Flat 2D panel APK first (Recommended) - -● Entered plan mode - Claude is now exploring and designing an implementation approach. - -● Web Search("QuestZDoom where to copy wad files sdcard folder path SideQuest") - ⎿  Did 1 search in 8s - -● Web Search("SDL2 2.0.9 android-project template Android.mk build apk - SDL_androidmain") - ⎿  Did 1 search in 8s - -● Web Search("Quest Android app requestLegacyExternalStorage sdcard scoped - storage sideload homebrew 2024") - ⎿  Did 1 search in 13s - -● Web Search("SDL2 android ResOpenFile fopen AAssetManager relative path - fallback SDL_RWFromFile") - ⎿  Did 1 search in 9s - -❯ one more constrant - the shockolate code should not be modified but rather - pathed if it is required. this is to avoid touching the GPL code. - -❯ sorry patched not pathed - -● User answered Claude's questions: - ⎿  · The desktop build is fully offline (everything prebuilt into the image). - Android's Gradle/AGP toolchain almost always needs network access at build - time for its own dependency resolution (androidx, Gradle plugin metadata), - even with the NDK/SDL2/etc. all prebuilt into the image. Is that an - acceptable exception for the APK build specifically? → Yes, accept it for - the APK build only (Recommended) - -● Updated plan - ⎿  /plan to preview - -● User approved Claude's plan - ⎿  Plan saved to: ~/.claude/plans/wiggly-spinning-swing.md · /plan to edit - Get System Shock running on Meta Quest 2 (milestone 1: flat 2D panel) - - Context - - The user wants to sideload System Shock onto a Quest 2 via SideQuest, with - an asset folder created on first run (like QuestZDoom's /QuestZDoom/ - folder) that they populate by copying files over. Research this session - confirmed: - - - There is no existing Android or Quest port of Shockolate to build on. - - QuestZDoom, the closest reference point, is a full 6DoF VR engine port - (stereo rendering, Touch controller mapping, OpenXR) - a much bigger - scope than "build an APK". The user has confirmed (via question) that - this step targets a flat 2D panel app first - Quest natively runs a - sideloaded plain Android activity as a floating 2D panel with no VR SDK - or manifest entries required. Full 6DoF VR (OpenXR, controller input, - stereo rendering) is explicitly out of scope for this step. - - Input for this milestone: a Bluetooth mouse/keyboard connected to the - Quest (SDL2's Android backend supports HID keyboard/mouse). No - on-screen touch controls or controller mapping now. - - Hard constraint (user-stated): engine/ (the vendored GPLv3 - Shockolate source) must not be modified in place. Any Android-specific - change to its source, if one turns out to be unavoidable, must be a - patch applied at build time to a scratch copy - never a hand-edit of - the committed snapshot. - - Confirmed trade-off: unlike the fully-offline desktop build, - make apk is allowed to need network access at build time (Gradle/AGP's - own dependency resolution) - a scoped, documented exception, not a - change to the desktop build's philosophy. - - Per the saved build-image version policy: build-image/VERSION stays - at 1 until a build with the new Android toolchain actually succeeds - locally - then bump it. - - Key design decision: zero changes to engine/, via a constructor shim - - Shockolate's main() (in src/MacSrc/Shock.c) already becomes SDL2's - SDL_main automatically (SDL_main.h's macro rename, already in effect on - the desktop build too) - there is no way to run code before it without - either editing engine/ or a link-time trick. The plan uses the trick: a - new Android-only source file (android/app/src/main/cpp/android_shim.c, - NOT under engine/) defines a function marked - __attribute__((constructor)), which the C runtime guarantees runs at - shared-library load time - before main()/SDL_main() executes. That - function will: - - 1. Ensure /sdcard/questshock/ exists. - 2. Extract our own bundled build artifacts (shaders/, the soundfont, - a copy of the "get assets" instructions) from the APK's Android assets - into that folder, if not already present - mirroring make package's - approach on desktop (ship everything except the proprietary game data). - 3. chdir() into /sdcard/questshock/. - - Because Shockolate's own file I/O (ResOpenFile, fopen_caseless, plain - fopen) already uses bare relative paths like "res/data/xxx.res" - (confirmed via grep during the desktop build work), and none of it routes - through SDL_RWops (so Android's automatic asset-manager fallback for - SDL_RWFromFile doesn't apply here), this chdir alone is sufficient: if - the user copies res/data/ and res/sound/ into - /sdcard/questshock/res/, the exact same relative-path lookups that - already work in the Linux dist/ build will resolve correctly on - Android too - no engine source changes needed. Missing-asset behavior - (warn-then-crash) will be identical to what was already observed and - accepted for the Linux package tarball without assets. - - If, once building/testing, something in engine/ genuinely needs a - change (the main known risk: the desktop OpenGL shaders in - engine/shaders/*.{vert,frag} may not compile as-is under OpenGL ES, - which Android uses) - per the hard constraint, this must NOT be hand- - edited into the tracked engine/ snapshot. Instead: add .patch files - under a new android/engine-patches/ directory, and have the Android - build step apply them (patch/git apply) to a throwaway copy of - engine/ made during the build, before compiling. engine/ as committed - stays byte-for-byte the vendored upstream snapshot either way. - - What's being added - - android/ (new Gradle project, parallel to engine/, build-image/): - - Standard SDL2-for-Android skeleton: SDLActivity-based Java Activity, - AndroidManifest.xml (targetSdkVersion 29 + requestLegacyExternalStorage - - READ/WRITE_EXTERNAL_STORAGE - keeps /sdcard/questshock/ a plain, - unrestricted shared folder, avoiding Android 11+ scoped storage - entirely, matching the QuestZDoom-style precedent), build.gradle, - gradlew. - - app/src/main/cpp/CMakeLists.txt: compiles engine/src/**/*.c(c) - by reference (relative path into engine/, not copied) plus - android_shim.c, linking the prebuilt Android SDL2/SDL2_mixer/ - fluidsynth-lite from the image. Applies any android/engine-patches/ - patches to a build-time scratch copy first, if that directory is - non-empty. - - app/src/main/assets/: shaders/*, the soundfont, and a copy of a - Quest-specific "get assets" text file - bundled into the APK, extracted - to /sdcard/questshock/ on first run by android_shim.c. - - App icon derived from the existing res/logo.png/res/logo_small.png. - - Application ID: de.ladkau.questshock (matches the existing - cr.ladkau.de/dl.ladkau.de naming already used in this project; - trivial to rename later). - - build-image/Dockerfile (still version 1 until a local build - succeeds): add OpenJDK 17, the Android cmdline-tools + platform 29 + - build-tools + a pinned NDK, and cross-compile SDL2, SDL2_mixer, and - fluidsynth-lite for arm64-v8a at image-build time into - /opt/prebuilt/android/{sdl2,sdl2_mixer,fluidsynth-lite} - same - "prebuilt so the actual build needs minimal extra downloads" philosophy - as the existing desktop layer, with the one accepted exception that - Gradle itself still needs network access when ./gradlew assembleDebug - actually runs. - - build-image/build-apk.sh (parallel to build-engine.sh): runs - ./gradlew assembleDebug in android/, copies the resulting APK to - dist/. - - Makefile: new apk target, mirroring the existing engine target's - QUESTSHOCK_BUILD_IMAGE detection (compile directly if already inside the - image, else shell out via ./run-image.sh). - - README.md: document the Quest install flow (SideQuest sideload, - /sdcard/questshock/ folder, what goes in it) and make apk. - - Verification (what I can check vs. what needs the real headset) - - I can verify locally: the build-image builds with the new Android layer, - the arm64 cross-compiles of SDL2/SDL2_mixer/fluidsynth-lite succeed, and - make apk produces a real .apk with the native library and assets - correctly embedded (via unzip -l, aapt dump badging/manifest checks). - - I cannot verify actual runtime behavior - this sandboxed environment has - no GPU-accelerated Android emulator and no access to real Quest hardware. - OpenGL ES shader compatibility, the first-run folder/extraction behavior, - and actual gameplay will need to be tested by the user on their Quest 2 - via SideQuest. I'll flag remaining risk areas explicitly rather than - claim success I can't check. - - Once a local build of the new image succeeds, bump build-image/VERSION - to 2 (per the saved policy - not before). -❯ yes that is indeed acceptable - the offline build for the (core) engine - should just give a solid foundation. concern was that some dependencies might - not be reliable. - -● Good context — that confirms the scoped exception is fine. Let's start - building. First, pinning a modern SDL2 release for the Android side. - - Ran 2 shell commands (ctrl+o to expand) - -❯ the current build for linux should of course continue to work. - - Ran 4 shell commands (ctrl+o to expand) - -✻ Crunched for 35m 40s - -❯ please continue - - Searched for 1 pattern, ran 1 shell command (ctrl+o to expand) - -● Now I have a clear technical picture. Let me start implementing. First, let's - inspect the SDL2 Java glue and Activity class we'll reuse, then build out the - Android layer piece by piece. - - Read 1 file, ran 2 shell commands (ctrl+o to expand) -● Update(build-image/Dockerfile) - ⎿  Added 17 lines - 24 # by anything else. - 25 ARG NODE_VERSION=20.18.1 - 26 - 27 +# Android layer, for the Quest 2 APK build (see build-apk.sh). - +Same SDL2 - 28 +# major version as the desktop build above, but SDL2/SDL2_mixer - + are much - 29 +# newer releases here - unlike the desktop autotools build, And - +roid needs - 30 +# their real CMake+NDK build support, which only landed in late - +r - 31 +# releases. targetSdkVersion 29 (not compileSdk, which can and - +does stay - 32 +# newer) is deliberate: paired with requestLegacyExternalStorag - +e in the - 33 +# manifest, it keeps /sdcard a plain, unrestricted shared folde - +r on - 34 +# Android 10-13 devices like the Quest 2, instead of hitting An - +droid 11+ - 35 +# scoped storage. - 36 +ARG ANDROID_SDL2_VERSION=2.28.5 - 37 +ARG ANDROID_SDL2_MIXER_VERSION=2.8.0 - 38 +ARG ANDROID_CMDLINE_TOOLS_VERSION=11076708 - 39 +ARG ANDROID_PLATFORM_VERSION=29 - 40 +ARG ANDROID_BUILD_TOOLS_VERSION=34.0.0 - 41 +ARG ANDROID_NDK_VERSION=26.1.10909125 - 42 +ARG ANDROID_CMAKE_VERSION=3.22.1 - 43 + - 44 ENV DEBIAN_FRONTEND=noninteractive - 45 - 46 # build-essential/cmake/make: engine/ itself (CMake) and SDL2/S - DL2_mixer - -● Update(build-image/Dockerfile) - ⎿  Added 89 lines - 112 && tar -xJf /tmp/node.tar.xz -C /usr/local --strip-compone - nts=1 \ - 113 && rm /tmp/node.tar.xz - 114 - 115 +# openjdk-17-jdk-headless: Gradle/AGP's own minimum JDK for th - +e APK build. - 116 +# unzip: extracts the Android cmdline-tools zip below. - 117 +RUN apt-get update && apt-get install -y --no-install-recommen - +ds \ - 118 + openjdk-17-jdk-headless unzip \ - 119 + && rm -rf /var/lib/apt/lists/* - 120 + - 121 +# Android SDK/NDK: the cmdline-tools' own sdkmanager installs - +exactly the - 122 +# platform/build-tools/NDK/CMake versions the app/build.gradle - + below pins. - 123 +# gradlew (invoked by build-apk.sh) resolves its own pinned Gr - +adle version - 124 +# itself - no Gradle binary is baked into this image. Unlike t - +he manually - 125 +# cross-compiled SDL2/SDL2_mixer/fluidsynth-lite below, `./gra - +dlew - 126 +# assembleDebug` still needs network access at build time for - +Gradle/AGP's - 127 +# own dependency resolution - an accepted, deliberate exceptio - +n to this - 128 +# image's otherwise-offline build philosophy (see build-apk.sh - +). - 129 +ENV ANDROID_HOME=/opt/android-sdk \ - 130 + ANDROID_SDK_ROOT=/opt/android-sdk - 131 +ENV PATH=${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HO - +ME}/platform-tools:${PATH} - 132 + - 133 +RUN mkdir -p "${ANDROID_HOME}/cmdline-tools" \ - 134 + && curl -sSL -o /tmp/cmdline-tools.zip \ - 135 + "https://dl.google.com/android/repository/commandlinet - +ools-linux-${ANDROID_CMDLINE_TOOLS_VERSION}_latest.zip" \ - 136 + && unzip -q /tmp/cmdline-tools.zip -d "${ANDROID_HOME}/cmd - +line-tools" \ - 137 + && mv "${ANDROID_HOME}/cmdline-tools/cmdline-tools" "${AND - +ROID_HOME}/cmdline-tools/latest" \ - 138 + && rm /tmp/cmdline-tools.zip \ - 139 + && yes | sdkmanager --licenses >/dev/null \ - 140 + && sdkmanager --install \ - 141 + "platform-tools" \ - 142 + "platforms;android-${ANDROID_PLATFORM_VERSION}" \ - 143 + "build-tools;${ANDROID_BUILD_TOOLS_VERSION}" \ - 144 + "ndk;${ANDROID_NDK_VERSION}" \ - 145 + "cmake;${ANDROID_CMAKE_VERSION}" - 146 + - 147 +ENV ANDROID_NDK_HOME=${ANDROID_HOME}/ndk/${ANDROID_NDK_VERSION - +} - 148 +ENV ANDROID_NDK_TOOLCHAIN=${ANDROID_NDK_HOME}/build/cmake/andr - +oid.toolchain.cmake - 149 + - 150 +# SDL2, SDL2_mixer, and fluidsynth-lite, cross-compiled for ar - +m64-v8a (the - 151 +# Quest 2's only ABI) via the NDK's CMake toolchain file, each - + installed - 152 +# to its own prefix under /opt/prebuilt/android/ - mirrors the - + desktop - 153 +# build_ext/built_sdl / built_sdl_mixer layout, just for Andro - +id. Shipped - 154 +# in the APK as separate .so's (see build-apk.sh), loaded in t - +hat order by - 155 +# QuestShockActivity.getLibraries() before the game's own libm - +ain.so. - 156 +ARG ANDROID_ABI=arm64-v8a - 157 + - 158 +RUN curl -sSLO "https://www.libsdl.org/release/SDL2-${ANDROID_ - +SDL2_VERSION}.tar.gz" \ - 159 + && tar xf "SDL2-${ANDROID_SDL2_VERSION}.tar.gz" \ - 160 + && cmake -S "SDL2-${ANDROID_SDL2_VERSION}" -B build-sdl2-a - +ndroid \ - 161 + -DCMAKE_TOOLCHAIN_FILE="${ANDROID_NDK_TOOLCHAIN}" \ - 162 + -DANDROID_ABI="${ANDROID_ABI}" -DANDROID_PLATFORM="and - +roid-${ANDROID_PLATFORM_VERSION}" \ - 163 + -DCMAKE_INSTALL_PREFIX=/opt/prebuilt/android/sdl2 -DBU - +ILD_SHARED_LIBS=ON \ - 164 + -DSDL_STATIC=OFF \ - 165 + && cmake --build build-sdl2-android -j"$(nproc)" \ - 166 + && cmake --install build-sdl2-android \ - 167 + && rm -rf "SDL2-${ANDROID_SDL2_VERSION}" "SDL2-${ANDROID_S - +DL2_VERSION}.tar.gz" build-sdl2-android - 168 + - 169 +# All optional codecs disabled: Shockolate only ever calls - 170 +# Mix_LoadWAV_RW/Mix_HookMusic (confirmed by grep - it feeds f - +luidsynth's - 171 +# own PCM output through Mix_HookMusic, and never loads OGG/MO - +D/FLAC/MP3 - 172 +# game data), so plain WAVE support (always built in, no extra - 173 +# dependency) is all that's needed - avoiding SDL2_mixer's ven - +dored - 174 +# third-party codec libraries entirely. - 175 +RUN curl -sSLO "https://www.libsdl.org/projects/SDL_mixer/rele - +ase/SDL2_mixer-${ANDROID_SDL2_MIXER_VERSION}.tar.gz" \ - 176 + && tar xf "SDL2_mixer-${ANDROID_SDL2_MIXER_VERSION}.tar.gz - +" \ - 177 + && cmake -S "SDL2_mixer-${ANDROID_SDL2_MIXER_VERSION}" -B - +build-sdl2mixer-android \ - 178 + -DCMAKE_TOOLCHAIN_FILE="${ANDROID_NDK_TOOLCHAIN}" \ - 179 + -DANDROID_ABI="${ANDROID_ABI}" -DANDROID_PLATFORM="and - +roid-${ANDROID_PLATFORM_VERSION}" \ - 180 + -DCMAKE_PREFIX_PATH=/opt/prebuilt/android/sdl2 \ - 181 + -DCMAKE_INSTALL_PREFIX=/opt/prebuilt/android/sdl2_mixe - +r -DBUILD_SHARED_LIBS=ON \ - 182 + -DSDL2MIXER_VENDORED=OFF -DSDL2MIXER_SAMPLES=OFF -DSDL - +2MIXER_CMD=OFF \ - 183 + -DSDL2MIXER_FLAC=OFF -DSDL2MIXER_GME=OFF -DSDL2MIXER_M - +OD=OFF \ - 184 + -DSDL2MIXER_MP3=OFF -DSDL2MIXER_MIDI=OFF -DSDL2MIXER_O - +PUS=OFF -DSDL2MIXER_VORBIS=OFF \ - 185 + && cmake --build build-sdl2mixer-android -j"$(nproc)" \ - 186 + && cmake --install build-sdl2mixer-android \ - 187 + && rm -rf "SDL2_mixer-${ANDROID_SDL2_MIXER_VERSION}" "SDL2 - +_mixer-${ANDROID_SDL2_MIXER_VERSION}.tar.gz" build-sdl2mixer-a - +ndroid - 188 + - 189 +RUN git clone https://github.com/EtherTyper/fluidsynth-lite.gi - +t fluidsynth-lite-android \ - 190 + && cd fluidsynth-lite-android \ - 191 + && git checkout "${FLUIDSYNTH_LITE_REF}" \ - 192 + && sed -i 's/DLL"\ off/DLL"\ on/' CMakeLists.txt \ - 193 + && rm -rf .git \ - 194 + && cd .. \ - 195 + && cmake -S fluidsynth-lite-android -B build-fluidsynth-an - +droid \ - 196 + -DCMAKE_TOOLCHAIN_FILE="${ANDROID_NDK_TOOLCHAIN}" \ - 197 + -DANDROID_ABI="${ANDROID_ABI}" -DANDROID_PLATFORM="and - +roid-${ANDROID_PLATFORM_VERSION}" \ - 198 + && cmake --build build-fluidsynth-android -j"$(nproc)" \ - 199 + && mkdir -p /opt/prebuilt/android/fluidsynth-lite/lib /opt - +/prebuilt/android/fluidsynth-lite/include \ - 200 + && cp -a build-fluidsynth-android/src/libfluidsynth.so* /o - +pt/prebuilt/android/fluidsynth-lite/lib/ \ - 201 + && cp -a fluidsynth-lite-android/include/. /opt/prebuilt/a - +ndroid/fluidsynth-lite/include/ \ - 202 + && rm -rf fluidsynth-lite-android build-fluidsynth-android - 203 + - 204 COPY build-image/docker-entrypoint.sh /usr/local/bin/docker-en - trypoint.sh - 205 COPY build-image/build-engine.sh /usr/local/bin/build-engine.s - h - 206 RUN chmod +x /usr/local/bin/docker-entrypoint.sh /usr/local/bi - n/build-engine.sh - -❯ this should of course also run on a quest 3 and newer versions - -● Good point — since Quest 3 (and 3S/Pro) all use the same Snapdragon XR2-family - arm64-v8a chips and the same targetSdk-based legacy-storage behavior, nothing - technical changes — this design already generalizes. Let me just fix the - wording so it doesn't imply Quest 2-only. - - Searched for 1 pattern, read 1 file, ran 1 shell command (ctrl+o to expand) - -● Update(build-image/Dockerfile) - ⎿  Added 11 lines, removed 9 lines - 24 # by anything else. - 25 ARG NODE_VERSION=20.18.1 - 26 - 27 -# Android layer, for the Quest 2 APK build (see build-apk.sh). - -Same SDL2 - 28 -# major version as the desktop build above, but SDL2/SDL2_mixer - - are much - 29 -# newer releases here - unlike the desktop autotools build, And - -roid needs - 30 -# their real CMake+NDK build support, which only landed in late - -r - 31 -# releases. targetSdkVersion 29 (not compileSdk, which can and - -does stay - 32 -# newer) is deliberate: paired with requestLegacyExternalStorag - -e in the - 33 -# manifest, it keeps /sdcard a plain, unrestricted shared folde - -r on - 34 -# Android 10-13 devices like the Quest 2, instead of hitting An - -droid 11+ - 35 -# scoped storage. - 27 +# Android layer, for the Quest APK build (Quest 2, 3, 3S, Pro - - + all - 28 +# arm64-v8a, see build-apk.sh). Same SDL2 major version as the - +desktop - 29 +# build above, but SDL2/SDL2_mixer are much newer releases here - + - unlike - 30 +# the desktop autotools build, Android needs their real CMake+N - +DK build - 31 +# support, which only landed in later releases. targetSdkVersio - +n 29 (not - 32 +# compileSdk, which can and does stay newer) is deliberate: pai - +red with - 33 +# requestLegacyExternalStorage in the manifest, it keeps /sdcar - +d a plain, - 34 +# unrestricted shared folder instead of hitting Android 11+ sco - +ped - 35 +# storage - this is keyed off the *app's* targetSdkVersion, not - + the - 36 +# device's own Android version, so it keeps working on newer Qu - +est - 37 +# hardware/OS updates too, not just whatever's current today. - 38 ARG ANDROID_SDL2_VERSION=2.28.5 - 39 ARG ANDROID_SDL2_MIXER_VERSION=2.8.0 - 40 ARG ANDROID_CMDLINE_TOOLS_VERSION=11076708 - - Searched for 1 pattern (ctrl+o to expand) - -● Update(build-image/Dockerfile) - ⎿  Added 1 line, removed 1 line - 150 ENV ANDROID_NDK_TOOLCHAIN=${ANDROID_NDK_HOME}/build/cmake/andr - oid.toolchain.cmake - 151 - 152 # SDL2, SDL2_mixer, and fluidsynth-lite, cross-compiled for ar - m64-v8a (the - 153 -# Quest 2's only ABI) via the NDK's CMake toolchain file, each - - installed - 153 +# Quest's only ABI, across Quest 2/3/3S/Pro) via the NDK's CMa - +ke toolchain file, each installed - 154 # to its own prefix under /opt/prebuilt/android/ - mirrors the - desktop - 155 # build_ext/built_sdl / built_sdl_mixer layout, just for Andro - id. Shipped - 156 # in the APK as separate .so's (see build-apk.sh), loaded in t - hat order by - - Searched for 1 pattern, read 1 file, ran 1 shell command (ctrl+o to expand) - -● Let me generate the patch reliably via diff -u rather than hand-crafting it, - to avoid whitespace mismatches. - - Made 2 scratchpad edits +32 -16, read 1 file, ran 1 shell command (ctrl+o to expand) - -● Now let's generate the diff and verify it applies cleanly to a fresh copy. - - Ran 1 shell command (ctrl+o to expand) - -● Patch applies cleanly. Let's save it and clean the timestamp lines. - - Read 1 file (ctrl+o to expand) - -● Write(android/engine-patches/android-shared-lib.patch) - ⎿  Wrote 52 lines to android/engine-patches/android-shared-lib.patch - 1 --- a/CMakeLists.txt - 2 +++ b/CMakeLists.txt - 3 @@ -380,9 +380,22 @@ - 4 -include precompiled.h - 5 ) - 7 -add_executable(systemshock - 8 - ${MAC_SRC} - 9 -) - 10 +# Android has no standalone executables (Java loads a shared li - brary via - … +50 lines (ctrl+o to expand) - - Ran 2 shell commands (ctrl+o to expand) - Ran 1 shell command (ctrl+o to expand) - -● Write(android/build.gradle) - ⎿  Wrote 25 lines to android/build.gradle - 1 // Top-level Gradle build file. Adapted from SDL2's own android- - 2 // template (see app/src/main/java/org/libsdl/app/, copied from - the same - 3 // template). Unlike the C/SDL2 desktop build, this needs networ - 4 // at build time for Gradle/AGP's own dependency resolution - se - e - 6 buildscript { - 7 repositories { - 8 mavenCentral() - 10 } - … +18 lines (ctrl+o to expand) - -● Write(android/settings.gradle) - ⎿  Wrote 2 lines to android/settings.gradle - 1 rootProject.name = "questshock" - 2 include ':app' - - Read 1 file (ctrl+o to expand) - -● Write(android/gradle.properties) - ⎿  Wrote 2 lines to android/gradle.properties - 1 org.gradle.jvmargs=-Xmx1536m - 2 android.useAndroidX=true - -● Write(android/app/build.gradle) - ⎿  Wrote 63 lines to android/app/build.gradle - 1 apply plugin: 'com.android.application' - 2 - 4 // ../engine-patches/) scratch copy of engine/, written by build - -apk.sh - 5 // since it's only known at build time, not something a checked- - 6 // build.gradle can hardcode. - 7 def engineProps = new Properties() - 8 file("${projectDir}/../engine.properties").withInputStream { eng - 9 def engineDir = engineProps.getProperty('engineDir') - 10 if (engineDir == null) { - … +67 lines (ctrl+o to expand) - -● Write(android/app/src/main/AndroidManifest.xml) - ⎿  Wrote 49 lines to android/app/src/main/AndroidManifest.xml - 1 - oid" - 3 android:installLocation="auto"> - 4 - 6 - 7 - droid:required="false" /> - 9 - d:required="false" /> - … +56 lines (ctrl+o to expand) - -● Write(android/app/src/main/res/values/strings.xml) - ⎿  Wrote 4 lines to android/app/src/main/res/values/strings.xml - 1 - 2 - 3 System Shock - 4 - - Ran 2 shell commands (ctrl+o to expand) - -● Write(android/app/src/main/java/de/ladkau/questshock/QuestShockActivity.java) - ⎿  Wrote 135 lines to - android/app/src/main/java/de/ladkau/questshock/QuestShockActivity.java - 1 package de.ladkau.questshock; - 2 - 3 import android.Manifest; - 4 import android.content.pm.PackageManager; - 5 import android.content.res.AssetManager; - 6 import android.os.Bundle; - 7 import android.system.Os; - 8 import android.util.Log; - 9 import androidx.core.app.ActivityCompat; - 10 import java.io.File; - … +146 lines (ctrl+o to expand) - - Read 1 file (ctrl+o to expand) - -● Update(android/app/build.gradle) - ⎿  Added 8 lines - 61 abortOnError false - 62 } - 63 } - 64 + - 65 +dependencies { - 66 + // ActivityCompat.checkSelfPermission/requestPermissions in - 67 + // QuestShockActivity - the plain android.app.Activity APIs - + work the - 68 + // same way without androidx, but ActivityCompat is the sta - +ndard, - 69 + // documented way to request runtime permissions safely. - 70 + implementation 'androidx.core:core:1.12.0' - 71 +} - -● Write(res/assets/GET_ASSETS_QUEST.txt) - ⎿  Wrote 18 lines to res/assets/GET_ASSETS_QUEST.txt - 1 This app does not include System Shock's game data - it's copyrig - 2 proprietary content that can't be redistributed. To play, you nee - d a - 4 - 5 From your Enhanced Edition install, you need its classic-game dat - 6 sound files - the res/pc/hd/data and res/pc/cdrom/data trees merg - ed - in - 8 both: intro.res, objprop.dat), and the res/pc/hd/sound tree, pack - 9 inside the install's sshock.kpf (a zip file). - … +11 lines (ctrl+o to expand) - -● Now let's write build-apk.sh — the script that assembles everything - (scratch-patches engine/, wires up build_ext, stages APK assets, runs Gradle). - -● Write(build-image/build-apk.sh) - ⎿  Wrote 61 lines to build-image/build-apk.sh - 1 #!/usr/bin/env bash - see - 3 # android/engine-patches/ - engine/ itself is never modified) pl - 4 # Android SDL2/SDL2_mixer/fluidsynth-lite prebuilt into this ima - ge at - if - 6 # already inside this image - see the Makefile's `apk` target). - 8 # Unlike build-engine.sh, this step needs network access: Gradle - /AGP's - 10 # build-image/Dockerfile's comment on that trade-off). - … +67 lines (ctrl+o to expand) - -● Update(build-image/build-apk.sh) - ⎿  Added 1 line, removed 4 lines - 27 cp -a /opt/prebuilt/android/fluidsynth-lite/lib/. "$SCRATCH_ENG - INE/build_ext/fluidsynth-lite/src/" - 28 cp -a /opt/prebuilt/android/fluidsynth-lite/include/. "$SCRATCH - _ENGINE/build_ext/fluidsynth-lite/include/" - 29 - 30 -echo "$SCRATCH_ENGINE" > /dev/null # (documents intent; engineD - -ir is written below) - 31 -{ - 32 - echo "engineDir=$SCRATCH_ENGINE" - 33 -} > "$ANDROID_DIR/engine.properties" - 30 +echo "engineDir=$SCRATCH_ENGINE" > "$ANDROID_DIR/engine.propert - +ies" - 31 - 32 echo "== Staging bundled assets (shaders, soundfont, get-assets - text) ==" - 33 ASSETS_DIR="$ANDROID_DIR/app/src/main/assets" - Read 1 file, ran 1 shell command (ctrl+o to expand) - -● Update(Makefile) - ⎿  Added 1 line, removed 1 line - 14 BUILD_DIR := build - 15 ARCH := $(shell uname -m) - 16 - 17 -.PHONY: all dist build-image engine assets package clean - 17 +.PHONY: all dist build-image engine assets package apk clean - 18 - 19 all: dist - 20 - -❯ the build image before the android work worked btw and the assets were - produced in dl.ladkau.de - -● Great, glad the Node.js fix cleared CI end to end. That means version 1 is now - confirmed-good for its current (pre-Android) content — consistent with - keeping it at 1 while I finish this Android layer, then bumping to 2 once I've - validated the new build locally, per the saved policy. Continuing. - -● Update(Makefile) - ⎿  Added 16 lines, removed 1 line - 107 rm -rf "$(BUILD_DIR)/package"; \ - 108 echo "Wrote $(DIST_DIR)/$$PKG_NAME.tar.gz" - 109 - 110 +# Builds the Quest APK (see build-image/build-apk.sh and - 111 +# android/engine-patches/ - engine/ itself is never modified; - +a patch is - 112 +# applied to a scratch copy at build time instead). Same - 113 +# QUESTSHOCK_BUILD_IMAGE detection as `engine`. Unlike every o - +ther target - 114 +# here, this needs network access at build time (Gradle/AGP's - +own - 115 +# dependency resolution) - see build-image/Dockerfile. - 116 +apk: - 117 + @if [ -n "$$QUESTSHOCK_BUILD_IMAGE" ]; then \ - 118 + bash build-image/build-apk.sh; \ - 119 + else \ - 120 + ./run-image.sh bash build-image/build-apk.sh; \ - 121 + fi - 122 + - 123 clean: - 124 rm -rf "$(DIST_DIR)" "$(BUILD_DIR)" "$(ENGINE_OUT)" \ - 125 engine/build_ext engine/CMakeCache.txt engine/CMakeFiles \ - 126 engine/cmake_install.cmake engine/Makefile engine/systemsh - ock \ - 114 - engine/src/Libraries/CMakeFiles - 127 + engine/src/Libraries/CMakeFiles \ - 128 + android/engine.properties android/app/src/main/assets andr - +oid/app/src/main/jniLibs \ - 129 + android/app/build android/.gradle android/app/.cxx - -● Now let's update .gitignore for the new generated Android build artifacts. - - Read 1 file (ctrl+o to expand) - -● Update(.gitignore) - ⎿  Added 10 lines - 17 /engine/systemshock - 18 /engine/src/Libraries/CMakeFiles/ - 19 - 20 +# Android build artifacts (generated by build-image/build-apk.s - +h - see - 21 +# android/engine-patches/ for why engine/ itself is never touch - +ed) - 22 +/android/engine.properties - 23 +/android/app/src/main/assets/ - 24 +/android/app/src/main/jniLibs/ - 25 +/android/app/build/ - 26 +/android/.gradle/ - 27 +/android/app/.cxx/ - 28 +/android/local.properties - 29 + - 30 # Local registry credentials (see registry.env.example) - 31 /registry.env - - Read 1 file (ctrl+o to expand) - -● Update(README.md) - ⎿  Added 7 lines, removed 1 line - 23 - `Makefile` - assembles `dist/`, a self-contained runnable cop - y of the - 24 game, out of the compiled engine and the extracted assets. Al - so builds - 25 `dist/questshock--linux-.tar.gz`, a redistribu - table - 26 - package that omits the proprietary game assets (`make package - -`). - 26 + package that omits the proprietary game assets (`make package - +`), and - 27 + `dist/questshock-debug.apk` for the Quest (`make apk`). - 28 +- `android/` - the Quest app (Java `SDLActivity` glue, Gradle p - +roject). - 29 + `android/engine-patches/` holds the one small patch needed to - + build - 30 + `engine/` as an Android shared library instead of a desktop e - +xecutable - 31 + - applied to a scratch copy at build time; `engine/` itself i - +s never - 32 + modified. - 33 - 34 ## Building - 35 - -● Update(README.md) - ⎿  Added 21 lines - 78 extra setup is needed in CI beyond the image itself), and publ - ishes the - 79 resulting tarball to dl.ladkau.de. - 80 - 81 +## Playing on Meta Quest - 82 + - 83 +`make apk` builds `dist/questshock-debug.apk` - a plain (non-V - +R) Android - 84 +app that runs as a flat, floating panel in the Quest's Home en - +vironment, - 85 +same as any other sideloaded Android app. It's not a head-trac - +ked 6DoF VR - 86 +port (that's a much larger, separate undertaking); play with a - + Bluetooth - 87 +mouse/keyboard connected to the headset. - 88 + - 89 +1. Install the APK with [SideQuest](https://sidequestvr.com/) - +(or `adb - 90 + install`). - 91 +2. Launch it once. It'll ask for storage permission, then crea - +te - 92 + `/sdcard/questshock/` and extract its own bundled files (sh - +aders, a - 93 + default MIDI soundfont) there - `res/data/` and `res/sound/ - +` are - 94 + deliberately left missing, since that's the proprietary gam - +e data. - 95 +3. With the Quest connected to a PC, use SideQuest's file brow - +ser (or any - 96 + MTP file manager) to copy your own `res/data/` and `res/sou - +nd/` (see - 97 + `/sdcard/questshock/GET_ASSETS_QUEST.txt`, extracted in ste - +p 2, for - 98 + exactly what's needed and where it comes from) into - 99 + `/sdcard/questshock/res/`. - 100 +4. Launch it again. - 101 + - 102 ## License - 103 - 104 The original tooling in this repository (the Docker build imag - e, build - - Read 1 file (ctrl+o to expand) - -● Update(README.md) - ⎿  Added 7 lines, removed 2 lines - 102 ## License - 103 - 104 The original tooling in this repository (the Docker build imag - e, build - 105 -scripts, Makefile, and asset extraction script) is licensed un - -der the - 106 -[MIT License](LICENSE). - 105 +scripts, Makefile, asset extraction script, and the Quest app - +in - 106 +`android/` - aside from `org/libsdl/app/`, see below) is licen - +sed under - 107 +the [MIT License](LICENSE). - 108 - 109 +`android/app/src/main/java/org/libsdl/app/` is copied from - 110 +[SDL2](https://www.libsdl.org/)'s own android-project template - + and is - 111 +zlib-licensed, same as SDL2 itself. - 112 + - 113 The vendored engine snapshot in `engine/` is - 114 [Shockolate](https://github.com/Interrupt/systemshock), which - is licensed - 115 under the **GNU GPLv3** (see `engine/LICENSE`) - it is include - d unchanged - Ran 1 shell command (ctrl+o to expand) - -● This will take a while (Android SDK/NDK downloads + three arm64 - cross-compiles). I'll wait for it rather than poll. - ⎿  Error: `prompt` is required when `stop` is not true. - -● That tool is specific to /loop mode, not needed here — the background task - will notify me directly when it finishes. I'll wait for that. - -✻ Cooked for 22m 58s - -❯ is the build image being rebuild? - -● Yes, it's running in the background right now. Let me check how far it's - gotten. - - Read 1 file, ran 1 shell command (ctrl+o to expand) - -● Yes — it's actively building. Docker's currently running the Android SDK/NDK - setup step (I can see a container executing that stage). The full build - (SDK/NDK download + three arm64 cross-compiles: SDL2, SDL2_mixer, - fluidsynth-lite) will take a while; I'll let it run and report back once it - completes. - -✻ Cogitated for 1m 7s - -❯ for such long running downloads it would be good to have them in layers which - don't need to be regenerated every time the image needs to be rebuild - - Ran 1 shell command (ctrl+o to expand) - -● Good call — since I'll likely need to iterate on the Android layer - specifically, let me consolidate the two separate apt-get install blocks into - one at the top, so future edits to the Android portion never invalidate the - (already-validated) desktop build layers or force a redownload of everything - from scratch. This won't affect the build currently running (Docker already - snapshotted the Dockerfile when it started). - - Read 1 file (ctrl+o to expand) - -● Update(build-image/Dockerfile) - ⎿  Added 10 lines - 60 # libogg-dev/libvorbis-dev: SDL2_mixer's OGG Vorbis music decod - er. - 61 # libasound2-dev: engine/CMakeLists.txt's optional native ALSA - MIDI output. - 62 # openssh-client: the CI workflow's `sftp` publish step. - 63 +# openjdk-17-jdk-headless: Gradle/AGP's own minimum JDK for the - + APK build. - 64 +# unzip: extracts the Android cmdline-tools zip below. - 65 +# - 66 +# All apt installs deliberately live in this one RUN, first, so - + editing - 67 +# anything below it (in particular the Android cross-compile st - +eps, the - 68 +# newest and least settled part of this file) never invalidates - + Docker's - 69 +# build cache for this layer - and vice versa, adding a package - + here only - 70 +# ever costs a rebuild of the (slow) layers below it, not a re- - +download - 71 +# of packages that didn't change. - 72 RUN apt-get update && apt-get install -y --no-install-recommend - s \ - 73 build-essential cmake make git curl ca-certificates pkg - -config gosu \ - 74 libgl1-mesa-dev libglx-dev libxext-dev libx11-dev libxr - andr-dev \ - 75 libxi-dev libxfixes-dev libxss-dev libxinerama-dev libx - cursor-dev \ - 76 libogg-dev libvorbis-dev libasound2-dev openssh-client - \ - 77 + openjdk-17-jdk-headless unzip \ - 78 && rm -rf /var/lib/apt/lists/* - 79 - 80 WORKDIR /opt/prebuilt - -● Update(build-image/Dockerfile) - ⎿  Removed 6 lines - 124 && tar -xJf /tmp/node.tar.xz -C /usr/local --strip-compone - nts=1 \ - 125 && rm /tmp/node.tar.xz - 126 - 127 -# openjdk-17-jdk-headless: Gradle/AGP's own minimum JDK for th - -e APK build. - 128 -# unzip: extracts the Android cmdline-tools zip below. - 129 -RUN apt-get update && apt-get install -y --no-install-recommen - -ds \ - 130 - openjdk-17-jdk-headless unzip \ - 131 - && rm -rf /var/lib/apt/lists/* - 132 - - 127 # Android SDK/NDK: the cmdline-tools' own sdkmanager installs - exactly the - 128 # platform/build-tools/NDK/CMake versions the app/build.gradle - below pins. - 129 # gradlew (invoked by build-apk.sh) resolves its own pinned Gr - adle version - - Read 1 file (ctrl+o to expand) - -● Good — this now looks clean. Let me check on the background build's progress. - - Ran 1 shell command (ctrl+o to expand) - -● Still working through the apt-get install step (openjdk-17-jdk-headless is a - large package). I'll keep waiting for the background task to notify me when it - completes. - -✻ Baked for 3m 37s - -❯ still building? - - Ran 1 shell command (ctrl+o to expand) - -● Yes, still building — it's progressed past the apt-get step and is now in a - mkdir -... step, which matches the Android SDK cmdline-tools download/install - stage. I'll keep waiting for the completion notification. - -✻ Brewed for 45s - -※ recap: Building the questshock repo to run System Shock on Quest headsets; currently rebuilding the Docker build image with the new Android SDK/NDK layer. Once it finishes, I'll run `make apk` and validate the resulting APK. (disable recaps in /config) - -❯ still building? - Ran 3 shell commands (ctrl+o to expand) - -● Confirmed still actively working — I checked the container's process list and - network I/O directly: it's running sdkmanager --install (fetching - platform-tools, platform 29, build-tools, the NDK, and cmake), and I can see - real download traffic flowing (~670MB+ received and climbing). The NDK alone - is often 1GB+, so this step can take a while depending on connection speed. - Not stuck — I'll keep waiting for it to finish. - -✻ Sautéed for 1m 46s - -● Background command "./build-image.sh 2>&1 | tail -150" completed (exit -code 0) - -● The build-image rebuild finished successfully. Let me check the output and - then run the actual APK build. - - Read 1 file (ctrl+o to expand) - -● Found the first real bug: SDL2_mixer 2.8's CMake uses its own private - SDL2-detection module that isn't picking up our custom-installed SDL2 path. - This is the classic Android NDK toolchain gotcha — CMAKE_FIND_ROOT_PATH_MODE_* - restricts find_path/find_library to the NDK sysroot by default, excluding our - custom prefix. Let me fix it. - - Read 1 file (ctrl+o to expand) - -● Update(build-image/Dockerfile) - ⎿  Added 4 lines - 184 -DCMAKE_TOOLCHAIN_FILE="${ANDROID_NDK_TOOLCHAIN}" \ - 185 -DANDROID_ABI="${ANDROID_ABI}" -DANDROID_PLATFORM="and - roid-${ANDROID_PLATFORM_VERSION}" \ - 186 -DCMAKE_PREFIX_PATH=/opt/prebuilt/android/sdl2 \ - 187 + -DCMAKE_FIND_ROOT_PATH=/opt/prebuilt/android/sdl2 \ - 188 + -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH \ - 189 + -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH \ - 190 + -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH \ - 191 -DCMAKE_INSTALL_PREFIX=/opt/prebuilt/android/sdl2_mixe - r -DBUILD_SHARED_LIBS=ON \ - 192 -DSDL2MIXER_VENDORED=OFF -DSDL2MIXER_SAMPLES=OFF -DSDL - 2MIXER_CMD=OFF \ - 193 -DSDL2MIXER_FLAC=OFF -DSDL2MIXER_GME=OFF -DSDL2MIXER_M - OD=OFF \ - -● Let me proactively apply the same fix to the app's own CMake args - (Shockolate's find_package(SDL2 REQUIRED) will hit the identical restriction - otherwise). - - Read 1 file (ctrl+o to expand) - -● Update(android/app/build.gradle) - ⎿  Added 11 lines, removed 1 line - 38 // convention the desktop build already uses (p - opulated in the - 39 // scratch engine copy by build-apk.sh from - 40 // /opt/prebuilt/android/*). - 41 + // The CMAKE_FIND_ROOT_PATH* overrides below ar - +e needed - 42 + // because the NDK toolchain file restricts fin - +d_package/ - 43 + // find_path/find_library to its own sysroot by - + default, - 44 + // which would otherwise miss our custom-instal - +led SDL2 (see - 45 + // build-image/Dockerfile's SDL2_mixer build, w - +hich hit the - 46 + // exact same thing). - 47 arguments "-DENABLE_SDL2=ON", "-DENABLE_SOUND=B - UNDLED", "-DENABLE_FLUIDSYNTH=BUNDLED", \ - 42 - "-DCMAKE_PREFIX_PATH=/opt/prebuilt/android/ - -sdl2" - 48 + "-DCMAKE_PREFIX_PATH=/opt/prebuilt/android/ - +sdl2", \ - 49 + "-DCMAKE_FIND_ROOT_PATH=/opt/prebuilt/andro - +id/sdl2", \ - 50 + "-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH", - + \ - 51 + "-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH", - + \ - 52 + "-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH" - 53 abiFilters 'arm64-v8a' - 54 } - 55 } - - Ran 1 shell command (ctrl+o to expand) - -● Rebuilding — this time it should reuse the cached layers up through the SDL2 - (Android) build, and only redo SDL2_mixer onward. I'll wait for it. - - Ran 1 shell command (ctrl+o to expand) - -● That's expected — since I just merged the two apt-get layers into one, this - exact command never ran before, so Docker can't reuse cache for it (one-time - cost from the consolidation). Everything after it that's unchanged (desktop - SDL2/SDL2_mixer/fluidsynth-lite build, Node.js, Android SDK/NDK) should still - be quick since those steps are all proven already. I'll wait for it to reach - the actual fix. - ⎿  Error: `prompt` is required when `stop` is not true. - -● That tool isn't applicable here (it's /loop-specific). I'll just wait for the - background task's own completion notification instead. - -✻ Baked for 3m 49s - -❯ is it still building? - - Ran 1 shell command (ctrl+o to expand) - -● Yes, still on the consolidated apt-get install step (installing - openjdk-17-jdk-headless plus the rest, which took a while last time too). I'll - keep waiting for the completion notification. - -✻ Brewed for 34s - -❯ still building? - - Ran 1 shell command (ctrl+o to expand) - -● Progress — it's moved past the apt-get step and is now on a curl step (started - 19 seconds ago), which matches the desktop - SDL2/SDL2_mixer/fluidsynth-lite/soundfont/Node.js download sequence. Still - going, I'll keep waiting. - -✻ Baked for 33s -● Background command "./build-image.sh 2>&1 | tail -150" completed (exit -code 0) - -● The background build finished. Let's check the tail of the log to see if the - SDL2_mixer fix worked and whether the whole image build succeeded. - - Read 1 file (ctrl+o to expand) - -● Another codec I missed — WavPack. Let me check the exact option name and - disable it too. - - Searched for 2 patterns, read 1 file (ctrl+o to expand) - -● Update(build-image/Dockerfile) - ⎿  Added 1 line - 192 -DSDL2MIXER_VENDORED=OFF -DSDL2MIXER_SAMPLES=OFF -DSDL - 2MIXER_CMD=OFF \ - 193 -DSDL2MIXER_FLAC=OFF -DSDL2MIXER_GME=OFF -DSDL2MIXER_M - OD=OFF \ - 194 -DSDL2MIXER_MP3=OFF -DSDL2MIXER_MIDI=OFF -DSDL2MIXER_O - PUS=OFF -DSDL2MIXER_VORBIS=OFF \ - 195 + -DSDL2MIXER_WAVPACK=OFF \ - 196 && cmake --build build-sdl2mixer-android -j"$(nproc)" \ - 197 && cmake --install build-sdl2mixer-android \ - 198 && rm -rf "SDL2_mixer-${ANDROID_SDL2_MIXER_VERSION}" "SDL2 - _mixer-${ANDROID_SDL2_MIXER_VERSION}.tar.gz" build-sdl2mixer-a - ndroid - - Ran 1 shell command (ctrl+o to expand) - -● Rebuilding again — this time everything up through the SDL2 (Android) build - should hit cache, so it should reach the SDL2_mixer step quickly. I'll wait - for completion. - -✻ Brewed for 1m 53s - -● Background command "./build-image.sh 2>&1 | tail -150" completed (exit -code 0) - - Read 1 file (ctrl+o to expand) - -● Fluidsynth-lite compiles fine under desktop GCC but hits two Clang-specific - warnings-as-errors (-Werror is baked into its own CMakeLists). Since we - already sed-patch this exact file (to force the shared-lib option), extending - that same approach to relax -Werror for the Android/Clang build is consistent - — and it doesn't touch engine/ at all. - - Read 1 file (ctrl+o to expand) - -● Update(build-image/Dockerfile) - ⎿  Added 1 line - 201 && cd fluidsynth-lite-android \ - 202 && git checkout "${FLUIDSYNTH_LITE_REF}" \ - 203 && sed -i 's/DLL"\ off/DLL"\ on/' CMakeLists.txt \ - 204 + && sed -i 's/-Wall -Werror -std=gnu11/-Wall -std=gnu11/' C - +MakeLists.txt \ - 205 && rm -rf .git \ - 206 && cd .. \ - 207 && cmake -S fluidsynth-lite-android -B build-fluidsynth-an - droid \ - - Ran 1 shell command (ctrl+o to expand) - -● Rebuilding again — should be quick now, reusing cache up through the - SDL2_mixer step. I'll wait for it. - -✻ Baked for 1m 37s - -● Background command "./build-image.sh 2>&1 | tail -150" completed (exit -code 0) - - Read 1 file (ctrl+o to expand) - -● Another Clang/NDK-specific issue: modern Android NDKs no longer ship a - libpthread.so stub (pthread is built into bionic libc directly), but - fluidsynth-lite's CMakeLists unconditionally links -lpthread. Same sed-patch - approach as before. - -● Update(build-image/Dockerfile) - ⎿  Added 1 line - 202 && git checkout "${FLUIDSYNTH_LITE_REF}" \ - 203 && sed -i 's/DLL"\ off/DLL"\ on/' CMakeLists.txt \ - 204 && sed -i 's/-Wall -Werror -std=gnu11/-Wall -std=gnu11/' C - MakeLists.txt \ - 205 + && sed -i 's/set ( LIBFLUID_LIBS "m pthread" )/set ( LIBFL - +UID_LIBS "m" )/' CMakeLists.txt \ - 206 && rm -rf .git \ - 207 && cd .. \ - 208 && cmake -S fluidsynth-lite-android -B build-fluidsynth-an - droid \ - -● Let me verify the exact string format before rebuilding, since a sed mismatch - would silently no-op. - - Searched for 1 pattern (ctrl+o to expand) - -● Update(build-image/Dockerfile) - ⎿  Added 1 line, removed 1 line - 202 && git checkout "${FLUIDSYNTH_LITE_REF}" \ - 203 && sed -i 's/DLL"\ off/DLL"\ on/' CMakeLists.txt \ - 204 && sed -i 's/-Wall -Werror -std=gnu11/-Wall -std=gnu11/' C - MakeLists.txt \ - 205 - && sed -i 's/set ( LIBFLUID_LIBS "m pthread" )/set ( LIBFL - -UID_LIBS "m" )/' CMakeLists.txt \ - 205 + && sed -i 's/set ( LIBFLUID_LIBS m pthread )/set ( LIBFLUI - +D_LIBS m )/' CMakeLists.txt \ - 206 && rm -rf .git \ - 207 && cd .. \ - 208 && cmake -S fluidsynth-lite-android -B build-fluidsynth-an - droid \ - - Ran 1 shell command (ctrl+o to expand) - -● Rebuilding once more with the corrected sed pattern. Waiting for it. -✻ Cooked for 1m 54s - -● Background command "./build-image.sh 2>&1 | tail -150" completed (exit -code 0) - - Read 1 file (ctrl+o to expand) - -● The full image build succeeded — all three Android cross-compiles (SDL2, - SDL2_mixer, fluidsynth-lite) completed cleanly this time. Now let's actually - run make apk. - - Ran 2 shell commands (ctrl+o to expand) - -● Also worth fixing proactively: fluidsynth/version.h is generated by CMake at - build time (version.h.in), and since desktop does an in-source build it lands - right in the source tree, but my Android build is out-of-source — the - generated header would be missing from what I copy out. Let me fix that before - the next rebuild. - - Searched for 1 pattern, read 1 file (ctrl+o to expand) - -● Update(build-image/Dockerfile) - ⎿  Added 6 lines - 212 && mkdir -p /opt/prebuilt/android/fluidsynth-lite/lib /opt - /prebuilt/android/fluidsynth-lite/include \ - 213 && cp -a build-fluidsynth-android/src/libfluidsynth.so* /o - pt/prebuilt/android/fluidsynth-lite/lib/ \ - 214 && cp -a fluidsynth-lite-android/include/. /opt/prebuilt/a - ndroid/fluidsynth-lite/include/ \ - 215 + # version.h is generated from version.h.in at configure ti - +me - fine on - 216 + # the desktop build (in-source, so it lands right back in - +the source - 217 + # tree), but this is an out-of-source build, so it only ex - +ists under - 218 + # build-fluidsynth-android/ and must be copied in separate - +ly. - 219 + && cp -a build-fluidsynth-android/include/fluidsynth/versi - +on.h \ - 220 + /opt/prebuilt/android/fluidsynth-lite/include/fluidsyn - +th/version.h \ - 221 && rm -rf fluidsynth-lite-android build-fluidsynth-android - 222 - 223 COPY build-image/docker-entrypoint.sh /usr/local/bin/docker-en - trypoint.sh - - Ran 1 shell command (ctrl+o to expand) - -● Rebuilding with the version.h fix. Waiting for it. - -✻ Sautéed for 2m 33s - -● Background command "./build-image.sh 2>&1 | tail -60" completed (exit -code 0) - - Read 1 file (ctrl+o to expand) - -● Image build succeeded again. Now let's actually run make apk for real. - - Ran 1 shell command (ctrl+o to expand) - -● This is the big one — it'll patch engine/, wire up build_ext, stage - assets/jniLibs, and run ./gradlew assembleDebug (which needs network access, - as expected). Waiting for it. - -✻ Baked for 47s - -● Background command "make apk 2>&1 | tail -150" completed (exit code 0) - - Read 1 file (ctrl+o to expand) - -● Found it: AGP defaults to its own preferred NDK version (25.x) unless told - otherwise, and tries to auto-download it into the (root-owned, - non-writable-by-builder) SDK dir. Need to pin ndkVersion explicitly to match - the NDK we actually installed. - - Read 1 file (ctrl+o to expand) - -● Update(android/app/build.gradle) - ⎿  Added 4 lines - 14 android { - 15 namespace "de.ladkau.questshock" - 16 compileSdk 34 - 17 + // Must match build-image/Dockerfile's ANDROID_NDK_VERSION - +- otherwise - 18 + // AGP defaults to whatever NDK version it itself prefers a - +nd tries to - 19 + // download it into the SDK dir, which isn't writable at bu - +ild time. - 20 + ndkVersion "26.1.10909125" - 21 - 22 defaultConfig { - 23 applicationId "de.ladkau.questshock" - - Ran 1 shell command (ctrl+o to expand) - -● Waiting for this run (it'll re-download Gradle itself since the container is - ephemeral — that's a known, separate inefficiency I'll flag later, not - blocking right now). - -✻ Sautéed for 1m 29s - -● Background command "make apk 2>&1 | tail -150" completed (exit code 0) - - Read 1 file (ctrl+o to expand) - -● Same root cause again — AGP wants platforms;android-34 (matching compileSdk - 34) and its own default build-tools version (33.0.1, not the 34.0.0 I - installed), and tries to auto-fetch both into the non-writable SDK dir. Need - to install platform 34 too and pin buildToolsVersion explicitly. - - Read 1 file (ctrl+o to expand) -● Update(build-image/Dockerfile) - ⎿  Added 4 lines - 39 ARG ANDROID_SDL2_MIXER_VERSION=2.8.0 - 40 ARG ANDROID_CMDLINE_TOOLS_VERSION=11076708 - 41 ARG ANDROID_PLATFORM_VERSION=29 - 42 +# compileSdk in android/app/build.gradle - separate from the ap - +p's own - 43 +# targetSdkVersion (29, above): compileSdk is just which androi - +d.jar the - 44 +# app compiles against, and androidx.core needs a newer one tha - +n 29. - 45 +ARG ANDROID_COMPILE_SDK_VERSION=34 - 46 ARG ANDROID_BUILD_TOOLS_VERSION=34.0.0 - 47 ARG ANDROID_NDK_VERSION=26.1.10909125 - 48 ARG ANDROID_CMAKE_VERSION=3.22.1 - -● Update(build-image/Dockerfile) - ⎿  Added 1 line - 150 && sdkmanager --install \ - 151 "platform-tools" \ - 152 "platforms;android-${ANDROID_PLATFORM_VERSION}" \ - 153 + "platforms;android-${ANDROID_COMPILE_SDK_VERSION}" \ - 154 "build-tools;${ANDROID_BUILD_TOOLS_VERSION}" \ - 155 "ndk;${ANDROID_NDK_VERSION}" \ - 156 "cmake;${ANDROID_CMAKE_VERSION}" - -● Now pin buildToolsVersion explicitly in app/build.gradle to match what's - installed. - -● Update(android/app/build.gradle) - ⎿  Added 5 lines, removed 3 lines - 13 - 14 android { - 15 namespace "de.ladkau.questshock" - 16 + // compileSdk/buildToolsVersion/ndkVersion must all match w - +hat - 17 + // build-image/Dockerfile actually installs - otherwise AGP - + defaults to - 18 + // whatever version it itself prefers and tries to download - + it into the - 19 + // SDK dir at build time, which isn't writable. - 20 compileSdk 34 - 17 - // Must match build-image/Dockerfile's ANDROID_NDK_VERSION - -- otherwise - 18 - // AGP defaults to whatever NDK version it itself prefers a - -nd tries to - 19 - // download it into the SDK dir, which isn't writable at bu - -ild time. - 21 + buildToolsVersion "34.0.0" - 22 ndkVersion "26.1.10909125" - 23 - 24 defaultConfig { - - Ran 1 shell command (ctrl+o to expand) - -● Rebuilding with platform 34 added. This will cascade-rebuild the SDK/NDK layer - and everything after it. Waiting. - -✻ Worked for 2m 47s - -❯ is it still building? - - Ran 1 shell command (ctrl+o to expand) - -● Yes, still building — currently on the Android SDK/NDK install step (mkdir - -...), which now needs to fetch platform 34 in addition to what it had before. - I'll keep waiting for completion. - -✻ Churned for 42s - -● Background command "./build-image.sh 2>&1 | tail -60" completed (exit -code 0) - - Read 1 file (ctrl+o to expand) - -● Image build succeeded. Now let's try make apk again. - - Ran 1 shell command (ctrl+o to expand) - -● Waiting for this run. - -✻ Churned for 52s - -● Background command "make apk 2>&1 | tail -150" completed (exit code 0) - - Read 1 file (ctrl+o to expand) - -● Now hitting a real engine/CMakeLists.txt platform gap: find_package(OpenGL - REQUIRED) looks for desktop OpenGL (GLX), which doesn't exist on Android - (which uses GLES/EGL instead). This needs a genuine patch to - engine/CMakeLists.txt — exactly the scenario the patch mechanism was built - for. Let me look at the exact block. - - Searched for 1 pattern, read 1 file (ctrl+o to expand) - -● Let me restructure engine-patches/ to support multiple ordered patches (since - this is a second, separate concern from the shared-lib change), then add this - one. - - Made 1 scratchpad edit +15 -9, ran 3 shell commands (ctrl+o to expand) - -● Let's verify both patches apply cleanly in sequence, then save it. - - Ran 1 shell command (ctrl+o to expand) - -● Both apply cleanly in sequence. Let's save the patch and update build-apk.sh - to apply all patches in the directory. - -● Write(android/engine-patches/02-android-opengl-es.patch) - ⎿  Wrote 20 lines to android/engine-patches/02-android-opengl-es.patch - 1 --- a/CMakeLists.txt - 2 +++ b/CMakeLists.txt - 3 @@ -46,9 +46,15 @@ - 5 add_compile_options(-fsigned-char -fno-strict-aliasing) - 6 - 7 -# Find OpenGL - 8 +# Find OpenGL. Android has no desktop GL/GLX for CMake's FindOp - 9 +# to find - just link the NDK's own GLESv2/EGL libraries direct - ly. - 10 if(ENABLE_OPENGL) - … +12 lines (ctrl+o to expand) - -● Now let's verify against the actually-committed patch file (not the scratchpad - copy) and update build-apk.sh to apply all patches from the directory in - order. - - Ran 1 shell command (ctrl+o to expand) - -● Both apply cleanly from the committed files, in glob order. Now let's update - build-apk.sh to apply all patches in the directory. - - Read 1 file (ctrl+o to expand) -● Update(build-image/build-apk.sh) - ⎿  Added 3 lines, removed 1 line - 18 rm -rf "$SCRATCH_ENGINE" - 19 mkdir -p "$(dirname "$SCRATCH_ENGINE")" - 20 cp -a "$REPO_ROOT/engine" "$SCRATCH_ENGINE" - 21 -patch -p1 -d "$SCRATCH_ENGINE" < "$ANDROID_DIR/engine-patches/a - -ndroid-shared-lib.patch" - 21 +for p in "$ANDROID_DIR"/engine-patches/*.patch; do - 22 + patch -p1 -d "$SCRATCH_ENGINE" < "$p" - 23 +done - 24 - 25 echo "== Wiring up prebuilt Android SDL2_mixer/fluidsynth-lite - (BUNDLED mode, like the desktop build) ==" - 26 mkdir -p "$SCRATCH_ENGINE/build_ext/built_sdl_mixer" "$SCRATCH_ - ENGINE/build_ext/fluidsynth-lite" - -● No image rebuild needed this time — build-apk.sh and the patches run from the - bind-mounted repo, not baked into the image. Let's just re-run make apk. - - Ran 1 shell command (ctrl+o to expand) - -● Waiting for this run. - -✻ Sautéed for 4m 27s - -● Background command "make apk 2>&1 | tail -150" completed (exit code 0) - - Read 1 file (ctrl+o to expand) - -● Web Search("android.system.Os chdir method public API android") - ⎿  Did 1 search in 7s - -● Let me check directly what's actually in the API 34 stub jar, rather than - relying on possibly-wrong memory. - - Ran 1 shell command (ctrl+o to expand) - -● android.system.Os genuinely has no chdir (confirmed against the real API 34 - stub jar — Java has no public API for process-wide chdir at all). I need a - tiny bit of native code after all, but it stays entirely in android/ — never - touching engine/. Let me add it. - -● Write(android/app/src/main/cpp/questshock_native.c) - ⎿  Wrote 15 lines to android/app/src/main/cpp/questshock_native.c - 1 // android.system.Os has no public chdir() (confirmed against th - 2 // API 34 stub jar - Java has no way to change a process's worki - ng - 3 // directory at all). This is the one bit of native code QuestSh - 4 // needs, compiled into the same "main" library as engine/ (see - 5 // android/engine-patches/01-android-shared-lib.patch's ANDROID_ - 6 // - engine/ itself is never modified. - 7 #include - 8 #include - 10 JNIEXPORT void JNICALL - … +9 lines (ctrl+o to expand) - -● Now let's regenerate patch 01 to append ANDROID_EXTRA_SOURCES to the library - sources. - - Made 2 scratchpad edits +35 -16, ran 2 shell commands (ctrl+o to expand) - -● Let's verify all three patches apply cleanly in sequence and save the updated - patch 01. - - Ran 1 shell command (ctrl+o to expand) - -● Write(android/engine-patches/01-android-shared-lib.patch) - ⎿  Error writing file - - Read 1 file (ctrl+o to expand) - -● Now I can write it. - -● Write(android/engine-patches/01-android-shared-lib.patch) - ⎿  Added 7 lines, removed 4 lines - 1 --- a/CMakeLists.txt - 2 +++ b/CMakeLists.txt - 3 -@@ -380,9 +380,22 @@ - 3 +@@ -380,9 +380,25 @@ - 4 -include precompiled.h - 5 ) - 6 - ... - 11 +# JNI instead), so this is the one part of this file Android n - eeds to - 12 +# override - see android/engine-patches/. Everything else here - (sources, - 13 +# dependency discovery, link libraries) is untouched and appli - es the same - 14 -+# way on both platforms. - 14 ++# way on both platforms. ANDROID_EXTRA_SOURCES (passed in by - 15 ++# android/app/build.gradle) is QuestShockActivity's own small - +native - 16 ++# helper (android/app/src/main/cpp/) - not part of engine/ its - +elf. - 17 +if(ANDROID) - 18 + set(SHOCKOLATE_TARGET main) - 19 + add_library(${SHOCKOLATE_TARGET} SHARED - 20 + ${MAC_SRC} - 21 ++ ${ANDROID_EXTRA_SOURCES} - 22 + ) - 23 +else() - 24 + set(SHOCKOLATE_TARGET systemshock) - ... - 29 - 30 add_library(GAME_LIB ${GAME_SRC}) - 31 - 29 -@@ -391,7 +404,7 @@ - 32 +@@ -391,7 +407,7 @@ - 33 set(WINDOWS_LIBRARIES "mingw32 -mwindows") - 34 endif(MINGW) - 35 - ... - 38 ${WINDOWS_LIBRARIES} # Set it before any linker options! Bewar - e WinMain@16 error!! - 39 GAME_LIB - 40 UI_LIB - 38 -@@ -418,7 +431,10 @@ - 41 +@@ -418,7 +434,10 @@ - 42 ${ALSA_LIBRARIES} - 43 ) - 44 - - Ran 1 shell command (ctrl+o to expand) - -● Now let's update QuestShockActivity.java to load the native library early and - call nativeChdir instead of Os.chdir, and update build.gradle to pass - ANDROID_EXTRA_SOURCES. - - Read 1 file (ctrl+o to expand) - -● Update(android/app/src/main/java/de/ladkau/questshock/QuestShockActivity.java) - ⎿  Removed 1 line - 4 import android.content.pm.PackageManager; - 5 import android.content.res.AssetManager; - 6 import android.os.Bundle; - 7 -import android.system.Os; - 7 import android.util.Log; - 8 import androidx.core.app.ActivityCompat; - 9 import java.io.File; - -● Update(android/app/src/main/java/de/ladkau/questshock/QuestShockActivity.java) - ⎿  Added 16 lines - 28 private static final int PERMISSION_REQUEST_STORAGE = 1; - 29 private static final String GAME_DIR = "/sdcard/questshock" - ; - 30 - 31 + // Loaded here (rather than waiting for SDLActivity's own - 32 + // getLibraries()/loadLibraries(), which only runs from sup - +er.onCreate()) - 33 + // purely to get nativeChdir() below - Android's dynamic li - +nker resolves - 34 + // "main"'s own dependencies (SDL2, SDL2_mixer, fluidsynth) - + from the - 35 + // APK's native library directory regardless of Java-side l - +oad order, so - 36 + // loading it early here is safe. SDLActivity loading "main - +" again later - 37 + // is a harmless no-op (System.loadLibrary is idempotent pe - +r - 38 + // ClassLoader). - 39 + static { - 40 + System.loadLibrary("main"); - 41 + } - 42 + - 43 + // See android/app/src/main/cpp/questshock_native.c - andro - +id.system.Os - 44 + // has no public chdir() (confirmed against the actual API - +34 stub jar). - 45 + private static native void nativeChdir(String path); - 46 + - 47 private Bundle mSavedInstanceState; - 48 - 49 @Override - -● Update(android/app/src/main/java/de/ladkau/questshock/QuestShockActivity.java) - ⎿  Added 4 lines, removed 9 lines - 96 copyAssetFile("GET_ASSETS_QUEST.txt", marker); - 97 } - 98 - 99 - try { - 100 - // chdir() is process-wide, not per-thread - alrea - -dy in effect for - 101 - // every thread (including the one that will run S - -hockolate's own - 102 - // SDL_main) by the time super.onCreate() below lo - -ads the native - 103 - // libraries and starts it. - 104 - Os.chdir(GAME_DIR); - 105 - } catch (Exception e) { - 106 - Log.e(TAG, "chdir(" + GAME_DIR + ") failed", e); - 107 - } - 99 + // chdir() is process-wide, not per-thread - already i - +n effect for - 100 + // every thread (including the one that will run Shock - +olate's own - 101 + // SDL_main) by the time super.onCreate() below starts - + it. - 102 + nativeChdir(GAME_DIR); - 103 - 104 super.onCreate(mSavedInstanceState); - 105 } -● Now update build.gradle to pass ANDROID_EXTRA_SOURCES. - - Read 1 file (ctrl+o to expand) - -● Update(android/app/build.gradle) - ⎿  Added 2 lines, removed 1 line - 55 "-DCMAKE_FIND_ROOT_PATH=/opt/prebuilt/andro - id/sdl2", \ - 56 "-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH", - \ - 57 "-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH", - \ - 58 - "-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH" - 58 + "-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH", - + \ - 59 + "-DANDROID_EXTRA_SOURCES=${projectDir}/src/ - +main/cpp/questshock_native.c" - 60 abiFilters 'arm64-v8a' - 61 } - 62 } - -● No image rebuild needed — all these changes live in the bind-mounted repo. - Let's re-run make apk. - - Ran 1 shell command (ctrl+o to expand) - -● Waiting for this run. - -✻ Worked for 7m 41s - -● Background command "make apk 2>&1 | tail -150" completed (exit code 0) - - Read 1 file (ctrl+o to expand) - -● This confirms the exact risk I flagged in the plan: - engine/src/MacSrc/OpenGL.cc uses desktop OpenGL headers (), which - don't exist on Android. Let me look at the actual GL usage to see how big a - patch this needs. - - Searched for 2 patterns, read 1 file (ctrl+o to expand) - -● Real progress on the build pipeline, but I've hit a genuine engine-level - compatibility gap, not just a build-config issue: OpenGL.cc mixes modern - shader-based rendering (GLES2-compatible: glCreateShader, glUniform*, etc.) - with legacy immediate-mode GL calls (glBegin/glEnd/glVertex3f, - glAlphaFunc/GL_ALPHA_TEST) that don't exist in GLES2 at all, plus a couple of - extensions not guaranteed on GLES2 (GL_CLAMP_TO_BORDER, GL_UNPACK_ROW_LENGTH). - The immediate-mode calls look like they're specifically the star-field - rendering (there's a star.frag shader and matching GL_POINTS/GL_POINT_SPRITE - usage) — a separate, older code path from the main shader-based renderer. - - Fixing the header/extension issues is mechanical (a few more lines in the same - patch). But the immediate-mode star rendering needs an actual rewrite to a - VBO+shader approach to work under GLES2 — that's real engine rendering code, - not build plumbing, and it's something I can't visually verify without a - device or working GPU-accelerated emulator (neither of which I have here). - -● User answered Claude's questions: - ⎿  · How do you want to handle the immediate-mode star rendering - (glBegin/glEnd/glVertex3f + glAlphaFunc), which needs an actual GLES2 - rewrite, not just a build-config patch? → Attempt the rewrite now - (Recommended) - - Searched for 1 pattern (ctrl+o to expand) - -● This is more extensive than just the stars — immediate-mode - glBegin/glVertex3f/glEnd is actually the primary way all textured quads get - drawn throughout this file (not just the star field), since GLES2 removes the - entire immediate-mode API. Let me read the full file to do this rewrite - properly rather than guessing at call sites. - - Read 1 file (ctrl+o to expand) - -● Before writing the replacement draw calls, I need to check main.vert — I need - to know whether it declares an explicit position attribute or relies on - desktop's fixed-function gl_Vertex builtin (which doesn't exist in GLES2 at - all). - - Read 1 file (ctrl+o to expand) - -● Now let me check the fragment shaders too, since GLES fragment shaders require - explicit precision declarations that desktop GLSL doesn't need. - - Ran 1 shell command (ctrl+o to expand) - -● Let me check where the SDL window/GL context attributes get set up, since I - may need GLES 3.0 (universally available on Quest hardware) to get - GL_UNPACK_ROW_LENGTH/GL_CLAMP_TO_BORDER support natively rather than working - around their absence in ES2. - - Searched for 1 pattern (ctrl+o to expand) - -● Targeting GLES 3.2 (Quest 2/3 hardware fully supports it) gives native - GL_UNPACK_ROW_LENGTH and GL_CLAMP_TO_BORDER support, eliminating two of the - compatibility issues entirely. Let me write the ES shader variants first. - - Ran 1 shell command (ctrl+o to expand) - -● Write(android/gles-shaders/main.vert) - ⎿  Wrote 28 lines to android/gles-shaders/main.vert - 1 #version 100 - 3 // GLES port of engine/shaders/main.vert - the one real differenc - e is - oesn't - 5 // exist in GLES at all, replaced with an explicit "position" att - 6 // (see android/engine-patches/03-android-opengl-es-render.patch' - s - to match - 8 // the vertex arrays OpenGL.cc's GLES immediate-mode emulation su - 9 precision mediump float; - … +23 lines (ctrl+o to expand) - -● Write(android/gles-shaders/color.frag) ⎿  Wrote 13 lines to android/gles-shaders/color.frag - 1 #version 100 - 2 - 3 // GLES port of engine/shaders/color.frag - identical logic, just - needs an - 4 // explicit precision declaration (required in GLES fragment shad - 5 // valid in desktop GLSL). - 6 precision mediump float; - 7 - 8 varying vec4 Color; - 9 varying float Light; - … +6 lines (ctrl+o to expand) - -● Write(android/gles-shaders/texture.frag) - ⎿  Wrote 49 lines to android/gles-shaders/texture.frag - 1 #version 100 - 3 // GLES port of engine/shaders/texture.frag - same logic, plus: - 4 // - an explicit precision declaration (required in GLES, invali - d in - 6 // - the discard at the end, replacing the fixed-function alpha - test - ed-function - 8 // pipeline at all, so this is done in the shader instead; see - 9 // android/engine-patches/03-android-opengl-es-render.patch's - unc/ - … +44 lines (ctrl+o to expand) - -● Write(android/gles-shaders/star.frag) - ⎿  Wrote 22 lines to android/gles-shaders/star.frag - 1 #version 100 - 3 // GLES port of engine/shaders/star.frag - identical logic, plus - an - 4 // explicit precision declaration (required in GLES). gl_PointCo - 5 // point-sprite rasterization for GL_POINTS both work automatica - lly in - SPRITE, - 7 // which doesn't exist in GLES at all - see - 8 // android/engine-patches/03-android-opengl-es-render.patch's - 10 precision mediump float; - … +16 lines (ctrl+o to expand) - -● Now let's write the patches. First, Shock.c's GL context attributes. - - Made 1 scratchpad edit +16 -4, read 1 file, ran 2 shell commands (ctrl+o to expand) - -● Need to fix the path prefix — let me regenerate with the correct relative path - structure. - - Ran 2 shell commands (ctrl+o to expand) - -● Now let's write the big OpenGL.cc patch — headers, disabling desktop-only - fixed-function calls, and the immediate-mode → vertex-array conversion. - - Made 1 scratchpad edit +111 -16, ran 2 shell commands (ctrl+o to expand) - -● Now let's edit CreateShader() to bind the position attribute, and - init_opengl() to skip the desktop-only fixed-function calls. - - Made 2 scratchpad edits +24 -11 (ctrl+o to expand) - -● Now let's generate the diff and verify it applies cleanly, then check it's - internally consistent (proper preprocessor balance etc.) by compiling a syntax - check if possible. - - Ran 1 shell command (ctrl+o to expand) - -● Let's save both new patches and verify all four apply together cleanly. - - Ran 2 shell commands (ctrl+o to expand) - -● Now let's update build-apk.sh to stage the ES shader variants instead of the - desktop ones. - - Read 1 file (ctrl+o to expand) - -● Update(build-image/build-apk.sh) - ⎿  Added 4 lines, removed 1 line - 35 ASSETS_DIR="$ANDROID_DIR/app/src/main/assets" - 36 rm -rf "$ASSETS_DIR" - 37 mkdir -p "$ASSETS_DIR/shaders" "$ASSETS_DIR/res" - 38 -cp -a "$REPO_ROOT/engine/shaders/." "$ASSETS_DIR/shaders/" - 38 +# GLES ports of engine/shaders/ (see android/gles-shaders/ and - 39 +# android/engine-patches/04-android-opengl-es-render.patch) - n - +ot - 40 +# engine/shaders/ itself, which is desktop-only GLSL. - 41 +cp -a "$ANDROID_DIR/gles-shaders/." "$ASSETS_DIR/shaders/" - 42 cp "/opt/prebuilt/soundfont/default.sf2" "$ASSETS_DIR/res/sound - font.sf2" - 43 cp "$REPO_ROOT/res/assets/GET_ASSETS_QUEST.txt" "$ASSETS_DIR/GE - T_ASSETS_QUEST.txt" - 44 - -● No image rebuild needed. Let's re-run make apk. - - Ran 1 shell command (ctrl+o to expand) - -● Waiting for this run. - -✻ Sautéed for 15m 29s \ No newline at end of file diff --git a/NOTICE.txt b/NOTICE.txt index 630911e..e94ca54 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -7,3 +7,6 @@ under the GNU GPLv3 - see LICENSE.Shockolate. It is included unchanged and is not relicensed by this project's MIT license. Game assets are not included - see res/GET_ASSETS.txt. + +The Quest build bundles GL4ES (https://github.com/ptitSeb/gl4es), +prebuilt unmodified as lib/arm64-v8a/libGL.so, which is MIT-licensed. diff --git a/README.md b/README.md index ef0835e..f28066b 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,11 @@ the [MIT License](LICENSE). [SDL2](https://www.libsdl.org/)'s own android-project template and is zlib-licensed, same as SDL2 itself. +The Quest build also bundles [GL4ES](https://github.com/ptitSeb/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](https://github.com/Interrupt/systemshock), which is licensed under the **GNU GPLv3** (see `engine/LICENSE`) - it is included unchanged diff --git a/android/engine-patches/02-android-opengl-es.patch b/android/engine-patches/02-android-opengl-es.patch index d288efe..4a459dc 100644 --- a/android/engine-patches/02-android-opengl-es.patch +++ b/android/engine-patches/02-android-opengl-es.patch @@ -1,17 +1,19 @@ --- a/CMakeLists.txt +++ b/CMakeLists.txt -@@ -46,9 +46,15 @@ +@@ -46,9 +46,17 @@ add_compile_options(-fsigned-char -fno-strict-aliasing) -# Find OpenGL +# Find OpenGL. Android has no desktop GL/GLX for CMake's FindOpenGL module -+# to find - just link the NDK's own GLESv2/EGL libraries directly. ++# to find - link the prebuilt GL4ES instead (translates this file's ++# desktop-style GL calls into GLES/EGL; it links GLESv2/EGL itself, so ++# the engine doesn't need to link them directly). if(ENABLE_OPENGL) - find_package(OpenGL REQUIRED) + if(ANDROID) -+ set(OPENGL_INCLUDE_DIRS "") -+ set(OPENGL_LIBRARIES GLESv2 EGL) ++ set(OPENGL_INCLUDE_DIRS /opt/prebuilt/android/gl4es/include) ++ set(OPENGL_LIBRARIES /opt/prebuilt/android/gl4es/lib/libGL.so) + else() + find_package(OpenGL REQUIRED) + endif() diff --git a/android/engine-patches/04-android-opengl-es-render.patch b/android/engine-patches/04-android-opengl-es-render.patch index bd33882..86f9f08 100644 --- a/android/engine-patches/04-android-opengl-es-render.patch +++ b/android/engine-patches/04-android-opengl-es-render.patch @@ -1,153 +1,55 @@ --- a/src/MacSrc/OpenGL.cc +++ b/src/MacSrc/OpenGL.cc -@@ -7,6 +7,11 @@ - #define GLEW_STATIC 1 - #include - #include -+#elif defined(__ANDROID__) -+// GLES 3.2 (see Shock.c's context creation) - none of the desktop GL -+// headers below exist on Android; EGL only ever gives out GLES contexts. -+#include -+#include - #else - #define GL_GLEXT_PROTOTYPES - #ifdef __APPLE__ -@@ -20,6 +25,112 @@ +@@ -20,6 +20,24 @@ #include #endif +#ifdef __ANDROID__ -+// GLES has no immediate-mode rendering (glBegin/glVertex3f/glEnd, used -+// throughout this file below) at all. These macros transparently replace -+// it with an equivalent that accumulates vertex data into arrays and -+// issues one glDrawArrays call at glEnd() time - every actual call site -+// below (all unchanged) looks identical on both platforms. "position", -+// bound to attribute location 0 in CreateShader() below, replaces -+// gl_Vertex (a desktop-only builtin android/gles-shaders/main.vert can't -+// use either). -+#define glBegin questshock_glBegin -+#define glVertex3f questshock_glVertex3f -+#define glEnd questshock_glEnd -+#define glVertexAttrib1f questshock_glVertexAttrib1f -+#define glVertexAttrib2f questshock_glVertexAttrib2f ++// GL4ES's glPointSize()/custom-shader interop goes through the same kind ++// of shader-source rewrite as its alpha-test emulation (see ++// init_opengl() below) - avoid relying on it. GLES point size instead ++// comes from a "pointSize" uniform, written to gl_PointSize in ++// android/gles-shaders/main.vert. opengl_begin_stars() (the only ++// glPointSize() call site) now binds the star shader first, so ++// GL_CURRENT_PROGRAM here is always correct. +#define glPointSize questshock_glPointSize -+ -+// Generous enough for both the largest opengl_draw_poly() polygon and a -+// full frame's worth of opengl_draw_star() points accumulated between one -+// opengl_begin_stars()/opengl_end_stars() pair. -+static const int QUESTSHOCK_MAX_IM_VERTS = 4096; -+static float questshock_im_pos[QUESTSHOCK_MAX_IM_VERTS * 3]; -+static float questshock_im_tc[QUESTSHOCK_MAX_IM_VERTS * 2]; -+static float questshock_im_light[QUESTSHOCK_MAX_IM_VERTS]; -+static int questshock_im_count; -+static GLenum questshock_im_mode; -+static float questshock_im_cur_tc[2]; -+static float questshock_im_cur_light; -+static float questshock_point_size = 1.0f; -+ -+// GLES has no glPointSize() at all - point size is instead read from -+// gl_PointSize, set by a "pointSize" uniform in -+// android/gles-shaders/main.vert. Stashed here and applied in -+// questshock_glEnd() below, since opengl_begin_stars() calls this before -+// glUseProgram(starShaderProgram...) - the star shader isn't the current -+// program yet at this point. -+static void questshock_glPointSize(GLfloat size) { questshock_point_size = size; } -+ -+static void questshock_glBegin(GLenum mode) { -+ questshock_im_mode = mode; -+ questshock_im_count = 0; -+ questshock_im_cur_tc[0] = questshock_im_cur_tc[1] = 0.0f; -+ questshock_im_cur_light = 0.0f; -+} -+ -+static void questshock_glVertexAttrib1f(GLuint index, GLfloat v) { -+ // Only ever called for "light" at these call sites. -+ (void)index; -+ questshock_im_cur_light = v; -+} -+ -+static void questshock_glVertexAttrib2f(GLuint index, GLfloat a, GLfloat b) { -+ // Only ever called for "texcoords" at these call sites. -+ (void)index; -+ questshock_im_cur_tc[0] = a; -+ questshock_im_cur_tc[1] = b; -+} -+ -+static void questshock_glVertex3f(GLfloat x, GLfloat y, GLfloat z) { -+ if (questshock_im_count >= QUESTSHOCK_MAX_IM_VERTS) -+ return; -+ questshock_im_pos[questshock_im_count * 3 + 0] = x; -+ questshock_im_pos[questshock_im_count * 3 + 1] = y; -+ questshock_im_pos[questshock_im_count * 3 + 2] = z; -+ questshock_im_tc[questshock_im_count * 2 + 0] = questshock_im_cur_tc[0]; -+ questshock_im_tc[questshock_im_count * 2 + 1] = questshock_im_cur_tc[1]; -+ questshock_im_light[questshock_im_count] = questshock_im_cur_light; -+ questshock_im_count++; -+} -+ -+static void questshock_glEnd() { ++static void questshock_glPointSize(GLfloat size) { + GLint program = 0; + glGetIntegerv(GL_CURRENT_PROGRAM, &program); -+ GLint posAttrib = glGetAttribLocation(program, "position"); -+ GLint tcAttrib = glGetAttribLocation(program, "texcoords"); -+ GLint lightAttrib = glGetAttribLocation(program, "light"); -+ -+ if (questshock_im_mode == GL_POINTS) { -+ GLint pointSizeUniform = glGetUniformLocation(program, "pointSize"); -+ if (pointSizeUniform >= 0) -+ glUniform1f(pointSizeUniform, questshock_point_size); -+ } -+ -+ if (posAttrib >= 0) { -+ glVertexAttribPointer(posAttrib, 3, GL_FLOAT, GL_FALSE, 0, questshock_im_pos); -+ glEnableVertexAttribArray(posAttrib); -+ } -+ if (tcAttrib >= 0) { -+ glVertexAttribPointer(tcAttrib, 2, GL_FLOAT, GL_FALSE, 0, questshock_im_tc); -+ glEnableVertexAttribArray(tcAttrib); -+ } -+ if (lightAttrib >= 0) { -+ glVertexAttribPointer(lightAttrib, 1, GL_FLOAT, GL_FALSE, 0, questshock_im_light); -+ glEnableVertexAttribArray(lightAttrib); -+ } -+ -+ glDrawArrays(questshock_im_mode, 0, questshock_im_count); -+ -+ if (posAttrib >= 0) -+ glDisableVertexAttribArray(posAttrib); -+ if (tcAttrib >= 0) -+ glDisableVertexAttribArray(tcAttrib); -+ if (lightAttrib >= 0) -+ glDisableVertexAttribArray(lightAttrib); ++ GLint loc = glGetUniformLocation(program, "pointSize"); ++ if (loc >= 0) ++ glUniform1f(loc, size); +} +#endif // __ANDROID__ + extern "C" { #include "mainloop.h" #include "map.h" -@@ -229,6 +340,12 @@ +@@ -229,6 +247,13 @@ GLuint shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertShader); glAttachShader(shaderProgram, fragShader); +#ifdef __ANDROID__ -+ // Must be bound before linking. Matches questshock_glEnd()'s -+ // glGetAttribLocation(program, "position") above and ++ // Must be bound before linking. GL4ES routes glVertex3f() (used ++ // throughout this file below) to attribute location 0 of whatever ++ // shader is currently bound - matches + // android/gles-shaders/main.vert's "position" attribute. + glBindAttribLocation(shaderProgram, 0, "position"); +#endif glLinkProgram(shaderProgram); glUseProgram(shaderProgram); -@@ -313,10 +430,17 @@ +@@ -313,10 +338,18 @@ glEnable(GL_CULL_FACE); glEnable(GL_BLEND); +#ifndef __ANDROID__ -+ // GLES has no fixed-function pipeline: no alpha test (emulated with a -+ // discard in android/gles-shaders/texture.frag instead), and point -+ // sprites are always implicitly on for GL_POINTS rendering (via -+ // gl_PointCoord in android/gles-shaders/star.frag) - no enum/enable -+ // for either exists in GLES at all. ++ // GL4ES emulates both of these (alpha test, point sprites) for a ++ // custom shader by rewriting its source under the hood - a rougher ++ // edge of the library. Skip relying on that: alpha test is instead ++ // a discard in android/gles-shaders/texture.frag, and point-sprite ++ // rasterization for GL_POINTS works automatically in GLES with no ++ // enable call at all (see android/gles-shaders/star.frag). glEnable(GL_ALPHA_TEST); glEnable(GL_POINT_SPRITE); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); @@ -157,3 +59,17 @@ CreateShader("main.vert", "texture.frag", &textureShaderProgram); CreateShader("main.vert", "color.frag", &colorShaderProgram); +@@ -908,9 +941,12 @@ + void opengl_begin_stars() { + SDL_GL_MakeCurrent(window, context); + ++ // Bind the star shader before setting the point size: on Android, ++ // questshock_glPointSize() (see the __ANDROID__ block above) needs ++ // it to already be GL_CURRENT_PROGRAM. ++ glUseProgram(starShaderProgram.shaderProgram); + glPointSize(1.5 * (render_width / 320.0)); + +- glUseProgram(starShaderProgram.shaderProgram); + glUniformMatrix4fv(starShaderProgram.uniView, 1, false, IdentityMatrix); + glUniformMatrix4fv(starShaderProgram.uniProj, 1, false, IdentityMatrix); + diff --git a/build-image/Dockerfile b/build-image/Dockerfile index 0f92f84..92ebf08 100644 --- a/build-image/Dockerfile +++ b/build-image/Dockerfile @@ -46,6 +46,10 @@ ARG ANDROID_COMPILE_SDK_VERSION=34 ARG ANDROID_BUILD_TOOLS_VERSION=34.0.0 ARG ANDROID_NDK_VERSION=26.1.10909125 ARG ANDROID_CMAKE_VERSION=3.22.1 +# GL4ES (https://github.com/ptitSeb/gl4es) - translates the engine's +# desktop-style immediate-mode OpenGL calls into real GLES/EGL calls, so +# engine/src/MacSrc/OpenGL.cc needs no immediate-mode rewrite on Android. +ARG ANDROID_GL4ES_VERSION=1.1.6 ENV DEBIAN_FRONTEND=noninteractive @@ -225,6 +229,23 @@ RUN git clone https://github.com/EtherTyper/fluidsynth-lite.git fluidsynth-lite- /opt/prebuilt/android/fluidsynth-lite/include/fluidsynth/version.h \ && rm -rf fluidsynth-lite-android build-fluidsynth-android +# 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). +RUN git clone --branch "v${ANDROID_GL4ES_VERSION}" --depth 1 \ + https://github.com/ptitSeb/gl4es.git gl4es-android \ + && rm -rf gl4es-android/.git \ + && cmake -S gl4es-android -B build-gl4es-android \ + -DCMAKE_TOOLCHAIN_FILE="${ANDROID_NDK_TOOLCHAIN}" \ + -DANDROID_ABI="${ANDROID_ABI}" -DANDROID_PLATFORM="android-${ANDROID_PLATFORM_VERSION}" \ + -DANDROID=ON -DUSE_ANDROID_LOG=ON -DSTATICLIB=OFF \ + && 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 \ + && cp -a gl4es-android/include/. /opt/prebuilt/android/gl4es/include/ \ + && rm -rf gl4es-android build-gl4es-android + COPY build-image/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh COPY build-image/build-engine.sh /usr/local/bin/build-engine.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh /usr/local/bin/build-engine.sh diff --git a/build-image/VERSION b/build-image/VERSION index 0cfbf08..00750ed 100644 --- a/build-image/VERSION +++ b/build-image/VERSION @@ -1 +1 @@ -2 +3 diff --git a/build-image/build-apk.sh b/build-image/build-apk.sh index 576af68..817feeb 100755 --- a/build-image/build-apk.sh +++ b/build-image/build-apk.sh @@ -42,11 +42,12 @@ 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 .so's into jniLibs ==" +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 "== Building the APK (gradlew assembleDebug) =="