From 9172c68558ad2c5dea089383d4cdb7046f0931b5 Mon Sep 17 00:00:00 2001 From: ml Date: Sun, 20 Sep 2026 15:14:55 +0200 Subject: [PATCH] Update the docs and add AGENTS.md --- AGENTS.md | 373 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ CONFIG.md | 10 +- README.md | 93 ++++++++++++-- 3 files changed, 462 insertions(+), 14 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f7d99ef --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,373 @@ +# AGENTS.md + +Guidance for AI coding agents (and new contributors) working on Questshock. +README.md is the user-facing build/usage guide and CONFIG.md documents the +configuration file; this file covers how the code is organised and the rules to +follow when changing it. + +## 1. What this project is + +Questshock plays the 1994 game System Shock on a standalone VR headset (Meta +Quest 2 is the tested device). It wraps [Shockolate](https://github.com/Interrupt/systemshock), +a cross-platform port of the original engine, in an Android app that: + +- runs the unmodified 2D engine (software or OpenGL renderer) and shows its + frame on a head-tracked virtual screen (an OpenXR quad layer); +- shows a menu and an on-screen keyboard as separate OpenXR quads, operated by + a laser pointer from either controller; +- maps controller inputs to game actions through a configurable binding layer; +- persists settings in `/sdcard/questshock/questshock.config.json`. + +A desktop (Linux/Windows) build of the same engine exists for development +without a headset; it has none of the Android/OpenXR code. + +## 2. Hard rules + +These come from the maintainer. Follow them without being asked again. + +1. **Never commit.** Do not run `git commit` (or amend, rebase, push), even if + asked to commit. Draft the commit message and show it; the maintainer runs + the commit. Mention any new untracked file that has to be `git add`ed + (typically a new patch under `android/engine-patches/`). +2. **Do not build.** Do not run `make apk`, `./gradlew`, or `make android-studio` + to check that a change compiles. The maintainer deploys from Android Studio. + `make apk` and `make android-studio` overwrite `android/engine.properties` + with container-only paths and break an open Android Studio project. What you + can do is dry-run patches (section 6) and run static consistency checks + (section 12). Say plainly in your report that the change was not built or + tested. +3. **`engine/` and `android/gl4es-src/` are never edited.** They are vendored + snapshots. Every change to them is a numbered patch under + `android/engine-patches/` or `android/gl4es-patches/`, applied to a scratch + copy at build time. (`engine/` is GPLv3, see README section 6.) +4. **Patch numbers stay contiguous** (`01`, `02`, ... with no gap). When you + insert or delete a patch, renumber the rest and update every reference to + the numbers (README patch reference, code comments). +5. **Comments and documentation describe the current state and why it has to be + that way.** No history or narrative: no "used to", "originally", "no + longer", "now", "this was fixed", "confirmed on device", incident stories or + dropped alternatives. History belongs in commit messages. A comment that + would read oddly to someone who never saw an earlier version should be + rewritten or cut. Default to one or two lines. (README section 5.5, the + debugging notes, is the one deliberate exception.) +6. **Standalone headset only, OpenXR first.** No feature may need a host PC + (no PCVR, tethering or streaming). Prefer OpenXR APIs over vendor-specific + ones (Meta's Oculus Mobile SDK and the like) so the port can run on other + Android OpenXR headsets. Flag a request that would break either rule + instead of quietly doing it. +7. **Do not bump `build-image/VERSION`** just because `build-image/Dockerfile` + changed. Bump it only once a build with the current number has succeeded + end to end (for example a CI run). Ask if unsure. +8. **Keep docs in sync with behaviour.** A user-visible change updates + README.md and/or CONFIG.md in the same change; a new or renumbered engine + patch updates the README patch reference (section 5.4). + +## 3. Repository layout + +| Path | What it is | +|---|---| +| `engine/` | Vendored Shockolate source. Read-only for us. | +| `android/engine-patches/` | Numbered `patch -p1` files applied to a scratch copy of `engine/`. | +| `android/gl4es-src/`, `android/gl4es-patches/` | Vendored GL4ES (desktop GL to GLES) and its patches. | +| `android/app/src/main/cpp/` | First-party native code (section 4). | +| `android/app/src/main/java/de/ladkau/questshock/` | First-party Java (section 4). `org/libsdl/app/` is vendored SDL2 glue (zlib). | +| `android/app/build.gradle` | Gradle project; the `stageEngine` task stages the patched engine. | +| `android/force-rebuild.sh` | Stops Gradle daemons and wipes staged/build output for a fully fresh native build. | +| `build-image/` | Docker build environment (`Dockerfile`, `VERSION`, `prepare-android-project.sh`, `build-apk.sh`, engine build scripts). | +| `res/` | Bundled resources: `questshock.config.default.json`, `controller_profiles/` (JSON + PNG per controller), `assets/` (where the user's own game data goes, git-ignored), `run.sh`/`run.bat`. | +| `Makefile` | `engine`, `dist`, `package`, `apk`, `android-studio`, `clean`, ... (see README section 2). | +| `README.md`, `CONFIG.md`, `NOTICE.txt` | User docs, config reference, license notice. | + +`build/`, `dist/`, `dist-win/` and `res/assets/ss_ee/` are generated or +proprietary and git-ignored. `registry.env` holds local registry settings; do +not print it or commit it. + +## 4. Android app architecture + +### 4.1 Native modules (`android/app/src/main/cpp/`) + +| File | Role | +|---|---| +| `xr_session.c/.h` | OpenXR instance and session lifecycle, frame loop, composition layers (game quad, menu, keyboard, laser beams). | +| `xr_swapchain.c/.h` | Swapchain creation, acquire and release, per-image FBOs. | +| `xr_overlay.c/.h` | Generic "off-screen Android `View` rendered into an OpenXR quad" module, hit-tested by a laser pointer. The menu and the keyboard are two instances. | +| `xr_input.c/.h` | Controller input: OpenXR actions, raw-input layer, action bindings, capture-mode rebinding, key injection into the engine, free view, cyberspace scaling, snap-turn/smooth-move of the player's position, pointer rays. The largest file. | +| `xr_mouse.c/.h` | Injects mouse motion and clicks into the engine. | +| `qs_config.c/.h` | Configuration: JSON parse, apply, write; screen profiles; menu-driven requests; cheats; menu-triggered pause. | +| `questshock_native.c` | JNI exports and small native helpers (`nativeChdir`, `nativeSendPrintableChar`, ...). | + +### 4.2 Java classes + +| Class | Role | +|---|---| +| `QuestShockActivity` | `SDLActivity` subclass. Creates `/sdcard/questshock/`, extracts bundled files, blocks engine start until the game data exists. | +| `OverlayPanel` | Base for the off-screen panels: builds the view tree, renders it to a texture, turns ray hits into touch events. | +| `MenuOverlay` | The menu: Main Menu, Video Options, Controller Options and Cheats tabs. Singleton. | +| `KeyboardOverlay` | The on-screen keyboard. | +| `Strings` | All menu and keyboard text in English, French and German. | + +### 4.3 How the pieces fit + +- The engine's present path is redirected (engine patch 11) so that + `SDLDraw()` submits into the OpenXR game-quad swapchain. The per-frame + entry point of `xr_input.c` is `xr_input_sync_and_draw()`, called from the + frame path in `xr_session.c` on the engine's own thread. +- The engine's own log output goes to logcat (patch 08); Java and native + first-party code log under the tag `QuestShock`. +- Bindings, config and menu state are owned natively. Java only displays and + requests. + +### 4.4 Threads and cross-thread requests + +- Engine state (player struct, game loop, cheats, config apply) must only be + touched on the engine thread. +- Menu code calls native functions through JNI. Anything that changes engine + state must be enqueued, not executed inline: the JNI export calls a + `qs_config_request_*()` function, which pushes an SDL custom event + (`SDL_RegisterEvents`); `qs_config_handle_event()` handles it on the engine + thread (it is called from `pump_events()`, engine patch 21). To add a new + request, add an event type, the `qs_config_request_*()` enqueue function and + a branch in `qs_config_handle_event()`. +- Getters that Java calls (`qs_config_cheat_unlimited_mask()`, binding-name + getters, ...) must be plain scalar or string reads that are safe from any + thread. +- Per-frame work that must run on the engine thread (such as + `qs_config_frame_tick()`) is called from `xr_input.c`. +- JNI symbol names must match the Java `native` declarations exactly + (`Java_de_ladkau_questshock__`). A mismatch only fails at + runtime, so check it (section 12). + +### 4.5 Off-screen views (`OverlayPanel`, `MenuOverlay`, `KeyboardOverlay`) + +The view tree is built and drawn from native code and is never attached to a +window. Consequences that shape the UI code: + +- Only `ACTION_DOWN`/`ACTION_UP` touches arrive (the laser trigger). There is no + drag, fling or multi-touch. Scrolling is done by a self-drawn scroll bar + (`ScrollBarView` in `MenuOverlay`), not by the framework. +- `View.post()` never runs, so anything that defers work through it does not + work. `ProgressBar` and `SeekBar` are unusable for the same reason. +- Hover and hit-testing use `OverlayPanel.absoluteBounds()`, which subtracts + ancestor scroll offsets. +- Toggling a child's visibility needs an explicit re-measure and re-layout + (see `relayoutAndRedraw()`). +- All user-visible text goes through `Strings.get(key, language)` with an + English, French and German entry, and is re-applied in `applyTranslations()` + when the language changes. "OpenGL" is a brand name and is not translated. + +## 5. Configuration system + +- `qs_config.c` holds one parsed struct per group (`video_options`, `general`, + `player_position`, `player_controls`), a parse function, an apply function, + and the writer `qs_config_write_file()`. Groups are tagged live-appliable or + restart-required (`QS_*_REQUIRES_RESTART`). +- Load order at startup: `questshock.config.json`, else + `questshock.config.default.json` (bundled, refreshed on every launch), else + the engine's own `prefs.txt` behaviour. Save is only ever an explicit menu + action. +- Invalid values are logged and skipped individually; unknown keys are ignored. + +**Adding an option:** parsed-struct field, parse, apply (with validation and a +`WARN` on invalid input), write in `qs_config_write_file()`, restart flag, +`CONFIG.md`, and (if it should have a shipped default) `res/questshock.config.default.json`. + +The virtual screen has two profiles, "initial" (splash, cutscenes, main menu) +and "game", switched by `qs_screen_profile_notify_loop_mode()` from engine +patch 22. `s_screen_profile_is_game` in `qs_config.c` doubles as "a game is +running" and gates the cheats and the menu pause. + +## 6. Engine patches + +Rules: + +- Guard every engine change with `#ifdef __ANDROID__` so the desktop build is + unaffected, and keep hunks minimal. +- Patches are applied in filename order with `patch -p1` from the engine root + (paths look like `a/src/GameSrc/hkeyfunc.c`). Patch N is generated against + the tree with patches `01..N-1` applied. +- Some engine files use CRLF line endings (for example `mouselook.c`). Edits + and generated patches must preserve them byte for byte; check with `file` + or `cat -A`. An editor or script that rewrites line endings produces a + whole-file diff. +- Do not hand-edit hunk headers. Regenerate the patch from a scratch copy. + +Creating or changing a patch: + +1. Make a scratch copy of `engine/` (for example under the session scratch + directory) and apply patches `01..N-1` to it (`patch -p1 -d < ...`). +2. Copy the target file(s) to a second location, edit them in the scratch copy. +3. Produce the patch with `diff -u` using `a/` and `b/` path prefixes relative + to the engine root, and save it as `android/engine-patches/NN-android-.patch`. +4. Dry-run the whole series on a fresh copy of `engine/` + (`patch -p1 --dry-run`, or apply for real on the copy) and confirm every + patch applies cleanly, with the patched file byte-identical to what you + edited. +5. Keep numbering contiguous, and update the README patch reference (5.4) and + any code comments that cite patch numbers. + +Gradle's `stageEngine` task re-stages and re-patches on build, but it can wrongly +report `UP-TO-DATE` when a **new** patch file appears. If a native change does +not show up on the device, tell the maintainer to run `./gradlew --stop`, or +`android/force-rebuild.sh`, or delete `build/android-engine`, before assuming +the change is wrong. + +Engine symbols used from first-party code (for example `game_paused`, +`pause_game_func`, `toggle_giveall_func`, `qs_cheat_refill`, +`qs_in_cyberspace`) are declared `extern` next to their use in `qs_config.c` or +`xr_input.c`, with a comment naming the engine file and patch. + +## 7. Controller mapping (`xr_input.c`) + +Structure: + +- Physical controls are OpenXR actions, suggested once against three + interaction profiles: Meta Touch, HTC Vive wand and the core + `khr/simple_controller`. A single wrong path makes the whole + `xrSuggestInteractionProfileBindings` call fail, so each profile lists only + components it really has. A failure calls + `qs_config_report_fatal_input_error()` and aborts input init. +- The **raw-input layer** (`QsRawInput`) digitizes every physical control + (buttons, triggers, squeezes, thumbstick clicks, and eight thumbstick + directions with hysteresis) each frame. `QS_RAW_NONE` means unbound. +- **Actions** (`QsControlAction`) are bound to raw inputs through `s_binding[]`. + The default bindings are `kDefaults[]`. +- Contexts: `ALWAYS`, `NORMAL` (move, strafe, turn, jump, stand, crouch, prone) + and `CYBER` (the eleven `cyber_*` actions). The active set is chosen with + `qs_in_cyberspace()` (patch 24). Two actions conflict only if their contexts + can be active together (`xr_input_contexts_overlap()`). +- Capture mode (`xr_input_process_capture()`): the next fresh raw down-edge is + assigned to the chosen action. Any other action in an overlapping context that + held that input (or its symmetric alias) is set to `none`. The reset and the + timeout (8 s) paths never leave a half-assigned state. +- The menu button is not a raw input and can never be bound, so the menu can + always be reached to press **Reset to Defaults**. +- While the menu is open, `mouse_click` is always the trigger (the controller + used to operate the menu); after it closes, the bound `mouse_click` drives + both the keyboard and the game quad. +- While the Controller Options tab is open, the menu's own thumbstick + snap-turn and smooth-move are disabled + (`g_controller_options_tab_open`, set through + `nativeSetControllerOptionsTabOpen`). +- Free view: holding a `free_*_thumbstick_view` binding turns that stick into + the engine's mouse look (cursor forced hidden, stick direction bindings + suspended). It uses `SDL_ShowCursor(SDL_DISABLE)` because the engine zeroes + mouse-look velocity while the cursor is visible. +- Cyberspace pitch and bank scale with stick deflection through the globals + from patch 25 (`qs_cyber_pitch_scale`, `qs_cyber_bank_scale`). + +### Adding or changing a bindable action (checklist) + +All of these lists share one order and must stay identical: + +1. `QsControlAction` enum in `xr_input.c` (and append before `QS_ACTION_COUNT`). +2. `kActionNames[]` in `xr_input.c`. +3. `s_binding[]` initial values and `kDefaults[]` in `xr_input.c`. +4. If it injects a key: a `kKeyActions[]` entry (keycode, modifiers, whether it + needs `SDL_TEXTINPUT`); add the key to the stuck-key watchdog list + (`kWatchedKeys`) if it is held continuously. +5. `ACTION_KEYS` in `MenuOverlay.java`, and `FIRST_ROW_ACTIONS` if it should be + listed first. +6. `Strings.java`: `action_` in EN/FR/DE. +7. `QS_MAX_PLAYER_CONTROL_ACTIONS` in `qs_config.c` if the count outgrows it. +8. `CONFIG.md`: the action tables and the count in the Player Controls intro; + README default-controls table if it has a default binding. +9. Check that no two actions that can be active together share a default input. + +### Injecting keys into the engine + +- Plain printable keys (letters, digits, Space, punctuation) need an + `SDL_TEXTINPUT` event plus `SDL_KEYDOWN`/`SDL_KEYUP`. Modifier, control-range, + F and keypad keys use `SDL_KEYDOWN`/`SDL_KEYUP` with the right `mod`. +- **Send the release on the real up edge, never in the same call as the + press.** The engine polls movement keys once per frame before it drains + events, so a press and release drained together are never seen and the key + silently does nothing. Tie the release to the physical button's up edge (or + the touch's `ACTION_UP`/`ACTION_CANCEL`). +- Held keys are tracked in the engine's `sshockKeyStates[]`; `xr_input.c` + keeps a watchdog that clears a watched key the engine still thinks is down + after its binding is released. +- Actions that share an engine key (for example the several ways to fire, or + the movement keys) are handled with sibling logic in the dispatch loop + (`wantDown[]`); do not press or release the shared key independently. + +## 8. Cheats tab + +- Buttons and switches live in `MenuOverlay.buildCheatsTab()`; ids are + `QS_CHEAT_*` in `qs_config.c` and `CHEAT_*` in `MenuOverlay.java` (keep them + identical). +- The engine's own cheat handlers (`toggle_giveall_func`, `toggle_physics_func`, + `toggle_up_level_func`, `toggle_down_level_func`) are called on the engine + thread through `qs_apply_cheat()`, and only while a game is running. +- The four unlimited switches are a bitmask in `qs_config.c` + (`QS_CHEAT_MASK_*`: health 1, energy 2, ammo 4, items 8). Every frame + `qs_config_frame_tick()` calls `qs_cheat_refill(mask)` (patch 26). The mask + values must match the patch. The switches are not persisted. +- The four `cheat_*` hotkeys are deliberately not bindable in + `player_controls`. New cheats go on the Cheats tab. + +## 9. Menu pause + +Opening the menu pauses a running game (`qs_apply_menu_pause()` through the +menu-visibility event); closing it resumes only if the menu did the pausing and +the game is still paused. Do not toggle pause blindly, since the pause function +toggles. + +## 10. Bundled files on the device + +`QuestShockActivity` extracts these into `/sdcard/questshock/`: + +- shaders, the default soundfont and `GET_ASSETS_QUEST.txt` (once); +- `questshock.config.default.json` and `controller_profiles/` (every launch, + overwriting only the bundled filenames so user-added profiles survive). + +`res/controller_profiles/` and the default config are staged into APK assets by +`build-image/prepare-android-project.sh`; new staged directories must also be +declared as `inputs.dir(...)` of `stageEngine` in `android/app/build.gradle`. + +The proprietary game data (`res/data/`, `res/sound/`) is never bundled or +committed. The app refuses to start the engine until it is present. + +## 11. Code and documentation style + +- Match the surrounding code: naming, comment density, idiom. C is plain C with + `static` helpers and `//` comments; Java targets Android API 34 and uses only the SDK plus + `androidx.core`. +- Keep functions and file-level constants near their use. Put magic numbers in + named constants (see `QS_FREE_VIEW_*`, `QS_CYBER_ROTATION_*`). +- Keep the `xr_input.h` and `qs_config.h` declarations documented with a short + comment saying which thread may call them. +- Comments: what and why, current state only (rule 5 in section 2). +- Docs: README.md is organised by numbered sections that other text cites + ("5.5.3"); do not renumber existing sections. Add new material as a new + subsection instead. +- English, French and German strings live in `Strings.java`; keep the three + entries of each key on the same `put(...)`. + +## 12. Verifying a change without building + +Because you may not build (rule 2), check statically and say so in the report: + +- **Patches:** apply the whole series to a fresh copy of `engine/` and confirm + no rejects. Confirm the edited file matches the patched result byte for byte. +- **JNI:** every `native` method in the Java classes has a + `Java_de_ladkau_questshock__` export with a matching signature, + and no export is left without a Java declaration. +- **Ordered lists:** `QsControlAction`, `kActionNames[]`, `s_binding[]`, + `kDefaults[]` and `ACTION_KEYS` have the same length and order. +- **Strings:** every `Strings.get("...")` key exists in `Strings.java`. +- **Balance:** braces and parentheses balance. `qs_config.c` reports a + brace-count offset of +5 from character literals containing `'{'`; that is + not a bug. +- **Docs:** README and CONFIG.md agree with the code (action counts, default + bindings, patch list). + +Then report what was checked, what was not (build, on-device behaviour), and +any device check the maintainer should do (for example a new patch that may +need `android/force-rebuild.sh`). + +## 13. Commit messages + +Draft only (rule 1). Use a short imperative subject (about 70 characters), a +blank line, then a body that explains what the change does and why: the +user-visible behaviour, the mechanism, and any constraint worth remembering. +Mention new files that need `git add`. diff --git a/CONFIG.md b/CONFIG.md index af40420..a38284b 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -174,6 +174,9 @@ project's own menu/keyboard chrome text (title bar, buttons, etc.). 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. +- The intro cutscene is also played in the selected language when its + voice-over file (`svfrintr.res`/`svgeintr.res` under `res/data/`) is + present; otherwise the English intro plays. ## Player Position (`player_position`) @@ -230,6 +233,9 @@ wait 8 seconds, to cancel without changing it. - **Restart required?**: No (live-appliable) - a rebind takes effect immediately, and persists across restarts once saved (**Save Configuration**, same as every other group here). +- **Menu movement**: while the Controller Options tab is open, the menu's own + thumbstick snap-turn and smooth-move (see `player_position`) are disabled, so + pressing inputs to assign them doesn't move you away from the screen. - **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 @@ -358,7 +364,9 @@ keyboard (there is no hold behavior). - `keypad 0`-`keypad 9`: a controller button per digit is not useful; use the on-screen keyboard. - `toggle_freelook` (F): the free-view actions cover mouse look. -- The 4 `cheat_*` entries: debug cheats. They are on the menu's Cheats tab instead. +- The 4 `cheat_*` entries: debug cheats. They are on the menu's Cheats tab + instead, along with four switches for unlimited health, energy, ammo and + items. Those are not saved to the config. - `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 diff --git a/README.md b/README.md index c1ed3e0..d242cc9 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,10 @@ a cross-platform port of the original game. "3. Game assets" below); `res/assets/extract_assets.sh` extracts it into `ss_ee/`. - `res/run.sh` - the launcher script, copied into `dist/` on build. +- `res/questshock.config.default.json` - the bundled default configuration + (see [CONFIG.md](CONFIG.md)); `res/controller_profiles/` - the per-controller + illustrations and labels shown in the Controller Options tab (see + [CONFIG.md](CONFIG.md)). Both are copied into the APK. - `Makefile` - targets: - `all` (default) - alias for `dist`. - `build-image` - builds the Docker build-image (`./build-image.sh`). @@ -105,6 +109,10 @@ Enhanced Edition one and `extract_assets.sh` pulls them out of it too, entirely optionally - it still runs fine with just the Enhanced Edition installer. +The intro cutscene follows the selected language too, when the matching +voice-over file (`svfrintr.res`/`svgeintr.res` under `res/data/`) is present; +otherwise the English intro plays. + ## 4. Desktop build Builds and runs Questshock natively on the desktop (your dev machine, or @@ -218,18 +226,17 @@ driven by a laser-pointer-style aim ray from each hand interact with it, same as the game's own Bluetooth mouse/keyboard input otherwise works unchanged. -The menu launcher (`MenuOverlay.java` - just a "Keyboard" button so far) -and the keyboard (`KeyboardOverlay.java`) are two fully independent quads, -each its own `XrOverlay` instance (`android/app/src/main/cpp/xr_overlay.c` -- a generic "off-screen Android View rendered into an OpenXR quad, -hit-tested via a laser pointer" module shared by both, and by whatever -similar panel comes next) with its own swapchain, visibility, and -position, so they can be shown/hidden/moved independently - a design -specifically meant to let the keyboard stay open while also picking -something from the (still-to-be-built-out) menu. The controller's menu -button only ever opens/closes the launcher; clicking its "Keyboard" button -only opens the keyboard (never touching the launcher's own visibility) - -the keyboard's own title bar holds the only way to close it again. +The menu (`MenuOverlay.java`, described in "5.1.1. Menu, controls and +cheats" below) and the keyboard (`KeyboardOverlay.java`) are two fully +independent quads, each its own `XrOverlay` instance +(`android/app/src/main/cpp/xr_overlay.c` - a generic "off-screen Android View +rendered into an OpenXR quad, hit-tested via a laser pointer" module shared by +both) with its own swapchain, visibility, and position, so they can be +shown/hidden/moved independently - the keyboard can stay open while also +picking something from the menu. The controller's menu button only ever +opens/closes the menu; the menu's "Keyboard" button only opens the keyboard +(never touching the menu's own visibility) - the keyboard's own title bar +holds the only way to close it again. The keyboard is a hand-built, full US-layout key grid (there's no system IME to borrow once immersive) that forwards each key press straight to the @@ -288,7 +295,50 @@ debugging enabled.) `/sdcard/questshock/` also holds an optional persistent settings file, `questshock.config.json`, loaded automatically at startup and saved/loaded from the in-headset menu - see [CONFIG.md](CONFIG.md) for the documented -options. +options. Every launch also refreshes `questshock.config.default.json` (the +bundled defaults, used when there is no `questshock.config.json`) and +`controller_profiles/` (controller illustrations for the Controller Options +tab) there; your own saved config and any extra profiles you add are left +alone. + +#### 5.1.1. Menu, controls and cheats + +The controller's menu button (left controller) opens and closes the menu. It +can't be reassigned. Opening it pauses a running game; closing it resumes the +game, unless it was already paused. Aim with either hand's ray and pull the +trigger to press buttons. The menu has four tabs: + +- **Main Menu** - Keyboard, Save/Load Configuration, Reset Position and the + language. +- **Video Options** - renderer, resolution, and the width and distance of the + virtual screen for the initial screen (splash, intro, main menu) and the game + screen. +- **Controller Options** - shows the active controller with labelled inputs, + and one button per action to assign it to a controller input (tap the + button, then press the input). Assigning an input that another action + already uses unbinds that other action. **Reset to Defaults** restores the + default bindings. +- **Cheats** - the engine's cheats as buttons (give all items and weapons, + toggle physics, next and previous level) and four independent switches for + unlimited health, energy, ammo and items. The switches last until the app + restarts and are not saved. They only work while a game is running. + +Default controls (all assignable, see [CONFIG.md](CONFIG.md)): + +| Input | On foot | In cyberspace | +|---|---|---| +| Left stick | Move forward/back, strafe | Pitch up/down, bank left/right | +| Right stick left/right | Turn | Roll | +| Right stick up | Stand | Thrust | +| Right stick down | Prone | - | +| Left stick click | Jump | - | +| Right stick click | Automap | - | +| Right A / B | Click / double-click | Click / double-click | +| Right trigger | Fire weapon | Fire weapon | +| Left grip (hold) | Free view with the left stick | Free view with the left stick | + +Every game hotkey (weapon select, MFD panels, save/load, ...) can also be +assigned to a controller input; they are unbound by default. ### 5.2. Building natively in Android Studio @@ -533,6 +583,23 @@ each one does: `qs_config_load_startup()` in `Shock.c`'s `main()`, right after `LoadPrefs()`, and one check in `sdl_events.c`'s `pump_events()` to drain the custom SDL events the menu's Save/Load Configuration buttons enqueue. +- **`22-android-screen-profile.patch`** - `mainloop.c` reports every + loop-mode change to `qs_screen_profile_notify_loop_mode()`, which picks + between the initial-screen and game-screen virtual-screen presets (see + [CONFIG.md](CONFIG.md)) and tells the menu whether a game is running. +- **`23-android-intro-language.patch`** - `cutsloop.c` opens the intro + cutscene file for the current language (`svgaintr.res`/`svfrintr.res`/ + `svgeintr.res`), falling back to English when it isn't installed. +- **`24-android-cyberspace-query.patch`** - adds `qs_in_cyberspace()` to + `mouselook.c`, which `xr_input.c` uses to switch between the on-foot and + cyberspace controller actions. +- **`25-android-cyberspace-analog-scale.patch`** - `movekeys.c` scales the + cyberspace pitch and bank strength by `qs_cyber_pitch_scale`/ + `qs_cyber_bank_scale`, which `xr_input.c` sets from the thumbstick + deflection each frame (1.0 for keys and buttons). +- **`26-android-cheat-refill.patch`** - adds `qs_cheat_refill()` to + `hkeyfunc.c`, which tops up health, energy, ammo and items (grenades and + drugs the player already holds) for the Cheats tab's unlimited switches. `android/gl4es-patches/`: