This commit is contained in:
Frog Business 2026-03-10 09:31:30 +03:00 committed by GitHub
commit ca8c8e63c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1513 additions and 75 deletions

View file

@ -402,6 +402,7 @@ target_sources(rpcs3_emu PRIVATE
Io/GHLtar.cpp
Io/GunCon3.cpp
Io/Infinity.cpp
Io/wiimote_config.cpp
Io/interception.cpp
Io/KamenRider.cpp
Io/KeyboardHandler.cpp
@ -652,6 +653,7 @@ target_link_libraries(rpcs3_emu
3rdparty::vulkan
3rdparty::glew
3rdparty::libusb
3rdparty::hidapi
3rdparty::wolfssl
3rdparty::openal
3rdparty::cubeb

View file

@ -3,9 +3,15 @@
#include "MouseHandler.h"
#include "Emu/IdManager.h"
#include "Emu/Io/guncon3_config.h"
#include "Emu/Io/wiimote_config.h"
#include "Emu/Cell/lv2/sys_usbd.h"
#include "Emu/system_config.h"
#include "Emu/RSX/Overlays/overlay_cursor.h"
#include "Input/wiimote_handler.h"
#include "Input/pad_thread.h"
#include <cmath>
#include <climits>
#include <algorithm>
LOG_CHANNEL(guncon3_log);
@ -127,6 +133,18 @@ usb_device_guncon3::usb_device_guncon3(u32 controller_index, const std::array<u8
: usb_device_emulated(location)
, m_controller_index(controller_index)
{
{
std::lock_guard lock(s_instances_mutex);
s_instances.push_back(this);
// Sort instances by controller index (P1 < P2 < P3...)
// This ensures that the first available GunCon (e.g. at P3) takes the first Wiimote,
// and the second (e.g. at P4) takes the second Wiimote.
std::sort(s_instances.begin(), s_instances.end(), [](auto* a, auto* b)
{
return a->m_controller_index < b->m_controller_index;
});
}
device = UsbDescriptorNode(USB_DESCRIPTOR_DEVICE,
UsbDeviceDescriptor {
.bcdUSB = 0x0110,
@ -174,6 +192,11 @@ usb_device_guncon3::usb_device_guncon3(u32 controller_index, const std::array<u8
usb_device_guncon3::~usb_device_guncon3()
{
std::lock_guard lock(s_instances_mutex);
if (auto it = std::find(s_instances.begin(), s_instances.end(), this); it != s_instances.end())
{
s_instances.erase(it);
}
}
void usb_device_guncon3::control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer)
@ -188,6 +211,81 @@ void usb_device_guncon3::control_transfer(u8 bmRequestType, u8 bRequest, u16 wVa
extern bool is_input_allowed();
bool usb_device_guncon3::handle_wiimote(GunCon3_data& gc)
{
if (!get_wiimote_config().use_for_guncon.get())
return false;
auto* wm = wiimote_handler::get_instance();
auto states = wm->get_states();
// Determine which Wiimote to use based on our ordinal position among all GunCons
s64 my_wiimote_index = -1;
{
std::lock_guard lock(s_instances_mutex);
auto found = std::find(s_instances.begin(), s_instances.end(), this);
if (found != s_instances.end())
{
my_wiimote_index = std::distance(s_instances.begin(), found);
}
}
if (my_wiimote_index < 0 || static_cast<usz>(my_wiimote_index) >= states.size())
return false;
const auto& ws = states[my_wiimote_index];
if (!ws.connected)
return false;
const auto& map = get_wiimote_config().guncon_mapping;
const auto is_pressed = [&](wiimote_button btn) { return (ws.buttons & static_cast<u16>(btn)) != 0; };
if (is_pressed(map.trigger.get())) gc.btn_trigger = 1;
// Wiimote to GunCon3 Button Mapping
if (is_pressed(map.a1.get())) gc.btn_a1 = 1;
if (is_pressed(map.a2.get())) gc.btn_a2 = 1;
if (is_pressed(map.a3.get())) gc.btn_a3 = 1;
if (is_pressed(map.b1.get())) gc.btn_b1 = 1;
if (is_pressed(map.b2.get())) gc.btn_b2 = 1;
if (is_pressed(map.b3.get())) gc.btn_b3 = 1;
if (is_pressed(map.c1.get())) gc.btn_c1 = 1;
if (is_pressed(map.c2.get())) gc.btn_c2 = 1;
// Secondary / Hardcoded Alts
if (is_pressed(map.b1_alt.get())) gc.btn_b1 = 1;
if (is_pressed(map.b2_alt.get())) gc.btn_b2 = 1;
if (ws.ir[0].x < 1023)
{
// Map Wiimote IR (0..1023) to GunCon3 range (-32768..32767)
const s32 raw_x = ws.ir[0].x;
const s32 raw_y = ws.ir[0].y;
const s32 x_res = SHRT_MAX - (raw_x * USHRT_MAX / 1023);
const s32 y_res = SHRT_MAX - (raw_y * USHRT_MAX / 767);
gc.gun_x = static_cast<s16>(std::clamp(x_res, SHRT_MIN, SHRT_MAX));
gc.gun_y = static_cast<s16>(std::clamp(y_res, SHRT_MIN, SHRT_MAX));
if (g_cfg.io.show_move_cursor)
{
const s16 ax = static_cast<s16>((gc.gun_x + SHRT_MAX + 1) * rsx::overlays::overlay::virtual_width / USHRT_MAX);
const s16 ay = static_cast<s16>((SHRT_MAX - gc.gun_y) * rsx::overlays::overlay::virtual_height / USHRT_MAX);
rsx::overlays::set_cursor(rsx::overlays::cursor_offset::cell_gem + my_wiimote_index, ax, ay, { 1.0f, 1.0f, 1.0f, 1.0f }, 100'000, false);
}
if (ws.ir[1].x < 1023)
{
const s32 dx = static_cast<s32>(ws.ir[0].x) - ws.ir[1].x;
const s32 dy = static_cast<s32>(ws.ir[0].y) - ws.ir[1].y;
gc.gun_z = static_cast<s16>(std::sqrt(dx * dx + dy * dy));
}
}
return true;
}
void usb_device_guncon3::interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, UsbTransfer* transfer)
{
transfer->fake = true;
@ -213,6 +311,12 @@ void usb_device_guncon3::interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint,
return;
}
if (handle_wiimote(gc))
{
guncon3_encode(&gc, buf, m_key.data());
return;
}
if (g_cfg.io.mouse == mouse_handler::null)
{
guncon3_log.warning("GunCon3 requires a Mouse Handler enabled");

View file

@ -1,6 +1,8 @@
#pragma once
#include "Emu/Io/usb_device.h"
#include <vector>
#include <mutex>
class usb_device_guncon3 : public usb_device_emulated
{
@ -14,4 +16,9 @@ public:
private:
u32 m_controller_index;
std::array<u8, 8> m_key{};
bool handle_wiimote(struct GunCon3_data& gc);
static inline std::vector<usb_device_guncon3*> s_instances;
static inline std::mutex s_instances_mutex;
};

View file

@ -0,0 +1,58 @@
#include "stdafx.h"
#include "wiimote_config.h"
#include "Utilities/File.h"
LOG_CHANNEL(wiimote_log, "Wiimote");
template <>
void fmt_class_string<wiimote_button>::format(std::string& out, u64 arg)
{
format_enum(out, arg, [](wiimote_button value)
{
switch (value)
{
case wiimote_button::None: return "None";
case wiimote_button::Left: return "Left";
case wiimote_button::Right: return "Right";
case wiimote_button::Down: return "Down";
case wiimote_button::Up: return "Up";
case wiimote_button::Plus: return "Plus";
case wiimote_button::Two: return "Two";
case wiimote_button::One: return "One";
case wiimote_button::B: return "B";
case wiimote_button::A: return "A";
case wiimote_button::Minus: return "Minus";
case wiimote_button::Home: return "Home";
}
return unknown;
});
}
cfg_wiimote& get_wiimote_config()
{
static cfg_wiimote instance;
return instance;
}
cfg_wiimote::cfg_wiimote()
: cfg::node()
, path(fs::get_config_dir(true) + "wiimote.yml")
{
}
bool cfg_wiimote::load()
{
if (fs::file f{path, fs::read})
{
return cfg::node::from_string(f.to_string());
}
return false;
}
void cfg_wiimote::save() const
{
if (!cfg::node::save(path))
{
wiimote_log.error("Failed to save wiimote config to '%s'", path);
}
}

View file

@ -0,0 +1,35 @@
#pragma once
#include "Utilities/Config.h"
#include "Input/wiimote_types.h"
struct cfg_wiimote : cfg::node
{
cfg_wiimote();
bool load();
void save() const;
cfg::_bool use_for_guncon{ this, "UseForGunCon", true };
struct node_guncon_mapping : cfg::node
{
node_guncon_mapping(cfg::node* _parent) : cfg::node(_parent, "GunCon Mapping") {}
cfg::_enum<wiimote_button> trigger{ this, "Trigger", wiimote_button::B };
cfg::_enum<wiimote_button> a1{ this, "A1", wiimote_button::A };
cfg::_enum<wiimote_button> a2{ this, "A2", wiimote_button::Minus };
cfg::_enum<wiimote_button> a3{ this, "A3", wiimote_button::Left };
cfg::_enum<wiimote_button> b1{ this, "B1", wiimote_button::One };
cfg::_enum<wiimote_button> b2{ this, "B2", wiimote_button::Two };
cfg::_enum<wiimote_button> b3{ this, "B3", wiimote_button::Home };
cfg::_enum<wiimote_button> c1{ this, "C1", wiimote_button::Plus };
cfg::_enum<wiimote_button> c2{ this, "C2", wiimote_button::Right };
cfg::_enum<wiimote_button> b1_alt{ this, "B1_Alt", wiimote_button::Up };
cfg::_enum<wiimote_button> b2_alt{ this, "B2_Alt", wiimote_button::Down };
} guncon_mapping{ this };
const std::string path;
};
cfg_wiimote& get_wiimote_config();

View file

@ -11,7 +11,8 @@ namespace rsx
enum cursor_offset : u32
{
cell_gem = 0, // CELL_GEM_MAX_NUM = 4 Move controllers
last = 4
wiimote_ir = 4, // 4 points per Wiimote * 4 Wiimotes (up to 16 points)
last = 20
};
class cursor_item

View file

@ -0,0 +1,104 @@
#include "stdafx.h"
#include "hid_instance.h"
#include "util/logs.hpp"
#include "Emu/System.h"
#include "Utilities/Thread.h"
#if defined(__APPLE__)
#include "3rdparty/hidapi/hidapi/mac/hidapi_darwin.h"
#endif
LOG_CHANNEL(hid_log, "HID");
std::mutex g_hid_mutex;
hid_instance::~hid_instance()
{
std::lock_guard lock(m_hid_mutex);
// Only exit HIDAPI once on exit. HIDAPI uses a global state internally...
if (m_initialized)
{
hid_log.notice("Exiting HIDAPI...");
if (hid_exit() != 0)
{
hid_log.error("hid_exit failed!");
}
}
}
bool hid_instance::initialize()
{
std::lock_guard lock(m_hid_mutex);
// Only init HIDAPI once. HIDAPI uses a global state internally...
if (m_initialized)
{
return true;
}
hid_log.notice("Initializing HIDAPI ...");
#if defined(__APPLE__)
int error_code = 0;
Emu.BlockingCallFromMainThread([&error_code]()
{
error_code = hid_init();
hid_darwin_set_open_exclusive(0);
}, false);
#else
const int error_code = hid_init();
#endif
if (error_code != 0)
{
hid_log.fatal("hid_init error %d: %s", error_code, hid_error(nullptr));
return false;
}
m_initialized = true;
return true;
}
void hid_instance::enumerate_devices(u16 vid, u16 pid, std::function<void(hid_device_info*)> callback)
{
std::lock_guard lock(g_hid_mutex);
#if defined(__APPLE__)
Emu.BlockingCallFromMainThread([&]()
{
hid_device_info* devs = hid_enumerate(vid, pid);
callback(devs);
hid_free_enumeration(devs);
}, false);
#else
hid_device_info* devs = hid_enumerate(vid, pid);
callback(devs);
hid_free_enumeration(devs);
#endif
}
hid_device* hid_instance::open_path(const char* path)
{
std::lock_guard lock(g_hid_mutex);
#if defined(__APPLE__)
if (!thread_ctrl::is_main())
{
hid_device* dev = nullptr;
Emu.BlockingCallFromMainThread([&]() { dev = hid_open_path(path); }, false);
return dev;
}
#endif
return hid_open_path(path);
}
void hid_instance::close(hid_device* dev)
{
if (!dev) return;
std::lock_guard lock(g_hid_mutex);
#if defined(__APPLE__)
Emu.BlockingCallFromMainThread([&]() { hid_close(dev); }, false);
#else
hid_close(dev);
#endif
}

View file

@ -0,0 +1,32 @@
#pragma once
#include "util/types.hpp"
#include <mutex>
#include <functional>
#include <hidapi.h>
struct hid_instance
{
public:
hid_instance() = default;
~hid_instance();
static hid_instance& get_instance()
{
static hid_instance instance {};
return instance;
}
bool initialize();
static void enumerate_devices(u16 vid, u16 pid, std::function<void(hid_device_info*)> callback);
static hid_device* open_path(const char* path);
static void close(hid_device* dev);
private:
bool m_initialized = false;
std::mutex m_hid_mutex;
};
extern std::mutex g_hid_mutex;

View file

@ -4,15 +4,11 @@
#include "dualsense_pad_handler.h"
#include "skateboard_pad_handler.h"
#include "ps_move_handler.h"
#include "hid_instance.h"
#include "util/logs.hpp"
#include "Utilities/Timer.h"
#include "Emu/System.h"
#include "pad_thread.h"
#if defined(__APPLE__)
#include "3rdparty/hidapi/hidapi/mac/hidapi_darwin.h"
#endif
#include <algorithm>
#include <memory>
@ -23,74 +19,6 @@ std::vector<android_usb_device> g_android_usb_devices;
std::mutex g_android_usb_devices_mutex;
#endif
// Global mutex to allow "hid_enumerate()" and "hid_open_path()" are accessed by one thread at a time
// (e.g. thread running "process()" and thread running enumerate_devices()).
// It avoids the emulation crash in case the controller gets disconnected (e.g. due to inactivity)
std::mutex g_hid_mutex;
struct hid_instance
{
public:
hid_instance() = default;
~hid_instance()
{
std::lock_guard lock(m_hid_mutex);
// Only exit HIDAPI once on exit. HIDAPI uses a global state internally...
if (m_initialized)
{
hid_log.notice("Exiting HIDAPI...");
if (hid_exit() != 0)
{
hid_log.error("hid_exit failed!");
}
}
}
static hid_instance& get_instance()
{
static hid_instance instance {};
return instance;
}
bool initialize()
{
std::lock_guard lock(m_hid_mutex);
// Only init HIDAPI once. HIDAPI uses a global state internally...
if (m_initialized)
{
return true;
}
hid_log.notice("Initializing HIDAPI ...");
#if defined(__APPLE__)
int error_code = 0;
Emu.BlockingCallFromMainThread([&error_code]()
{
error_code = hid_init();
hid_darwin_set_open_exclusive(0);
}, false);
#else
const int error_code = hid_init();
#endif
if (error_code != 0)
{
hid_log.fatal("hid_init error %d: %s", error_code, hid_error(nullptr));
return false;
}
m_initialized = true;
return true;
}
private:
bool m_initialized = false;
std::mutex m_hid_mutex;
};
hid_device* HidDevice::open()
{
// Lock before calling "hid_open_path()"

View file

@ -0,0 +1,392 @@
#include "stdafx.h"
#include "wiimote_handler.h"
#include "Input/hid_instance.h"
#include "Emu/System.h"
#include "Emu/Io/wiimote_config.h"
#include "util/logs.hpp"
#include <algorithm>
#include <initializer_list>
#include <vector>
LOG_CHANNEL(wiimote_log, "Wiimote");
// Nintendo
static constexpr u16 VID_NINTENDO = 0x057e;
static constexpr u16 PID_WIIMOTE = 0x0306;
static constexpr u16 PID_WIIMOTE_PLUS = 0x0330;
// Mayflash DolphinBar
static constexpr u16 VID_MAYFLASH = 0x0079;
static constexpr u16 PID_DOLPHINBAR_START = 0x1800;
static constexpr u16 PID_DOLPHINBAR_END = 0x1803;
wiimote_device::wiimote_device()
{
m_state.connected = false;
}
wiimote_device::~wiimote_device()
{
close();
}
bool wiimote_device::open(hid_device_info* info)
{
if (m_handle) return false;
m_path = info->path;
m_serial = info->serial_number ? info->serial_number : L"";
m_handle = hid_instance::open_path(info->path);
if (m_handle)
{
// 1. Connectivity Test (Matching wiimote_test)
constexpr std::array<u8, 2> status_req = { 0x15, 0x00 };
if (hid_write(m_handle, status_req.data(), status_req.size()) < 0)
{
close();
return false;
}
// 2. Full Initialization
if (initialize_ir())
{
m_state.connected = true;
m_last_ir_check = std::chrono::steady_clock::now();
return true;
}
close();
}
return false;
}
void wiimote_device::close()
{
hid_instance::close(m_handle);
m_handle = nullptr;
m_state = {}; // Reset state including connected = false
m_path.clear();
m_serial.clear();
}
bool wiimote_device::write_reg(u32 addr, const std::vector<u8>& data)
{
u8 buf[22] = {0};
buf[0] = 0x16; // Write register
buf[1] = 0x06; // Register Space + Request Acknowledgement
buf[2] = (addr >> 16) & 0xFF;
buf[3] = (addr >> 8) & 0xFF;
buf[4] = addr & 0xFF;
buf[5] = static_cast<u8>(data.size());
std::copy(data.begin(), data.end(), &buf[6]);
if (hid_write(m_handle, buf, sizeof(buf)) < 0) return false;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
return true;
}
bool wiimote_device::initialize_ir()
{
// 1. Enable IR logic / Pixel Clock (Requesting Acknowledgement for stability)
constexpr std::array<u8, 2> ir_on1 = { 0x13, 0x06 };
hid_write(m_handle, ir_on1.data(), ir_on1.size());
thread_ctrl::wait_for(50'000);
constexpr std::array<u8, 2> ir_on2 = { 0x1a, 0x06 };
hid_write(m_handle, ir_on2.data(), ir_on2.size());
thread_ctrl::wait_for(50'000);
// 2. Enable IR Camera (Wii-style sequence)
if (!write_reg(0xb00030, {0x01})) return false;
// 3. Sensitivity Level 3 (Exactly matching wiimote_test / official levels)
if (!write_reg(0xb00000, {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x00, 0x41})) return false;
if (!write_reg(0xb0001a, {0x40, 0x00})) return false;
// 4. IR Mode: Extended (3 bytes per point)
if (!write_reg(0xb00033, {0x03})) return false;
// 5. Finalize IR Enable
if (!write_reg(0xb00030, {0x08})) return false;
// 6. Reporting mode: Buttons + Accel + IR (Continuous)
constexpr std::array<u8, 3> mode = { 0x12, 0x04, 0x33 };
if (hid_write(m_handle, mode.data(), mode.size()) < 0) return false;
thread_ctrl::wait_for(100'000);
return true;
}
bool wiimote_device::verify_ir_health()
{
if (!m_handle) return false;
// Request device status to verify communication
constexpr std::array<u8, 2> status_req = { 0x15, 0x00 };
if (hid_write(m_handle, status_req.data(), status_req.size()) < 0)
{
return false;
}
// Try to read a response within reasonable time
u8 buf[22];
const int res = hid_read_timeout(m_handle, buf, sizeof(buf), 100);
// If we got a response, device is alive. If timeout or error, it's dead.
return res > 0;
}
bool wiimote_device::update()
{
if (!m_handle) return false;
// Every 3 seconds, verify IR is still working
const auto now = std::chrono::steady_clock::now();
if (now - m_last_ir_check > std::chrono::seconds(3))
{
m_last_ir_check = now;
if (!verify_ir_health())
{
// Device not responding - attempt to reinitialize IR
if (!initialize_ir())
{
// Failed to reinitialize - close and mark disconnected
close();
return false;
}
}
}
u8 buf[22];
int res;
// Fully drain the buffer until empty to ensure we have the most recent data.
// This avoids getting stuck behind a backlog of old reports (e.g. from before IR was enabled).
while ((res = hid_read_timeout(m_handle, buf, sizeof(buf), 0)) > 0)
{
// All data reports (0x30-0x3F) carry buttons in the same location (first 2 bytes).
// We mask out accelerometer LSBs (bits 5,6 of both bytes).
if ((buf[0] & 0xF0) == 0x30)
{
m_state.buttons = (buf[1] | (buf[2] << 8)) & 0x9F1F;
}
// Mode 0x33: Buttons + Accel + IR (Extended Format)
if (buf[0] == 0x33)
{
// Parse Accelerometer (byte 1: bits 5,6 are X LSBs; byte 2: bit 5 Y LSB, bit 6 Z LSB)
m_state.acc_x = (buf[3] << 2) | ((buf[1] >> 5) & 3);
m_state.acc_y = (buf[4] << 2) | ((buf[2] >> 5) & 1);
m_state.acc_z = (buf[5] << 2) | ((buf[2] >> 6) & 1);
// Each IR point is 3 bytes in Extended report 0x33.
for (usz j = 0; j < MAX_WIIMOTE_IR_POINTS; j++)
{
const u8* ir = &buf[6 + j * 3];
m_state.ir[j].x = (ir[0] | ((ir[2] & 0x30) << 4));
m_state.ir[j].y = (ir[1] | ((ir[2] & 0xC0) << 2));
m_state.ir[j].size = ir[2] & 0x0f;
}
}
}
// hid_read_timeout returns -1 on error (e.g. device disconnected).
if (res < 0)
{
// Device error - try to recover once before giving up
if (!initialize_ir())
{
close();
return false;
}
}
return true;
}
static std::unique_ptr<wiimote_handler> s_instance;
wiimote_handler::wiimote_handler()
{
// Pre-initialize Wiimote slots (standard for DolphinBar and typical local multiplayer)
for (usz i = 0; i < MAX_WIIMOTES; i++)
{
m_devices.push_back(std::make_unique<wiimote_device>());
}
get_wiimote_config().load();
}
wiimote_handler::~wiimote_handler()
{
stop();
}
wiimote_handler* wiimote_handler::get_instance()
{
static std::mutex mtx;
std::lock_guard lock(mtx);
if (!s_instance)
{
s_instance = std::make_unique<wiimote_handler>();
s_instance->start();
}
return s_instance.get();
}
void wiimote_handler::start()
{
if (m_running) return;
// Note: initialize() is thread-safe and handles multiple calls.
if (!hid_instance::get_instance().initialize()) return;
m_running = true;
m_thread = std::make_unique<named_thread<std::function<void()>>>("WiiMoteManager", [this]() { thread_proc(); });
}
void wiimote_handler::stop()
{
m_running = false;
m_thread.reset();
}
usz wiimote_handler::get_device_count()
{
std::shared_lock lock(m_mutex);
return m_devices.size();
}
std::vector<wiimote_state> wiimote_handler::get_states()
{
std::shared_lock lock(m_mutex);
std::vector<wiimote_state> states;
states.reserve(m_devices.size());
for (const auto& dev : m_devices)
{
states.push_back(dev->get_state());
}
return states;
}
void wiimote_handler::thread_proc()
{
u32 counter = 0;
while (m_running && thread_ctrl::state() != thread_state::aborting)
{
// Scan every 2 seconds
if (counter++ % 200 == 0)
{
const auto scan_and_add = [&](u16 vid, std::initializer_list<std::pair<u16, u16>> ranges)
{
struct info_t
{
std::string path;
u16 product_id;
std::wstring serial;
};
std::vector<info_t> candidates;
hid_instance::enumerate_devices(vid, 0, [&](hid_device_info* devs)
{
for (hid_device_info* cur = devs; cur; cur = cur->next)
{
for (const auto& range : ranges)
{
if (cur->product_id >= range.first && cur->product_id <= range.second)
{
candidates.push_back({ cur->path, cur->product_id, cur->serial_number ? cur->serial_number : L"" });
break;
}
}
}
});
for (const auto& candidate : candidates)
{
// 1. Check if this physical device is already connected to any slot
{
std::shared_lock lock(m_mutex);
bool already_connected = false;
for (const auto& d : m_devices)
{
if (d->get_state().connected && d->get_path() == candidate.path)
{
already_connected = true;
break;
}
}
if (already_connected) continue;
}
// 2. Determine target slot
int slot_idx = -1;
if (vid == VID_MAYFLASH)
{
// DolphinBar Mode 4: PIDs 0x1800-0x1803 correspond to Players 1-4
slot_idx = candidate.product_id - PID_DOLPHINBAR_START;
}
else
{
// Generic Wiimote: Find first available slot
std::shared_lock lock(m_mutex);
for (usz i = 0; i < m_devices.size(); i++)
{
if (!m_devices[i]->get_state().connected)
{
slot_idx = static_cast<int>(i);
break;
}
}
}
// 3. Connect to slot
if (slot_idx >= 0 && slot_idx < static_cast<int>(m_devices.size()))
{
bool already_connected = false;
{
std::shared_lock lock(m_mutex);
already_connected = m_devices[slot_idx]->get_state().connected;
}
if (!already_connected)
{
// Re-create a temporary info struct for open()
hid_device_info temp_info = {};
temp_info.path = const_cast<char*>(candidate.path.c_str());
temp_info.serial_number = const_cast<wchar_t*>(candidate.serial.c_str());
temp_info.product_id = candidate.product_id;
// We call open() without holding m_mutex to avoid deadlock with main thread
// wiimote_device::open() internally handles m_handle and connectivity state
m_devices[slot_idx]->open(&temp_info);
}
}
}
};
// Generic Wiimote / Wiimote Plus / DolphinBar Mode 4 (Normal)
scan_and_add(VID_NINTENDO, {{PID_WIIMOTE, PID_WIIMOTE}, {PID_WIIMOTE_PLUS, PID_WIIMOTE_PLUS}});
// Mayflash DolphinBar Mode 4 (Custom VID/PIDs)
scan_and_add(VID_MAYFLASH, {{PID_DOLPHINBAR_START, PID_DOLPHINBAR_END}});
}
// Update all devices at 100Hz
{
std::unique_lock lock(m_mutex);
for (auto& d : m_devices)
{
if (d->get_state().connected)
{
d->update();
}
}
}
thread_ctrl::wait_for(10'000);
}
}

View file

@ -0,0 +1,75 @@
#pragma once
#include "util/types.hpp"
#include "Utilities/Thread.h"
#include "wiimote_types.h"
#include "Utilities/mutex.h"
#include <hidapi.h>
#include <vector>
#include <string>
#include <memory>
#include <thread>
#include <shared_mutex>
#include <chrono>
#include <array>
struct wiimote_state
{
u16 buttons = 0;
s16 acc_x = 0;
s16 acc_y = 0;
s16 acc_z = 0;
std::array<wiimote_ir_point, MAX_WIIMOTE_IR_POINTS> ir {};
bool connected = false;
};
class wiimote_device
{
public:
wiimote_device();
~wiimote_device();
bool open(hid_device_info* info);
void close();
bool update();
const wiimote_state& get_state() const { return m_state; }
const std::string& get_path() const { return m_path; }
const std::wstring& get_serial() const { return m_serial; }
private:
hid_device* m_handle = nullptr;
std::string m_path;
std::wstring m_serial;
wiimote_state m_state {};
std::chrono::steady_clock::time_point m_last_ir_check;
bool initialize_ir();
bool verify_ir_health();
bool write_reg(u32 addr, const std::vector<u8>& data);
};
class wiimote_handler
{
public:
wiimote_handler();
~wiimote_handler();
static wiimote_handler* get_instance();
void start();
void stop();
bool is_running() const { return m_running; }
std::vector<wiimote_state> get_states();
usz get_device_count();
private:
std::unique_ptr<named_thread<std::function<void()>>> m_thread;
atomic_t<bool> m_running{false};
std::vector<std::unique_ptr<wiimote_device>> m_devices;
shared_mutex m_mutex;
void thread_proc();
};

View file

@ -0,0 +1,29 @@
#pragma once
#include "util/types.hpp"
static constexpr usz MAX_WIIMOTES = 4;
static constexpr usz MAX_WIIMOTE_IR_POINTS = 4;
struct wiimote_ir_point
{
u16 x = 1023;
u16 y = 1023;
u8 size = 0;
};
enum class wiimote_button : 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
};

View file

@ -40,7 +40,7 @@
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<AdditionalIncludeDirectories>..\3rdparty\miniupnp\miniupnp\miniupnpc\include;..\3rdparty\wolfssl\wolfssl;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\protobuf_build\include;..\3rdparty\libusb\libusb\libusb;..\3rdparty\yaml-cpp\yaml-cpp\include;..\3rdparty\SoundTouch\soundtouch\include;..\3rdparty\rtmidi\rtmidi;..\3rdparty\zlib\zlib;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\llvm_build\include;$(SolutionDir)build\lib_ext\$(Configuration)-$(Platform)\llvm_build\include;$(SolutionDir)build\lib_ext\$(Configuration)-$(Platform)\llvm\include;$(SolutionDir)build\lib_ext\$(Configuration)-$(Platform)\llvm_build\include;$(VULKAN_SDK)\Include;..\3rdparty\zstd\zstd\lib;$(SolutionDir)3rdparty\fusion\fusion\Fusion;$(SolutionDir)3rdparty\wolfssl\extra\win32;$(SolutionDir)3rdparty\libsdl-org\SDL\include;$(SolutionDir)3rdparty\glslang\glslang;$(SolutionDir)3rdparty\curl\curl\include</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>..\3rdparty\hidapi\hidapi\hidapi;..\3rdparty\miniupnp\miniupnp\miniupnpc\include;..\3rdparty\wolfssl\wolfssl;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\protobuf_build\include;..\3rdparty\libusb\libusb\libusb;..\3rdparty\yaml-cpp\yaml-cpp\include;..\3rdparty\SoundTouch\soundtouch\include;..\3rdparty\rtmidi\rtmidi;..\3rdparty\zlib\zlib;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\llvm_build\include;$(SolutionDir)build\lib_ext\$(Configuration)-$(Platform)\llvm_build\include;$(SolutionDir)build\lib_ext\$(Configuration)-$(Platform)\llvm\include;$(SolutionDir)build\lib_ext\$(Configuration)-$(Platform)\llvm_build\include;$(VULKAN_SDK)\Include;..\3rdparty\zstd\zstd\lib;$(SolutionDir)3rdparty\fusion\fusion\Fusion;$(SolutionDir)3rdparty\wolfssl\extra\win32;$(SolutionDir)3rdparty\libsdl-org\SDL\include;$(SolutionDir)3rdparty\glslang\glslang;$(SolutionDir)3rdparty\curl\curl\include</AdditionalIncludeDirectories>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|x64'">MaxSpeed</Optimization>
<PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">AL_LIBTYPE_STATIC;MINIUPNP_STATICLIB;HAVE_VULKAN;HAVE_SDL3;ZLIB_CONST;WOLFSSL_USER_SETTINGS;CURL_STATICLIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|x64'">AL_LIBTYPE_STATIC;MINIUPNP_STATICLIB;HAVE_VULKAN;HAVE_SDL3;ZLIB_CONST;WOLFSSL_USER_SETTINGS;CURL_STATICLIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
@ -88,6 +88,7 @@
<ClCompile Include="Emu\Io\GameTablet.cpp" />
<ClCompile Include="Emu\Io\GHLtar.cpp" />
<ClCompile Include="Emu\Io\GunCon3.cpp" />
<ClCompile Include="Emu\Io\wiimote_config.cpp" />
<ClCompile Include="Emu\Io\midi_config_types.cpp" />
<ClCompile Include="Emu\Io\MouseHandler.cpp" />
<ClCompile Include="Emu\Io\pad_types.cpp" />
@ -602,6 +603,7 @@
<ClInclude Include="Emu\Io\GHLtar.h" />
<ClInclude Include="Emu\Io\ghltar_config.h" />
<ClInclude Include="Emu\Io\guncon3_config.h" />
<ClInclude Include="Emu\Io\wiimote_config.h" />
<ClInclude Include="Emu\Io\GunCon3.h" />
<ClInclude Include="Emu\Io\interception.h" />
<ClInclude Include="Emu\Io\Keyboard.h" />

View file

@ -957,6 +957,9 @@
<ClCompile Include="Emu\Io\GunCon3.cpp">
<Filter>Emu\Io</Filter>
</ClCompile>
<ClCompile Include="Emu\Io\wiimote_config.cpp">
<Filter>Emu\Io</Filter>
</ClCompile>
<ClCompile Include="Emu\Io\Buzz.cpp">
<Filter>Emu\Io</Filter>
</ClCompile>
@ -2590,6 +2593,9 @@
<ClInclude Include="Emu\Io\guncon3_config.h">
<Filter>Emu\Io</Filter>
</ClInclude>
<ClInclude Include="Emu\Io\wiimote_config.h">
<Filter>Emu\Io</Filter>
</ClInclude>
<ClInclude Include="Emu\Io\topshotelite_config.h">
<Filter>Emu\Io</Filter>
</ClInclude>

View file

@ -193,6 +193,7 @@
<ClCompile Include="Input\camera_video_sink.cpp" />
<ClCompile Include="Input\dualsense_pad_handler.cpp" />
<ClCompile Include="Input\gui_pad_thread.cpp" />
<ClCompile Include="Input\hid_instance.cpp" />
<ClCompile Include="Input\hid_pad_handler.cpp" />
<ClCompile Include="Input\mouse_gyro_handler.cpp" />
<ClCompile Include="Input\ps_move_calibration.cpp" />
@ -236,6 +237,9 @@
<ClCompile Include="QTGeneratedFiles\Debug\moc_camera_settings_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Debug\moc_wiimote_settings_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Debug\moc_ps_move_tracker_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
@ -539,6 +543,9 @@
<ClCompile Include="QTGeneratedFiles\Release\moc_camera_settings_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Release\moc_wiimote_settings_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Release\moc_ps_move_tracker_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</ClCompile>
@ -825,6 +832,7 @@
<ClCompile Include="rpcs3qt\breakpoint_list.cpp" />
<ClCompile Include="rpcs3qt\call_stack_list.cpp" />
<ClCompile Include="rpcs3qt\camera_settings_dialog.cpp" />
<ClCompile Include="rpcs3qt\wiimote_settings_dialog.cpp" />
<ClCompile Include="rpcs3qt\gui_game_info.cpp" />
<ClCompile Include="rpcs3qt\log_level_dialog.cpp" />
<ClCompile Include="rpcs3qt\permissions.cpp" />
@ -944,6 +952,7 @@
<ClCompile Include="rpcs3qt\save_data_dialog.cpp" />
<ClCompile Include="rpcs3qt\save_data_list_dialog.cpp" />
<ClCompile Include="rpcs3qt\settings_dialog.cpp" />
<ClCompile Include="Input\wiimote_handler.cpp" />
<ClCompile Include="Input\xinput_pad_handler.cpp" />
<ClCompile Include="util\console.cpp" />
<ClCompile Include="\rpcs3qt\*.cpp" />
@ -969,6 +978,7 @@
<ClInclude Include="Input\dualsense_pad_handler.h" />
<ClInclude Include="Input\evdev_joystick_handler.h" />
<ClInclude Include="Input\gui_pad_thread.h" />
<ClInclude Include="Input\hid_instance.h" />
<ClInclude Include="Input\hid_pad_handler.h" />
<ClInclude Include="Input\keyboard_pad_handler.h" />
<CustomBuild Include="rpcs3qt\gs_frame.h">
@ -1150,6 +1160,7 @@
<ClInclude Include="module_verifier.hpp" />
<ClInclude Include="QTGeneratedFiles\ui_about_dialog.h" />
<ClInclude Include="QTGeneratedFiles\ui_camera_settings_dialog.h" />
<ClInclude Include="QTGeneratedFiles\ui_wiimote_settings_dialog.h" />
<ClInclude Include="QTGeneratedFiles\ui_ps_move_tracker_dialog.h" />
<ClInclude Include="QTGeneratedFiles\ui_main_window.h" />
<ClInclude Include="QTGeneratedFiles\ui_music_player_dialog.h" />
@ -1224,6 +1235,16 @@
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\protobuf\protobuf\src" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia"</Command>
</CustomBuild>
<CustomBuild Include="rpcs3qt\wiimote_settings_dialog.h">
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Moc%27ing %(Identity)...</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">"$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\protobuf\protobuf\src" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia"</Command>
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Moc%27ing %(Identity)...</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\protobuf\protobuf\src" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia"</Command>
</CustomBuild>
<CustomBuild Include="rpcs3qt\ps_move_tracker_dialog.h">
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Moc%27ing %(Identity)...</Message>
@ -2023,6 +2044,8 @@
</CustomBuild>
<ClInclude Include="rpcs3qt\trophy_notification_helper.h" />
<ClInclude Include="rpcs3qt\user_account.h" />
<ClInclude Include="Input\wiimote_handler.h" />
<ClInclude Include="Input\wiimote_types.h" />
<ClInclude Include="Input\xinput_pad_handler.h" />
<ClInclude Include="util\console.h" />
<ClInclude Include="\rpcs3qt\*.h" />
@ -2174,6 +2197,16 @@
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\ui_%(Filename).h;%(Outputs)</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)"</Command>
</CustomBuild>
<CustomBuild Include="rpcs3qt\wiimote_settings_dialog.ui">
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\uic.exe;%(AdditionalInputs)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Uic%27ing %(Identity)...</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">.\QTGeneratedFiles\ui_%(Filename).h;%(Outputs)</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">"$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)"</Command>
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(QTDIR)\bin\uic.exe;%(AdditionalInputs)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Uic%27ing %(Identity)...</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\ui_%(Filename).h;%(Outputs)</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)"</Command>
</CustomBuild>
<CustomBuild Include="rpcs3qt\ps_move_tracker_dialog.ui">
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\uic.exe;%(AdditionalInputs)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Uic%27ing %(Identity)...</Message>

View file

@ -315,6 +315,9 @@
<ClCompile Include="Input\mm_joystick_handler.cpp">
<Filter>Io\MMJoystick</Filter>
</ClCompile>
<ClCompile Include="Input\wiimote_handler.cpp">
<Filter>Io</Filter>
</ClCompile>
<ClCompile Include="Input\xinput_pad_handler.cpp">
<Filter>Io\XInput</Filter>
</ClCompile>
@ -897,6 +900,9 @@
<ClCompile Include="QTGeneratedFiles\Release\moc_log_viewer.cpp">
<Filter>Generated Files\Release</Filter>
</ClCompile>
<ClCompile Include="Input\hid_instance.cpp">
<Filter>Io</Filter>
</ClCompile>
<ClCompile Include="Input\hid_pad_handler.cpp">
<Filter>Io</Filter>
</ClCompile>
@ -954,6 +960,15 @@
<ClCompile Include="QTGeneratedFiles\Release\moc_camera_settings_dialog.cpp">
<Filter>Generated Files\Release</Filter>
</ClCompile>
<ClCompile Include="rpcs3qt\wiimote_settings_dialog.cpp">
<Filter>Gui\settings</Filter>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Debug\moc_wiimote_settings_dialog.cpp">
<Filter>Generated Files\Debug</Filter>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Release\moc_wiimote_settings_dialog.cpp">
<Filter>Generated Files\Release</Filter>
</ClCompile>
<ClCompile Include="rpcs3qt\ps_move_tracker_dialog.cpp">
<Filter>Gui\ps_move_tracker_dialog</Filter>
</ClCompile>
@ -1283,6 +1298,12 @@
<ClInclude Include="Input\ds4_pad_handler.h">
<Filter>Io\DS4</Filter>
</ClInclude>
<ClInclude Include="Input\wiimote_handler.h">
<Filter>Io</Filter>
</ClInclude>
<ClInclude Include="Input\wiimote_types.h">
<Filter>Io</Filter>
</ClInclude>
<ClInclude Include="Input\xinput_pad_handler.h">
<Filter>Io\XInput</Filter>
</ClInclude>
@ -1397,6 +1418,9 @@
<ClInclude Include="rpcs3qt\progress_dialog.h">
<Filter>Gui\misc dialogs</Filter>
</ClInclude>
<ClInclude Include="Input\hid_instance.h">
<Filter>Io</Filter>
</ClInclude>
<ClInclude Include="Input\hid_pad_handler.h">
<Filter>Io</Filter>
</ClInclude>
@ -1412,6 +1436,9 @@
<ClInclude Include="QTGeneratedFiles\ui_camera_settings_dialog.h">
<Filter>Generated Files</Filter>
</ClInclude>
<ClInclude Include="QTGeneratedFiles\ui_wiimote_settings_dialog.h">
<Filter>Generated Files</Filter>
</ClInclude>
<ClInclude Include="QTGeneratedFiles\ui_ps_move_tracker_dialog.h">
<Filter>Generated Files</Filter>
</ClInclude>
@ -1771,9 +1798,15 @@
<CustomBuild Include="rpcs3qt\camera_settings_dialog.ui">
<Filter>Form Files</Filter>
</CustomBuild>
<CustomBuild Include="rpcs3qt\wiimote_settings_dialog.ui">
<Filter>Form Files</Filter>
</CustomBuild>
<CustomBuild Include="rpcs3qt\camera_settings_dialog.h">
<Filter>Gui\settings</Filter>
</CustomBuild>
<CustomBuild Include="rpcs3qt\wiimote_settings_dialog.h">
<Filter>Gui\settings</Filter>
</CustomBuild>
<CustomBuild Include="rpcs3qt\ps_move_tracker_dialog.ui">
<Filter>Form Files</Filter>
</CustomBuild>

View file

@ -6,6 +6,7 @@ add_library(rpcs3_ui STATIC
breakpoint_list.cpp
call_stack_list.cpp
camera_settings_dialog.cpp
wiimote_settings_dialog.cpp
cg_disasm_window.cpp
cheat_manager.cpp
clans_settings_dialog.cpp
@ -126,6 +127,7 @@ add_library(rpcs3_ui STATIC
about_dialog.ui
camera_settings_dialog.ui
wiimote_settings_dialog.ui
main_window.ui
music_player_dialog.ui
pad_led_settings_dialog.ui
@ -155,6 +157,7 @@ add_library(rpcs3_ui STATIC
../Input/dualsense_pad_handler.cpp
../Input/evdev_joystick_handler.cpp
../Input/gui_pad_thread.cpp
../Input/hid_instance.cpp
../Input/hid_pad_handler.cpp
../Input/keyboard_pad_handler.cpp
../Input/mm_joystick_handler.cpp
@ -172,6 +175,7 @@ add_library(rpcs3_ui STATIC
../Input/sdl_instance.cpp
../Input/sdl_pad_handler.cpp
../Input/skateboard_pad_handler.cpp
../Input/wiimote_handler.cpp
../Input/xinput_pad_handler.cpp
"../resources.qrc"

View file

@ -33,6 +33,7 @@
#include "gui_settings.h"
#include "input_dialog.h"
#include "camera_settings_dialog.h"
#include "wiimote_settings_dialog.h"
#include "ps_move_tracker_dialog.h"
#include "ipc_settings_dialog.h"
#include "shortcut_utils.h"
@ -3004,6 +3005,12 @@ void main_window::CreateConnects()
dlg->open();
});
connect(ui->confWiimoteAct, &QAction::triggered, this, [this]()
{
wiimote_settings_dialog* dlg = new wiimote_settings_dialog(this);
dlg->show();
});
const auto open_rpcn_settings = [this]
{
rpcn_settings_dialog dlg(this);

View file

@ -256,6 +256,7 @@
</widget>
<addaction name="confPadsAct"/>
<addaction name="menuMice"/>
<addaction name="confWiimoteAct"/>
<addaction name="confCamerasAct"/>
<addaction name="actionPS_Move_Tracker"/>
</widget>
@ -1425,6 +1426,11 @@
<string>GunCon 3</string>
</property>
</action>
<action name="confWiimoteAct">
<property name="text">
<string>Wiimotes</string>
</property>
</action>
<action name="confTopShotEliteAct">
<property name="text">
<string>Top Shot Elite</string>

View file

@ -0,0 +1,288 @@
#include "stdafx.h"
#include "wiimote_settings_dialog.h"
#include "Input/wiimote_handler.h"
#include "Emu/Io/wiimote_config.h"
#include <QTimer>
#include <QPainter>
#include <QPixmap>
#include <QCheckBox>
#include <QPushButton>
wiimote_settings_dialog::wiimote_settings_dialog(QWidget* parent)
: QDialog(parent)
, ui(new Ui::wiimote_settings_dialog)
{
ui->setupUi(this);
m_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
};
ui->useForGunCon->setChecked(get_wiimote_config().use_for_guncon.get());
connect(ui->useForGunCon, &QCheckBox::toggled, this, [](bool checked)
{
get_wiimote_config().use_for_guncon.set(checked);
});
update_list();
connect(ui->buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, &wiimote_settings_dialog::restore_defaults);
connect(this, &QDialog::finished, this, []()
{
get_wiimote_config().save();
});
// Timer updates both state AND device list (auto-refresh)
QTimer* timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &wiimote_settings_dialog::update_state);
connect(timer, &QTimer::timeout, this, &wiimote_settings_dialog::update_list);
timer->start(50);
populate_mappings();
}
void wiimote_settings_dialog::populate_mappings()
{
const auto& cfg = get_wiimote_config().guncon_mapping;
const std::array<std::pair<QString, wiimote_button>, 12> buttons = {
{ { tr("None"), wiimote_button::None },
{ tr("A"), wiimote_button::A },
{ tr("B"), wiimote_button::B },
{ tr("Plus (+)"), wiimote_button::Plus },
{ tr("Minus (-)"), wiimote_button::Minus },
{ tr("Home"), wiimote_button::Home },
{ tr("1"), wiimote_button::One },
{ tr("2"), wiimote_button::Two },
{ tr("D-Pad Up"), wiimote_button::Up },
{ tr("D-Pad Down"), wiimote_button::Down },
{ tr("D-Pad Left"), wiimote_button::Left },
{ tr("D-Pad Right"), wiimote_button::Right } }
};
const std::array<wiimote_button, 9> targets = {
cfg.trigger.get(), cfg.a1.get(), cfg.a2.get(), cfg.c1.get(),
cfg.b1.get(), cfg.b2.get(), cfg.b3.get(), cfg.a3.get(), cfg.c2.get()
};
ensure(m_boxes.size() == targets.size());
for (usz i = 0; i < m_boxes.size(); ++i)
{
m_boxes[i]->setMinimumWidth(150); // Make combo boxes wider for better readability
for (const auto& [name, btn] : buttons)
{
m_boxes[i]->addItem(name, QVariant::fromValue(static_cast<u16>(btn)));
}
// Set current selection
const int index = m_boxes[i]->findData(QVariant::fromValue(static_cast<u16>(targets[i])));
if (index >= 0) m_boxes[i]->setCurrentIndex(index);
// Connect change signal
connect(m_boxes[i], &QComboBox::currentIndexChanged, this, [this](int)
{
apply_mappings();
});
}
}
void wiimote_settings_dialog::restore_defaults()
{
// Reset to default mapping
get_wiimote_config().from_default();
ui->useForGunCon->setChecked(get_wiimote_config().use_for_guncon.get());
// Update UI
for (auto* box : m_boxes) box->blockSignals(true);
const auto& cfg = get_wiimote_config().guncon_mapping;
const std::array<wiimote_button, 9> targets = {
cfg.trigger.get(), cfg.a1.get(), cfg.a2.get(), cfg.c1.get(),
cfg.b1.get(), cfg.b2.get(), cfg.b3.get(), cfg.a3.get(), cfg.c2.get()
};
ensure(m_boxes.size() == targets.size());
for (usz i = 0; i < m_boxes.size(); ++i)
{
const int index = m_boxes[i]->findData(QVariant::fromValue(static_cast<u16>(targets[i])));
if (index >= 0) m_boxes[i]->setCurrentIndex(index);
}
for (auto* box : m_boxes) box->blockSignals(false);
}
void wiimote_settings_dialog::apply_mappings()
{
auto& cfg = get_wiimote_config().guncon_mapping;
const std::array<cfg::_enum<wiimote_button>*, 9> targets = {
&cfg.trigger, &cfg.a1, &cfg.a2, &cfg.c1,
&cfg.b1, &cfg.b2, &cfg.b3, &cfg.a3, &cfg.c2
};
ensure(m_boxes.size() == targets.size());
for (usz i = 0; i < m_boxes.size(); ++i)
{
targets[i]->set(static_cast<wiimote_button>(m_boxes[i]->currentData().toUInt()));
}
}
void wiimote_settings_dialog::update_state()
{
const int index = ui->wiimoteList->currentRow();
auto* wm = wiimote_handler::get_instance();
const auto running = wm && wm->is_running();
const auto states = running ? wm->get_states() : std::vector<wiimote_state>{};
if (!running || index < 0 || static_cast<usz>(index) >= states.size())
{
ui->connectionStatus->setText(tr("N/A"));
ui->buttonState->setText(tr("N/A"));
ui->irData->setText(tr("N/A"));
return;
}
const auto& state = states[index];
ui->connectionStatus->setText(state.connected ? tr("Connected") : tr("Disconnected"));
QStringList pressed_buttons;
const auto is_pressed = [&](wiimote_button btn) { return (state.buttons & static_cast<u16>(btn)) != 0; };
if (is_pressed(wiimote_button::Left)) pressed_buttons << tr("Left");
if (is_pressed(wiimote_button::Right)) pressed_buttons << tr("Right");
if (is_pressed(wiimote_button::Down)) pressed_buttons << tr("Down");
if (is_pressed(wiimote_button::Up)) pressed_buttons << tr("Up");
if (is_pressed(wiimote_button::Plus)) pressed_buttons << tr("Plus");
if (is_pressed(wiimote_button::Two)) pressed_buttons << tr("2");
if (is_pressed(wiimote_button::One)) pressed_buttons << tr("1");
if (is_pressed(wiimote_button::B)) pressed_buttons << tr("B");
if (is_pressed(wiimote_button::A)) pressed_buttons << tr("A");
if (is_pressed(wiimote_button::Minus)) pressed_buttons << tr("Minus");
if (is_pressed(wiimote_button::Home)) pressed_buttons << tr("Home");
QString button_text = QString("0x%1").arg(state.buttons, 4, 16, QChar('0')).toUpper();
if (!pressed_buttons.isEmpty())
{
button_text += " (" + pressed_buttons.join(", ") + ")";
}
ui->buttonState->setText(button_text);
QString ir_text;
const int w = ui->irVisual->width();
const int h = ui->irVisual->height();
QPixmap pixmap(w, h);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing);
// Calculate 4:3 drawing area (Wiimote IR space is 1024x768)
int draw_w, draw_h;
if (w * 3 > h * 4) // wider than 4:3
{
draw_h = h;
draw_w = h * 4 / 3;
}
else
{
draw_w = w;
draw_h = w * 3 / 4;
}
const int offset_x = (w - draw_w) / 2;
const int offset_y = (h - draw_h) / 2;
// Draw center crosshair in the 4:3 area
painter.setPen(QPen(Qt::darkGray, 1, Qt::DashLine));
painter.drawLine(offset_x + draw_w / 2, offset_y, offset_x + draw_w / 2, offset_y + draw_h);
painter.drawLine(offset_x, offset_y + draw_h / 2, offset_x + draw_w, offset_y + draw_h / 2);
static const std::array<QColor, MAX_WIIMOTE_IR_POINTS> colors = { Qt::red, Qt::green, Qt::blue, Qt::yellow };
for (usz i = 0; i < state.ir.size(); ++i)
{
if (state.ir[i].size > 0 && state.ir[i].x < 1023 && state.ir[i].y < 1023)
{
ir_text += QString("[%1: %2, %3] ").arg(i).arg(state.ir[i].x).arg(state.ir[i].y);
// Map 0..1023 X and 0..767 Y to 4:3 drawing area
const float x = offset_x + ((1023.0f - state.ir[i].x) / 1023.0f) * draw_w;
const float y = offset_y + (state.ir[i].y / 767.0f) * draw_h;
painter.setPen(colors[i]);
painter.setBrush(colors[i]);
painter.drawEllipse(QPointF(x, y), 4, 4);
painter.drawText(QPointF(x + 6, y + 6), QString::number(i));
}
}
ui->irVisual->setPixmap(pixmap);
ui->irData->setText(ir_text.isEmpty() ? tr("No IR data") : ir_text);
}
void wiimote_settings_dialog::update_list()
{
auto* wm = wiimote_handler::get_instance();
if (!wm || !wm->is_running())
{
const QString text = tr("Wiimote Manager not initialized.");
if (ui->wiimoteList->count() != 1 || ui->wiimoteList->item(0)->text() != text)
{
ui->wiimoteList->clear();
ui->wiimoteList->addItem(text);
}
return;
}
const auto states = wm->get_states();
if (states.empty())
{
const QString text = tr("No Wiimotes found.");
if (ui->wiimoteList->count() != 1 || ui->wiimoteList->item(0)->text() != text)
{
ui->wiimoteList->clear();
ui->wiimoteList->addItem(text);
}
return;
}
// Only update if the list count changed (avoid flicker)
if (static_cast<usz>(ui->wiimoteList->count()) != states.size())
{
const int current_row = ui->wiimoteList->currentRow();
ui->wiimoteList->clear();
for (usz i = 0; i < states.size(); i++)
{
QString label = tr("Wiimote #%1").arg(i + 1);
if (!states[i].connected) label += " (" + tr("Disconnected") + ")";
ui->wiimoteList->addItem(label);
}
if (current_row >= 0 && current_row < ui->wiimoteList->count())
{
ui->wiimoteList->setCurrentRow(current_row);
}
else if (ui->wiimoteList->count() > 0)
{
ui->wiimoteList->setCurrentRow(0);
}
}
else
{
for (usz i = 0; i < std::min(static_cast<usz>(ui->wiimoteList->count()), states.size()); i++)
{
QListWidgetItem* item = ui->wiimoteList->item(static_cast<int>(i));
if (!item) continue;
QString label = tr("Wiimote #%1").arg(i + 1);
if (!states[i].connected) label += " (" + tr("Disconnected") + ")";
if (item->text() != label)
{
item->setText(label);
}
}
}
}

View file

@ -0,0 +1,22 @@
#pragma once
#include <QDialog>
#include "ui_wiimote_settings_dialog.h"
class wiimote_settings_dialog : public QDialog
{
Q_OBJECT
public:
explicit wiimote_settings_dialog(QWidget* parent = nullptr);
~wiimote_settings_dialog() = default;
private:
std::unique_ptr<Ui::wiimote_settings_dialog> ui;
std::vector<QComboBox*> m_boxes;
void update_list();
void update_state();
void populate_mappings();
void apply_mappings();
void restore_defaults();
};

View file

@ -0,0 +1,270 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>wiimote_settings_dialog</class>
<widget class="QDialog" name="wiimote_settings_dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Wiimote Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="generalGroupBox">
<property name="title">
<string>General Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QCheckBox" name="useForGunCon">
<property name="text">
<string>Enable Wiimote for GunCon Emulation</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Connected Wiimotes</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QListWidget" name="wiimoteList"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="detailsGroupBox">
<property name="title">
<string>Wiimote Details</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="labelStatus">
<property name="text">
<string>Status:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="connectionStatus">
<property name="text">
<string>Disconnected</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelButtons">
<property name="text">
<string>Buttons:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="buttonState">
<property name="text">
<string>0x0000</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelIR">
<property name="text">
<string>IR Data:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="irData">
<property name="text">
<string>None</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="mappingGroupBox">
<property name="title">
<string>Button Mapping (GunCon3 &lt;- Wiimote)</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="lb_trigger">
<property name="text">
<string>Trigger:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="cb_trigger"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lb_a1">
<property name="text">
<string>A1 (Left Side):</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="cb_a1"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="lb_a2">
<property name="text">
<string>A2 (Left Side):</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="cb_a2"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="lb_c1">
<property name="text">
<string>C1 (Start - Left Side):</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="cb_c1"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="lb_b1">
<property name="text">
<string>B1 (Right Side):</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="cb_b1"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="lb_b2">
<property name="text">
<string>B2 (Right Side):</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="cb_b2"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="lb_b3">
<property name="text">
<string>B3 (Right Side):</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QComboBox" name="cb_b3"/>
</item>
<item row="7" column="0">
<widget class="QLabel" name="lb_a3">
<property name="text">
<string>A3 (Analog Button):</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QComboBox" name="cb_a3"/>
</item>
<item row="8" column="0">
<widget class="QLabel" name="lb_c2">
<property name="text">
<string>C2 (Select - Analog Button):</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QComboBox" name="cb_c2"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="irGroupBox">
<property name="title">
<string>IR Sensor View (All Pointers)</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="irVisual">
<property name="minimumSize">
<size>
<width>256</width>
<height>192</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">background-color: black; border: 1px solid gray;</string>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close|QDialogButtonBox::RestoreDefaults</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>wiimote_settings_dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>wiimote_settings_dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>