This commit is contained in:
@@ -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_<Class>_<method>`). 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 <scratch> < ...`).
|
||||||
|
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-<topic>.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_<key>` 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_<Class>_<method>` 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`.
|
||||||
@@ -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`/
|
open. See README.md section 3 ("Game assets") for where `mfdfrn.res`/
|
||||||
`mfdger.res` come from - the GOG Enhanced Edition installer alone doesn't
|
`mfdger.res` come from - the GOG Enhanced Edition installer alone doesn't
|
||||||
include them.
|
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`)
|
## 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
|
- **Restart required?**: No (live-appliable) - a rebind takes effect
|
||||||
immediately, and persists across restarts once saved (**Save
|
immediately, and persists across restarts once saved (**Save
|
||||||
Configuration**, same as every other group here).
|
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,
|
- **Not included here**: opening/closing the VR menu itself is a fixed,
|
||||||
non-remappable button (see "Menu button" below) - there's no
|
non-remappable button (see "Menu button" below) - there's no
|
||||||
`menu_toggle` field, and the Controller Options tab never lets you
|
`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
|
- `keypad 0`-`keypad 9`: a controller button per digit is not useful; use the
|
||||||
on-screen keyboard.
|
on-screen keyboard.
|
||||||
- `toggle_freelook` (F): the free-view actions cover mouse look.
|
- `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
|
- `mac_help` (disabled) and `audiolog_cancel` (its build flag is not
|
||||||
defined): they are not real hotkeys in this build.
|
defined): they are not real hotkeys in this build.
|
||||||
- `toggle_opengl` (Ctrl+G): the Video Options tab's Renderer buttons already
|
- `toggle_opengl` (Ctrl+G): the Video Options tab's Renderer buttons already
|
||||||
|
|||||||
@@ -6,6 +6,17 @@ An open source project to play the classic 1994 System Shock on a VR
|
|||||||
headset, built on top of [Shockolate](https://github.com/Interrupt/systemshock),
|
headset, built on top of [Shockolate](https://github.com/Interrupt/systemshock),
|
||||||
a cross-platform port of the original game.
|
a cross-platform port of the original game.
|
||||||
|
|
||||||
|
The game is played on a virtual screen: the engine renders its usual flat 2D
|
||||||
|
frame, and Questshock shows it as a large screen floating in front of you in
|
||||||
|
VR. It is not a stereoscopic 3D conversion; you sit or stand in front of the
|
||||||
|
screen the way you would in front of a monitor or a cinema screen. The
|
||||||
|
screen's width and distance can be set separately for the menus/intro and for
|
||||||
|
gameplay. Both controllers act as laser pointers for the game's mouse-driven
|
||||||
|
interface and for a VR menu and on-screen keyboard, and every controller
|
||||||
|
button, stick direction and game hotkey can be assigned to a game action.
|
||||||
|
Questshock runs entirely on the headset (tested on a Meta Quest 2) as a
|
||||||
|
sideloaded Android app, and needs your own copy of the game data.
|
||||||
|
|
||||||
## 1. Design principles
|
## 1. Design principles
|
||||||
|
|
||||||
- **Standalone-headset-only.** The game must run entirely on the headset
|
- **Standalone-headset-only.** The game must run entirely on the headset
|
||||||
@@ -31,6 +42,10 @@ a cross-platform port of the original game.
|
|||||||
"3. Game assets" below); `res/assets/extract_assets.sh` extracts it into
|
"3. Game assets" below); `res/assets/extract_assets.sh` extracts it into
|
||||||
`ss_ee/`.
|
`ss_ee/`.
|
||||||
- `res/run.sh` - the launcher script, copied into `dist/` on build.
|
- `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:
|
- `Makefile` - targets:
|
||||||
- `all` (default) - alias for `dist`.
|
- `all` (default) - alias for `dist`.
|
||||||
- `build-image` - builds the Docker build-image (`./build-image.sh`).
|
- `build-image` - builds the Docker build-image (`./build-image.sh`).
|
||||||
@@ -105,6 +120,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
|
entirely optionally - it still runs fine with just the Enhanced Edition
|
||||||
installer.
|
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
|
## 4. Desktop build
|
||||||
|
|
||||||
Builds and runs Questshock natively on the desktop (your dev machine, or
|
Builds and runs Questshock natively on the desktop (your dev machine, or
|
||||||
@@ -218,18 +237,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
|
interact with it, same as the game's own Bluetooth mouse/keyboard input
|
||||||
otherwise works unchanged.
|
otherwise works unchanged.
|
||||||
|
|
||||||
The menu launcher (`MenuOverlay.java` - just a "Keyboard" button so far)
|
The menu (`MenuOverlay.java`, described in "5.1.1. Menu, controls and
|
||||||
and the keyboard (`KeyboardOverlay.java`) are two fully independent quads,
|
cheats" below) and the keyboard (`KeyboardOverlay.java`) are two fully
|
||||||
each its own `XrOverlay` instance (`android/app/src/main/cpp/xr_overlay.c`
|
independent quads, each its own `XrOverlay` instance
|
||||||
- a generic "off-screen Android View rendered into an OpenXR quad,
|
(`android/app/src/main/cpp/xr_overlay.c` - a generic "off-screen Android View
|
||||||
hit-tested via a laser pointer" module shared by both, and by whatever
|
rendered into an OpenXR quad, hit-tested via a laser pointer" module shared by
|
||||||
similar panel comes next) with its own swapchain, visibility, and
|
both) with its own swapchain, visibility, and position, so they can be
|
||||||
position, so they can be shown/hidden/moved independently - a design
|
shown/hidden/moved independently - the keyboard can stay open while also
|
||||||
specifically meant to let the keyboard stay open while also picking
|
picking something from the menu. The controller's menu button only ever
|
||||||
something from the (still-to-be-built-out) menu. The controller's menu
|
opens/closes the menu; the menu's "Keyboard" button only opens the keyboard
|
||||||
button only ever opens/closes the launcher; clicking its "Keyboard" button
|
(never touching the menu's own visibility) - the keyboard's own title bar
|
||||||
only opens the keyboard (never touching the launcher's own visibility) -
|
holds the only way to close it again.
|
||||||
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
|
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
|
IME to borrow once immersive) that forwards each key press straight to the
|
||||||
@@ -288,7 +306,50 @@ debugging enabled.)
|
|||||||
`/sdcard/questshock/` also holds an optional persistent settings file,
|
`/sdcard/questshock/` also holds an optional persistent settings file,
|
||||||
`questshock.config.json`, loaded automatically at startup and saved/loaded
|
`questshock.config.json`, loaded automatically at startup and saved/loaded
|
||||||
from the in-headset menu - see [CONFIG.md](CONFIG.md) for the documented
|
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
|
### 5.2. Building natively in Android Studio
|
||||||
|
|
||||||
@@ -533,6 +594,23 @@ each one does:
|
|||||||
`qs_config_load_startup()` in `Shock.c`'s `main()`, right after
|
`qs_config_load_startup()` in `Shock.c`'s `main()`, right after
|
||||||
`LoadPrefs()`, and one check in `sdl_events.c`'s `pump_events()` to drain
|
`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.
|
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/`:
|
`android/gl4es-patches/`:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user