# Thrustmaster Sol-R 1 -> Space Engineers input mapping (Linux/Arch/Hyprland) ## Goal Fix "throttle behaves like a trigger" bug and build a full custom input scheme for the Sol-R1 (grip + base) in Space Engineers, on Arch/Hyprland, without Windows/T.A.R.G.E.T. ## Stack decided on - **joyful** (AUR: `yay -S joyful`) - Go-based Linux joystick remapper, creates a virtual gamepad device from physical Sol-R inputs via YAML rules in `~/.config/joyful/`. Chosen over input-remapper because it natively supports split-axis and segmented-axis-to-button mappings, which input-remapper doesn't (see upstream issue #900). - **switch_sync.py** (custom, in this repo/outputs) - a separate stateful daemon for switches that need to reconcile physical position against actual in-game state (bypasses joyful, which can't do this since it has no game-state feedback). - **Steam launch options wrapper script** - starts/stops joyful and switch_sync around the actual game session via `%command%`, instead of a Hyprland-IPC window-watcher (Proton WM_CLASS is unreliable for this). NOTE: Steam launch options are per-machine (stored in `userdata//config/localconfig.vdf`, not synced) - the wrapper script path should be identical across machines (keep it in dotfiles), but the launch-options string itself must be re-pasted into Steam on each machine. ## Diagnostic tools (already written, in outputs/) - `find_codes.py` - run this first on every physical control to get real evdev BTN_*/ABS_* codes. Nothing below has real codes yet - they're all `TODO:` placeholders until this is run. - `switch_sync.py` - screen-reads HUD state via `grim` region capture + color sampling, compares against physical switch position (evdev), and taps a virtual key only when they disagree. Has `--sample X,Y` for pixel-color calibration and `--dump-example-config`. ## Current mapping plan | Physical control | SE function | Mechanism | |---|---|---| | Throttle (base) | Move Forward/Backward | joyful axis passthrough (was original bug: SE read raw HID axis like a trigger) | | Main stick (grip) X/Y | View/look/rotate | joyful axis passthrough | | Ministick (grip) | Strafe horizontal/vertical | joyful axis passthrough | | Dual-position trigger, stage 1/2 | Primary/secondary click (use/fire, alt-fire) | joyful button passthrough | | 2 orange buttons (top of stick) | Dampeners on/off (Z) + Relative dampers (Ctrl+Z) | joyful, KEY_Z / [KEY_LEFTCTRL, KEY_Z] key emulation (not gamepad buttons - Relative Dampers isn't confirmed as a standalone bindable action, it's a modifier on the toggle key) | | Rotary knob 1 (base) | Switch toolbar (next/prev, default `,`/`.`) | joyful, detent pulses -> KEY_COMMA/KEY_DOT - ASSUMES knob reports as BTN pulses, not an ABS axis; confirm with find_codes.py | | Rotary knob 2 (base) | Hotbar slot select | joyful, same pulse assumption as above; if it's an ABS axis instead, needs joyful's segmented axis-to-button feature | | 4-button group (base, left side) | Jetpack / Helmet / Flashlight / Parking toggles | joyful button passthrough to virtual gamepad buttons, bound in SE's own control menu | | 4 tilt switches (base, top), 2 buttons each | Same 4 toggles as above, but as REAL on/off switches | switch_sync.py - NOT joyful, because this needs game-state feedback to avoid desync | ## Known caveats / open items - SE's native joystick support only got real per-binding customization in update 1.209; before that (and possibly still, depending on version) it treated joysticks through an Xbox-controller-shaped abstraction. Check Options -> Controls -> Controllers on the current version before assuming full flexibility. - Dampeners are a stateless toggle (Z) + contextual modifier (Ctrl+Z, only works if dampers are off AND you're looking at a moving target in range). No true 3-state action exists in SE. - `switch_sync.py`'s HUD detection is pixel-color based and fragile: breaks on HUD detail mode changes (Tab), resolution/UI scale changes, or camera view changes. Needs recalibration after any of those. - joyful's exact per-rule-type YAML field names (beyond the top-level devices/modes/rules structure and evdev keycode naming) were not fully verified against `examples/ruletypes.yml` in the actual installed version - check that file before trusting rule blocks verbatim. - None of the joyful config blocks have real BTN_*/ABS_* codes yet - all placeholders pending find_codes.py output on the actual hardware. ## Next steps 1. Run find_codes.py against every physical control, replace all TODOs. 2. Confirm rotary knobs are pulse (BTN) vs continuous (ABS) - determines which joyful rule type to use. 3. Verify SE's actual keybinds for helmet/flashlight/parking in Options before finalizing switch_sync.py's toggle_key values. 4. Calibrate switch_sync.py's HUD regions/colors for jetpack, helmet, light, parking indicators. 5. Write the Steam launch-options wrapper script, add to dotfiles repo. 6. Repeat launch-options paste on each machine (desktop, Framework 12, ThinkPad T440p) - not synced by Steam.