mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-28 09:06:52 +01:00
Plugins: joysticks are read on a thread of their own, with all 8 radio channels, and a plugin picks which one
reskate.input.joystick() scanned Windows' 16 winmm slots inside the plugin's call whenever no
controller was cached; on some PCs empty slots made one scan take over 50 ms, and the FPV drone
was stopped for its tick running long after switching to a radio with none plugged in. A thread
now finds the controllers (every 2 s) and reads them (250 a second) through DirectInput, only
while a plugin has asked in the last 2 s, and a call copies its last reading.
DirectInput gives the eight axes a radio sends, where winmm gave six: axes are now X, Y, Z, Rx,
Ry, Rz and the two sliders. A plugin that saved axis numbers should let players set them again.
reskate.input.joysticks() lists every controller plugged in ({device, name}), and
reskate.input.joystick(device) reads that one, so a player with a gamepad and a radio picks which
flies; with no argument it still reads the first, a radio first. The list and the device field
follow neekgate's plugin-joystick-pick branch.
This commit is contained in:
1 parent
be7a72d816
commit
6bad0e95d2
7 files changed
+292
-98
No files matched your search
@@ -524,13 +524,16 @@ struct FakeGame final : Game {
|
||||
std::optional<FlightInput> flight_input(std::string_view) override { return FlightInput{1, 0, 0.5f, 3, 4, true}; }
|
||||
std::optional<PadInput> pad = PadInput{0.25f, 0, 0, 0, 0, 1, 0x1000, false};
|
||||
std::optional<PadInput> gamepad() override { return pad; }
|
||||
std::optional<StickInput> joystick() override {
|
||||
std::optional<StickInput> joystick(int device) override {
|
||||
if (device > 1) return std::nullopt;
|
||||
StickInput stick;
|
||||
stick.axes = {0, -1, 0.5f, 0, 0, 0};
|
||||
stick.axes = {0, -1, 0.5f, 0, 0, 0, 0.25f, -0.75f};
|
||||
stick.buttons = 0x4;
|
||||
stick.name = "Radio";
|
||||
stick.name = device == 1 ? "Pad" : "Radio";
|
||||
stick.device = device == 1 ? 1 : 0;
|
||||
return stick;
|
||||
}
|
||||
std::vector<StickDevice> joysticks() override { return {{0, "Radio"}, {1, "Pad"}}; }
|
||||
std::string capture_input(std::string_view plugin, bool on) override {
|
||||
calls.push_back(std::format("capture {} {}", plugin, on ? "on" : "off"));
|
||||
return {};
|
||||
@@ -1347,6 +1350,10 @@ void a_permission_must_be_declared() {
|
||||
assert(not ok and err:find("needs the \"board\" permission"), err)
|
||||
ok, err = pcall(reskate.board.parameters, "deck")
|
||||
assert(not ok and err:find("needs the \"board\" permission"), err)
|
||||
ok, err = pcall(reskate.input.joysticks)
|
||||
assert(not ok and err:find("needs the \"input\" permission"), err)
|
||||
ok, err = pcall(reskate.input.joystick, 1)
|
||||
assert(not ok and err:find("needs the \"input\" permission"), err)
|
||||
reskate.console.print("never")
|
||||
)")});
|
||||
stopped(host, "bare", "raised an error");
|
||||
@@ -3058,6 +3065,12 @@ void the_wider_set_reaches_the_game() {
|
||||
assert(pad.buttons.a and not pad.buttons.b and pad.left_x == 0.25 and pad.right_trigger == 1, "the gamepad")
|
||||
local stick = reskate.input.joystick()
|
||||
assert(stick.axes[2] == -1 and stick.buttons[3] and not stick.buttons[1] and stick.name == "Radio", "the radio")
|
||||
assert(#stick.axes == 8 and stick.axes[7] == 0.25 and stick.axes[8] == -0.75, "a radio's channels 7 and 8")
|
||||
assert(stick.device == 0, "the radio's number")
|
||||
local list = reskate.input.joysticks()
|
||||
assert(#list == 2 and list[2].device == 1 and list[2].name == "Pad", "every controller")
|
||||
assert(reskate.input.joystick(1).name == "Pad" and reskate.input.joystick(5) == nil, "a chosen controller")
|
||||
assert(not pcall(reskate.input.joystick, -1) and not pcall(reskate.input.joystick, 1.5), "a controller's number")
|
||||
assert(reskate.input.capture(true))
|
||||
reskate.camera.release()
|
||||
assert(reskate.skater.freeze(true))
|
||||
|
||||
@@ -2700,17 +2700,26 @@ struct WorldApi {
|
||||
lua_setfield(L, -2, "playstation");
|
||||
return 1;
|
||||
}
|
||||
// joystick(): the first controller (a radio first); joystick(device): that one, a number
|
||||
// joysticks() lists.
|
||||
static int input_joystick(lua_State* L) {
|
||||
auto& plugin = plugin_of(L);
|
||||
need(L, plugin, "input", "reskate.input.joystick");
|
||||
const auto stick = game(plugin).joystick();
|
||||
int device = -1;
|
||||
if (!lua_isnoneornil(L, 1)) {
|
||||
const double wanted = luaL_checknumber(L, 1);
|
||||
if (!(wanted >= 0 && wanted < 16) || wanted != static_cast<double>(static_cast<int>(wanted)))
|
||||
luaL_argerror(L, 1, "a controller's number, as reskate.input.joysticks() lists it");
|
||||
device = static_cast<int>(wanted);
|
||||
}
|
||||
const auto stick = game(plugin).joystick(device);
|
||||
if (!stick) {
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
lua_createtable(L, 0, 3);
|
||||
lua_createtable(L, 6, 0);
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
lua_createtable(L, 0, 4);
|
||||
lua_createtable(L, static_cast<int>(stick->axes.size()), 0);
|
||||
for (int i = 0; i < static_cast<int>(stick->axes.size()); ++i) {
|
||||
lua_pushnumber(L, stick->axes[static_cast<std::size_t>(i)]);
|
||||
lua_rawseti(L, -2, i + 1);
|
||||
}
|
||||
@@ -2723,6 +2732,25 @@ struct WorldApi {
|
||||
lua_setfield(L, -2, "buttons");
|
||||
lua_pushlstring(L, stick->name.data(), stick->name.size());
|
||||
lua_setfield(L, -2, "name");
|
||||
lua_pushinteger(L, stick->device);
|
||||
lua_setfield(L, -2, "device");
|
||||
return 1;
|
||||
}
|
||||
// joysticks(): {{device = n, name = "..."}, ...}, every controller plugged in.
|
||||
static int input_joysticks(lua_State* L) {
|
||||
auto& plugin = plugin_of(L);
|
||||
need(L, plugin, "input", "reskate.input.joysticks");
|
||||
const auto found = game(plugin).joysticks();
|
||||
lua_createtable(L, static_cast<int>(found.size()), 0);
|
||||
int n = 0;
|
||||
for (const auto& stick : found) {
|
||||
lua_createtable(L, 0, 2);
|
||||
lua_pushinteger(L, stick.device);
|
||||
lua_setfield(L, -2, "device");
|
||||
lua_pushlstring(L, stick.name.data(), stick.name.size());
|
||||
lua_setfield(L, -2, "name");
|
||||
lua_rawseti(L, -2, ++n);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
static int input_capture(lua_State* L) {
|
||||
@@ -2999,7 +3027,7 @@ void WorldApi::install(Plugin& plugin, lua_State* L) {
|
||||
{"game", game_current}, {"game_next", game_next}});
|
||||
add_functions(L, "progression", {{"get", progress}});
|
||||
add_functions(L, "settings", {{"game", setting}, {"names", setting_names}, {"set", setting_set}});
|
||||
add_functions(L, "input", {{"bind", bind}, {"flight", input_flight}, {"gamepad", input_gamepad}, {"joystick", input_joystick}, {"capture", input_capture},
|
||||
add_functions(L, "input", {{"bind", bind}, {"flight", input_flight}, {"gamepad", input_gamepad}, {"joystick", input_joystick}, {"joysticks", input_joysticks}, {"capture", input_capture},
|
||||
{"hide", input_hide}, {"cursor", input_cursor}, {"text", input_text}});
|
||||
add_functions(L, "room", {{"open", room_open}, {"leave", room_leave}, {"state", room_state}});
|
||||
add_functions(L, "ui", {{"screen", screen}, {"open", open}, {"close", close}});
|
||||
|
||||
@@ -74,10 +74,17 @@ struct PadInput {
|
||||
std::uint32_t buttons{};
|
||||
bool playstation{};
|
||||
};
|
||||
// A joystick or radio's eight axes (X, Y, Z, Rx, Ry, Rz, the two sliders) and 32 buttons.
|
||||
struct StickInput {
|
||||
std::array<float, 6> axes{};
|
||||
std::array<float, 8> axes{};
|
||||
std::uint32_t buttons{};
|
||||
std::string name;
|
||||
int device = -1; // its number, as joysticks() lists it
|
||||
};
|
||||
// A controller plugged in: its number (for joystick(device)) and name.
|
||||
struct StickDevice {
|
||||
int device{};
|
||||
std::string name;
|
||||
};
|
||||
// A sound a plugin plays (reskate.audio): its wave, or the motor synth when it has none.
|
||||
struct SoundStart {
|
||||
@@ -932,7 +939,10 @@ public:
|
||||
// and only while the plugin's camera is shown.
|
||||
virtual std::optional<FlightInput> flight_input(std::string_view /*plugin*/) { return std::nullopt; }
|
||||
virtual std::optional<PadInput> gamepad() { return std::nullopt; }
|
||||
virtual std::optional<StickInput> joystick() { return std::nullopt; }
|
||||
// The first controller (a radio first), or with device >= 0 that one (StickDevice::device).
|
||||
// Neither ever waits on Windows: a thread of ReSkate's reads the controllers.
|
||||
virtual std::optional<StickInput> joystick(int /*device*/ = -1) { return std::nullopt; }
|
||||
virtual std::vector<StickDevice> joysticks() { return {}; }
|
||||
virtual std::string capture_input(std::string_view /*plugin*/, bool) { return unavailable; }
|
||||
// A named action on a key or pad combo (controller_bindings.h's encoding); presses come back
|
||||
// from take_presses as (plugin, action), only while the game itself has the input.
|
||||
|
||||
@@ -912,12 +912,18 @@ public:
|
||||
if (!pad.valid) return std::nullopt;
|
||||
return PadInput{pad.left_x, pad.left_y, pad.right_x, pad.right_y, pad.left_trigger, pad.right_trigger, pad.buttons, pad.playstation};
|
||||
}
|
||||
std::optional<StickInput> joystick() override {
|
||||
std::optional<StickInput> joystick(int device) override {
|
||||
if (!joystick::game_in_foreground()) return std::nullopt;
|
||||
const auto sample = joystick::poll();
|
||||
const auto sample = joystick::poll(device);
|
||||
if (!sample.valid) return std::nullopt;
|
||||
overlay::note_joystick(sample.buttons, sample.axes);
|
||||
return StickInput{sample.axes, sample.buttons, sample.name};
|
||||
// (Binds take a joystick's first six axes.)
|
||||
overlay::note_joystick(sample.buttons, {sample.axes[0], sample.axes[1], sample.axes[2], sample.axes[3], sample.axes[4], sample.axes[5]});
|
||||
return StickInput{sample.axes, sample.buttons, sample.name, sample.device};
|
||||
}
|
||||
std::vector<StickDevice> joysticks() override {
|
||||
std::vector<StickDevice> out;
|
||||
for (const auto& sample : joystick::list()) out.push_back({sample.device, sample.name});
|
||||
return out;
|
||||
}
|
||||
std::string capture_input(std::string_view plugin, bool on) override {
|
||||
if (!on) {
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
#include "plugin_joystick.h"
|
||||
#ifndef DIRECTINPUT_VERSION
|
||||
#define DIRECTINPUT_VERSION 0x0800
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#include <dinput.h>
|
||||
#include <wrl/client.h>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#pragma comment(lib, "dinput8.lib")
|
||||
#pragma comment(lib, "dxguid.lib")
|
||||
|
||||
namespace dingosdk::plugins::joystick {
|
||||
namespace {
|
||||
using Microsoft::WRL::ComPtr;
|
||||
constexpr LONG range = 1000; // each axis reads -range..range
|
||||
constexpr auto read_every = std::chrono::milliseconds(4); // 250 a second while asked
|
||||
constexpr ULONGLONG look_every_ms = 2000; // the device list, while asked
|
||||
constexpr ULONGLONG asked_for_ms = 2000; // reads go on this long after a call
|
||||
|
||||
std::mutex mutex;
|
||||
std::vector<Sample> latest; // under mutex: what the thread read last
|
||||
std::atomic<ULONGLONG> asked_at{};
|
||||
std::once_flag started;
|
||||
|
||||
std::string narrow(const wchar_t* text) {
|
||||
std::string out;
|
||||
for (; *text; ++text) out += *text < 128 ? static_cast<char>(*text) : '?';
|
||||
return out;
|
||||
}
|
||||
std::string lowered(std::string text) {
|
||||
for (auto& ch : text) if (ch >= 'A' && ch <= 'Z') ch = static_cast<char>(ch + 'a' - 'A');
|
||||
return text;
|
||||
}
|
||||
|
||||
// One controller the thread has open. Its number stays while it is plugged in.
|
||||
struct Device {
|
||||
GUID instance{};
|
||||
int number{};
|
||||
std::string name;
|
||||
ComPtr<IDirectInputDevice8W> input;
|
||||
bool seen{};
|
||||
};
|
||||
|
||||
BOOL CALLBACK found_one(const DIDEVICEINSTANCEW* found, void* list) {
|
||||
static_cast<std::vector<DIDEVICEINSTANCEW>*>(list)->push_back(*found);
|
||||
return DIENUM_CONTINUE;
|
||||
}
|
||||
|
||||
// Opens a controller for reading: the standard joystick layout, every axis -range..range, no dead
|
||||
// zone. Left at the default cooperative level (background, not exclusive), so the game and other
|
||||
// programs read it as before.
|
||||
ComPtr<IDirectInputDevice8W> open(IDirectInput8W& input, const GUID& instance) {
|
||||
ComPtr<IDirectInputDevice8W> device;
|
||||
if (FAILED(input.CreateDevice(instance, device.GetAddressOf(), nullptr))) return nullptr;
|
||||
if (FAILED(device->SetDataFormat(&c_dfDIJoystick2))) return nullptr;
|
||||
DIPROPRANGE axes{};
|
||||
axes.diph = {sizeof(axes), sizeof(axes.diph), 0, DIPH_DEVICE};
|
||||
axes.lMin = -range;
|
||||
axes.lMax = range;
|
||||
device->SetProperty(DIPROP_RANGE, &axes.diph);
|
||||
DIPROPDWORD dead{};
|
||||
dead.diph = {sizeof(dead), sizeof(dead.diph), 0, DIPH_DEVICE};
|
||||
device->SetProperty(DIPROP_DEADZONE, &dead.diph);
|
||||
device->Acquire();
|
||||
return device;
|
||||
}
|
||||
|
||||
bool read(Device& device, Sample& out) {
|
||||
DIJOYSTATE2 state{};
|
||||
device.input->Poll();
|
||||
auto result = device.input->GetDeviceState(sizeof(state), &state);
|
||||
if (result == DIERR_INPUTLOST || result == DIERR_NOTACQUIRED) {
|
||||
device.input->Acquire();
|
||||
device.input->Poll();
|
||||
result = device.input->GetDeviceState(sizeof(state), &state);
|
||||
}
|
||||
if (FAILED(result)) return false;
|
||||
const auto axis = [](LONG value) { return std::clamp(static_cast<float>(value) / static_cast<float>(range), -1.0f, 1.0f); };
|
||||
out.axes = {axis(state.lX), axis(state.lY), axis(state.lZ), axis(state.lRx), axis(state.lRy), axis(state.lRz),
|
||||
axis(state.rglSlider[0]), axis(state.rglSlider[1])};
|
||||
out.buttons = 0;
|
||||
for (int i = 0; i < 32; ++i)
|
||||
if (state.rgbButtons[i] & 0x80) out.buttons |= 1u << i;
|
||||
out.name = device.name;
|
||||
out.device = device.number;
|
||||
out.valid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Looks at the device list: new controllers open with the lowest free number, gone ones close.
|
||||
void look(IDirectInput8W& input, std::vector<Device>& devices) {
|
||||
std::vector<DIDEVICEINSTANCEW> found;
|
||||
if (FAILED(input.EnumDevices(DI8DEVCLASS_GAMECTRL, found_one, &found, DIEDFL_ATTACHEDONLY))) return;
|
||||
for (auto& device : devices) device.seen = false;
|
||||
for (const auto& instance : found) {
|
||||
const auto known = std::find_if(devices.begin(), devices.end(),
|
||||
[&](const Device& device) { return IsEqualGUID(device.instance, instance.guidInstance); });
|
||||
if (known != devices.end()) {
|
||||
known->seen = true;
|
||||
continue;
|
||||
}
|
||||
if (devices.size() >= 16) continue;
|
||||
auto opened = open(input, instance.guidInstance);
|
||||
if (!opened) continue;
|
||||
int number = 0;
|
||||
while (std::any_of(devices.begin(), devices.end(), [&](const Device& device) { return device.number == number; })) ++number;
|
||||
devices.push_back({instance.guidInstance, number, narrow(instance.tszProductName), std::move(opened), true});
|
||||
}
|
||||
std::erase_if(devices, [](const Device& device) { return !device.seen; });
|
||||
std::sort(devices.begin(), devices.end(), [](const Device& a, const Device& b) { return a.number < b.number; });
|
||||
}
|
||||
|
||||
void run() {
|
||||
// The overlay keeps captured controllers from the game's own reads; these are ReSkate's, for
|
||||
// the thread's whole life.
|
||||
const InputAccess access;
|
||||
ComPtr<IDirectInput8W> input;
|
||||
if (FAILED(DirectInput8Create(GetModuleHandleW(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8W,
|
||||
reinterpret_cast<void**>(input.GetAddressOf()), nullptr)))
|
||||
return;
|
||||
std::vector<Device> devices;
|
||||
ULONGLONG looked_at{};
|
||||
for (;;) {
|
||||
const auto now = GetTickCount64();
|
||||
if (now - asked_at.load() > asked_for_ms) {
|
||||
// Nobody is asking: nothing is read, and what was read is forgotten.
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
latest.clear();
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
continue;
|
||||
}
|
||||
if (!looked_at || now - looked_at >= look_every_ms) {
|
||||
look(*input.Get(), devices);
|
||||
looked_at = now;
|
||||
}
|
||||
std::vector<Sample> read_now;
|
||||
for (auto& device : devices) {
|
||||
Sample sample;
|
||||
if (read(device, sample)) read_now.push_back(std::move(sample));
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
latest = std::move(read_now);
|
||||
}
|
||||
std::this_thread::sleep_for(read_every);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Sample> snapshot() {
|
||||
asked_at.store(GetTickCount64());
|
||||
std::call_once(started, [] {
|
||||
try {
|
||||
std::thread(run).detach();
|
||||
} catch (...) {}
|
||||
});
|
||||
std::lock_guard lock(mutex);
|
||||
return latest;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int preferred(const std::vector<Sample>& found) {
|
||||
for (const auto& sample : found) {
|
||||
const auto name = lowered(sample.name);
|
||||
if (name.find("radio") != std::string::npos || name.find("edgetx") != std::string::npos ||
|
||||
name.find("opentx") != std::string::npos)
|
||||
return sample.device;
|
||||
}
|
||||
return found.empty() ? -1 : found.front().device;
|
||||
}
|
||||
|
||||
Sample poll(int device) {
|
||||
const auto found = snapshot();
|
||||
const int wanted = device < 0 ? preferred(found) : device;
|
||||
for (const auto& sample : found)
|
||||
if (sample.device == wanted) return sample;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<Sample> list() { return snapshot(); }
|
||||
|
||||
bool game_in_foreground() {
|
||||
DWORD pid{};
|
||||
GetWindowThreadProcessId(GetForegroundWindow(), &pid);
|
||||
return pid == GetCurrentProcessId();
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,25 @@
|
||||
#pragma once
|
||||
// Joysticks and radio controllers for plugins (reskate.input.joystick): anything that shows up in
|
||||
// Windows' "Set up USB game controllers" (a radio in its USB Joystick mode, a flight stick). Axis
|
||||
// numbers 1..6 are winmm's X, Y, Z, R, U, V, each -1..1. Windows only (winmm).
|
||||
#include <algorithm>
|
||||
// Joysticks and radio controllers for plugins (reskate.input.joystick, reskate.input.joysticks):
|
||||
// anything Windows lists as a game controller (a radio in its USB Joystick mode, a flight stick, a
|
||||
// gamepad). Read through DirectInput, which gives all eight axes a radio sends (winmm gave six):
|
||||
// axes 1..8 are X, Y, Z, Rx, Ry, Rz and the two sliders, each -1..1, 0 for one the controller
|
||||
// does not have. Windows only.
|
||||
//
|
||||
// A thread of its own finds the controllers and reads them: looking at the device list can take
|
||||
// far longer than a plugin's tick may (empty winmm slots took over 50 ms on some PCs, and a plugin
|
||||
// was stopped for it), so a call only copies what the thread read last. It reads only while a
|
||||
// plugin has asked in the last two seconds.
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#ifdef _WIN32
|
||||
#include <Windows.h>
|
||||
#include <mmsystem.h>
|
||||
#pragma comment(lib, "winmm.lib")
|
||||
#include <vector>
|
||||
|
||||
namespace dingosdk::overlay { // UI/Overlay/overlay.h: reads that see the controller while the game is kept off it
|
||||
void begin_input_access() noexcept;
|
||||
void end_input_access() noexcept;
|
||||
}
|
||||
namespace dingosdk::plugins::joystick {
|
||||
struct Sample {
|
||||
bool valid = false;
|
||||
std::array<float, 6> axes{};
|
||||
std::uint32_t buttons = 0; // bit n-1 set = button n pressed
|
||||
std::string name;
|
||||
int device = -1;
|
||||
};
|
||||
|
||||
// While it lives, reads see the controllers even while a plugin keeps the game off them.
|
||||
// While it lives, this thread's reads see the controllers even while a plugin keeps the game off them.
|
||||
struct InputAccess {
|
||||
InputAccess() noexcept { overlay::begin_input_access(); }
|
||||
~InputAccess() { overlay::end_input_access(); }
|
||||
@@ -33,73 +27,23 @@ struct InputAccess {
|
||||
InputAccess& operator=(const InputAccess&) = delete;
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
inline float normalize_axis(std::uint32_t value, std::uint32_t lo, std::uint32_t hi) {
|
||||
if (hi <= lo) return 0.0f;
|
||||
const float t = (static_cast<float>(value) - static_cast<float>(lo)) / (static_cast<float>(hi) - static_cast<float>(lo));
|
||||
return std::clamp(t * 2.0f - 1.0f, -1.0f, 1.0f);
|
||||
}
|
||||
inline std::string narrow(const wchar_t* text) {
|
||||
std::string out;
|
||||
for (; *text; ++text) out += *text < 128 ? static_cast<char>(*text) : '?';
|
||||
return out;
|
||||
}
|
||||
inline std::string lowered(std::string s) {
|
||||
for (auto& ch : s) if (ch >= 'A' && ch <= 'Z') ch = static_cast<char>(ch + 'a' - 'A');
|
||||
return s;
|
||||
}
|
||||
inline bool read_device(UINT id, Sample& out) {
|
||||
JOYCAPSW caps{};
|
||||
if (joyGetDevCapsW(id, &caps, sizeof(caps)) != JOYERR_NOERROR) return false;
|
||||
JOYINFOEX info{};
|
||||
info.dwSize = sizeof(info);
|
||||
info.dwFlags = JOY_RETURNX | JOY_RETURNY | JOY_RETURNZ | JOY_RETURNR | JOY_RETURNU | JOY_RETURNV | JOY_RETURNBUTTONS;
|
||||
if (joyGetPosEx(id, &info) != JOYERR_NOERROR) return false;
|
||||
const UINT n = caps.wNumAxes;
|
||||
out.axes = {};
|
||||
if (n > 0) out.axes[0] = normalize_axis(info.dwXpos, caps.wXmin, caps.wXmax);
|
||||
if (n > 1) out.axes[1] = normalize_axis(info.dwYpos, caps.wYmin, caps.wYmax);
|
||||
if (n > 2) out.axes[2] = normalize_axis(info.dwZpos, caps.wZmin, caps.wZmax);
|
||||
if (n > 3) out.axes[3] = normalize_axis(info.dwRpos, caps.wRmin, caps.wRmax);
|
||||
if (n > 4) out.axes[4] = normalize_axis(info.dwUpos, caps.wUmin, caps.wUmax);
|
||||
if (n > 5) out.axes[5] = normalize_axis(info.dwVpos, caps.wVmin, caps.wVmax);
|
||||
out.buttons = info.dwButtons;
|
||||
out.name = narrow(caps.szPname);
|
||||
out.device = static_cast<int>(id);
|
||||
out.valid = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
inline constexpr std::size_t axis_count = 8;
|
||||
struct Sample {
|
||||
bool valid = false;
|
||||
std::array<float, axis_count> axes{};
|
||||
std::uint32_t buttons = 0; // bit n-1 set = button n pressed
|
||||
std::string name;
|
||||
int device = -1; // its number while it stays plugged in (list())
|
||||
};
|
||||
|
||||
// Reads the first controller, preferring one named a radio. Any thread; rescans at most once a second.
|
||||
inline Sample poll() {
|
||||
InputAccess access;
|
||||
static std::atomic<int> cached{-1};
|
||||
static std::atomic<ULONGLONG> next_scan{0};
|
||||
Sample sample;
|
||||
int id = cached.load();
|
||||
if (id >= 0 && detail::read_device(static_cast<UINT>(id), sample)) return sample;
|
||||
cached.store(-1);
|
||||
const auto now = GetTickCount64();
|
||||
if (now < next_scan.load()) return sample;
|
||||
next_scan.store(now + 1000);
|
||||
const UINT count = std::min<UINT>(joyGetNumDevs(), 16);
|
||||
int first = -1;
|
||||
for (UINT i = 0; i < count; ++i) {
|
||||
Sample candidate;
|
||||
if (!detail::read_device(i, candidate)) continue;
|
||||
if (first < 0) first = static_cast<int>(i);
|
||||
if (detail::lowered(candidate.name).find("radio") != std::string::npos) { cached.store(static_cast<int>(i)); return candidate; }
|
||||
}
|
||||
if (first >= 0) { detail::read_device(static_cast<UINT>(first), sample); cached.store(first); }
|
||||
return sample;
|
||||
}
|
||||
// The controller `device` (a number list() gives), or with -1 the first one, preferring one named
|
||||
// a radio. Not valid when there is none. Any thread; never waits on Windows.
|
||||
Sample poll(int device = -1);
|
||||
// Every controller plugged in, lowest number first. Any thread; never waits on Windows.
|
||||
std::vector<Sample> list();
|
||||
// The first of `found` a plugin reads when it names none: one named a radio, else the lowest number.
|
||||
int preferred(const std::vector<Sample>& found);
|
||||
|
||||
// True while this game's window has focus, so a controller does not drive it from the background.
|
||||
inline bool game_in_foreground() {
|
||||
DWORD pid{};
|
||||
GetWindowThreadProcessId(GetForegroundWindow(), &pid);
|
||||
return pid == GetCurrentProcessId();
|
||||
bool game_in_foreground();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -140,6 +140,7 @@ add_library(dingosdk_runtime SHARED
|
||||
Extension/Plugins/plugin_http.cpp
|
||||
Extension/Plugins/plugin_game_runtime.cpp
|
||||
Extension/Rendering/plugin_effects.cpp
|
||||
Extension/Plugins/plugin_joystick.cpp
|
||||
Extension/Skater/skater_animation.cpp
|
||||
Extension/Plugins/plugin_sound.cpp
|
||||
Extension/Skater/skater_events.cpp
|
||||
|
||||
Reference in new issue
Block a user