mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 01:00:14 +02:00
Ported from heurazy's mario-kart-wii-VR-port. F10 > Controllers > USB wheel and pedals (also in the headset panel) picks and calibrates the steering axis and both pedals by moving them, and assigns buttons by pressing them, over raw SDL joysticks: no per-model table or gamepad mapping, and separate pedals, reversed axes and combined pedal axes all calibrate the same way. Settings live in PhysicalWheel.toml beside Config.toml; heurazy's five-button files load unchanged. The wheel is player 1's GameCube controller. In a race it owns port 0 (steering on the stick, the accelerator on A, the brake pedal braking then reversing over A and drift, the paddles on R and L), keeping only the other source's pause and item aim. In menus it adds only deliberate presses. It arms once every pedal and button is released, stays neutral in a race while its setup is incomplete, and nothing is opened until it is enabled. Optional light rumble follows the game's own, capped at 15 %. Added here: its confirm, a new back button and the steering device's first hat (D-pad) work the menus, since our VR controllers default to a Wii Remote and the wheel needs to navigate on its own. Devices SDL classifies as wheels are marked in the list. In the VR cockpit the wheel follows the hardware steering and hand steering steps aside while it drives. README documents setup and Logitech notes (G HUB, the G29's PS3 switch, shifter gears as buttons); not yet tried on a physical wheel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
174 lines
6.2 KiB
C++
174 lines
6.2 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// USB steering wheel and pedals for player 1. Ported from heurazy's
|
|
// mario-kart-wii-VR-port (GPL-3.0-or-later).
|
|
//
|
|
// Any SDL joystick works: the steering axis, both pedals and the buttons are
|
|
// chosen and calibrated by moving or pressing them (F10 > Controllers > USB
|
|
// wheel and pedals), so no per-model table or gamepad mapping is involved, and
|
|
// separate pedals, reversed axes and combined pedal axes all calibrate the same
|
|
// way. The wheel is a GameCube controller on port 0: in a race it owns
|
|
// steering, pedals and the assigned buttons; in menus it adds its confirm,
|
|
// back, pause and D-pad (the steering device's first hat) to whatever else
|
|
// drives port 0. The settings live in PhysicalWheel.toml beside Config.toml.
|
|
//
|
|
// The mapping below is pure and tested (tests/physical_wheel_tests.cpp).
|
|
|
|
#pragma once
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <dolphin/pad.h>
|
|
|
|
namespace physical_wheel {
|
|
|
|
// Endpoint calibration also handles reversed and combined pedal axes.
|
|
inline float Pedal(int raw, int released, int pressed) {
|
|
if (std::abs(pressed - released) < 1024) return 0;
|
|
return std::clamp(float(raw - released) / float(pressed - released), 0.f, 1.f);
|
|
}
|
|
inline float Steering(int raw, int left, int center, int right, float deadzone) {
|
|
if (std::abs(left - center) < 1024 || std::abs(right - center) < 1024 || (left < center) == (right < center))
|
|
return 0;
|
|
const float direction = float(raw - center) / float(right - center);
|
|
const float x = direction >= 0 ? Pedal(raw, center, right) : -Pedal(raw, center, left);
|
|
deadzone = std::clamp(deadzone, 0.f, .25f);
|
|
return std::copysign(std::max(0.f, (std::abs(x) - deadzone) / (1 - deadzone)), x);
|
|
}
|
|
inline PADStatus Map(float steering, float throttle, float brake, bool drift, bool item) {
|
|
PADStatus p{};
|
|
p.err = PAD_ERR_NONE;
|
|
p.stickX = static_cast<int8_t>(std::lround(std::clamp(steering, -1.f, 1.f) * 100));
|
|
if (throttle > .1f) {
|
|
p.button |= PAD_BUTTON_A;
|
|
p.analogA = 255;
|
|
}
|
|
if (drift) {
|
|
p.button |= PAD_TRIGGER_R;
|
|
p.triggerR = 255;
|
|
}
|
|
if (item) {
|
|
p.button |= PAD_TRIGGER_L;
|
|
p.triggerL = 255;
|
|
}
|
|
if (brake > .1f) {
|
|
p.button &= ~(PAD_BUTTON_A | PAD_TRIGGER_R);
|
|
p.analogA = p.triggerR = 0;
|
|
p.button |= PAD_BUTTON_B;
|
|
p.analogB = 255;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
// SDL_HAT_UP/RIGHT/DOWN/LEFT bits as the GameCube D-pad.
|
|
inline uint16_t HatButtons(uint8_t hat) {
|
|
uint16_t buttons = 0;
|
|
if (hat & 0x01) buttons |= PAD_BUTTON_UP;
|
|
if (hat & 0x02) buttons |= PAD_BUTTON_RIGHT;
|
|
if (hat & 0x04) buttons |= PAD_BUTTON_DOWN;
|
|
if (hat & 0x08) buttons |= PAD_BUTTON_LEFT;
|
|
return buttons;
|
|
}
|
|
|
|
// One sample of the calibrated hardware.
|
|
struct Controls {
|
|
float steering = 0, throttle = 0, brake = 0;
|
|
bool drift = false, item = false, trick = false, confirm = false, pause = false, back = false;
|
|
uint8_t hat = 0;
|
|
};
|
|
|
|
// Race: steering on the stick, the accelerator on A, the brake pedal on B
|
|
// (braking, then reversing, over the accelerator and drift), R drift, L item,
|
|
// the D-pad and the trick button for tricks and wheelies.
|
|
inline PADStatus RacePad(const Controls& c) {
|
|
auto p = Map(c.steering, c.throttle, c.brake, c.drift, c.item);
|
|
p.button |= HatButtons(c.hat);
|
|
if (c.trick) p.button |= PAD_BUTTON_UP;
|
|
if (c.confirm && !(p.button & PAD_BUTTON_B)) {
|
|
p.button |= PAD_BUTTON_A;
|
|
p.analogA = 255;
|
|
}
|
|
if (c.pause) p.button |= PAD_BUTTON_START;
|
|
return p;
|
|
}
|
|
|
|
// Menus: only deliberate presses, so a resting foot or a turned wheel never
|
|
// moves a cursor or confirms. The pedals and the steering stay out.
|
|
inline PADStatus MenuPad(const Controls& c) {
|
|
PADStatus p{};
|
|
p.err = PAD_ERR_NONE;
|
|
p.button = HatButtons(c.hat);
|
|
if (c.confirm) {
|
|
p.button |= PAD_BUTTON_A;
|
|
p.analogA = 255;
|
|
}
|
|
if (c.back) {
|
|
p.button |= PAD_BUTTON_B;
|
|
p.analogB = 255;
|
|
}
|
|
if (c.pause) p.button |= PAD_BUTTON_START;
|
|
return p;
|
|
}
|
|
|
|
// Input that was held while blocked (settings open, not yet armed) stays out of
|
|
// the game until it is released.
|
|
class PadFilter {
|
|
public:
|
|
PADStatus Apply(PADStatus pad, bool blocked) noexcept {
|
|
if (blocked) {
|
|
suppressed_ = pad.button;
|
|
suppress_stick_ = pad.stickX != 0 || pad.stickY != 0;
|
|
pad = {};
|
|
pad.err = PAD_ERR_NONE;
|
|
return pad;
|
|
}
|
|
suppressed_ &= pad.button;
|
|
pad.button &= ~suppressed_;
|
|
if (!(pad.button & PAD_BUTTON_A)) pad.analogA = 0;
|
|
if (!(pad.button & PAD_BUTTON_B)) pad.analogB = 0;
|
|
if (!(pad.button & PAD_TRIGGER_L)) pad.triggerL = 0;
|
|
if (!(pad.button & PAD_TRIGGER_R)) pad.triggerR = 0;
|
|
if (suppress_stick_) {
|
|
suppress_stick_ = pad.stickX != 0 || pad.stickY != 0;
|
|
pad.stickX = pad.stickY = 0;
|
|
}
|
|
return pad;
|
|
}
|
|
|
|
private:
|
|
uint16_t suppressed_ = 0;
|
|
bool suppress_stick_ = false;
|
|
};
|
|
|
|
// Folds the wheel into port 0. A race gives the wheel the whole pad, keeping
|
|
// only the other source's pause and its stick's vertical axis (item aiming);
|
|
// menus add the wheel's buttons to it.
|
|
inline void Merge(PADStatus& pad, PADStatus wheel, bool race, bool blocked, PadFilter& filter) {
|
|
if (race) {
|
|
const auto pause = pad.button & PAD_BUTTON_START;
|
|
const auto aim = pad.stickY;
|
|
pad = filter.Apply(wheel, blocked);
|
|
pad.button |= pause;
|
|
pad.stickY = blocked ? 0 : aim;
|
|
} else {
|
|
wheel.stickX = wheel.stickY = 0;
|
|
wheel = filter.Apply(wheel, blocked);
|
|
pad.button |= wheel.button;
|
|
pad.analogA = std::max(pad.analogA, wheel.analogA);
|
|
pad.analogB = std::max(pad.analogB, wheel.analogB);
|
|
pad.err = PAD_ERR_NONE;
|
|
}
|
|
}
|
|
|
|
// Device and UI operations run on the game thread (PADRead and the settings
|
|
// overlay); the XR pacing thread reads a locked snapshot.
|
|
bool ReadPad(PADStatus& pad, bool blocked, bool race);
|
|
void DrawSettings();
|
|
// The calibrated steering, -1..1, while the wheel is driving a race.
|
|
bool SteeringSnapshot(float& steering);
|
|
// PADControlMotor for port 0; false when the wheel does not own it.
|
|
bool Motor(int channel, unsigned command);
|
|
void Shutdown();
|
|
|
|
} // namespace physical_wheel
|