Files
riojoy/docs/INPUT-INTEGRATION.md
T
CydandClaude Fable 5 97caf124a6 pod: bundled per-game deployment (portable config, --exit-with, build-pod)
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>
2026-07-31 22:01:52 -05:00

12 KiB
Raw Blame History

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 0x000x47, grouped into five MFD clusters and four columns — the physical map is in OUTPUT-INTEGRATION.md.
  • 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) 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 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): 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 ~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

  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).