Files
ReSkate/Launcher/gui_gamepad.cpp
T
NorthDroplet 360338e6e1 Launcher: controller navigation and the Steam Deck trackpad as a mouse
The launcher read no controller at all. Started from Steam (Desktop or Game
Mode on a Steam Deck), Steam Input applies the game's gamepad layout to it,
where the right trackpad is the right stick and its click is R3, so the
trackpad did nothing; only the touch screen, which arrives as mouse
messages, worked.

- Every XInput pad is merged (Xbox, Steam Input's virtual pad), else a
  DualShock 4 / DualSense is read over HID with the reader the in-game menu
  already uses, now its own library (dingosdk_playstation_input).
- D-pad and left stick move ImGui's focus, outlined in the launcher's blue;
  A / Cross presses. The hand-drawn tiles, switches and menu buttons take
  part in navigation (ImGuiButtonFlags_EnableNav).
- B / Circle closes an open combo or menu and leaves a text field, and is
  Escape otherwise, which the launcher's pages already go back on. Escape
  now also closes the mod overviews and answers Cancel in the uninstall and
  replace confirmations; closing an overview no longer also leaves the page.
- The right stick (the trackpad) moves a pointer that ImGui draws, and R3
  (the trackpad's click) is the left button. The system cursor follows it;
  a mouse move or a touch elsewhere takes the pointer back.
- The ImGui Win32 backend's own gamepad code (slot 0 only) is compiled out
  so it cannot fight the launcher's events; nothing else used it.
2026-10-05 14:27:08 +00:00

95 lines
3.9 KiB
C++

#include "gui_internal.h"
#include "Extension/UI/Overlay/playstation_input.h"
#include <Xinput.h>
#include <cstdlib>
#include <cstring>
namespace dingosdk::launcher_gui::detail {
namespace {
using GetState = DWORD(WINAPI*)(DWORD, XINPUT_STATE*);
// Loaded on first use like the overlay does: a PC without the XInput runtime
// still gets a launcher, just without Xbox pads.
GetState xinput_get_state() {
static const auto get_state = []() -> GetState {
for (const auto name : {L"xinput1_4.dll", L"xinput1_3.dll", L"xinput9_1_0.dll"}) {
const auto module = LoadLibraryExW(name, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
if (!module) continue;
if (const auto entry = GetProcAddress(module, "XInputGetState")) {
GetState function{};
static_assert(sizeof(function) == sizeof(entry));
std::memcpy(&function, &entry, sizeof(function));
return function;
}
FreeLibrary(module);
}
return nullptr;
}();
return get_state;
}
// Asking an empty XInput slot is slow (it looks for a device), so only slots
// with a pad are read every frame, and one empty slot is tried a second.
struct Slots {
unsigned connected{(1u << XUSER_MAX_COUNT) - 1}; // all four tried on the first frame
DWORD next_probe{};
ULONGLONG probe_at{};
};
} // namespace
PadState read_pad() {
static Slots slots;
PadState pad;
if (const auto get_state = xinput_get_state()) {
const auto now = GetTickCount64();
unsigned probe{};
if (now >= slots.probe_at) {
slots.probe_at = now + 1000;
for (DWORD tried = 0; tried < XUSER_MAX_COUNT && !probe; ++tried) {
const DWORD slot = (slots.next_probe + tried) % XUSER_MAX_COUNT;
if (!(slots.connected & (1u << slot))) probe = 1u << slot;
}
slots.next_probe = (slots.next_probe + 1) % XUSER_MAX_COUNT;
}
// Steam, DS4Windows and ViGEm put virtual pads in any slot, and an idle
// one may sit ahead of the pad in use, so every pad is merged.
for (DWORD slot = 0; slot < XUSER_MAX_COUNT; ++slot) {
const unsigned bit = 1u << slot;
if (!((slots.connected | probe) & bit)) continue;
XINPUT_STATE sample{};
if (get_state(slot, &sample) != ERROR_SUCCESS) { slots.connected &= ~bit; continue; }
slots.connected |= bit;
const auto& gamepad = sample.Gamepad;
pad.connected = true;
pad.buttons |= gamepad.wButtons;
pad.left_trigger = std::max(pad.left_trigger, gamepad.bLeftTrigger);
pad.right_trigger = std::max(pad.right_trigger, gamepad.bRightTrigger);
const auto magnitude = [](SHORT x, SHORT y) { return std::abs(int(x)) + std::abs(int(y)); };
if (magnitude(gamepad.sThumbLX, gamepad.sThumbLY) > magnitude(pad.left_x, pad.left_y)) {
pad.left_x = gamepad.sThumbLX; pad.left_y = gamepad.sThumbLY;
}
if (magnitude(gamepad.sThumbRX, gamepad.sThumbRY) > magnitude(pad.right_x, pad.right_y)) {
pad.right_x = gamepad.sThumbRX; pad.right_y = gamepad.sThumbRY;
}
}
}
// XInput wins whenever a pad is there: DS4Windows and Steam Input leave the
// PlayStation pad visible beside their XInput twin, and both would double up.
if (pad.connected) return pad;
const auto playstation = overlay::detail::read_playstation_pads();
if (!playstation.available) return {};
const auto& sony = playstation.pad;
pad.connected = true;
pad.buttons = sony.buttons;
pad.left_trigger = sony.left_trigger; pad.right_trigger = sony.right_trigger;
pad.left_x = sony.left_x; pad.left_y = sony.left_y;
pad.right_x = sony.right_x; pad.right_y = sony.right_y;
return pad;
}
} // namespace dingosdk::launcher_gui::detail