# 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 `0x00–0x47`, 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 `0x50–0x5F`, external `0x60–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..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 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`](../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 ``` 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 ``` 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 ``` `--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 1–11, 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).