1 Commits

Author SHA1 Message Date
ml 9586c58a67 Add a QuestShock startup splash; rename the app and data folder
build / build (push) Successful in 2m35s
2026-09-20 16:49:28 +02:00
17 changed files with 245 additions and 30 deletions
+15 -6
View File
@@ -16,7 +16,7 @@ a cross-platform port of the original engine, in an Android app that:
- shows a menu and an on-screen keyboard as separate OpenXR quads, operated by - shows a menu and an on-screen keyboard as separate OpenXR quads, operated by
a laser pointer from either controller; a laser pointer from either controller;
- maps controller inputs to game actions through a configurable binding layer; - maps controller inputs to game actions through a configurable binding layer;
- persists settings in `/sdcard/questshock/questshock.config.json`. - persists settings in `/sdcard/QuestShock/questshock.config.json`.
A desktop (Linux/Windows) build of the same engine exists for development A desktop (Linux/Windows) build of the same engine exists for development
without a headset; it has none of the Android/OpenXR code. without a headset; it has none of the Android/OpenXR code.
@@ -94,13 +94,14 @@ not print it or commit it.
| `xr_input.c/.h` | Controller input: OpenXR actions, raw-input layer, action bindings, capture-mode rebinding, key injection into the engine, free view, cyberspace scaling, snap-turn/smooth-move of the player's position, pointer rays. The largest file. | | `xr_input.c/.h` | Controller input: OpenXR actions, raw-input layer, action bindings, capture-mode rebinding, key injection into the engine, free view, cyberspace scaling, snap-turn/smooth-move of the player's position, pointer rays. The largest file. |
| `xr_mouse.c/.h` | Injects mouse motion and clicks into the engine. | | `xr_mouse.c/.h` | Injects mouse motion and clicks into the engine. |
| `qs_config.c/.h` | Configuration: JSON parse, apply, write; screen profiles; menu-driven requests; cheats; menu-triggered pause. | | `qs_config.c/.h` | Configuration: JSON parse, apply, write; screen profiles; menu-driven requests; cheats; menu-triggered pause. |
| `qs_splash.c/.h` | Startup splash: `qs_splash_show()` fades the image (`res/splash.png`, decoded by Java and passed in through `nativeSetSplashImage`) in and out on the game screen. Called from engine patch 27. |
| `questshock_native.c` | JNI exports and small native helpers (`nativeChdir`, `nativeSendPrintableChar`, ...). | | `questshock_native.c` | JNI exports and small native helpers (`nativeChdir`, `nativeSendPrintableChar`, ...). |
### 4.2 Java classes ### 4.2 Java classes
| Class | Role | | Class | Role |
|---|---| |---|---|
| `QuestShockActivity` | `SDLActivity` subclass. Creates `/sdcard/questshock/`, extracts bundled files, blocks engine start until the game data exists. | | `QuestShockActivity` | `SDLActivity` subclass. Creates `/sdcard/QuestShock/`, extracts bundled files, blocks engine start until the game data exists. |
| `OverlayPanel` | Base for the off-screen panels: builds the view tree, renders it to a texture, turns ray hits into touch events. | | `OverlayPanel` | Base for the off-screen panels: builds the view tree, renders it to a texture, turns ray hits into touch events. |
| `MenuOverlay` | The menu: Main Menu, Video Options, Controller Options and Cheats tabs. Singleton. | | `MenuOverlay` | The menu: Main Menu, Video Options, Controller Options and Cheats tabs. Singleton. |
| `KeyboardOverlay` | The on-screen keyboard. | | `KeyboardOverlay` | The on-screen keyboard. |
@@ -116,6 +117,10 @@ not print it or commit it.
first-party code log under the tag `QuestShock`. first-party code log under the tag `QuestShock`.
- Bindings, config and menu state are owned natively. Java only displays and - Bindings, config and menu state are owned natively. Java only displays and
requests. requests.
- `gScreenWide`/`gScreenHigh` are only set by the engine's
`ChangeScreenSize()`, so they are 0 until its first mode change. Code that
runs during startup (such as `qs_splash_show()`) takes the screen size from
`xr_get_game_swapchain_size()` instead.
### 4.4 Threads and cross-thread requests ### 4.4 Threads and cross-thread requests
@@ -314,15 +319,19 @@ toggles.
## 10. Bundled files on the device ## 10. Bundled files on the device
`QuestShockActivity` extracts these into `/sdcard/questshock/`: `QuestShockActivity` extracts these into `/sdcard/QuestShock/`:
- shaders, the default soundfont and `GET_ASSETS_QUEST.txt` (once); - shaders, the default soundfont and `GET_ASSETS_QUEST.txt` (once);
- `questshock.config.default.json` and `controller_profiles/` (every launch, - `questshock.config.default.json` and `controller_profiles/` (every launch,
overwriting only the bundled filenames so user-added profiles survive). overwriting only the bundled filenames so user-added profiles survive).
`res/controller_profiles/` and the default config are staged into APK assets by `res/splash.png` stays in the APK assets and is not extracted to the device;
`build-image/prepare-android-project.sh`; new staged directories must also be `QuestShockActivity` decodes it and hands the pixels to `qs_splash.c`.
declared as `inputs.dir(...)` of `stageEngine` in `android/app/build.gradle`.
`res/controller_profiles/`, `res/splash.png` and the default config are staged
into APK assets by `build-image/prepare-android-project.sh`; every staged file
or directory must also be declared as `inputs.file(...)`/`inputs.dir(...)` of
`stageEngine` in `android/app/build.gradle`.
The proprietary game data (`res/data/`, `res/sound/`) is never bundled or The proprietary game data (`res/data/`, `res/sound/`) is never bundled or
committed. The app refuses to start the engine until it is present. committed. The app refuses to start the engine until it is present.
+2 -2
View File
@@ -3,7 +3,7 @@
QuestShock can load a persistent, human-editable configuration file at QuestShock can load a persistent, human-editable configuration file at
startup, in addition to (and independent of) the engine's own `prefs.txt`. startup, in addition to (and independent of) the engine's own `prefs.txt`.
- **Location**: `/sdcard/questshock/questshock.config.json` - the same - **Location**: `/sdcard/QuestShock/questshock.config.json` - the same
folder you already copy `res/data`/`res/sound` into (see README.md folder you already copy `res/data`/`res/sound` into (see README.md
section 5.1). section 5.1).
- **Format**: plain JSON, grouped by concern. Each top-level key is a - **Format**: plain JSON, grouped by concern. Each top-level key is a
@@ -421,7 +421,7 @@ profile is active.
The Controller Options tab shows a labeled illustration of whichever The Controller Options tab shows a labeled illustration of whichever
controller is actually active, driven by `res/controller_profiles/*.json` controller is actually active, driven by `res/controller_profiles/*.json`
(bundled with the app, copied to `/sdcard/questshock/controller_profiles/` (bundled with the app, copied to `/sdcard/QuestShock/controller_profiles/`
on every launch, refreshing the known bundled files without touching any on every launch, refreshing the known bundled files without touching any
extra ones you add there yourself). Each file: extra ones you add there yourself). Each file:
+13 -6
View File
@@ -45,7 +45,8 @@ sideloaded Android app, and needs your own copy of the game data.
- `res/questshock.config.default.json` - the bundled default configuration - `res/questshock.config.default.json` - the bundled default configuration
(see [CONFIG.md](CONFIG.md)); `res/controller_profiles/` - the per-controller (see [CONFIG.md](CONFIG.md)); `res/controller_profiles/` - the per-controller
illustrations and labels shown in the Controller Options tab (see illustrations and labels shown in the Controller Options tab (see
[CONFIG.md](CONFIG.md)). Both are copied into the APK. [CONFIG.md](CONFIG.md)). Both are copied into the APK, as is
`res/splash.png`, the startup splash image.
- `Makefile` - targets: - `Makefile` - targets:
- `all` (default) - alias for `dist`. - `all` (default) - alias for `dist`.
- `build-image` - builds the Docker build-image (`./build-image.sh`). - `build-image` - builds the Docker build-image (`./build-image.sh`).
@@ -293,17 +294,17 @@ debugging enabled.)
1. Install the APK with [SideQuest](https://sidequestvr.com/) (or `adb 1. Install the APK with [SideQuest](https://sidequestvr.com/) (or `adb
install`). install`).
2. Launch it once. It'll ask for storage permission, then create 2. Launch it once. It'll ask for storage permission, then create
`/sdcard/questshock/` and extract its own bundled files (shaders, a `/sdcard/QuestShock/` and extract its own bundled files (shaders, a
default MIDI soundfont) there - `res/data/` and `res/sound/` are default MIDI soundfont) there - `res/data/` and `res/sound/` are
deliberately left missing, since that's the proprietary game data. deliberately left missing, since that's the proprietary game data.
3. With the headset connected to a PC, use SideQuest's file browser (or 3. With the headset connected to a PC, use SideQuest's file browser (or
any MTP file manager) to copy your own `res/data/` and `res/sound/` any MTP file manager) to copy your own `res/data/` and `res/sound/`
(see `/sdcard/questshock/GET_ASSETS_QUEST.txt`, extracted in step 2, (see `/sdcard/QuestShock/GET_ASSETS_QUEST.txt`, extracted in step 2,
for exactly what's needed and where it comes from) into for exactly what's needed and where it comes from) into
`/sdcard/questshock/res/`. `/sdcard/QuestShock/res/`.
4. Launch it again. 4. Launch it again.
`/sdcard/questshock/` also holds an optional persistent settings file, `/sdcard/QuestShock/` also holds an optional persistent settings file,
`questshock.config.json`, loaded automatically at startup and saved/loaded `questshock.config.json`, loaded automatically at startup and saved/loaded
from the in-headset menu - see [CONFIG.md](CONFIG.md) for the documented from the in-headset menu - see [CONFIG.md](CONFIG.md) for the documented
options. Every launch also refreshes `questshock.config.default.json` (the options. Every launch also refreshes `questshock.config.default.json` (the
@@ -314,6 +315,9 @@ alone.
#### 5.1.1. Menu, controls and cheats #### 5.1.1. Menu, controls and cheats
At startup the QuestShock title (`res/splash.png`) fades in and out on the
virtual screen for a few seconds, before the game's own intro screens.
The controller's menu button (left controller) opens and closes the menu. It The controller's menu button (left controller) opens and closes the menu. It
can't be reassigned. Opening it pauses a running game; closing it resumes the can't be reassigned. Opening it pauses a running game; closing it resumes the
game, unless it was already paused. Aim with either hand's ray and pull the game, unless it was already paused. Aim with either hand's ray and pull the
@@ -411,7 +415,7 @@ the same as any other Android Studio project:
Android Studio builds, installs, and launches the app on the headset Android Studio builds, installs, and launches the app on the headset
directly. No manual `adb install` or SideQuest step needed for this directly. No manual `adb install` or SideQuest step needed for this
loop. loop.
4. `/sdcard/questshock/` (game assets, extracted shaders/soundfont) 4. `/sdcard/QuestShock/` (game assets, extracted shaders/soundfont)
persists across reinstalls from Android Studio, so this loop doesn't persists across reinstalls from Android Studio, so this loop doesn't
require re-copying `res/data/`/`res/sound/` on every iteration - only require re-copying `res/data/`/`res/sound/` on every iteration - only
a full uninstall or wiping that directory clears it. a full uninstall or wiping that directory clears it.
@@ -611,6 +615,9 @@ each one does:
- **`26-android-cheat-refill.patch`** - adds `qs_cheat_refill()` to - **`26-android-cheat-refill.patch`** - adds `qs_cheat_refill()` to
`hkeyfunc.c`, which tops up health, energy, ammo and items (grenades and `hkeyfunc.c`, which tops up health, energy, ammo and items (grenades and
drugs the player already holds) for the Cheats tab's unlimited switches. drugs the player already holds) for the Cheats tab's unlimited switches.
- **`27-android-startup-splash.patch`** - `setup.c`'s `splash_draw()` calls
`qs_splash_show()` first, which fades the QuestShock title in and out on the
game screen before the engine's own splash screens.
`android/gl4es-patches/`: `android/gl4es-patches/`:
+3 -2
View File
@@ -107,6 +107,7 @@ def stageEngineTask = tasks.register("stageEngine", Exec) {
// Staged into assets/ by prepare-android-project.sh. // Staged into assets/ by prepare-android-project.sh.
inputs.file("${repoRoot}/res/questshock.config.default.json") inputs.file("${repoRoot}/res/questshock.config.default.json")
inputs.dir("${repoRoot}/res/controller_profiles") inputs.dir("${repoRoot}/res/controller_profiles")
inputs.file("${repoRoot}/res/splash.png")
outputs.dir("${repoRoot}/build/android-engine") outputs.dir("${repoRoot}/build/android-engine")
outputs.dir("${repoRoot}/build/android-gl4es") outputs.dir("${repoRoot}/build/android-gl4es")
outputs.dir("${repoRoot}/build/android-prebuilt") outputs.dir("${repoRoot}/build/android-prebuilt")
@@ -138,7 +139,7 @@ android {
// 29, not compileSdk's 34, is deliberate - see build-image/Dockerfile's // 29, not compileSdk's 34, is deliberate - see build-image/Dockerfile's
// comment on ANDROID_PLATFORM_VERSION: paired with // comment on ANDROID_PLATFORM_VERSION: paired with
// requestLegacyExternalStorage in the manifest, this keeps // requestLegacyExternalStorage in the manifest, this keeps
// /sdcard/questshock/ a plain, unrestricted shared folder instead of // /sdcard/QuestShock/ a plain, unrestricted shared folder instead of
// Android 11+ scoped storage, on the Quest 2/3/3S/Pro headsets this // Android 11+ scoped storage, on the Quest 2/3/3S/Pro headsets this
// targets - not just whatever's the current API level today. // targets - not just whatever's the current API level today.
minSdkVersion 24 minSdkVersion 24
@@ -199,7 +200,7 @@ android {
"-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH", \ "-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH", \
"-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH", \ "-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH", \
"-DCMAKE_SHARED_LINKER_FLAGS=-Wl,-z,max-page-size=16384", \ "-DCMAKE_SHARED_LINKER_FLAGS=-Wl,-z,max-page-size=16384", \
"-DANDROID_EXTRA_SOURCES=${projectDir}/src/main/cpp/questshock_native.c;${projectDir}/src/main/cpp/xr_session.c;${projectDir}/src/main/cpp/xr_input.c;${projectDir}/src/main/cpp/xr_overlay.c;${projectDir}/src/main/cpp/xr_swapchain.c;${projectDir}/src/main/cpp/xr_mouse.c;${projectDir}/src/main/cpp/qs_config.c" "-DANDROID_EXTRA_SOURCES=${projectDir}/src/main/cpp/questshock_native.c;${projectDir}/src/main/cpp/xr_session.c;${projectDir}/src/main/cpp/xr_input.c;${projectDir}/src/main/cpp/xr_overlay.c;${projectDir}/src/main/cpp/xr_swapchain.c;${projectDir}/src/main/cpp/xr_mouse.c;${projectDir}/src/main/cpp/qs_config.c;${projectDir}/src/main/cpp/qs_splash.c"
abiFilters 'arm64-v8a' abiFilters 'arm64-v8a'
} }
} }
+9 -7
View File
@@ -38,6 +38,11 @@
OpenXR session to actually start. --> OpenXR session to actually start. -->
<meta-data android:name="com.oculus.vr.focusaware" android:value="true" /> <meta-data android:name="com.oculus.vr.focusaware" android:value="true" />
<!-- Meta-specific: the headsets this app is built for. Other OpenXR
runtimes ignore it. -->
<meta-data android:name="com.oculus.supportedDevices"
android:value="quest2|questpro|quest3|quest3s" />
<activity android:name="de.ladkau.questshock.QuestShockActivity" <activity android:name="de.ladkau.questshock.QuestShockActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:alwaysRetainTaskState="true" android:alwaysRetainTaskState="true"
@@ -51,13 +56,10 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
<!-- The vendor-neutral, Khronos-defined way to declare an <!-- The vendor-neutral, Khronos-defined way to declare an
immersive OpenXR entry point (vs. e.g. Meta's own immersive OpenXR entry point, plus Meta's own VR
com.oculus.intent.category.VR) - see this project's category, which Horizon OS looks for to treat the app as
"favor OpenXR" rule in the README. If Horizon OS still a VR app. Other runtimes ignore the Meta one. -->
launches this as a flat panel on-device, the next thing <category android:name="com.oculus.intent.category.VR" />
to try is adding a com.oculus.supportedDevices meta-data
listing the target Quest models - not confirmed
necessary from static analysis alone. -->
<category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" /> <category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
+1 -1
View File
@@ -10,7 +10,7 @@ extern "C" {
// Persistent, human-editable JSON config file - see CONFIG.md at the repo // Persistent, human-editable JSON config file - see CONFIG.md at the repo
// root for the documented schema. Lives at a plain relative path // root for the documented schema. Lives at a plain relative path
// ("questshock.config.json") since the whole engine already runs with its // ("questshock.config.json") since the whole engine already runs with its
// cwd chdir()'d to the user-facing /sdcard/questshock/ folder (see // cwd chdir()'d to the user-facing /sdcard/QuestShock/ folder (see
// QuestShockActivity.java's nativeChdir()) before main() ever runs - the // QuestShockActivity.java's nativeChdir()) before main() ever runs - the
// same way prefs.txt/keybinds.txt (Prefs.c) already do. // same way prefs.txt/keybinds.txt (Prefs.c) already do.
// //
+110
View File
@@ -0,0 +1,110 @@
#include "qs_splash.h"
#include <SDL.h>
#include <android/log.h>
#include <stdbool.h>
#include <string.h>
#include "xr_session.h"
#define QS_SPLASH_HOLD_MS 2500
#define QS_SPLASH_FADE_MS 500
#define QS_SPLASH_TOTAL_MS (QS_SPLASH_HOLD_MS + 2 * QS_SPLASH_FADE_MS)
// The XR session only starts once frames are polled, so the display time is
// counted from the first submitted frame; this bounds the wait for it.
#define QS_SPLASH_SESSION_TIMEOUT_MS 5000
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "QuestShock", __VA_ARGS__)
extern void pump_events(void);
// OpenGL.cc (engine patch 11): draws a software frame into the XR game swapchain.
extern void android_composite_software_frame(SDL_Surface *ui);
static SDL_Surface *s_image = NULL;
void qs_splash_set_image(const uint32_t *argb, int width, int height) {
if (s_image != NULL) {
SDL_FreeSurface(s_image);
s_image = NULL;
}
if (argb == NULL || width <= 0 || height <= 0)
return;
s_image = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_ARGB8888);
if (s_image == NULL)
return;
for (int y = 0; y < height; y++)
memcpy((uint8_t *)s_image->pixels + (size_t)y * s_image->pitch, argb + (size_t)y * width,
(size_t)width * sizeof(uint32_t));
SDL_SetSurfaceBlendMode(s_image, SDL_BLENDMODE_BLEND);
}
// 0-255 brightness at `elapsed` ms after the first frame.
static int qs_splash_level(uint32_t elapsed) {
uint32_t edge = elapsed < QS_SPLASH_TOTAL_MS - elapsed ? elapsed : QS_SPLASH_TOTAL_MS - elapsed;
if (edge >= QS_SPLASH_FADE_MS)
return 255;
return (int)(255 * edge / QS_SPLASH_FADE_MS);
}
void qs_splash_show(void) {
if (s_image == NULL) {
LOGI("Startup splash: no image");
return;
}
int width, height;
xr_get_game_swapchain_size(&width, &height);
if (width <= 0 || height <= 0) {
LOGI("Startup splash: no XR game swapchain");
return;
}
// The image is stretched over the whole swapchain image, so it is
// letterboxed into a canvas of that size to keep its aspect ratio.
SDL_Surface *canvas =
SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_RGBA32);
if (canvas == NULL)
return;
const float scale = SDL_min((float)width / s_image->w, (float)height / s_image->h);
SDL_Rect dst;
dst.w = (int)(s_image->w * scale);
dst.h = (int)(s_image->h * scale);
dst.x = (width - dst.w) / 2;
dst.y = (height - dst.h) / 2;
const uint32_t waitStart = SDL_GetTicks();
uint32_t shownStart = 0;
uint32_t elapsed = 0;
int drawnLevel = -1;
bool done = false;
while (!done) {
pump_events();
xr_poll_events();
if (xr_frame_begin()) {
if (shownStart == 0)
shownStart = SDL_GetTicks();
elapsed = SDL_GetTicks() - shownStart;
if (elapsed >= QS_SPLASH_TOTAL_MS) {
elapsed = QS_SPLASH_TOTAL_MS;
done = true;
}
const int level = qs_splash_level(elapsed);
if (level != drawnLevel) {
drawnLevel = level;
SDL_FillRect(canvas, NULL, SDL_MapRGB(canvas->format, 0, 0, 0));
SDL_SetSurfaceColorMod(s_image, level, level, level);
SDL_BlitScaled(s_image, NULL, canvas, &dst);
}
android_composite_software_frame(canvas);
} else {
SDL_Delay(10);
if (shownStart == 0 && SDL_GetTicks() - waitStart > QS_SPLASH_SESSION_TIMEOUT_MS)
done = true;
}
xr_frame_end();
}
LOGI("Startup splash: %s after %u ms", shownStart != 0 ? "shown" : "XR session did not start",
(unsigned)(SDL_GetTicks() - waitStart));
SDL_FreeSurface(canvas);
}
+15
View File
@@ -0,0 +1,15 @@
// Startup splash: the QuestShock title, shown on the game screen before the
// engine's own splash sequence.
#pragma once
#include <stdint.h>
// Stores a copy of the splash image (straight-alpha ARGB, one int per pixel).
// Called from JNI before the engine starts; replaces any earlier image. Without
// an image qs_splash_show() does nothing.
void qs_splash_set_image(const uint32_t *argb, int width, int height);
// Engine thread only (called from engine patch 27). Fades the image in and out
// on the game screen and returns when done, or after a few seconds if the XR
// session never starts.
void qs_splash_show(void);
@@ -11,6 +11,7 @@
#include <SDL.h> #include <SDL.h>
#include "qs_config.h" #include "qs_config.h"
#include "qs_splash.h"
#include "xr_input.h" #include "xr_input.h"
#include "xr_overlay.h" #include "xr_overlay.h"
#include "xr_session.h" #include "xr_session.h"
@@ -22,6 +23,15 @@ Java_de_ladkau_questshock_QuestShockActivity_nativeChdir(JNIEnv *env, jclass cla
(*env)->ReleaseStringUTFChars(env, path, cpath); (*env)->ReleaseStringUTFChars(env, path, cpath);
} }
JNIEXPORT void JNICALL Java_de_ladkau_questshock_QuestShockActivity_nativeSetSplashImage(
JNIEnv *env, jclass clazz, jintArray pixels, jint width, jint height) {
jint *data = (*env)->GetIntArrayElements(env, pixels, NULL);
if (data == NULL)
return;
qs_splash_set_image((const uint32_t *)data, width, height);
(*env)->ReleaseIntArrayElements(env, pixels, data, JNI_ABORT);
}
// KeyboardOverlay's hand-built on-screen keyboard (see // KeyboardOverlay's hand-built on-screen keyboard (see
// KeyboardOverlay.handleCharKeyDown()/handleCharKeyUp()) has no real // KeyboardOverlay.handleCharKeyDown()/handleCharKeyUp()) has no real
// Android IME session behind it, so printable characters can't reach the // Android IME session behind it, so printable characters can't reach the
+5
View File
@@ -871,6 +871,11 @@ void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half
} }
} }
void xr_get_game_swapchain_size(int *width, int *height) {
*width = g_game_swapchain.width;
*height = g_game_swapchain.height;
}
void xr_get_game_resolution(int *width, int *height) { void xr_get_game_resolution(int *width, int *height) {
// Not g_game_swapchain.width/height: that's the fixed physical size of // Not g_game_swapchain.width/height: that's the fixed physical size of
// the swapchain image the game is composited into (see xr_init()) - // the swapchain image the game is composited into (see xr_init()) -
+6
View File
@@ -102,6 +102,12 @@ void xr_session_set_game_screen_shape(float width_m, float curve_deg, float dist
// swapchain. // swapchain.
void xr_get_game_resolution(int *width, int *height); void xr_get_game_resolution(int *width, int *height);
// Pixel size of the game quad's swapchain image, fixed at launch (0, 0 before
// xr_init()). The quad's aspect ratio is this size, so full-quad content
// drawn outside the engine's own frame path (the startup splash) is laid out
// against it.
void xr_get_game_swapchain_size(int *width, int *height);
// Exponentially-smoothed frame rate/frame time, updated once per real // Exponentially-smoothed frame rate/frame time, updated once per real
// rendered frame in xr_frame_end() - read by MenuOverlay's header (see // rendered frame in xr_frame_end() - read by MenuOverlay's header (see
// questshock_native.c's nativeGetFps()/nativeGetFrameTimeMs()). 0 before // questshock_native.c's nativeGetFps()/nativeGetFrameTimeMs()). 0 before
@@ -5,6 +5,8 @@ import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
@@ -18,7 +20,7 @@ import org.libsdl.app.SDLActivity;
import org.libsdl.app.SDLSurface; import org.libsdl.app.SDLSurface;
/** /**
* Prepares /sdcard/questshock/ the way Shockolate's plain relative-path * Prepares /sdcard/QuestShock/ the way Shockolate's plain relative-path
* file I/O ("res/data/...", confirmed via grep - none of it goes through * file I/O ("res/data/...", confirmed via grep - none of it goes through
* SDL_RWops, so Android's asset-manager fallback for SDL_RWFromFile * SDL_RWops, so Android's asset-manager fallback for SDL_RWFromFile
* doesn't apply here) expects to find it - see android/engine-patches/ for * doesn't apply here) expects to find it - see android/engine-patches/ for
@@ -40,7 +42,7 @@ public class QuestShockActivity extends SDLActivity {
private static final String TAG = "QuestShock"; private static final String TAG = "QuestShock";
private static final int PERMISSION_REQUEST_STORAGE = 1; private static final int PERMISSION_REQUEST_STORAGE = 1;
// Package-visible: MenuOverlay reads controller_profiles/ from here. // Package-visible: MenuOverlay reads controller_profiles/ from here.
static final String GAME_DIR = "/sdcard/questshock"; static final String GAME_DIR = "/sdcard/QuestShock";
// Loaded here (rather than waiting for SDLActivity's own // Loaded here (rather than waiting for SDLActivity's own
// getLibraries()/loadLibraries(), which only runs from super.onCreate()) // getLibraries()/loadLibraries(), which only runs from super.onCreate())
@@ -58,6 +60,9 @@ public class QuestShockActivity extends SDLActivity {
// has no public chdir() (confirmed against the actual API 34 stub jar). // has no public chdir() (confirmed against the actual API 34 stub jar).
private static native void nativeChdir(String path); private static native void nativeChdir(String path);
// Straight-alpha ARGB pixels of the startup splash (qs_splash.c).
private static native void nativeSetSplashImage(int[] pixels, int width, int height);
@Override @Override
protected String[] getLibraries() { protected String[] getLibraries() {
return new String[] { return new String[] {
@@ -187,11 +192,31 @@ public class QuestShockActivity extends SDLActivity {
// before clearing mBrokenLibraries below so the engine can never // before clearing mBrokenLibraries below so the engine can never
// start pre-chdir. // start pre-chdir.
nativeChdir(GAME_DIR); nativeChdir(GAME_DIR);
loadSplashImage();
// Assets confirmed present - safe to let the native engine start now. // Assets confirmed present - safe to let the native engine start now.
SDLActivity.mBrokenLibraries = false; SDLActivity.mBrokenLibraries = false;
} }
// The engine shows this at startup (qs_splash.c); without it there is no splash.
private void loadSplashImage() {
try (InputStream in = getAssets().open("splash.png")) {
Bitmap bitmap = BitmapFactory.decodeStream(in);
if (bitmap == null) {
Log.e(TAG, "Could not decode splash.png");
return;
}
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int[] pixels = new int[width * height];
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
bitmap.recycle();
nativeSetSplashImage(pixels, width, height);
} catch (IOException e) {
Log.e(TAG, "Could not read splash.png", e);
}
}
private static boolean isNonEmptyDir(File dir) { private static boolean isNonEmptyDir(File dir) {
String[] entries = dir.list(); String[] entries = dir.list();
return entries != null && entries.length > 0; return entries != null && entries.length > 0;
@@ -219,7 +244,7 @@ public class QuestShockActivity extends SDLActivity {
+ "merging or extraction required.\n\n" + "merging or extraction required.\n\n"
+ "Then relaunch.\n\n" + "Then relaunch.\n\n"
+ "(These instructions are also in GET_ASSETS_QUEST.txt in the " + "(These instructions are also in GET_ASSETS_QUEST.txt in the "
+ "questshock folder on this device, if you need to read them again.)") + "QuestShock folder on this device, if you need to read them again.)")
.setCancelable(false) .setCancelable(false)
.setPositiveButton("Exit", (dialog, which) -> finish()) .setPositiveButton("Exit", (dialog, which) -> finish())
.create() .create()
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">System Shock</string> <string name="app_name">QuestShock</string>
</resources> </resources>
@@ -0,0 +1,24 @@
--- a/src/GameSrc/setup.c 2026-09-20 16:04:46.197310679 +0200
+++ b/src/GameSrc/setup.c 2026-09-20 16:05:08.400330327 +0200
@@ -58,6 +58,9 @@
#include "gamestrn.h"
#include "mainloop.h"
+#ifdef __ANDROID__
+#include "qs_splash.h"
+#endif
#include "tools.h"
#include "input.h"
#include "game_screen.h"
@@ -1300,6 +1303,11 @@
if (!show_splash)
return;
+#ifdef __ANDROID__
+ // The QuestShock title, before the engine's own splash screens.
+ qs_splash_show();
+#endif
+
// Need to load the splash palette file
INFO("Loading splshpal.res");
+1
View File
@@ -93,6 +93,7 @@ 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" cp "$REPO_ROOT/res/assets/GET_ASSETS_QUEST.txt" "$ASSETS_DIR/GET_ASSETS_QUEST.txt"
cp "$REPO_ROOT/res/questshock.config.default.json" "$ASSETS_DIR/questshock.config.default.json" cp "$REPO_ROOT/res/questshock.config.default.json" "$ASSETS_DIR/questshock.config.default.json"
cp -a "$REPO_ROOT/res/controller_profiles" "$ASSETS_DIR/controller_profiles" cp -a "$REPO_ROOT/res/controller_profiles" "$ASSETS_DIR/controller_profiles"
cp "$REPO_ROOT/res/splash.png" "$ASSETS_DIR/splash.png"
echo "== Staging prebuilt Android SDL2/SDL2_mixer/fluidsynth-lite/openxr .so's into jniLibs ==" echo "== Staging prebuilt Android SDL2/SDL2_mixer/fluidsynth-lite/openxr .so's into jniLibs =="
# gl4es is deliberately absent here - it's no longer prebuilt, it's compiled # gl4es is deliberately absent here - it's no longer prebuilt, it's compiled
+2 -2
View File
@@ -5,8 +5,8 @@ installed (on Windows, or via Wine/Proton on Linux).
Using SideQuest (or any MTP file browser) with your Quest connected, Using SideQuest (or any MTP file browser) with your Quest connected,
copy its res/data/ and res/sound/ folders directly into: copy its res/data/ and res/sound/ folders directly into:
/sdcard/questshock/res/data/ /sdcard/QuestShock/res/data/
/sdcard/questshock/res/sound/ /sdcard/QuestShock/res/sound/
They already contain everything needed, in the right layout - no They already contain everything needed, in the right layout - no
merging or extraction required. merging or extraction required.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB