Files
mitch030504--Wiicompiled_VR…/runtime/tests/physical_wheel_tests.cpp
T
iChris4andClaude Opus 5 c11a7c9a61 Add USB steering wheel and pedal support
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>
2026-09-22 14:53:14 +02:00

136 lines
5.0 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// USB wheel calibration and its GameCube pad mapping (physical_wheel.h). The
// calibration and race-mapping cases are heurazy's (mario-kart-wii-VR-port).
#include "physical_wheel.h"
#include <iostream>
using namespace physical_wheel;
namespace {
int g_failures = 0;
void Check(bool condition, const char* what) {
if (!condition) {
++g_failures;
std::cerr << "FAILED: " << what << '\n';
}
}
void TestCalibration() {
Check(Steering(-32768, -32768, 100, 32767, .02f) == -1, "left endpoint");
Check(Steering(32767, -32768, 100, 32767, .02f) == 1, "right endpoint");
Check(Steering(200, -32768, 100, 32767, .02f) == 0, "calibrated center deadzone");
Check(Steering(-32768, 32767, 0, -32768, 0) == 1, "inverted steering");
Check(Steering(100, 0, 0, 100, 0) == 0, "reject missing calibration");
// A 900-degree wheel calibrated at a quarter turn each way: full lock there,
// and clamped beyond it.
Check(Steering(-8192, -8192, 0, 8192, 0) == -1 && Steering(-30000, -8192, 0, 8192, 0) == -1,
"full lock at the recorded turn, clamped past it");
Check(Pedal(-32768, 32767, -32768) == 1, "reversed pedals (released at +32767, as Logitech pedals report)");
Check(Pedal(32767, 32767, -32768) == 0, "released reversed pedal");
Check(Pedal(-16000, 0, 32767) == 0 && Pedal(16000, 0, -32768) == 0, "combined pedals isolate opposite direction");
Check(Pedal(30000, 0, 10000) == 1, "pedal clamps beyond calibration");
}
void TestRace() {
auto p = Map(.5f, 1, 0, true, true);
Check(p.stickX == 50 && (p.button & PAD_BUTTON_A) && (p.button & PAD_TRIGGER_R) && (p.button & PAD_TRIGGER_L),
"accelerate drift and item together");
p = Map(-1, 1, 1, true, true);
Check(p.stickX == -100 && (p.button & PAD_BUTTON_B) && !(p.button & (PAD_BUTTON_A | PAD_TRIGGER_R)) &&
p.analogA == 0 && p.triggerR == 0 && (p.button & PAD_TRIGGER_L),
"brake overrides throttle and drift while preserving item");
Controls c;
c.steering = .25f;
c.throttle = 1;
c.hat = 0x08; // left
c.trick = true;
c.pause = true;
p = RacePad(c);
Check(p.stickX == 25 && (p.button & PAD_BUTTON_A) && (p.button & PAD_BUTTON_LEFT) && (p.button & PAD_BUTTON_UP) &&
(p.button & PAD_BUTTON_START),
"race: steering, accelerator, D-pad trick and pause");
c = {};
c.brake = 1;
c.confirm = true;
p = RacePad(c);
Check((p.button & PAD_BUTTON_B) && !(p.button & PAD_BUTTON_A), "the brake pedal beats the confirm button");
}
void TestMenu() {
Controls c;
c.steering = 1;
c.throttle = 1;
c.brake = 1;
c.drift = c.item = true;
PADStatus p = MenuPad(c);
Check(p.button == 0 && p.stickX == 0, "menus ignore the wheel, pedals and paddles");
c.confirm = true;
c.back = true;
c.pause = true;
c.hat = 0x01 | 0x02; // up-right
p = MenuPad(c);
Check((p.button & PAD_BUTTON_A) && (p.button & PAD_BUTTON_B) && (p.button & PAD_BUTTON_START) &&
(p.button & PAD_BUTTON_UP) && (p.button & PAD_BUTTON_RIGHT) && !(p.button & PAD_BUTTON_DOWN),
"menus: confirm, back, pause and the D-pad");
Check(HatButtons(0x04 | 0x08) == (PAD_BUTTON_DOWN | PAD_BUTTON_LEFT), "hat down-left");
}
void TestMerge() {
// Race: the wheel owns port 0 but keeps the other source's pause and item aim.
PADStatus port{};
port.err = PAD_ERR_NO_CONTROLLER;
port.button = PAD_BUTTON_START | PAD_BUTTON_A;
port.stickX = -80;
port.stickY = 70;
PadFilter filter;
Controls c;
c.steering = .5f;
Merge(port, RacePad(c), true, false, filter);
Check(port.err == PAD_ERR_NONE && port.stickX == 50 && port.stickY == 70, "race: wheel steers, stick still aims");
Check((port.button & PAD_BUTTON_START) && !(port.button & PAD_BUTTON_A), "race: other source keeps only pause");
// Menus: the wheel's buttons join the other source's navigation.
port = {};
port.err = PAD_ERR_NONE;
port.stickY = 60;
c = {};
c.confirm = true;
PadFilter menus;
Merge(port, MenuPad(c), false, false, menus);
Check((port.button & PAD_BUTTON_A) && port.analogA == 255 && port.stickY == 60, "menus: wheel adds confirm");
// A button held while blocked stays out until it is released.
PadFilter held;
port = {};
Merge(port, MenuPad(c), false, true, held);
Check(port.button == 0, "blocked: nothing reaches the game");
port = {};
Merge(port, MenuPad(c), false, false, held);
Check(!(port.button & PAD_BUTTON_A), "still held after unblocking: suppressed");
port = {};
Merge(port, MenuPad(Controls{}), false, false, held);
port = {};
Merge(port, MenuPad(c), false, false, held);
Check((port.button & PAD_BUTTON_A) != 0, "a fresh press after release goes through");
}
} // namespace
int main() {
TestCalibration();
TestRace();
TestMenu();
TestMerge();
if (g_failures != 0) {
std::cerr << g_failures << " check(s) failed\n";
return 1;
}
std::cout << "physical wheel tests passed\n";
return 0;
}