Pause menu + control settings (hold by_button 2s) #9

Merged
merith-tk merged 2 commits from muse-lantern/da-game:pause-menu into main 2026-10-25 03:12:49 +00:00
Member

Stacked on PR #8 (branch is based on menu-save-v1; it will rebase cleanly once #8 merges — the diff to review is the single commit cfcb789).

Implements the control rulings from the 2026-10-22 design session (logged on game-planning #19 / #28):

Pause

  • Hold the upper face button (by_button, B/Y) for 2 seconds to pause — the hold is deliberate; the press action stays unallocated for future use. The controller menu button and desktop Esc open the same pause (note: they no longer teleport straight to the menu place — Return to Menu inside the pause does that now).
  • Pausing freezes the world (the level subtree's process mode) and anchors the rig in place — no teleport. The pause panel is a second instance of the shared MenuPanel under the persistent Game root, placed in front of the player's view on open.
  • Buttons: Resume / Options / Save / Load / Return to Menu — all functional, no stubs. Save dumps the run in place; Load restores it wholesale. Both are game-mode only, and Save also honours Game.save_blocked — the hook for the traversal rule (no saving in the traversal region); nothing sets it yet.

Settings (the Options page)

  • Movement-stick side (left/right), turn mode (snap/smooth), snap angle, smooth turn speed — editable live, applied to the rig immediately.
  • Persisted by the new GameSettings to user://settings.cfg — their own file, separate from the run save, so settings survive across runs. Stick swap re-homes the movement/turn providers between controllers (they re-resolve their controller on _enter_tree).

Refactor: MenuPanel now finds its menu control by the action_selected signal instead of the MainMenu type, so the level panel and pause panel share one implementation.

Verified: pause_check PASS (pause state machine, world freeze/restore, save gate, settings round-trip, provider re-homing both ways) · save_check / smoke / arms / addon / rig_motion PASS · bridge e2e PASS · real boot clean, no script errors. CI list gains pause_check.

— Lantern

**Stacked on PR #8** (branch is based on `menu-save-v1`; it will rebase cleanly once #8 merges — the diff to review is the single commit `cfcb789`). Implements the control rulings from the 2026-10-22 design session (logged on game-planning #19 / #28): **Pause** - **Hold the upper face button (`by_button`, B/Y) for 2 seconds** to pause — the hold is deliberate; the press action stays unallocated for future use. The controller menu button and desktop Esc open the same pause (note: they no longer teleport straight to the menu place — Return to Menu inside the pause does that now). - Pausing **freezes the world** (the level subtree's process mode) and anchors the rig in place — no teleport. The pause panel is a second instance of the shared MenuPanel under the persistent Game root, placed in front of the player's view on open. - Buttons: **Resume / Options / Save / Load / Return to Menu — all functional**, no stubs. Save dumps the run in place; Load restores it wholesale. Both are game-mode only, and Save also honours `Game.save_blocked` — the hook for the traversal rule (no saving in the traversal region); nothing sets it yet. **Settings (the Options page)** - Movement-stick side (left/right), turn mode (snap/smooth), snap angle, smooth turn speed — editable live, applied to the rig immediately. - Persisted by the new `GameSettings` to **`user://settings.cfg`** — their own file, separate from the run save, so settings survive across runs. Stick swap re-homes the movement/turn providers between controllers (they re-resolve their controller on `_enter_tree`). **Refactor**: `MenuPanel` now finds its menu control by the `action_selected` signal instead of the MainMenu type, so the level panel and pause panel share one implementation. **Verified**: `pause_check` PASS (pause state machine, world freeze/restore, save gate, settings round-trip, provider re-homing both ways) · save_check / smoke / arms / addon / rig_motion PASS · bridge e2e PASS · real boot clean, no script errors. CI list gains `pause_check`. — Lantern
SaveGame gains the working file boundary the design always
planned: save_state/load_state/has_state — a state Dictionary as
human-readable JSON, backup-first (an overwrite copies the old
file to .bak before writing). The #28 full-snapshot skeleton stays
as the documented refinement of what goes inside the dict.

Game wiring:
- New Game writes the run slot (user://saves/run.json)
- Returning to the menu from a game re-dumps, rig still where the
  player stopped — Continue resumes from exactly there
- Continue reads the slot back: level (re)load if it differs, rig
  transform restored wholesale via PlayerRig.place_at_transform
- Continue button lights up whenever a save exists (boot + every
  return to menu refresh it)
- Sandbox never reads or writes the save (throwaway by design)

tests/save_check.gd: unit round-trip + backup contents, boot
no-op Continue, dump/restore pose round-trip through the real
Game flow, sandbox byte-isolation. SAVE CHECK PASS; smoke, arms,
addon suites and the bridge e2e still PASS.
CI: Forgejo Actions test workflow (push + PR, status only)
Some checks failed
tests / test (pull_request) Failing after 14s
1aa6b8be12
Runs everything automatable on the pinned Godot 4.6.3 standard
build, downloaded per run:
- project import
- the headless suites (smoke_menu, arms_check, addon_check,
  save_check; terrain_check joins automatically once it lands —
  branches without it skip it in the open). The suites exit 0
  either way, so the step greps their PASS/FAIL output
- the debug bridge e2e, under Xvfb, which exits non-zero itself

No artifacts are uploaded, nothing is published, no release job —
the workflow only reports test status.

Verified locally: YAML parses; the suite step's script runs all
four suites green and exits 0; a deliberate FAIL probe makes it
exit 1.
Placement negates velocity; strafe on; harness-proof the suites
Some checks failed
tests / test (pull_request) Failing after 19s
fb3998af7d
Zach's bug (found testing main): walk off the sandbox, fall, return
to the menu, New Game — the body's stored fall velocity punched the
player through the floor. Rule (game-planning #28): placement means
at rest, and velocity is never saved. PlayerRig.place_at and
place_at_transform now zero the body's velocity and ground control
via _reset_motion().

Also: MovementDirect's strafe flag was never set (library default
off = stick X discarded = forward/back only). Strafing is the
decided scheme (game-planning #19); strafe = true in the rig scene.

Test fallout, all harness-only (real boots verified clean): in
--script mode a script keeps its first compile, which runs before
autoload identifiers register — the rig's typed children stay Nil
in any suite loading the game scene, and a mid-body script error
once hung save_check by re-running its body on dirty state.
- save_check: completion latch (abort reports FAIL, never loops);
  strafe read from scene state; velocity assertions moved out
- tests/rig_motion_check.gd (new): clean-process unit check that
  loads rig+body scripts after the broken window and exercises the
  real place_at_transform — velocity and ground control zeroed
- CI suite list gains rig_motion_check

Verified: save_check PASS, rig_motion_check PASS, smoke/arms/addon
PASS, bridge e2e PASS.
Pause menu + control settings (hold by_button 2s)
Some checks failed
tests / test (pull_request) Failing after 19s
cfcb78988d
Implements Zach's 2026-10-22 control rulings (game-planning #19):

- HOLD the upper face button (by_button, B/Y) for 2s to pause; the
  press action stays unallocated. Controller menu button and desktop
  Esc open the same pause (they no longer teleport straight to the
  menu place — Return to Menu inside pause does that now).
- Pausing freezes the world (level subtree process mode) and
  anchors the rig in place; the pause panel — a second MenuPanel
  under the persistent Game root — is placed in front of the
  player's view. Buttons: Resume / Options / Save / Load / Return
  to Menu, all functional (no stubs needed).
- Save inside pause writes the run in place; Load restores it
  wholesale; both are GAME-mode only and Save also honours
  Game.save_blocked — the traversal rule that saving is impossible
  there (game-planning #28). The flag is the traversal layer's hook;
  nothing sets it yet.
- Options page = the control settings surface: movement-stick side
  (left/right), turn mode (snap/smooth), snap angle, smooth speed.
  Persisted by GameSettings to user://settings.cfg — their own
  file, separate from the run save. PlayerRig.apply_settings writes
  turn params onto the provider and re-homes the two providers
  between controllers for the stick side (providers re-resolve
  their controller on _enter_tree).
- MenuPanel generalised: its menu control is found by the
  action_selected signal, not by MainMenu type, so main and pause
  panels share the one panel implementation.

Verified: pause_check PASS (pause machine, save gate, settings
round-trip, provider re-homing), save/smoke/arms/addon/rig_motion
PASS, bridge e2e PASS, real boot clean.
CI: install xvfb before the bridge e2e
Some checks failed
tests / test (pull_request) Failing after 17s
ae114a5108
Same fix as on menu-save-v1: the runner image lacks xvfb-run,
which the bridge e2e step needs. Install when missing (sudo only
when not already root).
CI: fetch xvfb from the Debian archive (bullseye is EOL)
Some checks failed
tests / test (pull_request) Failing after 40s
3233031032
Same fix as on menu-save-v1 — archive.debian.org sources for the
xvfb install; see that commit for the run-log evidence.
CI: dump the e2e game log on failure; add mesa DRI drivers
Some checks failed
tests / test (pull_request) Failing after 35s
54dc0cc162
Same change as on menu-save-v1 (see that commit for the run-log
evidence): print /tmp/da_game_e2e.log when the e2e fails, and
install mesa DRI/GLX with xvfb.
CI: install the X11 client libraries Godot's display server needs
Some checks failed
tests / test (pull_request) Failing after 45s
0dafd7ceb0
Run 2990's dumped game log gives the actual root cause of the e2e
startup exit (not GL): 'libXcursor.so.1: cannot open shared object
file' -> the X11 display server fails, the Wayland fallback fails
with it, Godot exits 1 before the bridge ever starts. xvfb does
not pull the X client libraries in. Add libxcursor1 plus the rest
of Godot's usual X11 runtime set (xinerama, xrandr, xi, xkbcommon).
CI: rerun to test the runner in isolation (no code change)
All checks were successful
tests / test (pull_request) Successful in 37s
22b8202855
Pause gesture: both face buttons at once, not a hold (Zach's correction)
All checks were successful
tests / test (pull_request) Successful in 37s
effbf51720
PR #9 as first shipped opened pause by HOLDING the upper face
button (by_button) for 2s. Zach's correction: the pause menu
replaces the legacy gesture — pressing BOTH face buttons at once
— and the hold was never the design.

- Game._pause_gesture(ax_l, by_l, ax_r, by_r): the chord is both
  face buttons together on ONE controller (either hand),
  edge-triggered — fires on the frame the chord completes, never
  re-fires while held, a single face button never fires (each
  button's own press stays unallocated), and one button per hand
  is not the gesture. Extracted as a pure function of the four
  button states so the headless suite can exercise it (live
  trackers report nothing headless).
- _process: the chord toggles pause exactly like the controller
  menu button (opens in game, closes while paused). Menu button
  and desktop Esc behavior unchanged.
- PAUSE_HOLD_SECONDS, the hold accumulator and latch are gone.

Controls validated: ax_button and by_button both exist in
openxr_action_map.tres with per-hand bindings (A/X, B/Y on each
controller); no other rig system consumes either action (the one
library mention is the vendored flight provider's default, and
the rig has no flight provider).

pause_check gains seven gesture assertions; all suites PASS.
Author
Member

Gesture fix (Zach's correction): as first shipped, this PR opened pause by holding the upper face button for 2s. The design is the legacy gesture — press both face buttons at once (ax + by on one controller, either hand) — and the pause menu replaces what that gesture used to call up. Fixed in effbf51:

  • Game._pause_gesture() — pure function of the four button states: per-hand chord, edge-triggered, no re-fire while held, single buttons never fire, one-button-per-hand doesn't count.
  • The chord toggles pause exactly like the menu button; Esc/menu-button behavior unchanged. PAUSE_HOLD_SECONDS and the hold machinery are gone.
  • Controls validated: ax_button/by_button both exist in the action map with per-hand bindings; nothing else in the rig consumes them.
  • pause_check gains seven gesture assertions; all suites PASS locally (CI running).

The plan record is corrected too (game-planning #19). Rest of the PR is as reviewed — Zach: "otherwise it's good."

— Lantern

**Gesture fix (Zach's correction):** as first shipped, this PR opened pause by *holding* the upper face button for 2s. The design is the legacy gesture — **press both face buttons at once** (ax + by on one controller, either hand) — and the pause menu replaces what that gesture used to call up. Fixed in `effbf51`: - `Game._pause_gesture()` — pure function of the four button states: per-hand chord, edge-triggered, no re-fire while held, single buttons never fire, one-button-per-hand doesn't count. - The chord toggles pause exactly like the menu button; Esc/menu-button behavior unchanged. `PAUSE_HOLD_SECONDS` and the hold machinery are gone. - Controls validated: `ax_button`/`by_button` both exist in the action map with per-hand bindings; nothing else in the rig consumes them. - pause_check gains seven gesture assertions; all suites PASS locally (CI running). The plan record is corrected too (game-planning #19). Rest of the PR is as reviewed — Zach: "otherwise it's good." — Lantern
Pause gesture: strict B/Y 2s hold (reverts the chord; it was a misread)
All checks were successful
tests / test (pull_request) Successful in 1m2s
c22d369563
Zach's 20:20 report described a BUG — pressing both face buttons
opened the pause menu — and I misread it as a design change,
shipping the both-buttons chord in effbf51 as if it were the
intent. It was not: the intent, from the original #19 dictation,
is holding the upper face button (by_button: B/Y) for 2 seconds.
This commit reverts the chord and implements the hold STRICTLY,
per Zach's confirmed spec:

- Game._pause_hold(delta, ax_l, by_l, ax_r, by_r): each hand's
  timer accumulates only while that hand's by is held and its ax
  is NOT. An ax press on the same hand at any point zeroes the
  timer, so the both-buttons press — in any order or overlap —
  can never open the menu. Among face-button inputs, this hold is
  the only way in. Fires once at PAUSE_HOLD_SECONDS (2.0); the
  latch holds until by is released on both hands. Pure function
  of button states + time, so the suite drives it headlessly.
- Menu button / Esc still open and close: platform note recorded
  in the section comment — the current target is Quest
  controllers via SteamVR, where the menu/system button is
  reserved and unusable, so the hold is the operative opener
  there; the menu-button path stays for a native Quest build,
  where it is the conventional opener.
- pause_check: the seven chord assertions are replaced by hold
  assertions — fires at exactly 2s, no re-fire while held, both
  buttons held never fires (16 x 0.25s), ax mid-hold resets the
  timer, right-hand B works and the other hand's ax doesn't
  poison it.

All suites PASS; boot clean.
Author
Member

Gesture settled — strict hold (reverts effbf51): the chord commit was my misreading of Zach's bug report (he was describing the both-buttons press opening the menu as a bug, not requesting it). Reverted in c22d369; what ships now:

  • Hold B/Y alone for 2s opens pause — strict: A/X pressed on the same hand at any point during the hold resets that hand's timer, so the both-buttons press can never open it. Only face-button way in.
  • Implemented as Game._pause_hold() — a pure function of the four button states + elapsed time. pause_check now asserts the hold semantics headlessly: fires at exactly 2s, no re-fire while held, both-buttons-held never fires, A/X mid-hold resets, other hand's A/X doesn't poison.
  • Menu button / Esc paths stay, with the platform caveat recorded in code: current target is Quest controllers via SteamVR, where the menu/system button is reserved and unusable — the hold is the operative opener there; the menu-button path is kept for a native Quest build, where it would be the convention.

All suites PASS locally; CI running on this push.

— Lantern

**Gesture settled — strict hold (reverts `effbf51`):** the chord commit was my misreading of Zach's bug report (he was describing the both-buttons press *opening the menu as a bug*, not requesting it). Reverted in `c22d369`; what ships now: - **Hold B/Y alone for 2s** opens pause — strict: A/X pressed on the same hand at any point during the hold resets that hand's timer, so the both-buttons press can never open it. Only face-button way in. - Implemented as `Game._pause_hold()` — a pure function of the four button states + elapsed time. pause_check now asserts the hold semantics headlessly: fires at exactly 2s, no re-fire while held, both-buttons-held never fires, A/X mid-hold resets, other hand's A/X doesn't poison. - Menu button / Esc paths stay, with the platform caveat recorded in code: current target is Quest controllers via **SteamVR**, where the menu/system button is reserved and unusable — the hold is the operative opener there; the menu-button path is kept for a native Quest build, where it would be the convention. All suites PASS locally; CI running on this push. — Lantern
muse-lantern force-pushed pause-menu from c22d369563
All checks were successful
tests / test (pull_request) Successful in 1m2s
to 7d9f0b6660
All checks were successful
tests / test (pull_request) Successful in 48s
2026-10-23 18:57:00 +00:00
Compare
Author
Member

Branch folded + input code removed (2026-10-23, Zach's direction).

History rewritten into three chunks (tree-verified identical apart from the deltas below):

  • 0bcc2f2 — CI: the runner fixes squashed into one commit
  • e037f0e — Pause menu + control settings (this PR's actual feature)
  • 7d9f0b6 — docs: input system design; prep for the input rebuild

Removed by design: the in-game pause-gesture code (the strict B/Y hold timer, and the earlier chord). Gesture disambiguation — press vs hold vs chord — belongs to the runtime binding layer, not game code: SteamVR bindings do it natively (e.g. Vivecraft: Y press = backpack, Y hold = keyboard). The pause menu's remaining triggers (menu button, desktop Esc) are stopgaps that call open_pause() directly until the input system lands.

The design is documented in docs/input-system.md on this branch: src/input/ layout (generic contract + per-controller folders + per-runtime presets for steamvr / oculus / openxr), semantic actions only for game code, pause produced per runtime (steamvr: by_button hold 2s; oculus/openxr: menu key direct), Quest capacitive touch (incl. the thumbrest) as built-in backend events, addon-supplied controllers, and the deferred cosmetic layer (the loading-circle plan is preserved there, unbuilt — SteamVR has its own hold cosmetics).

PRs #10 and #11 were restacked onto the folded history (feature commits cherry-picked; their CI replay commits dropped as now-redundant). Their trees differ from before only by this fold's deltas.

**Branch folded + input code removed (2026-10-23, Zach's direction).** History rewritten into three chunks (tree-verified identical apart from the deltas below): - `0bcc2f2` — CI: the runner fixes squashed into one commit - `e037f0e` — Pause menu + control settings (this PR's actual feature) - `7d9f0b6` — docs: input system design; prep for the input rebuild **Removed by design:** the in-game pause-gesture code (the strict B/Y hold timer, and the earlier chord). Gesture disambiguation — press vs hold vs chord — belongs to the **runtime binding layer**, not game code: SteamVR bindings do it natively (e.g. Vivecraft: Y press = backpack, Y hold = keyboard). The pause menu's remaining triggers (menu button, desktop Esc) are stopgaps that call `open_pause()` directly until the input system lands. **The design** is documented in `docs/input-system.md` on this branch: `src/input/` layout (generic contract + per-controller folders + per-runtime presets for **steamvr / oculus / openxr**), semantic actions only for game code, `pause` produced per runtime (steamvr: by_button hold 2s; oculus/openxr: menu key direct), Quest capacitive touch (incl. the thumbrest) as built-in backend events, addon-supplied controllers, and the deferred cosmetic layer (the loading-circle plan is preserved there, unbuilt — SteamVR has its own hold cosmetics). PRs #10 and #11 were restacked onto the folded history (feature commits cherry-picked; their CI replay commits dropped as now-redundant). Their trees differ from before only by this fold's deltas.
docs/input: menu key + Esc pause on non-SteamVR is settled behavior
All checks were successful
tests / test (pull_request) Successful in 44s
dc6189c1c8
Zach's ruling (2026-10-23): on non-SteamVR setups the menu key
and desktop Esc opening the pause menu is permanent, intended
behavior — the input system will express it as the oculus/openxr
preset binding rather than a direct call. No runtime-gating in
game code: under SteamVR the menu press is reserved and never
reaches the app, so the scoping lives in the bindings.
Input system v1: generic contract, RigInput, Quest Touch backend
All checks were successful
tests / test (pull_request) Successful in 1m14s
0bb094f2f2
The ground-up input system from docs/input-system.md, introduced
together with its first consumer, the pause menu:

- generic/InputContract: the positional/semantic vocabulary
  (face_lower/face_upper/menu/trigger/grip/stick + the capacitive
  touch ids) that backends produce and game code consumes.
- generic/ControllerBackend: base class with the state feed
  (update_from_states) and touch edges as signals; synthetic
  states make backends headless-testable end to end.
- generic/RigInput: a node on the rig. Discovers controller
  descriptors data-driven (LevelRegistry pattern), owns the
  active backend, and turns the contract menu state's rising
  edge into the semantic pause_pressed signal; in flat mode it
  produces pause from desktop Esc instead.
- controllers/quest_touch/: the backend (reads the action map's
  existing actions; the FULL capacitive surface is wired as
  touch_changed signals from day one, thumbrest included), the
  descriptor with capabilities, and the per-runtime binding
  presets — steamvr pause = by_button hold 2.0s (a native
  binding-layer production onto menu_button), oculus/openxr
  pause = menu key direct.

Game now subscribes to pause_pressed and toggles; its remaining
button polling and _unhandled_input are gone. Verified by
tests/input_check.gd (28 assertions, incl. the end-to-end toggle
through the real game scene); added to the CI suite list.
Author
Member

Input system v1 built into this PR (0bb094f, 2026-10-23) — per Zach's call, the input system and the pause menu ship together: the pause menu is the input system's first consumer, so there is no separate branch for it.

What landed, following docs/input-system.md (status there is now "built v1"):

  • src/input/generic/input_contract.gd — the positional/semantic vocabulary (face_lower/face_upper/menu/…, capacitive touch ids included). Backends produce it; game code consumes it.
  • src/input/generic/controller_backend.gd — backend base class: per-hand state feed via update_from_states(), touch edges as signals. Synthetic states make backends headless-testable.
  • src/input/generic/rig_input.gd — a node on the rig. Discovers controller descriptors data-driven (the LevelRegistry pattern), owns the active backend, and turns the contract menu state's rising edge into the semantic pause_pressed signal; in flat mode it produces pause from desktop Esc.
  • src/input/controllers/quest_touch/ — the backend (reads the action map's existing actions; the full capacitive surface is wired as touch_changed signals from day one, thumbrest/secondary_touch included, with no consumers yet, by design), the descriptor (capabilities: capacitive_touch, thumbrest, analog_trigger, analog_grip), and the per-runtime presets: steamvr pause = by_button hold 2.0s (native binding-layer production onto menu_button), oculus/openxr pause = menu key direct.

Game changes: subscribes to pause_pressed and toggles. The last of the stopgap input code (_process menu polling, _unhandled_input Esc) is deleted — Game now contains no button polling and no gesture code at all.

Verified: new tests/input_check.gd (discovery, presets, synthetic state feed, touch edges, semantic edges, end-to-end pause toggle through the real game scene — added to the CI suite list); all suites pass on the restacked stack tip and the real boot is clean.

PRs #10/#11 restacked onto this (their restack surfaced and fixed a real latent issue: the rig scene had two scripts claiming ext_resource id 7 — RigInput and the avatar skeleton; the avatar is now id 8).

**Input system v1 built into this PR** (`0bb094f`, 2026-10-23) — per Zach's call, the input system and the pause menu ship together: the pause menu is the input system's first consumer, so there is no separate branch for it. What landed, following `docs/input-system.md` (status there is now "built v1"): - `src/input/generic/input_contract.gd` — the positional/semantic vocabulary (`face_lower`/`face_upper`/`menu`/…, capacitive touch ids included). Backends produce it; game code consumes it. - `src/input/generic/controller_backend.gd` — backend base class: per-hand state feed via `update_from_states()`, touch edges as signals. Synthetic states make backends headless-testable. - `src/input/generic/rig_input.gd` — a node on the rig. Discovers controller descriptors data-driven (the LevelRegistry pattern), owns the active backend, and turns the contract menu state's rising edge into the semantic `pause_pressed` signal; in flat mode it produces pause from desktop Esc. - `src/input/controllers/quest_touch/` — the backend (reads the action map's existing actions; the **full capacitive surface is wired as `touch_changed` signals from day one, thumbrest/`secondary_touch` included**, with no consumers yet, by design), the descriptor (`capabilities`: capacitive_touch, thumbrest, analog_trigger, analog_grip), and the per-runtime presets: **steamvr** pause = `by_button` hold 2.0s (native binding-layer production onto `menu_button`), **oculus**/**openxr** pause = menu key direct. Game changes: subscribes to `pause_pressed` and toggles. The last of the stopgap input code (`_process` menu polling, `_unhandled_input` Esc) is deleted — Game now contains no button polling and no gesture code at all. Verified: new `tests/input_check.gd` (discovery, presets, synthetic state feed, touch edges, semantic edges, end-to-end pause toggle through the real game scene — added to the CI suite list); all suites pass on the restacked stack tip and the real boot is clean. PRs #10/#11 restacked onto this (their restack surfaced and fixed a real latent issue: the rig scene had two scripts claiming ext_resource id 7 — RigInput and the avatar skeleton; the avatar is now id 8).
Input: tab-indent the generated JSONs; doc the identity invariant + remappability
All checks were successful
tests / test (pull_request) Successful in 53s
7d3489e8fa
Formatting rule (Zach): JSON files we generate use TAB
indentation. Content is unchanged (input_check parses them
semantically).

docs/input-system.md gains two settled points from the ABXY
deep-dive: (1) contract identity is (hand, id) — a shared
positional name never merges physical buttons; A/B/X/Y stay four
distinct inputs because OpenXR state is queried per subaction
path and each controller node reads only its own hand;
(2) bindings are owned by each runtime's setup, 100% where
possible — the presets are suggested defaults, and game code
reads action/contract state only, so user remapping is honored
by construction.
Input: generic da_* vocabulary — names are the mapping (dead-air/da-game#12)
All checks were successful
tests / test (pull_request) Successful in 37s
2230310d55
The action map's inherited template names (ax_button, by_button,
primary, ...) are gone. Input actions now carry the generic
natural-language names the contract speaks — da_button_primary
(A/X anchor), da_button_secondary (B/Y), da_trigger_primary(+_analog),
da_grip_primary(+_analog/_force), da_menu_action, da_stick(+_click),
da_pad(+_click), da_touch_* — so runtime binding UIs (SteamVR's
above all) present users the real vocabulary, and a controller we
ship no config for is still fully mappable by its owner.

- openxr_action_map.tres: 18 action resource_names renamed;
  bindings untouched (they reference sub-resources, not names).
  Kept: pose actions (tracking critical path — renaming is a
  separate headset-verified change) and haptic (vendored
  xr-tools references it by const).
- InputContract: ids ARE the da_* names; identity stays (hand, id).
- Quest translator: explicit da_* -> contract tables (the
  authored translation an addon translator copies); reads the
  pad surface too.
- Rig scene: provider overrides point MovementDirect/Turn at
  da_stick and both FunctionPointers at da_trigger_primary
  (vendored defaults name the old actions; exports override).
- ArmsView reads the renamed analog actions. Presets speak the
  new vocabulary (steamvr: hold of button_secondary onto
  da_menu_action). docs/input-system.md + tests updated.
  Suite sweep: all PASS; real boot clean.
Author
Member

The generic-vocabulary rename landed on top of this PR (2230310), per the design tracked in #12:

  • The action map speaks the generic vocabulary now. 18 input actions renamed to natural-language da_* names — da_button_primary (A/X anchor), da_button_secondary (B/Y), da_trigger_primary/da_trigger_primary_analog, da_grip_primary/da_grip_primary_analog/da_grip_primary_force, da_menu_action, da_stick/da_stick_click, da_pad/da_pad_click, da_touch_* — bindings untouched (they reference sub-resources). So SteamVR's binding UI now presents these names: a controller with no shipped config is fully mappable by its owner, because the name says what the mapping is.
  • Two deliberate exceptions: the pose actions keep their conventional names (tracking critical path — renaming them is a separate, headset-verified change), and haptic keeps its name (vendored xr-tools references it by const).
  • Contract = the same names; the Quest translator's mapping tables are the authored translation statement (the pattern addon translators copy); the rig's providers are re-pointed by instance overrides (da_stick, da_trigger_primary) since the vendored defaults name the old actions. Presets updated (steamvr: hold of button_secondary onto da_menu_action); docs/input-system.md carries the refined model + the two addon authoring paths.
  • Restacked: full-skeleton c53df2a, traversal-v1 750c606. Verification: pause-menu 7/7 suites, skeleton 8/8, traversal tip 9/9, real boots clean on each. (One log line chased down along the way: a bool constructor error in input_check's harness cascade — A/B-verified via stash to pre-date this change; the real boot is clean.)
The generic-vocabulary rename landed on top of this PR (`2230310`), per the design tracked in #12: - **The action map speaks the generic vocabulary now.** 18 input actions renamed to natural-language `da_*` names — `da_button_primary` (A/X anchor), `da_button_secondary` (B/Y), `da_trigger_primary`/`da_trigger_primary_analog`, `da_grip_primary`/`da_grip_primary_analog`/`da_grip_primary_force`, `da_menu_action`, `da_stick`/`da_stick_click`, `da_pad`/`da_pad_click`, `da_touch_*` — bindings untouched (they reference sub-resources). So SteamVR's binding UI now presents these names: a controller with no shipped config is fully mappable by its owner, because the name says what the mapping is. - **Two deliberate exceptions:** the pose actions keep their conventional names (tracking critical path — renaming them is a separate, headset-verified change), and `haptic` keeps its name (vendored xr-tools references it by const). - **Contract = the same names**; the Quest translator's mapping tables are the authored translation statement (the pattern addon translators copy); the rig's providers are re-pointed by instance overrides (`da_stick`, `da_trigger_primary`) since the vendored defaults name the old actions. Presets updated (steamvr: hold of `button_secondary` onto `da_menu_action`); `docs/input-system.md` carries the refined model + the two addon authoring paths. - Restacked: full-skeleton `c53df2a`, traversal-v1 `750c606`. Verification: pause-menu 7/7 suites, skeleton 8/8, traversal tip 9/9, real boots clean on each. (One log line chased down along the way: a `bool` constructor error in input_check's harness cascade — A/B-verified via stash to pre-date this change; the real boot is clean.)
Input: pin SteamVR delivery shapes in input_check; document the binding reality
All checks were successful
tests / test (pull_request) Successful in 38s
175dba4250
Hardware finding (2026-10-23, Quest via SteamVR): SteamVR
synthesizes the binding for OpenXR apps from our suggested
bindings — grouping one source block per action instead of
merging a button's click/touch, and parking da_menu_action on
the X click (our map binds it only to menu/home/system paths).
With those defaults nothing opens pause; the divergence is in
the binding, not the game.

The game-side contract is delivery-shape-agnostic, and now the
suite proves the shapes SteamVR actually produces:
- menu state held true for many frames (the Held input method)
  toggles pause exactly once, never per frame;
- menu co-fired with another action on the same click (the
  auto-rebind park job) fires once, and the rest of the state
  flows through untouched.

docs/input-system.md gains a 'SteamVR bindings in practice'
section: what the synthesis does, the two-minute manual
configuration the preset prescribes (consolidate face-button
sources; Long/Held da_menu_action on button_secondary, 2.0s),
and the export/import paths — the first verified export from
real hardware becomes the repo's tester reference.
Author
Member

Follow-up from the first real-hardware look (Quest via SteamVR, Zach testing): the da_* names show up in SteamVR's binding UI exactly as designed. The default binding SteamVR synthesizes from our suggested bindings is another story — one source block per action instead of merged button sources, and da_menu_action parked on the X click (our map binds it only to menu/home/system paths; the left menu source appears to be claimed by SteamVR's dashboard in that profile). With those defaults, nothing in the documented scheme opens pause — the divergence is in the binding layer, not the game.

Code side (175dba4, restacked through #10 e6cf141 / #11 aad321f): no game-logic change was needed — RigInput's per-hand rising-edge detection is already delivery-shape-agnostic. What landed is the proof, pinned in tests/input_check.gd: a menu state held true across frames (SteamVR's Held method) toggles exactly once; menu co-fired with another action on the same click fires once and the rest of the state flows through. Plus docs/input-system.md now has a "SteamVR bindings in practice" section with the manual configuration the preset prescribes (consolidate the face-button sources; Long/Held da_menu_action on button_secondary at 2.0s) and the export/import paths — the first verified export from hardware becomes the repo's tester reference binding.

Local verification: full sweeps green at every stack level (traversal_check flaked once under back-to-back load — its streaming assertion is timing-sensitive — then passed 3/3 solo).

Follow-up from the first real-hardware look (Quest via SteamVR, Zach testing): the `da_*` names show up in SteamVR's binding UI exactly as designed. The *default binding* SteamVR synthesizes from our suggested bindings is another story — one source block per action instead of merged button sources, and `da_menu_action` parked on the X click (our map binds it only to menu/home/system paths; the left menu source appears to be claimed by SteamVR's dashboard in that profile). With those defaults, nothing in the documented scheme opens pause — the divergence is in the binding layer, not the game. Code side (`175dba4`, restacked through #10 `e6cf141` / #11 `aad321f`): no game-logic change was needed — RigInput's per-hand rising-edge detection is already delivery-shape-agnostic. What landed is the proof, pinned in `tests/input_check.gd`: a menu state held true across frames (SteamVR's Held method) toggles exactly once; menu co-fired with another action on the same click fires once and the rest of the state flows through. Plus `docs/input-system.md` now has a "SteamVR bindings in practice" section with the manual configuration the preset prescribes (consolidate the face-button sources; Long/Held `da_menu_action` on `button_secondary` at 2.0s) and the export/import paths — the first verified export from hardware becomes the repo's tester reference binding. Local verification: full sweeps green at every stack level (traversal_check flaked once under back-to-back load — its streaming assertion is timing-sensitive — then passed 3/3 solo).
Input: vocabulary v2 — [control][_channel] names, extras pool, da_menu (dead-air/da-game#12)
All checks were successful
tests / test (pull_request) Successful in 1m13s
344ccaa401
The naming review's outcome, replacing v1's touch-fronted
names before anything (bindings, exports) depends on them:

- Shape is [control][_channel]: a control's channels suffix
  onto it — da_button_primary_touch, da_trigger_primary_touch,
  da_stick_touch (was da_touch_*). Buttons pair with their
  alternatives; analog already read this way.
- Three groups. GENERICS unchanged in substance (bare-minimum
  set). EXTRA: da_extra_0..da_extra_10, eleven blank booleans
  with no base-game meaning — state for mods, targets for
  SteamVR chords and spare hardware; extra_0 is the odd-surface
  slot by convention and inherits the thumbrest/trackpad-touch
  bindings (was da_touch_rest). SYSTEM: da_menu (was
  da_menu_action — functions are not buttons).
- da_select deleted outright (template leftover, never
  allocated). da_pad/da_pad_click folded: trackpad paths in the
  map now bind the stick actions — one 2D position control in
  the generics.
- Map surgery verified by parse: exactly the 30 intended
  actions, all references resolve, no duplicate (action, path)
  per profile; engine loads the map and enumerates the set.
  Contract reorganized into the three groups; the Quest
  translator passes extras through as state; input_check gains
  extra_0 state-flow assertions (state only, no touch edges)
  and keeps its touch coverage on da_stick_touch. Suite sweep
  green; boot clean.
Author
Member

Vocabulary v2 landed (344ccaa, restacked: #10 6b08864, #11 8df7395 + a test de-flake daddfcb): the [control][_channel] naming, three groups (generics / da_extra_0..10 / system da_menu), da_select deleted, pad folded into the stick actions. Details on #12. Map surgery verified by parse (exactly the 30 intended actions; engine enumerates them) and full sweeps are green at every level.

The de-flake, for the record: traversal_check's "exit closed while destination streams" assertion sampled a transient state after a fixed 3-frame wait; the tiny destination level's threaded load legitimately completes within a frame or two on a warm cache, so the check raced correct behavior (~50% failures). It now asserts the invariant that always holds — the gate starts closed — synchronously at begin_traversal. 6/6 solo runs pass.

Vocabulary v2 landed (`344ccaa`, restacked: #10 `6b08864`, #11 `8df7395` + a test de-flake `daddfcb`): the [control][_channel] naming, three groups (generics / `da_extra_0..10` / system `da_menu`), `da_select` deleted, pad folded into the stick actions. Details on #12. Map surgery verified by parse (exactly the 30 intended actions; engine enumerates them) and full sweeps are green at every level. The de-flake, for the record: `traversal_check`'s "exit closed while destination streams" assertion sampled a transient state after a fixed 3-frame wait; the tiny destination level's threaded load legitimately completes within a frame or two on a warm cache, so the check raced correct behavior (~50% failures). It now asserts the invariant that always holds — the gate starts closed — synchronously at `begin_traversal`. 6/6 solo runs pass.
Input: fix two comments left stale by the SteamVR hold flip (dead-air/da-game#12)
All checks were successful
tests / test (pull_request) Successful in 38s
7634346138
The flip moved SteamVR pause production from the binding layer to
RigInput's game-code hold timer, but input_check's sustained-state
comment still credited SteamVR's Held input method, and
input_contract's semantic-actions comment still claimed production
is never game code. Both now state the documented exception.
Comment-only; no behavior change.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
dead-air/engine!9
No description provided.