Add remappable controller controls with a Controller Options tab
build / build (push) Successful in 2m14s
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.
This commit is contained in:
@@ -66,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",
|
||||||
|
"toggle_freelook": "right_thumbstick_click",
|
||||||
|
"fire_weapon": "right_trigger"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -177,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`
|
||||||
|
|
||||||
@@ -198,3 +214,196 @@ 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 45 in-game actions to a physical controller input: 9
|
||||||
|
movement/click actions plus 36 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 |
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
`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 36 bindable actions. `stand` (`right_thumbstick_up`), `prone`
|
||||||
|
(`right_thumbstick_down`), `toggle_freelook` (`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 |
|
||||||
|
| `toggle_freelook` | `f` | Toggle mouse-look |
|
||||||
|
| `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.
|
||||||
|
- The 4 `cheat_*` entries: debug cheats.
|
||||||
|
- `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.
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ def stageEngineTask = tasks.register("stageEngine", Exec) {
|
|||||||
inputs.file("${repoRoot}/build-image/Dockerfile")
|
inputs.file("${repoRoot}/build-image/Dockerfile")
|
||||||
// Staged into assets/ by prepare-android-project.sh.
|
// Staged into assets/ by prepare-android-project.sh.
|
||||||
inputs.file("${repoRoot}/res/questshock.config.default.json")
|
inputs.file("${repoRoot}/res/questshock.config.default.json")
|
||||||
|
inputs.dir("${repoRoot}/res/controller_profiles")
|
||||||
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")
|
||||||
|
|||||||
@@ -369,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;
|
||||||
@@ -376,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) {
|
||||||
@@ -428,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 == '{')
|
||||||
@@ -437,6 +458,9 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -874,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)
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -1013,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) {
|
||||||
@@ -1037,6 +1098,14 @@ static bool qs_config_write_file(void) {
|
|||||||
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);
|
||||||
@@ -1117,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)
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -1131,16 +1246,21 @@ static Uint32 s_set_initial_screen_width_event_type;
|
|||||||
static Uint32 s_set_initial_screen_distance_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_width_event_type;
|
||||||
static Uint32 s_set_game_screen_distance_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;
|
||||||
|
|
||||||
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()/
|
||||||
// the four _set_*_screen_*() screen-profile setters enqueue - done
|
// the four _set_*_screen_*() screen-profile setters/the three
|
||||||
// here, once, on the engine's own thread and before the menu/its
|
// control-capture/reset requests enqueue - done here, once, on the
|
||||||
// buttons could possibly exist yet, rather than lazily from those
|
// engine's own thread and before the menu/its buttons could possibly
|
||||||
// functions themselves (which run on Android's UI thread and could
|
// exist yet, rather than lazily from those functions themselves (which
|
||||||
// otherwise race each other doing this lazily).
|
// run on Android's UI thread and could otherwise race each other doing
|
||||||
Uint32 base = SDL_RegisterEvents(10);
|
// this lazily).
|
||||||
|
Uint32 base = SDL_RegisterEvents(14);
|
||||||
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;
|
||||||
@@ -1151,6 +1271,10 @@ void qs_config_load_startup(void) {
|
|||||||
s_set_game_screen_width_event_type = base + 7;
|
s_set_game_screen_width_event_type = base + 7;
|
||||||
s_set_initial_screen_distance_event_type = base + 8;
|
s_set_initial_screen_distance_event_type = base + 8;
|
||||||
s_set_game_screen_distance_event_type = base + 9;
|
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;
|
||||||
|
|
||||||
QsParsedConfig parsed;
|
QsParsedConfig parsed;
|
||||||
bool haveConfig = qs_config_read_file(&parsed);
|
bool haveConfig = qs_config_read_file(&parsed);
|
||||||
@@ -1266,6 +1390,65 @@ void qs_config_request_set_game_screen_distance(int preset_index) {
|
|||||||
SDL_PushEvent(&event);
|
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_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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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())
|
||||||
@@ -1305,15 +1488,10 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -1345,5 +1523,29 @@ bool qs_config_handle_event(const SDL_Event *event) {
|
|||||||
qs_preset_value(s_screen_distance_offset_m, s_screen_distance_step_m, index));
|
qs_preset_value(s_screen_distance_offset_m, s_screen_distance_step_m, index));
|
||||||
return true;
|
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_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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,14 +56,10 @@ 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);
|
||||||
|
|
||||||
// Same cross-thread-dispatch reasoning as the four above - called from the
|
// Same cross-thread-dispatch reasoning as the four above - called from the
|
||||||
@@ -81,14 +77,45 @@ 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_width(int preset_index);
|
||||||
void qs_config_request_set_game_screen_distance(int preset_index);
|
void qs_config_request_set_game_screen_distance(int preset_index);
|
||||||
|
|
||||||
// Returns true and handles the event if it's one of the ten custom events
|
// Cross-thread requests from the Controller Options tab (MenuOverlay.java),
|
||||||
// qs_config_request_save()/_load()/_set_renderer()/_set_resolution()/
|
// enqueued like every request_*() above. action_index indexes
|
||||||
// _set_language()/_reset_position()/the four _set_*_screen_*() setters
|
// xr_input_action_name()'s list. Begin/cancel start and stop rebind capture;
|
||||||
// enqueue - called from pump_events() (sdl_events.c, see
|
// reset restores all bindings (the built-in defaults, then
|
||||||
// android/engine-patches/21-android-config.patch) on the engine's own
|
// questshock.config.default.json's player_controls group if it has one).
|
||||||
// thread, once per polled event, before its normal switch on well-known
|
void qs_config_request_begin_control_capture(int action_index);
|
||||||
// SDL_* event types. Returns false (no-op) for any other event type,
|
void qs_config_request_cancel_control_capture(void);
|
||||||
// including before the ten custom types have even been registered.
|
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);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// engine's thread, once per polled event. Returns false for any other event
|
||||||
|
// type, including before the custom types are 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
|
// Called directly (no event-queue dispatch - see
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
@@ -233,3 +234,39 @@ 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);
|
||||||
|
}
|
||||||
|
|||||||
+1105
-194
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|||||||
@@ -2,22 +2,43 @@ package de.ladkau.questshock;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.ActivityManager;
|
import android.app.ActivityManager;
|
||||||
|
import android.app.AlertDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.graphics.drawable.StateListDrawable;
|
import android.graphics.drawable.StateListDrawable;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main menu launcher quad (see OverlayPanel for the shared off-screen-
|
* The main menu launcher quad (see OverlayPanel for the shared off-screen-
|
||||||
@@ -73,6 +94,33 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
// this same language's own Strings.java translation is filed under.
|
// this same language's own Strings.java translation is filed under.
|
||||||
private static final String[] LANGUAGE_KEYS = {"english", "french", "german"};
|
private static final String[] LANGUAGE_KEYS = {"english", "french", "german"};
|
||||||
|
|
||||||
|
// Controller Options tab actions. Order must match xr_input.c's
|
||||||
|
// QsControlAction enum/kActionNames[]: the indices go straight to
|
||||||
|
// nativeRequestBeginControlCapture()/nativeGetControlBindingKey(). Row
|
||||||
|
// labels are Strings.get("action_" + ACTION_KEYS[i], ...). The hotkey
|
||||||
|
// actions are unbound by default; xr_input.c lists the HotKeyLookup[]
|
||||||
|
// entries that are not exposed.
|
||||||
|
private static final String[] ACTION_KEYS = {
|
||||||
|
"move_forward", "move_back", "strafe_left", "strafe_right",
|
||||||
|
"turn_left", "turn_right", "jump", "mouse_click", "mouse_double_click",
|
||||||
|
"stand", "crouch", "prone", "toggle_freelook", "pause",
|
||||||
|
"full_view", "normal_view", "map_view", "save_game", "load_game",
|
||||||
|
"select_grenade", "arm_grenade", "select_drug", "use_drug",
|
||||||
|
"toggle_online_help", "show_help", "toggle_music", "quit",
|
||||||
|
"cycle_weapon_next", "cycle_weapon_prev", "cycle_detail",
|
||||||
|
"reload_weapon_primary", "reload_weapon_secondary", "hud_color",
|
||||||
|
"clear_fullscreen",
|
||||||
|
"mfd_left_1", "mfd_left_2", "mfd_left_3", "mfd_left_4", "mfd_left_5",
|
||||||
|
"mfd_right_1", "mfd_right_2", "mfd_right_3", "mfd_right_4", "mfd_right_5",
|
||||||
|
"fire_weapon",
|
||||||
|
};
|
||||||
|
private static final String CONTROLLER_PROFILES_DIR = QuestShockActivity.GAME_DIR + "/controller_profiles";
|
||||||
|
// Shown (with its own explanatory note - see Strings.java's
|
||||||
|
// "generic_profile_note") whenever no bundled profile's
|
||||||
|
// interaction_profile matches the runtime's active one, same fallback
|
||||||
|
// reasoning as qs_config.c's own generic khr/simple_controller binding.
|
||||||
|
private static final String GENERIC_PROFILE_FILENAME = "generic.json";
|
||||||
|
|
||||||
private static MenuOverlay instance;
|
private static MenuOverlay instance;
|
||||||
|
|
||||||
// Kept so relayoutAndRedraw() (see its own comment) can re-measure/
|
// Kept so relayoutAndRedraw() (see its own comment) can re-measure/
|
||||||
@@ -87,13 +135,15 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
private TextView titleView;
|
private TextView titleView;
|
||||||
private TextView statsView;
|
private TextView statsView;
|
||||||
|
|
||||||
// Content area (tabs) - only one of mainMenuTab/videoOptionsTab is
|
// Content area (tabs) - only one of mainMenuTab/videoOptionsTab/
|
||||||
// VISIBLE at a time; currentTab tracks which.
|
// controllerOptionsTab is VISIBLE at a time; currentTab tracks which.
|
||||||
private View mainMenuTab;
|
private View mainMenuTab;
|
||||||
private View videoOptionsTab;
|
private View videoOptionsTab;
|
||||||
|
private View controllerOptionsTab;
|
||||||
private View currentTab;
|
private View currentTab;
|
||||||
private Button mainMenuTabButton;
|
private Button mainMenuTabButton;
|
||||||
private Button videoOptionsTabButton;
|
private Button videoOptionsTabButton;
|
||||||
|
private Button controllerOptionsTabButton;
|
||||||
|
|
||||||
// Main Menu tab's own two states (only one VISIBLE at a time, within
|
// Main Menu tab's own two states (only one VISIBLE at a time, within
|
||||||
// mainMenuTab) - mainMenuContent is the normal state (button row +
|
// mainMenuTab) - mainMenuContent is the normal state (button row +
|
||||||
@@ -134,6 +184,32 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
private Button[] gameScreenWidthButtons;
|
private Button[] gameScreenWidthButtons;
|
||||||
private Button[] gameScreenDistanceButtons;
|
private Button[] gameScreenDistanceButtons;
|
||||||
|
|
||||||
|
// Controller Options tab controls - one row per ACTION_KEYS entry
|
||||||
|
// (controlRows[i] toggles VISIBLE/GONE per refreshControllerOptionsSelection()'s
|
||||||
|
// active-profile filtering; controlActionLabels/controlBindingButtons
|
||||||
|
// stay 1:1 with ACTION_KEYS regardless of visibility).
|
||||||
|
private ImageView controllerImageView;
|
||||||
|
private TextView controllerProfileLabel;
|
||||||
|
private TextView controllerGenericNoteView;
|
||||||
|
private ScrollView controllerScrollView;
|
||||||
|
private ScrollBarView controllerScrollBar;
|
||||||
|
private LinearLayout[] controlRows;
|
||||||
|
private TextView[] controlActionLabels;
|
||||||
|
private Button[] controlBindingButtons;
|
||||||
|
private Button controlResetButton;
|
||||||
|
// -1 = no row currently listening for a press; otherwise an index into
|
||||||
|
// ACTION_KEYS - the one row whose text refreshControllerOptionsSelection()
|
||||||
|
// leaves alone (showing the "Press a button…" placeholder) rather than
|
||||||
|
// overwriting with its actual current binding.
|
||||||
|
private int capturingActionIndex = -1;
|
||||||
|
// Cheap memoization keyed on the active interaction profile path string
|
||||||
|
// (xr_input.c only recomputes that once per frame anyway) - avoids
|
||||||
|
// re-reading/re-parsing every *.json file in CONTROLLER_PROFILES_DIR on
|
||||||
|
// every refreshControllerOptionsSelection() call (periodic, every
|
||||||
|
// STATS_REFRESH_MS while this tab is visible).
|
||||||
|
private String cachedProfilePath;
|
||||||
|
private ControllerProfile cachedProfile;
|
||||||
|
|
||||||
private int currentLanguage = Strings.ENGLISH;
|
private int currentLanguage = Strings.ENGLISH;
|
||||||
|
|
||||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
@@ -167,11 +243,15 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
FrameLayout content = new FrameLayout(activity);
|
FrameLayout content = new FrameLayout(activity);
|
||||||
mainMenuTab = buildMainMenuTab();
|
mainMenuTab = buildMainMenuTab();
|
||||||
videoOptionsTab = buildVideoOptionsTab();
|
videoOptionsTab = buildVideoOptionsTab();
|
||||||
|
controllerOptionsTab = buildControllerOptionsTab();
|
||||||
content.addView(mainMenuTab, new FrameLayout.LayoutParams(
|
content.addView(mainMenuTab, new FrameLayout.LayoutParams(
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
|
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
|
||||||
content.addView(videoOptionsTab, new FrameLayout.LayoutParams(
|
content.addView(videoOptionsTab, new FrameLayout.LayoutParams(
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
|
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
|
||||||
|
content.addView(controllerOptionsTab, new FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
|
||||||
videoOptionsTab.setVisibility(View.GONE);
|
videoOptionsTab.setVisibility(View.GONE);
|
||||||
|
controllerOptionsTab.setVisibility(View.GONE);
|
||||||
currentTab = mainMenuTab;
|
currentTab = mainMenuTab;
|
||||||
panel.addView(content, new LinearLayout.LayoutParams(
|
panel.addView(content, new LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f));
|
LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||||
@@ -435,6 +515,398 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
return tab;
|
return tab;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Horizontal scroll bar for the Controller Options tab - a track with a
|
||||||
|
// round thumb at `fraction` (0..1) along it. See
|
||||||
|
// buildControllerOptionsTab() for why this isn't a SeekBar.
|
||||||
|
private static final class ScrollBarView extends View {
|
||||||
|
private static final float PAD = 20f;
|
||||||
|
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private float fraction;
|
||||||
|
|
||||||
|
ScrollBarView(Context context) {
|
||||||
|
super(context);
|
||||||
|
paint.setStrokeCap(Paint.Cap.ROUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFraction(float value) {
|
||||||
|
fraction = Math.max(0f, Math.min(1f, value));
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
float fractionForX(float x) {
|
||||||
|
float usable = getWidth() - 2 * PAD;
|
||||||
|
return usable > 0 ? Math.max(0f, Math.min(1f, (x - PAD) / usable)) : 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
float cy = getHeight() / 2f;
|
||||||
|
float left = PAD;
|
||||||
|
float right = getWidth() - PAD;
|
||||||
|
paint.setColor(0xFF606060);
|
||||||
|
paint.setStrokeWidth(8f);
|
||||||
|
canvas.drawLine(left, cy, right, cy, paint);
|
||||||
|
paint.setColor(0xFFE0E0E0);
|
||||||
|
canvas.drawCircle(left + fraction * (right - left), cy, 16f, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controller Options tab (see CONFIG.md's "Player Controls"): an optional
|
||||||
|
// controller illustration and profile name at the top (from a matching
|
||||||
|
// res/controller_profiles/*.json and image, see
|
||||||
|
// refreshControllerOptionsSelection()), one row per ACTION_KEYS entry
|
||||||
|
// (label plus a button showing/capturing its binding), and a Reset to
|
||||||
|
// Defaults button. All rows are built once;
|
||||||
|
// refreshControllerOptionsSelection() shows or hides each to match the
|
||||||
|
// active controller profile.
|
||||||
|
//
|
||||||
|
// The rows don't fit the panel's fixed height, so they sit in a
|
||||||
|
// ScrollView. OverlayPanel.handleTouch() only delivers ACTION_DOWN and
|
||||||
|
// ACTION_UP (never ACTION_MOVE), so drag-to-scroll cannot work; a scroll
|
||||||
|
// bar below the list is tapped to jump to a position instead. A stock
|
||||||
|
// SeekBar cannot serve as that bar: it defers a press until an
|
||||||
|
// ACTION_MOVE past the touch slop (View.shouldDelayChildPressedState()
|
||||||
|
// defaults to true for every ViewGroup), so a tap does nothing.
|
||||||
|
// ScrollBarView computes the tapped position from its own touch
|
||||||
|
// listener, like each button's CLICK_ON_TOUCH_UP.
|
||||||
|
private View buildControllerOptionsTab() {
|
||||||
|
LinearLayout tab = new LinearLayout(activity);
|
||||||
|
tab.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
LinearLayout scrollContent = new LinearLayout(activity);
|
||||||
|
scrollContent.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
scrollContent.setPadding(24, 16, 24, 8);
|
||||||
|
|
||||||
|
controllerImageView = new ImageView(activity);
|
||||||
|
controllerImageView.setVisibility(View.GONE);
|
||||||
|
scrollContent.addView(controllerImageView,
|
||||||
|
new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 140));
|
||||||
|
|
||||||
|
controllerProfileLabel = new TextView(activity);
|
||||||
|
controllerProfileLabel.setTextColor(0xFFFFFFFF);
|
||||||
|
controllerProfileLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
|
||||||
|
controllerProfileLabel.setGravity(Gravity.CENTER);
|
||||||
|
scrollContent.addView(controllerProfileLabel);
|
||||||
|
|
||||||
|
controllerGenericNoteView = new TextView(activity);
|
||||||
|
controllerGenericNoteView.setTextColor(0xFFFFD060);
|
||||||
|
controllerGenericNoteView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||||
|
controllerGenericNoteView.setGravity(Gravity.CENTER);
|
||||||
|
controllerGenericNoteView.setPadding(0, 4, 0, 4);
|
||||||
|
controllerGenericNoteView.setVisibility(View.GONE);
|
||||||
|
scrollContent.addView(controllerGenericNoteView);
|
||||||
|
|
||||||
|
controlRows = new LinearLayout[ACTION_KEYS.length];
|
||||||
|
controlActionLabels = new TextView[ACTION_KEYS.length];
|
||||||
|
controlBindingButtons = new Button[ACTION_KEYS.length];
|
||||||
|
// Rows are created in display order (Fire Weapon first, then the
|
||||||
|
// rest in ACTION_KEYS order); every array below stays indexed by
|
||||||
|
// the real action index, which must keep matching xr_input.c.
|
||||||
|
int fireIndex = Arrays.asList(ACTION_KEYS).indexOf("fire_weapon");
|
||||||
|
for (int d = 0; d < ACTION_KEYS.length; d++) {
|
||||||
|
final int i = d == 0 ? fireIndex : (d - 1 < fireIndex ? d - 1 : d);
|
||||||
|
final int actionIndex = i;
|
||||||
|
LinearLayout row = new LinearLayout(activity);
|
||||||
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
row.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
|
||||||
|
TextView label = new TextView(activity);
|
||||||
|
label.setTextColor(0xFFFFFFFF);
|
||||||
|
label.setTextSize(TypedValue.COMPLEX_UNIT_SP, 15);
|
||||||
|
label.setText(Strings.get("action_" + ACTION_KEYS[i], currentLanguage));
|
||||||
|
row.addView(label,
|
||||||
|
new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||||||
|
|
||||||
|
Button bindingButton = new Button(activity);
|
||||||
|
bindingButton.setOnClickListener(v -> onControlBindingButtonTapped(actionIndex));
|
||||||
|
bindingButton.setOnTouchListener(CLICK_ON_TOUCH_UP);
|
||||||
|
bindingButton.setBackground(selectableButtonBackground());
|
||||||
|
bindingButton.setTextColor(buttonTextColors());
|
||||||
|
registerHoverable(bindingButton);
|
||||||
|
LinearLayout.LayoutParams bindingParams = new LinearLayout.LayoutParams(0, 64, 1f);
|
||||||
|
bindingParams.setMargins(4, 4, 4, 4);
|
||||||
|
row.addView(bindingButton, bindingParams);
|
||||||
|
|
||||||
|
scrollContent.addView(row, new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||||
|
controlRows[i] = row;
|
||||||
|
controlActionLabels[i] = label;
|
||||||
|
controlBindingButtons[i] = bindingButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
controllerScrollView = new ScrollView(activity);
|
||||||
|
controllerScrollView.addView(scrollContent, new ScrollView.LayoutParams(
|
||||||
|
ScrollView.LayoutParams.MATCH_PARENT, ScrollView.LayoutParams.WRAP_CONTENT));
|
||||||
|
tab.addView(controllerScrollView, new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||||
|
|
||||||
|
// The scroll bar (see this method's comment above). Not a SeekBar:
|
||||||
|
// ProgressBar records its constructing thread (the native render
|
||||||
|
// thread here) and, when set from the UI thread, defers the visual
|
||||||
|
// update through View.post(), which never runs on this unattached
|
||||||
|
// tree. ScrollBarView draws from a plain field instead. It is a
|
||||||
|
// sibling of controllerScrollView, so it stays fixed below the
|
||||||
|
// scrolling rows, and it is always shown (a tap is a no-op when
|
||||||
|
// nothing overflows).
|
||||||
|
controllerScrollBar = new ScrollBarView(activity);
|
||||||
|
controllerScrollBar.setOnTouchListener((v, event) -> {
|
||||||
|
if (event.getAction() != MotionEvent.ACTION_DOWN) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
float fraction = controllerScrollBar.fractionForX(event.getX());
|
||||||
|
int range = scrollContent.getHeight() - controllerScrollView.getHeight();
|
||||||
|
if (range > 0) {
|
||||||
|
controllerScrollView.scrollTo(0, Math.round(range * fraction));
|
||||||
|
}
|
||||||
|
controllerScrollBar.setFraction(fraction);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
// Keeps the thumb tracking the actual scroll position however it
|
||||||
|
// changed, not just from a tap on the bar itself.
|
||||||
|
controllerScrollView.setOnScrollChangeListener((v, scrollX, scrollY, oldX, oldY) -> {
|
||||||
|
int range = scrollContent.getHeight() - controllerScrollView.getHeight();
|
||||||
|
controllerScrollBar.setFraction(range > 0 ? (float) scrollY / range : 0f);
|
||||||
|
});
|
||||||
|
LinearLayout.LayoutParams scrollBarParams =
|
||||||
|
new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 48);
|
||||||
|
scrollBarParams.setMargins(24, 0, 24, 0);
|
||||||
|
tab.addView(controllerScrollBar, scrollBarParams);
|
||||||
|
|
||||||
|
LinearLayout resetRow = new LinearLayout(activity);
|
||||||
|
resetRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
resetRow.setGravity(Gravity.CENTER);
|
||||||
|
resetRow.setPadding(0, 4, 0, 8);
|
||||||
|
controlResetButton = new Button(activity);
|
||||||
|
controlResetButton.setText(Strings.get("reset_to_defaults", currentLanguage));
|
||||||
|
controlResetButton.setOnClickListener(v -> onResetControlsClicked());
|
||||||
|
controlResetButton.setOnTouchListener(CLICK_ON_TOUCH_UP);
|
||||||
|
controlResetButton.setBackground(buttonBackground());
|
||||||
|
controlResetButton.setTextColor(buttonTextColors());
|
||||||
|
registerHoverable(controlResetButton);
|
||||||
|
LinearLayout.LayoutParams resetParams =
|
||||||
|
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 56);
|
||||||
|
resetParams.setMargins(8, 8, 8, 8);
|
||||||
|
resetRow.addView(controlResetButton, resetParams);
|
||||||
|
tab.addView(resetRow);
|
||||||
|
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tapping a binding button starts capture for that row (or cancels it,
|
||||||
|
// if that row is the one already listening) - see xr_input.h's own
|
||||||
|
// capture_begin()/capture_cancel() comment. The row's own text is set
|
||||||
|
// to the localized placeholder immediately for instant feedback, rather
|
||||||
|
// than waiting for onControlCaptureResult() - which never arrives at
|
||||||
|
// all on cancel.
|
||||||
|
private void onControlBindingButtonTapped(int actionIndex) {
|
||||||
|
if (capturingActionIndex == actionIndex) {
|
||||||
|
nativeRequestCancelControlCapture();
|
||||||
|
capturingActionIndex = -1;
|
||||||
|
refreshControllerOptionsSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
capturingActionIndex = actionIndex;
|
||||||
|
nativeRequestBeginControlCapture(actionIndex);
|
||||||
|
controlBindingButtons[actionIndex].setText(Strings.get("capture_prompt", currentLanguage));
|
||||||
|
controlBindingButtons[actionIndex].setSelected(true);
|
||||||
|
relayoutAndRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onResetControlsClicked() {
|
||||||
|
if (capturingActionIndex != -1) {
|
||||||
|
nativeRequestCancelControlCapture();
|
||||||
|
capturingActionIndex = -1;
|
||||||
|
}
|
||||||
|
nativeRequestResetPlayerControls();
|
||||||
|
refreshControllerOptionsSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reflects the currently-active controller profile + bindings onto the
|
||||||
|
// tab - called when the tab is shown (switchTab()) and, while it's the
|
||||||
|
// visible tab, on the same periodic timer as the header stats
|
||||||
|
// (refreshStats()). Mirrors refreshVideoOptionsSelection()'s "query
|
||||||
|
// fresh, stays correct even if changed some other way" reasoning,
|
||||||
|
// except for whichever single row is mid-capture (see
|
||||||
|
// capturingActionIndex's own comment) - that one is left untouched so
|
||||||
|
// its placeholder isn't clobbered by this same periodic refresh.
|
||||||
|
private void refreshControllerOptionsSelection() {
|
||||||
|
String activeProfilePath = nativeGetActiveInteractionProfilePath();
|
||||||
|
ControllerProfile profile = loadControllerProfile(activeProfilePath);
|
||||||
|
|
||||||
|
boolean isGeneric = profile == null || GENERIC_PROFILE_FILENAME.equals(profile.sourceFileName);
|
||||||
|
controllerGenericNoteView.setVisibility(isGeneric ? View.VISIBLE : View.GONE);
|
||||||
|
if (isGeneric) {
|
||||||
|
controllerGenericNoteView.setText(Strings.get("generic_profile_note", currentLanguage));
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitmap image = profile != null ? loadProfileImage(profile) : null;
|
||||||
|
if (image != null) {
|
||||||
|
controllerImageView.setImageBitmap(image);
|
||||||
|
controllerImageView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
controllerImageView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
controllerProfileLabel.setText(profile != null ? profile.name : "");
|
||||||
|
|
||||||
|
for (int i = 0; i < ACTION_KEYS.length; i++) {
|
||||||
|
boolean relevant = profile == null || isActionRelevantForProfile(profile, nativeGetControlBindingKey(i));
|
||||||
|
controlRows[i].setVisibility(relevant ? View.VISIBLE : View.GONE);
|
||||||
|
if (i == capturingActionIndex) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
controlBindingButtons[i].setText(rawInputDisplayName(nativeGetControlBindingKey(i)));
|
||||||
|
controlBindingButtons[i].setSelected(false);
|
||||||
|
}
|
||||||
|
relayoutAndRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String rawInputDisplayName(String rawKey) {
|
||||||
|
return Strings.get("raw_" + rawKey, currentLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// "trigger"/"squeeze" are the two symmetric default bindings (see
|
||||||
|
// xr_input.h's own comment) - relevant to a profile if it has *either*
|
||||||
|
// hand's concrete equivalent; "none" (unbound) always shows, so a
|
||||||
|
// player can see - and rebind - an action this profile happens not to
|
||||||
|
// default anything useful to. Every other key is a concrete raw input,
|
||||||
|
// checked against the profile's own controls list at that list's own
|
||||||
|
// granularity - see baseControlId()'s own comment.
|
||||||
|
private static boolean isActionRelevantForProfile(ControllerProfile profile, String rawKey) {
|
||||||
|
if ("none".equals(rawKey)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ("trigger".equals(rawKey)) {
|
||||||
|
return profile.controlIds.contains("left_trigger") || profile.controlIds.contains("right_trigger");
|
||||||
|
}
|
||||||
|
if ("squeeze".equals(rawKey)) {
|
||||||
|
return profile.controlIds.contains("left_squeeze") || profile.controlIds.contains("right_squeeze");
|
||||||
|
}
|
||||||
|
return profile.controlIds.contains(baseControlId(rawKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The 8 digitized thumbstick-direction raw inputs (see xr_input.h's own
|
||||||
|
// comment on QsRawInput) all point at the same physical stick, so
|
||||||
|
// res/controller_profiles/*.json lists just one "left_thumbstick"/
|
||||||
|
// "right_thumbstick" control id per hand (one diagram anchor point,
|
||||||
|
// one row-visibility check) rather than 4 - this maps any of the 4
|
||||||
|
// direction-specific names back to that shared id. Every other raw
|
||||||
|
// input (including the *_thumbstick_click ones, which are listed as
|
||||||
|
// their own separate control id - see CONFIG.md) already matches a
|
||||||
|
// profile's controls list directly, unchanged.
|
||||||
|
private static String baseControlId(String rawKey) {
|
||||||
|
switch (rawKey) {
|
||||||
|
case "left_thumbstick_up":
|
||||||
|
case "left_thumbstick_down":
|
||||||
|
case "left_thumbstick_left":
|
||||||
|
case "left_thumbstick_right":
|
||||||
|
return "left_thumbstick";
|
||||||
|
case "right_thumbstick_up":
|
||||||
|
case "right_thumbstick_down":
|
||||||
|
case "right_thumbstick_left":
|
||||||
|
case "right_thumbstick_right":
|
||||||
|
return "right_thumbstick";
|
||||||
|
default:
|
||||||
|
return rawKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parsed res/controller_profiles/*.json content (see CONFIG.md) - name/
|
||||||
|
// interaction_profile/image/controls straight from the file, plus the
|
||||||
|
// lazily-decoded image Bitmap itself (decoded at most once per profile
|
||||||
|
// object, not on every refresh - see loadProfileImage()).
|
||||||
|
private static final class ControllerProfile {
|
||||||
|
String sourceFileName;
|
||||||
|
String name;
|
||||||
|
String interactionProfile;
|
||||||
|
String imageFileName;
|
||||||
|
final Set<String> controlIds = new HashSet<>();
|
||||||
|
Bitmap image;
|
||||||
|
boolean imageLoadFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scans CONTROLLER_PROFILES_DIR for the *.json file whose
|
||||||
|
// interaction_profile matches activeProfilePath, falling back to
|
||||||
|
// GENERIC_PROFILE_FILENAME if none match (e.g. the folder is missing,
|
||||||
|
// or a future runtime reports a profile this build doesn't ship a file
|
||||||
|
// for) - same fallback reasoning as xr_input.c's own
|
||||||
|
// khr/simple_controller binding. Result is memoized per active-profile-
|
||||||
|
// path string (see cachedProfilePath's own comment); returns null only
|
||||||
|
// if even the generic fallback file can't be found/parsed.
|
||||||
|
private ControllerProfile loadControllerProfile(String activeProfilePath) {
|
||||||
|
if (cachedProfile != null && Objects.equals(cachedProfilePath, activeProfilePath)) {
|
||||||
|
return cachedProfile;
|
||||||
|
}
|
||||||
|
File dir = new File(CONTROLLER_PROFILES_DIR);
|
||||||
|
File[] files = dir.listFiles((d, name) -> name.endsWith(".json"));
|
||||||
|
ControllerProfile matched = null;
|
||||||
|
ControllerProfile generic = null;
|
||||||
|
if (files != null) {
|
||||||
|
for (File file : files) {
|
||||||
|
ControllerProfile parsed = parseControllerProfileFile(file);
|
||||||
|
if (parsed == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (activeProfilePath != null && activeProfilePath.equals(parsed.interactionProfile)) {
|
||||||
|
matched = parsed;
|
||||||
|
}
|
||||||
|
if (GENERIC_PROFILE_FILENAME.equals(file.getName())) {
|
||||||
|
generic = parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ControllerProfile result = matched != null ? matched : generic;
|
||||||
|
cachedProfile = result;
|
||||||
|
cachedProfilePath = activeProfilePath;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ControllerProfile parseControllerProfileFile(File file) {
|
||||||
|
try (InputStream in = new FileInputStream(file)) {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
byte[] buf = new byte[4096];
|
||||||
|
int n;
|
||||||
|
while ((n = in.read(buf)) > 0) {
|
||||||
|
out.write(buf, 0, n);
|
||||||
|
}
|
||||||
|
JSONObject json = new JSONObject(out.toString("UTF-8"));
|
||||||
|
ControllerProfile profile = new ControllerProfile();
|
||||||
|
profile.sourceFileName = file.getName();
|
||||||
|
profile.name = json.optString("name", file.getName());
|
||||||
|
profile.interactionProfile = json.isNull("interaction_profile") ? null
|
||||||
|
: json.optString("interaction_profile", null);
|
||||||
|
profile.imageFileName = json.isNull("image") ? null : json.optString("image", null);
|
||||||
|
JSONArray controls = json.optJSONArray("controls");
|
||||||
|
if (controls != null) {
|
||||||
|
for (int i = 0; i < controls.length(); i++) {
|
||||||
|
String id = controls.getJSONObject(i).optString("id", null);
|
||||||
|
if (id != null) {
|
||||||
|
profile.controlIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return profile;
|
||||||
|
} catch (IOException | JSONException e) {
|
||||||
|
Log.e("QuestShock", "Could not parse controller profile " + file, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decoded at most once per ControllerProfile instance (cached alongside
|
||||||
|
// it, see loadControllerProfile()'s own memoization) - a missing/
|
||||||
|
// corrupt image file (e.g. the illustration hasn't been added to
|
||||||
|
// res/controller_profiles/ yet - see CONFIG.md) just means the tab
|
||||||
|
// shows no image, not a crash; imageLoadFailed avoids retrying the
|
||||||
|
// decode on every subsequent refresh.
|
||||||
|
private static Bitmap loadProfileImage(ControllerProfile profile) {
|
||||||
|
if (profile.imageFileName == null || profile.imageLoadFailed) {
|
||||||
|
return profile.image;
|
||||||
|
}
|
||||||
|
File file = new File(CONTROLLER_PROFILES_DIR, profile.imageFileName);
|
||||||
|
profile.image = BitmapFactory.decodeFile(file.getAbsolutePath());
|
||||||
|
profile.imageLoadFailed = profile.image == null;
|
||||||
|
return profile.image;
|
||||||
|
}
|
||||||
|
|
||||||
private View buildTabBar() {
|
private View buildTabBar() {
|
||||||
LinearLayout tabBar = new LinearLayout(activity);
|
LinearLayout tabBar = new LinearLayout(activity);
|
||||||
tabBar.setOrientation(LinearLayout.HORIZONTAL);
|
tabBar.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
@@ -469,6 +941,20 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
tabBar.addView(videoOptionsTabButton,
|
tabBar.addView(videoOptionsTabButton,
|
||||||
new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
||||||
|
|
||||||
|
View separator2 = new View(activity);
|
||||||
|
separator2.setBackgroundColor(0xFFFFFFFF);
|
||||||
|
tabBar.addView(separator2, new LinearLayout.LayoutParams(2, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
|
||||||
|
controllerOptionsTabButton = new Button(activity);
|
||||||
|
controllerOptionsTabButton.setText(Strings.get("controller_options", currentLanguage));
|
||||||
|
controllerOptionsTabButton.setBackground(selectableButtonBackground());
|
||||||
|
controllerOptionsTabButton.setTextColor(buttonTextColors());
|
||||||
|
controllerOptionsTabButton.setOnClickListener(v -> switchTab(controllerOptionsTab));
|
||||||
|
controllerOptionsTabButton.setOnTouchListener(CLICK_ON_TOUCH_UP);
|
||||||
|
registerHoverable(controllerOptionsTabButton);
|
||||||
|
tabBar.addView(controllerOptionsTabButton,
|
||||||
|
new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
||||||
|
|
||||||
return tabBar;
|
return tabBar;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,16 +962,35 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
if (tab == currentTab) {
|
if (tab == currentTab) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Defensive, per the Controller Options tab's own design - leaving
|
||||||
|
// this tab (however it happens) always cancels a pending capture,
|
||||||
|
// so it can never be silently left listening for a press that will
|
||||||
|
// now land on a row the player can't even see.
|
||||||
|
if (currentTab == controllerOptionsTab && capturingActionIndex != -1) {
|
||||||
|
nativeRequestCancelControlCapture();
|
||||||
|
capturingActionIndex = -1;
|
||||||
|
}
|
||||||
currentTab.setVisibility(View.GONE);
|
currentTab.setVisibility(View.GONE);
|
||||||
tab.setVisibility(View.VISIBLE);
|
tab.setVisibility(View.VISIBLE);
|
||||||
currentTab = tab;
|
currentTab = tab;
|
||||||
mainMenuTabButton.setSelected(tab == mainMenuTab);
|
mainMenuTabButton.setSelected(tab == mainMenuTab);
|
||||||
videoOptionsTabButton.setSelected(tab == videoOptionsTab);
|
videoOptionsTabButton.setSelected(tab == videoOptionsTab);
|
||||||
|
controllerOptionsTabButton.setSelected(tab == controllerOptionsTab);
|
||||||
|
nativeSetControllerOptionsTabOpen(tab == controllerOptionsTab);
|
||||||
resetHover();
|
resetHover();
|
||||||
if (tab == videoOptionsTab) {
|
if (tab == videoOptionsTab) {
|
||||||
refreshVideoOptionsSelection();
|
refreshVideoOptionsSelection();
|
||||||
} else if (tab == mainMenuTab) {
|
} else if (tab == mainMenuTab) {
|
||||||
refreshLanguageSelection();
|
refreshLanguageSelection();
|
||||||
|
} else if (tab == controllerOptionsTab) {
|
||||||
|
// Reset to the top only on actually switching into this tab -
|
||||||
|
// refreshControllerOptionsSelection() also runs on every
|
||||||
|
// periodic stats tick while it's the visible tab (see
|
||||||
|
// refreshStats()), which must never yank a mid-scroll view back
|
||||||
|
// to the top.
|
||||||
|
controllerScrollView.scrollTo(0, 0);
|
||||||
|
controllerScrollBar.setFraction(0f);
|
||||||
|
refreshControllerOptionsSelection();
|
||||||
}
|
}
|
||||||
relayoutAndRedraw();
|
relayoutAndRedraw();
|
||||||
}
|
}
|
||||||
@@ -649,6 +1154,8 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
refreshVideoOptionsSelection();
|
refreshVideoOptionsSelection();
|
||||||
} else if (currentTab == mainMenuTab) {
|
} else if (currentTab == mainMenuTab) {
|
||||||
refreshLanguageSelection();
|
refreshLanguageSelection();
|
||||||
|
} else if (currentTab == controllerOptionsTab) {
|
||||||
|
refreshControllerOptionsSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The stats text's width changes every tick (digit count, "ms"
|
// The stats text's width changes every tick (digit count, "ms"
|
||||||
@@ -702,6 +1209,20 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
gameScreenTitleView.setText(Strings.get("game_screen", currentLanguage));
|
gameScreenTitleView.setText(Strings.get("game_screen", currentLanguage));
|
||||||
gameScreenWidthLabel.setText(Strings.get("width", currentLanguage));
|
gameScreenWidthLabel.setText(Strings.get("width", currentLanguage));
|
||||||
gameScreenDistanceLabel.setText(Strings.get("distance", currentLanguage));
|
gameScreenDistanceLabel.setText(Strings.get("distance", currentLanguage));
|
||||||
|
|
||||||
|
controllerOptionsTabButton.setText(Strings.get("controller_options", currentLanguage));
|
||||||
|
controllerGenericNoteView.setText(Strings.get("generic_profile_note", currentLanguage));
|
||||||
|
controlResetButton.setText(Strings.get("reset_to_defaults", currentLanguage));
|
||||||
|
for (int i = 0; i < ACTION_KEYS.length; i++) {
|
||||||
|
controlActionLabels[i].setText(Strings.get("action_" + ACTION_KEYS[i], currentLanguage));
|
||||||
|
// The listening row's placeholder also needs re-translating on
|
||||||
|
// a language change mid-capture; every other row's actual
|
||||||
|
// binding text is refreshed by the very next periodic
|
||||||
|
// refreshControllerOptionsSelection() call regardless.
|
||||||
|
if (i == capturingActionIndex) {
|
||||||
|
controlBindingButtons[i].setText(Strings.get("capture_prompt", currentLanguage));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Dismiss" on the restart dialog - the live-appliable subset was
|
// "Dismiss" on the restart dialog - the live-appliable subset was
|
||||||
@@ -793,6 +1314,27 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
private static native float nativeGetCpuUtilization();
|
private static native float nativeGetCpuUtilization();
|
||||||
private static native float nativeGetGpuUtilization();
|
private static native float nativeGetGpuUtilization();
|
||||||
|
|
||||||
|
// Controller Options tab - same enqueue-only reasoning as every other
|
||||||
|
// request_*() above. actionIndex indexes ACTION_KEYS (must match
|
||||||
|
// xr_input.c's own action ordering - see that array's own comment).
|
||||||
|
private static native void nativeRequestBeginControlCapture(int actionIndex);
|
||||||
|
private static native void nativeRequestCancelControlCapture();
|
||||||
|
private static native void nativeRequestResetPlayerControls();
|
||||||
|
// Read-only queries, safe to call directly - same reasoning as
|
||||||
|
// nativeGetRendererIsOpenGl() etc. above. nativeGetControlBindingKey()
|
||||||
|
// returns one of xr_input.c's kRawInputNames strings (e.g. "right_b",
|
||||||
|
// "trigger", "none") - rawInputDisplayName() maps it to a localized
|
||||||
|
// label. nativeGetActiveInteractionProfilePath() returns the OpenXR
|
||||||
|
// interaction profile path string (e.g.
|
||||||
|
// "/interaction_profiles/oculus/touch_controller"), or "" before the
|
||||||
|
// first successful in-engine query.
|
||||||
|
private static native String nativeGetControlBindingKey(int actionIndex);
|
||||||
|
private static native String nativeGetActiveInteractionProfilePath();
|
||||||
|
// Tells xr_input.c when Controller Options is the visible tab, so the
|
||||||
|
// menu's thumbstick locomotion is off while bindings are being tested
|
||||||
|
// or reassigned - see switchTab().
|
||||||
|
private static native void nativeSetControllerOptionsTabOpen(boolean open);
|
||||||
|
|
||||||
// Called from xr_overlay.c's xr_overlay_init_jni(), on the render
|
// Called from xr_overlay.c's xr_overlay_init_jni(), on the render
|
||||||
// thread, right after it resolves this class via the activity's own
|
// thread, right after it resolves this class via the activity's own
|
||||||
// ClassLoader (plain FindClass() can't see app classes from a thread
|
// ClassLoader (plain FindClass() can't see app classes from a thread
|
||||||
@@ -841,6 +1383,56 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called from xr_input.c's xr_input_process_capture() (via
|
||||||
|
// qs_config_notify_control_capture_result(), qs_config.c) on the
|
||||||
|
// engine's own SDL thread, once a capture resolves - either a
|
||||||
|
// successful press (actionIndex is the row that was capturing) or a
|
||||||
|
// second call reporting a *different* row that got displaced by
|
||||||
|
// exclusivity (see xr_input.h's own comment), or a timeout/cancel
|
||||||
|
// (rawName null). Rather than applying rawName itself, this just clears
|
||||||
|
// capturingActionIndex (if it matches) and re-reads every row fresh
|
||||||
|
// from native truth - correct for either call shape with no need to
|
||||||
|
// special-case which one this is.
|
||||||
|
public static void onControlCaptureResult(final int actionIndex, final String rawName) {
|
||||||
|
if (instance == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
instance.activity.runOnUiThread(() -> {
|
||||||
|
if (actionIndex == instance.capturingActionIndex) {
|
||||||
|
instance.capturingActionIndex = -1;
|
||||||
|
}
|
||||||
|
instance.refreshControllerOptionsSelection();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from qs_config.c's qs_config_report_fatal_input_error(), from
|
||||||
|
// inside xr_input_init() - which runs before this class's own
|
||||||
|
// nativeInit() (see xr_session.c: xr_input_init() happens before the
|
||||||
|
// menu/keyboard overlays' xr_overlay_create() calls), so `instance` is
|
||||||
|
// guaranteed still null here and can't be used to reach the Activity
|
||||||
|
// the way showRestartDialog()/showMessageDialog() do. SDLActivity's own
|
||||||
|
// getContext() is set from onCreate(), long before any of this native
|
||||||
|
// XR startup runs, so it's used instead - the one deliberate exception
|
||||||
|
// to this class's usual "everything hangs off `instance`" convention.
|
||||||
|
// Non-cancelable, single "Exit" button, same hard-kill pattern as
|
||||||
|
// restartApp() (Runtime.getRuntime().exit(0), not finish() - by the
|
||||||
|
// time this fires, native startup is already partway through, so only
|
||||||
|
// killing the whole process is guaranteed to actually stop it).
|
||||||
|
public static void showFatalControllerBindingError(final String message) {
|
||||||
|
Context context = org.libsdl.app.SDLActivity.getContext();
|
||||||
|
if (!(context instanceof Activity)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Activity fatalActivity = (Activity) context;
|
||||||
|
fatalActivity.runOnUiThread(() -> new AlertDialog.Builder(fatalActivity)
|
||||||
|
.setTitle("Controller setup failed")
|
||||||
|
.setMessage(message)
|
||||||
|
.setCancelable(false)
|
||||||
|
.setPositiveButton("Exit", (dialog, which) -> Runtime.getRuntime().exit(0))
|
||||||
|
.create()
|
||||||
|
.show());
|
||||||
|
}
|
||||||
|
|
||||||
public static byte[] nativeTakePixelsIfDirty() {
|
public static byte[] nativeTakePixelsIfDirty() {
|
||||||
return instance == null ? null : instance.takePixelsIfDirty();
|
return instance == null ? null : instance.takePixelsIfDirty();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import org.libsdl.app.SDLSurface;
|
|||||||
* SDL_RWops, so Android's asset-manager fallback for SDL_RWFromFile
|
* SDL_RWops, so Android's asset-manager fallback for SDL_RWFromFile
|
||||||
* doesn't apply here) expects to find it - see android/engine-patches/ for
|
* doesn't apply here) expects to find it - see android/engine-patches/ for
|
||||||
* why this is done from Java instead of patching engine/ itself, which is
|
* why this is done from Java instead of patching engine/ itself, which is
|
||||||
* never modified.q
|
* never modified.
|
||||||
*
|
*
|
||||||
* super.onCreate() (SDLActivity's) must always run first and unconditionally
|
* super.onCreate() (SDLActivity's) must always run first and unconditionally
|
||||||
* - Android throws SuperNotCalledException otherwise, checked right after
|
* - Android throws SuperNotCalledException otherwise, checked right after
|
||||||
@@ -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,7 +144,7 @@ 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"), false);
|
copyAssetFile("res/soundfont.sf2", new File(gameDir, "res/soundfont.sf2"), false);
|
||||||
copyAssetFile("GET_ASSETS_QUEST.txt", marker, false);
|
copyAssetFile("GET_ASSETS_QUEST.txt", marker, false);
|
||||||
}
|
}
|
||||||
@@ -155,6 +156,13 @@ public class QuestShockActivity extends SDLActivity {
|
|||||||
copyAssetFile("questshock.config.default.json",
|
copyAssetFile("questshock.config.default.json",
|
||||||
new File(gameDir, "questshock.config.default.json"), true);
|
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
|
||||||
@@ -239,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 {
|
||||||
@@ -253,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), false);
|
copyAssetFile(assetDir + "/" + entry, new File(destDir, entry), overwrite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,106 @@ 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_toggle_freelook", "Toggle Free Look", "Basculer la vue libre",
|
||||||
|
"Freies Umsehen umschalten");
|
||||||
|
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");
|
||||||
|
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() {}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -92,6 +92,7 @@ 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 "$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
|
||||||
|
|||||||
@@ -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 |
@@ -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 |
@@ -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 |
Binary file not shown.
|
After Width: | Height: | Size: 283 KiB |
@@ -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 |
Reference in New Issue
Block a user