Phase 10: RIO hardware exists only on pods + dev boxes, so production is one RIOJoy copy inside each podized game folder, no resident tray. ConfigLocator makes a config.json beside the exe win over %APPDATA%; --exit-with <exe|pid> (CompanionTarget/CompanionExit, 60s startup grace) tears down and quits when the game exits; a starting --exit-with instance waits up to 15s for the predecessor mutex instead of silently exiting. deploy/build-pod.ps1 emits the ~4.5MB drop-in (app + portable config wrapping the profile + start script, no drivers) - verified against the shipped Descent profile. 455 tests; PLAN.md Phase 10 + INPUT-INTEGRATION.md pod section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
12 KiB
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 (game → cockpit); the wire protocol lives in PROTOCOL.md, the profile/auto-switch model in 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
0x00–0x47, grouped into five MFD clusters and four columns — the physical map is in OUTPUT-INTEGRATION.md. - Two 4×4 keypads: internal
0x50–0x5F, external0x60–0x6F(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..32766with center16383.
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 1–11 (or 1–96 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 ((value−16383)×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) 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
- 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.
- 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.
- Triggers: comma-separated executable names that auto-activate the
profile when their window is foreground (
d1x-rebirth, descent). Matching is basename, case-insensitive,.exeoptional. 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 inNativeGameExecutablesinstead — RIOJoy releases the ports for them. - RIO port: leave
(app default), or a COM name, orpipe:vriofor the emulator. - JSON-only settings (edit
%APPDATA%\RIOJoy\config.json):AxisRouting,Calibrationfine points,PlasmaComPort/PlasmaGreeting, and theFeedbacksection (see FEEDBACK.md). - Legacy import: the
Import .initray menu converts an originalRIO.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 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 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):
0imported,1failed,2usage,3RIOJoy 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 ~5 MB drop-in — riojoy\ (app + a portable config.json
holding just this game's profile; a config beside the exe wins over the
per-user %APPDATA% store) plus start-riojoy.bat — which the game's
launch script calls before starting the game:
start "" "...\riojoy\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 drivers stay a one-time pod provisioning step (the
universal package's install-rio.ps1) — pod bundles deliberately carry none.
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
- Find the game's real executable name (foreground window process) → Triggers.
- 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). - Map the few primary actions to pad buttons 1–11, everything else to keyboard; mark cockpit-lit buttons Lit.
- Set
EnableZRif the game wants one rudder axis rather than two pedals. - Live-check in the editor, then in-game; bind a spare cockpit button to RIO command → recalibrate for the cabinet.
- Add the
Feedbacksection if the game will drive lamps/plasma back (OUTPUT-INTEGRATION.md).