Files
SpeedBreakerProject--speedb…/runtime/hid/wheel.cpp
T
2026-10-22 14:02:14 +00:00

1117 lines
49 KiB
C++

// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See wheel.h.
#include "wheel.h"
#include "key_map.h"
#include "wheel_map.h"
#include <user/settings.h>
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <format>
#include <string>
#include <vector>
#include <SDL3/SDL_haptic.h>
#include <SDL3/SDL_joystick.h>
#include <SDL3/SDL_timer.h>
// wheel_map.h names XInput's bits as literals (it has no kernel header), as
// key_map.h does; input.cpp checks key_map.h's against the bits the game
// reads, so agreeing with it is enough.
static_assert(hid::wheelmap::button::DpadUp == hid::keymap::button::DpadUp &&
hid::wheelmap::button::DpadDown == hid::keymap::button::DpadDown &&
hid::wheelmap::button::DpadLeft == hid::keymap::button::DpadLeft &&
hid::wheelmap::button::DpadRight == hid::keymap::button::DpadRight &&
hid::wheelmap::button::Start == hid::keymap::button::Start && hid::wheelmap::button::Back == hid::keymap::button::Back &&
hid::wheelmap::button::LeftThumb == hid::keymap::button::LeftThumb &&
hid::wheelmap::button::RightThumb == hid::keymap::button::RightThumb &&
hid::wheelmap::button::LeftShoulder == hid::keymap::button::LeftShoulder &&
hid::wheelmap::button::RightShoulder == hid::keymap::button::RightShoulder &&
hid::wheelmap::button::A == hid::keymap::button::A && hid::wheelmap::button::B == hid::keymap::button::B &&
hid::wheelmap::button::X == hid::keymap::button::X && hid::wheelmap::button::Y == hid::keymap::button::Y,
"wheel_map.h's button bits are key_map.h's, and so XInput's");
// ...and its hat bits are SDL's.
static_assert(hid::wheelmap::hat::Centred == SDL_HAT_CENTERED && hid::wheelmap::hat::Up == SDL_HAT_UP &&
hid::wheelmap::hat::Right == SDL_HAT_RIGHT && hid::wheelmap::hat::Down == SDL_HAT_DOWN &&
hid::wheelmap::hat::Left == SDL_HAT_LEFT, "wheel_map.h's hat bits are SDL's");
namespace hid::wheel
{
namespace
{
using wheelmap::Axes;
using wheelmap::Binding;
// A device reports at most this many buttons to us; the wheels we
// know report 25 (a G29 with a shifter attached).
constexpr int kMaxButtons = 64;
static_assert(size_t(kMaxButtons) == wheelmap::kCalibrateButtons, "Calibrate watches every button read");
bool s_started = false;
SDL_Joystick* s_joystick = nullptr;
SDL_JoystickID s_id = 0;
std::string s_name;
const wheelmap::Model* s_model = nullptr;
std::vector<Binding> s_bindings;
Axes s_axes;
wheelmap::Steering s_steering; // the wheel's two ends, measured or assumed
wheelmap::Pedal s_pedals[3]; // in Step order: Throttle, Brake, Clutch
int s_fullTurn = 0; // degrees lock to lock, 0: unknown
// The axes' resting places are read again on the first frame after
// the device was opened: a joystick SDL has not polled yet reads 0
// on every axis, and a pedal taken to rest at 0 when it really
// rests at one end would hold the throttle down.
bool s_restAgain = false;
// Which axes SDL has reported a value for. It sends an axis only
// when it moves, so everything reads 0 until something does: a
// reading from an axis that has never spoken means nothing.
std::vector<bool> s_axisSeen;
SDL_Haptic* s_haptic = nullptr;
bool s_canAutocentre = false; // the wheel takes an autocentre level
bool s_canSpring = false; // ...or a spring effect
bool s_canHapticRumble = false; // SDL's own rumble: only where the wheel can't play a sine
bool s_canKnock = false; // it plays a sine of ours: the knocks (wheel_map.h's Knock)
bool s_canJoystickRumble = false;
bool s_onlyOneEffect = false; // a rumble would stop the centring: the centring wins
SDL_HapticEffectID s_spring = -1;
wheelmap::Centring s_centring; // when the wheel is told its centring force (wheel_map.h)
bool s_reported = false; // the wheel has sent an event (SDL's driver has cleared its autocentre)
Uint64 s_centringLogAt = 0; // the per-minute count's last line (SDL ticks)
int s_centringLogged = 0; // ...and s_centring.sends then
float s_rumble = 0.0f; // what it was last told to play
// The knocks (wheel_map.h's Knock): the sine, made at the first one
// and kept, and what the one under way has done, for its log line.
SDL_HapticEffectID s_knockEffect = -1;
wheelmap::Knock s_knock;
Uint64 s_knockFrameNs = 0; // the last frame's time (SDL_GetTicksNS)
Uint64 s_knockStartNs = 0;
float s_knockPeak = 0.0f;
int s_knockChanges = 0;
bool s_knockSustained = false; // this knock became a long rumble (eased)
int s_knocks = 0; // since the wheel opened: each one's log line has its number
const bool s_log = std::getenv("NFSMW_WHEEL_LOG") != nullptr;
// A pedal step's place in s_pedals, and back: slot 0 is the throttle,
// 1 the brake, 2 the clutch (wheel_map.h; Step starts with the wheel's
// two ends, so a slot is not a Step by itself).
int Slot(Step step) { return wheelmap::PedalSlot(step); }
using wheelmap::PedalStep;
settings::Id AxisOption(Step step)
{
switch (step)
{
case Step::Throttle: return settings::Id::WheelThrottleAxis;
case Step::Brake: return settings::Id::WheelBrakeAxis;
case Step::Clutch: return settings::Id::WheelClutchAxis;
default: return settings::Id::WheelSteerAxis;
}
}
settings::Id RestOption(Step step)
{
switch (step)
{
case Step::Brake: return settings::Id::BrakeRest;
case Step::Clutch: return settings::Id::ClutchRest;
default: return settings::Id::ThrottleRest;
}
}
settings::Id FullOption(Step step)
{
switch (step)
{
case Step::Brake: return settings::Id::BrakeFull;
case Step::Clutch: return settings::Id::ClutchFull;
default: return settings::Id::ThrottleFull;
}
}
// The setting holding the button given to a pad control; they run in
// Control order from ButtonA, as the static_assert below checks.
settings::Id ButtonOption(wheelmap::Control control)
{
return settings::Id(size_t(settings::Id::ButtonA) + size_t(control));
}
static_assert(size_t(settings::Id::ButtonShiftDown) - size_t(settings::Id::ButtonA) + 1 == wheelmap::kControlCount,
"one Button option per wheel_map.h Control, in Control order");
int AxisOf(Step step)
{
switch (step)
{
case Step::Throttle: return s_axes.throttle;
case Step::Brake: return s_axes.brake;
case Step::Clutch: return s_axes.clutch;
default: return s_axes.steer;
}
}
// What Calibrate recorded for each control, in Control order.
std::array<int, wheelmap::kControlCount> StoredButtons()
{
std::array<int, wheelmap::kControlCount> out{};
for (size_t i = 0; i < wheelmap::kControlCount; i++)
out[i] = settings::GetInt(ButtonOption(wheelmap::Control(i)));
return out;
}
int16_t AxisNow(int axis)
{
if (!s_joystick || axis < 0)
return 0;
return SDL_GetJoystickAxis(s_joystick, axis);
}
// Where every axis is now, which is where they rest when the device
// has just been opened.
std::vector<int16_t> AxisStates(bool initial)
{
std::vector<int16_t> values;
if (!s_joystick)
return values;
const int count = std::max(0, SDL_GetNumJoystickAxes(s_joystick));
values.resize(size_t(count));
for (int a = 0; a < count; a++)
{
Sint16 state = 0;
// The state SDL saw when the device turned up, which a pedal
// hasn't had a chance to move yet; failing that, now's.
if (!initial || !SDL_GetJoystickAxisInitialState(s_joystick, a, &state))
state = SDL_GetJoystickAxis(s_joystick, a);
values[size_t(a)] = state;
}
return values;
}
// The axes the player assigned (Settings > Wheel > Calibrate); -1 each
// where they haven't.
Axes StoredAxes()
{
return { settings::GetInt(settings::Id::WheelSteerAxis), settings::GetInt(settings::Id::WheelThrottleAxis),
settings::GetInt(settings::Id::WheelBrakeAxis), settings::GetInt(settings::Id::WheelClutchAxis) };
}
// Which axis is which, and each pedal's resting place: the model's
// idea, checked against where the axes rest, with the player's own
// assignments on top of both.
void Calibrate(bool initial)
{
const std::vector<int16_t> rest = AxisStates(initial);
bool anySeen = false;
for (size_t a = 0; a < s_axisSeen.size(); a++)
anySeen = anySeen || s_axisSeen[a];
const Axes stored = StoredAxes();
Axes preferred = s_model->axes;
if (stored.steer >= 0) preferred.steer = stored.steer;
if (stored.throttle >= 0) preferred.throttle = stored.throttle;
if (stored.brake >= 0) preferred.brake = stored.brake;
if (stored.clutch >= 0) preferred.clutch = stored.clutch;
// Where the axes rest only means something once they have
// reported: before that, the model's own layout is all there is.
s_axes = anySeen ? wheelmap::Detect(rest, preferred) : preferred;
// The player's assignments are not guesses: they stand.
if (stored.steer >= 0) s_axes.steer = stored.steer;
if (stored.throttle >= 0) s_axes.throttle = stored.throttle;
if (stored.brake >= 0) s_axes.brake = stored.brake;
if (stored.clutch >= 0) s_axes.clutch = stored.clutch;
// The wheel's two ends: both as Calibrate measured them, else
// the axis as it comes. Both have to be there - one alone would
// put the centre halfway to nowhere.
const int16_t left = int16_t(settings::GetInt(settings::Id::SteerLeft));
const int16_t right = int16_t(settings::GetInt(settings::Id::SteerRight));
if (left != right && left != 0 && right != 0)
s_steering.Start(left, right);
else
s_steering.Start(-wheelmap::kAxisMax, wheelmap::kAxisMax);
// Each pedal's axis and the span it runs over: what Calibrate
// measured, else what this wheel's model says its pedals do,
// else the axis itself once it has reported (its far end being
// as far as it can go). Without any of those it reads nothing,
// rather than reading as pressed.
struct Plan { int axis = -1; int16_t rest = 0, full = 0; bool known = false; };
Plan plan[3];
for (Step step : { Step::Throttle, Step::Brake, Step::Clutch })
{
Plan& it = plan[Slot(step)];
it.axis = AxisOf(step);
if (it.axis < 0 || size_t(it.axis) >= rest.size())
{
it.axis = -1;
continue;
}
const int16_t storedRest = int16_t(settings::GetInt(RestOption(step)));
const int16_t storedFull = int16_t(settings::GetInt(FullOption(step)));
if (storedRest != storedFull)
it = { it.axis, storedRest, storedFull, true };
else if (s_model->pedalRest != s_model->pedalFull)
it = { it.axis, s_model->pedalRest, s_model->pedalFull, true };
else if (size_t(it.axis) < s_axisSeen.size() && s_axisSeen[size_t(it.axis)])
{
const int16_t at = rest[size_t(it.axis)];
it = { it.axis, at, int16_t(at >= 0 ? -32768 : 32767), true };
}
}
// No two controls may read one axis the same way: that is how a
// calibration gone wrong has one pedal working two of the
// game's, while the other pedal does nothing at all. Two pedals
// sharing an axis is allowed only where their spans run
// opposite ways, which is how a wheel with both on one axis
// reports them. The wheel's own axis is never a pedal.
for (int a = 0; a < 3; a++)
{
if (plan[a].axis >= 0 && plan[a].axis == s_axes.steer)
{
fprintf(stderr, "[wheel] %s is on the wheel's own axis %d: ignored\n", StepName(PedalStep(a)), plan[a].axis);
plan[a] = {};
}
for (int b = a + 1; b < 3; b++)
{
if (plan[a].axis < 0 || plan[a].axis != plan[b].axis)
continue;
const bool opposite = plan[a].known && plan[b].known &&
(plan[a].full > plan[a].rest) != (plan[b].full > plan[b].rest);
if (opposite)
continue; // one axis, one each way: both are real
fprintf(stderr, "[wheel] %s and %s both read axis %d the same way: %s dropped (calibrate it)\n",
StepName(PedalStep(a)), StepName(PedalStep(b)), plan[a].axis, StepName(PedalStep(b)));
plan[b] = {};
settings::Set(AxisOption(PedalStep(b)), -1); // so it doesn't come back next run
}
}
for (Step step : { Step::Throttle, Step::Brake, Step::Clutch })
{
const Plan& it = plan[Slot(step)];
s_pedals[Slot(step)] = {}; // not ready: reads nothing
if (it.axis >= 0 && it.known)
s_pedals[Slot(step)].Start(it.rest, it.full);
}
s_axes.throttle = plan[0].axis;
s_axes.brake = plan[1].axis;
s_axes.clutch = plan[2].axis;
// Each pedal's span as well as its axis: a pedal reading
// nothing, or reading backwards, shows here before anyone has
// to guess at it.
std::string pedals;
for (Step step : { Step::Throttle, Step::Brake, Step::Clutch })
{
const wheelmap::Pedal& it = s_pedals[Slot(step)];
pedals += std::format(", {} {}", StepName(step), AxisOf(step));
pedals += it.ready ? std::format(" ({:+d} to {:+d})", int(it.idle), int(it.full)) : std::string(" (not placed)");
}
fprintf(stderr, "[wheel] axes: steering %d%s (of %zu; rest", s_axes.steer, pedals.c_str(), rest.size());
for (int16_t v : rest)
fprintf(stderr, " %d", int(v));
fprintf(stderr, ")\n");
}
// The knock under way, in one line when it ends (every one: what the
// wheel did is what a tester's report is about). "up to" is the
// strength of a full rumble (Force Feedback applied), "force" the
// share of the wheel's own that the sine swung to.
void LogKnock(const char* how)
{
if (s_knockStartNs == 0)
return;
fprintf(stderr, "[wheel] knock %d: %.0f ms, up to %.0f%% (force %.0f%%, Force Feedback %d%%), %d changes%s, %s\n",
s_knocks, double(SDL_GetTicksNS() - s_knockStartNs) / 1e6, double(s_knockPeak) * 100.0,
double(s_knockPeak * wheelmap::kKnockGain) * 100.0, settings::GetInt(settings::Id::ForceFeedback), s_knockChanges,
s_knockSustained ? ", eased as a long rumble" : "", how);
}
void EndKnock(const char* how)
{
if (!s_knock.playing && s_knockStartNs == 0)
return;
if (s_haptic && s_knockEffect >= 0)
SDL_StopHapticEffect(s_haptic, s_knockEffect);
LogKnock(how);
s_knock = {};
s_knockStartNs = 0;
s_knockSustained = false;
}
// A frame of the knocks (wheel_map.h's Knock): the strength the game
// asks for now, at Settings > Wheel > Force Feedback.
void PlayKnock(float strength)
{
const Uint64 now = SDL_GetTicksNS();
const float ms = s_knockFrameNs ? float(double(now - s_knockFrameNs) / 1e6) : 0.0f;
s_knockFrameNs = now;
using Action = wheelmap::Knock::Action;
// (A long frame - a hitch, the menu - lets go of no more than 100 ms' worth.)
const bool wasSustained = s_knock.sustained;
const Action action = s_knock.Step(strength, std::min(ms, 100.0f));
if (s_knock.sustained && !wasSustained)
{
s_knockSustained = true;
fprintf(stderr, "[wheel] knock %d: a long rumble (asked for over %d ms, %.0f%% now): eased to %.0f%% of it\n",
s_knocks, wheelmap::kKnockSustainMs, double(strength) * 100.0, double(wheelmap::kKnockSustainScale) * 100.0);
}
if (action == Action::None)
return;
if (action == Action::Stop)
{
EndKnock("let go");
return;
}
SDL_HapticEffect effect{};
effect.periodic.type = SDL_HAPTIC_SINE;
effect.periodic.direction.type = SDL_HAPTIC_CARTESIAN;
effect.periodic.direction.dir[0] = 1; // along the wheel's one axis
effect.periodic.period = wheelmap::kKnockPeriodMs;
effect.periodic.magnitude = wheelmap::Knock::Magnitude(s_knock.level);
effect.periodic.length = SDL_HAPTIC_INFINITY; // it plays until the knock is let go of
if (s_log)
fprintf(stderr, "[wheel] knock %s: magnitude %d\n", action == Action::Start ? "starts" : "changes",
int(effect.periodic.magnitude));
if (action == Action::Change)
{
// In place: the driver keeps the sine where it is in its swing.
if (SDL_UpdateHapticEffect(s_haptic, s_knockEffect, &effect))
{
s_knockChanges++;
s_knockPeak = std::max(s_knockPeak, s_knock.level);
}
return;
}
// Start: made at the first knock and kept; each knock runs it from
// the start of its swing, where it pushes from nothing.
bool ok = s_knockEffect >= 0 ? SDL_UpdateHapticEffect(s_haptic, s_knockEffect, &effect)
: (s_knockEffect = SDL_CreateHapticEffect(s_haptic, &effect)) >= 0;
ok = ok && SDL_RunHapticEffect(s_haptic, s_knockEffect, 1);
if (!ok)
{
fprintf(stderr, "[wheel] knock failed: %s; SDL's rumble instead from now on\n", SDL_GetError());
if (s_knockEffect >= 0)
SDL_DestroyHapticEffect(s_haptic, s_knockEffect);
s_knockEffect = -1;
s_knock = {};
s_canKnock = false;
s_canHapticRumble = SDL_InitHapticRumble(s_haptic);
return;
}
s_knockStartNs = now;
s_knockPeak = s_knock.level;
s_knockChanges = 0;
s_knockSustained = false;
s_knocks++;
}
void StopRumble()
{
s_rumble = 0.0f;
EndKnock("stopped");
if (s_haptic && s_canHapticRumble)
SDL_StopHapticRumble(s_haptic);
if (s_joystick && s_canJoystickRumble)
SDL_RumbleJoystick(s_joystick, 0, 0, 0);
}
void StopSpring()
{
if (s_haptic && s_spring >= 0)
{
SDL_StopHapticEffect(s_haptic, s_spring);
SDL_DestroyHapticEffect(s_haptic, s_spring);
}
s_spring = -1;
}
// Settings > Wheel > Centering Force: an autocentre level where the
// wheel takes one, else a spring effect that pulls it to the middle,
// said only when wheel_map.h's Centring says it is due (never on a
// timer: each time, SDL lets the wheel's centring go for a moment).
void ApplyCentering()
{
if (!s_haptic)
return;
const int percent = std::clamp(settings::GetInt(settings::Id::CenteringForce), 0, 100);
if (!s_centring.Due(percent, s_reported))
return;
if (s_log)
fprintf(stderr, "[wheel] centring %d%% said (%d since the wheel opened)\n", percent, s_centring.sends);
if (s_canAutocentre)
{
if (!SDL_SetHapticAutocenter(s_haptic, percent))
fprintf(stderr, "[wheel] autocentre %d%% failed: %s\n", percent, SDL_GetError());
return;
}
if (!s_canSpring)
return;
if (percent == 0)
{
StopSpring();
return;
}
SDL_HapticEffect effect{};
effect.condition.type = SDL_HAPTIC_SPRING;
effect.condition.direction.type = SDL_HAPTIC_CARTESIAN;
effect.condition.direction.dir[0] = 1;
effect.condition.length = SDL_HAPTIC_INFINITY;
for (int axis = 0; axis < 3; axis++)
{
effect.condition.right_sat[axis] = wheelmap::SpringSaturation(percent);
effect.condition.left_sat[axis] = wheelmap::SpringSaturation(percent);
effect.condition.right_coeff[axis] = Sint16(wheelmap::SpringLevel(percent));
effect.condition.left_coeff[axis] = Sint16(wheelmap::SpringLevel(percent));
}
if (s_spring >= 0)
{
if (SDL_UpdateHapticEffect(s_haptic, s_spring, &effect))
return;
StopSpring(); // it wouldn't take the change: make it afresh
}
s_spring = SDL_CreateHapticEffect(s_haptic, &effect);
if (s_spring < 0 || !SDL_RunHapticEffect(s_haptic, s_spring, SDL_HAPTIC_INFINITY))
{
fprintf(stderr, "[wheel] centring force failed: %s\n", SDL_GetError());
StopSpring();
s_canSpring = false;
}
}
// What the wheel can do with its motors, which needs the system to
// drive them: on a Mac SDL 3.4's own Logitech driver
// (SDL_hidapihaptic_lg4ff.c: a constant force, a sine, a spring, an
// autocentre level and gain, 16 effects at once, as measured on a
// G29), on Linux the kernel's driver (not tried yet).
void OpenHaptics()
{
s_canAutocentre = s_canSpring = s_canHapticRumble = s_canKnock = s_onlyOneEffect = false;
s_knockEffect = -1;
s_knock = {};
s_knockStartNs = 0;
s_canJoystickRumble = false;
s_centring = {};
s_centringLogged = 0;
s_centringLogAt = SDL_GetTicks();
if (!s_joystick)
return;
SDL_PropertiesID props = SDL_GetJoystickProperties(s_joystick);
s_canJoystickRumble = SDL_GetBooleanProperty(props, SDL_PROP_JOYSTICK_CAP_RUMBLE_BOOLEAN, false);
if (!SDL_IsJoystickHaptic(s_joystick))
{
fprintf(stderr, "[wheel] no force feedback on this system: the wheel stays slack%s\n",
s_canJoystickRumble ? " (it does take plain rumble)" : "");
return;
}
s_haptic = SDL_OpenHapticFromJoystick(s_joystick);
if (!s_haptic)
{
fprintf(stderr, "[wheel] force feedback didn't open: %s\n", SDL_GetError());
return;
}
const Uint32 features = SDL_GetHapticFeatures(s_haptic);
s_canAutocentre = (features & SDL_HAPTIC_AUTOCENTER) != 0;
s_canSpring = (features & SDL_HAPTIC_SPRING) != 0;
// Knocks are a sine of our own where the wheel plays one (the
// Logitech wheels on a Mac); SDL's own rumble only where it
// can't (wheel_map.h's Knock says why).
s_canKnock = (features & SDL_HAPTIC_SINE) != 0;
s_canHapticRumble = !s_canKnock && SDL_InitHapticRumble(s_haptic);
// One effect at a time: a knock would take the centring force
// away, which matters far more while driving.
const int playing = SDL_GetMaxHapticEffectsPlaying(s_haptic);
s_onlyOneEffect = playing == 1 && !s_canAutocentre && s_canSpring;
const std::string sine = std::format("yes (our own {} ms sine at up to {:.0f}% of the wheel's force, rising over {} ms, "
"let go of over {} ms)",
wheelmap::kKnockPeriodMs, double(wheelmap::kKnockGain) * 100.0, wheelmap::kKnockAttackMs, wheelmap::kKnockReleaseMs);
fprintf(stderr, "[wheel] force feedback: centring %s, knocks %s\n",
s_canAutocentre ? "autocentre" : s_canSpring ? "spring" : "no",
s_onlyOneEffect ? "no (one effect at a time: the centring force has it)"
: s_canKnock ? sine.c_str()
: s_canHapticRumble ? "yes" : s_canJoystickRumble ? "rumble" : "no");
}
void Close()
{
StopRumble();
StopSpring();
if (s_haptic)
{
if (s_canAutocentre)
SDL_SetHapticAutocenter(s_haptic, 0);
SDL_CloseHaptic(s_haptic);
s_haptic = nullptr;
}
if (s_joystick)
{
SDL_CloseJoystick(s_joystick);
s_joystick = nullptr;
}
s_id = 0;
s_restAgain = false;
s_axisSeen.clear();
s_name.clear();
s_bindings.clear();
s_axes = {};
s_model = nullptr;
s_fullTurn = 0;
s_centring = {};
s_reported = false;
}
// NFSMW_WHEEL_BUTTONS="0=A,2=B,8=BACK,9=START,4=SHIFTUP": what the
// wheel's buttons do, for one whose numbering the model has wrong.
void TakeBindings()
{
// The model's buttons, with whatever Calibrate recorded on top.
const std::array<int, wheelmap::kControlCount> assigned = StoredButtons();
s_bindings = wheelmap::WithAssigned(s_model->buttons, assigned);
const char* text = std::getenv("NFSMW_WHEEL_BUTTONS");
if (!text || !*text)
return;
std::string error;
s_bindings = wheelmap::ParseBindings(text, s_bindings, error);
if (!error.empty())
fprintf(stderr, "[wheel] NFSMW_WHEEL_BUTTONS: can't read \"%s\"\n", error.c_str());
std::string shown;
for (const Binding& b : s_bindings)
shown += (shown.empty() ? "" : " ") + std::to_string(b.index) + "=" + std::string(wheelmap::NameOf(b));
fprintf(stderr, "[wheel] buttons: %s\n", shown.c_str());
}
void Open()
{
if (s_joystick || !s_started)
return;
int count = 0;
SDL_JoystickID* ids = SDL_GetJoysticks(&count);
if (!ids)
return;
for (int i = 0; i < count && !s_joystick; i++)
{
const SDL_JoystickID id = ids[i];
const char* name = SDL_GetJoystickNameForID(id);
const bool sdlSaysWheel = SDL_GetJoystickTypeForID(id) == SDL_JOYSTICK_TYPE_WHEEL;
const uint16_t vendor = SDL_GetJoystickVendorForID(id), product = SDL_GetJoystickProductForID(id);
SDL_Joystick* joystick = SDL_OpenJoystick(id);
if (!joystick)
{
fprintf(stderr, "[wheel] joystick %u didn't open: %s\n", unsigned(id), SDL_GetError());
continue;
}
// How many axes and buttons it has takes an open device, and
// tells a wheel from a gamepad whose name happens to match.
if (!wheelmap::LooksLikeWheel(sdlSaysWheel, vendor, product, name ? name : "",
SDL_GetNumJoystickAxes(joystick), SDL_GetNumJoystickButtons(joystick)))
{
SDL_CloseJoystick(joystick);
continue;
}
// The device has only just been opened: make sure SDL has
// read it before its axes are taken for where they rest.
SDL_UpdateJoysticks();
s_joystick = joystick;
s_id = id;
s_name = name ? name : "Steering wheel";
s_model = &wheelmap::ModelFor(vendor, product);
s_fullTurn = s_model->fullTurnDegrees;
const std::string turn = s_fullTurn ? std::to_string(s_fullTurn) + " degrees" : "an unknown amount";
fprintf(stderr, "[wheel] %s (USB %04x:%04x, %s, %d axes, %d buttons, %d hats, turns %s)\n", s_name.c_str(),
vendor, product, s_model->vendor ? s_model->name : "not one we know",
SDL_GetNumJoystickAxes(s_joystick), SDL_GetNumJoystickButtons(s_joystick),
SDL_GetNumJoystickHats(s_joystick), turn.c_str());
TakeBindings();
s_axisSeen.assign(size_t(std::max(0, SDL_GetNumJoystickAxes(s_joystick))), false);
Calibrate(true);
s_restAgain = true;
OpenHaptics();
ApplyCentering();
}
SDL_free(ids);
}
// Settings > Wheel > Calibrate (wheel_map.h's rules; Sample feeds it).
wheelmap::Calibration s_cal;
// The joystick button that is this pad button now (-1: none).
int JoystickButtonOf(uint16_t padButton)
{
for (const Binding& b : s_bindings)
if (b.role == wheelmap::Role::Button && b.button == padButton)
return b.index;
return -1;
}
// What a calibration step did: a kept step is stored at once, so
// stopping (or the wheel going) keeps everything done so far.
void ApplyCalibration(wheelmap::Calibration::Event event)
{
using Event = wheelmap::Calibration::Event;
if (event == Event::None)
return;
const Step step = s_cal.LastStep();
if (event == Event::Kept)
{
const wheelmap::Calibration::Found f = s_cal.LastFound();
if (wheelmap::IsButtonStep(step))
{
settings::Set(ButtonOption(wheelmap::ControlOf(step)), double(f.button));
TakeBindings();
fprintf(stderr, "[wheel] calibrate: %s: button %d\n", StepName(step), f.button);
}
else
{
fprintf(stderr, "[wheel] calibrate: %s: axis %d, from %+d to %+d (%.0f%% of its travel)\n", StepName(step),
f.axis, int(f.rest), int(f.full), double(f.moved) * 100.0);
if (step == Step::SteerLeft)
{
settings::Set(settings::Id::WheelSteerAxis, double(f.axis));
settings::Set(settings::Id::SteerLeft, double(f.full));
// The two ends say which way round the wheel is, so
// the manual flip starts clear.
settings::Set(settings::Id::InvertSteering, 0.0);
}
else if (step == Step::SteerRight)
{
settings::Set(settings::Id::WheelSteerAxis, double(f.axis));
settings::Set(settings::Id::SteerRight, double(f.full));
}
else
{
settings::Set(AxisOption(step), double(f.axis));
settings::Set(RestOption(step), double(f.rest));
settings::Set(FullOption(step), double(f.full));
}
Calibrate(false);
}
}
else if (event == Event::Skipped)
fprintf(stderr, "[wheel] calibrate: %s skipped\n", StepName(step));
else if (event == Event::Back)
fprintf(stderr, "[wheel] calibrate: back to %s\n", StepName(step));
else if (event == Event::Stopped)
fprintf(stderr, "[wheel] calibrate: stopped at %s\n", StepName(step));
if (!s_cal.Running() && event != Event::Stopped)
fprintf(stderr, "[wheel] calibrate: done\n");
}
// NFSMW_WHEEL_LOG=1: every button as it is pressed (which is how to
// find a wheel's numbering), and the axes once a second.
void LogSample(const std::array<bool, kMaxButtons>& buttons, const Reading& r)
{
if (!s_log)
return;
static std::array<bool, kMaxButtons> was{};
for (int b = 0; b < kMaxButtons; b++)
if (buttons[size_t(b)] != was[size_t(b)])
fprintf(stderr, "[wheel] button %d %s\n", b, buttons[size_t(b)] ? "down" : "up");
was = buttons;
static auto last = std::chrono::steady_clock::now();
const auto now = std::chrono::steady_clock::now();
if (now - last < std::chrono::seconds(1))
return;
last = now;
fprintf(stderr, "[wheel] steering raw %d -> %+.3f (the game's stick %+.3f); pedals raw (%d, %d, %d) -> throttle %.2f, "
"brake %.2f, clutch %.2f\n",
int(AxisNow(s_axes.steer)), r.steer, r.stick, int(AxisNow(s_axes.throttle)), int(AxisNow(s_axes.brake)),
int(AxisNow(s_axes.clutch)), r.throttle, r.brake, r.clutch);
}
std::array<bool, kMaxButtons> ButtonsNow()
{
std::array<bool, kMaxButtons> held{};
if (!s_joystick)
return held;
const int count = std::min(kMaxButtons, std::max(0, SDL_GetNumJoystickButtons(s_joystick)));
for (int b = 0; b < count; b++)
held[size_t(b)] = SDL_GetJoystickButton(s_joystick, b);
return held;
}
uint8_t HatNow()
{
if (!s_joystick || SDL_GetNumJoystickHats(s_joystick) < 1)
return wheelmap::hat::Centred;
return SDL_GetJoystickHat(s_joystick, 0);
}
// How far a pedal is down, or nothing at all while its axis has yet
// to report: until then the 0 SDL reads needn't be where the pedal
// is. On a wheel whose
// pedals run +32767 to -32768 that 0 is halfway, so without this a
// resting pedal reads half pressed.
float PedalValue(Step step)
{
const int axis = AxisOf(step);
if (axis < 0 || size_t(axis) >= s_axisSeen.size() || !s_axisSeen[size_t(axis)])
return 0.0f;
return s_pedals[Slot(step)].Value(AxisNow(axis));
}
bool InPlay()
{
return s_joystick != nullptr && settings::GetBool(settings::Id::Wheel);
}
Reading s_last; // what Sample last read (Last)
}
bool StepIsButton(Step step)
{
return wheelmap::IsButtonStep(step);
}
const char* StepName(Step step)
{
switch (step)
{
case Step::SteerLeft: return "Full left";
case Step::SteerRight: return "Full right";
case Step::Throttle: return "Throttle";
case Step::Brake: return "Brake";
case Step::Clutch: return "Clutch";
default: break;
}
if (StepIsButton(step))
return wheelmap::kControls[size_t(step) - size_t(Step::ButtonA)].name;
return "?";
}
const char* StepAsk(Step step)
{
switch (step)
{
case Step::SteerLeft: return "Turn the wheel fully left";
case Step::SteerRight: return "Turn the wheel fully right";
case Step::Throttle: return "Press the throttle to the floor";
case Step::Brake: return "Press the brake to the floor";
case Step::Clutch: return "Press the clutch to the floor";
default: break;
}
return StepIsButton(step) ? "Press the wheel button you want for it" : "?";
}
void Initialize()
{
s_started = true;
// A Steering Response below 1.00 from a test build before the game's
// own 20% deadzone was stepped over (settings_version 0) was set to
// fight that deadzone, and now makes the centre far too sharp (0.50:
// the first 10% of steering inside 3 degrees of the wheel). Back to
// 1.00 once; the file takes it at the game's next save. A value set
// since (version 1) is the player's.
if (settings::GetInt(settings::Id::WheelSettingsVersion) < 1)
{
const float response = settings::GetFloat(settings::Id::SteeringResponse);
if (response < 1.0f && !settings::EnvOverride(settings::Id::SteeringResponse))
{
settings::Set(settings::Id::SteeringResponse, 1.0);
fprintf(stderr, "[wheel] Steering Response %.2f was set while the game's own steering deadzone was in the way: "
"back to 1.00, which now steers in step with the wheel\n",
double(response));
}
settings::Set(settings::Id::WheelSettingsVersion, 1.0);
}
// Every Wheel setting as it is read, so a run says for itself what
// it is playing by: a value here that differs from settings.toml
// means the file didn't reach the game.
fprintf(stderr, "[wheel] settings: wheel %s, range %s, invert %s, deadzone %.2f, response %.2f, "
"force feedback %d%%, centring %d%%\n",
settings::GetBool(settings::Id::Wheel) ? "on" : "off",
settings::FormatValue(settings::Id::SteeringRange, settings::GetValue(settings::Id::SteeringRange)).c_str(),
settings::GetBool(settings::Id::InvertSteering) ? "yes" : "no",
double(settings::GetFloat(settings::Id::SteeringDeadzone)), double(settings::GetFloat(settings::Id::SteeringResponse)),
settings::GetInt(settings::Id::ForceFeedback), settings::GetInt(settings::Id::CenteringForce));
Open();
}
void HandleEvent(const SDL_Event& e)
{
if (!s_started)
return;
// The wheel's first event of any kind follows SDL's driver clearing
// its autocentre (on the first report that changed anything): the
// centring force is said once more (wheel_map.h Centring).
if (s_joystick && !s_reported &&
((e.type == SDL_EVENT_JOYSTICK_AXIS_MOTION && e.jaxis.which == s_id) ||
((e.type == SDL_EVENT_JOYSTICK_BUTTON_DOWN || e.type == SDL_EVENT_JOYSTICK_BUTTON_UP) && e.jbutton.which == s_id) ||
(e.type == SDL_EVENT_JOYSTICK_HAT_MOTION && e.jhat.which == s_id)))
s_reported = true;
if (e.type == SDL_EVENT_JOYSTICK_AXIS_MOTION && s_joystick && e.jaxis.which == s_id)
{
const int a = e.jaxis.axis;
if (a >= 0 && size_t(a) < s_axisSeen.size() && !s_axisSeen[size_t(a)])
{
s_axisSeen[size_t(a)] = true;
// SDL's first report of an axis carries where it was before it
// moved: a calibration step measures such an axis from there.
s_cal.FirstValue(a, e.jaxis.value);
// The device is certainly reporting now: whether it can knock
// is asked again if it said no (the centring: below).
if (s_haptic && !s_canKnock && !s_canHapticRumble && SDL_InitHapticRumble(s_haptic))
{
s_canHapticRumble = true;
fprintf(stderr, "[wheel] force feedback: knocks yes after all (the wheel was busy at first)\n");
}
// A pedal on this axis that had no resting place to work
// from takes the value it was at as it started to move,
// which is near enough where it rests. A wheel we know
// never gets here: its model said so at the open.
for (Step step : { Step::Throttle, Step::Brake, Step::Clutch })
if (AxisOf(step) == a && !s_pedals[Slot(step)].ready)
{
// Where it was as it started to move is near enough
// where it rests, and it can go as far as its axis.
const int16_t at = e.jaxis.value;
s_pedals[Slot(step)].Start(at, int16_t(at >= 0 ? -32768 : 32767));
fprintf(stderr, "[wheel] %s: axis %d first reported %+d, taken as where it rests\n", StepName(step),
a, int(at));
}
}
}
else if (e.type == SDL_EVENT_JOYSTICK_ADDED)
{
Open();
}
else if (e.type == SDL_EVENT_JOYSTICK_REMOVED && s_joystick && e.jdevice.which == s_id)
{
fprintf(stderr, "[wheel] %s disconnected\n", s_name.c_str());
StopCalibrate();
Close();
Open(); // a second wheel, if one is plugged in
}
}
bool Found()
{
return s_joystick != nullptr;
}
bool Playing()
{
return InPlay();
}
bool Owns(SDL_JoystickID joystick)
{
return s_joystick != nullptr && joystick == s_id;
}
std::string Name()
{
return s_joystick ? s_name : std::string();
}
bool HasForceFeedback()
{
return s_haptic != nullptr && (s_canAutocentre || s_canSpring || s_canHapticRumble || s_canKnock);
}
bool Sample(Reading& out)
{
if (!s_joystick)
return false;
if (s_restAgain)
{
s_restAgain = false;
Calibrate(true); // a frame on, with the device certainly read
}
const std::array<bool, kMaxButtons> held = ButtonsNow();
if (s_cal.Running())
ApplyCalibration(s_cal.Update(AxisStates(false), s_axisSeen, held, HatNow(), JoystickButtonOf(wheelmap::button::A),
JoystickButtonOf(wheelmap::button::B)));
const wheelmap::Pressed pressed = wheelmap::Resolve(held, HatNow(), s_bindings);
Reading r;
r.buttons = pressed.buttons;
r.rightY = pressed.rightY;
r.back = (pressed.buttons & wheelmap::button::Back) != 0;
r.start = (pressed.buttons & wheelmap::button::Start) != 0;
if (s_axes.steer >= 0)
{
float turn = s_steering.Fraction(AxisNow(s_axes.steer));
if (settings::GetBool(settings::Id::InvertSteering))
turn = -turn;
r.steer = wheelmap::Steer(turn, settings::GetInt(settings::Id::SteeringRange), s_fullTurn,
settings::GetFloat(settings::Id::SteeringDeadzone), settings::GetFloat(settings::Id::SteeringResponse));
r.stick = wheelmap::StickPastGameDeadzone(r.steer);
}
r.throttle = PedalValue(Step::Throttle);
r.brake = PedalValue(Step::Brake);
r.clutch = PedalValue(Step::Clutch);
LogSample(held, r);
s_last = r;
// Switched off, the wheel plays nothing - but it was still read, so
// Settings > Wheel keeps showing what it reads, and the menus (and
// the chord that opens them) keep working from it.
if (!settings::GetBool(settings::Id::Wheel))
return false;
out = r;
return true;
}
Reading Last()
{
return s_joystick ? s_last : Reading{};
}
uint16_t HeldButtons()
{
if (!s_joystick || s_cal.Running()) // Calibrate has the wheel's buttons
return 0;
return wheelmap::Resolve(ButtonsNow(), HatNow(), s_bindings).buttons;
}
uint16_t ButtonFor(SDL_JoystickID joystick, int button)
{
if (!s_joystick || joystick != s_id || s_cal.Running()) // Calibrate has the wheel's buttons
return 0;
for (const Binding& b : s_bindings)
if (b.index == button && b.role == wheelmap::Role::Button)
return b.button;
return 0;
}
void Rumble(uint16_t low, uint16_t high, bool vibration)
{
if (!s_joystick)
return;
const int percent = settings::GetInt(settings::Id::ForceFeedback);
const float strength = vibration ? wheelmap::RumbleStrength(low, high, percent) : 0.0f;
if (s_canKnock && !s_onlyOneEffect)
{
PlayKnock(strength); // every frame: a knock is let go of over several
return;
}
if (strength == s_rumble)
return;
s_rumble = strength;
if (strength <= 0.0f)
{
StopRumble();
return;
}
if (s_onlyOneEffect)
return; // the centring force has the wheel's one effect
if (s_canHapticRumble && SDL_PlayHapticRumble(s_haptic, strength, 5000))
return;
if (s_canJoystickRumble)
SDL_RumbleJoystick(s_joystick, uint16_t(low * percent / 100), uint16_t(high * percent / 100), 5000);
}
void Update()
{
if (!s_joystick)
return;
ApplyCentering(); // only when due: the setting changed, or the device's first report
// How often the centring was said, once a minute while a wheel is open
// (0 is the answer in play: a click a second was a re-send a second).
const Uint64 now = SDL_GetTicks();
if (s_haptic && now - s_centringLogAt >= 60000)
{
fprintf(stderr, "[wheel] centring said %d times in the last minute (%s at %d%%, %d since the wheel opened)\n",
s_centring.sends - s_centringLogged, s_canAutocentre ? "autocentre" : s_canSpring ? "spring" : "none",
settings::GetInt(settings::Id::CenteringForce), s_centring.sends);
s_centringLogged = s_centring.sends;
s_centringLogAt = now;
}
}
void NoteMenu(bool open)
{
if (!s_joystick)
return;
const char* knocks = s_haptic && s_onlyOneEffect ? "none (the centring force has the wheel's one effect)"
: s_haptic && s_canKnock ? "our sine"
: s_haptic && s_canHapticRumble ? "SDL's rumble"
: s_canJoystickRumble ? "plain rumble"
: "none (no force feedback here)";
fprintf(stderr, "[wheel] menu %s: knocks by %s (%d so far%s); centring %s at %d%%; Force Feedback %d%%; range %s, "
"response %.2f, deadzone %.2f\n",
open ? "opened" : "closed", knocks, s_knocks, s_knock.playing ? ", one playing" : "",
!s_haptic ? "none" : s_canAutocentre ? "autocentre" : s_canSpring ? "spring" : "none",
settings::GetInt(settings::Id::CenteringForce),
settings::GetInt(settings::Id::ForceFeedback),
settings::FormatValue(settings::Id::SteeringRange, settings::GetValue(settings::Id::SteeringRange)).c_str(),
double(settings::GetFloat(settings::Id::SteeringResponse)), double(settings::GetFloat(settings::Id::SteeringDeadzone)));
}
void Suspend()
{
StopRumble();
}
void Shutdown()
{
Close();
s_started = false;
}
void StartCalibrate()
{
if (!s_joystick)
return;
s_cal.Start(AxisStates(false), s_axisSeen, ButtonsNow(), HatNow(), s_axes.steer);
fprintf(stderr, "[wheel] calibrate: started\n");
}
bool Calibrating()
{
return s_cal.Running();
}
Step CalibratingStep()
{
return s_cal.Current();
}
Candidate Watch()
{
return s_cal.Watch();
}
bool KeepStep()
{
const auto event = s_cal.Keep(AxisStates(false));
ApplyCalibration(event);
return event == wheelmap::Calibration::Event::Kept;
}
void SkipStep()
{
ApplyCalibration(s_cal.Skip(AxisStates(false)));
}
void BackStep()
{
ApplyCalibration(s_cal.Back(AxisStates(false)));
}
void StopCalibrate()
{
ApplyCalibration(s_cal.Stop());
}
Assignment Axes()
{
return { s_axes.steer, s_axes.throttle, s_axes.brake, s_axes.clutch };
}
void ResetAxes()
{
StopCalibrate();
for (settings::Id id : { settings::Id::WheelSteerAxis, settings::Id::WheelThrottleAxis, settings::Id::WheelBrakeAxis,
settings::Id::WheelClutchAxis })
settings::Set(id, -1);
for (settings::Id id : { settings::Id::InvertSteering, settings::Id::ThrottleRest, settings::Id::ThrottleFull,
settings::Id::BrakeRest, settings::Id::BrakeFull, settings::Id::ClutchRest,
settings::Id::ClutchFull, settings::Id::SteerLeft, settings::Id::SteerRight })
settings::Reset(id);
for (size_t i = 0; i < wheelmap::kControlCount; i++)
settings::Reset(ButtonOption(wheelmap::Control(i)));
if (s_joystick)
TakeBindings();
if (s_joystick)
Calibrate(false);
}
}