Add remappable controller controls with a Controller Options tab
build / build (push) Successful in 2m14s

Add a player_controls config group that maps game actions to controller
inputs, and a Controller Options menu tab to rebind them in-headset.

- 45 actions: movement, click/double-click, jump, fire, and the engine's own
  keyboard hotkeys (posture, save/load, weapon/MFD select, etc.), sent to the
  engine as synthetic key events. Cheat and dead hotkeys are not exposed.
- Press-to-capture rebinding with per-input exclusivity, Reset to Defaults
  (built-in defaults, then questshock.config.default.json), and new defaults:
  left stick moves/strafes, right stick turns.
- Controller profiles for Touch, Vive and generic OpenXR
  (res/controller_profiles/), matched to the active interaction profile at
  runtime, with a fatal dialog if a profile's bindings are rejected at
  startup.
- The menu's thumbstick locomotion is disabled while the Controller Options
  tab is open or a capture is active. A scrollable list with a tap-to-jump
  scroll bar handles the long action list.
- Opening the menu pauses a running game and resumes it on close.
- Reset Position now keeps the player's facing instead of turning the screen
  to the current gaze.
- Play the intro video in the game's language (engine patch 23), falling back
  to English.
- Watchdog and shared-key handling for stuck movement keys, plus key-edge
  logging.
This commit is contained in:
ml
2026-09-20 06:08:50 +02:00
parent e1e6a006d4
commit c24b7f15cb
21 changed files with 2489 additions and 392 deletions
+68 -19
View File
@@ -67,27 +67,22 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw);
bool xr_input_get_beam_layer(int hand, XrCompositionLayerQuad *out_quad);
// Recenters the player so the game quad ("virtual screen") appears directly
// ahead of wherever they're currently really standing/facing - a pure,
// live "return to a sane view of the screen" action, unrelated to any
// stored/config value (unlike xr_session_set_player_transform()'s other
// callers, this never reads or writes questshock.config.json - it's driven
// entirely by the player's current real head pose, the same way
// xr_input_sync_and_draw()'s own menu re-center is). Called from the Main
// Menu tab's "Reset Position" button (via qs_config.c's cross-thread
// 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.
// ahead of wherever they are standing and facing, from their current real head
// pose. Never reads or writes questshock.config.json. Called automatically
// once, from xr_input_sync_and_draw(), on the first frame with a valid head
// pose after startup, unless a complete saved player_position was loaded (see
// xr_input_skip_startup_recenter()): a saved position is relative to a
// previous session's real-space origin, which is not stable across launches.
// Returns false (no-op) if no valid head pose is available yet.
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
// complete saved player_position was actually loaded from
// 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).
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);
#ifdef __cplusplus