6 Commits

Author SHA1 Message Date
ml 70c43240bc Add a Cheats menu tab with engine cheat buttons and unlimited-resource switches
build / build (push) Successful in 3m42s
The new Cheats tab exposes the engine's own cheat handlers as buttons:
give all items and weapons, toggle physics, next level and previous
level. Physics shows its state through the button highlight. The buttons
need a running game and show a message otherwise.

It also has four independent session-only switches for unlimited health,
energy, ammo and items (grenades and drugs). While a switch is on, its
resource is topped up every frame by qs_cheat_refill() from the new
engine patch 26. The switches are not saved to the config.
2026-09-20 14:59:33 +02:00
ml d8324d409e Add free view and a separate cyberspace action set
build / build (push) Successful in 3m24s
- Free view: hold a mapped input (default: left grip) and that thumbstick
  drives the engine's mouse look, with the stick's normal bindings suspended
  while held. Also available for the right stick, unbound by default.
- Cyberspace actions: eleven thrust/climb/dive/bank/roll actions with their
  own bindings, active only in cyberspace. The normal movement actions are
  inactive there, and capture exclusivity only compares actions that can be
  active together, so one input can be bound in both sets.
- Cyberspace pitch and bank scale with thumbstick deflection instead of
  running at full strength.
- Remove the toggle_freelook action; bind the automap view to the right
  stick click by default.
- Engine patches: 24 exposes whether the player is in cyberspace, 25 lets
  the controller code scale cyberspace pitch and bank.
