Files
riojoy/docs/INPUT-INTEGRATION.md
T
CydandClaude Fable 5 38cf37c5e6 pod: self-contained bundles - drivers install via the game postinstall
Per Cyd: no operator ever installs anything on a pod by hand. build-pod.ps1
now mirrors the universal inner layout (app/app-xp, vendor, install-core.bat
+ install-rio.ps1 reused verbatim) and bundles the flavor prerequisites
(ViGEmBus / XP .NET+KB+RioGamepadXP). New install-riojoy.bat entry point is
called from the game postinstall.bat: self-elevating, idempotent, add-only.
Deliberately no uninstall step - other podized games may share the drivers,
so they are abandoned in place. Also: deploy scripts must stay pure ASCII
(PS5.1 reads BOM-less UTF-8 as ANSI; an em dash decodes to a smart quote
that terminates strings).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 22:15:23 -05:00

260 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Input integration guide (cockpit → RIOJoy → game)
How a game receives the cockpit's **inputs** — the 72 lighted buttons, two
16-key keypads, and 5 analog axes — and how to build the profile that maps
them. This is the mirror of [OUTPUT-INTEGRATION.md](OUTPUT-INTEGRATION.md)
(game → cockpit); the wire protocol lives in [PROTOCOL.md](PROTOCOL.md), the
profile/auto-switch model in [PLAN.md](PLAN.md).
## What a game sees
RIOJoy translates cockpit events into ordinary Windows input, per profile,
through three surfaces (all can be active at once — each button picks its
route):
| Surface | What the game sees | When |
|---|---|---|
| **Virtual Xbox 360 pad** (ViGEm) | a normal XInput controller: 11 buttons, D-pad, 2 sticks, 2 triggers | default on Windows 10/11 when ViGEmBus is installed |
| **Keyboard / mouse** (`SendInput`) | scancode keystrokes with modifiers; relative mouse moves + clicks | any button routed to a key/mouse action |
| **RioGamepad HID** | a native 6-axis, 96-button, 1-hat joystick | fallback when ViGEm is absent; the XP flavor |
The sink is chosen at activation: ViGEm → RioGamepad feeder → none (keyboard
and mouse always work). Most games — XInput and DirectInput alike — see the
Xbox 360 pad as a standard controller; the practical limit is its **11
mappable buttons**, so keyboard routing carries everything beyond that.
## The input inventory
- **72 lighted buttons**, RIO addresses `0x000x47`, grouped into five MFD
clusters and four columns — the physical map is in
[OUTPUT-INTEGRATION.md](OUTPUT-INTEGRATION.md#address-map-functional-groups).
- **Two 4×4 keypads**: internal `0x500x5F`, external `0x600x6F` (key label →
address = base + hex digit; no lamps).
- **5 analog inputs** — joystick X/Y, throttle, left pedal, right pedal —
calibrated into **6 virtual axes** (X, Y, Z, Rx, Ry, Rz), each `0..32766`
with center `16383`.
## Per-button routing
Every mapped address carries one action (the `iRIO` word, PROTOCOL.md §5).
The editor exposes these as the **Action** kinds:
| Kind | What happens on press/release |
|---|---|
| **Keyboard** | key down/up by **scancode** (so DOS-era and raw-input games see it), with optional Shift/Ctrl/Alt held around it and the extended-key flag for nav keys |
| **Joystick** | virtual pad button 111 (or 196 on the RioGamepad HID) |
| **Hat** | the POV hat / D-pad direction (up/right/down/left; release = centered) |
| **Mouse** | relative move in clean 50-px steps (up/right/down/left) or left/right click — the legacy build's mixed-up move deltas are fixed in the port |
| **RIO command** | internal: axis recalibrations/resets, version/check request, diagnostic toggles — useful on a spare cockpit button so recalibration never needs the desktop |
| **Lit** flag | lamp follows the button (dim idle, bright pressed). Also marks the lamp as *profile-owned*, which shields it from the feedback endpoint (see OUTPUT-INTEGRATION.md) |
### The Xbox 360 button map
RIO joystick buttons are assigned in this fixed order — pick low numbers for
the game's most important actions:
| RIO joy button | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Pad button | A | B | X | Y | LB | RB | Back | Start | L3 | R3 | Guide |
The hat maps to the D-pad. Buttons past 11 are dropped by the pad — route
those to the keyboard instead. (On the RioGamepad HID all 96 buttons exist
natively and no mapping table applies.)
## Axes
### Calibration (per profile)
`Calibration` holds per-axis invert flags and `EnableZR`:
- **Joystick X/Y** auto-range from observed travel with a small deadzone.
- **Throttle (Z)** is the ratcheted lever; calibrated so the detent rest
position reads **0**, full forward `32766` — i.e. it is naturally
**unipolar**.
- **Pedals** feed Rx/Ry directly, or — with `EnableZR` — mix into a single
rudder axis: `Rz = 16383 left/2 + right/2` (Rx/Ry then idle).
### Routing onto the pad (`AxisRouting`, JSON-only)
Each of the six axes picks a `Target` and `Mode`
(`src/RioJoy.Core/Output/AxisRoutingConfig.cs`; null section = the legacy
default routing):
| Axis | Default target | Conversion |
|---|---|---|
| X | LeftThumbX | Centered |
| Y | LeftThumbY | Centered |
| Z | LeftTrigger | trigger byte `value×255/32766` |
| Rx | RightThumbX | Centered |
| Ry | RightThumbY | Centered |
| Rz | RightTrigger | trigger byte |
Modes for thumb targets: **`Centered`** (`(value16383)×2` → stick range) for
axes that rest mid-travel, **`UnipolarPositive`** (rest 0 = stick center,
32766 = stick max — only the upper half is used) for the ratcheted throttle.
`Target: "None"` suppresses an axis entirely.
**The triggers-are-buttons trap:** many games hard-bind the pad triggers to
fire/actions. If the throttle rides `LeftTrigger` (the default), advancing the
throttle *fires*. The shipped Descent profile
([`profiles/descent-d1x.json`](../profiles/descent-d1x.json)) is the worked
example: throttle → `RightThumbY` `UnipolarPositive`, rudder mix →
`RightThumbX`, pedals `None`, keeping both triggers free for the game.
## Choosing a strategy per game
- **Modern XInput game** — pad buttons + axes for the flight controls, keyboard
routing for the long tail (MFD pages, systems). Check the game's own binding
UI to see the pad.
- **DOS / emulated game (DOSBox, source ports)** — mostly keyboard routing (it
arrives as scancodes, which DOSBox maps cleanly); axes via the pad if the
emulator supports a controller, else map coarse throttle steps to keys.
- **Legacy DirectInput sim** — the x360 pad appears as a DirectInput device
too; if the game needs more than 11 buttons on the *stick itself*, prefer
keyboard routing or run the RioGamepad HID (96 native buttons).
- **Anything with a clickable cockpit** — mouse routing gives you cursor
nudges and clicks from cockpit buttons.
## Building the profile
1. **Create/edit** from the tray: *Edit profile*. The editor shows the cockpit
panel in its functional groups; click a button, set its label, action,
modifiers, and **Lit**, then Apply. Save writes the config.
2. **Live check**: with the RIO (or vRIO) connected, physical presses light the
panel and the axis gauges move — before any game is involved. The "Send
button output to the PC" toggle turns real keystroke injection on when you
want to test into an editor/notepad.
3. **Triggers**: comma-separated executable names that auto-activate the
profile when their window is foreground (`d1x-rebirth, descent`). Matching
is basename, case-insensitive, `.exe` optional. First matching profile
wins; DOSBox-hosted games all share the DOSBox exe name (rename per game or
switch manually); native games (Firestorm, Red Planet) go in
`NativeGameExecutables` instead — RIOJoy releases the ports for them.
4. **RIO port**: leave `(app default)`, or a COM name, or `pipe:vrio` for the
emulator.
5. **JSON-only settings** (edit `%APPDATA%\RIOJoy\config.json`): `AxisRouting`,
`Calibration` fine points, `PlasmaComPort`/`PlasmaGreeting`, and the
`Feedback` section (see FEEDBACK.md).
6. **Legacy import**: the `Import .ini` tray menu converts an original
`RIO.ini` (buttons, inverts, greeting). For programmatic install-time
handoff, see the next section.
## Shipping a profile with your game
Two models, chosen per deployment:
- **Pod bundle (production — cockpit cabinets):** the game's folder carries
its own RIOJoy copy + profile; nothing is registered anywhere. See
[Pod-bundled deployment](#pod-bundled-deployment-production) below.
- **Import into a resident RIOJoy (dev boxes):** hand a profile document to
the shared tray install, as follows.
A game (or its installer/launcher) hands its profile to RIOJoy as a
**single-profile JSON document** plus one command:
```
RioJoy.Tray.exe --import-profile <your-game-riojoy-profile.json>
```
The document is one `RioProfile` object — the shipped
[`profiles/descent-d1x.json`](../profiles/descent-d1x.json) is the reference
example. It must carry a `"Name"` (imports without one are rejected), and it
bundles everything in one payload: `Buttons`, `MatchExecutables` (the
triggers), `Calibration`, `AxisRouting`, `PlasmaGreeting`, `Feedback`,
overlay labels. Author it in the profile editor, then lift the profile object
out of `%APPDATA%\RIOJoy\config.json` into your distribution.
The import merges into the user's `%APPDATA%\RIOJoy\config.json` — created
with defaults if absent, all other content preserved. A profile with the
same name (case-insensitive) is **replaced in place**, so re-running the
import on a game update is idempotent and never disturbs other games'
profiles.
Contract for installers:
- **Check the exit code, not stdout** (the tray is a GUI-subsystem exe;
console output only appears when redirected): `0` imported, `1` failed,
`2` usage, `3` **RIOJoy is running**.
- **The tray must not be running** during import — a running tray holds the
config in memory and its own saves would silently discard the merge, so
the import refuses (exit 3) instead. Sequence: quit/skip the tray →
import → (re)launch. On launcher-managed cabinets (TeslaConsole owns the
RIOJoy lifecycle) a game's install step can import safely before the next
boot.
- **Per-user, per-session**: the config lives under the user's `%APPDATA%`
and the running-instance check is per-session — run the import in the
user's session, not as an elevated SYSTEM step.
Nothing else needs registering: once the tray starts with the merged config,
the profile's `MatchExecutables` auto-activates it whenever the game's window
is foreground (tray in Auto mode).
For games you control end-to-end, keep the profile document in the *game's*
repo as the source of truth — dxx-rebirth does this, and a RIOJoy test
(`ShippedDescentProfile_MatchesDxxRebirthReferenceCopy`) asserts the two
checkouts stay byte-identical so drift is caught in CI.
## Pod-bundled deployment (production)
On the pods (the cockpit cabinets) no resident RIOJoy runs at all. Each
podized game's install carries its own copy, built by:
```
deploy\build-pod.ps1 -ProfileJson <your-game-profile.json>
```
That emits a **self-contained** drop-in (~8 MB on net48): `riojoy\` — the app,
a **portable** `config.json` beside the exe holding just this game's profile
(it wins over the per-user `%APPDATA%` store), and all RIOJoy prerequisites
(ViGEmBus; on the XP flavor .NET 4.0 + the RioGamepadXP driver) — plus two
entry points the game's package wires up:
- **`install-riojoy.bat`** — call from the game's `postinstall.bat`.
Self-elevating and **idempotent**: safe on every install, reinstall, and
update; it installs only what's absent and never removes anything. Put
**nothing** in the game's pre-uninstall — drivers stay in place by design,
since another podized game may share them and idle drivers are harmless.
- **`start-riojoy.bat`** — call from the game's launch script before the
game:
```
start "" "...\riojoy\app\RioJoy.Tray.exe" --exit-with <game exe>
```
`--exit-with` makes RIOJoy self-managing: it activates when the game's window
comes foreground, and once the game process has run and then exited it tears
itself down completely (ports released, wallpaper restored, plasma blanked)
and quits. If the game never appears within 60 s it also quits, so a failed
launch can't strand it. Back-to-back launches hand over cleanly: a starting
`--exit-with` instance waits up to 15 s for the previous game's copy to
release the single-instance lock.
Properties that matter on a cabinet: each game pins the RIOJoy build it was
verified with (updating RIOJoy for a new game can't regress an old one);
native games simply don't bundle RIOJoy, so the COM ports are free for them
by construction; and every bundle is fully self-sufficient — drivers install
through the game's own postinstall, so a fresh pod needs no separate RIOJoy
provisioning pass. The bundled exe is still the full tray app: run it with no
arguments on the pod and you have the profile editor.
## Testing without hardware or game
- **vRIO** (`pipe:vrio`): click buttons on the emulator's panel and watch them
arrive — the editor lights up, the pad reacts.
- **joy.cpl** (Game Controllers): shows the virtual pad's axes/buttons moving.
- Keyboard routes: open Notepad, enable the editor's output toggle, press
cockpit buttons.
## Checklist for a new game
1. Find the game's real executable name (foreground window process) → Triggers.
2. Decide the axis story first: does the game hard-use triggers? If yes, route
the throttle to a thumb axis `UnipolarPositive` (copy the Descent pattern).
3. Map the few primary actions to pad buttons 111, everything else to
keyboard; mark cockpit-lit buttons **Lit**.
4. Set `EnableZR` if the game wants one rudder axis rather than two pedals.
5. Live-check in the editor, then in-game; bind a spare cockpit button to
*RIO command → recalibrate* for the cabinet.
6. Add the `Feedback` section if the game will drive lamps/plasma back
(OUTPUT-INTEGRATION.md).