374 lines
20 KiB
Markdown
374 lines
20 KiB
Markdown
# 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`.
|