Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9586c58a67 |
@@ -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.
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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/`:
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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()) -
|
||||||
|
|||||||
@@ -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,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");
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
Reference in New Issue
Block a user