Input: generic action vocabulary (da_*) + per-controller translators; addon controller support #12

Open
opened 2026-10-23 20:13:51 +00:00 by muse-lantern · 3 comments
Member

Tracking issue for the input-system design refinement settled 2026-10-23 (design of record: da-game docs/input-system.md; implementation lands in PR #9). Filed for the record — the work is already scoped and underway, so this can be closed as soon as it's served its tracking purpose.

The model

Three vocabularies, two translations, and the game speaks only one of them:

  1. Runtime vocabulary — what a runtime/controller actually delivers (OpenXR paths and action states, a tracker feed, ...). Nothing outside a translator sees this.
  2. Translators (controller.gd, per controller type) — authored artifacts that declare "this button is this generic action": they consume runtime events and emit generic actions. We ship ours; addon mods ship theirs — this is how non-standard controllers get supported, by mapping their information to the generic input. Two authoring paths cover different gaps: a controller that is a SteamVR/OpenXR citizen but has no shipped config needs no addon (the user maps our generic action names in the runtime's binding UI — the Index case); a source that is not a runtime citizen at all (DIY trackers, hand-tracking cameras, wheels) needs an addon translator, which may stand up its own XRControllerTracker and feed poses + inputs.
  3. Generic actions — named in natural language for what the mapping IS, da_-namespaced, addressed per hand as controller/<hand>/<name>:
    da_button_primary (anchored: the A/X position), da_button_secondary (B/Y), da_trigger_primary, da_trigger_primary_analog, da_grip_primary, da_grip_primary_analog, da_menu_action, da_stick, da_stick_click, plus da_touch_* for the capacitive surface.

Rules that come with it

  • Identity is (hand, action): the four face buttons stay four distinct inputs (OpenXR subaction paths); a shared positional name never merges signals.
  • Bindings are owned by each runtime's setup, 100% where possible. Our presets are suggested defaults only; game code reads action/contract state and never names a physical control, so user remapping (incl. SteamVR press/hold/chords) is honored by construction.
  • The generic vocabulary grows by addition only. Once shipped, a da_* name is forever — runtime bindings are keyed by action name, so renaming orphans users' saved bindings.
  • Translators must express their device in generic actions + declared capabilities; anything inexpressible stays on the device's own surface (cf. Quest capacitive touch) instead of leaking into the shared vocabulary.

Rename pass (the work this tracks)

The inherited OpenXR action names (ax_button, by_button, primary, ...) are template vocabulary, not ours. The pass renames the action map's input actions to the generic names, re-points InputContract / the Quest translator / presets / rig provider configs at them, and re-verifies (input_check + full suite sweep + boot).

Scoping calls (Lantern's, correct here if wrong):

  • Pose actions keep their current names this pass (aim_pose, grip_pose, ...). Poses are the tracking critical path and Godot derives tracker pose names from them; renaming is a separate, headset-verified change.
  • da_button_primary is anchored to the A/X (lower) face button, da_button_secondary to B/Y (upper), per the Quest layout the generics are designed against.
Tracking issue for the input-system design refinement settled 2026-10-23 (design of record: da-game `docs/input-system.md`; implementation lands in PR #9). Filed for the record — the work is already scoped and underway, so this can be closed as soon as it's served its tracking purpose. ## The model Three vocabularies, two translations, and the game speaks only one of them: 1. **Runtime vocabulary** — what a runtime/controller actually delivers (OpenXR paths and action states, a tracker feed, ...). Nothing outside a translator sees this. 2. **Translators (`controller.gd`, per controller type)** — authored artifacts that declare "this button is this generic action": they consume runtime events and emit generic actions. We ship ours; **addon mods ship theirs** — this is how non-standard controllers get supported, by mapping their information to the generic input. Two authoring paths cover different gaps: a controller that is a SteamVR/OpenXR citizen but has no shipped config needs no addon (the user maps our generic action names in the runtime's binding UI — the Index case); a source that is not a runtime citizen at all (DIY trackers, hand-tracking cameras, wheels) needs an addon translator, which may stand up its own `XRControllerTracker` and feed poses + inputs. 3. **Generic actions** — named in natural language for what the mapping IS, `da_`-namespaced, addressed per hand as `controller/<hand>/<name>`: `da_button_primary` (anchored: the A/X position), `da_button_secondary` (B/Y), `da_trigger_primary`, `da_trigger_primary_analog`, `da_grip_primary`, `da_grip_primary_analog`, `da_menu_action`, `da_stick`, `da_stick_click`, plus `da_touch_*` for the capacitive surface. ## Rules that come with it - Identity is (hand, action): the four face buttons stay four distinct inputs (OpenXR subaction paths); a shared positional name never merges signals. - Bindings are owned by each runtime's setup, 100% where possible. Our presets are suggested defaults only; game code reads action/contract state and never names a physical control, so user remapping (incl. SteamVR press/hold/chords) is honored by construction. - The generic vocabulary grows by addition only. Once shipped, a `da_*` name is forever — runtime bindings are keyed by action name, so renaming orphans users' saved bindings. - Translators must express their device in generic actions + declared capabilities; anything inexpressible stays on the device's own surface (cf. Quest capacitive touch) instead of leaking into the shared vocabulary. ## Rename pass (the work this tracks) The inherited OpenXR action names (`ax_button`, `by_button`, `primary`, ...) are template vocabulary, not ours. The pass renames the action map's input actions to the generic names, re-points InputContract / the Quest translator / presets / rig provider configs at them, and re-verifies (input_check + full suite sweep + boot). **Scoping calls (Lantern's, correct here if wrong):** - **Pose actions keep their current names this pass** (`aim_pose`, `grip_pose`, ...). Poses are the tracking critical path and Godot derives tracker pose names from them; renaming is a separate, headset-verified change. - `da_button_primary` is anchored to the A/X (lower) face button, `da_button_secondary` to B/Y (upper), per the Quest layout the generics are designed against.
Author
Member

Implemented in PR #9 (2230310), restacked through #10 (c53df2a) and #11 (750c606): the action map, InputContract, Quest translator, presets, rig provider configs, ArmsView's analog reads, tests/input_check.gd, and docs/input-system.md all speak the da_* vocabulary now. Suite sweeps green at every stack level (7/7, 8/8, 9/9) with clean real boots.

Ready to close whenever — leaving the close to Zach per org policy.

Implemented in PR #9 (`2230310`), restacked through #10 (`c53df2a`) and #11 (`750c606`): the action map, InputContract, Quest translator, presets, rig provider configs, ArmsView's analog reads, `tests/input_check.gd`, and `docs/input-system.md` all speak the `da_*` vocabulary now. Suite sweeps green at every stack level (7/7, 8/8, 9/9) with clean real boots. Ready to close whenever — leaving the close to Zach per org policy.
Author
Member

Vocabulary v2 (2026-10-23 review, supersedes the naming in this issue's original text where they differ; implemented in PR #9 344ccaa):

  • Shape is [control][_channel]. A control's channels suffix onto it: da_button_primary_touch, da_trigger_primary_touch, da_stick_touch (replacing the da_touch_* front-loaded names). No suffix = the control's primary channel.
  • Three groups. Generics: the bare-minimum set as before. 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). da_extra_0 is the odd-surface slot by convention and inherits the thumbrest/trackpad-touch bindings formerly on da_touch_rest. Richer extra channels, if ever needed, are a mod concern grown by addition — explicitly out of base-game scope. System: da_menu (was da_menu_action — functions are not buttons).
  • da_select deleted (template leftover, never allocated). da_pad/da_pad_click folded: trackpad paths now bind the stick actions — one 2D position control in the generics.
**Vocabulary v2** (2026-10-23 review, supersedes the naming in this issue's original text where they differ; implemented in PR #9 `344ccaa`): - **Shape is [control][_channel].** A control's channels suffix onto it: `da_button_primary_touch`, `da_trigger_primary_touch`, `da_stick_touch` (replacing the `da_touch_*` front-loaded names). No suffix = the control's primary channel. - **Three groups.** *Generics*: the bare-minimum set as before. *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). `da_extra_0` is the odd-surface slot by convention and inherits the thumbrest/trackpad-touch bindings formerly on `da_touch_rest`. Richer extra channels, if ever needed, are a mod concern grown by addition — explicitly out of base-game scope. *System*: `da_menu` (was `da_menu_action` — functions are not buttons). - **`da_select` deleted** (template leftover, never allocated). **`da_pad`/`da_pad_click` folded**: trackpad paths now bind the stick actions — one 2D position control in the generics.

Two gaps between this issue's addon-controller goal and what RigInput does today, plus one doc fix.

1. The active translator is whichever folder is listed first, not the connected hardware.

RigInput._ready() (src/input/generic/rig_input.gd:76-80) runs discover_controllers() and takes controllers[0]. The discovery loop (rig_input.gd:59-71) walks DirAccess.get_next() and never sorts, so the order is whatever the filesystem or pack returns.

With only quest_touch/ that's fine, but once an addon ships a second translator folder, which one wins is effectively arbitrary. The choice never looks at the controllers actually connected, and nothing re-selects when the user switches controllers mid-session.

Suggested shape:

  • Each controller.json declares the OpenXR interaction profiles it handles, for example "profiles": ["/interaction_profiles/oculus/touch_controller"].
  • RigInput picks by the tracker's current profile: XRServer.get_tracker("left_hand").profile, or the XRController3D's tracker.
  • It re-picks on profile_changed, which both the tracker and XRController3D emit.
  • If nothing matches, it falls back to a generic translator.
  • Sort the discovered folders, so ties are deterministic.

2. The SteamVR pause hold claims left da_button_primary.

On SteamVR, pause is hard-wired to holding left da_button_primary (X) for 2.0 s (rig_input.gd:115-137). That's documented as a deliberate exception.

The catch is that da_button_primary is also one of the generics, the inputs base game content is meant to rely on. So the first feature that reads a left-X hold will collide with pause, and with hold_to_pause off, any feature that reads a left-X press will pause the game on every press.

Worth reserving now, before gameplay claims X. Either:

  • move the SteamVR pause hold onto a dedicated action that the preset binds to X; or
  • document that left da_button_primary is reserved on SteamVR.

For the first option, the existing System action da_menu fits: SteamVR's synthesized Quest binding already put it on X click (docs/input-system.md:153-158), and RigInput ignores it on SteamVR today (rig_input.gd:138-141). The extras (da_extra_0 to da_extra_10) are defined as having no base-game meaning (src/input/generic/input_contract.gd:58-66), so they're not a good home for pause.

3. Doc drift.

The status paragraph at the top of docs/input-system.md (lines 3-8) still says the in-game hold timer was "removed when this design was adopted". Principle 1 (lines 12-21) says gesture disambiguation belongs to the binding layer, "not game code", and the lead-in to the pause table (lines 133-134) calls all three mechanisms "binding data". The v1 implementation notes (lines 249-251) say RigInput makes pause from the MENU rising edge, which is only true off SteamVR.

All of these are contradicted later in the same document (line 138, lines 254-256 and 261-270) and by RigInput, which runs a game-code hold timer on left X. The header and principle 1 should point at the exception instead of denying it.

Two gaps between this issue's addon-controller goal and what `RigInput` does today, plus one doc fix. **1. The active translator is whichever folder is listed first, not the connected hardware.** `RigInput._ready()` (`src/input/generic/rig_input.gd:76-80`) runs `discover_controllers()` and takes `controllers[0]`. The discovery loop (`rig_input.gd:59-71`) walks `DirAccess.get_next()` and never sorts, so the order is whatever the filesystem or pack returns. With only `quest_touch/` that's fine, but once an addon ships a second translator folder, which one wins is effectively arbitrary. The choice never looks at the controllers actually connected, and nothing re-selects when the user switches controllers mid-session. Suggested shape: - Each `controller.json` declares the OpenXR interaction profiles it handles, for example `"profiles": ["/interaction_profiles/oculus/touch_controller"]`. - `RigInput` picks by the tracker's current profile: `XRServer.get_tracker("left_hand").profile`, or the `XRController3D`'s tracker. - It re-picks on `profile_changed`, which both the tracker and `XRController3D` emit. - If nothing matches, it falls back to a generic translator. - Sort the discovered folders, so ties are deterministic. **2. The SteamVR pause hold claims left `da_button_primary`.** On SteamVR, pause is hard-wired to holding left `da_button_primary` (X) for 2.0 s (`rig_input.gd:115-137`). That's documented as a deliberate exception. The catch is that `da_button_primary` is also one of the generics, the inputs base game content is meant to rely on. So the first feature that reads a left-X hold will collide with pause, and with `hold_to_pause` off, any feature that reads a left-X press will pause the game on every press. Worth reserving now, before gameplay claims X. Either: - move the SteamVR pause hold onto a dedicated action that the preset binds to X; or - document that left `da_button_primary` is reserved on SteamVR. For the first option, the existing System action `da_menu` fits: SteamVR's synthesized Quest binding already put it on X click (`docs/input-system.md:153-158`), and `RigInput` ignores it on SteamVR today (`rig_input.gd:138-141`). The extras (`da_extra_0` to `da_extra_10`) are defined as having no base-game meaning (`src/input/generic/input_contract.gd:58-66`), so they're not a good home for pause. **3. Doc drift.** The status paragraph at the top of `docs/input-system.md` (lines 3-8) still says the in-game hold timer was "removed when this design was adopted". Principle 1 (lines 12-21) says gesture disambiguation belongs to the binding layer, "not game code", and the lead-in to the pause table (lines 133-134) calls all three mechanisms "binding data". The v1 implementation notes (lines 249-251) say `RigInput` makes pause from the MENU rising edge, which is only true off SteamVR. All of these are contradicted later in the same document (line 138, lines 254-256 and 261-270) and by `RigInput`, which runs a game-code hold timer on left X. The header and principle 1 should point at the exception instead of denying it.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
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#12
No description provided.