// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See input.h. #include #include "input.h" #include "key_map.h" #include #include #include #include #include #include // key_map.h names XInput's bits as literals (it has no kernel header): keep // them the bits the game reads. static_assert(hid::keymap::button::DpadUp == XAMINPUT_GAMEPAD_DPAD_UP && hid::keymap::button::DpadDown == XAMINPUT_GAMEPAD_DPAD_DOWN && hid::keymap::button::DpadLeft == XAMINPUT_GAMEPAD_DPAD_LEFT && hid::keymap::button::DpadRight == XAMINPUT_GAMEPAD_DPAD_RIGHT && hid::keymap::button::Start == XAMINPUT_GAMEPAD_START && hid::keymap::button::Back == XAMINPUT_GAMEPAD_BACK && hid::keymap::button::LeftThumb == XAMINPUT_GAMEPAD_LEFT_THUMB && hid::keymap::button::RightThumb == XAMINPUT_GAMEPAD_RIGHT_THUMB && hid::keymap::button::LeftShoulder == XAMINPUT_GAMEPAD_LEFT_SHOULDER && hid::keymap::button::RightShoulder == XAMINPUT_GAMEPAD_RIGHT_SHOULDER && hid::keymap::button::A == XAMINPUT_GAMEPAD_A && hid::keymap::button::B == XAMINPUT_GAMEPAD_B && hid::keymap::button::X == XAMINPUT_GAMEPAD_X && hid::keymap::button::Y == XAMINPUT_GAMEPAD_Y, "key_map.h's button bits are XInput's"); // The key codes of user/key_names.h are SDL's scancodes (USB HID usages); // tests/key_names_test.cpp checks every row, this pins a few. static_assert(settings::keynames::Code("a") == SDL_SCANCODE_A && settings::keynames::Code("keypad_1") == SDL_SCANCODE_KP_1 && settings::keynames::Code("right_alt") == SDL_SCANCODE_RALT && settings::keynames::Code("escape") == SDL_SCANCODE_ESCAPE); namespace { constexpr uint32_t ERROR_SUCCESS = 0; constexpr uint32_t ERROR_DEVICE_NOT_CONNECTED = 0x48F; struct Pad { uint16_t buttons = 0; uint8_t leftTrigger = 0, rightTrigger = 0; int16_t lx = 0, ly = 0, rx = 0, ry = 0; }; std::mutex s_mutex; Pad s_pad; // latest sample (devices only) bool s_deviceConnected = false; // a gamepad, or the keyboard of our window uint32_t s_packet = 0; uint16_t s_rumbleLow = 0, s_rumbleHigh = 0; bool s_rumbleDirty = false; SDL_Gamepad* s_gamepad = nullptr; std::atomic s_blocked{ false }; // Main thread: buttons held when a menu closed, hidden from the game // until each is released (the B, Start or A that closed it). uint16_t s_latchedButtons = 0; bool s_wasBlocked = false; bool s_scriptSuppressed = false; // under s_mutex: no scripted input while a menu is open std::chrono::steady_clock::time_point s_lastInput; // main thread: a control last in use (SecondsSinceInput) bool s_inputSeen = false; // The controller's Back is held back from the game until it's clear it // isn't the start of the Back + Start settings chord: released (then // replayed as a short tap), or held 250 ms without Start. Main thread. // In guest time, so a suspension can't eat the replayed tap. enum class BackState { Idle, Pending, Passing, Replay, Swallow }; BackState s_backState = BackState::Idle; int64_t s_backSince = 0; // guest ns bool PadBackForGame(bool back, bool start) { int64_t now = guesttime::NowNs(); switch (s_backState) { case BackState::Idle: if (back) { s_backState = start ? BackState::Swallow : BackState::Pending; s_backSince = now; } return false; case BackState::Pending: if (start) { s_backState = BackState::Swallow; // the chord: the UI has it return false; } if (!back) { s_backState = BackState::Replay; s_backSince = now; return true; } if (now - s_backSince >= 250'000'000) { s_backState = BackState::Passing; return true; } return false; case BackState::Passing: if (!back) s_backState = BackState::Idle; return back; case BackState::Replay: if (now - s_backSince < 100'000'000) return true; s_backState = BackState::Idle; return false; case BackState::Swallow: if (!back && !start) s_backState = BackState::Idle; return false; } return false; } // A scripted input: buttons (or analog: RT/LT triggers, LL/LR/LU/LD left // stick) held over [time, time + duration). struct ScriptTap { double time; uint16_t buttons; int16_t lx, ly; double duration = 0.15; uint8_t lt = 0, rt = 0; }; std::vector s_script; // Guest ns at launch: the script and the virtual pad run on guest time, // so a press due while the game is suspended comes after it instead of // being lost. const int64_t s_start = guesttime::NowNs(); double SecondsSinceLaunch() { return double(guesttime::NowNs() - s_start) / 1e9; } int16_t ToShort(float v) { return int16_t(std::clamp(v * 32767.0f, -32768.0f, 32767.0f)); } // Radial deadzone with rescale: |v| < dz -> 0, else (|v|-dz)/(1-dz). void ApplyDeadzone(float& x, float& y, float dz) { float m = std::sqrt(x * x + y * y); if (m <= dz) { x = y = 0.0f; return; } float scaled = std::min(1.0f, (m - dz) / (1.0f - dz)); x = x / m * scaled; y = y / m * scaled; } uint8_t Trigger(float v, float dz) { if (v <= dz) return 0; return uint8_t(std::clamp((v - dz) / (1.0f - dz), 0.0f, 1.0f) * 255.0f + 0.5f); } uint16_t ButtonByName(const std::string& n) { static const std::pair names[] = { { "UP", XAMINPUT_GAMEPAD_DPAD_UP }, { "DOWN", XAMINPUT_GAMEPAD_DPAD_DOWN }, { "LEFT", XAMINPUT_GAMEPAD_DPAD_LEFT }, { "RIGHT", XAMINPUT_GAMEPAD_DPAD_RIGHT }, { "START", XAMINPUT_GAMEPAD_START }, { "BACK", XAMINPUT_GAMEPAD_BACK }, { "LS", XAMINPUT_GAMEPAD_LEFT_THUMB }, { "RS", XAMINPUT_GAMEPAD_RIGHT_THUMB }, { "LB", XAMINPUT_GAMEPAD_LEFT_SHOULDER }, { "RB", XAMINPUT_GAMEPAD_RIGHT_SHOULDER }, { "A", XAMINPUT_GAMEPAD_A }, { "B", XAMINPUT_GAMEPAD_B }, { "X", XAMINPUT_GAMEPAD_X }, { "Y", XAMINPUT_GAMEPAD_Y }, }; for (auto& [name, bit] : names) if (n == name) return bit; return 0; } void ParseScript() { const char* s = std::getenv("NFSMW_INPUT_SCRIPT"); if (!s) return; std::string all = s; size_t pos = 0; while (pos < all.size()) { size_t end = all.find(',', pos); std::string item = all.substr(pos, end == std::string::npos ? std::string::npos : end - pos); pos = end == std::string::npos ? all.size() : end + 1; size_t colon = item.find(':'); if (colon == std::string::npos) continue; // "sec:BUTTON", "from-to/step:BUTTON" (a tap every `step` s) or // "from~to:BUTTON" (held). std::string when = item.substr(0, colon); std::string name = item.substr(colon + 1); ScriptTap proto{ 0, ButtonByName(name), 0, 0 }; if (name == "RT") proto.rt = 255; else if (name == "LT") proto.lt = 255; else if (name == "LL") proto.lx = -32767; else if (name == "LR") proto.lx = 32767; else if (name == "LU") proto.ly = 32767; else if (name == "LD") proto.ly = -32767; else if (proto.buttons == 0) { fprintf(stderr, "[input] script: unknown button in \"%s\"\n", item.c_str()); continue; } uint16_t button = proto.buttons; if (size_t tilde = when.find('~'); tilde != std::string::npos) { ScriptTap hold = proto; hold.time = std::atof(when.substr(0, tilde).c_str()); hold.duration = std::atof(when.substr(tilde + 1).c_str()) - hold.time; s_script.push_back(hold); continue; } size_t dash = when.find('-'), slash = when.find('/'); if (dash != std::string::npos && slash != std::string::npos && slash > dash) { double from = std::atof(when.substr(0, dash).c_str()); double to = std::atof(when.substr(dash + 1, slash - dash - 1).c_str()); double step = std::max(0.2, std::atof(when.substr(slash + 1).c_str())); for (double t = from; t <= to; t += step) { ScriptTap tap = proto; tap.time = t; s_script.push_back(tap); } } else { ScriptTap tap = proto; tap.time = std::atof(when.c_str()); s_script.push_back(tap); } } fprintf(stderr, "[input] script: %zu taps\n", s_script.size()); } // Apply the scripted inputs active now on top of the device state. void ApplyScript(Pad& p) { if (s_script.empty()) return; double t = SecondsSinceLaunch(); for (const auto& tap : s_script) if (t >= tap.time && t < tap.time + tap.duration) { p.buttons |= tap.buttons; p.leftTrigger = std::max(p.leftTrigger, tap.lt); p.rightTrigger = std::max(p.rightTrigger, tap.rt); if (tap.lx) p.lx = tap.lx; if (tap.ly) p.ly = tap.ly; } } const bool s_scriptParsed = (ParseScript(), true); // NFSMW_VIRTUAL_PAD="2:LX=0.05,4:LX=0.2,6:RT=0.01": an SDL virtual // controller, the only one opened, whose axes move to those values at // seconds since launch (sticks LX/LY/RX/RY -1..1, up positive; triggers // LT/RT 0..1) and hold them. Its buttons too: "5:BACK=1" holds one down, // "5.2:BACK=0" lets it go, "6:DOWN" taps it (0.15 s); A B X Y BACK START // LB RB LS RS and the D-pad's UP DOWN LEFT RIGHT. Being a real SDL // controller, it drives the menus (Back + Start, the D-pad, A, B) as // well as the game. A button changes once a frame at most, so a tap due // whole within one long frame (a capture's stall) still shows a frame // pressed. "12:SHOT" saves the next presented frame (ui::RequestShot), on // the pad's clock. NFSMW_INPUT_SCRIPT feeds the game after the // device path; this goes through it, so the deadzones and Vibration // reach it as they would a real controller, for tests without one. Each // step logs what the game then gets; the rumble the game asks for and // the rumble the pad receives are logged. struct PadStep { double time; SDL_GamepadAxis axis; float value; std::string text; int button = -1; bool down = false; }; constexpr int kShotStep = -2; // PadStep::button: a capture, not a button std::vector s_padSteps; size_t s_padNext = 0; SDL_JoystickID s_virtualPadId = 0; SDL_Joystick* s_virtualPad = nullptr; bool SDLCALL VirtualPadRumble(void*, Uint16 low, Uint16 high) { fprintf(stderr, "[input] virtual pad: rumble low %u, high %u\n", unsigned(low), unsigned(high)); return true; } void AttachVirtualPad() { const char* v = std::getenv("NFSMW_VIRTUAL_PAD"); if (!v || !*v) return; // A test's window rarely has the keyboard focus (on the Mac it's // started from a shell behind other windows), and without it SDL // drops stick and trigger changes: the pad kept its first value. SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); static const std::pair axes[] = { { "LX", SDL_GAMEPAD_AXIS_LEFTX }, { "LY", SDL_GAMEPAD_AXIS_LEFTY }, { "RX", SDL_GAMEPAD_AXIS_RIGHTX }, { "RY", SDL_GAMEPAD_AXIS_RIGHTY }, { "LT", SDL_GAMEPAD_AXIS_LEFT_TRIGGER }, { "RT", SDL_GAMEPAD_AXIS_RIGHT_TRIGGER }, }; for (std::string_view rest = v; !rest.empty();) { size_t comma = rest.find(','); std::string item(rest.substr(0, comma)); rest = comma == std::string_view::npos ? std::string_view() : rest.substr(comma + 1); size_t colon = item.find(':'), equals = item.find('='); if (colon == std::string::npos || (equals != std::string::npos && equals < colon)) continue; std::string name = item.substr(colon + 1, equals == std::string::npos ? std::string::npos : equals - colon - 1); static const std::pair buttons[] = { { "A", SDL_GAMEPAD_BUTTON_SOUTH }, { "B", SDL_GAMEPAD_BUTTON_EAST }, { "X", SDL_GAMEPAD_BUTTON_WEST }, { "Y", SDL_GAMEPAD_BUTTON_NORTH }, { "BACK", SDL_GAMEPAD_BUTTON_BACK }, { "START", SDL_GAMEPAD_BUTTON_START }, { "LB", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER }, { "RB", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER }, { "LS", SDL_GAMEPAD_BUTTON_LEFT_STICK }, { "RS", SDL_GAMEPAD_BUTTON_RIGHT_STICK }, { "UP", SDL_GAMEPAD_BUTTON_DPAD_UP }, { "DOWN", SDL_GAMEPAD_BUTTON_DPAD_DOWN }, { "LEFT", SDL_GAMEPAD_BUTTON_DPAD_LEFT }, { "RIGHT", SDL_GAMEPAD_BUTTON_DPAD_RIGHT }, }; if (name == "SHOT") { s_padSteps.push_back({ std::atof(item.c_str()), SDL_GAMEPAD_AXIS_INVALID, 0.0f, name, kShotStep, false }); continue; } bool found = false; for (auto& [n, button] : buttons) if (name == n) { const double at = std::atof(item.c_str()); if (equals == std::string::npos) { // A tap: down, and up again 0.15 s later. s_padSteps.push_back({ at, SDL_GAMEPAD_AXIS_INVALID, 0.0f, name + " down", int(button), true }); s_padSteps.push_back({ at + 0.15, SDL_GAMEPAD_AXIS_INVALID, 0.0f, name + " up", int(button), false }); } else { const bool down = std::atof(item.c_str() + equals + 1) != 0.0; s_padSteps.push_back({ at, SDL_GAMEPAD_AXIS_INVALID, 0.0f, name + (down ? " down" : " up"), int(button), down }); } found = true; } if (found) continue; if (equals == std::string::npos) { fprintf(stderr, "[input] NFSMW_VIRTUAL_PAD: unknown button in \"%s\"\n", item.c_str()); continue; } for (auto& [n, axis] : axes) if (name == n) { s_padSteps.push_back({ std::atof(item.c_str()), axis, float(std::atof(item.c_str() + equals + 1)), item.substr(colon + 1) }); found = true; } if (!found) fprintf(stderr, "[input] NFSMW_VIRTUAL_PAD: unknown axis in \"%s\"\n", item.c_str()); } std::stable_sort(s_padSteps.begin(), s_padSteps.end(), [](const PadStep& a, const PadStep& b) { return a.time < b.time; }); SDL_VirtualJoystickDesc desc; SDL_INIT_INTERFACE(&desc); desc.type = SDL_JOYSTICK_TYPE_GAMEPAD; desc.naxes = SDL_GAMEPAD_AXIS_COUNT; desc.nbuttons = SDL_GAMEPAD_BUTTON_COUNT; desc.name = "SpeedBreaker virtual pad"; desc.Rumble = VirtualPadRumble; s_virtualPadId = SDL_AttachVirtualJoystick(&desc); s_virtualPad = s_virtualPadId ? SDL_OpenJoystick(s_virtualPadId) : nullptr; if (!s_virtualPad) { fprintf(stderr, "[input] NFSMW_VIRTUAL_PAD: no virtual controller: %s\n", SDL_GetError()); return; } fprintf(stderr, "[input] virtual pad attached, %zu steps\n", s_padSteps.size()); } // Moves the virtual pad's axes due by now. Returns the steps taken ("" if none). std::string StepVirtualPad() { if (!s_virtualPad || s_padNext >= s_padSteps.size()) return {}; double t = SecondsSinceLaunch(); std::string taken; uint32_t changed = 0; // buttons changed this frame for (; s_padNext < s_padSteps.size() && s_padSteps[s_padNext].time <= t; s_padNext++) { const PadStep& step = s_padSteps[s_padNext]; if (step.button == kShotStep) { ui::RequestShot(); taken += (taken.empty() ? "" : " ") + step.text; continue; } if (step.button >= 0) { if (changed & (1u << step.button)) break; // its next change waits a frame (the steps stay in order) changed |= 1u << step.button; SDL_SetJoystickVirtualButton(s_virtualPad, step.button, step.down); taken += (taken.empty() ? "" : " ") + step.text; continue; } float value = step.value; bool trigger = step.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER || step.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER; if (step.axis == SDL_GAMEPAD_AXIS_LEFTY || step.axis == SDL_GAMEPAD_AXIS_RIGHTY) value = -value; // SDL: down is positive // A virtual gamepad's triggers span the whole axis (-32768 is released). float raw = trigger ? std::clamp(value, 0.0f, 1.0f) * 65535.0f - 32768.0f : std::clamp(value, -1.0f, 1.0f) * 32767.0f; SDL_SetJoystickVirtualAxis(s_virtualPad, int(step.axis), Sint16(std::lround(std::clamp(raw, -32768.0f, 32767.0f)))); taken += (taken.empty() ? "" : " ") + step.text; } if (!taken.empty()) SDL_UpdateJoysticks(); // read back this frame return taken; } void OpenFirstGamepad() { if (s_gamepad) return; if (s_virtualPad) { // Only the test's pad: a real controller nearby must not rumble. s_gamepad = SDL_OpenGamepad(s_virtualPadId); if (s_gamepad) fprintf(stderr, "[input] gamepad: %s\n", SDL_GetGamepadName(s_gamepad)); return; } int count = 0; SDL_JoystickID* ids = SDL_GetGamepads(&count); if (ids && count > 0) { s_gamepad = SDL_OpenGamepad(ids[0]); if (s_gamepad) { // What it can do: a pad without motors takes rumble and ignores it. SDL_PropertiesID props = SDL_GetGamepadProperties(s_gamepad); fprintf(stderr, "[input] gamepad: %s (rumble %s, trigger rumble %s)\n", SDL_GetGamepadName(s_gamepad), SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, false) ? "yes" : "no", SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_TRIGGER_RUMBLE_BOOLEAN, false) ? "yes" : "no"); } } SDL_free(ids); } #if defined(__APPLE__) && TARGET_OS_IOS // A phone or tablet has nothing to play the game with but a controller // (or a keyboard attached to it, which counts as on desktop), and a // player who has just installed it may not know. So a notice when there // is none: once as the game starts (not over the installer, which a // finger works), and again when the last one goes. Each waits a moment // and looks again, so a controller that iOS reports a little late (a // Bluetooth pad woken by its button after launch takes a few seconds), // or one that drops out and straight back, brings up nothing; and one // that turns up while the notice shows takes it down. Main thread; // guest ns, 0: none due. constexpr const char* kNeedsController = "The game needs a controller: an Xbox or PlayStation one over Bluetooth, or a Backbone."; constexpr const char* kControllerGone = "Controller disconnected: the game needs one (Xbox or PlayStation over Bluetooth, or a Backbone)."; int64_t s_noControllerAt = 0; const char* s_noControllerText = nullptr; void NoControllerNoticeIn(int64_t ns, const char* text) { s_noControllerAt = guesttime::NowNs() + ns; s_noControllerText = text; } void NoControllerNotice() { if (!s_noControllerAt || guesttime::NowNs() < s_noControllerAt) return; s_noControllerAt = 0; // (NFSMW_INPUT_SCRIPT is the controller of an automated run.) if (s_gamepad || SDL_HasKeyboard() || !s_script.empty()) return; fprintf(stderr, "[input] no controller: %s\n", s_noControllerText); ui::Toast(s_noControllerText, 10.0); } // Something to play with turned up: a notice still showing (or waiting // its turn) no longer holds. void WithdrawNoControllerNotice() { ui::WithdrawToast(kNeedsController); ui::WithdrawToast(kControllerGone); } #endif } namespace { const bool s_inputLog = std::getenv("NFSMW_INPUT_LOG") != nullptr; // NFSMW_INPUT_LOG=1: once a second, the left stick as the device reports // it (last value and range over the second) and as the game receives it. // A stick at rest must reach the game as exactly 0 (the drift check). // The triggers too (a worn one that never reads 0 holds the brake). void LogStick(float rawX, float rawY, float x, float y, float rawLt, float rawRt, uint8_t lt, uint8_t rt) { if (!s_inputLog) return; static float minX = 1, maxX = -1, minY = 1, maxY = -1; static auto last = std::chrono::steady_clock::now(); minX = std::min(minX, rawX); maxX = std::max(maxX, rawX); minY = std::min(minY, rawY); maxY = std::max(maxY, rawY); auto now = std::chrono::steady_clock::now(); if (now - last < std::chrono::seconds(1)) return; fprintf(stderr, "[input] left stick raw (%+.4f, %+.4f) range x[%+.4f, %+.4f] y[%+.4f, %+.4f] -> game (%d, %d); " "triggers raw (%.3f, %.3f) -> game (%u, %u)\n", rawX, rawY, minX, maxX, minY, maxY, int(ToShort(x)), int(ToShort(y)), rawLt, rawRt, unsigned(lt), unsigned(rt)); minX = minY = 1; maxX = maxY = -1; last = now; } } namespace hid { void Initialize() { if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) fprintf(stderr, "[input] SDL gamepad init failed: %s\n", SDL_GetError()); fprintf(stderr, "[input] stick deadzone %.2f, trigger deadzone %.2f\n", settings::GetFloat(settings::Id::StickDeadzone), settings::GetFloat(settings::Id::TriggerDeadzone)); AttachVirtualPad(); OpenFirstGamepad(); std::lock_guard lock(s_mutex); s_deviceConnected = true; // the keyboard always counts as controller 0 } void NoteGameStarting() { #if defined(__APPLE__) && TARGET_OS_IOS NoControllerNoticeIn(5'000'000'000, kNeedsController); #endif } void HandleEvent(const SDL_Event& e) { if (e.type == SDL_EVENT_GAMEPAD_ADDED) { OpenFirstGamepad(); #if defined(__APPLE__) && TARGET_OS_IOS if (s_gamepad) WithdrawNoControllerNotice(); #endif } else if (e.type == SDL_EVENT_GAMEPAD_REMOVED && s_gamepad && e.gdevice.which == SDL_GetGamepadID(s_gamepad)) { SDL_CloseGamepad(s_gamepad); s_gamepad = nullptr; fprintf(stderr, "[input] gamepad removed\n"); OpenFirstGamepad(); #if defined(__APPLE__) && TARGET_OS_IOS if (!s_gamepad) NoControllerNoticeIn(1'000'000'000, kControllerGone); #endif } #if defined(__APPLE__) && TARGET_OS_IOS else if (e.type == SDL_EVENT_KEYBOARD_ADDED) WithdrawNoControllerNotice(); // (a keyboard counts as a controller here) #endif } void Shutdown() { if (s_gamepad) { SDL_RumbleGamepad(s_gamepad, 0, 0, 0); SDL_CloseGamepad(s_gamepad); s_gamepad = nullptr; } } void Suspend() { // Update sends a rumble for 5 s at a time and doesn't run meanwhile: // stop it now rather than buzz on for a game that stands still. if (s_gamepad) SDL_RumbleGamepad(s_gamepad, 0, 0, 0); } void Resume() { // The next Update sends the game's last request again, as the // Vibration setting allows. std::lock_guard lock(s_mutex); s_rumbleDirty = true; } double SecondsSinceInput() { if (!s_inputSeen) return 0.0; return std::chrono::duration(std::chrono::steady_clock::now() - s_lastInput).count(); } void SetBlocked(bool blocked) { s_blocked.store(blocked, std::memory_order_relaxed); } void Update() { #if defined(__APPLE__) && TARGET_OS_IOS NoControllerNotice(); #endif Pad p; float lx = 0, ly = 0, rx = 0, ry = 0, lt = 0, rt = 0; bool padBack = false; // the controller's Back, as the device reports it float rawX = 0, rawY = 0; // the left stick before its deadzone (NFSMW_INPUT_LOG) const std::string padSteps = StepVirtualPad(); const float deadzone = settings::GetFloat(settings::Id::StickDeadzone); const float triggerDeadzone = settings::GetFloat(settings::Id::TriggerDeadzone); if (s_gamepad) { static const std::pair map[] = { { SDL_GAMEPAD_BUTTON_SOUTH, XAMINPUT_GAMEPAD_A }, { SDL_GAMEPAD_BUTTON_EAST, XAMINPUT_GAMEPAD_B }, { SDL_GAMEPAD_BUTTON_WEST, XAMINPUT_GAMEPAD_X }, { SDL_GAMEPAD_BUTTON_NORTH, XAMINPUT_GAMEPAD_Y }, { SDL_GAMEPAD_BUTTON_BACK, XAMINPUT_GAMEPAD_BACK }, { SDL_GAMEPAD_BUTTON_START, XAMINPUT_GAMEPAD_START }, { SDL_GAMEPAD_BUTTON_LEFT_STICK, XAMINPUT_GAMEPAD_LEFT_THUMB }, { SDL_GAMEPAD_BUTTON_RIGHT_STICK, XAMINPUT_GAMEPAD_RIGHT_THUMB }, { SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, XAMINPUT_GAMEPAD_LEFT_SHOULDER }, { SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, XAMINPUT_GAMEPAD_RIGHT_SHOULDER }, { SDL_GAMEPAD_BUTTON_DPAD_UP, XAMINPUT_GAMEPAD_DPAD_UP }, { SDL_GAMEPAD_BUTTON_DPAD_DOWN, XAMINPUT_GAMEPAD_DPAD_DOWN }, { SDL_GAMEPAD_BUTTON_DPAD_LEFT, XAMINPUT_GAMEPAD_DPAD_LEFT }, { SDL_GAMEPAD_BUTTON_DPAD_RIGHT, XAMINPUT_GAMEPAD_DPAD_RIGHT }, }; for (auto& [button, bit] : map) if (bit != XAMINPUT_GAMEPAD_BACK && SDL_GetGamepadButton(s_gamepad, button)) p.buttons |= bit; padBack = SDL_GetGamepadButton(s_gamepad, SDL_GAMEPAD_BUTTON_BACK); if (PadBackForGame(padBack, SDL_GetGamepadButton(s_gamepad, SDL_GAMEPAD_BUTTON_START))) p.buttons |= XAMINPUT_GAMEPAD_BACK; auto axis = [](SDL_Gamepad* g, SDL_GamepadAxis a) { return float(SDL_GetGamepadAxis(g, a)) / 32767.0f; }; lx = axis(s_gamepad, SDL_GAMEPAD_AXIS_LEFTX); ly = -axis(s_gamepad, SDL_GAMEPAD_AXIS_LEFTY); // XInput: up is positive rx = axis(s_gamepad, SDL_GAMEPAD_AXIS_RIGHTX); ry = -axis(s_gamepad, SDL_GAMEPAD_AXIS_RIGHTY); lt = axis(s_gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER); rt = axis(s_gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER); rawX = lx; rawY = ly; ApplyDeadzone(lx, ly, deadzone); ApplyDeadzone(rx, ry, deadzone); } const float rawLt = lt, rawRt = rt; // Keyboard (digital; replaces the controller's value for what it // touches): each control's key is Settings > Controls (the Key // options), resolved by key_map.h. A key is a scancode SDL's state // array is indexed by; an out-of-range value (a hand edit beat // Load's checks) counts as not held. if (const bool* k = SDL_GetKeyboardState(nullptr)) { bool pressed[keymap::kControls] = {}; auto down = [&](settings::Id id) { int code = settings::GetInt(id); // 0: no key return code > 0 && code < SDL_SCANCODE_COUNT && k[code]; }; for (size_t i = 0; i < keymap::kControls; i++) pressed[i] = down(keymap::kEntries[i].id) || down(keymap::kEntries[i].id2); keymap::Resolved r = keymap::Resolve(pressed); p.buttons |= r.buttons; if (r.rightTrigger) rt = 1.0f; if (r.leftTrigger) lt = 1.0f; float* axes[size_t(keymap::Axis::Count)] = { &lx, &ly, &rx, &ry }; // Axis order: LX, LY, RX, RY for (size_t a = 0; a < size_t(keymap::Axis::Count); a++) if (r.axisSet[a]) *axes[a] = r.axis[a]; } p.lx = ToShort(lx); p.ly = ToShort(ly); p.rx = ToShort(rx); p.ry = ToShort(ry); p.leftTrigger = Trigger(lt, triggerDeadzone); p.rightTrigger = Trigger(rt, triggerDeadzone); if (s_gamepad) LogStick(rawX, rawY, lx, ly, rawLt, rawRt, p.leftTrigger, p.rightTrigger); if (!padSteps.empty()) fprintf(stderr, "[input] virtual pad %s: left stick raw (%+.4f, %+.4f) -> game (%d, %d), right (%d, %d); " "triggers raw (%.3f, %.3f) -> game (%u, %u); deadzones: stick %.2f, trigger %.2f%s\n", padSteps.c_str(), rawX, rawY, int(p.lx), int(p.ly), int(p.rx), int(p.ry), rawLt, rawRt, unsigned(p.leftTrigger), unsigned(p.rightTrigger), deadzone, triggerDeadzone, s_blocked.load(std::memory_order_relaxed) ? " (a menu is open: the game sees an idle pad)" : ""); // The last time anything was pressed or moved, the script's taps // included (SecondsSinceInput). { Pad scripted = p; ApplyScript(scripted); if (scripted.buttons || scripted.leftTrigger || scripted.rightTrigger || scripted.lx || scripted.ly || scripted.rx || scripted.ry || !s_inputSeen) { s_lastInput = std::chrono::steady_clock::now(); s_inputSeen = true; } } // While a menu has the input the game sees an idle pad; after it // closes, each button still held stays hidden until released, so the // press that closed the menu (B, Start, Esc) doesn't reach the game. bool blocked = s_blocked.load(std::memory_order_relaxed); if (s_wasBlocked && !blocked) s_latchedButtons = p.buttons; // A Back pressed in the menu (or the one that closed it) is never replayed. if ((blocked || s_wasBlocked) && padBack) s_backState = BackState::Swallow; s_wasBlocked = blocked; s_latchedButtons &= p.buttons; if (blocked) p = Pad{}; else p.buttons &= ~s_latchedButtons; uint16_t low, high; bool rumble; { std::lock_guard lock(s_mutex); s_pad = p; s_scriptSuppressed = blocked; rumble = s_rumbleDirty; s_rumbleDirty = false; low = s_rumbleLow; high = s_rumbleHigh; } static bool vibration = true; if (bool on = settings::GetBool(settings::Id::Vibration); on != vibration) { vibration = on; rumble = true; // stop (or restart) a rumble under way } if (rumble && s_virtualPad) { // What the game asked for and what the controller gets, on // change. Only with the test's pad: while driving the game // changes its request every frame, which would be a line a frame // in a player's last-run.log (speedbreaker.sh sets NFSMW_INPUT_LOG). static uint64_t logged = ~0ull; uint64_t now = uint64_t(low) << 17 | uint64_t(high) << 1 | (vibration ? 1u : 0u); if (now != logged) { logged = now; fprintf(stderr, "[input] rumble: game low %u, high %u; Vibration %s%s\n", unsigned(low), unsigned(high), vibration ? "on" : "off (the controller gets 0)", s_gamepad ? "" : "; no controller"); } } if (!vibration) low = high = 0; if (rumble && s_gamepad) SDL_RumbleGamepad(s_gamepad, low, high, low || high ? 5000 : 0); } } namespace { // The pad as the game sees it: devices plus scripted taps. Caller holds s_mutex. Pad CurrentPadLocked() { Pad p = s_pad; if (!s_scriptSuppressed) ApplyScript(p); return p; } // Keystrokes (XamInputGetKeystroke[Ex]): edges of buttons, triggers and // stick directions as XInput VK_PAD_* codes, with auto-repeat for the // navigation keys, generated on demand from the same state as GetState. struct KeyDef { uint16_t vk; bool repeats; bool (*down)(const Pad&); }; const KeyDef kKeys[] = { { 0x5800, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_A) != 0; } }, { 0x5801, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_B) != 0; } }, { 0x5802, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_X) != 0; } }, { 0x5803, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_Y) != 0; } }, { 0x5804, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_RIGHT_SHOULDER) != 0; } }, { 0x5805, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_LEFT_SHOULDER) != 0; } }, { 0x5806, false, [](const Pad& p) { return p.leftTrigger > 30; } }, { 0x5807, false, [](const Pad& p) { return p.rightTrigger > 30; } }, { 0x5810, true, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_DPAD_UP) != 0; } }, { 0x5811, true, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_DPAD_DOWN) != 0; } }, { 0x5812, true, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_DPAD_LEFT) != 0; } }, { 0x5813, true, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_DPAD_RIGHT) != 0; } }, { 0x5814, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_START) != 0; } }, { 0x5815, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_BACK) != 0; } }, { 0x5816, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_LEFT_THUMB) != 0; } }, { 0x5817, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_RIGHT_THUMB) != 0; } }, { 0x5820, true, [](const Pad& p) { return p.ly > 16384; } }, { 0x5821, true, [](const Pad& p) { return p.ly < -16384; } }, { 0x5822, true, [](const Pad& p) { return p.lx > 16384; } }, { 0x5823, true, [](const Pad& p) { return p.lx < -16384; } }, { 0x5830, true, [](const Pad& p) { return p.ry > 16384; } }, { 0x5831, true, [](const Pad& p) { return p.ry < -16384; } }, { 0x5832, true, [](const Pad& p) { return p.rx > 16384; } }, { 0x5833, true, [](const Pad& p) { return p.rx < -16384; } }, }; constexpr size_t kKeyCount = sizeof(kKeys) / sizeof(kKeys[0]); bool s_keyDown[kKeyCount] = {}; // Guest ns: a key held through a suspension doesn't repeat as soon as it's back. int64_t s_keyRepeatAt[kKeyCount] = {}; struct Keystroke { be virtualKey; be unicode; be flags; // 1 key down, 2 key up, 4 repeat uint8_t userIndex; uint8_t hidCode; }; static_assert(sizeof(Keystroke) == 8); constexpr uint32_t ERROR_EMPTY = 0x10D2; uint32_t NextKeystroke(Keystroke* out) { std::lock_guard lock(s_mutex); if (!(s_deviceConnected || !s_script.empty())) return ERROR_DEVICE_NOT_CONNECTED; Pad p = CurrentPadLocked(); int64_t now = guesttime::NowNs(); auto emit = [&](size_t i, uint16_t flags) { out->virtualKey = kKeys[i].vk; out->unicode = 0; out->flags = flags; out->userIndex = 0; out->hidCode = 0; return ERROR_SUCCESS; }; for (size_t i = 0; i < kKeyCount; i++) { bool down = kKeys[i].down(p); if (down != s_keyDown[i]) { s_keyDown[i] = down; s_keyRepeatAt[i] = now + 400'000'000; return emit(i, down ? 1 : 2); } } for (size_t i = 0; i < kKeyCount; i++) if (s_keyDown[i] && kKeys[i].repeats && now >= s_keyRepeatAt[i]) { s_keyRepeatAt[i] = now + 100'000'000; return emit(i, 1 | 4); } return ERROR_EMPTY; } } uint32_t XamInputGetCapabilities(uint32_t userIndex, uint32_t flags, XAMINPUT_CAPABILITIES* caps) { bool scripted = !s_script.empty(); { std::lock_guard lock(s_mutex); if ((userIndex != 0 && userIndex != 0xFF) || !(s_deviceConnected || scripted)) return ERROR_DEVICE_NOT_CONNECTED; } memset(caps, 0, sizeof(*caps)); caps->Type = XAMINPUT_DEVTYPE_GAMEPAD; caps->SubType = XAMINPUT_DEVSUBTYPE_GAMEPAD; caps->Gamepad.wButtons = ByteSwap(uint16_t(0xF3FF)); caps->Gamepad.bLeftTrigger = 0xFF; caps->Gamepad.bRightTrigger = 0xFF; for (int16_t* v : { &caps->Gamepad.sThumbLX, &caps->Gamepad.sThumbLY, &caps->Gamepad.sThumbRX, &caps->Gamepad.sThumbRY }) *v = ByteSwap(int16_t(0xFFC0)); caps->Vibration.wLeftMotorSpeed = 0xFFFF; caps->Vibration.wRightMotorSpeed = 0xFFFF; return ERROR_SUCCESS; } uint32_t XamInputGetState(uint32_t userIndex, uint32_t flags, XAMINPUT_STATE* state) { Pad p; uint32_t packet; { std::lock_guard lock(s_mutex); if ((userIndex != 0 && userIndex != 0xFF) || !(s_deviceConnected || !s_script.empty())) return ERROR_DEVICE_NOT_CONNECTED; p = CurrentPadLocked(); static Pad last; if (memcmp(&p, &last, sizeof(p)) != 0) { s_packet++; last = p; } packet = s_packet; } state->dwPacketNumber = ByteSwap(packet); state->Gamepad.wButtons = ByteSwap(p.buttons); state->Gamepad.bLeftTrigger = p.leftTrigger; state->Gamepad.bRightTrigger = p.rightTrigger; state->Gamepad.sThumbLX = ByteSwap(p.lx); state->Gamepad.sThumbLY = ByteSwap(p.ly); state->Gamepad.sThumbRX = ByteSwap(p.rx); state->Gamepad.sThumbRY = ByteSwap(p.ry); return ERROR_SUCCESS; } uint32_t XamInputSetState(uint32_t userIndex, uint32_t flags, XAMINPUT_VIBRATION* vibration) { std::lock_guard lock(s_mutex); if ((userIndex != 0 && userIndex != 0xFF) || !(s_deviceConnected || !s_script.empty())) return ERROR_DEVICE_NOT_CONNECTED; s_rumbleLow = ByteSwap(vibration->wLeftMotorSpeed); s_rumbleHigh = ByteSwap(vibration->wRightMotorSpeed); s_rumbleDirty = true; return ERROR_SUCCESS; } // Xenia's semantics: user 0xFF ("any") is pinned to user 0; queries for // non-gamepad devices report none connected. uint32_t XamInputGetKeystroke(uint32_t userIndex, uint32_t flags, Keystroke* keystroke) { if (!keystroke) return 0xA0; // ERROR_BAD_ARGUMENTS if ((flags & 0xFF) && (flags & 1) == 0) return ERROR_DEVICE_NOT_CONNECTED; if ((userIndex & 0xFF) != 0xFF && userIndex != 0 && !(flags & 0x40000000)) return ERROR_DEVICE_NOT_CONNECTED; return NextKeystroke(keystroke); } uint32_t XamInputGetKeystrokeEx(be* userIndex, uint32_t flags, Keystroke* keystroke) { uint32_t result = XamInputGetKeystroke(userIndex ? uint32_t(*userIndex) : 0xFF, flags, keystroke); if (result == ERROR_SUCCESS && userIndex) *userIndex = 0; return result; } GUEST_FUNCTION_HOOK(__imp__XamInputGetCapabilities, XamInputGetCapabilities); GUEST_FUNCTION_HOOK(__imp__XamInputGetState, XamInputGetState); GUEST_FUNCTION_HOOK(__imp__XamInputSetState, XamInputSetState); GUEST_FUNCTION_HOOK(__imp__XamInputGetKeystrokeEx, XamInputGetKeystrokeEx);