2026-09-20 07:24:44 +02:00
ml c24b7f15cb Add remappable controller controls with a Controller Options tab
build / build (push) Successful in 2m14s
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.
2026-09-20 06:08:50 +02:00
ml e1e6a006d4 Add configurable game/initial screen width and distance, with a bundled first-run config
build / build (push) Successful in 2m41s
- 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.
2026-09-06 09:08:54 +02:00
ml 5fe4cdf7f8 Bump AGP to 8.5.2 and Gradle to 8.7 for 16 KB page-size support
build / build (push) Successful in 2m21s
Every .so this project builds was already ELF-segment-aligned for 16 KB
pages (build-image/Dockerfile's ANDROID_16KB_LDFLAGS, app/build.gradle's
own CMAKE_SHARED_LINKER_FLAGS), but AGP 8.1.1 doesn't align native
libraries' zip entries within the APK itself - that only became automatic
in AGP 8.5.1, which is what Android Studio's "not compatible with 16 KB
devices" warning was actually flagging. No build-image changes needed:
Gradle/AGP are resolved over the network at build time, not baked into
the image.
2026-09-05 16:18:29 +02:00
ml 369555efc7 Making the menu slightly larger 2026-09-05 15:53:15 +02:00
30 changed files with 4375 additions and 472 deletions
+321 -11
View File
@@ -9,9 +9,11 @@ startup, in addition to (and independent of) the engine's own `prefs.txt`.
- **Format**: plain JSON, grouped by concern. Each top-level key is a - **Format**: plain JSON, grouped by concern. Each top-level key is a
*group*; each key inside a group is one *option*. *group*; each key inside a group is one *option*.
- **Loading**: read automatically at app startup, before the splash screen, - **Loading**: read automatically at app startup, before the splash screen,
if the file exists. If it doesn't exist, nothing changes - the engine if the file exists. If it doesn't, `questshock.config.default.json` (same
behaves exactly as it did before this file existed (driven by `prefs.txt` folder) is used instead - a copy bundled with the app, refreshed on every
as usual). launch, never overwriting your own saved `questshock.config.json`. If
neither file exists, the engine behaves as it did before this file
existed (driven by `prefs.txt` as usual).
- **Saving**: never automatic. In-headset, open the menu (controller menu - **Saving**: never automatic. In-headset, open the menu (controller menu
button) and press **Save Configuration** to write the file from your button) and press **Save Configuration** to write the file from your
current live settings, overwriting it if present. Press **Load current live settings, overwriting it if present. Press **Load
@@ -47,7 +49,15 @@ startup, in addition to (and independent of) the engine's own `prefs.txt`.
{ {
"video_options": { "video_options": {
"renderer": "opengl", "renderer": "opengl",
"resolution": "1024x768" "resolution": "1024x768",
"initial_screen_width_m": 1.6,
"initial_screen_distance_m": 2.0,
"game_screen_width_m": 2.8,
"game_screen_distance_m": 2.0,
"screen_width_offset_m": 1.0,
"screen_width_step_m": 0.6,
"screen_distance_offset_m": 1.0,
"screen_distance_step_m": 0.5
}, },
"general": { "general": {
"language": "english" "language": "english"
@@ -56,6 +66,21 @@ startup, in addition to (and independent of) the engine's own `prefs.txt`.
"x": 0.0, "x": 0.0,
"z": 0.0, "z": 0.0,
"yaw_degrees": 0.0 "yaw_degrees": 0.0
},
"player_controls": {
"move_forward": "left_thumbstick_up",
"move_back": "left_thumbstick_down",
"strafe_left": "left_thumbstick_left",
"strafe_right": "left_thumbstick_right",
"turn_left": "right_thumbstick_left",
"turn_right": "right_thumbstick_right",
"jump": "left_thumbstick_click",
"mouse_click": "right_a",
"mouse_double_click": "right_b",
"stand": "right_thumbstick_up",
"prone": "right_thumbstick_down",
"map_view": "right_thumbstick_click",
"fire_weapon": "right_trigger"
} }
} }
``` ```
@@ -78,6 +103,56 @@ The game's internal rendering resolution.
- **Possible values**: `"320x200"`, `"320x400"`, `"640x400"`, `"640x480"`, `"1024x768"` - **Possible values**: `"320x200"`, `"320x400"`, `"640x400"`, `"640x480"`, `"1024x768"`
- **Restart required?**: No (live-appliable) - **Restart required?**: No (live-appliable)
### `initial_screen_width_m`, `initial_screen_distance_m`, `game_screen_width_m`, `game_screen_distance_m`
The virtual screen's physical width and distance from the player (both in
meters) - the game shows two independently configurable screen
"profiles", switched automatically based on what's currently on screen:
- **Initial Screen** (`initial_screen_*`) - the splash screen, intro
cutscenes, and the game's own main menu.
- **Game Screen** (`game_screen_*`) - actual gameplay (the 3D view,
automap, cyberspace, etc.).
Switching happens automatically - no player action needed - the instant
the game itself transitions between these (e.g. starting/loading a game,
or returning to the menu).
- **Type**: number (meters)
- **Possible values**: the in-headset Video Options tab offers 5 presets
each - widths `1.0`, `1.6` (the original, pre-this-feature default),
`2.2`, `2.8`, `3.4`; distances `1.0`, `1.5`, `2.0` (the original,
pre-this-feature default), `2.5`, `3.0` - but a hand-edited value
doesn't need to match a preset exactly (it's clamped to a sane range -
width `[0.2, 10.0]`, distance `[0.5, 10.0]` - rather than snapped or
rejected; the Video Options tab just shows no button selected for an
in-between value).
- **Restart required?**: No (live-appliable)
- **Note**: the screen is always flat - there's no curve option.
### `screen_width_offset_m`, `screen_width_step_m`, `screen_distance_offset_m`, `screen_distance_step_m`
Reshapes the 5 preset buttons the Video Options tab offers for width and
distance (above) - config-file-only, there's no in-headset control for
these. Shared by both **Initial Screen** and **Game Screen** (both
columns' buttons show the same 5 values per row) - this only changes
what the buttons offer, not either profile's current value.
Preset button `index` (0-4) shows `offset + step * index`. For example,
a distance offset of `0` and step of `2` shows buttons `0`, `2`, `4`,
`6`, `8`; the defaults below reproduce this feature's original launch
presets exactly.
- **Type**: number (meters)
- **Default values**: width offset `1.0`, step `0.6` (buttons `1.0`,
`1.6`, `2.2`, `2.8`, `3.4`); distance offset `1.0`, step `0.5` (buttons
`1.0`, `1.5`, `2.0`, `2.5`, `3.0`).
- **Restart required?**: No (live-appliable)
- **Note**: not clamped - an unusual offset/step (e.g. negative step) is
accepted as-is. A resulting preset value outside the current value's
own clamp range (see above) still clamps normally when that preset
button is actually pressed.
## General (`general`) ## General (`general`)
### `language` ### `language`
@@ -117,13 +192,14 @@ to an automatic recenter instead (see **Reset Position** below), using
your real head pose at that moment. A complete saved position is applied your real head pose at that moment. A complete saved position is applied
as-is, with no such recenter. as-is, with no such recenter.
**Reset Position** (Main Menu tab) is a separate, live-only action, not a **Reset Position** (Main Menu tab) is a live-only action, not a config
config value: it recenters using your *current* real head pose, so the value: it moves you to the centre in front of the game quad, based on where
game quad ends up directly ahead of wherever you're actually standing/ your head actually is right now. It resets only your *position*, not which
facing right now - not a plain "go back to `(0, 0, 0)`" (which would only way you're facing, so the screen does not rotate to follow your gaze - press
undo thumbstick movement, not any physical drift/rotation since the it while looking off to one side and the screen stays off to the other side.
session started). It's unrelated to `questshock.config.json` - it never The automatic recenter at startup (when there's no saved position) does
reads or writes this file. face the screen towards where you're looking. Reset Position never reads or
writes `questshock.config.json`.
### `x`, `z` ### `x`, `z`
@@ -138,3 +214,237 @@ Which way the player is virtually facing, in degrees.
- **Type**: number - **Type**: number
- **Restart required?**: No (live-appliable) - **Restart required?**: No (live-appliable)
## Player Controls (`player_controls`)
Maps each of 57 in-game actions to a physical controller input: 9
movement/click actions, 2 free-view actions, 11 cyberspace actions, and 35
for the engine's own keyboard hotkeys and firing (posture, save/load, weapon
select, MFD panels, etc. - see the hotkey table below). Some actions are bound by default (see the default-input
columns); the rest are unbound and can be bound on demand. Assign them in the
**Controller Options** menu tab: tap an action's binding button, then press
the button or stick direction to use ("Press a button…"); tap it again, or
wait 8 seconds, to cancel without changing it.
- **Type**: one enum-string field per action (below)
- **Restart required?**: No (live-appliable) - a rebind takes effect
immediately, and persists across restarts once saved (**Save
Configuration**, same as every other group here).
- **Not included here**: opening/closing the VR menu itself is a fixed,
non-remappable button (see "Menu button" below) - there's no
`menu_toggle` field, and the Controller Options tab never lets you
reassign it. This guarantees you can always reach this tab and press
**Reset to Defaults** to recover from any bad rebind, no matter what else
you've reassigned.
- **Reset to Defaults**: restores the "Default input" column below for
every action not overridden by a `player_controls` group in
`questshock.config.default.json` - if that file defines one (it doesn't,
out of the box), the values it specifies are applied on top of the table
below instead, so you can customize what "reset" means without a rebuild.
This is independent of your own `questshock.config.json` - resetting
doesn't read or write that file, just the live bindings (**Save
Configuration** afterward to persist them).
### Actions
| Action key | Default input | Effect |
|---|---|---|
| `move_forward` | `left_thumbstick_up` | Walk forward |
| `move_back` | `left_thumbstick_down` | Walk backward |
| `strafe_left` | `left_thumbstick_left` | Step sideways, left |
| `strafe_right` | `left_thumbstick_right` | Step sideways, right |
| `turn_left` | `right_thumbstick_left` | Turn left |
| `turn_right` | `right_thumbstick_right` | Turn right |
| `jump` | `left_thumbstick_click` | Jump |
| `mouse_click` | `right_a` | Click - operates the keyboard/game quad's cursor, whichever the pointing hand's ray is over |
| `mouse_double_click` | `right_b` | Double-click - game quad only (e.g. using an inventory item); has no effect over the menu or keyboard |
| `free_left_thumbstick_view` | `left_squeeze` | Hold to look around with the left thumbstick (see below) |
| `free_right_thumbstick_view` | `none` | Hold to look around with the right thumbstick (see below) |
Movement actions bound to a thumbstick direction are suppressed while the
VR menu is open, since the same physical stick already snap-turns/smooth-
moves the player in that state (see `player_position` above) - this is the
one case where a binding's effect depends on whether the menu is open, and
it only ever affects the 8 thumbstick-direction inputs below.
**Free view** (`free_left_thumbstick_view`, `free_right_thumbstick_view`) is
the engine's mouse look driven by a thumbstick. While the bound input is held,
that stick looks around instead of doing its normal job: left/right turns the
view, up/down looks up and down, in proportion to how far the stick is pushed.
The stick's own direction bindings (move, strafe, turn, stand/prone) are
suspended while it is held, and the cursor stays centred in the view. Release
the input to return to normal. It has no effect while the VR menu is open.
### Cyberspace actions
Cyberspace has its own set of movement actions, active only while the player
is in cyberspace. While in cyberspace the normal movement actions
(`move_*`, `strafe_*`, `turn_*`, `jump`, `stand`, `crouch`, `prone`) are
inactive, and the cyberspace actions are inactive everywhere else. Each set
has its own bindings, so one input can be bound in both (for example the left
stick up is `move_forward` on foot and `cyber_climb` in cyberspace);
the Controller Options tab only reports a conflict between actions that can
be active at the same time. Free view, clicking and firing work in both.
Pitch and bank (`cyber_climb`, `cyber_dive`, `cyber_bank_*` and the four
diagonals) are proportional when bound to a thumbstick direction: pushing the
stick further turns faster, from 20% of full speed just past the trigger point
to 60% at full deflection. Bound to a button they run at full speed. Thrust and
roll are always full speed.
| Action key | Default input | Keyboard equivalent | Effect |
|---|---|---|---|
| `cyber_thrust` | `right_thumbstick_up` | `s` | Thrust forward |
| `cyber_climb` | `left_thumbstick_up` | `w` | Pitch up |
| `cyber_dive` | `left_thumbstick_down` | `x` | Pitch down |
| `cyber_bank_left` | `left_thumbstick_left` | `a` | Bank left |
| `cyber_bank_right` | `left_thumbstick_right` | `d` | Bank right |
| `cyber_roll_left` | `right_thumbstick_left` | `e` | Roll left |
| `cyber_roll_right` | `right_thumbstick_right` | `q` | Roll right |
| `cyber_climb_left` | `none` | Keypad 7 | Pitch up and bank left |
| `cyber_climb_right` | `none` | Keypad 9 | Pitch up and bank right |
| `cyber_dive_left` | `none` | Keypad 1 | Pitch down and bank left |
| `cyber_dive_right` | `none` | Keypad 3 | Pitch down and bank right |
`mouse_click` additionally behaves differently depending on the menu:
while the VR menu is open, its "click" is always whichever hand's trigger
is used to interact with the menu (so you can never lock yourself out of
this settings screen by rebinding `mouse_click` elsewhere); once the menu
is closed, both the on-screen keyboard (if it's still open) and the game
quad use your actual `mouse_click` binding. The default (`right_a`) is a
specific button on the right controller, so only the right hand's ray clicks;
bind the `trigger` identifier (see below) for either hand to click.
### Hotkey actions
The engine's own keyboard hotkeys (`HotKeyLookup[]` in `Prefs.c`) plus
firing, as 35 bindable actions. `stand` (`right_thumbstick_up`), `prone`
(`right_thumbstick_down`), `map_view` (`right_thumbstick_click`) and
`fire_weapon` (`right_trigger`) are bound by default; the rest are unbound
(`none`). Each fires once per press, like pressing the key on a real
keyboard (there is no hold behavior).
| Action key | Keyboard equivalent | Effect |
|---|---|---|
| `stand` | `t` | Stand up |
| `crouch` | `g` | Crouch |
| `prone` | `b` | Go prone |
| `pause` | `p` | Pause the game |
| `full_view` | Ctrl+F | Switch to the fullscreen view loop |
| `normal_view` | Ctrl+D | Switch to the normal gameplay view |
| `map_view` | Ctrl+A | Switch to the automap view |
| `save_game` | Ctrl+S | Save the game |
| `load_game` | Ctrl+L | Load the game |
| `select_grenade` | Ctrl+' | Select the next grenade type |
| `arm_grenade` | Alt+' | Arm the selected grenade |
| `select_drug` | Ctrl+; | Select the next drug/patch |
| `use_drug` | Alt+; | Use the selected drug/patch |
| `toggle_online_help` | Ctrl+H | Toggle the online help overlay |
| `show_help` | Alt+O | Show the help screen |
| `toggle_music` | Ctrl+M | Toggle music |
| `quit` | Ctrl+Q | Quit the game - opt-in only; nothing binds this by default |
| `cycle_weapon_next` | Tab | Ready the next weapon |
| `cycle_weapon_prev` | Shift+Tab | Ready the previous weapon |
| `cycle_detail` | Ctrl+1 | Cycle the graphics detail level |
| `reload_weapon_primary` | Ctrl+Backspace | Reload the current weapon's primary ammo |
| `reload_weapon_secondary` | Alt+Backspace | Reload the current weapon's secondary ammo |
| `hud_color` | Alt+H | Cycle the HUD color scheme |
| `clear_fullscreen` | Backspace | Close a fullscreen view (e.g. a datapad or email) |
| `mfd_left_1`-`mfd_left_5` | F1-F5 | Set the left MFD panel to Weapon/Item/Map/Target/Info |
| `mfd_right_1`-`mfd_right_5` | F6-F10 | Set the right MFD panel to Weapon/Item/Map/Target/Info |
| `fire_weapon` | Enter | Fire the readied weapon at the cursor (the engine's default fire key). Clicking in the 3D view with `mouse_click` also fires; this is a separate button for it |
**Not exposed** (other `HotKeyLookup[]` entries):
- `keypad 0`-`keypad 9`: a controller button per digit is not useful; use the
on-screen keyboard.
- `toggle_freelook` (F): the free-view actions cover mouse look.
- The 4 `cheat_*` entries: debug cheats. They are on the menu's Cheats tab instead.
- `mac_help` (disabled) and `audiolog_cancel` (its build flag is not
defined): they are not real hotkeys in this build.
- `toggle_opengl` (Ctrl+G): the Video Options tab's Renderer buttons already
toggle the same setting.
- The 10 numeric hardware-item hotkeys (bio scan, fullscreen, 360 view,
lantern, shield, infrared, nav unit, data reader, booster, jumpjets): their
registered keys never match a real keypress in the engine.
### Raw input identifiers
Every raw physical input available to bind an action to, with its generic
OpenXR interaction-profile component name and its Meta Quest Touch control
name. Two symmetric identifiers (`trigger`, `squeeze`) are also accepted, but
only for `mouse_click`/`mouse_double_click`: they mean "either hand's own
trigger/grip, whichever hand is pointing". Every other identifier below
refers to one specific control.
| Identifier | OpenXR component (Touch) | Meta Quest control |
|---|---|---|
| `left_trigger` | `/user/hand/left/input/trigger/value` | Left Trigger |
| `right_trigger` | `/user/hand/right/input/trigger/value` | Right Trigger |
| `left_squeeze` | `/user/hand/left/input/squeeze/value` | Left Grip |
| `right_squeeze` | `/user/hand/right/input/squeeze/value` | Right Grip |
| `left_x` | `/user/hand/left/input/x/click` | X Button |
| `left_y` | `/user/hand/left/input/y/click` | Y Button |
| `right_a` | `/user/hand/right/input/a/click` | A Button |
| `right_b` | `/user/hand/right/input/b/click` | B Button |
| `left_thumbstick_click` | `/user/hand/left/input/thumbstick/click` | Left Thumbstick Click |
| `right_thumbstick_click` | `/user/hand/right/input/thumbstick/click` | Right Thumbstick Click |
| `left_thumbstick_up/down/left/right` | `/user/hand/left/input/thumbstick` (digitized) | Left Thumbstick, tilted |
| `right_thumbstick_up/down/left/right` | `/user/hand/right/input/thumbstick` (digitized) | Right Thumbstick, tilted |
| `trigger` | either hand's trigger | Either Trigger (`mouse_click`/`mouse_double_click` only) |
| `squeeze` | either hand's grip | Either Grip (`mouse_click`/`mouse_double_click` only) |
| `none` | - | Unbound - the action never fires |
**Menu button** (left controller, Touch/Vive/generic alike) is not in this
list - it always opens/closes the VR menu and can never be reassigned.
**Other supported controllers**: HTC Vive wands have no A/B/X/Y and no
analog grip - `left_x`/`left_y`/`right_a`/`right_b` simply never fire on
that profile, and `left/right_squeeze` are a plain click rather than
analog. Their trackpad stands in for the thumbstick identifiers above
(same digitization, just a trackpad instead of a stick). A bare-minimum
OpenXR controller (`khr/simple_controller`, used as a fallback when
neither Touch nor Vive bindings are suggested) only has `left_trigger`/
`right_trigger` (via its `select/click` component) and the menu button -
every other identifier above simply never fires on it, which is why the
Controller Options tab shows a note and hides irrelevant rows when this
profile is active.
### Controller profile files (`controller_profiles/`)
The Controller Options tab shows a labeled illustration of whichever
controller is actually active, driven by `res/controller_profiles/*.json`
(bundled with the app, copied to `/sdcard/questshock/controller_profiles/`
on every launch, refreshing the known bundled files without touching any
extra ones you add there yourself). Each file:
```json
{
"name": "Meta Quest Touch",
"interaction_profile": "/interaction_profiles/oculus/touch_controller",
"image": "touch.png",
"controls": [
{"id": "right_b", "label": "B", "anchor_x": 0.76, "anchor_y": 0.22}
]
}
```
- `interaction_profile` - the OpenXR interaction profile path this file
describes; matched against whichever one is actually bound at runtime.
Falls back to `generic.json` if nothing matches.
- `image` - filename (same folder) of the controller illustration.
- `controls` - one entry per labeled point on that illustration: `id` is a
raw input identifier from the table above (thumbstick directions share
one `left_thumbstick`/`right_thumbstick` entry, since they're the same
physical stick); `anchor_x`/`anchor_y` are 0-1 fractions of the image for
positioning that label's pointer line.
This is presentation data only - adding real support for a new controller
still needs matching bindings in the app itself; a profile file with no
corresponding code-level support just never gets matched.
### Fatal startup error
If the OpenXR runtime rejects any of the three supported controller
profiles' bindings at launch (an app bug, not something you did), a
non-cancelable dialog explains this and exits - this should never happen
in a normal build.
+4
View File
@@ -104,10 +104,14 @@ def stageEngineTask = tasks.register("stageEngine", Exec) {
inputs.dir("${projectDir}/../gl4es-src") inputs.dir("${projectDir}/../gl4es-src")
inputs.dir("${projectDir}/../gl4es-patches") inputs.dir("${projectDir}/../gl4es-patches")
inputs.file("${repoRoot}/build-image/Dockerfile") inputs.file("${repoRoot}/build-image/Dockerfile")
// Staged into assets/ by prepare-android-project.sh.
inputs.file("${repoRoot}/res/questshock.config.default.json")
inputs.dir("${repoRoot}/res/controller_profiles")
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")
outputs.file("${projectDir}/../engine.properties") outputs.file("${projectDir}/../engine.properties")
outputs.dir("${projectDir}/src/main/assets")
} }
// preBuild is what every variant's compile/native-build tasks already // preBuild is what every variant's compile/native-build tasks already
+673 -23
View File
@@ -28,6 +28,10 @@
#include "tools.h" #include "tools.h"
#define QS_CONFIG_FILENAME "questshock.config.json" #define QS_CONFIG_FILENAME "questshock.config.json"
// Bundled with the app (QuestShockActivity.java extracts it fresh on every
// launch) and read only at startup, only if QS_CONFIG_FILENAME doesn't
// exist yet - never touched by Save/Load Configuration.
#define QS_CONFIG_DEFAULT_FILENAME "questshock.config.default.json"
// =========================================================================== // ===========================================================================
// Minimal JSON parser // Minimal JSON parser
@@ -330,6 +334,27 @@ static void qs_json_take_number_field(QsJsonParser *p, float *out, bool *has) {
typedef struct { typedef struct {
char renderer[16]; // "" = not present in the file char renderer[16]; // "" = not present in the file
char resolution[16]; // "" = not present in the file char resolution[16]; // "" = not present in the file
// The virtual screen's physical width (meters) for each of the two
// screen profiles - see qs_screen_profile_notify_loop_mode()'s own
// comment. Like QsParsedPlayerPosition below (and unlike renderer/
// resolution above), these are plain numbers, not string enums, and
// need their own has_* bools since 0.0f is a valid value.
float initial_screen_width_m, initial_screen_distance_m;
float game_screen_width_m, game_screen_distance_m;
bool has_initial_screen_width_m, has_initial_screen_distance_m;
bool has_game_screen_width_m, has_game_screen_distance_m;
// Each of the two options (width/distance) is offered as 5 preset
// buttons in the Video Options tab, generated as offset + step*index
// (index 0..4) rather than a fixed table - see qs_preset_value()'s
// own comment. Shared by both "Initial Screen" and "Game Screen"
// (both columns' preset buttons show the same 5 values per row) -
// config-file-only (no UI control): the buttons are for adjusting
// each profile's own current *value*, this offset/step pair is for
// reshaping what those buttons themselves offer.
float screen_width_offset_m, screen_width_step_m;
float screen_distance_offset_m, screen_distance_step_m;
bool has_screen_width_offset_m, has_screen_width_step_m;
bool has_screen_distance_offset_m, has_screen_distance_step_m;
} QsParsedVideoOptions; } QsParsedVideoOptions;
typedef struct { typedef struct {
@@ -344,6 +369,14 @@ typedef struct {
bool has_x, has_z, has_yaw_degrees; bool has_x, has_z, has_yaw_degrees;
} QsParsedPlayerPosition; } QsParsedPlayerPosition;
// One string slot per remappable action, indexed by
// xr_input_action_index_from_name(). This file does not list the actions
// itself; xr_input.c owns them. Sized above the current action count.
#define QS_MAX_PLAYER_CONTROL_ACTIONS 96
typedef struct {
char bindings[QS_MAX_PLAYER_CONTROL_ACTIONS][24]; // "" = not present in the file
} QsParsedPlayerControls;
typedef struct { typedef struct {
QsParsedVideoOptions video_options; QsParsedVideoOptions video_options;
bool has_video_options; bool has_video_options;
@@ -351,6 +384,8 @@ typedef struct {
bool has_general; bool has_general;
QsParsedPlayerPosition player_position; QsParsedPlayerPosition player_position;
bool has_player_position; bool has_player_position;
QsParsedPlayerControls player_controls;
bool has_player_controls;
} QsParsedConfig; } QsParsedConfig;
static void qs_parse_video_options_field(const char *key, QsJsonParser *p, void *ctx) { static void qs_parse_video_options_field(const char *key, QsJsonParser *p, void *ctx) {
@@ -359,6 +394,26 @@ static void qs_parse_video_options_field(const char *key, QsJsonParser *p, void
qs_json_take_string_field(p, out->renderer, sizeof(out->renderer)); qs_json_take_string_field(p, out->renderer, sizeof(out->renderer));
else if (strcmp(key, "resolution") == 0) else if (strcmp(key, "resolution") == 0)
qs_json_take_string_field(p, out->resolution, sizeof(out->resolution)); qs_json_take_string_field(p, out->resolution, sizeof(out->resolution));
else if (strcmp(key, "initial_screen_width_m") == 0)
qs_json_take_number_field(p, &out->initial_screen_width_m, &out->has_initial_screen_width_m);
else if (strcmp(key, "game_screen_width_m") == 0)
qs_json_take_number_field(p, &out->game_screen_width_m, &out->has_game_screen_width_m);
else if (strcmp(key, "initial_screen_distance_m") == 0)
qs_json_take_number_field(p, &out->initial_screen_distance_m,
&out->has_initial_screen_distance_m);
else if (strcmp(key, "game_screen_distance_m") == 0)
qs_json_take_number_field(p, &out->game_screen_distance_m,
&out->has_game_screen_distance_m);
else if (strcmp(key, "screen_width_offset_m") == 0)
qs_json_take_number_field(p, &out->screen_width_offset_m, &out->has_screen_width_offset_m);
else if (strcmp(key, "screen_width_step_m") == 0)
qs_json_take_number_field(p, &out->screen_width_step_m, &out->has_screen_width_step_m);
else if (strcmp(key, "screen_distance_offset_m") == 0)
qs_json_take_number_field(p, &out->screen_distance_offset_m,
&out->has_screen_distance_offset_m);
else if (strcmp(key, "screen_distance_step_m") == 0)
qs_json_take_number_field(p, &out->screen_distance_step_m,
&out->has_screen_distance_step_m);
else else
qs_json_skip_value(p); qs_json_skip_value(p);
} }
@@ -383,6 +438,17 @@ static void qs_parse_player_position_field(const char *key, QsJsonParser *p, voi
qs_json_skip_value(p); qs_json_skip_value(p);
} }
// Looks up `key` in xr_input.c's action-name list instead of a fixed strcmp
// chain.
static void qs_parse_player_controls_field(const char *key, QsJsonParser *p, void *ctx) {
QsParsedPlayerControls *out = (QsParsedPlayerControls *)ctx;
int index;
if (xr_input_action_index_from_name(key, &index) && index < QS_MAX_PLAYER_CONTROL_ACTIONS)
qs_json_take_string_field(p, out->bindings[index], sizeof(out->bindings[index]));
else
qs_json_skip_value(p);
}
static void qs_parse_top_level_field(const char *key, QsJsonParser *p, void *ctx) { static void qs_parse_top_level_field(const char *key, QsJsonParser *p, void *ctx) {
QsParsedConfig *out = (QsParsedConfig *)ctx; QsParsedConfig *out = (QsParsedConfig *)ctx;
if (strcmp(key, "video_options") == 0 && !p->error && p->p < p->end && *p->p == '{') if (strcmp(key, "video_options") == 0 && !p->error && p->p < p->end && *p->p == '{')
@@ -392,17 +458,20 @@ static void qs_parse_top_level_field(const char *key, QsJsonParser *p, void *ctx
else if (strcmp(key, "player_position") == 0 && !p->error && p->p < p->end && *p->p == '{') else if (strcmp(key, "player_position") == 0 && !p->error && p->p < p->end && *p->p == '{')
out->has_player_position = out->has_player_position =
qs_json_parse_object(p, qs_parse_player_position_field, &out->player_position); qs_json_parse_object(p, qs_parse_player_position_field, &out->player_position);
else if (strcmp(key, "player_controls") == 0 && !p->error && p->p < p->end && *p->p == '{')
out->has_player_controls =
qs_json_parse_object(p, qs_parse_player_controls_field, &out->player_controls);
else else
qs_json_skip_value(p); qs_json_skip_value(p);
} }
// Reads and parses QS_CONFIG_FILENAME into *out. Returns false (with *out // Reads and parses `filename` into *out. Returns false (with *out zeroed)
// zeroed) if the file doesn't exist, is empty, or fails to parse as valid // if the file doesn't exist, is empty, or fails to parse as valid JSON -
// JSON - logging a WARN only for the last case, since a missing file is the // logging a WARN only for the last case, since a missing file is the
// normal/expected state, not an error. // normal/expected state, not an error.
static bool qs_config_read_file(QsParsedConfig *out) { static bool qs_config_read_file_named(const char *filename, QsParsedConfig *out) {
memset(out, 0, sizeof(*out)); memset(out, 0, sizeof(*out));
FILE *f = fopen(QS_CONFIG_FILENAME, "rb"); FILE *f = fopen(filename, "rb");
if (!f) if (!f)
return false; return false;
fseek(f, 0, SEEK_END); fseek(f, 0, SEEK_END);
@@ -425,13 +494,17 @@ static bool qs_config_read_file(QsParsedConfig *out) {
bool ok = qs_json_parse_object(&parser, qs_parse_top_level_field, out) && !parser.error; bool ok = qs_json_parse_object(&parser, qs_parse_top_level_field, out) && !parser.error;
free(buf); free(buf);
if (!ok) { if (!ok) {
WARN("%s: malformed JSON - ignoring file", QS_CONFIG_FILENAME); WARN("%s: malformed JSON - ignoring file", filename);
memset(out, 0, sizeof(*out)); memset(out, 0, sizeof(*out));
return false; return false;
} }
return true; return true;
} }
static bool qs_config_read_file(QsParsedConfig *out) {
return qs_config_read_file_named(QS_CONFIG_FILENAME, out);
}
// =========================================================================== // ===========================================================================
// Video Options: renderer // Video Options: renderer
// =========================================================================== // ===========================================================================
@@ -531,6 +604,149 @@ static const char *qs_current_resolution(void) {
return "640x480"; // shouldn't happen - safe fallback for Save return "640x480"; // shouldn't happen - safe fallback for Save
} }
// ===========================================================================
// Video Options: screen profile (initial screen / game screen)
// ===========================================================================
// Live-appliable: xr_session_set_game_screen_shape() (xr_session.c) is a
// plain state setter with no XR calls, same category as
// xr_session_set_player_transform() - safe to call live or at startup.
#define QS_SCREEN_PROFILE_REQUIRES_RESTART false
// mainloop.h's own loop-mode #defines - declared locally rather than
// #include "mainloop.h", which pulls in an `extern LGRegion *_current_view;`
// this file can't resolve (LGRegion isn't visible via any header this file
// already includes) - same "avoid a header with declarations this file has
// no other reason to need" reasoning as screen_draw()/language_change()
// above. These two values must stay in sync with mainloop.h's own
// SETUP_LOOP/CUTSCENE_LOOP.
#define QS_SETUP_LOOP 4
#define QS_CUTSCENE_LOOP 6
// Each of the two options (width/distance) offers 5 preset buttons in
// the Video Options tab, generated as offset+step*index (index 0..4)
// rather than a fixed table - see qs_preset_value() below. Shared by
// both "Initial Screen" and "Game Screen" (both columns' preset buttons
// show the same 5 values per row), config-file-only (no UI control):
// unlike the four *_screen_width_m-style current-value statics below
// (button-driven, live-appliable, one pair per profile), these only
// reshape what the buttons themselves offer.
#define QS_NUM_SCREEN_PRESETS 5
static float s_screen_width_offset_m = 1.0f;
static float s_screen_width_step_m = 0.6f;
static float s_screen_distance_offset_m = 1.0f;
static float s_screen_distance_step_m = 0.5f;
// Generates preset index `index`'s value (index 0..QS_NUM_SCREEN_PRESETS-1)
// for a given offset/step pair - e.g. offset 1.0/step 0.6 generates
// 1.0, 1.6, 2.2, 2.8, 3.4 (this feature's original width presets).
static float qs_preset_value(float offset, float step, int index) {
return offset + step * (float)index;
}
// Inverse of qs_preset_value() - returns -1 (a legitimate result here,
// unlike kResolutions'-style "shouldn't happen" fallbacks - a hand-edited
// config file can set any value in between presets) if the current value
// doesn't exactly match one of the 5 presets this offset/step pair
// generates.
static int qs_preset_index(float offset, float step, float value) {
for (int i = 0; i < QS_NUM_SCREEN_PRESETS; i++)
if (qs_preset_value(offset, step, i) == value)
return i;
return -1;
}
// 1.6m/2.0m (qs_preset_value() index 1/2 of the shared offset/step
// defaults above) for both profiles - the game screen's own original,
// pre-this-feature hardcoded shape (QUAD_DISTANCE_METERS in xr_session.c),
// so a config file/build that never touches these renders identically to
// before until the player opts in.
static float s_initial_screen_width_m = 1.6f;
static float s_initial_screen_distance_m = 2.0f;
static float s_game_screen_width_m = 1.6f;
static float s_game_screen_distance_m = 2.0f;
// Which profile xr_session.c's game-screen shape was last set to - so
// qs_screen_profile_notify_loop_mode() can skip re-applying anything on a
// same-profile transition (e.g. game -> automap -> game), and so a preset
// button tap while the *other* profile is active knows to just update the
// stored value without touching the live shape (see
// qs_config_handle_event()'s own screen-profile branches).
static bool s_screen_profile_is_game = false;
static float qs_clamp_screen_width(float width_m) {
if (width_m < 0.2f)
return 0.2f;
if (width_m > 10.0f)
return 10.0f;
return width_m;
}
static float qs_clamp_screen_distance(float distance_m) {
if (distance_m < 0.5f)
return 0.5f;
if (distance_m > 10.0f)
return 10.0f;
return distance_m;
}
static void qs_screen_profile_reapply(void) {
if (s_screen_profile_is_game)
xr_session_set_game_screen_shape(s_game_screen_width_m, 0.0f, s_game_screen_distance_m);
else
xr_session_set_game_screen_shape(s_initial_screen_width_m, 0.0f,
s_initial_screen_distance_m);
}
// Called directly from the engine (see android/engine-patches/
// 22-android-screen-profile.patch's hook into mainloop.c's
// loopmode_switch(), and qs_config_load_startup() below for the initial
// call) - runs on the engine's own thread already, so unlike every other
// action in this file (all UI-thread-button-triggered), no cross-thread
// SDL-event dispatch is needed here.
void qs_screen_profile_notify_loop_mode(short new_mode) {
bool isGame = new_mode != QS_SETUP_LOOP && new_mode != QS_CUTSCENE_LOOP;
if (isGame == s_screen_profile_is_game)
return;
s_screen_profile_is_game = isGame;
qs_screen_profile_reapply();
}
static bool qs_apply_initial_screen_width(float width_m) {
width_m = qs_clamp_screen_width(width_m);
bool changed = s_initial_screen_width_m != width_m;
s_initial_screen_width_m = width_m;
if (changed && !s_screen_profile_is_game)
qs_screen_profile_reapply();
return changed;
}
static bool qs_apply_game_screen_width(float width_m) {
width_m = qs_clamp_screen_width(width_m);
bool changed = s_game_screen_width_m != width_m;
s_game_screen_width_m = width_m;
if (changed && s_screen_profile_is_game)
qs_screen_profile_reapply();
return changed;
}
static bool qs_apply_initial_screen_distance(float distance_m) {
distance_m = qs_clamp_screen_distance(distance_m);
bool changed = s_initial_screen_distance_m != distance_m;
s_initial_screen_distance_m = distance_m;
if (changed && !s_screen_profile_is_game)
qs_screen_profile_reapply();
return changed;
}
static bool qs_apply_game_screen_distance(float distance_m) {
distance_m = qs_clamp_screen_distance(distance_m);
bool changed = s_game_screen_distance_m != distance_m;
s_game_screen_distance_m = distance_m;
if (changed && s_screen_profile_is_game)
qs_screen_profile_reapply();
return changed;
}
// =========================================================================== // ===========================================================================
// General: language // General: language
// =========================================================================== // ===========================================================================
@@ -682,6 +898,38 @@ static bool qs_apply_player_position(const QsParsedPlayerPosition *parsed) {
return changed; return changed;
} }
// ===========================================================================
// General: player controls
// ===========================================================================
// Live-appliable: xr_input_set_action_binding() (xr_input.c) just writes
// into the per-frame lookup table xr_input_sync_and_draw() already reads
// every frame - no restart distinction needed, same as player_position.
#define QS_PLAYER_CONTROLS_REQUIRES_RESTART false
// Applies only the actions present in *parsed and leaves the rest untouched.
// Invalid raw-input names (a typo, or a symmetric form on the wrong action -
// see xr_input_set_action_binding()) are WARN+skipped per action.
static bool qs_apply_player_controls(const QsParsedPlayerControls *parsed) {
bool changed = false;
int count = xr_input_action_count();
if (count > QS_MAX_PLAYER_CONTROL_ACTIONS)
count = QS_MAX_PLAYER_CONTROL_ACTIONS;
for (int i = 0; i < count; i++) {
if (parsed->bindings[i][0] == '\0')
continue;
int before = xr_input_get_action_binding(i);
if (!xr_input_set_action_binding(i, parsed->bindings[i])) {
WARN("%s: player_controls.%s=\"%s\" is invalid - ignoring", QS_CONFIG_FILENAME,
xr_input_action_name(i), parsed->bindings[i]);
continue;
}
if (xr_input_get_action_binding(i) != before)
changed = true;
}
return changed;
}
// =========================================================================== // ===========================================================================
// Read-only queries for the Video Options tab (see qs_config.h) // Read-only queries for the Video Options tab (see qs_config.h)
// =========================================================================== // ===========================================================================
@@ -697,6 +945,45 @@ int qs_config_current_resolution_index(void) {
int qs_config_current_language(void) { return gShockPrefs.goLanguage; } int qs_config_current_language(void) { return gShockPrefs.goLanguage; }
// -1 (no exact preset match) is a legitimate result for all four of these -
// see qs_preset_index()'s own comment.
int qs_config_current_initial_screen_width_index(void) {
return qs_preset_index(s_screen_width_offset_m, s_screen_width_step_m, s_initial_screen_width_m);
}
int qs_config_current_initial_screen_distance_index(void) {
return qs_preset_index(s_screen_distance_offset_m, s_screen_distance_step_m,
s_initial_screen_distance_m);
}
int qs_config_current_game_screen_width_index(void) {
return qs_preset_index(s_screen_width_offset_m, s_screen_width_step_m, s_game_screen_width_m);
}
int qs_config_current_game_screen_distance_index(void) {
return qs_preset_index(s_screen_distance_offset_m, s_screen_distance_step_m,
s_game_screen_distance_m);
}
// Returns the value preset button `index` (0..QS_NUM_SCREEN_PRESETS-1)
// currently shows for a given option - shared by both profiles' columns
// (see the offset/step statics' own comment) - so MenuOverlay.java can
// label its preset buttons correctly even after a config-file offset/step
// change. Out-of-range index clamps into range rather than returning a
// sentinel: unlike the index getters above, there's no "not present"
// concept here - every index 0..4 always has a value.
static int qs_clamp_preset_index(int index) {
if (index < 0)
return 0;
if (index >= QS_NUM_SCREEN_PRESETS)
return QS_NUM_SCREEN_PRESETS - 1;
return index;
}
float qs_config_screen_width_preset_value(int index) {
return qs_preset_value(s_screen_width_offset_m, s_screen_width_step_m, qs_clamp_preset_index(index));
}
float qs_config_screen_distance_preset_value(int index) {
return qs_preset_value(s_screen_distance_offset_m, s_screen_distance_step_m,
qs_clamp_preset_index(index));
}
// =========================================================================== // ===========================================================================
// Apply / Save orchestration // Apply / Save orchestration
// =========================================================================== // ===========================================================================
@@ -734,6 +1021,41 @@ static void qs_config_apply_parsed(const QsParsedConfig *parsed, bool live, char
if (changed && QS_RESOLUTION_REQUIRES_RESTART) if (changed && QS_RESOLUTION_REQUIRES_RESTART)
qs_append_restart_reason(restart_needed, restart_needed_size, "resolution"); qs_append_restart_reason(restart_needed, restart_needed_size, "resolution");
} }
if (parsed->video_options.has_initial_screen_width_m) {
bool changed = qs_apply_initial_screen_width(parsed->video_options.initial_screen_width_m);
if (changed && QS_SCREEN_PROFILE_REQUIRES_RESTART)
qs_append_restart_reason(restart_needed, restart_needed_size, "initial_screen_width_m");
}
if (parsed->video_options.has_game_screen_width_m) {
bool changed = qs_apply_game_screen_width(parsed->video_options.game_screen_width_m);
if (changed && QS_SCREEN_PROFILE_REQUIRES_RESTART)
qs_append_restart_reason(restart_needed, restart_needed_size, "game_screen_width_m");
}
if (parsed->video_options.has_initial_screen_distance_m) {
bool changed =
qs_apply_initial_screen_distance(parsed->video_options.initial_screen_distance_m);
if (changed && QS_SCREEN_PROFILE_REQUIRES_RESTART)
qs_append_restart_reason(restart_needed, restart_needed_size,
"initial_screen_distance_m");
}
if (parsed->video_options.has_game_screen_distance_m) {
bool changed =
qs_apply_game_screen_distance(parsed->video_options.game_screen_distance_m);
if (changed && QS_SCREEN_PROFILE_REQUIRES_RESTART)
qs_append_restart_reason(restart_needed, restart_needed_size,
"game_screen_distance_m");
}
// The four offset/step fields below only reshape what the preset
// buttons offer (see their own statics' comment) - no live-reapply
// or restart consideration needed, just a direct assignment.
if (parsed->video_options.has_screen_width_offset_m)
s_screen_width_offset_m = parsed->video_options.screen_width_offset_m;
if (parsed->video_options.has_screen_width_step_m)
s_screen_width_step_m = parsed->video_options.screen_width_step_m;
if (parsed->video_options.has_screen_distance_offset_m)
s_screen_distance_offset_m = parsed->video_options.screen_distance_offset_m;
if (parsed->video_options.has_screen_distance_step_m)
s_screen_distance_step_m = parsed->video_options.screen_distance_step_m;
} }
if (parsed->has_general) { if (parsed->has_general) {
if (parsed->general.language[0] != '\0') { if (parsed->general.language[0] != '\0') {
@@ -747,6 +1069,11 @@ static void qs_config_apply_parsed(const QsParsedConfig *parsed, bool live, char
if (changed && QS_PLAYER_POSITION_REQUIRES_RESTART) if (changed && QS_PLAYER_POSITION_REQUIRES_RESTART)
qs_append_restart_reason(restart_needed, restart_needed_size, "player_position"); qs_append_restart_reason(restart_needed, restart_needed_size, "player_position");
} }
if (parsed->has_player_controls) {
bool changed = qs_apply_player_controls(&parsed->player_controls);
if (changed && QS_PLAYER_CONTROLS_REQUIRES_RESTART)
qs_append_restart_reason(restart_needed, restart_needed_size, "player_controls");
}
} }
static bool qs_config_write_file(void) { static bool qs_config_write_file(void) {
@@ -758,11 +1085,27 @@ static bool qs_config_write_file(void) {
fprintf(f, "{\n"); fprintf(f, "{\n");
fprintf(f, " \"video_options\": {\n"); fprintf(f, " \"video_options\": {\n");
fprintf(f, " \"renderer\": \"%s\",\n", qs_current_renderer()); fprintf(f, " \"renderer\": \"%s\",\n", qs_current_renderer());
fprintf(f, " \"resolution\": \"%s\"\n", qs_current_resolution()); fprintf(f, " \"resolution\": \"%s\",\n", qs_current_resolution());
fprintf(f, " \"initial_screen_width_m\": %f,\n", s_initial_screen_width_m);
fprintf(f, " \"initial_screen_distance_m\": %f,\n", s_initial_screen_distance_m);
fprintf(f, " \"game_screen_width_m\": %f,\n", s_game_screen_width_m);
fprintf(f, " \"game_screen_distance_m\": %f,\n", s_game_screen_distance_m);
fprintf(f, " \"screen_width_offset_m\": %f,\n", s_screen_width_offset_m);
fprintf(f, " \"screen_width_step_m\": %f,\n", s_screen_width_step_m);
fprintf(f, " \"screen_distance_offset_m\": %f,\n", s_screen_distance_offset_m);
fprintf(f, " \"screen_distance_step_m\": %f\n", s_screen_distance_step_m);
fprintf(f, " },\n"); fprintf(f, " },\n");
fprintf(f, " \"general\": {\n"); fprintf(f, " \"general\": {\n");
fprintf(f, " \"language\": \"%s\"\n", qs_current_language_name()); fprintf(f, " \"language\": \"%s\"\n", qs_current_language_name());
fprintf(f, " },\n"); fprintf(f, " },\n");
{
int count = xr_input_action_count();
fprintf(f, " \"player_controls\": {\n");
for (int i = 0; i < count; i++)
fprintf(f, " \"%s\": \"%s\"%s\n", xr_input_action_name(i),
xr_input_get_action_binding_name(i), i + 1 < count ? "," : "");
fprintf(f, " },\n");
}
{ {
float x, z, yaw_degrees; float x, z, yaw_degrees;
xr_session_get_player_transform(&x, &z, &yaw_degrees); xr_session_get_player_transform(&x, &z, &yaw_degrees);
@@ -843,6 +1186,52 @@ static void qs_show_restart_dialog(const char *restart_needed) {
// only qs_apply_language()'s missing-resource-files warning above. // only qs_apply_language()'s missing-resource-files warning above.
static void qs_show_message_dialog(const char *message) { qs_call_dialog_method("showMessageDialog", message); } static void qs_show_message_dialog(const char *message) { qs_call_dialog_method("showMessageDialog", message); }
// Non-cancelable, single-"Exit"-button dialog (MenuOverlay.java's
// showFatalControllerBindingError()) - see qs_config_report_fatal_input_error()'s
// own comment (qs_config.h) on when/why this fires.
void qs_config_report_fatal_input_error(const char *profile_display_name) {
char message[256];
snprintf(message, sizeof(message),
"Controller support failed to initialize: the OpenXR runtime rejected the \"%s\" "
"interaction profile's bindings. This is an app bug, not something you can fix.",
profile_display_name);
qs_call_dialog_method("showFatalControllerBindingError", message);
}
// Shared by qs_config_notify_control_capture_result() below - resolves
// MenuOverlay.onControlCaptureResult(int, String), called once per changed
// row (up to twice per capture - see this function's own header comment).
static void qs_call_control_capture_result(int action_index, const char *raw_name) {
JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv();
jobject activity = (jobject)SDL_AndroidGetActivity();
if (!env || !activity)
return;
jclass menuOverlayClass = qs_find_class(env, activity, "de.ladkau.questshock.MenuOverlay");
if (!menuOverlayClass)
return;
jmethodID method = (*env)->GetStaticMethodID(env, menuOverlayClass, "onControlCaptureResult",
"(ILjava/lang/String;)V");
if (!method) {
(*env)->ExceptionClear(env);
return;
}
jstring rawNameStr = raw_name ? (*env)->NewStringUTF(env, raw_name) : NULL;
(*env)->CallStaticVoidMethod(env, menuOverlayClass, method, (jint)action_index, rawNameStr);
if ((*env)->ExceptionCheck(env))
(*env)->ExceptionClear(env);
}
void qs_config_notify_control_capture_result(int action_index, const char *raw_name,
int displaced_action_index,
const char *displaced_raw_name) {
qs_call_control_capture_result(action_index, raw_name);
if (displaced_action_index >= 0)
qs_call_control_capture_result(displaced_action_index, displaced_raw_name);
}
// =========================================================================== // ===========================================================================
// Public API (see qs_config.h) // Public API (see qs_config.h)
// =========================================================================== // ===========================================================================
@@ -853,24 +1242,47 @@ static Uint32 s_set_renderer_event_type;
static Uint32 s_set_resolution_event_type; static Uint32 s_set_resolution_event_type;
static Uint32 s_set_language_event_type; static Uint32 s_set_language_event_type;
static Uint32 s_reset_position_event_type; static Uint32 s_reset_position_event_type;
static Uint32 s_set_initial_screen_width_event_type;
static Uint32 s_set_initial_screen_distance_event_type;
static Uint32 s_set_game_screen_width_event_type;
static Uint32 s_set_game_screen_distance_event_type;
static Uint32 s_begin_control_capture_event_type;
static Uint32 s_cancel_control_capture_event_type;
static Uint32 s_reset_player_controls_event_type;
static Uint32 s_menu_visibility_event_type;
static Uint32 s_cheat_event_type;
void qs_config_load_startup(void) { void qs_config_load_startup(void) {
// Registers the custom SDL event types qs_config_request_save()/_load()/ // Registers the custom SDL event types qs_config_request_save()/_load()/
// _set_renderer()/_set_resolution()/_set_language()/_reset_position() // _set_renderer()/_set_resolution()/_set_language()/_reset_position()/
// enqueue - done here, once, on the engine's own thread and before the // the four _set_*_screen_*() screen-profile setters/the three
// menu/its buttons could possibly exist yet, rather than lazily from // control-capture/reset requests enqueue - done here, once, on the
// those functions themselves (which run on Android's UI thread and // engine's own thread and before the menu/its buttons could possibly
// could otherwise race each other doing this lazily). // exist yet, rather than lazily from those functions themselves (which
Uint32 base = SDL_RegisterEvents(6); // run on Android's UI thread and could otherwise race each other doing
// this lazily).
Uint32 base = SDL_RegisterEvents(15);
s_save_event_type = base; s_save_event_type = base;
s_load_event_type = base + 1; s_load_event_type = base + 1;
s_set_renderer_event_type = base + 2; s_set_renderer_event_type = base + 2;
s_set_resolution_event_type = base + 3; s_set_resolution_event_type = base + 3;
s_set_language_event_type = base + 4; s_set_language_event_type = base + 4;
s_reset_position_event_type = base + 5; s_reset_position_event_type = base + 5;
s_set_initial_screen_width_event_type = base + 6;
s_set_game_screen_width_event_type = base + 7;
s_set_initial_screen_distance_event_type = base + 8;
s_set_game_screen_distance_event_type = base + 9;
s_begin_control_capture_event_type = base + 10;
s_cancel_control_capture_event_type = base + 11;
s_reset_player_controls_event_type = base + 12;
s_menu_visibility_event_type = base + 13;
s_cheat_event_type = base + 14;
QsParsedConfig parsed; QsParsedConfig parsed;
if (qs_config_read_file(&parsed)) bool haveConfig = qs_config_read_file(&parsed);
if (!haveConfig)
haveConfig = qs_config_read_file_named(QS_CONFIG_DEFAULT_FILENAME, &parsed);
if (haveConfig)
qs_config_apply_parsed(&parsed, /*live=*/false, NULL, 0); qs_config_apply_parsed(&parsed, /*live=*/false, NULL, 0);
// A saved player_position only means anything if all three fields are // A saved player_position only means anything if all three fields are
@@ -884,6 +1296,19 @@ void qs_config_load_startup(void) {
if (parsed.has_player_position && parsed.player_position.has_x && if (parsed.has_player_position && parsed.player_position.has_x &&
parsed.player_position.has_z && parsed.player_position.has_yaw_degrees) parsed.player_position.has_z && parsed.player_position.has_yaw_degrees)
xr_input_skip_startup_recenter(); xr_input_skip_startup_recenter();
// The engine's own startup state is always QS_SETUP_LOOP (mainloop.c:
// `short _current_loop = SETUP_LOOP;`, Shock.c's main() never changes
// it before entering mainloop()), matching s_screen_profile_is_game's
// own default (false/"initial") - so calling
// qs_screen_profile_notify_loop_mode() here would see "no change" and
// skip actually applying anything (its whole point is to skip
// redundant re-applies on a same-profile transition, but there's no
// "previous" profile yet this early). Call the apply step directly
// instead, unconditionally, so whichever profile's shape was just
// loaded/defaulted above actually reaches xr_session.c before the
// first frame renders.
qs_screen_profile_reapply();
} }
void qs_config_request_save(void) { void qs_config_request_save(void) {
@@ -931,6 +1356,180 @@ void qs_config_request_reset_position(void) {
SDL_PushEvent(&event); SDL_PushEvent(&event);
} }
// Screen profile preset buttons (Video Options tab) - index into the
// shared width/distance offset+step-generated presets (see
// qs_preset_value()'s own comment), same enqueue-only reasoning as every
// other button in this file.
void qs_config_request_set_initial_screen_width(int preset_index) {
SDL_Event event;
SDL_zero(event);
event.type = s_set_initial_screen_width_event_type;
event.user.code = preset_index;
SDL_PushEvent(&event);
}
void qs_config_request_set_game_screen_width(int preset_index) {
SDL_Event event;
SDL_zero(event);
event.type = s_set_game_screen_width_event_type;
event.user.code = preset_index;
SDL_PushEvent(&event);
}
void qs_config_request_set_initial_screen_distance(int preset_index) {
SDL_Event event;
SDL_zero(event);
event.type = s_set_initial_screen_distance_event_type;
event.user.code = preset_index;
SDL_PushEvent(&event);
}
void qs_config_request_set_game_screen_distance(int preset_index) {
SDL_Event event;
SDL_zero(event);
event.type = s_set_game_screen_distance_event_type;
event.user.code = preset_index;
SDL_PushEvent(&event);
}
// Controller Options tab (see qs_config.h) - same enqueue-only reasoning as
// every button above.
void qs_config_request_begin_control_capture(int action_index) {
SDL_Event event;
SDL_zero(event);
event.type = s_begin_control_capture_event_type;
event.user.code = action_index;
SDL_PushEvent(&event);
}
void qs_config_request_cancel_control_capture(void) {
SDL_Event event;
SDL_zero(event);
event.type = s_cancel_control_capture_event_type;
SDL_PushEvent(&event);
}
void qs_config_request_cheat(int cheat_id) {
SDL_Event event;
SDL_zero(event);
event.type = s_cheat_event_type;
event.user.code = cheat_id;
SDL_PushEvent(&event);
}
void qs_config_notify_menu_visibility(bool visible) {
SDL_Event event;
SDL_zero(event);
event.type = s_menu_visibility_event_type;
event.user.code = visible ? 1 : 0;
SDL_PushEvent(&event);
}
void qs_config_request_reset_player_controls(void) {
SDL_Event event;
SDL_zero(event);
event.type = s_reset_player_controls_event_type;
SDL_PushEvent(&event);
}
// hkeyfunc.c - the same toggle the game's own "pause" hotkey ('p') calls,
// which also handles the mouse capture/cursor/sound side of pausing.
extern uchar game_paused;
extern uchar pause_game_func(ushort keycode, uint32_t context, intptr_t data);
// True only while a pause this file caused is (as far as it knows) still in
// effect - see qs_config_notify_menu_visibility().
static bool s_menu_paused_game = false;
// s_screen_profile_is_game is "a game is running" (anything but the setup/
// main-menu and cutscene loops, see qs_screen_profile_notify_loop_mode()).
static void qs_apply_menu_pause(bool menu_visible) {
if (menu_visible) {
if (s_screen_profile_is_game && !game_paused) {
pause_game_func(0, 0, 0);
s_menu_paused_game = true;
}
} else if (s_menu_paused_game) {
s_menu_paused_game = false;
// Only toggle back if it's still paused - the game may have
// unpaused itself (or the player did) while the menu was open, and
// toggling then would pause it instead.
if (game_paused)
pause_game_func(0, 0, 0);
}
}
// hkeyfunc.c - the engine's own cheat hotkey handlers, and the refill added by
// android/engine-patches/26-android-cheat-refill.patch.
extern uchar toggle_giveall_func(ushort keycode, uint32_t context, intptr_t data);
extern uchar toggle_physics_func(ushort keycode, uint32_t context, intptr_t data);
extern uchar toggle_up_level_func(ushort keycode, uint32_t context, intptr_t data);
extern uchar toggle_down_level_func(ushort keycode, uint32_t context, intptr_t data);
extern void qs_cheat_refill(int what);
extern uchar physics_running;
#define QS_CHEAT_GIVE_ALL 0
#define QS_CHEAT_PHYSICS 1
#define QS_CHEAT_LEVEL_UP 2
#define QS_CHEAT_LEVEL_DOWN 3
#define QS_CHEAT_UNLIMITED_HEALTH 4
#define QS_CHEAT_UNLIMITED_ENERGY 5
#define QS_CHEAT_UNLIMITED_AMMO 6
#define QS_CHEAT_UNLIMITED_ITEMS 7
// Bits of s_cheat_unlimited; the values match qs_cheat_refill() in patch 26.
#define QS_CHEAT_MASK_HEALTH 1
#define QS_CHEAT_MASK_ENERGY 2
#define QS_CHEAT_MASK_AMMO 4
#define QS_CHEAT_MASK_ITEMS 8
static int s_cheat_unlimited = 0;
int qs_config_cheat_unlimited_mask(void) { return s_cheat_unlimited; }
bool qs_config_cheat_physics_running(void) { return physics_running != 0; }
void qs_config_frame_tick(void) {
if (s_cheat_unlimited && s_screen_profile_is_game)
qs_cheat_refill(s_cheat_unlimited);
}
static void qs_apply_cheat(int cheat_id) {
if (cheat_id >= QS_CHEAT_UNLIMITED_HEALTH && cheat_id <= QS_CHEAT_UNLIMITED_ITEMS) {
static const struct {
int mask;
const char *on, *off;
} kUnlimited[] = {
{QS_CHEAT_MASK_HEALTH, "Unlimited health on", "Unlimited health off"},
{QS_CHEAT_MASK_ENERGY, "Unlimited energy on", "Unlimited energy off"},
{QS_CHEAT_MASK_AMMO, "Unlimited ammo on", "Unlimited ammo off"},
{QS_CHEAT_MASK_ITEMS, "Unlimited items on", "Unlimited items off"},
};
const int i = cheat_id - QS_CHEAT_UNLIMITED_HEALTH;
s_cheat_unlimited ^= kUnlimited[i].mask;
message_info((s_cheat_unlimited & kUnlimited[i].mask) ? kUnlimited[i].on : kUnlimited[i].off);
return;
}
// The engine's own handlers assume a running game (player state, levels).
if (!s_screen_profile_is_game) {
message_info("Cheats need a running game");
return;
}
switch (cheat_id) {
case QS_CHEAT_GIVE_ALL:
toggle_giveall_func(0, 0, 0);
break;
case QS_CHEAT_PHYSICS:
toggle_physics_func(0, 0, 0);
break;
case QS_CHEAT_LEVEL_UP:
toggle_up_level_func(0, 0, 0);
break;
case QS_CHEAT_LEVEL_DOWN:
toggle_down_level_func(0, 0, 0);
break;
}
}
bool qs_config_handle_event(const SDL_Event *event) { bool qs_config_handle_event(const SDL_Event *event) {
if (event->type == s_save_event_type) { if (event->type == s_save_event_type) {
if (qs_config_write_file()) if (qs_config_write_file())
@@ -970,17 +1569,68 @@ bool qs_config_handle_event(const SDL_Event *event) {
return true; return true;
} }
if (event->type == s_reset_position_event_type) { if (event->type == s_reset_position_event_type) {
// Deliberately not xr_session_set_player_transform(0,0,0) - see // Re-anchors the position onto the player's current real head position
// xr_input_recenter_player_to_screen()'s own comment: a "return to // and keeps their facing (see xr_input_reset_player_position()). Does
// (0,0,0)" reset only undoes thumbstick locomotion, not any real, // not read or write questshock.config.json.
// physical drift/rotation since the session started, which is what xr_input_reset_player_position();
// actually made this feel "slightly off" - recentering onto the
// player's current real head pose fixes both at once, and is
// unrelated to questshock.config.json (this event doesn't read or
// write the config file at all, just like this one never has).
xr_input_recenter_player_to_screen();
message_info("Recentered"); message_info("Recentered");
return true; return true;
} }
if (event->type == s_set_initial_screen_width_event_type) {
int index = event->user.code;
if (index >= 0 && index < QS_NUM_SCREEN_PRESETS)
qs_apply_initial_screen_width(
qs_preset_value(s_screen_width_offset_m, s_screen_width_step_m, index));
return true;
}
if (event->type == s_set_game_screen_width_event_type) {
int index = event->user.code;
if (index >= 0 && index < QS_NUM_SCREEN_PRESETS)
qs_apply_game_screen_width(
qs_preset_value(s_screen_width_offset_m, s_screen_width_step_m, index));
return true;
}
if (event->type == s_set_initial_screen_distance_event_type) {
int index = event->user.code;
if (index >= 0 && index < QS_NUM_SCREEN_PRESETS)
qs_apply_initial_screen_distance(
qs_preset_value(s_screen_distance_offset_m, s_screen_distance_step_m, index));
return true;
}
if (event->type == s_set_game_screen_distance_event_type) {
int index = event->user.code;
if (index >= 0 && index < QS_NUM_SCREEN_PRESETS)
qs_apply_game_screen_distance(
qs_preset_value(s_screen_distance_offset_m, s_screen_distance_step_m, index));
return true;
}
if (event->type == s_begin_control_capture_event_type) {
xr_input_capture_begin(event->user.code);
return true;
}
if (event->type == s_cancel_control_capture_event_type) {
xr_input_capture_cancel();
return true;
}
if (event->type == s_cheat_event_type) {
qs_apply_cheat(event->user.code);
return true;
}
if (event->type == s_menu_visibility_event_type) {
qs_apply_menu_pause(event->user.code != 0);
return true;
}
if (event->type == s_reset_player_controls_event_type) {
// Built-in defaults first (they cover every action), then
// questshock.config.default.json's player_controls group, if it has
// one, overrides the actions it specifies.
xr_input_reset_player_controls();
QsParsedConfig defaultParsed;
if (qs_config_read_file_named(QS_CONFIG_DEFAULT_FILENAME, &defaultParsed) &&
defaultParsed.has_player_controls)
qs_apply_player_controls(&defaultParsed.player_controls);
message_info("Controller bindings reset to defaults");
return true;
}
return false; return false;
} }
+104 -14
View File
@@ -56,25 +56,93 @@ void qs_config_request_set_resolution(int resolution_index);
// menu applies a language change (see qs_config.c's qs_apply_language()). // menu applies a language change (see qs_config.c's qs_apply_language()).
void qs_config_request_set_language(int language_index); void qs_config_request_set_language(int language_index);
// Same cross-thread-dispatch reasoning as the three above - called from the // Same cross-thread dispatch as the three above - called from the Main Menu
// Main Menu tab's "Reset Position" button. A pure, live "return to a sane // tab's "Reset Position" button. Re-anchors the player's position in front of
// view of the game quad" action (see xr_input.h's own // the game quad (see xr_input_reset_player_position()); never reads or writes
// xr_input_recenter_player_to_screen(), which this dispatches to) - // the config file.
// unrelated to questshock.config.json despite living in this file for the
// cross-thread dispatch machinery: it never reads or writes the config
// file, and isn't a "reset to (0,0,0)" - it recenters based on the
// player's current real head pose instead.
void qs_config_request_reset_position(void); void qs_config_request_reset_position(void);
// Returns true and handles the event if it's one of the six custom events // Same cross-thread-dispatch reasoning as the four above - called from the
// qs_config_request_save()/_load()/_set_renderer()/_set_resolution()/ // Video Options tab's "Initial Screen"/"Game Screen" width/distance
// _set_language()/_reset_position() enqueue - called from pump_events() // preset rows (MenuOverlay.java). preset_index indexes qs_config.c's own
// shared, offset+step-generated width/distance preset lists (both in
// meters; the offset/step themselves are config-file-only, not exposed
// as buttons), read back via the four qs_config_current_*_screen_*_index()
// getters below. Applies live immediately if the just-changed profile is
// the currently active one (see CONFIG.md) - otherwise just updates the
// stored preference, invisibly, until that profile becomes active (e.g.
// tweaking "Initial Screen" mid-game).
void qs_config_request_set_initial_screen_width(int preset_index);
void qs_config_request_set_initial_screen_distance(int preset_index);
void qs_config_request_set_game_screen_width(int preset_index);
void qs_config_request_set_game_screen_distance(int preset_index);
// Cross-thread requests from the Controller Options tab (MenuOverlay.java),
// enqueued like every request_*() above. action_index indexes
// xr_input_action_name()'s list. Begin/cancel start and stop rebind capture;
// reset restores all bindings (the built-in defaults, then
// questshock.config.default.json's player_controls group if it has one).
void qs_config_request_begin_control_capture(int action_index);
void qs_config_request_cancel_control_capture(void);
void qs_config_request_reset_player_controls(void);
// Called from xr_input.c's xr_input_process_capture() on the engine's own
// thread, so no cross-thread dispatch is needed (as with
// qs_screen_profile_notify_loop_mode() below). Resolves
// MenuOverlay.onControlCaptureResult(int, String) for action_index (raw_name
// NULL on timeout or cancel), and again for displaced_action_index if the new
// binding took the input from another action (-1/NULL when none), so both
// rows can be updated together.
void qs_config_notify_control_capture_result(int action_index, const char *raw_name,
int displaced_action_index,
const char *displaced_raw_name);
// Called from xr_input.c whenever the VR menu's visibility changes. Showing
// the menu pauses a running game (not the main menu, intro or cutscenes) that
// is not already paused; hiding it unpauses only if the menu did the pausing.
// Enqueued like the request_*() functions above; the pause itself runs on the
// engine's thread at pump_events() time.
void qs_config_notify_menu_visibility(bool visible);
// Cheats tab (MenuOverlay.java). Same enqueue-only requests as above; the work
// runs on the engine's thread. cheat_id: 0 give everything, 1 toggle physics,
// 2 next level, 3 previous level, 4-7 toggle unlimited health, energy, ammo,
// items (grenades and drugs). Cheats need a running game (the unlimited
// switches can be set anywhere). The getters are plain scalar reads, safe from
// the UI thread; the unlimited mask has bit 0 health, 1 energy, 2 ammo, 3 items.
void qs_config_request_cheat(int cheat_id);
int qs_config_cheat_unlimited_mask(void);
bool qs_config_cheat_physics_running(void);
// Called once per frame from xr_input.c on the engine's thread; applies the
// unlimited-resource cheats that are switched on.
void qs_config_frame_tick(void);
// Called from xr_input.c's xr_input_suggest_bindings() when the OpenXR runtime
// rejects a supported interaction profile's suggested bindings at startup.
// That is an app bug, not a config problem, so it shows
// MenuOverlay.showFatalControllerBindingError()'s non-cancelable "Exit" dialog;
// xr_input_init() aborts right after.
void qs_config_report_fatal_input_error(const char *profile_display_name);
// Returns true and handles the event if it is one of the custom events the
// request_*()/notify functions above enqueue. Called from pump_events()
// (sdl_events.c, see android/engine-patches/21-android-config.patch) on the // (sdl_events.c, see android/engine-patches/21-android-config.patch) on the
// engine's own thread, once per polled event, before its normal switch on // engine's thread, once per polled event. Returns false for any other event
// well-known SDL_* event types. Returns false (no-op) for any other event // type, including before the custom types are registered.
// type, including before the six custom types have even been registered.
bool qs_config_handle_event(const SDL_Event *event); bool qs_config_handle_event(const SDL_Event *event);
// Called directly (no event-queue dispatch - see
// android/engine-patches/22-android-screen-profile.patch and this
// function's own definition in qs_config.c for why) from the engine's
// mainloop.c whenever the loop mode changes, and once from
// qs_config_load_startup() - picks between the "Initial Screen" (menu/
// intro) and "Game Screen" (actual gameplay) presets based on new_mode
// (mainloop.h's SETUP_LOOP/CUTSCENE_LOOP vs. everything else) and applies
// the newly-active one's shape to xr_session.c, but only if it's actually
// different from what was already active.
void qs_screen_profile_notify_loop_mode(short new_mode);
// Read-only queries for the Video Options tab's own selection highlight // Read-only queries for the Video Options tab's own selection highlight
// (MenuOverlay.java re-reads these whenever the tab is (re)shown, so it // (MenuOverlay.java re-reads these whenever the tab is (re)shown, so it
// reflects the live value even if it was last changed some other way, e.g. // reflects the live value even if it was last changed some other way, e.g.
@@ -87,6 +155,28 @@ bool qs_config_handle_event(const SDL_Event *event);
bool qs_config_current_renderer_is_opengl(void); bool qs_config_current_renderer_is_opengl(void);
int qs_config_current_resolution_index(void); int qs_config_current_resolution_index(void);
// -1 (not just an unlikely fallback, a routine, expected result here -
// unlike qs_config_current_resolution_index() above) if the profile's
// current value doesn't exactly match one of qs_config.c's own
// offset+step-generated width/distance presets, e.g. after a hand-edited
// config file sets an in-between value - MenuOverlay.java's own
// setSelected() naturally shows no button selected in that case, no
// special-casing needed.
int qs_config_current_initial_screen_width_index(void);
int qs_config_current_initial_screen_distance_index(void);
int qs_config_current_game_screen_width_index(void);
int qs_config_current_game_screen_distance_index(void);
// Returns the value preset button `index` (0..4) currently shows for the
// width/distance option - shared by both "Initial Screen" and "Game
// Screen" columns (their offset/step is config-file-only, not per-profile
// - see qs_config.c's own comment on the underlying statics), so
// MenuOverlay.java can label its preset buttons correctly even after a
// Load Configuration changes the offset/step. Index is clamped into
// range, not validated - every index 0..4 always has a value.
float qs_config_screen_width_preset_value(int index);
float qs_config_screen_distance_preset_value(int index);
// gShockPrefs.goLanguage (0 English/1 French/2 German, Prefs.h) - read by // gShockPrefs.goLanguage (0 English/1 French/2 German, Prefs.h) - read by
// MenuOverlay.java's/KeyboardOverlay.java's periodic header-refresh timer // MenuOverlay.java's/KeyboardOverlay.java's periodic header-refresh timer
// to pick which language's chrome text (Strings.java) to display, so a // to pick which language's chrome text (Strings.java) to display, so a
@@ -11,6 +11,7 @@
#include <SDL.h> #include <SDL.h>
#include "qs_config.h" #include "qs_config.h"
#include "xr_input.h"
#include "xr_overlay.h" #include "xr_overlay.h"
#include "xr_session.h" #include "xr_session.h"
@@ -132,6 +133,33 @@ Java_de_ladkau_questshock_MenuOverlay_nativeRequestResetPosition(JNIEnv *env, jc
qs_config_request_reset_position(); qs_config_request_reset_position();
} }
// MenuOverlay's Video Options tab - "Initial Screen"/"Game Screen" width/
// distance preset rows, same enqueue-only reasoning as above.
JNIEXPORT void JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetInitialScreenWidth(JNIEnv *env, jclass clazz,
jint presetIndex) {
qs_config_request_set_initial_screen_width(presetIndex);
}
JNIEXPORT void JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetGameScreenWidth(JNIEnv *env, jclass clazz,
jint presetIndex) {
qs_config_request_set_game_screen_width(presetIndex);
}
JNIEXPORT void JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetInitialScreenDistance(JNIEnv *env,
jclass clazz,
jint presetIndex) {
qs_config_request_set_initial_screen_distance(presetIndex);
}
JNIEXPORT void JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetGameScreenDistance(JNIEnv *env, jclass clazz,
jint presetIndex) {
qs_config_request_set_game_screen_distance(presetIndex);
}
// Read-only queries - safe to call directly (no enqueue needed, see // Read-only queries - safe to call directly (no enqueue needed, see
// qs_config.h's own comment on each of these). // qs_config.h's own comment on each of these).
JNIEXPORT jboolean JNICALL JNIEXPORT jboolean JNICALL
@@ -144,6 +172,42 @@ Java_de_ladkau_questshock_MenuOverlay_nativeGetResolutionIndex(JNIEnv *env, jcla
return qs_config_current_resolution_index(); return qs_config_current_resolution_index();
} }
JNIEXPORT jint JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetInitialScreenWidthIndex(JNIEnv *env, jclass clazz) {
return qs_config_current_initial_screen_width_index();
}
JNIEXPORT jint JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetGameScreenWidthIndex(JNIEnv *env, jclass clazz) {
return qs_config_current_game_screen_width_index();
}
JNIEXPORT jint JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetInitialScreenDistanceIndex(JNIEnv *env,
jclass clazz) {
return qs_config_current_initial_screen_distance_index();
}
JNIEXPORT jint JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetGameScreenDistanceIndex(JNIEnv *env, jclass clazz) {
return qs_config_current_game_screen_distance_index();
}
// Preset button *labels* (not the current value) - shared by both
// "Initial Screen" and "Game Screen" columns, since their offset/step is
// config-file-only, not per-profile (see qs_config.h's own comment).
JNIEXPORT jfloat JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetScreenWidthPresetValue(JNIEnv *env, jclass clazz,
jint index) {
return qs_config_screen_width_preset_value(index);
}
JNIEXPORT jfloat JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetScreenDistancePresetValue(JNIEnv *env, jclass clazz,
jint index) {
return qs_config_screen_distance_preset_value(index);
}
JNIEXPORT jint JNICALL JNIEXPORT jint JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetLanguage(JNIEnv *env, jclass clazz) { Java_de_ladkau_questshock_MenuOverlay_nativeGetLanguage(JNIEnv *env, jclass clazz) {
return qs_config_current_language(); return qs_config_current_language();
@@ -170,3 +234,55 @@ JNIEXPORT jfloat JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetGpuUtilization(JNIEnv *env, jclass clazz) { Java_de_ladkau_questshock_MenuOverlay_nativeGetGpuUtilization(JNIEnv *env, jclass clazz) {
return xr_session_get_gpu_utilization(); return xr_session_get_gpu_utilization();
} }
// Controller Options tab: actionIndex follows xr_input.c's action order
// (MenuOverlay.java and Strings.java hold the matching labels).
JNIEXPORT void JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeRequestBeginControlCapture(JNIEnv *env, jclass clazz,
jint actionIndex) {
qs_config_request_begin_control_capture(actionIndex);
}
JNIEXPORT void JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeRequestCancelControlCapture(JNIEnv *env, jclass clazz) {
qs_config_request_cancel_control_capture();
}
JNIEXPORT void JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeRequestResetPlayerControls(JNIEnv *env, jclass clazz) {
qs_config_request_reset_player_controls();
}
JNIEXPORT jstring JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetControlBindingKey(JNIEnv *env, jclass clazz,
jint actionIndex) {
return (*env)->NewStringUTF(env, xr_input_get_action_binding_name(actionIndex));
}
JNIEXPORT jstring JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetActiveInteractionProfilePath(JNIEnv *env,
jclass clazz) {
return (*env)->NewStringUTF(env, xr_input_get_active_interaction_profile_path());
}
JNIEXPORT void JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeSetControllerOptionsTabOpen(JNIEnv *env, jclass clazz,
jboolean open) {
xr_input_set_controller_options_tab_open(open == JNI_TRUE);
}
// Cheats tab: same enqueue-only pattern as the other requests.
JNIEXPORT void JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeRequestCheat(JNIEnv *env, jclass clazz, jint cheatId) {
qs_config_request_cheat(cheatId);
}
JNIEXPORT jint JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetCheatUnlimitedMask(JNIEnv *env, jclass clazz) {
return qs_config_cheat_unlimited_mask();
}
JNIEXPORT jboolean JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetCheatPhysicsRunning(JNIEnv *env, jclass clazz) {
return qs_config_cheat_physics_running();
}
File diff suppressed because it is too large Load Diff
+68 -19
View File
@@ -67,27 +67,22 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw);
bool xr_input_get_beam_layer(int hand, XrCompositionLayerQuad *out_quad); bool xr_input_get_beam_layer(int hand, XrCompositionLayerQuad *out_quad);
// Recenters the player so the game quad ("virtual screen") appears directly // Recenters the player so the game quad ("virtual screen") appears directly
// ahead of wherever they're currently really standing/facing - a pure, // ahead of wherever they are standing and facing, from their current real head
// live "return to a sane view of the screen" action, unrelated to any // pose. Never reads or writes questshock.config.json. Called automatically
// stored/config value (unlike xr_session_set_player_transform()'s other // once, from xr_input_sync_and_draw(), on the first frame with a valid head
// callers, this never reads or writes questshock.config.json - it's driven // pose after startup, unless a complete saved player_position was loaded (see
// entirely by the player's current real head pose, the same way // xr_input_skip_startup_recenter()): a saved position is relative to a
// xr_input_sync_and_draw()'s own menu re-center is). Called from the Main // previous session's real-space origin, which is not stable across launches.
// Menu tab's "Reset Position" button (via qs_config.c's cross-thread // Returns false (no-op) if no valid head pose is available yet.
// dispatch - see qs_config_request_reset_position()), but doesn't touch
// anything else qs_config.c owns; also called once automatically, from
// inside xr_input_sync_and_draw() itself, on whichever frame first has a
// valid head pose after startup (see g_pending_startup_recenter's own
// comment) - but only as a *fallback* default for when there's no
// complete saved player_position to fall back on instead (see
// xr_input_skip_startup_recenter() below), since a config-loaded value
// describes a transform relative to a *previous* session's real-space
// origin, which is only meaningful if that previous position was actually
// captured (LOCAL space's own origin isn't a stable point across
// launches). Returns false (no-op otherwise) if no valid head pose was
// available to recenter onto yet.
bool xr_input_recenter_player_to_screen(void); bool xr_input_recenter_player_to_screen(void);
// The Main Menu tab's "Reset Position" button (via qs_config.c). Like
// xr_input_recenter_player_to_screen(), but re-anchors only the player's
// position to the centre in front of the game quad; the current facing (yaw)
// is kept, so the screen does not rotate to follow the gaze. Returns false
// (no-op) if no valid head pose is available yet.
bool xr_input_reset_player_position(void);
// Called once, from qs_config_load_startup() (qs_config.c), when a // Called once, from qs_config_load_startup() (qs_config.c), when a
// complete saved player_position was actually loaded from // complete saved player_position was actually loaded from
// questshock.config.json - cancels the pending automatic startup recenter // questshock.config.json - cancels the pending automatic startup recenter
@@ -96,6 +91,60 @@ bool xr_input_recenter_player_to_screen(void);
// yet (this only touches the plain file-local flag, no XR calls). // yet (this only touches the plain file-local flag, no XR calls).
void xr_input_skip_startup_recenter(void); void xr_input_skip_startup_recenter(void);
// ===========================================================================
// Remappable controls ("player_controls" config group; see qs_config.c and
// CONFIG.md). menu_toggle is not among them and has no getter/setter: it is
// fixed (see xr_input_sync_and_draw()).
// ===========================================================================
// Number of remappable actions and each one's index<->name mapping, which
// qs_config.c uses to parse/write the player_controls group without its own
// copy of the list. The names are the keys used in questshock.config.json;
// index_from_name() returns false for an unrecognized name.
int xr_input_action_count(void);
const char *xr_input_action_name(int action_index);
bool xr_input_action_index_from_name(const char *name, int *out_index);
// Current binding for one action, as an opaque int and as its string form
// (e.g. "right_b", "trigger", "none" for unbound), the same strings used in
// questshock.config.json and reported by capture.
int xr_input_get_action_binding(int action_index);
const char *xr_input_get_action_binding_name(int action_index);
// Sets one action's binding from a config-file string. Returns false (binding
// unchanged) for an unrecognized name, or for the symmetric "trigger"/
// "squeeze" on an action other than mouse_click/mouse_double_click. Does not
// enforce one-action-per-input the way capture does: a hand-edited file that
// binds two actions to one input is accepted as-is.
bool xr_input_set_action_binding(int action_index, const char *raw_name);
// Restores every action to its built-in default (the table xr_input_init()
// starts from). qs_config.c calls this first for "Reset to Defaults", then
// applies questshock.config.default.json's player_controls group on top if it
// has one.
void xr_input_reset_player_controls(void);
// Begins/cancels rebind capture for one action (an index into
// xr_input_action_name()'s list): the next fresh button or thumbstick-direction
// edge on any hand becomes that action's binding and is reported via
// qs_config_notify_control_capture_result(); with no press, capture cancels
// after 8 seconds. Called via qs_config.c's cross-thread request functions.
// The menu button is never captured.
void xr_input_capture_begin(int action_index);
void xr_input_capture_cancel(void);
// The OpenXR interaction profile currently bound (e.g.
// "/interaction_profiles/oculus/touch_controller"), refreshed once per frame.
// MenuOverlay.java's Controller Options tab uses it to pick a file from
// controller_profiles/ (see CONFIG.md). Empty until the first successful
// query.
const char *xr_input_get_active_interaction_profile_path(void);
// Set by MenuOverlay.java when the Controller Options tab becomes or stops
// being the menu's current tab. While it is open, the menu's thumbstick
// locomotion (snap turn/smooth move) is disabled.
void xr_input_set_controller_options_tab_open(bool open);
void xr_input_shutdown(void); void xr_input_shutdown(void);
#ifdef __cplusplus #ifdef __cplusplus
+138 -42
View File
@@ -26,14 +26,18 @@
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
// Fixed pose/size for the single game quad - 2m in front of the local space // Original, pre-screen-profile-feature default distance for the game
// origin, sized to preserve the game's own aspect ratio at a comfortable // screen - 2m in front of the local space origin - now just the initial
// visual angle. No per-eye view/projection work is needed for a quad layer // value of g_screen_distance_m (below) rather than a fixed constant.
// at all - the compositor handles reprojecting it per eye itself, which is // Width/curve/distance are all configurable (see g_screen_width_m/
// exactly why this milestone doesn't need xrLocateViews or any stereo // g_screen_curve_deg/g_screen_distance_m above and CONFIG.md's Video
// rendering (see the plan's step A notes). // Options "Initial Screen"/"Game Screen" settings), sized to preserve the
// game's own pixel aspect ratio at whatever physical width is currently
// configured. No per-eye view/projection work is needed for a quad/
// cylinder layer at all - the compositor handles reprojecting it per eye
// itself, which is exactly why this milestone doesn't need xrLocateViews
// or any stereo rendering (see the plan's step A notes).
#define QUAD_DISTANCE_METERS 2.0f #define QUAD_DISTANCE_METERS 2.0f
#define QUAD_WIDTH_METERS 1.6f
// The menu launcher (just a "Keyboard" button - see MenuOverlay.java) is // The menu launcher (just a "Keyboard" button - see MenuOverlay.java) is
// small and sits above where the keyboard defaults to; the keyboard keeps // small and sits above where the keyboard defaults to; the keyboard keeps
@@ -41,16 +45,22 @@
// xr_overlay.h) so both can be shown/hidden/dragged separately - deliberately // xr_overlay.h) so both can be shown/hidden/dragged separately - deliberately
// positioned apart by default so they don't start out overlapping. // positioned apart by default so they don't start out overlapping.
#define MENU_JAVA_CLASS "de.ladkau.questshock.MenuOverlay" #define MENU_JAVA_CLASS "de.ladkau.questshock.MenuOverlay"
// 768x552 (was 512x256, then 768x512) - grown to fit the header/tabs/Video // 768x800 (was 512x256, then 768x512, then 768x552) - grown to fit the
// Options content added on top of the original single "Keyboard" button, // header/tabs/Video Options content added on top of the original single
// then again to fit the stats ribbon below the tab bar; taller in world // "Keyboard" button, then again to fit the stats ribbon below the tab
// space too since MENU_WIDTH_METERS (the physical size) stays fixed and // bar, then again to fit the Video Options tab's screen-size grid; taller
// height_m is derived from it by aspect ratio (see xr_overlay.c's // in world space too since MENU_WIDTH_METERS (the physical size) stays
// xr_overlay_init()). // fixed and height_m is derived from it by aspect ratio (see
// xr_overlay.c's xr_overlay_init()). Must match MenuOverlay.java's own
// WIDTH/HEIGHT, and xr_input.c's MENU_TITLE_BAR_V_FRACTION denominator.
#define MENU_WIDTH 768 #define MENU_WIDTH 768
#define MENU_HEIGHT 552 #define MENU_HEIGHT 800
#define MENU_DISTANCE_METERS 1.3f #define MENU_DISTANCE_METERS 1.3f
#define MENU_WIDTH_METERS 0.35f // Physical size - was 0.35m, too small to read comfortably at
// MENU_DISTANCE_METERS given how much text the header/tabs/stats ribbon
// pack in; same pixel resolution (MENU_WIDTH/HEIGHT above) just displayed
// bigger, not a layout change.
#define MENU_WIDTH_METERS 0.5f
#define MENU_DEFAULT_CENTER_X_METERS 0.0f #define MENU_DEFAULT_CENTER_X_METERS 0.0f
#define MENU_DEFAULT_CENTER_Y_METERS 0.15f #define MENU_DEFAULT_CENTER_Y_METERS 0.15f
@@ -123,6 +133,24 @@ static PFN_xrQueryPerformanceMetricsCounterMETA g_xrQueryPerformanceMetricsCount
static XrPath g_cpu_utilization_path = XR_NULL_PATH; static XrPath g_cpu_utilization_path = XR_NULL_PATH;
static XrPath g_gpu_utilization_path = XR_NULL_PATH; static XrPath g_gpu_utilization_path = XR_NULL_PATH;
// XR_KHR_composition_layer_cylinder support - same "only if the runtime
// advertised it" reasoning as g_performance_metrics_supported above. If
// unsupported, the curve is forced to 0 (flat) everywhere regardless of
// configuration (see xr_get_game_quad_extent()) so the game screen just
// silently stays flat instead of failing to submit a layer at all.
static bool g_cylinder_layer_supported = false;
// The game screen's currently-configured shape (see qs_config.c's
// qs_screen_profile_notify_loop_mode(), which is what actually calls
// xr_session_set_game_screen_shape() below - this file has no notion of
// "which profile" is active, only "what shape is current"). Defaults
// match the game quad's own original, pre-this-feature hardcoded width
// (1.6m) and lack of curve, so a build that never touches the new config
// options renders identically to before.
static float g_screen_width_m = 1.6f;
static float g_screen_curve_deg = 0.0f;
static float g_screen_distance_m = QUAD_DISTANCE_METERS;
static bool xr_check(XrResult result, const char *what) { static bool xr_check(XrResult result, const char *what) {
if (XR_SUCCEEDED(result)) if (XR_SUCCEEDED(result))
return true; return true;
@@ -351,12 +379,24 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
g_performance_metrics_supported = g_performance_metrics_supported =
xr_instance_extension_supported(XR_META_PERFORMANCE_METRICS_EXTENSION_NAME); xr_instance_extension_supported(XR_META_PERFORMANCE_METRICS_EXTENSION_NAME);
g_cylinder_layer_supported =
xr_instance_extension_supported(XR_KHR_COMPOSITION_LAYER_CYLINDER_EXTENSION_NAME);
const char *extensions[3] = { // The two KHR extensions here are always required; the other two are
// each independently optional device capabilities (see
// g_performance_metrics_supported/g_cylinder_layer_supported's own
// comments) - appended only if actually advertised, since listing an
// unsupported extension fails xrCreateInstance entirely, not just that
// one feature.
const char *extensions[4] = {
XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME, XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME,
XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME, XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME,
XR_META_PERFORMANCE_METRICS_EXTENSION_NAME,
}; };
uint32_t extensionCount = 2;
if (g_performance_metrics_supported)
extensions[extensionCount++] = XR_META_PERFORMANCE_METRICS_EXTENSION_NAME;
if (g_cylinder_layer_supported)
extensions[extensionCount++] = XR_KHR_COMPOSITION_LAYER_CYLINDER_EXTENSION_NAME;
XrInstanceCreateInfoAndroidKHR androidInfo = {XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR}; XrInstanceCreateInfoAndroidKHR androidInfo = {XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR};
androidInfo.applicationVM = vm; androidInfo.applicationVM = vm;
@@ -364,7 +404,7 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
XrInstanceCreateInfo createInfo = {XR_TYPE_INSTANCE_CREATE_INFO}; XrInstanceCreateInfo createInfo = {XR_TYPE_INSTANCE_CREATE_INFO};
createInfo.next = &androidInfo; createInfo.next = &androidInfo;
createInfo.enabledExtensionCount = g_performance_metrics_supported ? 3 : 2; createInfo.enabledExtensionCount = extensionCount;
createInfo.enabledExtensionNames = extensions; createInfo.enabledExtensionNames = extensions;
strncpy(createInfo.applicationInfo.applicationName, "QuestShock", strncpy(createInfo.applicationInfo.applicationName, "QuestShock",
XR_MAX_APPLICATION_NAME_SIZE - 1); XR_MAX_APPLICATION_NAME_SIZE - 1);
@@ -659,30 +699,54 @@ void xr_frame_end(void) {
if (!xr_is_session_running()) if (!xr_is_session_running())
return; return;
// Up to 5 layers: the game quad (if a frame was actually rendered), // Up to 5 layers: the game screen (if a frame was actually rendered) -
// the menu launcher and keyboard overlays while visible - each gets // a flat quad, or a curved cylinder if the currently-active screen
// its own acquire/render/release cycle against its own swapchain (see // profile has a nonzero curve (see xr_get_game_quad_extent()'s own
// xr_overlay_render_and_build_layer()) any time before xrEndFrame, // comment; xr_input.c's xr_input_try_game_quad() hit-tests whichever
// unlike the game quad there's no per-frame engine rendering to wrap // shape is actually submitted here, always agreeing since both read
// around here, just each overlay's own blit - and, per hand, a // the same getter) - the menu launcher and keyboard overlays while
// laser-beam quad while that hand's ray is aimed at the keyboard, // visible - each gets its own acquire/render/release cycle against
// menu, or the game quad itself (see xr_input_get_beam_layer()). // its own swapchain (see xr_overlay_render_and_build_layer()) any
// time before xrEndFrame, unlike the game screen there's no per-frame
// engine rendering to wrap around here, just each overlay's own blit -
// and, per hand, a laser-beam quad while that hand's ray is aimed at
// the keyboard, menu, or the game screen itself (see
// xr_input_get_beam_layer()).
float gameDistance, gameHalfWidth, gameHalfHeight, gameRadius, gameCentralAngle;
xr_get_game_quad_extent(&gameDistance, &gameHalfWidth, &gameHalfHeight, &gameRadius,
&gameCentralAngle);
bool gameScreenCurved = gameCentralAngle > 0.0f;
XrCompositionLayerQuad gameQuad = {XR_TYPE_COMPOSITION_LAYER_QUAD}; XrCompositionLayerQuad gameQuad = {XR_TYPE_COMPOSITION_LAYER_QUAD};
gameQuad.space = g_play_space; XrCompositionLayerCylinderKHR gameCylinder = {XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR};
gameQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH; if (gameScreenCurved) {
gameQuad.subImage.swapchain = g_game_swapchain.swapchain; gameCylinder.space = g_play_space;
gameQuad.subImage.imageRect.extent.width = g_game_swapchain.width; gameCylinder.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
gameQuad.subImage.imageRect.extent.height = g_game_swapchain.height; gameCylinder.subImage.swapchain = g_game_swapchain.swapchain;
gameQuad.pose.orientation.w = 1.0f; gameCylinder.subImage.imageRect.extent.width = g_game_swapchain.width;
gameQuad.pose.position.z = -QUAD_DISTANCE_METERS; gameCylinder.subImage.imageRect.extent.height = g_game_swapchain.height;
gameQuad.size.width = QUAD_WIDTH_METERS; gameCylinder.pose.orientation.w = 1.0f;
gameQuad.size.height = gameCylinder.pose.position.z = -gameDistance;
QUAD_WIDTH_METERS * (float)g_game_swapchain.height / (float)g_game_swapchain.width; gameCylinder.radius = gameRadius;
gameCylinder.centralAngle = gameCentralAngle;
gameCylinder.aspectRatio = (float)g_game_swapchain.width / (float)g_game_swapchain.height;
} else {
gameQuad.space = g_play_space;
gameQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
gameQuad.subImage.swapchain = g_game_swapchain.swapchain;
gameQuad.subImage.imageRect.extent.width = g_game_swapchain.width;
gameQuad.subImage.imageRect.extent.height = g_game_swapchain.height;
gameQuad.pose.orientation.w = 1.0f;
gameQuad.pose.position.z = -gameDistance;
gameQuad.size.width = gameHalfWidth * 2.0f;
gameQuad.size.height = gameHalfHeight * 2.0f;
}
const XrCompositionLayerBaseHeader *layers[5]; const XrCompositionLayerBaseHeader *layers[5];
uint32_t layerCount = 0; uint32_t layerCount = 0;
if (g_have_acquired_game_image) if (g_have_acquired_game_image)
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&gameQuad; layers[layerCount++] = gameScreenCurved ? (XrCompositionLayerBaseHeader *)&gameCylinder
: (XrCompositionLayerBaseHeader *)&gameQuad;
XrCompositionLayerQuad menuQuad; XrCompositionLayerQuad menuQuad;
if (g_frame_should_render && xr_overlay_render_and_build_layer(g_menu_overlay, &menuQuad)) { if (g_frame_should_render && xr_overlay_render_and_build_layer(g_menu_overlay, &menuQuad)) {
@@ -766,13 +830,45 @@ void xr_shutdown(void) {
g_xrQueryPerformanceMetricsCounterMETA = NULL; g_xrQueryPerformanceMetricsCounterMETA = NULL;
g_cpu_utilization_path = XR_NULL_PATH; g_cpu_utilization_path = XR_NULL_PATH;
g_gpu_utilization_path = XR_NULL_PATH; g_gpu_utilization_path = XR_NULL_PATH;
g_cylinder_layer_supported = false;
// g_screen_width_m/g_screen_curve_deg/g_screen_distance_m deliberately
// NOT reset here - same reasoning as xr_session_set_player_transform()'s
// own state: it's meant to persist across a shutdown/reinit within the
// same process (e.g. an Android lifecycle pause/resume), not just
// within one XR session.
} }
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m) { void xr_session_set_game_screen_shape(float width_m, float curve_deg, float distance_m) {
*distance_m = QUAD_DISTANCE_METERS; g_screen_width_m = width_m;
*half_width_m = QUAD_WIDTH_METERS * 0.5f; g_screen_curve_deg = curve_deg;
*half_height_m = g_screen_distance_m = distance_m;
QUAD_WIDTH_METERS * 0.5f * (float)g_game_swapchain.height / (float)g_game_swapchain.width; }
// Single source of truth for the game screen's current shape, read by
// both xr_frame_end() (to build/submit the actual layer) and
// xr_input_try_game_quad() (xr_input.c, to hit-test the laser pointer
// against it). half_width_m is meaningful only when *central_angle_rad is
// 0 (flat); a curved screen's horizontal extent is bounded by
// *central_angle_rad instead, with *half_height_m always the vertical
// bound either way.
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m,
float *radius_m, float *central_angle_rad) {
float curveDeg = g_cylinder_layer_supported ? g_screen_curve_deg : 0.0f;
float aspect = (float)g_game_swapchain.height / (float)g_game_swapchain.width;
*distance_m = g_screen_distance_m;
*half_width_m = g_screen_width_m * 0.5f;
*half_height_m = g_screen_width_m * 0.5f * aspect;
if (curveDeg > 0.0f) {
float theta = curveDeg * QS_DEG_TO_RAD;
*central_angle_rad = theta;
*radius_m = g_screen_width_m / theta;
} else {
*central_angle_rad = 0.0f;
*radius_m = 0.0f;
}
} }
void xr_get_game_resolution(int *width, int *height) { void xr_get_game_resolution(int *width, int *height) {
+22 -3
View File
@@ -68,11 +68,30 @@ void xr_frame_end(void);
void xr_shutdown(void); void xr_shutdown(void);
// Play-space geometry of the single game quad xr_frame_end() submits // Play-space geometry of the single game screen xr_frame_end() submits
// (distance in front of the play space origin, half-width/half-height, // (distance in front of the play space origin, half-width/half-height,
// all in meters) - shared with xr_input.c's ray/quad hit-testing so the // all in meters) - shared with xr_input.c's ray/quad hit-testing so the
// laser pointer always matches whatever's actually visible. // laser pointer always matches whatever's actually visible. *radius_m/
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m); // *central_angle_rad are 0 when flat, in which case *half_width_m is the
// horizontal extent; otherwise the screen is a curved cylinder segment
// and the caller uses *central_angle_rad instead of *half_width_m for the
// horizontal bound (*half_height_m stays meaningful either way). Curving
// is currently unused - qs_config.c has no config surface for it - but
// kept working so it can be re-enabled later.
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m,
float *radius_m, float *central_angle_rad);
// Sets the game screen's current physical width (meters) and curve (wrap
// angle in degrees, 0 = flat) - a plain state setter, no XR calls, same
// category as xr_session_set_player_transform() above (safe to call
// before xr_init() has even run). Read by both xr_frame_end() (to build/
// submit the actual layer) and, via xr_get_game_quad_extent() above,
// xr_input.c's hit-testing. Driven by qs_config.c's
// qs_screen_profile_notify_loop_mode(), which decides which of the
// "Initial Screen"/"Game Screen" config profiles (see CONFIG.md) is
// currently active - this function itself has no notion of "profiles",
// only "what shape is current".
void xr_session_set_game_screen_shape(float width_m, float curve_deg, float distance_m);
// The engine's current screen resolution (gScreenWide/gScreenHigh, // The engine's current screen resolution (gScreenWide/gScreenHigh,
// InitMac.h) - not the game quad's own swapchain size, which is fixed at // InitMac.h) - not the game quad's own swapchain size, which is fixed at
File diff suppressed because it is too large Load Diff
@@ -271,7 +271,11 @@ abstract class OverlayPanel {
// Absolute bounds of view within this panel's own coordinate space // Absolute bounds of view within this panel's own coordinate space
// (i.e. relative to root, which is always laid out at (0,0,width, // (i.e. relative to root, which is always laid out at (0,0,width,
// height) - see the constructor) - View's own getLeft()/getTop() are // height) - see the constructor) - View's own getLeft()/getTop() are
// parent-relative, so this walks up the parent chain summing them. // parent-relative, so this walks up the parent chain summing them,
// additionally subtracting each ancestor's own getScrollX()/getScrollY()
// (0 for a plain ViewGroup, non-zero for e.g. a ScrollView - see
// MenuOverlay's Controller Options tab) so a view inside a scrolled
// container reports its actual on-screen position, not its laid-out one.
// Assumes view is actually part of this panel's tree (every registered // Assumes view is actually part of this panel's tree (every registered
// view always is, since registerHoverable() is only ever called from // view always is, since registerHoverable() is only ever called from
// buildContent()). // buildContent()).
@@ -285,7 +289,10 @@ abstract class OverlayPanel {
if (current == root || !(current.getParent() instanceof View)) { if (current == root || !(current.getParent() instanceof View)) {
break; break;
} }
current = (View) current.getParent(); View parent = (View) current.getParent();
left -= parent.getScrollX();
top -= parent.getScrollY();
current = parent;
} }
return new Rect(left, top, left + view.getWidth(), top + view.getHeight()); return new Rect(left, top, left + view.getWidth(), top + view.getHeight());
} }
@@ -39,7 +39,8 @@ import org.libsdl.app.SDLSurface;
public class QuestShockActivity extends SDLActivity { 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;
private static final String GAME_DIR = "/sdcard/questshock"; // Package-visible: MenuOverlay reads controller_profiles/ from here.
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())
@@ -143,11 +144,25 @@ public class QuestShockActivity extends SDLActivity {
// is already there, assume a previous run already extracted everything. // is already there, assume a previous run already extracted everything.
File marker = new File(gameDir, "GET_ASSETS_QUEST.txt"); File marker = new File(gameDir, "GET_ASSETS_QUEST.txt");
if (!marker.exists()) { if (!marker.exists()) {
copyAssetDir("shaders", new File(gameDir, "shaders")); copyAssetDir("shaders", new File(gameDir, "shaders"), false);
copyAssetFile("res/soundfont.sf2", new File(gameDir, "res/soundfont.sf2")); copyAssetFile("res/soundfont.sf2", new File(gameDir, "res/soundfont.sf2"), false);
copyAssetFile("GET_ASSETS_QUEST.txt", marker); copyAssetFile("GET_ASSETS_QUEST.txt", marker, false);
} }
// Refreshed on every launch (unlike the marker-gated block above) so
// it always matches the installed build - qs_config.c reads this as
// the initial config only when questshock.config.json doesn't exist
// yet, never overwriting a user's own saved config.
copyAssetFile("questshock.config.default.json",
new File(gameDir, "questshock.config.default.json"), true);
// Refreshed on every launch. MenuOverlay's Controller Options tab
// matches the active OpenXR interaction profile against these files'
// "interaction_profile" and shows the paired image (see CONFIG.md).
// Only the bundled filenames are overwritten, so profiles a user adds
// to this folder are kept.
copyAssetDir("controller_profiles", new File(gameDir, "controller_profiles"), true);
// Shockolate's engine/ is never patched to check this itself (see the // Shockolate's engine/ is never patched to check this itself (see the
// class doc above) - it just does plain fopen("res/data/...", ...) // class doc above) - it just does plain fopen("res/data/...", ...)
// and, on a fresh install with no game data copied in yet, that // and, on a fresh install with no game data copied in yet, that
@@ -211,8 +226,8 @@ public class QuestShockActivity extends SDLActivity {
.show(); .show();
} }
private void copyAssetFile(String assetPath, File dest) { private void copyAssetFile(String assetPath, File dest, boolean overwrite) {
if (dest.exists()) { if (dest.exists() && !overwrite) {
return; return;
} }
File parent = dest.getParentFile(); File parent = dest.getParentFile();
@@ -232,7 +247,7 @@ public class QuestShockActivity extends SDLActivity {
} }
} }
private void copyAssetDir(String assetDir, File destDir) { private void copyAssetDir(String assetDir, File destDir, boolean overwrite) {
AssetManager assets = getAssets(); AssetManager assets = getAssets();
String[] entries; String[] entries;
try { try {
@@ -246,7 +261,7 @@ public class QuestShockActivity extends SDLActivity {
} }
destDir.mkdirs(); destDir.mkdirs();
for (String entry : entries) { for (String entry : entries) {
copyAssetFile(assetDir + "/" + entry, new File(destDir, entry)); copyAssetFile(assetDir + "/" + entry, new File(destDir, entry), overwrite);
} }
} }
} }
@@ -50,6 +50,10 @@ final class Strings {
put("english", "English", "Anglais", "Englisch"); put("english", "English", "Anglais", "Englisch");
put("french", "French", "Français", "Französisch"); put("french", "French", "Français", "Französisch");
put("german", "German", "Allemand", "Deutsch"); put("german", "German", "Allemand", "Deutsch");
put("initial_screen", "Initial Screen", "Écran initial", "Startbildschirm");
put("game_screen", "Game Screen", "Écran de jeu", "Spielbildschirm");
put("width", "Width", "Largeur", "Breite");
put("distance", "Distance", "Distance", "Abstand");
// "OpenGL" is a proper/brand name - not translated, same in every // "OpenGL" is a proper/brand name - not translated, same in every
// language, so it has no entry here; call sites just use it as a // language, so it has no entry here; call sites just use it as a
// literal. // literal.
@@ -57,6 +61,145 @@ final class Strings {
"Clavier - faites glisser ici pour déplacer", "Clavier - faites glisser ici pour déplacer",
"Tastatur - hier ziehen zum Verschieben"); "Tastatur - hier ziehen zum Verschieben");
put("close", "Close", "Fermer", "Schließen"); put("close", "Close", "Fermer", "Schließen");
// Controller Options tab: action row labels, keyed by xr_input.c's
// action names (see MenuOverlay.ACTION_KEYS).
put("controller_options", "Controller Options", "Options de manette", "Controller-Optionen");
put("action_move_forward", "Move Forward", "Avancer", "Vorwärts gehen");
put("action_move_back", "Move Back", "Reculer", "Rückwärts gehen");
put("action_strafe_left", "Strafe Left", "Pas latéral gauche", "Seitwärts links");
put("action_strafe_right", "Strafe Right", "Pas latéral droit", "Seitwärts rechts");
put("action_turn_left", "Turn Left", "Tourner à gauche", "Nach links drehen");
put("action_turn_right", "Turn Right", "Tourner à droite", "Nach rechts drehen");
put("action_jump", "Jump", "Sauter", "Springen");
put("action_mouse_click", "Click", "Cliquer", "Klicken");
put("action_mouse_double_click", "Double-Click", "Double-clic", "Doppelklick");
// Hotkey actions (see xr_input.c); all unbound by default.
put("action_stand", "Stand", "Debout", "Stehen");
put("action_crouch", "Crouch", "Accroupi", "Ducken");
put("action_prone", "Prone", "Couché", "Liegen");
put("action_pause", "Pause Game", "Mettre en pause", "Spiel pausieren");
put("action_full_view", "Fullscreen View", "Vue plein écran", "Vollbildansicht");
put("action_normal_view", "Normal View", "Vue normale", "Normale Ansicht");
put("action_map_view", "Automap View", "Vue de la carte", "Automap-Ansicht");
put("action_save_game", "Save Game", "Sauvegarder", "Spiel speichern");
put("action_load_game", "Load Game", "Charger", "Spiel laden");
put("action_select_grenade", "Select Grenade", "Sélectionner une grenade",
"Granate auswählen");
put("action_arm_grenade", "Arm Grenade", "Armer la grenade", "Granate scharfmachen");
put("action_select_drug", "Select Drug", "Sélectionner une drogue", "Droge auswählen");
put("action_use_drug", "Use Drug", "Utiliser la drogue", "Droge benutzen");
put("action_toggle_online_help", "Toggle Online Help", "Basculer l'aide en ligne",
"Online-Hilfe umschalten");
put("action_show_help", "Show Help", "Afficher l'aide", "Hilfe anzeigen");
put("action_toggle_music", "Toggle Music", "Basculer la musique", "Musik umschalten");
put("action_quit", "Quit Game", "Quitter le jeu", "Spiel beenden");
put("action_cycle_weapon_next", "Next Weapon", "Arme suivante", "Nächste Waffe");
put("action_cycle_weapon_prev", "Previous Weapon", "Arme précédente", "Vorherige Waffe");
put("action_cycle_detail", "Cycle Detail Level", "Changer le niveau de détail",
"Detailgrad wechseln");
put("action_reload_weapon_primary", "Reload Weapon (Primary)",
"Recharger l'arme (principal)", "Waffe nachladen (primär)");
put("action_reload_weapon_secondary", "Reload Weapon (Secondary)",
"Recharger l'arme (secondaire)", "Waffe nachladen (sekundär)");
put("action_hud_color", "Cycle HUD Color", "Changer la couleur de l'interface",
"HUD-Farbe wechseln");
put("action_clear_fullscreen", "Clear Fullscreen View", "Quitter la vue plein écran",
"Vollbildansicht schließen");
put("action_mfd_left_1", "Left MFD: Weapon", "MFD gauche : arme", "Linkes MFD: Waffe");
put("action_mfd_left_2", "Left MFD: Item", "MFD gauche : objet", "Linkes MFD: Gegenstand");
put("action_mfd_left_3", "Left MFD: Map", "MFD gauche : carte", "Linkes MFD: Karte");
put("action_mfd_left_4", "Left MFD: Target", "MFD gauche : cible", "Linkes MFD: Ziel");
put("action_mfd_left_5", "Left MFD: Info", "MFD gauche : info", "Linkes MFD: Info");
put("action_mfd_right_1", "Right MFD: Weapon", "MFD droit : arme", "Rechtes MFD: Waffe");
put("action_mfd_right_2", "Right MFD: Item", "MFD droit : objet",
"Rechtes MFD: Gegenstand");
put("action_mfd_right_3", "Right MFD: Map", "MFD droit : carte", "Rechtes MFD: Karte");
put("action_mfd_right_4", "Right MFD: Target", "MFD droit : cible", "Rechtes MFD: Ziel");
put("action_mfd_right_5", "Right MFD: Info", "MFD droit : info", "Rechtes MFD: Info");
put("action_fire_weapon", "Fire Weapon", "Tirer", "Waffe abfeuern");
// Cheats tab.
put("cheats", "Cheats", "Triches", "Cheats");
put("cheats_note", "Cheats need a running game.", "Les triches nécessitent une partie en cours.",
"Cheats benötigen ein laufendes Spiel.");
put("cheat_give_all", "Give All Items & Weapons", "Tout obtenir", "Alles erhalten");
put("cheat_physics", "Physics", "Physique", "Physik");
put("cheat_level_up", "Next Level", "Niveau suivant", "Nächstes Level");
put("cheat_level_down", "Previous Level", "Niveau précédent", "Vorheriges Level");
put("cheat_unlimited_health", "Unlimited Health", "Santé illimitée", "Unbegrenzt Leben");
put("cheat_unlimited_energy", "Unlimited Energy", "Énergie illimitée", "Unbegrenzt Energie");
put("cheat_unlimited_ammo", "Unlimited Ammo", "Munitions illimitées", "Unbegrenzt Munition");
put("cheat_unlimited_items", "Unlimited Items", "Objets illimités", "Unbegrenzt Items");
put("action_free_left_thumbstick_view", "Free View (Left Stick)",
"Vue libre (stick gauche)", "Freie Ansicht (linker Stick)");
put("action_free_right_thumbstick_view", "Free View (Right Stick)",
"Vue libre (stick droit)", "Freie Ansicht (rechter Stick)");
// Cyberspace actions (active only in cyberspace, see xr_input.c).
put("action_cyber_thrust", "Cyberspace: Thrust", "Cyberespace : poussée",
"Cyberspace: Schub");
put("action_cyber_climb", "Cyberspace: Climb", "Cyberespace : monter",
"Cyberspace: Steigen");
put("action_cyber_dive", "Cyberspace: Dive", "Cyberespace : plonger",
"Cyberspace: Sinken");
put("action_cyber_bank_left", "Cyberspace: Bank Left", "Cyberespace : virage à gauche",
"Cyberspace: Links kippen");
put("action_cyber_bank_right", "Cyberspace: Bank Right", "Cyberespace : virage à droite",
"Cyberspace: Rechts kippen");
put("action_cyber_roll_left", "Cyberspace: Roll Left", "Cyberespace : roulis gauche",
"Cyberspace: Links rollen");
put("action_cyber_roll_right", "Cyberspace: Roll Right", "Cyberespace : roulis droit",
"Cyberspace: Rechts rollen");
put("action_cyber_climb_left", "Cyberspace: Climb Left", "Cyberespace : monter à gauche",
"Cyberspace: Links steigen");
put("action_cyber_climb_right", "Cyberspace: Climb Right", "Cyberespace : monter à droite",
"Cyberspace: Rechts steigen");
put("action_cyber_dive_left", "Cyberspace: Dive Left", "Cyberespace : plonger à gauche",
"Cyberspace: Links sinken");
put("action_cyber_dive_right", "Cyberspace: Dive Right", "Cyberespace : plonger à droite",
"Cyberspace: Rechts sinken");
put("capture_prompt", "Press a button…", "Appuyez sur un bouton…", "Taste drücken…");
put("reset_to_defaults", "Reset to Defaults", "Réinitialiser", "Auf Standard zurücksetzen");
put("generic_profile_note",
"This controller only exposes a trigger and a menu button - most actions above have "
+ "nothing to bind to.",
"Cette manette n'expose qu'une gâchette et un bouton menu - la plupart des actions "
+ "ci-dessus n'ont rien à quoi être assignées.",
"Dieser Controller bietet nur einen Trigger und eine Menütaste - die meisten "
+ "Aktionen oben haben nichts, woran sie gebunden werden können.");
// Raw physical-input display names (xr_input.c's kRawInputNames);
// "none" is the unbound sentinel.
put("raw_left_trigger", "Left Trigger", "Gâchette gauche", "Linker Trigger");
put("raw_right_trigger", "Right Trigger", "Gâchette droite", "Rechter Trigger");
put("raw_left_squeeze", "Left Grip", "Préhension gauche", "Linker Griff");
put("raw_right_squeeze", "Right Grip", "Préhension droite", "Rechter Griff");
put("raw_left_x", "X Button", "Bouton X", "X-Taste");
put("raw_left_y", "Y Button", "Bouton Y", "Y-Taste");
put("raw_right_a", "A Button", "Bouton A", "A-Taste");
put("raw_right_b", "B Button", "Bouton B", "B-Taste");
put("raw_left_thumbstick_click", "Left Stick Click", "Clic du stick gauche",
"Linker Stick-Klick");
put("raw_right_thumbstick_click", "Right Stick Click", "Clic du stick droit",
"Rechter Stick-Klick");
put("raw_left_thumbstick_up", "Left Stick Up", "Stick gauche haut", "Linker Stick hoch");
put("raw_left_thumbstick_down", "Left Stick Down", "Stick gauche bas", "Linker Stick runter");
put("raw_left_thumbstick_left", "Left Stick Left", "Stick gauche à gauche",
"Linker Stick links");
put("raw_left_thumbstick_right", "Left Stick Right", "Stick gauche à droite",
"Linker Stick rechts");
put("raw_right_thumbstick_up", "Right Stick Up", "Stick droit haut", "Rechter Stick hoch");
put("raw_right_thumbstick_down", "Right Stick Down", "Stick droit bas",
"Rechter Stick runter");
put("raw_right_thumbstick_left", "Right Stick Left", "Stick droit à gauche",
"Rechter Stick links");
put("raw_right_thumbstick_right", "Right Stick Right", "Stick droit à droite",
"Rechter Stick rechts");
put("raw_trigger", "Either Trigger", "L'une ou l'autre gâchette", "Beliebiger Trigger");
put("raw_squeeze", "Either Grip", "L'une ou l'autre préhension", "Beliebiger Griff");
put("raw_none", "(unbound)", "(non assigné)", "(nicht zugewiesen)");
} }
private Strings() {} private Strings() {}
+12 -1
View File
@@ -9,7 +9,18 @@ buildscript {
google() google()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.1.1' // 8.5.1+ needed for AGP to automatically 16 KB-align native
// libraries' zip entries within the APK - the actual .so's are
// already ELF-segment-aligned for this regardless of AGP version
// (see build-image/Dockerfile's ANDROID_16KB_LDFLAGS and
// app/build.gradle's own CMAKE_SHARED_LINKER_FLAGS), but that alone
// doesn't get them 16 KB-aligned *within the APK's zip*, which is
// what Android Studio's "not compatible with 16 KB devices"
// warning is actually about. Needs Gradle 8.7+ (see
// gradle/wrapper/gradle-wrapper.properties) and JDK 17 in whatever
// runs gradlew (already the case - see build-image/Dockerfile's
// openjdk-17-jdk-headless).
classpath 'com.android.tools.build:gradle:8.5.2'
} }
} }
@@ -0,0 +1,30 @@
--- a/src/GameSrc/mainloop.c
+++ b/src/GameSrc/mainloop.c
@@ -57,6 +57,10 @@
#include "tools.h"
#include "wrapper.h"
+#ifdef __ANDROID__
+#include "qs_config.h"
+#endif
+
// how is the game doing, anyway, set to true at end of time
uchar cit_success = FALSE;
@@ -103,6 +107,16 @@
_last_mode = *cmode;
(*exit_modes[_last_mode])();
*cmode = _new_mode;
+#ifdef __ANDROID__
+ // Notifies qs_config.c of every loop-mode transition so it can pick
+ // between the "Initial Screen" and "Game Screen" virtual-screen
+ // presets - see qs_config.h/CONFIG.md. Called synchronously, directly,
+ // on this thread (mainloop()'s own) - unlike the UI-thread-triggered
+ // config actions elsewhere in qs_config.c, this doesn't need the
+ // cross-thread SDL-event dance since it already originates on the
+ // right thread.
+ qs_screen_profile_notify_loop_mode(*cmode);
+#endif
_static_change = 0;
if (*cmode >= 0)
(*enter_modes[*cmode])();
@@ -0,0 +1,42 @@
--- a/src/GameSrc/cutsloop.c
+++ b/src/GameSrc/cutsloop.c
@@ -292,11 +292,38 @@
+#ifdef __ANDROID__
+// The intro (cutscene 0) ships in one file per language - localized
+// voice-over - each holding the same resource id (cutscene_anims[0]), so
+// opening the current language's file instead is all it takes. The death
+// and ending cutscenes only have one version.
+static char *intro_files_by_language[3] =
+{
+ "res/data/svgaintr.res", // English
+ "res/data/svfrintr.res", // French
+ "res/data/svgeintr.res" // German
+};
+
+static int open_cutscene_file(int id)
+{
+ if (id == 0 && which_lang > 0 && which_lang < 3)
+ {
+ int handle = ResOpenFile(intro_files_by_language[(int)which_lang]);
+ if (handle > 0)
+ return handle;
+ // Not installed for this language - fall back to English below.
+ }
+ return ResOpenFile(cutscene_files[id]);
+}
+#else
+#define open_cutscene_file(id) ResOpenFile(cutscene_files[id])
+#endif
+
short play_cutscene(int id, bool show_credits)
{
MacTuneKillCurrentTheme();
- cutscene_filehandle = ResOpenFile(cutscene_files[id]);
+ cutscene_filehandle = open_cutscene_file(id);
if (cutscene_filehandle <= 0) {
// If we failed to play the cutscene, go to setup / credits.
_new_mode = SETUP_LOOP;
@@ -0,0 +1,13 @@
--- a/src/GameSrc/mouselook.c
+++ b/src/GameSrc/mouselook.c
@@ -121,3 +121,10 @@
get_mouselook_vel(&mvelx, &mvely);
}
}
+
+#ifdef __ANDROID__
+// Whether the player is currently in cyberspace; read by the Android
+// controller input code (android/app/src/main/cpp/xr_input.c) to switch
+// between the normal and cyberspace action sets.
+int qs_in_cyberspace(void) { return global_fullmap != NULL && global_fullmap->cyber; }
+#endif
@@ -0,0 +1,110 @@
--- a/src/GameSrc/movekeys.c
+++ b/src/GameSrc/movekeys.c
@@ -44,6 +44,12 @@
extern bool gKeypadOverride;
uchar parse_motion_key(ushort code, short *cnum, short *cval);
+#ifdef __ANDROID__
+// Set every frame by android/app/src/main/cpp/xr_input.c.
+float qs_cyber_pitch_scale = 1.0f;
+float qs_cyber_bank_scale = 1.0f;
+#endif
+
uchar parse_motion_key_cyber(ushort code, short *cnum, short *cval);
void init_motion_polling(void);
@@ -173,6 +179,20 @@
*cnum = -1;
*cval = 0;
+#ifdef __ANDROID__
+ // Pitch and bank strength, scaled by the controller thumbstick's
+ // deflection (see android/app/src/main/cpp/xr_input.c); 1.0 for a key.
+ short pitchMax = (short)(CONTROL_MAX_VAL * qs_cyber_pitch_scale + 0.5f);
+ short bankMax = (short)(CONTROL_MAX_VAL * qs_cyber_bank_scale + 0.5f);
+ if (pitchMax < 1)
+ pitchMax = 1;
+ if (bankMax < 1)
+ bankMax = 1;
+#else
+ short pitchMax = CONTROL_MAX_VAL;
+ short bankMax = CONTROL_MAX_VAL;
+#endif
+
while (MoveCyberKeybinds[i].code != 255) {
if (code == MoveCyberKeybinds[i].code) {
move = MoveCyberKeybinds[i].move;
@@ -189,22 +209,22 @@
case M_CLIMB:
*cnum = CONTROL_YVEL;
- *cval = -CONTROL_MAX_VAL;
+ *cval = -pitchMax;
break;
case M_BANKLEFT:
*cnum = CONTROL_XYROT;
- *cval = -CONTROL_MAX_VAL;
+ *cval = -bankMax;
break;
case M_BANKRIGHT:
*cnum = CONTROL_XYROT;
- *cval = CONTROL_MAX_VAL;
+ *cval = bankMax;
break;
case M_DIVE:
*cnum = CONTROL_YVEL;
- *cval = CONTROL_MAX_VAL;
+ *cval = pitchMax;
break;
case M_ROLLRIGHT:
@@ -219,38 +239,38 @@
case M_CLIMBLEFT:
*cnum = CONTROL_YVEL;
- *cval = -CONTROL_MAX_VAL;
+ *cval = -pitchMax;
if (abs(poll_controls[*cnum]) < abs(*cval))
poll_controls[*cnum] = *cval;
*cnum = CONTROL_XYROT;
- *cval = -CONTROL_MAX_VAL;
+ *cval = -bankMax;
break;
case M_CLIMBRIGHT:
*cnum = CONTROL_YVEL;
- *cval = -CONTROL_MAX_VAL;
+ *cval = -pitchMax;
if (abs(poll_controls[*cnum]) < abs(*cval))
poll_controls[*cnum] = *cval;
*cnum = CONTROL_XYROT;
- *cval = CONTROL_MAX_VAL;
+ *cval = bankMax;
break;
case M_DIVERIGHT:
*cnum = CONTROL_YVEL;
- *cval = CONTROL_MAX_VAL;
+ *cval = pitchMax;
if (abs(poll_controls[*cnum]) < abs(*cval))
poll_controls[*cnum] = *cval;
*cnum = CONTROL_XYROT;
- *cval = CONTROL_MAX_VAL;
+ *cval = bankMax;
break;
case M_DIVELEFT:
*cnum = CONTROL_YVEL;
- *cval = CONTROL_MAX_VAL;
+ *cval = pitchMax;
if (abs(poll_controls[*cnum]) < abs(*cval))
poll_controls[*cnum] = *cval;
*cnum = CONTROL_XYROT;
- *cval = -CONTROL_MAX_VAL;
+ *cval = -bankMax;
break;
default:
// Unhandled movement. What I gonna do?
@@ -0,0 +1,74 @@
--- a/src/GameSrc/hkeyfunc.c
+++ b/src/GameSrc/hkeyfunc.c
@@ -1465,3 +1465,71 @@
#pragma enable_message(202)
#endif // NOT_YET
+
+#ifdef __ANDROID__
+// Tops up parts of the player's state; android/app/src/main/cpp/qs_config.c
+// calls it once per frame with the "unlimited" cheats that are switched on.
+// `what` is a bitmask: 1 health, 2 energy, 4 ammo, 8 items (the same values as
+// QS_CHEAT_MASK_* in qs_config.c). Only conventional guns use ammo: beam and
+// melee weapons keep heat in the same field, so they are left alone (beams draw
+// on energy). Spare ammo clips, grenades and drugs are only refilled for kinds
+// the player already holds, so the cheat never creates items.
+void qs_cheat_refill(int what) {
+ const int kAmmoFloor = 99;
+ const int kItemFloor = 99;
+ uchar vitalsChanged = FALSE;
+ uchar inventoryChanged = FALSE;
+
+ if (player_struct.dead)
+ return;
+ if ((what & 1) && player_struct.hit_points < PLAYER_MAX_HP) {
+ player_struct.hit_points = PLAYER_MAX_HP;
+ vitalsChanged = TRUE;
+ }
+ if ((what & 1) && player_struct.cspace_hp < PLAYER_MAX_HP) {
+ player_struct.cspace_hp = PLAYER_MAX_HP;
+ vitalsChanged = TRUE;
+ }
+ if ((what & 2) && player_struct.energy < 255) {
+ player_struct.energy = 255;
+ vitalsChanged = TRUE;
+ }
+ if (vitalsChanged)
+ chg_set_flg(VITALS_UPDATE);
+
+ if (what & 4) {
+ for (int i = 0; i < NUM_WEAPON_SLOTS; i++) {
+ weapon_slot *slot = &player_struct.weapons[i];
+ if (slot->type != EMPTY_WEAPON_SLOT && slot->type <= GUN_SUBCLASS_SPECIAL &&
+ slot->ammo < kAmmoFloor) {
+ slot->ammo = kAmmoFloor;
+ inventoryChanged = TRUE;
+ }
+ }
+ for (int i = 0; i < NUM_AMMO_TYPES; i++) {
+ if (player_struct.cartridges[i] > 0 && player_struct.cartridges[i] < kItemFloor) {
+ player_struct.cartridges[i] = kItemFloor;
+ inventoryChanged = TRUE;
+ }
+ }
+ }
+ if (what & 8) {
+ for (int i = 0; i < NUM_GRENADEZ; i++) {
+ if (player_struct.grenades[i] > 0 && player_struct.grenades[i] < kItemFloor) {
+ player_struct.grenades[i] = kItemFloor;
+ inventoryChanged = TRUE;
+ }
+ }
+ for (int i = 0; i < NUM_DRUGZ; i++) {
+ if (player_struct.drugs[i] > 0 && player_struct.drugs[i] < kItemFloor) {
+ player_struct.drugs[i] = kItemFloor;
+ inventoryChanged = TRUE;
+ }
+ }
+ }
+ if (inventoryChanged) {
+ chg_set_flg(INVENTORY_UPDATE);
+ mfd_force_update();
+ }
+}
+#endif
+4 -3
View File
@@ -1,6 +1,7 @@
#Thu Nov 11 18:20:34 PST 2021 #Sat Sep 05 16:12:16 CEST 2026
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+3 -1
View File
@@ -81,7 +81,7 @@ fi
echo "prebuiltDir=$PREBUILT_DIR_PROP" echo "prebuiltDir=$PREBUILT_DIR_PROP"
} > "$ANDROID_DIR/engine.properties" } > "$ANDROID_DIR/engine.properties"
echo "== Staging bundled assets (shaders, soundfont, get-assets text) ==" echo "== Staging bundled assets (shaders, soundfont, get-assets text, default config) =="
ASSETS_DIR="$ANDROID_DIR/app/src/main/assets" ASSETS_DIR="$ANDROID_DIR/app/src/main/assets"
rm -rf "$ASSETS_DIR" rm -rf "$ASSETS_DIR"
mkdir -p "$ASSETS_DIR/shaders" "$ASSETS_DIR/res" mkdir -p "$ASSETS_DIR/shaders" "$ASSETS_DIR/res"
@@ -91,6 +91,8 @@ mkdir -p "$ASSETS_DIR/shaders" "$ASSETS_DIR/res"
cp -a "$ANDROID_DIR/gles-shaders/." "$ASSETS_DIR/shaders/" cp -a "$ANDROID_DIR/gles-shaders/." "$ASSETS_DIR/shaders/"
cp "/opt/prebuilt/soundfont/default.sf2" "$ASSETS_DIR/res/soundfont.sf2" 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 -a "$REPO_ROOT/res/controller_profiles" "$ASSETS_DIR/controller_profiles"
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
+9
View File
@@ -0,0 +1,9 @@
{
"name": "Generic OpenXR Controller",
"interaction_profile": "/interaction_profiles/khr/simple_controller",
"image": "generic.png",
"controls": [
{"id": "left_trigger", "label": "Select (Left)", "anchor_x": 0.30, "anchor_y": 0.60},
{"id": "right_trigger", "label": "Select (Right)", "anchor_x": 0.30, "anchor_y": 0.60}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+19
View File
@@ -0,0 +1,19 @@
{
"name": "Meta Quest Touch",
"interaction_profile": "/interaction_profiles/oculus/touch_controller",
"image": "touch.png",
"controls": [
{"id": "left_trigger", "label": "Trigger", "anchor_x": 0.30, "anchor_y": 0.45},
{"id": "right_trigger", "label": "Trigger", "anchor_x": 0.72, "anchor_y": 0.45},
{"id": "left_squeeze", "label": "Grip", "anchor_x": 0.12, "anchor_y": 0.55},
{"id": "right_squeeze", "label": "Grip", "anchor_x": 0.58, "anchor_y": 0.55},
{"id": "left_x", "label": "X", "anchor_x": 0.14, "anchor_y": 0.33},
{"id": "left_y", "label": "Y", "anchor_x": 0.24, "anchor_y": 0.35},
{"id": "right_a", "label": "A", "anchor_x": 0.68, "anchor_y": 0.35},
{"id": "right_b", "label": "B", "anchor_x": 0.75, "anchor_y": 0.33},
{"id": "left_thumbstick", "label": "Thumbstick", "anchor_x": 0.20, "anchor_y": 0.28},
{"id": "right_thumbstick", "label": "Thumbstick", "anchor_x": 0.62, "anchor_y": 0.30},
{"id": "left_thumbstick_click", "label": "Thumbstick Click", "anchor_x": 0.20, "anchor_y": 0.28},
{"id": "right_thumbstick_click", "label": "Thumbstick Click", "anchor_x": 0.62, "anchor_y": 0.30}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

+15
View File
@@ -0,0 +1,15 @@
{
"name": "HTC Vive Wand",
"interaction_profile": "/interaction_profiles/htc/vive_controller",
"image": "vive.png",
"controls": [
{"id": "left_trigger", "label": "Trigger", "anchor_x": 0.13, "anchor_y": 0.62},
{"id": "right_trigger", "label": "Trigger", "anchor_x": 0.78, "anchor_y": 0.62},
{"id": "left_squeeze", "label": "Grip", "anchor_x": 0.08, "anchor_y": 0.68},
{"id": "right_squeeze", "label": "Grip", "anchor_x": 0.85, "anchor_y": 0.68},
{"id": "left_thumbstick", "label": "Trackpad", "anchor_x": 0.20, "anchor_y": 0.42},
{"id": "right_thumbstick", "label": "Trackpad", "anchor_x": 0.72, "anchor_y": 0.42},
{"id": "left_thumbstick_click", "label": "Trackpad Click", "anchor_x": 0.20, "anchor_y": 0.42},
{"id": "right_thumbstick_click", "label": "Trackpad Click", "anchor_x": 0.72, "anchor_y": 0.42}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

+17
View File
@@ -0,0 +1,17 @@
{
"video_options": {
"renderer": "opengl",
"resolution": "1024x768",
"initial_screen_width_m": 1.600000,
"initial_screen_distance_m": 2.000000,
"game_screen_width_m": 3.400000,
"game_screen_distance_m": 2.000000,
"screen_width_offset_m": 1.000000,
"screen_width_step_m": 0.600000,
"screen_distance_offset_m": 1.000000,
"screen_distance_step_m": 0.500000
},
"general": {
"language": "english"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

-132
View File
@@ -1,132 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="sketch_main_menu.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="2.5287798"
inkscape:cx="167.47208"
inkscape:cy="123.18194"
inkscape:window-width="1920"
inkscape:window-height="962"
inkscape:window-x="0"
inkscape:window-y="33"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#000080;stroke-width:0.193167"
id="rect1"
width="63.682735"
height="4.0229344"
x="4.4946499"
y="3.4182527" />
<rect
style="fill:#b3b3b3;stroke-width:0.543629"
id="rect1-5"
width="63.682735"
height="31.862694"
x="4.5049582"
y="7.3757277" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:1.41111px;line-height:1.25;font-family:FreeSans;-inkscape-font-specification:FreeSans;stroke-width:0.264583"
x="25.181696"
y="20.839491"
id="text2"><tspan
sodipodi:role="line"
id="tspan2"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:FreeSans;-inkscape-font-specification:FreeSans;stroke-width:0.264583"
x="25.181696"
y="20.839491">Menu content here</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:1.41111px;line-height:1.25;font-family:FreeSans;-inkscape-font-specification:FreeSans;fill:#ffffff;stroke-width:0.264583"
x="6.1205692"
y="5.8075914"
id="text3"><tspan
sodipodi:role="line"
id="tspan3"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:FreeSans;-inkscape-font-specification:FreeSans;fill:#ffffff;stroke-width:0.264583"
x="6.1205692"
y="5.8075914">Questshock (v3.0.2)</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:1.41111px;line-height:1.25;font-family:FreeSans;-inkscape-font-specification:FreeSans;fill:#ffffff;stroke-width:0.264583"
x="60.855247"
y="5.7559261"
id="text4"><tspan
sodipodi:role="line"
id="tspan4"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:FreeSans;-inkscape-font-specification:FreeSans;fill:#ffffff;stroke-width:0.264583"
x="60.855247"
y="5.7559261">[FPS: 70]</tspan></text>
<rect
style="fill:#000080;stroke-width:0.156477"
id="rect1-2"
width="63.682735"
height="2.6398373"
x="4.471632"
y="39.1059" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:1.41111px;line-height:1.25;font-family:FreeSans;-inkscape-font-specification:FreeSans;fill:#ffffff;stroke-width:0.264583"
x="5.4945946"
y="40.859875"
id="text3-7"><tspan
sodipodi:role="line"
id="tspan3-5"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:FreeSans;-inkscape-font-specification:FreeSans;fill:#ffffff;stroke-width:0.264583"
x="5.4945946"
y="40.859875">Main Menu</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:1.41111px;line-height:1.25;font-family:FreeSans;-inkscape-font-specification:FreeSans;fill:#ffffff;stroke-width:0.264583"
x="13.718193"
y="40.915668"
id="text3-7-8"><tspan
sodipodi:role="line"
id="tspan3-5-9"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:FreeSans;-inkscape-font-specification:FreeSans;fill:#ffffff;stroke-width:0.264583"
x="13.718193"
y="40.915668">Video Options</tspan></text>
<rect
style="fill:#1a1a1a;stroke:#ffffff;stroke-width:0.256016"
id="rect4"
width="0.055807162"
height="2.4157476"
x="12.918808"
y="39.218266" />
<rect
style="fill:#1a1a1a;stroke:#ffffff;stroke-width:0.256016"
id="rect4-1"
width="0.055807162"
height="2.4157476"
x="22.843752"
y="39.203552" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.2 KiB