modding/mods/ holds message edits as plain JSON (type, index, language,
plus only the changed fields - a long body can live in a sibling .txt to
avoid escaping in JSON), so mod authoring is reviewable in git instead of
living only in hacked's GUI. mdlp (modding/src/mdlp/, its own Docker image
mdlp-image/) turns them into the same mod folder/zip hacked's own Save
produces, reusing hacked's ss1/edit, ss1/world and ss1/content/text
packages via a go.mod replace - no GUI/GLFW/GTK needed. `make mods`
builds every mod into dist/mods/<name>/, scoped per-mod so it never
touches hacked's own build output; `make` (no target) now builds both.
Fixes a bug this surfaced: hacked's FileLocation.AbsolutePathFrom
silently writes into the current directory instead of the given output
folder when passed a relative path (the GUI never hits this since its
folder picker is always absolute) - mdlp now resolves its own paths
up front.
res/run-mods.sh/.bat are new companion launchers to run.sh/.bat: they
pass every dist/mods/*/mod folder to the engine as a separate absolute
argument, since ProcessModDirectory() only scans one directory level
deep. Wired into `make dist`/`dist-win` only, not the release archives.
modding/hacked/, its patches, sqweek-dialog/, its patches, and the new
mdlp/ move under modding/src/, separating source from the surrounding
Docker/script scaffolding; all path references updated to match.
NOTICE.txt gains a section for hacked's distributed build, listing hacked
(New BSD), sqweek/dialog (ISC - corrected from a wrong "New BSD" note),
and hacked's other direct Go dependencies.
Add a player_controls config group that maps game actions to controller
inputs, and a Controller Options menu tab to rebind them in-headset.
- 45 actions: movement, click/double-click, jump, fire, and the engine's own
keyboard hotkeys (posture, save/load, weapon/MFD select, etc.), sent to the
engine as synthetic key events. Cheat and dead hotkeys are not exposed.
- Press-to-capture rebinding with per-input exclusivity, Reset to Defaults
(built-in defaults, then questshock.config.default.json), and new defaults:
left stick moves/strafes, right stick turns.
- Controller profiles for Touch, Vive and generic OpenXR
(res/controller_profiles/), matched to the active interaction profile at
runtime, with a fatal dialog if a profile's bindings are rejected at
startup.
- The menu's thumbstick locomotion is disabled while the Controller Options
tab is open or a capture is active. A scrollable list with a tap-to-jump
scroll bar handles the long action list.
- Opening the menu pauses a running game and resumes it on close.
- Reset Position now keeps the player's facing instead of turning the screen
to the current gaze.
- Play the intro video in the game's language (engine patch 23), falling back
to English.
- Watchdog and shared-key handling for stuck movement keys, plus key-edge
logging.
- Video Options tab offers width and distance presets independently for
the Initial Screen (splash/intro/menu) and Game Screen (gameplay)
profiles, with their preset step/offset configurable via
questshock.config.json.
- Ships res/questshock.config.default.json as the initial config on
first run (or whenever questshock.config.json doesn't exist yet),
refreshed from the APK on every launch without touching a user's own
saved config.
- New "Language" row on the Main Menu tab (English/French/German),
backed by a new "general" group in questshock.config.json
- Fixes a device-reproduced crash: switching language used to call
language_dealfunc(), whose render_run()/opanel_redraw() repaint
assumes the game's own in-game options panel is on screen - crashes
(SIGSEGV) when triggered from our independent VR overlay instead.
Now calls language_change() directly, which is all that's actually
needed to reload resources and refresh the inventory/MFD/side-icon UI
- Fixes a second crash: this data set ships frnstrng.res/gerstrng.res
but not mfdfrn.res/mfdger.res, and language_change() closes the
current resource files before discovering the new ones don't exist -
leaving no valid resource file open and crashing on the next lookup.
Now checks both files exist before ever switching, refusing the
change (with an in-headset popup) instead of corrupting resource state
- extract_assets.sh can now also pull mfdfrn.res/mfdger.res out of a
System Shock: Classic installer (which includes them, unlike Enhanced
Edition) if one is present in res/assets/ - entirely optional
- Documented all of the above in README.md/CONFIG.md
- Header with title, git-tag-derived version, and a live FPS/frame-time/
CPU/GPU-utilization/free-memory stats ribbon below the tab bar (FPS via
a new per-frame tracker in xr_session.c; CPU/GPU via the OpenXR
XR_META_performance_metrics extension, gracefully unavailable on
runtimes that don't support it)
- Bottom tab bar with a live-editable Video Options tab (renderer/
resolution), applying changes immediately via qs_config.c
- Hover and press feedback on every menu/keyboard button via a
StateListDrawable (Android's default ripple never animates on this
permanently unattached view tree), with a lighter default button color
for visibility
- i18n for all menu/keyboard chrome text, following the game's own
language setting (Strings.java)
- The main menu is now draggable via its header, matching the keyboard;
xr_input.c's drag handling is generalized to per-overlay state instead
of being keyboard-only
- The keyboard's title bar now matches the menu header's style
- opengl_end_frame() bound framebuffer 0 on Android, which has no valid
target there (real presentation goes through the OpenXR swapchain's own
FBOs) - gl4es reported this every frame. Skip it there.
- render_hack_cameras() re-entered opengl_start_frame()/opengl_end_frame()
for camera-monitor renders that never touch the GL screen; excluded them
to match use_opengl()'s existing behavior. Also drains a stray error left
by xrPollEvent().
- ChangeScreenSize() never called opengl_resize() on Android, freezing the
3D render target at launch size after an in-game resolution change; and
xr_get_game_resolution() used the swapchain's fixed size instead of the
current screen resolution, misplacing the laser-pointer cursor after such
a change.
- Removes all temporary bisection diagnostics added while tracking down the
GL_INVALID_OPERATION source.
Stale GL_UNPACK state in gl4es and a missing pre-release GPU sync
caused diagonal-banded corruption in the 3D view. Synthetic key
press/release must track the real controller button or touch down/up
edges, not fire together in one call - fixes a typed movement key
sticking forever, then a follow-up that had silently broken A-button
jump and on-screen keyboard arrow keys. Also adds force-rebuild.sh for
the known stageEngine stale-cache issue.
issues on vboxsf-mounted checkouts
New `make dist-win`/`package-win` targets (folded into `dist`/`package`
alongside the renamed `dist-linux`/`package-linux`) cross-compile
systemshock.exe via MinGW, using prebuilt SDL2/SDL2_mixer/GLEW/
fluidsynth-lite baked into the build-image - no Windows machine or Wine
needed to build it, confirmed working and playable on a real Windows
machine. dist-win/ ships DLLs flat alongside the exe plus a new
res/run.bat launcher, packaged into a .zip the same way dist/ becomes a
.tar.gz.
Also fixes three build-image bugs hit while testing on a VirtualBox
vboxsf-mounted checkout: build-engine.sh/docker-entrypoint.sh losing
their execute bit (chmod +x on restrictive source perms), the container
user missing access to /workspace's supplementary vboxsf group, and
cp -a failing on symlink/hard-link creation (vboxsf doesn't support
either) - now falls back to dereferencing copies when detected. Also
adds Docker/zip/etc. prerequisites to the README for both the desktop
and Quest builds.
- xr_input.c/h: an OpenXR action set (aim pose, trigger/select click, a
menu-toggle button) plus ray/quad hit-testing, and a visible
laser-pointer line drawn from the controller toward the hit point.
- xr_menu.c/h + MenuOverlay.java: a second composition-layer quad,
toggled by a controller button, showing an off-screen, never-attached
Android View tree (one "Keyboard" button so far) driven by synthetic
touch events computed from the laser ray's hit UV.
- Migrate gl4es from a prebuilt binary baked into the Docker build-image
to a vendored source snapshot (android/gl4es-src/) compiled from
source at APK build time via CMake add_subdirectory(), patched through
a new android/gl4es-patches/ (mirrors the existing engine-patches/
pattern). This was needed to track down and fix a bug hit while
building the menu: gl4es's fixed-pipeline emulation (fpe.c) was
unconditionally substituting its own shader onto the menu's draw call
(fpe_ReleventState() always sets alphafunc to a nonzero sentinel, so
its fpe_IsEmpty() check could never see the state as empty), making
the menu quad show the game's own rendering instead of its own
content. Fixed by routing the menu's blit through a real
glBlitFramebuffer() call instead of a shader-based draw, which gl4es's
fpe.c has nothing to intercept - documented in README.md's new
"Debugging notes" section.
- Renumber android/engine-patches/ to close the gap left by removing an
unrelated diagnostic-only patch, and strip investigation-journal
comments and dead diagnostic code (temporary tracing, env-var probes)
left over from finding the bug above.
- Restructure README.md into numbered sections, document every
engine/gl4es patch, add Android Studio dev/testing-cycle instructions,
and generalize Quest-specific wording to any OpenXR headset. Update
NOTICE.txt to match (gl4es is now vendored/patched source, not a
prebuilt binary; add the OpenXR-SDK loader).
- New android/app/src/main/cpp/xr_session.c (+.h): owns the OpenXR
instance/session/local-space/swapchain and the per-frame
xrWaitFrame/xrBeginFrame/xrEndFrame loop, submitting the game's
existing flat render as a single head-tracked XrCompositionLayerQuad.
No stereo rendering or controller input yet - that's steps B/C/D of
the plan.
- 11-android-openxr-cmake.patch: links the OpenXR loader as a build
dependency, staged into jniLibs the same way as gl4es/SDL2.
build-image/Dockerfile and prepare-android-project.sh build and stage
it.
- 12-android-openxr-present.patch: redirects OpenGL.cc's final present
step (opengl_swap_and_restore / SDLDraw's software path) into the XR
swapchain FBO instead of the window, on Android only. Two real gl4es
bugs had to be worked around to get pixels on screen at all:
- gl4es's own glBindFramebuffer errors on an FBO id it didn't create
itself, even though the real driver-level bind succeeds - fixed by
creating the swapchain FBO container via gl4es's own
glGenFramebuffers/glBindFramebuffer, and only using the real
(dlsym'd) driver call for attaching OpenXR's foreign swapchain
texture, which gl4es's own attach can't handle.
- gl4es's glBegin/glVertexAttrib/glVertex3f immediate-mode emulation
only captures a fresh per-vertex value for attribute 0 (position,
driven directly by glVertex3f) - custom attributes like this
shader's texcoords/light are GLES2's *constant*-attribute API and
applied once for the whole draw, not per vertex, silently
collapsing the UI-overlay quad to a single sampled texel. Fixed by
switching that one draw call to real vertex arrays
(glVertexAttribPointer/glDrawArrays).
Also flips the V texcoord to match SDL's top-down row order against
GL's texture convention, and reuses a persistent texture object
instead of a fresh gen/upload/delete every frame.
- AndroidManifest.xml: declares the immersive-HMD intent category and
focus-aware metadata, drops the 2D-panel layout hint.
- README: documents the new OpenXR immersive mode.
<layout> defaultWidth/defaultHeight/minWidth/minHeight/gravity hint -
Quest's Home shell otherwise defaults a freshly-launched 2D panel to a
portrait shape, cropping the game's 4:3 640x480 content down to a
sliver.
- The actual root cause of the remaining crop (game rendering into one
corner of an otherwise correctly-sized panel), found via on-device
logcat and the new diagnostics below rather than guesswork:
ChangeScreenSize() (engine/src/MacSrc/ShockBitmap.c) calls
SDL_SetWindowSize() whenever the game sets its video mode. That's a
desktop-only operation in effect - Android has no SetWindowSize driver
hook, so SDL's generic layer instead overwrites its own cached window
size to the game's internal resolution (640x480) and synthesizes a
resize event from that, desyncing SDL's notion of the window size from
the real, unchanged Android surface (e.g. 1600x1200). Both SDL's own
renderer viewport and the engine's custom GL viewport then scale
against that corrupted cached size. 10-android-no-window-resize.patch
skips the desktop-only SDL_SetWindowFullscreen/SetWindowSize/
SetWindowPosition calls on Android, keeping the legitimate
SDL_RenderSetLogicalSize/offscreen-bitmap setup.
- 06-android-resize-event.patch: also react to SDL_WINDOWEVENT_RESIZED
in the engine's event loop, not just SIZE_CHANGED - Android's SDL video
backend never sends SIZE_CHANGED for surface-driven resizes, only
RESIZED, an independent gap worth closing regardless of the bug above.
- 08/09-android-logcat-*.patch: route the engine's existing log.c output
(previously plain fprintf(stderr,...), never actually captured by
logcat on this build) through __android_log_vprint instead, so every
existing INFO/DEBUG/WARN/ERROR call site becomes visible for on-device
debugging. This is what made the diagnostic below (and everything
since) observable at all.
- 07-android-size-diagnostics.patch: one-time startup log comparing
SDL_GetWindowSize/SDL_GL_GetDrawableSize/SDL_GetRendererOutputSize -
the evidence that actually pinned down the SDL_SetWindowSize bug above.
- QuestShockActivity.java: add a diagnostic onSizeChanged() log on
GameSurface, used to rule out a later Android-side panel relayout as
the cause before finding the real one.
- QuestShockActivity now actually blocks the native engine from starting
when game data is missing, closing three gaps found via on-device
testing: super.onCreate() must run unconditionally first (Android
throws SuperNotCalledException otherwise); SDLActivity.mBrokenLibraries
is now set provisionally before the storage-permission check, since
onWindowFocusChanged() closing the permission dialog could otherwise
start the engine before the async onRequestPermissionsResult() callback
ran; and a new GameSurface (SDLSurface subclass) closes the actual gap
that let the init_popups NULL-deref crash through even with
mBrokenLibraries set - SDLSurface.surfaceChanged() starts the native
thread directly without ever checking that flag.
- Force Android to use SDL2's openslES audio backend instead of AAudio
(android/engine-patches/05-android-audio-driver.patch): AAudio only
allows one open playback device at a time, but the engine opens two
(cutscene audio via SDL_OpenAudioDevice, SFX/MIDI via Mix_OpenAudio),
hitting an assertion failure on real hardware.
- Add 16 KB ELF page-size alignment (-Wl,-z,max-page-size=16384) to every
Android shared library - the four prebuilts (SDL2, SDL2_mixer,
fluidsynth-lite, gl4es, in build-image/Dockerfile) and the engine's own
libmain.so (build.gradle) - matching Google's Play Store requirement
for Android 15+ and clearing Android Studio's compatibility warning.
- Add a stageEngine Gradle task that automatically re-stages the patched
engine/ copy and prebuilt libraries before any Android Studio build
(hooked into preBuild, with proper up-to-date checking), so source/
patch changes can't silently go stale in the build/android-engine
scratch copy - previously a manual, easy-to-forget step. Skips
automatically inside the build-image container so make apk/CI are
unaffected.
- patchelf gl4es's embedded SONAME to libGL.so so AGP's jniLibs packaging
(which drops any file not literally named "*.so") and the dynamic
linker's NEEDED-entry resolution (by embedded SONAME, not filename)
finally agree - fixes the "library \"libGL.so.1\" not found" crash seen
on real Quest hardware.
- QuestShockActivity now checks that res/data and res/sound exist before
starting the native engine, showing an explanatory dialog instead of
crashing on init_popups' unchecked NULL resource load when a fresh
install has no game data copied in yet.
- dist/questshock-<version>-android-arm64.apk is now versioned from the
same git-tag-or-dev-placeholder scheme as the desktop tarball
(build-image/version.sh, shared by both via the Makefile and
build-apk.sh).
- build-image/prepare-android-project.sh (prep logic extracted out of
build-apk.sh) can now stage android/ for a native build directly in
Android Studio (--host-paths), exporting the prebuilt SDL2/SDL2_mixer/
fluidsynth-lite/gl4es libraries and writing host-resolvable paths,
instead of only ever building inside the Docker image.
- Corrected GET_ASSETS.txt/GET_ASSETS_QUEST.txt, which wrongly described
merging res/pc/hd and res/pc/cdrom trees out of the raw installer's
sshock.kpf - an already-installed copy's res/data res/sound can just
be copied directly, with extract_assets.sh only needed from the raw
installer.
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.