From e20e74edda68625f6cc62ca83442e25cf7a1ca06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Fri, 6 Feb 2026 21:28:05 +0300 Subject: [PATCH] Wiimote to GunCon3 remapping support --- rpcs3/Emu/Io/GunCon3.cpp | 32 +++--- rpcs3/Emu/Io/GunCon3.h | 3 + rpcs3/Emu/Io/WiimoteManager.cpp | 101 +++++++++++++++++ rpcs3/Emu/Io/WiimoteManager.h | 39 +++++++ rpcs3/rpcs3qt/wiimote_settings_dialog.cpp | 128 ++++++++++++++++++++++ rpcs3/rpcs3qt/wiimote_settings_dialog.h | 3 + rpcs3/rpcs3qt/wiimote_settings_dialog.ui | 106 ++++++++++++++++++ 7 files changed, 398 insertions(+), 14 deletions(-) diff --git a/rpcs3/Emu/Io/GunCon3.cpp b/rpcs3/Emu/Io/GunCon3.cpp index f12bda848f..c26463cdad 100644 --- a/rpcs3/Emu/Io/GunCon3.cpp +++ b/rpcs3/Emu/Io/GunCon3.cpp @@ -3,6 +3,8 @@ #include "MouseHandler.h" #include #include +#include +#include #include "Emu/IdManager.h" #include "Emu/Io/guncon3_config.h" #include "Emu/Cell/lv2/sys_usbd.h" @@ -250,23 +252,25 @@ void usb_device_guncon3::interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, if (my_wiimote_index >= 0 && static_cast(my_wiimote_index) < states.size()) { const auto& ws = states[my_wiimote_index]; + const auto map = wm->get_mapping(); - if (ws.buttons & 0x0400) - { - gc.btn_trigger = 1; - } + auto is_pressed = [&](WiimoteButton btn) { return (ws.buttons & static_cast(btn)) != 0; }; + + if (is_pressed(map.trigger)) gc.btn_trigger = 1; // Wiimote to GunCon3 Button Mapping - if (ws.buttons & 0x0800) gc.btn_a1 = 1; // Wiimote A -> A1 - if (ws.buttons & 0x1000) gc.btn_a2 = 1; // Wiimote Minus -> A2 - if (ws.buttons & 0x0010) gc.btn_c1 = 1; // Wiimote Plus -> C1 - if (ws.buttons & 0x0200) gc.btn_b1 = 1; // Wiimote 1 -> B1 - if (ws.buttons & 0x0100) gc.btn_b2 = 1; // Wiimote 2 -> B2 - if (ws.buttons & 0x8000) gc.btn_b3 = 1; // Wiimote Home -> B3 - if (ws.buttons & 0x0001) gc.btn_a3 = 1; // Wiimote Left (D-pad) -> A3 - if (ws.buttons & 0x0002) gc.btn_c2 = 1; // Wiimote Right (D-pad) - if (ws.buttons & 0x0008) gc.btn_b1 = 1; // D-pad Up -> B1 (Alt) - if (ws.buttons & 0x0004) gc.btn_b2 = 1; // D-pad Down -> B2 (Alt) + if (is_pressed(map.a1)) gc.btn_a1 = 1; + if (is_pressed(map.a2)) gc.btn_a2 = 1; + if (is_pressed(map.a3)) gc.btn_a3 = 1; + if (is_pressed(map.b1)) gc.btn_b1 = 1; + if (is_pressed(map.b2)) gc.btn_b2 = 1; + if (is_pressed(map.b3)) gc.btn_b3 = 1; + if (is_pressed(map.c1)) gc.btn_c1 = 1; + if (is_pressed(map.c2)) gc.btn_c2 = 1; + + // Secondary / Hardcoded Alts (if kept in mapping struct) + if (is_pressed(map.b1_alt)) gc.btn_b1 = 1; + if (is_pressed(map.b2_alt)) gc.btn_b2 = 1; if (ws.ir[0].x < 1023) { diff --git a/rpcs3/Emu/Io/GunCon3.h b/rpcs3/Emu/Io/GunCon3.h index daa34415b1..dd82c1442e 100644 --- a/rpcs3/Emu/Io/GunCon3.h +++ b/rpcs3/Emu/Io/GunCon3.h @@ -1,6 +1,9 @@ #pragma once #include "Emu/Io/usb_device.h" +#include +#include +#include class usb_device_guncon3 : public usb_device_emulated { diff --git a/rpcs3/Emu/Io/WiimoteManager.cpp b/rpcs3/Emu/Io/WiimoteManager.cpp index f49f6155d8..0c94ff0f73 100644 --- a/rpcs3/Emu/Io/WiimoteManager.cpp +++ b/rpcs3/Emu/Io/WiimoteManager.cpp @@ -2,7 +2,9 @@ #include "WiimoteManager.h" #include "Emu/System.h" #include "Emu/system_config.h" +#include "Utilities/File.h" #include +#include // Nintendo static constexpr u16 VID_NINTENDO = 0x057e; @@ -135,10 +137,94 @@ bool WiimoteDevice::update() static WiimoteManager* s_instance = nullptr; +static std::string get_config_path() +{ + return fs::get_config_dir(true) + "wiimote.yml"; +} + +void WiimoteManager::load_config() +{ + fs::file f(get_config_path(), fs::read); + if (!f) return; + + std::string line; + std::stringstream ss(f.to_string()); + WiimoteGunConMapping map; + + auto parse_btn = [](const std::string& val) -> WiimoteButton { + return static_cast(std::strtoul(val.c_str(), nullptr, 0)); + }; + + while (std::getline(ss, line)) + { + auto pos = line.find(':'); + if (pos == std::string::npos) continue; + + std::string key = line.substr(0, pos); + std::string val = line.substr(pos + 1); + + // Trim whitespace + key.erase(0, key.find_first_not_of(" \t")); + key.erase(key.find_last_not_of(" \t") + 1); + val.erase(0, val.find_first_not_of(" \t")); + val.erase(val.find_last_not_of(" \t") + 1); + + if (key == "trigger") map.trigger = parse_btn(val); + else if (key == "a1") map.a1 = parse_btn(val); + else if (key == "a2") map.a2 = parse_btn(val); + else if (key == "a3") map.a3 = parse_btn(val); + else if (key == "b1") map.b1 = parse_btn(val); + else if (key == "b2") map.b2 = parse_btn(val); + else if (key == "b3") map.b3 = parse_btn(val); + else if (key == "c1") map.c1 = parse_btn(val); + else if (key == "c2") map.c2 = parse_btn(val); + } + + std::unique_lock lock(m_mutex); + m_mapping = map; +} + +void WiimoteManager::save_config() +{ + fs::file f(get_config_path(), fs::write + fs::create + fs::trunc); + if (!f) return; + + std::stringstream ss; + // Helper to write lines + auto write_line = [&](const char* key, WiimoteButton btn) { + ss << key << ": " << static_cast(btn) << "\n"; + }; + + { + std::shared_lock lock(m_mutex); + write_line("trigger", m_mapping.trigger); + write_line("a1", m_mapping.a1); + write_line("a2", m_mapping.a2); + write_line("a3", m_mapping.a3); + write_line("b1", m_mapping.b1); + write_line("b2", m_mapping.b2); + write_line("b3", m_mapping.b3); + write_line("c1", m_mapping.c1); + write_line("c2", m_mapping.c2); + } + + f.write(ss.str()); +} + WiimoteManager::WiimoteManager() { if (!s_instance) s_instance = this; + + // Set default mapping explicitly to match user preference: C1=Plus, A3=Left + // (Struct default constructor might have different values if I didn't edit header defaults) + // Let's force it here before loading config. + m_mapping.c1 = WiimoteButton::Plus; + m_mapping.a3 = WiimoteButton::Left; + // Defaults for others from struct: + // a1=A, a2=Minus, etc. + + load_config(); } WiimoteManager::~WiimoteManager() @@ -177,6 +263,21 @@ size_t WiimoteManager::get_device_count() return m_devices.size(); } +void WiimoteManager::set_mapping(const WiimoteGunConMapping& mapping) +{ + { + std::unique_lock lock(m_mutex); + m_mapping = mapping; + } + save_config(); +} + +WiimoteGunConMapping WiimoteManager::get_mapping() const +{ + // shared_lock not strictly needed for trivial copy but good practice if it becomes complex + return m_mapping; +} + std::vector WiimoteManager::get_states() { std::shared_lock lock(m_mutex); diff --git a/rpcs3/Emu/Io/WiimoteManager.h b/rpcs3/Emu/Io/WiimoteManager.h index 9decda4dea..a9188b1e46 100644 --- a/rpcs3/Emu/Io/WiimoteManager.h +++ b/rpcs3/Emu/Io/WiimoteManager.h @@ -18,6 +18,39 @@ struct WiimoteIRPoint u8 size = 0; }; +enum class WiimoteButton : u16 +{ + None = 0, + Left = 0x0001, + Right = 0x0002, + Down = 0x0004, + Up = 0x0008, + Plus = 0x0010, + Two = 0x0100, + One = 0x0200, + B = 0x0400, + A = 0x0800, + Minus = 0x1000, + Home = 0x8000 +}; + +struct WiimoteGunConMapping +{ + WiimoteButton trigger = WiimoteButton::B; + WiimoteButton a1 = WiimoteButton::A; + WiimoteButton a2 = WiimoteButton::Minus; + WiimoteButton a3 = WiimoteButton::Left; + WiimoteButton b1 = WiimoteButton::One; + WiimoteButton b2 = WiimoteButton::Two; + WiimoteButton b3 = WiimoteButton::Home; + WiimoteButton c1 = WiimoteButton::Plus; + WiimoteButton c2 = WiimoteButton::Right; + + // Secondary mappings (optional, e.g. D-Pad acting as buttons) + WiimoteButton b1_alt = WiimoteButton::Up; + WiimoteButton b2_alt = WiimoteButton::Down; +}; + struct WiimoteState { u16 buttons = 0; @@ -60,11 +93,17 @@ public: std::vector get_states(); size_t get_device_count(); + void set_mapping(const WiimoteGunConMapping& mapping); + WiimoteGunConMapping get_mapping() const; + private: std::thread m_thread; atomic_t m_running{false}; std::vector> m_devices; shared_mutex m_mutex; + WiimoteGunConMapping m_mapping; void thread_proc(); + void load_config(); + void save_config(); }; diff --git a/rpcs3/rpcs3qt/wiimote_settings_dialog.cpp b/rpcs3/rpcs3qt/wiimote_settings_dialog.cpp index a42f9af8ae..2c323ddb2a 100644 --- a/rpcs3/rpcs3qt/wiimote_settings_dialog.cpp +++ b/rpcs3/rpcs3qt/wiimote_settings_dialog.cpp @@ -13,14 +13,142 @@ wiimote_settings_dialog::wiimote_settings_dialog(QWidget* parent) ui->setupUi(this); update_list(); connect(ui->scanButton, &QPushButton::clicked, this, [this] { update_list(); }); + connect(ui->restoreDefaultsButton, &QPushButton::clicked, this, &wiimote_settings_dialog::restore_defaults); QTimer* timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &wiimote_settings_dialog::update_state); timer->start(50); + + populate_mappings(); } wiimote_settings_dialog::~wiimote_settings_dialog() = default; +void wiimote_settings_dialog::populate_mappings() +{ + auto* wm = WiimoteManager::get_instance(); + if (!wm) return; + + const QPair buttons[] = { + { tr("None"), WiimoteButton::None }, + { tr("A"), WiimoteButton::A }, + { tr("B"), WiimoteButton::B }, + { tr("Plus (+)"), WiimoteButton::Plus }, + { tr("Minus (-)"), WiimoteButton::Minus }, + { tr("Home"), WiimoteButton::Home }, + { tr("1"), WiimoteButton::One }, + { tr("2"), WiimoteButton::Two }, + { tr("D-Pad Up"), WiimoteButton::Up }, + { tr("D-Pad Down"), WiimoteButton::Down }, + { tr("D-Pad Left"), WiimoteButton::Left }, + { tr("D-Pad Right"), WiimoteButton::Right }, + }; + + QComboBox* boxes[] = { + ui->cb_trigger, ui->cb_a1, ui->cb_a2, ui->cb_c1, + ui->cb_b1, ui->cb_b2, ui->cb_b3, ui->cb_a3, ui->cb_c2 + }; + + WiimoteGunConMapping current = wm->get_mapping(); + WiimoteButton* targets[] = { + ¤t.trigger, ¤t.a1, ¤t.a2, ¤t.c1, + ¤t.b1, ¤t.b2, ¤t.b3, ¤t.a3, ¤t.c2 + }; + + for (int i = 0; i < 9; ++i) + { + boxes[i]->setMinimumWidth(150); // Make combo boxes wider for better readability + + for (const auto& pair : buttons) + { + boxes[i]->addItem(pair.first, QVariant::fromValue(static_cast(pair.second))); + } + + // Set current selection + int index = boxes[i]->findData(QVariant::fromValue(static_cast(*targets[i]))); + if (index >= 0) boxes[i]->setCurrentIndex(index); + + // Connect change signal + connect(boxes[i], QOverload::of(&QComboBox::currentIndexChanged), this, [this](int) { + apply_mappings(); + }); + } +} + +void wiimote_settings_dialog::restore_defaults() +{ + auto* wm = WiimoteManager::get_instance(); + if (!wm) return; + + // Reset to default mapping + WiimoteGunConMapping default_map; + wm->set_mapping(default_map); + + // Update UI + ui->cb_trigger->blockSignals(true); + ui->cb_a1->blockSignals(true); + ui->cb_a2->blockSignals(true); + ui->cb_c1->blockSignals(true); + ui->cb_b1->blockSignals(true); + ui->cb_b2->blockSignals(true); + ui->cb_b3->blockSignals(true); + ui->cb_a3->blockSignals(true); + ui->cb_c2->blockSignals(true); + + auto set_box = [](QComboBox* box, WiimoteButton btn) { + int index = box->findData(QVariant::fromValue(static_cast(btn))); + if (index >= 0) box->setCurrentIndex(index); + }; + + set_box(ui->cb_trigger, default_map.trigger); + set_box(ui->cb_a1, default_map.a1); + set_box(ui->cb_a2, default_map.a2); + set_box(ui->cb_c1, default_map.c1); + set_box(ui->cb_b1, default_map.b1); + set_box(ui->cb_b2, default_map.b2); + set_box(ui->cb_b3, default_map.b3); + set_box(ui->cb_a3, default_map.a3); + set_box(ui->cb_c2, default_map.c2); + + ui->cb_trigger->blockSignals(false); + ui->cb_a1->blockSignals(false); + ui->cb_a2->blockSignals(false); + ui->cb_c1->blockSignals(false); + ui->cb_b1->blockSignals(false); + ui->cb_b2->blockSignals(false); + ui->cb_b3->blockSignals(false); + ui->cb_a3->blockSignals(false); + ui->cb_c2->blockSignals(false); +} + +void wiimote_settings_dialog::apply_mappings() +{ + auto* wm = WiimoteManager::get_instance(); + if (!wm) return; + + WiimoteGunConMapping map; + auto get_btn = [](QComboBox* box) { + return static_cast(box->currentData().toUInt()); + }; + + map.trigger = get_btn(ui->cb_trigger); + map.a1 = get_btn(ui->cb_a1); + map.a2 = get_btn(ui->cb_a2); + map.c1 = get_btn(ui->cb_c1); + map.b1 = get_btn(ui->cb_b1); + map.b2 = get_btn(ui->cb_b2); + map.b3 = get_btn(ui->cb_b3); + map.a3 = get_btn(ui->cb_a3); + map.c2 = get_btn(ui->cb_c2); + + // Preserve alts or add UI for them later. For now, keep defaults or sync with main if matched + // To be safe, we can just leave alts as default Up/Down for now since they are D-Pad shortcuts + // Or we can reset them if the user maps Up/Down to something else to avoid conflict? + // For simplicity, let's keep defaults in the struct constructor. + + wm->set_mapping(map); +} + void wiimote_settings_dialog::update_state() { int index = ui->wiimoteList->currentRow(); diff --git a/rpcs3/rpcs3qt/wiimote_settings_dialog.h b/rpcs3/rpcs3qt/wiimote_settings_dialog.h index db7b76e9f5..3c02af7317 100644 --- a/rpcs3/rpcs3qt/wiimote_settings_dialog.h +++ b/rpcs3/rpcs3qt/wiimote_settings_dialog.h @@ -14,4 +14,7 @@ private: std::unique_ptr ui; void update_list(); void update_state(); + void populate_mappings(); + void apply_mappings(); + void restore_defaults(); }; diff --git a/rpcs3/rpcs3qt/wiimote_settings_dialog.ui b/rpcs3/rpcs3qt/wiimote_settings_dialog.ui index 74a4560333..cec29d02a1 100644 --- a/rpcs3/rpcs3qt/wiimote_settings_dialog.ui +++ b/rpcs3/rpcs3qt/wiimote_settings_dialog.ui @@ -77,6 +77,105 @@ + + + + Button Mapping (GunCon3 <- Wiimote) + + + + + + Trigger: + + + + + + + + + + A1 (Left Side): + + + + + + + + + + A2 (Left Side): + + + + + + + + + + C1 (Start - Left Side): + + + + + + + + + + B1 (Right Side): + + + + + + + + + + B2 (Right Side): + + + + + + + + + + B3 (Right Side): + + + + + + + + + + A3 (Analog Button): + + + + + + + + + + C2 (Select - Analog Button): + + + + + + + + + @@ -114,6 +213,13 @@ + + + + Restore Defaults + + +