/* Crafter®.Graphics Copyright (C) 2026 Catcrafts® Catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND #include #include #include #include #include #include #include #include #include #endif #ifdef CRAFTER_GRAPHICS_WINDOW_WIN32 #define COBJMACROS #define WIN32_LEAN_AND_MEAN #define NOMINMAX #include #include #include #include #endif module Crafter.Graphics; import std; using namespace Crafter; // ──────────────────────────────────────────────────────────────────── // Linux backend (libudev + libevdev) // ──────────────────────────────────────────────────────────────────── #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND namespace { // Per-device runtime state. The public `Gamepad::Device` carries the // identity + polled state + events; this struct holds the libevdev / // fd / mapping tables we need to drain events from. struct LinuxBackend { libevdev* evdev = nullptr; int fd = -1; // Per-axis calibration (libevdev's min/max so we can normalize). // Indexed by Gamepad::Axis. -1 in `code` means "this axis isn't // exposed by this device". struct AxisInfo { int code = -1; // kernel ABS_* code, or -1 int min = 0; int max = 0; float flat = 0.0f; // deadzone hint (raw units) bool invertY = false; // negate output (stick Y by convention) }; AxisInfo axisInfo[(std::size_t)Crafter::Gamepad::Axis::Max]; // FF effect id from EVIOCSFF, -1 if no rumble support / not active. int ffEffectId = -1; std::chrono::steady_clock::time_point rumbleExpiry{}; }; std::unordered_map> g_backends; udev* g_udev = nullptr; udev_monitor* g_monitor = nullptr; bool g_initialized = false; std::uint32_t SyspathToId(const char* syspath) { // FNV-1a 32-bit, stable across runs for the same physical device. std::uint32_t h = 0x811C9DC5u; for (const char* p = syspath; *p; ++p) { h ^= static_cast(*p); h *= 0x01000193u; } // Reserve 0 as "unmapped". return h == 0 ? 1 : h; } // BTN_* → Gamepad::Button. -1 = not a button we care about. int ButtonFromKernel(int code) { using B = Crafter::Gamepad::Button; switch (code) { case BTN_SOUTH: return (int)B::South; case BTN_EAST: return (int)B::East; case BTN_NORTH: return (int)B::North; case BTN_WEST: return (int)B::West; case BTN_TL: return (int)B::LeftBumper; case BTN_TR: return (int)B::RightBumper; case BTN_TL2: return (int)B::LeftTrigger; case BTN_TR2: return (int)B::RightTrigger; case BTN_SELECT: return (int)B::Select; case BTN_START: return (int)B::Start; case BTN_MODE: return (int)B::Home; case BTN_THUMBL: return (int)B::LeftStickClick; case BTN_THUMBR: return (int)B::RightStickClick; case BTN_DPAD_UP: return (int)B::DPadUp; case BTN_DPAD_DOWN: return (int)B::DPadDown; case BTN_DPAD_LEFT: return (int)B::DPadLeft; case BTN_DPAD_RIGHT: return (int)B::DPadRight; default: return -1; } } // Populate `bp->axisInfo` from libevdev. Sticks use ABS_X/Y/RX/RY; // triggers prefer ABS_Z/RZ but fall back to ABS_BRAKE/GAS. void DiscoverAxes(LinuxBackend* bp) { using A = Crafter::Gamepad::Axis; auto setup = [&](A which, int code, bool invertY = false) { if (!libevdev_has_event_code(bp->evdev, EV_ABS, code)) return; auto* info = libevdev_get_abs_info(bp->evdev, code); if (!info) return; auto& slot = bp->axisInfo[(std::size_t)which]; slot.code = code; slot.min = info->minimum; slot.max = info->maximum; slot.flat = (float)info->flat; slot.invertY = invertY; }; setup(A::LeftStickX, ABS_X); setup(A::LeftStickY, ABS_Y, /*invertY=*/true); setup(A::RightStickX, ABS_RX); setup(A::RightStickY, ABS_RY, /*invertY=*/true); // Trigger axis: Z/RZ on Xbox, BRAKE/GAS on some others. if (libevdev_has_event_code(bp->evdev, EV_ABS, ABS_Z)) { setup(A::LeftTrigger, ABS_Z); } else if (libevdev_has_event_code(bp->evdev, EV_ABS, ABS_BRAKE)) { setup(A::LeftTrigger, ABS_BRAKE); } if (libevdev_has_event_code(bp->evdev, EV_ABS, ABS_RZ)) { setup(A::RightTrigger, ABS_RZ); } else if (libevdev_has_event_code(bp->evdev, EV_ABS, ABS_GAS)) { setup(A::RightTrigger, ABS_GAS); } } // Normalize a raw axis value to -1..1 (sticks) or 0..1 (triggers). float NormalizeAxis(const LinuxBackend::AxisInfo& info, int raw, bool isTrigger) { if (info.code < 0 || info.max == info.min) return 0.0f; float v; if (isTrigger) { v = float(raw - info.min) / float(info.max - info.min); if (v < 0.0f) v = 0.0f; if (v > 1.0f) v = 1.0f; } else { float mid = 0.5f * (info.max + info.min); float halfR = 0.5f * (info.max - info.min); v = (raw - mid) / halfR; if (v < -1.0f) v = -1.0f; if (v > 1.0f) v = 1.0f; if (info.invertY) v = -v; } return v; } bool IsGamepadDevice(libevdev* evdev) { // A real gamepad reports BTN_GAMEPAD (a kernel alias for BTN_SOUTH // when the device is classified as a gamepad). This filters out // keyboards/mice/touchpads/tablets that share the evdev interface. return libevdev_has_event_type(evdev, EV_KEY) && libevdev_has_event_code(evdev, EV_KEY, BTN_GAMEPAD); } Crafter::Gamepad::Device* OpenDevice(const char* devnode, const char* syspath, const char* name) { int fd = ::open(devnode, O_RDWR | O_NONBLOCK); if (fd < 0) { // Read-only is fine if we just can't rumble. fd = ::open(devnode, O_RDONLY | O_NONBLOCK); if (fd < 0) return nullptr; } libevdev* evdev = nullptr; if (libevdev_new_from_fd(fd, &evdev) < 0) { ::close(fd); return nullptr; } if (!IsGamepadDevice(evdev)) { libevdev_free(evdev); ::close(fd); return nullptr; } auto dev = std::make_unique(); dev->id = SyspathToId(syspath); dev->name = name ? name : libevdev_get_name(evdev); auto bp = std::make_unique(); bp->evdev = evdev; bp->fd = fd; DiscoverAxes(bp.get()); auto* raw = dev.get(); g_backends[raw] = std::move(bp); Crafter::Gamepad::connected.push_back(std::move(dev)); return raw; } void CloseDevice(Crafter::Gamepad::Device* dev) { auto it = g_backends.find(dev); if (it != g_backends.end()) { if (it->second->evdev) libevdev_free(it->second->evdev); if (it->second->fd >= 0) ::close(it->second->fd); g_backends.erase(it); } auto& v = Crafter::Gamepad::connected; for (auto vi = v.begin(); vi != v.end(); ++vi) { if (vi->get() == dev) { v.erase(vi); break; } } } void EnumerateExisting() { udev_enumerate* en = udev_enumerate_new(g_udev); if (!en) return; udev_enumerate_add_match_subsystem(en, "input"); udev_enumerate_scan_devices(en); udev_list_entry* list = udev_enumerate_get_list_entry(en); udev_list_entry* entry; udev_list_entry_foreach(entry, list) { const char* syspath = udev_list_entry_get_name(entry); udev_device* d = udev_device_new_from_syspath(g_udev, syspath); if (!d) continue; const char* devnode = udev_device_get_devnode(d); const char* sysname = udev_device_get_sysname(d); if (devnode && sysname && std::strncmp(sysname, "event", 5) == 0) { const char* name = udev_device_get_sysattr_value( udev_device_get_parent_with_subsystem_devtype(d, "input", nullptr), "name"); auto* dev = OpenDevice(devnode, syspath, name); if (dev) Crafter::Gamepad::onConnected.Invoke(dev); } udev_device_unref(d); } udev_enumerate_unref(en); } void HandleHotplugEvent(udev_device* d) { const char* action = udev_device_get_action(d); const char* devnode = udev_device_get_devnode(d); const char* sysname = udev_device_get_sysname(d); const char* syspath = udev_device_get_syspath(d); if (!action || !devnode || !sysname || !syspath) return; if (std::strncmp(sysname, "event", 5) != 0) return; if (std::strcmp(action, "add") == 0) { const char* name = udev_device_get_sysattr_value( udev_device_get_parent_with_subsystem_devtype(d, "input", nullptr), "name"); auto* dev = OpenDevice(devnode, syspath, name); if (dev) Crafter::Gamepad::onConnected.Invoke(dev); } else if (std::strcmp(action, "remove") == 0) { std::uint32_t id = SyspathToId(syspath); for (auto& up : Crafter::Gamepad::connected) { if (up->id == id) { Crafter::Gamepad::Device* dev = up.get(); Crafter::Gamepad::onDisconnected.Invoke(dev); CloseDevice(dev); return; } } } } void DrainHotplug() { if (!g_monitor) return; for (;;) { udev_device* d = udev_monitor_receive_device(g_monitor); if (!d) break; HandleHotplugEvent(d); udev_device_unref(d); } } void DrainDeviceEvents(Crafter::Gamepad::Device* dev, LinuxBackend* bp) { using A = Crafter::Gamepad::Axis; using B = Crafter::Gamepad::Button; // Walk libevdev's internal queue; SYNC means we missed events and // need to resync the state. We treat the synced events the same // way as normal events — net effect is the polled state ends up // correct and the event stream reports the deltas. input_event ev; int rc; while ((rc = libevdev_next_event(bp->evdev, LIBEVDEV_READ_FLAG_NORMAL | LIBEVDEV_READ_FLAG_SYNC, &ev)) >= 0) { if (rc == LIBEVDEV_READ_STATUS_SYNC) continue; // handled in next iter if (ev.type == EV_KEY) { int b = ButtonFromKernel(ev.code); if (b < 0) continue; bool pressed = (ev.value != 0); if (dev->buttons[b] == pressed) continue; dev->buttons[b] = pressed; if (pressed) dev->onButtonDown.Invoke((B)b); else dev->onButtonUp .Invoke((B)b); } else if (ev.type == EV_ABS) { // ABS_HAT0X / ABS_HAT0Y emulate the D-pad on some // controllers. Translate to the four DPad buttons so // bindings can be uniform. if (ev.code == ABS_HAT0X) { bool left = ev.value < 0; bool right = ev.value > 0; auto edge = [&](int idx, bool now) { if (dev->buttons[idx] == now) return; dev->buttons[idx] = now; if (now) dev->onButtonDown.Invoke((B)idx); else dev->onButtonUp .Invoke((B)idx); }; edge((int)B::DPadLeft, left); edge((int)B::DPadRight, right); continue; } if (ev.code == ABS_HAT0Y) { bool up = ev.value < 0; bool down = ev.value > 0; auto edge = [&](int idx, bool now) { if (dev->buttons[idx] == now) return; dev->buttons[idx] = now; if (now) dev->onButtonDown.Invoke((B)idx); else dev->onButtonUp .Invoke((B)idx); }; edge((int)B::DPadUp, up); edge((int)B::DPadDown, down); continue; } // Look up which Gamepad::Axis this kernel code maps to // (by scanning the calibration table — sticks first, // triggers second). for (std::size_t i = 0; i < (std::size_t)A::Max; ++i) { if (bp->axisInfo[i].code != ev.code) continue; bool isTrigger = (i == (std::size_t)A::LeftTrigger || i == (std::size_t)A::RightTrigger); float v = NormalizeAxis(bp->axisInfo[i], ev.value, isTrigger); if (dev->axes[i] == v) break; dev->axes[i] = v; dev->onAxisChanged.Invoke((A)i); break; } } } } void ExpireRumbles() { auto now = std::chrono::steady_clock::now(); for (auto& [dev, bp] : g_backends) { if (bp->ffEffectId >= 0 && now >= bp->rumbleExpiry) { input_event stop{}; stop.type = EV_FF; stop.code = bp->ffEffectId; stop.value = 0; if (bp->fd >= 0) ::write(bp->fd, &stop, sizeof(stop)); ioctl(bp->fd, EVIOCRMFF, bp->ffEffectId); bp->ffEffectId = -1; } } } void EnsureInit() { if (g_initialized) return; g_initialized = true; g_udev = udev_new(); if (!g_udev) return; g_monitor = udev_monitor_new_from_netlink(g_udev, "udev"); if (g_monitor) { udev_monitor_filter_add_match_subsystem_devtype(g_monitor, "input", nullptr); udev_monitor_enable_receiving(g_monitor); } EnumerateExisting(); } } void Crafter::Gamepad::Tick() { EnsureInit(); DrainHotplug(); for (auto& up : Gamepad::connected) { Device* dev = up.get(); auto it = g_backends.find(dev); if (it == g_backends.end()) continue; DrainDeviceEvents(dev, it->second.get()); } ExpireRumbles(); } void Crafter::Gamepad::Rumble(Device& dev, float low, float high, std::chrono::milliseconds duration) { auto it = g_backends.find(&dev); if (it == g_backends.end()) return; LinuxBackend* bp = it->second.get(); if (bp->fd < 0) return; // Cancel any active effect first. if (bp->ffEffectId >= 0) { input_event stop{}; stop.type = EV_FF; stop.code = bp->ffEffectId; stop.value = 0; ::write(bp->fd, &stop, sizeof(stop)); ioctl(bp->fd, EVIOCRMFF, bp->ffEffectId); bp->ffEffectId = -1; } if (duration.count() <= 0 || (low <= 0.0f && high <= 0.0f)) return; auto clamp01 = [](float v) { return v < 0 ? 0.0f : v > 1.0f ? 1.0f : v; }; ff_effect effect{}; effect.type = FF_RUMBLE; effect.id = -1; effect.u.rumble.strong_magnitude = (std::uint16_t)(clamp01(low) * 0xFFFFu); effect.u.rumble.weak_magnitude = (std::uint16_t)(clamp01(high) * 0xFFFFu); effect.replay.length = (std::uint16_t)std::min(duration.count(), 0xFFFF); if (ioctl(bp->fd, EVIOCSFF, &effect) < 0) return; bp->ffEffectId = effect.id; bp->rumbleExpiry = std::chrono::steady_clock::now() + duration; input_event play{}; play.type = EV_FF; play.code = effect.id; play.value = 1; ::write(bp->fd, &play, sizeof(play)); } #endif // ──────────────────────────────────────────────────────────────────── // Win32 backend (Windows.Gaming.Input via C ABI) // ──────────────────────────────────────────────────────────────────── #ifdef CRAFTER_GRAPHICS_WINDOW_WIN32 namespace { using IGamepad = __x_ABI_CWindows_CGaming_CInput_CIGamepad; using IGamepadStatics = __x_ABI_CWindows_CGaming_CInput_CIGamepadStatics; using GamepadAddedHandler = __FIEventHandler_1_Windows__CGaming__CInput__CGamepad; using GamepadRemovedHandler = __FIEventHandler_1_Windows__CGaming__CInput__CGamepad; using GamepadReading = __x_ABI_CWindows_CGaming_CInput_CGamepadReading; using GamepadVibration = __x_ABI_CWindows_CGaming_CInput_CGamepadVibration; using GamepadButtons = __x_ABI_CWindows_CGaming_CInput_CGamepadButtons; IGamepadStatics* g_statics = nullptr; EventRegistrationToken g_addedToken {}; EventRegistrationToken g_removedToken {}; bool g_initialized = false; // Cross-thread queue. WGI fires Added/Removed on a thread pool; the // event loop drains this on the main thread inside Tick(). struct QueueEntry { bool added; IGamepad* gamepad; // AddRef'd }; std::mutex g_queueMutex; std::vector g_queue; // Maps a live IGamepad* to its Crafter::Gamepad::Device. Pointer is // stable from WGI's perspective for the lifetime of the connection. std::unordered_map g_byGamepad; std::uint32_t PointerToId(IGamepad* g) { // Hash the pointer for a stable-while-connected id. (NonRoamableId // would be more durable across reconnects but adds another COM // round-trip; pointer hash is fine for v1.) std::uintptr_t v = reinterpret_cast(g); v = (v ^ (v >> 16)) * 0x85EBCA6Bu; v = (v ^ (v >> 13)) * 0xC2B2AE35u; v = v ^ (v >> 16); std::uint32_t out = (std::uint32_t)v; return out == 0 ? 1 : out; } // ── Delegate vtable for GamepadAdded / GamepadRemoved ─────────── struct AddedDelegate { // The first member matches the C ABI vtable layout. Cast yields // a valid GamepadAddedHandler*. void* vtbl; LONG refCount; bool isAdded; }; extern GamepadAddedHandler::__GamepadAddedHandlerVtbl g_addedVtbl; extern GamepadAddedHandler::__GamepadAddedHandlerVtbl g_removedVtbl; HRESULT STDMETHODCALLTYPE DelegateQI(GamepadAddedHandler* This, REFIID riid, void** out) { if (IsEqualIID(riid, &IID_IUnknown) || IsEqualIID(riid, &IID_IAgileObject) || IsEqualIID(riid, &IID___FIEventHandler_1_Windows__CGaming__CInput__CGamepad)) { *out = This; This->lpVtbl->AddRef(This); return S_OK; } *out = nullptr; return E_NOINTERFACE; } ULONG STDMETHODCALLTYPE DelegateAddRef(GamepadAddedHandler* This) { AddedDelegate* d = reinterpret_cast(This); return (ULONG)InterlockedIncrement(&d->refCount); } ULONG STDMETHODCALLTYPE DelegateRelease(GamepadAddedHandler* This) { AddedDelegate* d = reinterpret_cast(This); LONG r = InterlockedDecrement(&d->refCount); return (ULONG)r; } HRESULT STDMETHODCALLTYPE DelegateInvoke(GamepadAddedHandler* This, IInspectable* sender, IGamepad* gamepad) { AddedDelegate* d = reinterpret_cast(This); if (gamepad) IGamepad_AddRef(gamepad); { std::lock_guard lock(g_queueMutex); g_queue.push_back({ d->isAdded, gamepad }); } return S_OK; } GamepadAddedHandler::__GamepadAddedHandlerVtbl g_addedVtbl = { DelegateQI, DelegateAddRef, DelegateRelease, DelegateInvoke }; GamepadAddedHandler::__GamepadAddedHandlerVtbl g_removedVtbl = { DelegateQI, DelegateAddRef, DelegateRelease, DelegateInvoke }; AddedDelegate g_addedDelegate { &g_addedVtbl, 1, true }; AddedDelegate g_removedDelegate { &g_removedVtbl, 1, false }; // Translate GamepadReading.Buttons bitfield to our Button enum. bool ReadingHas(const GamepadReading& r, GamepadButtons b) { return (r.Buttons & b) != 0; } void ApplyReading(Crafter::Gamepad::Device* dev, const GamepadReading& r) { using B = Crafter::Gamepad::Button; using A = Crafter::Gamepad::Axis; struct Map { B which; GamepadButtons mask; }; static const Map buttonMap[] = { { B::DPadUp, GamepadButtons_DPadUp }, { B::DPadDown, GamepadButtons_DPadDown }, { B::DPadLeft, GamepadButtons_DPadLeft }, { B::DPadRight, GamepadButtons_DPadRight }, { B::Start, GamepadButtons_Menu }, { B::Select, GamepadButtons_View }, { B::LeftStickClick, GamepadButtons_LeftThumbstick }, { B::RightStickClick,GamepadButtons_RightThumbstick }, { B::LeftBumper, GamepadButtons_LeftShoulder }, { B::RightBumper, GamepadButtons_RightShoulder }, { B::South, GamepadButtons_A }, { B::East, GamepadButtons_B }, { B::West, GamepadButtons_X }, { B::North, GamepadButtons_Y }, }; for (const Map& m : buttonMap) { bool now = ReadingHas(r, m.mask); bool& cur = dev->buttons[(std::size_t)m.which]; if (cur == now) continue; cur = now; if (now) dev->onButtonDown.Invoke(m.which); else dev->onButtonUp .Invoke(m.which); } // WGI doesn't have a Home/Guide button in the standard Gamepad // interface (it's reserved for the OS). Leave dev->buttons[Home] // at false. auto axis = [&](A which, float v) { float& cur = dev->axes[(std::size_t)which]; if (cur == v) return; cur = v; dev->onAxisChanged.Invoke(which); }; axis(A::LeftStickX, (float)r.LeftThumbstickX); axis(A::LeftStickY, (float)r.LeftThumbstickY); axis(A::RightStickX, (float)r.RightThumbstickX); axis(A::RightStickY, (float)r.RightThumbstickY); axis(A::LeftTrigger, (float)r.LeftTrigger); axis(A::RightTrigger, (float)r.RightTrigger); // Triggers also expose digital "pressed" buttons at a threshold. constexpr float kTriggerThreshold = 0.1f; auto trigBtn = [&](B which, double value) { bool now = value >= kTriggerThreshold; bool& cur = dev->buttons[(std::size_t)which]; if (cur == now) return; cur = now; if (now) dev->onButtonDown.Invoke(which); else dev->onButtonUp .Invoke(which); }; trigBtn(B::LeftTrigger, r.LeftTrigger); trigBtn(B::RightTrigger, r.RightTrigger); } void EnsureInit() { if (g_initialized) return; g_initialized = true; // RoInitialize is required once per thread; ignore RPC_E_CHANGED_MODE // (some hosts initialize as STA, that's fine for WGI). RoInitialize(RO_INIT_MULTITHREADED); HSTRING_HEADER header; HSTRING className; const wchar_t* name = RuntimeClass_Windows_Gaming_Input_Gamepad; if (FAILED(WindowsCreateStringReference(name, (UINT32)wcslen(name), &header, &className))) return; if (FAILED(RoGetActivationFactory(className, &IID___x_ABI_CWindows_CGaming_CInput_CIGamepadStatics, (void**)&g_statics))) return; __x_ABI_CWindows_CGaming_CInput_CIGamepadStatics_add_GamepadAdded( g_statics, reinterpret_cast(&g_addedDelegate), &g_addedToken); __x_ABI_CWindows_CGaming_CInput_CIGamepadStatics_add_GamepadRemoved( g_statics, reinterpret_cast(&g_removedDelegate), &g_removedToken); } } void Crafter::Gamepad::Tick() { EnsureInit(); if (!g_statics) return; // Drain the cross-thread connect/disconnect queue. std::vector pending; { std::lock_guard lock(g_queueMutex); pending.swap(g_queue); } for (QueueEntry& e : pending) { if (!e.gamepad) continue; if (e.added) { if (g_byGamepad.find(e.gamepad) != g_byGamepad.end()) { IGamepad_Release(e.gamepad); continue; } auto dev = std::make_unique(); dev->id = PointerToId(e.gamepad); dev->name = "Gamepad"; auto* raw = dev.get(); g_byGamepad[e.gamepad] = raw; connected.push_back(std::move(dev)); onConnected.Invoke(raw); // keep AddRef'd; released on disconnect } else { auto it = g_byGamepad.find(e.gamepad); if (it != g_byGamepad.end()) { Crafter::Gamepad::Device* raw = it->second; onDisconnected.Invoke(raw); for (auto vi = connected.begin(); vi != connected.end(); ++vi) { if (vi->get() == raw) { connected.erase(vi); break; } } g_byGamepad.erase(it); } IGamepad_Release(e.gamepad); } } // Poll each connected gamepad. for (auto& [gp, dev] : g_byGamepad) { GamepadReading reading{}; if (FAILED(__x_ABI_CWindows_CGaming_CInput_CIGamepad_GetCurrentReading(gp, &reading))) continue; ApplyReading(dev, reading); } } void Crafter::Gamepad::Rumble(Device& dev, float low, float high, std::chrono::milliseconds /*duration*/) { // WGI vibration is "set and forget" — no duration. Caller can stop // by re-calling with zero magnitudes. We honor 0 duration as stop. IGamepad* gp = nullptr; for (auto& [g, d] : g_byGamepad) { if (d == &dev) { gp = g; break; } } if (!gp) return; auto clamp01 = [](float v) { return v < 0 ? 0.0f : v > 1.0f ? 1.0f : v; }; GamepadVibration vib{}; vib.LeftMotor = clamp01(low); vib.RightMotor = clamp01(high); __x_ABI_CWindows_CGaming_CInput_CIGamepad_put_Vibration(gp, vib); } #endif Crafter::Gamepad::Device* Crafter::Gamepad::FindById(std::uint32_t id) { for (auto& up : connected) { if (up->id == id) return up.get(); } return nullptr; }