mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-10 17:16:47 -04:00
Merge pull request #43 from patchzyy/Unknown-Controller-support
Unknown controller support
This commit is contained in:
@@ -255,6 +255,18 @@ IdentityMatch identity_match(const ControllerIdentity& saved, const ControllerId
|
||||
: IdentityMatch::None;
|
||||
}
|
||||
|
||||
void assign_player_index(GameController& controller, int32_t port) {
|
||||
SDL_SetGamepadPlayerIndex(controller.m_controller, port);
|
||||
controller.m_playerIndex = port;
|
||||
}
|
||||
|
||||
// SDL forgets the index for devices mapped after connect, so player_index() falls
|
||||
// back to the cached copy; both have to move together or a port looks doubly taken.
|
||||
int32_t effective_player_index(const GameController& controller) {
|
||||
const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller);
|
||||
return player >= 0 ? player : controller.m_playerIndex;
|
||||
}
|
||||
|
||||
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) !=
|
||||
@@ -269,10 +281,10 @@ void apply_port_preferences() noexcept {
|
||||
}
|
||||
|
||||
for (auto& [instance, controller] : g_GameControllers) {
|
||||
const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller);
|
||||
const int32_t player = effective_player_index(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);
|
||||
assign_player_index(controller, -1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +305,7 @@ void apply_port_preferences() noexcept {
|
||||
|
||||
switch (identity_match(preference.identity, controller_identity(controller))) {
|
||||
case IdentityMatch::Exact:
|
||||
SDL_SetGamepadPlayerIndex(controller.m_controller, static_cast<int32_t>(port));
|
||||
assign_player_index(controller, static_cast<int32_t>(port));
|
||||
claimedControllers[claimedCount++] = instance;
|
||||
fallbackController = nullptr;
|
||||
break;
|
||||
@@ -311,11 +323,45 @@ void apply_port_preferences() noexcept {
|
||||
}
|
||||
|
||||
if (fallbackController != nullptr) {
|
||||
SDL_SetGamepadPlayerIndex(fallbackController->m_controller, static_cast<int32_t>(port));
|
||||
assign_player_index(*fallbackController, static_cast<int32_t>(port));
|
||||
claimedControllers[claimedCount++] = fallbackInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SDL only hands out a player index when the device already had a gamepad mapping
|
||||
// at connect time, so anything mapped later (the setup wizard) stays at -1.
|
||||
void ensure_player_index(GameController& controller) noexcept {
|
||||
const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller);
|
||||
if (player >= 0) {
|
||||
controller.m_playerIndex = player;
|
||||
return;
|
||||
}
|
||||
if (controller.m_playerIndex >= 0) {
|
||||
return;
|
||||
}
|
||||
ensure_port_preferences_loaded();
|
||||
const auto claim = [&](bool skipConfiguredPorts) {
|
||||
for (int32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
||||
if (skipConfiguredPorts && g_portPreferences[port].state != PortPreferenceState::Unset) {
|
||||
continue;
|
||||
}
|
||||
const bool taken = std::any_of(g_GameControllers.begin(), g_GameControllers.end(), [&](const auto& entry) {
|
||||
return entry.second.m_controller != controller.m_controller && effective_player_index(entry.second) == port;
|
||||
});
|
||||
if (!taken) {
|
||||
assign_player_index(controller, port);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// Explicitly configured ports are only used as a last resort so a hot-plugged
|
||||
// controller cannot steal the port its preferred device will claim.
|
||||
if (!claim(true)) {
|
||||
claim(false);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
GameController* get_controller_for_player(uint32_t player) noexcept {
|
||||
@@ -364,6 +410,7 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
|
||||
controller.m_hasRgbLed = SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false);
|
||||
SDL_JoystickID instance = SDL_GetJoystickID(SDL_GetGamepadJoystick(ctrl));
|
||||
g_GameControllers[instance] = controller;
|
||||
ensure_player_index(g_GameControllers[instance]);
|
||||
apply_port_preferences();
|
||||
return instance;
|
||||
}
|
||||
@@ -371,6 +418,19 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool refresh_controller(SDL_JoystickID instance) noexcept {
|
||||
const auto it = g_GameControllers.find(instance);
|
||||
if (it == g_GameControllers.end()) {
|
||||
return false;
|
||||
}
|
||||
// The SDL mapping changed underneath us; drop the cached PAD bindings so they
|
||||
// are rebuilt from the new one.
|
||||
it->second.m_mappingLoaded = false;
|
||||
ensure_player_index(it->second);
|
||||
apply_port_preferences();
|
||||
return true;
|
||||
}
|
||||
|
||||
void remove_controller(Uint32 instance) noexcept {
|
||||
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
|
||||
SDL_CloseGamepad(it->second.m_controller);
|
||||
|
||||
@@ -51,6 +51,7 @@ struct GameController {
|
||||
GameController* get_controller_for_player(uint32_t player) noexcept;
|
||||
Sint32 get_instance_for_player(uint32_t player) noexcept;
|
||||
SDL_JoystickID add_controller(SDL_JoystickID which) noexcept;
|
||||
bool refresh_controller(SDL_JoystickID instance) noexcept;
|
||||
void remove_controller(Uint32 instance) noexcept;
|
||||
Sint32 player_index(Uint32 instance) noexcept;
|
||||
void set_player_index(Uint32 instance, Sint32 index) noexcept;
|
||||
|
||||
@@ -294,6 +294,15 @@ void process_event(SDL_Event& event) {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_GAMEPAD_REMAPPED: {
|
||||
if (input::refresh_controller(event.gdevice.which)) {
|
||||
g_events.push_back(AuroraEvent{
|
||||
.type = AURORA_CONTROLLER_ADDED,
|
||||
.controller = event.gdevice.which,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_GAMEPAD_REMOVED: {
|
||||
input::remove_controller(event.gdevice.which);
|
||||
g_events.push_back(AuroraEvent{
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_events.h>
|
||||
|
||||
// Press-to-bind setup for joysticks SDL either doesn't recognize as gamepads or
|
||||
// recognizes with a mapping that lacks the analog stick (e.g. raphnet adapters).
|
||||
// The wizard produces a standard SDL gamepad mapping, applies it live, and
|
||||
// persists it to gamecontrollerdb.txt in the user data directory.
|
||||
namespace controller_mapping_wizard {
|
||||
|
||||
void LoadPersistedMappings();
|
||||
void HandleSdlEvent(const SDL_Event& event);
|
||||
|
||||
// Lists devices that need setup inside the controller settings menu.
|
||||
void DrawSetupList();
|
||||
// Draws the wizard window when active; call once per overlay frame.
|
||||
void Draw();
|
||||
|
||||
bool IsActive();
|
||||
|
||||
} // namespace controller_mapping_wizard
|
||||
@@ -0,0 +1,476 @@
|
||||
#include "controller_mapping_wizard.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <SDL3/SDL_joystick.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace controller_mapping_wizard {
|
||||
namespace {
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
constexpr int16_t kStickThreshold = 16000;
|
||||
constexpr int16_t kTriggerThreshold = 10000;
|
||||
constexpr auto kCaptureDebounce = std::chrono::milliseconds(350);
|
||||
|
||||
enum class StepKind {
|
||||
Button, // button or single-direction hat press
|
||||
Trigger, // button press or axis pull
|
||||
Stick, // axis motion in the prompted direction
|
||||
};
|
||||
|
||||
struct Step {
|
||||
const char* mappingKey;
|
||||
const char* prompt;
|
||||
StepKind kind;
|
||||
};
|
||||
|
||||
// Prompts describe what the control does in-game; the SDL fields land on the
|
||||
// right GC controls through pad.cpp's "standard" defaults (Z lives on
|
||||
// rightshoulder, L/R on the trigger axes).
|
||||
constexpr std::array<Step, 16> kSteps = {{
|
||||
{"a", "Press the button for A (accelerate / select)", StepKind::Button},
|
||||
{"b", "Press the button for B (brake / back)", StepKind::Button},
|
||||
{"x", "Press the button for X", StepKind::Button},
|
||||
{"y", "Press the button for Y", StepKind::Button},
|
||||
{"start", "Press the button for pause (Start)", StepKind::Button},
|
||||
{"rightshoulder", "Press the button for rear view (Z)", StepKind::Button},
|
||||
{"lefttrigger", "Press or pull the control for using items (L)", StepKind::Trigger},
|
||||
{"righttrigger", "Press or pull the control for hop / drift (R)", StepKind::Trigger},
|
||||
{"dpup", "Press D-pad Up", StepKind::Button},
|
||||
{"dpdown", "Press D-pad Down", StepKind::Button},
|
||||
{"dpleft", "Press D-pad Left", StepKind::Button},
|
||||
{"dpright", "Press D-pad Right", StepKind::Button},
|
||||
{"leftx", "Move the Control Stick LEFT", StepKind::Stick},
|
||||
{"lefty", "Move the Control Stick UP", StepKind::Stick},
|
||||
{"rightx", "Move the C-Stick LEFT (or Skip)", StepKind::Stick},
|
||||
{"righty", "Move the C-Stick UP (or Skip)", StepKind::Stick},
|
||||
}};
|
||||
|
||||
struct WizardState {
|
||||
bool active = false;
|
||||
SDL_JoystickID instance = 0;
|
||||
SDL_Joystick* joystick = nullptr;
|
||||
bool ownsJoystick = false;
|
||||
std::string deviceName;
|
||||
size_t stepIndex = 0;
|
||||
std::array<std::optional<std::string>, kSteps.size()> bindings{};
|
||||
std::vector<int16_t> axisBaseline;
|
||||
Clock::time_point acceptAfter{};
|
||||
std::string status;
|
||||
};
|
||||
|
||||
WizardState g_wizard;
|
||||
|
||||
std::filesystem::path MappingDbPath() {
|
||||
return RuntimeConfigFile::ApplicationDataDirectory() / "gamecontrollerdb.txt";
|
||||
}
|
||||
|
||||
std::string GuidString(SDL_JoystickID instance) {
|
||||
char buf[33] = {};
|
||||
SDL_GUIDToString(SDL_GetJoystickGUIDForID(instance), buf, sizeof(buf));
|
||||
return buf;
|
||||
}
|
||||
|
||||
bool BindingUsed(const std::string& value) {
|
||||
return std::any_of(g_wizard.bindings.begin(), g_wizard.bindings.end(),
|
||||
[&](const std::optional<std::string>& b) { return b && *b == value; });
|
||||
}
|
||||
|
||||
void SnapshotAxes() {
|
||||
g_wizard.axisBaseline.clear();
|
||||
const int axes = SDL_GetNumJoystickAxes(g_wizard.joystick);
|
||||
for (int i = 0; i < axes; ++i) {
|
||||
g_wizard.axisBaseline.push_back(SDL_GetJoystickAxis(g_wizard.joystick, i));
|
||||
}
|
||||
}
|
||||
|
||||
void AdvanceStep(std::optional<std::string> value) {
|
||||
g_wizard.bindings[g_wizard.stepIndex] = std::move(value);
|
||||
++g_wizard.stepIndex;
|
||||
g_wizard.acceptAfter = Clock::now() + kCaptureDebounce;
|
||||
SnapshotAxes();
|
||||
}
|
||||
|
||||
void StopWizard() {
|
||||
if (g_wizard.ownsJoystick && g_wizard.joystick != nullptr) {
|
||||
SDL_CloseJoystick(g_wizard.joystick);
|
||||
}
|
||||
g_wizard = WizardState{};
|
||||
}
|
||||
|
||||
void StartWizard(SDL_JoystickID instance) {
|
||||
StopWizard();
|
||||
SDL_Joystick* joystick = nullptr;
|
||||
bool owns = false;
|
||||
if (SDL_Gamepad* gamepad = SDL_GetGamepadFromID(instance)) {
|
||||
joystick = SDL_GetGamepadJoystick(gamepad);
|
||||
} else {
|
||||
joystick = SDL_OpenJoystick(instance);
|
||||
owns = true;
|
||||
}
|
||||
if (joystick == nullptr) {
|
||||
RT_LOG(RT_TAG_CONFIG) << "controller wizard: failed to open joystick " << instance << ": "
|
||||
<< SDL_GetError() << std::endl;
|
||||
return;
|
||||
}
|
||||
g_wizard.active = true;
|
||||
g_wizard.instance = instance;
|
||||
g_wizard.joystick = joystick;
|
||||
g_wizard.ownsJoystick = owns;
|
||||
const char* name = SDL_GetJoystickNameForID(instance);
|
||||
g_wizard.deviceName = name != nullptr ? name : "Controller";
|
||||
g_wizard.acceptAfter = Clock::now() + kCaptureDebounce;
|
||||
SnapshotAxes();
|
||||
}
|
||||
|
||||
std::string BuildMappingString() {
|
||||
std::string name = g_wizard.deviceName;
|
||||
std::replace(name.begin(), name.end(), ',', ' ');
|
||||
std::string mapping = GuidString(g_wizard.instance) + "," + name + ",";
|
||||
for (size_t i = 0; i < kSteps.size(); ++i) {
|
||||
if (g_wizard.bindings[i]) {
|
||||
mapping += std::string(kSteps[i].mappingKey) + ":" + *g_wizard.bindings[i] + ",";
|
||||
}
|
||||
}
|
||||
mapping += "platform:Windows,";
|
||||
return mapping;
|
||||
}
|
||||
|
||||
bool PersistMapping(const std::string& guid, const std::string& mapping) {
|
||||
const std::filesystem::path path = MappingDbPath();
|
||||
std::vector<std::string> lines;
|
||||
{
|
||||
std::ifstream in(path);
|
||||
std::string line;
|
||||
while (std::getline(in, line)) {
|
||||
if (line.rfind(guid + ",", 0) != 0) {
|
||||
lines.push_back(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.push_back(mapping);
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
std::ofstream out(path, std::ios::trunc);
|
||||
if (!out) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& line : lines) {
|
||||
out << line << '\n';
|
||||
}
|
||||
out.close();
|
||||
return static_cast<bool>(out);
|
||||
}
|
||||
|
||||
void FinishWizard() {
|
||||
const std::string guid = GuidString(g_wizard.instance);
|
||||
const std::string mapping = BuildMappingString();
|
||||
if (SDL_AddGamepadMapping(mapping.c_str()) < 0) {
|
||||
g_wizard.status = std::string("Failed to apply mapping: ") + SDL_GetError();
|
||||
RT_LOG(RT_TAG_CONFIG) << "controller wizard: " << g_wizard.status << " (" << mapping << ")"
|
||||
<< std::endl;
|
||||
return;
|
||||
}
|
||||
if (!PersistMapping(guid, mapping)) {
|
||||
g_wizard.status = "Failed to save mapping to " + MappingDbPath().string();
|
||||
RT_LOG(RT_TAG_CONFIG) << "controller wizard: " << g_wizard.status << std::endl;
|
||||
return;
|
||||
}
|
||||
RT_LOG(RT_TAG_CONFIG) << "controller wizard: applied mapping " << mapping << std::endl;
|
||||
StopWizard();
|
||||
}
|
||||
|
||||
struct SetupCandidate {
|
||||
SDL_JoystickID id;
|
||||
std::string name;
|
||||
bool incompleteMapping;
|
||||
};
|
||||
|
||||
// A device needs setup when SDL has no gamepad mapping for it at all, or when
|
||||
// the mapping it matched has no analog stick even though the hardware reports
|
||||
// axes (SDL's built-in raphnet WUSBMote entry is button-only).
|
||||
std::vector<SetupCandidate> CollectCandidates() {
|
||||
std::vector<SetupCandidate> candidates;
|
||||
int count = 0;
|
||||
SDL_JoystickID* ids = SDL_GetJoysticks(&count);
|
||||
if (ids == nullptr) {
|
||||
return candidates;
|
||||
}
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const SDL_JoystickID id = ids[i];
|
||||
const char* rawName = SDL_GetJoystickNameForID(id);
|
||||
const std::string name = rawName != nullptr ? rawName : "Unknown controller";
|
||||
if (!SDL_IsGamepad(id)) {
|
||||
candidates.push_back({id, name, false});
|
||||
continue;
|
||||
}
|
||||
SDL_Gamepad* gamepad = SDL_GetGamepadFromID(id);
|
||||
if (gamepad == nullptr) {
|
||||
continue;
|
||||
}
|
||||
char* mapping = SDL_GetGamepadMappingForID(id);
|
||||
if (mapping == nullptr) {
|
||||
continue;
|
||||
}
|
||||
const std::string mappingStr = mapping;
|
||||
SDL_free(mapping);
|
||||
const bool hasStick = mappingStr.find("leftx:") != std::string::npos &&
|
||||
mappingStr.find("lefty:") != std::string::npos;
|
||||
SDL_Joystick* joystick = SDL_GetGamepadJoystick(gamepad);
|
||||
if (!hasStick && joystick != nullptr && SDL_GetNumJoystickAxes(joystick) >= 2) {
|
||||
candidates.push_back({id, name, true});
|
||||
}
|
||||
}
|
||||
SDL_free(ids);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
void HandleButtonDown(const SDL_JoyButtonEvent& event) {
|
||||
const Step& step = kSteps[g_wizard.stepIndex];
|
||||
if (step.kind == StepKind::Stick) {
|
||||
return;
|
||||
}
|
||||
const std::string value = "b" + std::to_string(event.button);
|
||||
if (BindingUsed(value)) {
|
||||
g_wizard.status = "That button is already bound";
|
||||
return;
|
||||
}
|
||||
g_wizard.status.clear();
|
||||
AdvanceStep(value);
|
||||
}
|
||||
|
||||
void HandleHatMotion(const SDL_JoyHatEvent& event) {
|
||||
const Step& step = kSteps[g_wizard.stepIndex];
|
||||
if (step.kind == StepKind::Stick) {
|
||||
return;
|
||||
}
|
||||
// Only single-direction presses bind cleanly; diagonals are ignored.
|
||||
if (event.value != SDL_HAT_UP && event.value != SDL_HAT_RIGHT && event.value != SDL_HAT_DOWN &&
|
||||
event.value != SDL_HAT_LEFT) {
|
||||
return;
|
||||
}
|
||||
const std::string value =
|
||||
"h" + std::to_string(event.hat) + "." + std::to_string(static_cast<int>(event.value));
|
||||
if (BindingUsed(value)) {
|
||||
g_wizard.status = "That direction is already bound";
|
||||
return;
|
||||
}
|
||||
g_wizard.status.clear();
|
||||
AdvanceStep(value);
|
||||
}
|
||||
|
||||
void HandleAxisMotion(const SDL_JoyAxisEvent& event) {
|
||||
const Step& step = kSteps[g_wizard.stepIndex];
|
||||
if (step.kind == StepKind::Button) {
|
||||
return;
|
||||
}
|
||||
if (event.axis >= g_wizard.axisBaseline.size()) {
|
||||
return;
|
||||
}
|
||||
const int32_t delta =
|
||||
static_cast<int32_t>(event.value) - static_cast<int32_t>(g_wizard.axisBaseline[event.axis]);
|
||||
const int16_t threshold = step.kind == StepKind::Stick ? kStickThreshold : kTriggerThreshold;
|
||||
if (std::abs(delta) < threshold) {
|
||||
return;
|
||||
}
|
||||
// Stick prompts ask for LEFT/UP, which SDL expects to be negative; triggers
|
||||
// are expected to increase when pulled. A wrong-way delta means the raw
|
||||
// axis is inverted, which the mapping expresses with a '~' suffix.
|
||||
const bool expectNegative = step.kind == StepKind::Stick;
|
||||
const bool inverted = expectNegative ? delta > 0 : delta < 0;
|
||||
std::string value = "a" + std::to_string(event.axis);
|
||||
// Reject reusing an axis already bound (with or without inversion).
|
||||
if (BindingUsed(value) || BindingUsed(value + "~")) {
|
||||
g_wizard.status = "That axis is already bound";
|
||||
return;
|
||||
}
|
||||
if (inverted) {
|
||||
value += "~";
|
||||
}
|
||||
g_wizard.status.clear();
|
||||
AdvanceStep(value);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void LoadPersistedMappings() {
|
||||
std::ifstream in(MappingDbPath());
|
||||
if (!in) {
|
||||
return;
|
||||
}
|
||||
std::string line;
|
||||
int added = 0;
|
||||
while (std::getline(in, line)) {
|
||||
const std::string trimmed = RuntimeConfigFile::Trim(line);
|
||||
if (trimmed.empty() || trimmed[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
if (SDL_AddGamepadMapping(trimmed.c_str()) >= 0) {
|
||||
++added;
|
||||
} else {
|
||||
RT_LOG(RT_TAG_CONFIG) << "gamecontrollerdb.txt: rejected mapping: " << trimmed
|
||||
<< " (" << SDL_GetError() << ")" << std::endl;
|
||||
}
|
||||
}
|
||||
if (added > 0) {
|
||||
RT_LOG(RT_TAG_CONFIG) << "gamecontrollerdb.txt: applied " << added << " custom mapping"
|
||||
<< (added == 1 ? "" : "s") << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void HandleSdlEvent(const SDL_Event& event) {
|
||||
if (!g_wizard.active) {
|
||||
return;
|
||||
}
|
||||
if (event.type == SDL_EVENT_JOYSTICK_REMOVED && event.jdevice.which == g_wizard.instance) {
|
||||
StopWizard();
|
||||
return;
|
||||
}
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||
StopWizard();
|
||||
return;
|
||||
}
|
||||
if (g_wizard.stepIndex >= kSteps.size() || Clock::now() < g_wizard.acceptAfter) {
|
||||
return;
|
||||
}
|
||||
switch (event.type) {
|
||||
case SDL_EVENT_JOYSTICK_BUTTON_DOWN:
|
||||
if (event.jbutton.which == g_wizard.instance) {
|
||||
HandleButtonDown(event.jbutton);
|
||||
}
|
||||
break;
|
||||
case SDL_EVENT_JOYSTICK_HAT_MOTION:
|
||||
if (event.jhat.which == g_wizard.instance) {
|
||||
HandleHatMotion(event.jhat);
|
||||
}
|
||||
break;
|
||||
case SDL_EVENT_JOYSTICK_AXIS_MOTION:
|
||||
if (event.jaxis.which == g_wizard.instance) {
|
||||
HandleAxisMotion(event.jaxis);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DrawSetupList() {
|
||||
const std::vector<SetupCandidate> candidates = CollectCandidates();
|
||||
if (candidates.empty()) {
|
||||
return;
|
||||
}
|
||||
ImGui::SeparatorText("Unrecognized controllers");
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
||||
ImGui::TextDisabled(
|
||||
"These devices have no usable gamepad mapping. Set one up by pressing "
|
||||
"each control when asked.");
|
||||
ImGui::PopTextWrapPos();
|
||||
for (const auto& candidate : candidates) {
|
||||
ImGui::PushID(static_cast<int>(candidate.id));
|
||||
ImGui::TextUnformatted(candidate.name.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(candidate.incompleteMapping ? "Fix mapping" : "Set up")) {
|
||||
StartWizard(candidate.id);
|
||||
}
|
||||
if (candidate.incompleteMapping && ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("SDL matched a mapping without an analog stick for this device");
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
void Draw() {
|
||||
if (!g_wizard.active) {
|
||||
return;
|
||||
}
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x + viewport->Size.x * 0.5f,
|
||||
viewport->Pos.y + viewport->Size.y * 0.5f),
|
||||
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
ImGui::SetNextWindowSize(ImVec2(420.0f, 0.0f), ImGuiCond_Appearing);
|
||||
bool open = true;
|
||||
if (ImGui::Begin("Controller setup", &open,
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings)) {
|
||||
ImGui::TextUnformatted(g_wizard.deviceName.c_str());
|
||||
ImGui::Separator();
|
||||
if (g_wizard.stepIndex < kSteps.size()) {
|
||||
ImGui::Text("Step %d of %d", static_cast<int>(g_wizard.stepIndex + 1),
|
||||
static_cast<int>(kSteps.size()));
|
||||
ImGui::Spacing();
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 400.0f);
|
||||
ImGui::TextUnformatted(kSteps[g_wizard.stepIndex].prompt);
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Skip")) {
|
||||
g_wizard.status.clear();
|
||||
AdvanceStep(std::nullopt);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(g_wizard.stepIndex == 0);
|
||||
if (ImGui::Button("Back")) {
|
||||
--g_wizard.stepIndex;
|
||||
g_wizard.bindings[g_wizard.stepIndex].reset();
|
||||
g_wizard.status.clear();
|
||||
g_wizard.acceptAfter = Clock::now() + kCaptureDebounce;
|
||||
SnapshotAxes();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel")) {
|
||||
open = false;
|
||||
}
|
||||
} else {
|
||||
const size_t boundCount =
|
||||
std::count_if(g_wizard.bindings.begin(), g_wizard.bindings.end(),
|
||||
[](const std::optional<std::string>& b) { return b.has_value(); });
|
||||
ImGui::Text("Captured %d of %d controls.", static_cast<int>(boundCount),
|
||||
static_cast<int>(kSteps.size()));
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 400.0f);
|
||||
ImGui::TextDisabled("Save applies the mapping now and remembers it for future launches.");
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Spacing();
|
||||
ImGui::BeginDisabled(boundCount == 0);
|
||||
if (ImGui::Button("Save")) {
|
||||
FinishWizard();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Back")) {
|
||||
--g_wizard.stepIndex;
|
||||
g_wizard.bindings[g_wizard.stepIndex].reset();
|
||||
g_wizard.acceptAfter = Clock::now() + kCaptureDebounce;
|
||||
SnapshotAxes();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel")) {
|
||||
open = false;
|
||||
}
|
||||
}
|
||||
if (!g_wizard.status.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.3f, 1.0f), "%s", g_wizard.status.c_str());
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
if (!open && g_wizard.active) {
|
||||
StopWizard();
|
||||
}
|
||||
}
|
||||
|
||||
bool IsActive() { return g_wizard.active; }
|
||||
|
||||
} // namespace controller_mapping_wizard
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "settings_overlay.h"
|
||||
#include "audio_backend.h"
|
||||
#include "controller_mapping_wizard.h"
|
||||
#include "game_graphics_options.h"
|
||||
#include "music_attenuation.h"
|
||||
#include "runtime_config.h"
|
||||
@@ -320,6 +321,7 @@ void DrawControllerSettings() {
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
controller_mapping_wizard::DrawSetupList();
|
||||
const uint32_t controllerCount = PADCount();
|
||||
if (controllerCount == 0) {
|
||||
ImGui::TextDisabled("No controller connected");
|
||||
@@ -840,6 +842,7 @@ void PersistDisplayModeIfChanged() {
|
||||
} // namespace
|
||||
|
||||
void InitializeRuntimeSettings() noexcept {
|
||||
controller_mapping_wizard::LoadPersistedMappings();
|
||||
ApplyConfiguredMappings();
|
||||
AudioBackend::Instance().SetMasterVolume(static_cast<float>(g_audioVolumePercent) / 100.0f);
|
||||
AudioBackend::Instance().SetMuted(g_audioMuted);
|
||||
@@ -872,6 +875,7 @@ void HandleEvents(const AuroraEvent* events) noexcept {
|
||||
if (ev->type != AURORA_SDL_EVENT) {
|
||||
continue;
|
||||
}
|
||||
controller_mapping_wizard::HandleSdlEvent(ev->sdl);
|
||||
if (IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) {
|
||||
SetTopBarVisible(!g_topBarVisible);
|
||||
}
|
||||
@@ -893,6 +897,10 @@ void Draw() noexcept {
|
||||
}
|
||||
DrawFpsOverlay();
|
||||
DrawTopBar();
|
||||
controller_mapping_wizard::Draw();
|
||||
// The wizard captures raw presses; keep them out of the game even when the
|
||||
// top bar is hidden mid-setup.
|
||||
PADBlockInput(g_topBarVisible || controller_mapping_wizard::IsActive());
|
||||
DrawStartupScreen();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user