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