# Persistent configuration (`questshock.config.json`) QuestShock can load a persistent, human-editable configuration file at startup, in addition to (and independent of) the engine's own `prefs.txt`. - **Location**: `/sdcard/questshock/questshock.config.json` - the same folder you already copy `res/data`/`res/sound` into (see README.md section 5.1). - **Format**: plain JSON, grouped by concern. Each top-level key is a *group*; each key inside a group is one *option*. - **Loading**: read automatically at app startup, before the splash screen, if the file exists. If it doesn't, `questshock.config.default.json` (same folder) is used instead - a copy bundled with the app, refreshed on every launch, never overwriting your own saved `questshock.config.json`. If neither file exists, the engine behaves as it did before this file existed (driven by `prefs.txt` as usual). - **Saving**: never automatic. In-headset, open the menu (controller menu button) and press **Save Configuration** to write the file from your current live settings, overwriting it if present. Press **Load Configuration** to re-read the file and apply it without restarting the app. - **Live vs. restart-required**: every option below is tagged as either *live-appliable* (applying it while already playing takes effect immediately) or *restart-required*. Pressing **Load Configuration** applies every live-appliable change straight away; if the file also changes something restart-required, a dialog lists exactly which option(s) need it and offers: - **Dismiss** - keep playing with the live-appliable changes already applied; the restart-required option(s) stay as they were. - **Restart Now** - relaunches the app, which re-runs this same startup load and so applies every group in the file, including the restart-required ones. (Every option currently documented below is live-appliable, so this dialog won't actually appear yet - it's built in for options added in a future version that do need a restart.) - Unrecognized groups/keys are ignored (harmless if you're editing the file in advance for a future version). An invalid value for a *specific* option (see each option's possible values below) is logged and that one option is skipped - every other valid option in the file still applies. A file that isn't valid JSON at all is ignored entirely, with nothing applied. - `questshock.config.json` and `prefs.txt` are fully independent - loading or saving one never reads or writes the other. ## Example ```json { "video_options": { "renderer": "opengl", "resolution": "1024x768", "initial_screen_width_m": 1.6, "initial_screen_distance_m": 2.0, "game_screen_width_m": 2.8, "game_screen_distance_m": 2.0, "screen_width_offset_m": 1.0, "screen_width_step_m": 0.6, "screen_distance_offset_m": 1.0, "screen_distance_step_m": 0.5 }, "general": { "language": "english" }, "player_position": { "x": 0.0, "z": 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" } } ``` ## Video Options (`video_options`) ### `renderer` Which renderer the game uses to draw the 3D view. - **Type**: enum - **Possible values**: `"software"`, `"opengl"` - **Restart required?**: No (live-appliable) ### `resolution` The game's internal rendering resolution. - **Type**: enum - **Possible values**: `"320x200"`, `"320x400"`, `"640x400"`, `"640x480"`, `"1024x768"` - **Restart required?**: No (live-appliable) ### `initial_screen_width_m`, `initial_screen_distance_m`, `game_screen_width_m`, `game_screen_distance_m` The virtual screen's physical width and distance from the player (both in meters) - the game shows two independently configurable screen "profiles", switched automatically based on what's currently on screen: - **Initial Screen** (`initial_screen_*`) - the splash screen, intro cutscenes, and the game's own main menu. - **Game Screen** (`game_screen_*`) - actual gameplay (the 3D view, automap, cyberspace, etc.). Switching happens automatically - no player action needed - the instant the game itself transitions between these (e.g. starting/loading a game, or returning to the menu). - **Type**: number (meters) - **Possible values**: the in-headset Video Options tab offers 5 presets each - widths `1.0`, `1.6` (the original, pre-this-feature default), `2.2`, `2.8`, `3.4`; distances `1.0`, `1.5`, `2.0` (the original, pre-this-feature default), `2.5`, `3.0` - but a hand-edited value doesn't need to match a preset exactly (it's clamped to a sane range - width `[0.2, 10.0]`, distance `[0.5, 10.0]` - rather than snapped or rejected; the Video Options tab just shows no button selected for an in-between value). - **Restart required?**: No (live-appliable) - **Note**: the screen is always flat - there's no curve option. ### `screen_width_offset_m`, `screen_width_step_m`, `screen_distance_offset_m`, `screen_distance_step_m` Reshapes the 5 preset buttons the Video Options tab offers for width and distance (above) - config-file-only, there's no in-headset control for these. Shared by both **Initial Screen** and **Game Screen** (both columns' buttons show the same 5 values per row) - this only changes what the buttons offer, not either profile's current value. Preset button `index` (0-4) shows `offset + step * index`. For example, a distance offset of `0` and step of `2` shows buttons `0`, `2`, `4`, `6`, `8`; the defaults below reproduce this feature's original launch presets exactly. - **Type**: number (meters) - **Default values**: width offset `1.0`, step `0.6` (buttons `1.0`, `1.6`, `2.2`, `2.8`, `3.4`); distance offset `1.0`, step `0.5` (buttons `1.0`, `1.5`, `2.0`, `2.5`, `3.0`). - **Restart required?**: No (live-appliable) - **Note**: not clamped - an unusual offset/step (e.g. negative step) is accepted as-is. A resulting preset value outside the current value's own clamp range (see above) still clamps normally when that preset button is actually pressed. ## General (`general`) ### `language` The game's UI/text language - both the game's own resource strings and this project's own menu/keyboard chrome text (title bar, buttons, etc.). - **Type**: enum - **Possible values**: `"english"`, `"french"`, `"german"` - **Restart required?**: No (live-appliable) - applies the same way the game's own in-game options menu applies a language change (reloads the language-specific resource files and refreshes the inventory/MFD/side-icon UI immediately). - **Note**: French and German both need their own full resource pack (`frnstrng.res`/`mfdfrn.res`, `gerstrng.res`/`mfdger.res` under `res/data/`) - if any of those files aren't present, the switch is refused (logged, `questshock.config.json` and the current language are left unchanged) rather than leaving the game with no valid resource file open. See README.md section 3 ("Game assets") for where `mfdfrn.res`/ `mfdger.res` come from - the GOG Enhanced Edition installer alone doesn't include them. ## Player Position (`player_position`) Where the player is virtually standing/facing, independent of - and not reset by - starting a new game or loading a save (same independence from `prefs.txt`/savegames as every other group here). Set by moving around in-headset (menu open, left thumbstick snap-turns, right thumbstick smooth- moves) to line yourself up with the game quad ("virtual screen"). Height is never included here - it always comes from your real tracked headset height, not this file. **Startup fallback**: only applied at startup if all three fields (`x`, `z`, `yaw_degrees`) are present - a config file with none of them (or none at all), or a partial/hand-edited one missing any of the three, falls back to an automatic recenter instead (see **Reset Position** below), using your real head pose at that moment. A complete saved position is applied as-is, with no such recenter. **Reset Position** (Main Menu tab) is a live-only action, not a config value: it moves you to the centre in front of the game quad, based on where your head actually is right now. It resets only your *position*, not which way you're facing, so the screen does not rotate to follow your gaze - press it while looking off to one side and the screen stays off to the other side. The automatic recenter at startup (when there's no saved position) does face the screen towards where you're looking. Reset Position never reads or writes `questshock.config.json`. ### `x`, `z` Horizontal position, in meters, on the play space's floor plane. - **Type**: number - **Restart required?**: No (live-appliable) ### `yaw_degrees` Which way the player is virtually facing, in degrees. - **Type**: number - **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.