2022-07-27 11:25:25 -04:00
|
|
|
#include "input.hpp"
|
2026-05-27 22:56:00 -06:00
|
|
|
#include "device.hpp"
|
2022-07-27 11:25:25 -04:00
|
|
|
#include "internal.hpp"
|
|
|
|
|
|
|
|
|
|
#include "magic_enum.hpp"
|
|
|
|
|
|
2025-04-02 19:57:16 -06:00
|
|
|
#include <SDL3/SDL_haptic.h>
|
|
|
|
|
#include <SDL3/SDL.h>
|
2026-05-05 00:32:21 -06:00
|
|
|
#include <SDL3/SDL_filesystem.h>
|
|
|
|
|
#include <SDL3/SDL_iostream.h>
|
2022-07-27 11:25:25 -04:00
|
|
|
|
|
|
|
|
#include <absl/container/flat_hash_map.h>
|
2026-05-05 00:32:21 -06:00
|
|
|
#include <algorithm>
|
|
|
|
|
#include <array>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <utility>
|
2022-07-27 11:25:25 -04:00
|
|
|
|
|
|
|
|
using namespace std::string_view_literals;
|
|
|
|
|
|
|
|
|
|
namespace aurora::input {
|
2025-04-14 17:31:48 -07:00
|
|
|
Module Log("aurora::input");
|
2022-07-27 11:25:25 -04:00
|
|
|
absl::flat_hash_map<Uint32, GameController> g_GameControllers;
|
|
|
|
|
|
2026-05-05 00:32:21 -06:00
|
|
|
namespace {
|
|
|
|
|
constexpr uint32_t kPortPreferencesMagic = SBIG('CPRT');
|
2026-05-27 22:56:00 -06:00
|
|
|
constexpr uint32_t kPortPreferencesVersion = 3;
|
2026-05-05 00:32:21 -06:00
|
|
|
constexpr uint32_t kMaxPersistedStringLength = 256;
|
2026-05-27 22:56:00 -06:00
|
|
|
constexpr uint16_t kDefaultDeviceRumbleIntensityLow = 0x8000;
|
|
|
|
|
constexpr uint16_t kDefaultDeviceRumbleIntensityHigh = 0x8000;
|
2026-05-05 00:32:21 -06:00
|
|
|
|
|
|
|
|
enum class PortPreferenceState : uint8_t {
|
|
|
|
|
Unset = 0,
|
|
|
|
|
None = 1,
|
|
|
|
|
Controller = 2,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct ControllerIdentity {
|
|
|
|
|
std::string guid;
|
|
|
|
|
std::string serial;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct PortPreference {
|
|
|
|
|
PortPreferenceState state = PortPreferenceState::Unset;
|
|
|
|
|
ControllerIdentity identity;
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-27 22:56:00 -06:00
|
|
|
struct DevicePreference {
|
|
|
|
|
uint16_t rumbleIntensityLow = kDefaultDeviceRumbleIntensityLow;
|
|
|
|
|
uint16_t rumbleIntensityHigh = kDefaultDeviceRumbleIntensityHigh;
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-05 00:32:21 -06:00
|
|
|
std::array<PortPreference, PAD_MAX_CONTROLLERS> g_portPreferences;
|
2026-05-27 22:56:00 -06:00
|
|
|
DevicePreference g_devicePreference;
|
2026-05-05 00:32:21 -06:00
|
|
|
bool g_portPreferencesLoaded = false;
|
|
|
|
|
|
|
|
|
|
std::string port_preferences_path() {
|
2026-05-13 00:45:19 -06:00
|
|
|
if (g_config.userPath == nullptr) {
|
2026-05-05 00:32:21 -06:00
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 00:45:19 -06:00
|
|
|
std::string path{g_config.userPath};
|
2026-05-05 00:32:21 -06:00
|
|
|
if (!path.empty() && path.back() != '/' && path.back() != '\\') {
|
|
|
|
|
path += '/';
|
|
|
|
|
}
|
|
|
|
|
path += "controller_ports.dat";
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string normalize_serial(const char* serial) {
|
|
|
|
|
std::string normalized;
|
|
|
|
|
if (serial == nullptr) {
|
|
|
|
|
return normalized;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const char* c = serial; *c != '\0'; ++c) {
|
|
|
|
|
if (*c == ':' || *c == '-') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
normalized += *c >= 'A' && *c <= 'Z' ? static_cast<char>(*c - 'A' + 'a') : *c;
|
|
|
|
|
}
|
|
|
|
|
return normalized;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool serial_matches(const std::string& saved, const std::string& current) {
|
|
|
|
|
return !saved.empty() && !current.empty() && normalize_serial(saved.c_str()) == normalize_serial(current.c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ControllerIdentity controller_identity(const GameController& controller) {
|
|
|
|
|
ControllerIdentity identity;
|
|
|
|
|
char guid[33] = {};
|
|
|
|
|
SDL_GUIDToString(SDL_GetGamepadGUIDForID(SDL_GetGamepadID(controller.m_controller)), guid, sizeof(guid));
|
|
|
|
|
identity.guid = guid;
|
|
|
|
|
identity.serial = normalize_serial(SDL_GetGamepadSerial(controller.m_controller));
|
|
|
|
|
return identity;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool read_exact(SDL_IOStream* file, void* dst, size_t size) {
|
|
|
|
|
auto* bytes = static_cast<uint8_t*>(dst);
|
|
|
|
|
size_t total = 0;
|
|
|
|
|
while (total < size) {
|
|
|
|
|
const size_t read = SDL_ReadIO(file, bytes + total, size - total);
|
|
|
|
|
if (read == 0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
total += read;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool write_exact(SDL_IOStream* file, const void* src, size_t size) {
|
|
|
|
|
auto* bytes = static_cast<const uint8_t*>(src);
|
|
|
|
|
size_t total = 0;
|
|
|
|
|
while (total < size) {
|
|
|
|
|
const size_t written = SDL_WriteIO(file, bytes + total, size - total);
|
|
|
|
|
if (written == 0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
total += written;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
|
bool read_value(SDL_IOStream* file, T& value) {
|
|
|
|
|
return read_exact(file, &value, sizeof(value));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
|
bool write_value(SDL_IOStream* file, const T& value) {
|
|
|
|
|
return write_exact(file, &value, sizeof(value));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool read_string(SDL_IOStream* file, std::string& value) {
|
|
|
|
|
uint32_t size = 0;
|
|
|
|
|
if (!read_value(file, size) || size > kMaxPersistedStringLength) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
value.resize(size);
|
|
|
|
|
return size == 0 || read_exact(file, value.data(), size);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool write_string(SDL_IOStream* file, const std::string& value) {
|
|
|
|
|
const uint32_t size = static_cast<uint32_t>(std::min<size_t>(value.size(), kMaxPersistedStringLength));
|
|
|
|
|
return write_value(file, size) && (size == 0 || write_exact(file, value.data(), size));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool read_identity(SDL_IOStream* file, ControllerIdentity& identity) {
|
|
|
|
|
return read_string(file, identity.guid) && read_string(file, identity.serial);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool write_identity(SDL_IOStream* file, const ControllerIdentity& identity) {
|
|
|
|
|
return write_string(file, identity.guid) && write_string(file, identity.serial);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool read_port_preferences_file(std::array<PortPreference, PAD_MAX_CONTROLLERS>& preferences) {
|
|
|
|
|
const auto path = port_preferences_path();
|
|
|
|
|
if (path.empty()) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SDL_IOStream* file = SDL_IOFromFile(path.c_str(), "rb");
|
|
|
|
|
if (file == nullptr) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t magic = 0;
|
|
|
|
|
uint32_t version = 0;
|
|
|
|
|
bool ok = read_value(file, magic) && read_value(file, version) && magic == kPortPreferencesMagic &&
|
2026-05-27 22:56:00 -06:00
|
|
|
(version == 2 || version == kPortPreferencesVersion);
|
2026-05-05 00:32:21 -06:00
|
|
|
|
|
|
|
|
for (auto& preference : preferences) {
|
|
|
|
|
uint8_t state = 0;
|
|
|
|
|
ControllerIdentity identity;
|
|
|
|
|
ok = ok && read_value(file, state) && state <= static_cast<uint8_t>(PortPreferenceState::Controller) &&
|
|
|
|
|
read_identity(file, identity);
|
|
|
|
|
if (ok) {
|
|
|
|
|
preference.state = static_cast<PortPreferenceState>(state);
|
|
|
|
|
preference.identity = std::move(identity);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-27 22:56:00 -06:00
|
|
|
if (ok && version >= 3) {
|
|
|
|
|
ok = read_value(file, g_devicePreference.rumbleIntensityLow) &&
|
|
|
|
|
read_value(file, g_devicePreference.rumbleIntensityHigh);
|
|
|
|
|
}
|
2026-05-05 00:32:21 -06:00
|
|
|
|
|
|
|
|
SDL_CloseIO(file);
|
|
|
|
|
if (!ok) {
|
|
|
|
|
Log.warn("Ignoring invalid controller port preference file '{}'", path);
|
|
|
|
|
}
|
|
|
|
|
return ok;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ensure_port_preferences_loaded() {
|
|
|
|
|
if (g_portPreferencesLoaded) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::array<PortPreference, PAD_MAX_CONTROLLERS> preferences;
|
|
|
|
|
if (read_port_preferences_file(preferences)) {
|
|
|
|
|
g_portPreferences = std::move(preferences);
|
|
|
|
|
}
|
|
|
|
|
g_portPreferencesLoaded = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void save_port_preferences() {
|
|
|
|
|
const auto path = port_preferences_path();
|
|
|
|
|
if (path.empty()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 00:45:19 -06:00
|
|
|
if (!SDL_CreateDirectory(g_config.userPath)) {
|
|
|
|
|
Log.warn("Failed to create controller port preference directory '{}': {}", g_config.userPath, SDL_GetError());
|
2026-05-05 00:32:21 -06:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SDL_IOStream* file = SDL_IOFromFile(path.c_str(), "wb");
|
|
|
|
|
if (file == nullptr) {
|
|
|
|
|
Log.warn("Failed to open controller port preference file '{}': {}", path, SDL_GetError());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ok = write_value(file, kPortPreferencesMagic) && write_value(file, kPortPreferencesVersion);
|
|
|
|
|
for (const auto& preference : g_portPreferences) {
|
|
|
|
|
const auto state = static_cast<uint8_t>(preference.state);
|
|
|
|
|
ok = ok && write_value(file, state) && write_identity(file, preference.identity);
|
|
|
|
|
}
|
2026-05-27 22:56:00 -06:00
|
|
|
ok = ok && write_value(file, g_devicePreference.rumbleIntensityLow) &&
|
|
|
|
|
write_value(file, g_devicePreference.rumbleIntensityHigh);
|
2026-05-05 00:32:21 -06:00
|
|
|
|
|
|
|
|
if (!SDL_FlushIO(file)) {
|
|
|
|
|
ok = false;
|
|
|
|
|
}
|
|
|
|
|
if (!SDL_CloseIO(file)) {
|
|
|
|
|
ok = false;
|
|
|
|
|
}
|
|
|
|
|
if (!ok) {
|
|
|
|
|
Log.warn("Failed to write controller port preference file '{}': {}", path, SDL_GetError());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum class IdentityMatch {
|
|
|
|
|
None,
|
|
|
|
|
Fallback,
|
|
|
|
|
Exact,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
IdentityMatch identity_match(const ControllerIdentity& saved, const ControllerIdentity& current) {
|
|
|
|
|
if (saved.guid.empty()) {
|
|
|
|
|
return IdentityMatch::None;
|
|
|
|
|
}
|
|
|
|
|
if (saved.guid == current.guid) {
|
|
|
|
|
return saved.serial.empty() || serial_matches(saved.serial, current.serial) ? IdentityMatch::Exact
|
|
|
|
|
: IdentityMatch::None;
|
|
|
|
|
}
|
|
|
|
|
if (!serial_matches(saved.serial, current.serial)) {
|
|
|
|
|
return IdentityMatch::None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Attempt to match against VID/PID as a fallback if the GUID changes
|
|
|
|
|
uint16_t savedVendor = 0;
|
|
|
|
|
uint16_t savedProduct = 0;
|
|
|
|
|
uint16_t currentVendor = 0;
|
|
|
|
|
uint16_t currentProduct = 0;
|
|
|
|
|
SDL_GetJoystickGUIDInfo(SDL_StringToGUID(saved.guid.c_str()), &savedVendor, &savedProduct, nullptr, nullptr);
|
|
|
|
|
SDL_GetJoystickGUIDInfo(SDL_StringToGUID(current.guid.c_str()), ¤tVendor, ¤tProduct, nullptr, nullptr);
|
|
|
|
|
return savedVendor != 0 && savedVendor == currentVendor && savedProduct != 0 && savedProduct == currentProduct
|
|
|
|
|
? IdentityMatch::Fallback
|
|
|
|
|
: IdentityMatch::None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool is_instance_claimed(const std::array<Uint32, PAD_MAX_CONTROLLERS>& claimedControllers, size_t claimedCount,
|
|
|
|
|
Uint32 instance) {
|
|
|
|
|
return std::find(claimedControllers.begin(), claimedControllers.begin() + claimedCount, instance) !=
|
|
|
|
|
claimedControllers.begin() + claimedCount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void apply_port_preferences() noexcept {
|
|
|
|
|
ensure_port_preferences_loaded();
|
|
|
|
|
if (!std::any_of(g_portPreferences.begin(), g_portPreferences.end(),
|
|
|
|
|
[](const auto& preference) { return preference.state != PortPreferenceState::Unset; })) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (auto& [instance, controller] : g_GameControllers) {
|
|
|
|
|
const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller);
|
|
|
|
|
if (player >= 0 && player < PAD_MAX_CONTROLLERS && g_portPreferences[player].state != PortPreferenceState::Unset) {
|
|
|
|
|
// Keep SDL's default player assignment from taking explicitly configured ports
|
|
|
|
|
SDL_SetGamepadPlayerIndex(controller.m_controller, -1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::array<Uint32, PAD_MAX_CONTROLLERS> claimedControllers{};
|
|
|
|
|
size_t claimedCount = 0;
|
|
|
|
|
for (uint32_t port = 0; port < g_portPreferences.size(); ++port) {
|
|
|
|
|
const auto& preference = g_portPreferences[port];
|
|
|
|
|
if (preference.state != PortPreferenceState::Controller) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Uint32 fallbackInstance = 0;
|
|
|
|
|
GameController* fallbackController = nullptr;
|
|
|
|
|
for (auto& [instance, controller] : g_GameControllers) {
|
|
|
|
|
if (is_instance_claimed(claimedControllers, claimedCount, instance)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (identity_match(preference.identity, controller_identity(controller))) {
|
|
|
|
|
case IdentityMatch::Exact:
|
|
|
|
|
SDL_SetGamepadPlayerIndex(controller.m_controller, static_cast<int32_t>(port));
|
|
|
|
|
claimedControllers[claimedCount++] = instance;
|
|
|
|
|
fallbackController = nullptr;
|
|
|
|
|
break;
|
|
|
|
|
case IdentityMatch::Fallback:
|
|
|
|
|
// Prefer any later exact match before claiming a fallback candidate
|
|
|
|
|
if (fallbackController == nullptr) {
|
|
|
|
|
fallbackInstance = instance;
|
|
|
|
|
fallbackController = &controller;
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
case IdentityMatch::None:
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fallbackController != nullptr) {
|
|
|
|
|
SDL_SetGamepadPlayerIndex(fallbackController->m_controller, static_cast<int32_t>(port));
|
|
|
|
|
claimedControllers[claimedCount++] = fallbackInstance;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} // namespace
|
|
|
|
|
|
2022-07-27 11:25:25 -04:00
|
|
|
GameController* get_controller_for_player(uint32_t player) noexcept {
|
|
|
|
|
for (auto& [which, controller] : g_GameControllers) {
|
|
|
|
|
if (player_index(which) == player) {
|
|
|
|
|
return &controller;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if 0
|
|
|
|
|
/* If we don't have a controller assigned to this port use the first unassigned controller */
|
|
|
|
|
if (!g_GameControllers.empty()) {
|
|
|
|
|
int32_t availIndex = -1;
|
|
|
|
|
GameController* ct = nullptr;
|
|
|
|
|
for (auto& controller : g_GameControllers) {
|
|
|
|
|
if (player_index(controller.first) == -1) {
|
|
|
|
|
availIndex = controller.first;
|
|
|
|
|
ct = &controller.second;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (availIndex != -1) {
|
|
|
|
|
set_player_index(availIndex, player);
|
|
|
|
|
return ct;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Sint32 get_instance_for_player(uint32_t player) noexcept {
|
|
|
|
|
for (const auto& [which, controller] : g_GameControllers) {
|
|
|
|
|
if (player_index(which) == player) {
|
|
|
|
|
return which;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-02 19:57:16 -06:00
|
|
|
SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
|
|
|
|
|
auto* ctrl = SDL_OpenGamepad(which);
|
2022-07-27 11:25:25 -04:00
|
|
|
if (ctrl != nullptr) {
|
|
|
|
|
GameController controller;
|
|
|
|
|
controller.m_controller = ctrl;
|
|
|
|
|
controller.m_index = which;
|
2025-04-02 19:57:16 -06:00
|
|
|
controller.m_vid = SDL_GetGamepadVendor(ctrl);
|
|
|
|
|
controller.m_pid = SDL_GetGamepadProduct(ctrl);
|
2022-07-27 11:25:25 -04:00
|
|
|
if (controller.m_vid == 0x05ac /* USB_VENDOR_APPLE */ && controller.m_pid == 3) {
|
|
|
|
|
// Ignore Apple TV remote
|
2025-04-02 19:57:16 -06:00
|
|
|
SDL_CloseGamepad(ctrl);
|
2022-07-27 11:25:25 -04:00
|
|
|
return -1;
|
|
|
|
|
}
|
2026-05-23 19:21:48 -07:00
|
|
|
controller.m_isGameCube = controller.m_vid == 0x057E && controller.m_pid == 0x0337;
|
2026-05-13 17:52:02 -07:00
|
|
|
if (controller.m_isGameCube ||
|
|
|
|
|
(SDL_GetGamepadType(ctrl) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO && controller.m_pid == 0x2073)) {
|
2026-05-11 21:42:04 -07:00
|
|
|
controller.m_deadZones.emulateTriggers = false;
|
|
|
|
|
}
|
2025-04-02 19:57:16 -06:00
|
|
|
const auto props = SDL_GetGamepadProperties(ctrl);
|
|
|
|
|
controller.m_hasRumble = SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, true);
|
2026-04-29 21:21:40 +02:00
|
|
|
controller.m_hasRgbLed = SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false);
|
2025-04-02 19:57:16 -06:00
|
|
|
SDL_JoystickID instance = SDL_GetJoystickID(SDL_GetGamepadJoystick(ctrl));
|
2026-06-09 23:33:34 -06:00
|
|
|
Log.info("Added controller '{}' (instance {}, vid {:04x}, pid {:04x}, type {})",
|
|
|
|
|
SDL_GetGamepadName(ctrl) != nullptr ? SDL_GetGamepadName(ctrl) : "unknown", instance, controller.m_vid,
|
|
|
|
|
controller.m_pid, static_cast<int>(SDL_GetGamepadType(ctrl)));
|
2022-07-27 11:25:25 -04:00
|
|
|
g_GameControllers[instance] = controller;
|
2026-05-05 00:32:21 -06:00
|
|
|
apply_port_preferences();
|
2022-07-27 11:25:25 -04:00
|
|
|
return instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void remove_controller(Uint32 instance) noexcept {
|
2026-04-12 18:40:21 -07:00
|
|
|
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
|
2026-06-09 23:33:34 -06:00
|
|
|
Log.info("Removed controller '{}' (instance {})",
|
|
|
|
|
SDL_GetGamepadName(it->second.m_controller) != nullptr ? SDL_GetGamepadName(it->second.m_controller)
|
|
|
|
|
: "unknown",
|
|
|
|
|
instance);
|
2026-04-12 18:40:21 -07:00
|
|
|
SDL_CloseGamepad(it->second.m_controller);
|
|
|
|
|
g_GameControllers.erase(it);
|
2026-05-05 00:32:21 -06:00
|
|
|
apply_port_preferences();
|
2022-07-27 11:25:25 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool is_gamecube(Uint32 instance) noexcept {
|
2026-04-12 18:40:21 -07:00
|
|
|
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
|
|
|
|
|
return it->second.m_isGameCube;
|
2022-07-27 11:25:25 -04:00
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int32_t player_index(Uint32 instance) noexcept {
|
2026-04-12 18:40:21 -07:00
|
|
|
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
|
|
|
|
|
return SDL_GetGamepadPlayerIndex(it->second.m_controller);
|
2022-07-27 11:25:25 -04:00
|
|
|
}
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void set_player_index(Uint32 instance, Sint32 index) noexcept {
|
2026-04-12 18:40:21 -07:00
|
|
|
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
|
|
|
|
|
SDL_SetGamepadPlayerIndex(it->second.m_controller, index);
|
2022-07-27 11:25:25 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string controller_name(Uint32 instance) noexcept {
|
2026-04-12 18:40:21 -07:00
|
|
|
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
|
|
|
|
|
const auto* name = SDL_GetGamepadName(it->second.m_controller);
|
2022-07-27 11:25:25 -04:00
|
|
|
if (name != nullptr) {
|
|
|
|
|
return {name};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool controller_has_rumble(Uint32 instance) noexcept {
|
2026-04-12 18:40:21 -07:00
|
|
|
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
|
|
|
|
|
return it->second.m_hasRumble;
|
2022-07-27 11:25:25 -04:00
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void controller_rumble(uint32_t instance, uint16_t low_freq_intensity, uint16_t high_freq_intensity,
|
|
|
|
|
uint16_t duration_ms) noexcept {
|
2026-04-12 18:40:21 -07:00
|
|
|
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
|
|
|
|
|
SDL_RumbleGamepad(it->second.m_controller, low_freq_intensity, high_freq_intensity, duration_ms);
|
2022-07-27 11:25:25 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 22:56:00 -06:00
|
|
|
void get_device_rumble_intensity(uint16_t* low_freq_intensity, uint16_t* high_freq_intensity) noexcept {
|
|
|
|
|
ensure_port_preferences_loaded();
|
|
|
|
|
*low_freq_intensity = g_devicePreference.rumbleIntensityLow;
|
|
|
|
|
*high_freq_intensity = g_devicePreference.rumbleIntensityHigh;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void set_device_rumble_intensity(const uint16_t low_freq_intensity, const uint16_t high_freq_intensity) noexcept {
|
|
|
|
|
ensure_port_preferences_loaded();
|
|
|
|
|
g_devicePreference.rumbleIntensityLow = low_freq_intensity;
|
|
|
|
|
g_devicePreference.rumbleIntensityHigh = high_freq_intensity;
|
|
|
|
|
save_port_preferences();
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-27 11:25:25 -04:00
|
|
|
uint32_t controller_count() noexcept { return g_GameControllers.size(); }
|
|
|
|
|
|
2026-05-05 00:32:21 -06:00
|
|
|
void persist_controller_for_player(uint32_t player, const GameController* controller) noexcept {
|
|
|
|
|
if (player >= PAD_MAX_CONTROLLERS) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ensure_port_preferences_loaded();
|
|
|
|
|
if (controller != nullptr) {
|
|
|
|
|
g_portPreferences[player].state = PortPreferenceState::Controller;
|
|
|
|
|
g_portPreferences[player].identity = controller_identity(*controller);
|
|
|
|
|
} else {
|
|
|
|
|
g_portPreferences[player].state = PortPreferenceState::None;
|
|
|
|
|
g_portPreferences[player].identity = {};
|
|
|
|
|
}
|
|
|
|
|
save_port_preferences();
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-21 19:48:26 -08:00
|
|
|
void initialize() noexcept {
|
|
|
|
|
/* Make sure we initialize everything input related now, this will automatically add all of the connected controllers
|
|
|
|
|
* as expected */
|
2026-05-27 22:56:00 -06:00
|
|
|
ASSERT(SDL_Init(SDL_INIT_HAPTIC | SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD | SDL_INIT_SENSOR),
|
|
|
|
|
"Failed to initialize SDL subsystems: {}", SDL_GetError());
|
2023-01-21 19:48:26 -08:00
|
|
|
}
|
2026-04-16 12:57:10 -07:00
|
|
|
|
|
|
|
|
struct MouseScrollStatus {
|
|
|
|
|
float scrollX;
|
|
|
|
|
float scrollY;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static MouseScrollStatus g_MouseStatus;
|
|
|
|
|
|
|
|
|
|
void set_mouse_scroll(const float scrollX, const float scrollY) noexcept {
|
|
|
|
|
g_MouseStatus.scrollX = scrollX;
|
|
|
|
|
g_MouseStatus.scrollY = scrollY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void get_mouse_scroll(float* scrollX, float* scrollY) noexcept {
|
|
|
|
|
*scrollX = g_MouseStatus.scrollX;
|
|
|
|
|
*scrollY = g_MouseStatus.scrollY;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 19:22:28 -07:00
|
|
|
void shutdown() noexcept {
|
|
|
|
|
// Upon shutdown we want to ensure all controllers are in a default state, so force all rumble supporting controllers
|
2026-05-05 00:32:21 -06:00
|
|
|
// to shut off their rumble motors.
|
2026-04-21 19:22:28 -07:00
|
|
|
for (const auto& controller : g_GameControllers) {
|
|
|
|
|
if (!controller.second.m_hasRumble) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
controller_rumble(controller.first, 0, 0, 0);
|
|
|
|
|
}
|
2026-05-27 22:56:00 -06:00
|
|
|
device::shutdown();
|
2026-04-21 19:22:28 -07:00
|
|
|
}
|
2022-07-27 11:25:25 -04:00
|
|
|
} // namespace aurora::input
|