diff --git a/rpcs3/Emu/CMakeLists.txt b/rpcs3/Emu/CMakeLists.txt index edb98a6fa8..e42fe70cf6 100644 --- a/rpcs3/Emu/CMakeLists.txt +++ b/rpcs3/Emu/CMakeLists.txt @@ -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 diff --git a/rpcs3/Emu/Io/GunCon3.cpp b/rpcs3/Emu/Io/GunCon3.cpp index 522369d8a4..b661dcede9 100644 --- a/rpcs3/Emu/Io/GunCon3.cpp +++ b/rpcs3/Emu/Io/GunCon3.cpp @@ -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 +#include +#include LOG_CHANNEL(guncon3_log); @@ -127,6 +133,18 @@ usb_device_guncon3::usb_device_guncon3(u32 controller_index, const std::arraym_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::arrayget_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(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(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(std::clamp(x_res, SHRT_MIN, SHRT_MAX)); + gc.gun_y = static_cast(std::clamp(y_res, SHRT_MIN, SHRT_MAX)); + + if (g_cfg.io.show_move_cursor) + { + const s16 ax = static_cast((gc.gun_x + SHRT_MAX + 1) * rsx::overlays::overlay::virtual_width / USHRT_MAX); + const s16 ay = static_cast((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(ws.ir[0].x) - ws.ir[1].x; + const s32 dy = static_cast(ws.ir[0].y) - ws.ir[1].y; + gc.gun_z = static_cast(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"); diff --git a/rpcs3/Emu/Io/GunCon3.h b/rpcs3/Emu/Io/GunCon3.h index df013e4240..d69183057a 100644 --- a/rpcs3/Emu/Io/GunCon3.h +++ b/rpcs3/Emu/Io/GunCon3.h @@ -1,6 +1,8 @@ #pragma once #include "Emu/Io/usb_device.h" +#include +#include class usb_device_guncon3 : public usb_device_emulated { @@ -14,4 +16,9 @@ public: private: u32 m_controller_index; std::array m_key{}; + + bool handle_wiimote(struct GunCon3_data& gc); + + static inline std::vector s_instances; + static inline std::mutex s_instances_mutex; }; diff --git a/rpcs3/Emu/Io/wiimote_config.cpp b/rpcs3/Emu/Io/wiimote_config.cpp new file mode 100644 index 0000000000..1a37c7e8ce --- /dev/null +++ b/rpcs3/Emu/Io/wiimote_config.cpp @@ -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::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); + } +} diff --git a/rpcs3/Emu/Io/wiimote_config.h b/rpcs3/Emu/Io/wiimote_config.h new file mode 100644 index 0000000000..6d18f078e7 --- /dev/null +++ b/rpcs3/Emu/Io/wiimote_config.h @@ -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 trigger{ this, "Trigger", wiimote_button::B }; + cfg::_enum a1{ this, "A1", wiimote_button::A }; + cfg::_enum a2{ this, "A2", wiimote_button::Minus }; + cfg::_enum a3{ this, "A3", wiimote_button::Left }; + cfg::_enum b1{ this, "B1", wiimote_button::One }; + cfg::_enum b2{ this, "B2", wiimote_button::Two }; + cfg::_enum b3{ this, "B3", wiimote_button::Home }; + cfg::_enum c1{ this, "C1", wiimote_button::Plus }; + cfg::_enum c2{ this, "C2", wiimote_button::Right }; + + cfg::_enum b1_alt{ this, "B1_Alt", wiimote_button::Up }; + cfg::_enum b2_alt{ this, "B2_Alt", wiimote_button::Down }; + } guncon_mapping{ this }; + + const std::string path; +}; + +cfg_wiimote& get_wiimote_config(); diff --git a/rpcs3/Emu/RSX/Overlays/overlay_cursor.h b/rpcs3/Emu/RSX/Overlays/overlay_cursor.h index d464c509fe..8b619f4af7 100644 --- a/rpcs3/Emu/RSX/Overlays/overlay_cursor.h +++ b/rpcs3/Emu/RSX/Overlays/overlay_cursor.h @@ -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 diff --git a/rpcs3/Input/hid_instance.cpp b/rpcs3/Input/hid_instance.cpp new file mode 100644 index 0000000000..18bf6cdce7 --- /dev/null +++ b/rpcs3/Input/hid_instance.cpp @@ -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 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 +} diff --git a/rpcs3/Input/hid_instance.h b/rpcs3/Input/hid_instance.h new file mode 100644 index 0000000000..b16978bfa8 --- /dev/null +++ b/rpcs3/Input/hid_instance.h @@ -0,0 +1,32 @@ +#pragma once + +#include "util/types.hpp" +#include +#include +#include + +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 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; diff --git a/rpcs3/Input/hid_pad_handler.cpp b/rpcs3/Input/hid_pad_handler.cpp index 9d040a0a73..b080c725cb 100644 --- a/rpcs3/Input/hid_pad_handler.cpp +++ b/rpcs3/Input/hid_pad_handler.cpp @@ -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 #include @@ -23,74 +19,6 @@ std::vector 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()" diff --git a/rpcs3/Input/wiimote_handler.cpp b/rpcs3/Input/wiimote_handler.cpp new file mode 100644 index 0000000000..063bee487a --- /dev/null +++ b/rpcs3/Input/wiimote_handler.cpp @@ -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 +#include +#include + +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 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& 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(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 ir_on1 = { 0x13, 0x06 }; + hid_write(m_handle, ir_on1.data(), ir_on1.size()); + thread_ctrl::wait_for(50'000); + constexpr std::array 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 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 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 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()); + } + + 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(); + 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>>("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_handler::get_states() +{ + std::shared_lock lock(m_mutex); + std::vector 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> ranges) + { + struct info_t + { + std::string path; + u16 product_id; + std::wstring serial; + }; + std::vector 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(i); + break; + } + } + } + + // 3. Connect to slot + if (slot_idx >= 0 && slot_idx < static_cast(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(candidate.path.c_str()); + temp_info.serial_number = const_cast(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); + } +} diff --git a/rpcs3/Input/wiimote_handler.h b/rpcs3/Input/wiimote_handler.h new file mode 100644 index 0000000000..a173159a47 --- /dev/null +++ b/rpcs3/Input/wiimote_handler.h @@ -0,0 +1,75 @@ +#pragma once + +#include "util/types.hpp" +#include "Utilities/Thread.h" +#include "wiimote_types.h" +#include "Utilities/mutex.h" +#include +#include +#include +#include +#include +#include +#include +#include + +struct wiimote_state +{ + u16 buttons = 0; + s16 acc_x = 0; + s16 acc_y = 0; + s16 acc_z = 0; + std::array 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& 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 get_states(); + usz get_device_count(); + +private: + std::unique_ptr>> m_thread; + atomic_t m_running{false}; + std::vector> m_devices; + shared_mutex m_mutex; + + void thread_proc(); +}; diff --git a/rpcs3/Input/wiimote_types.h b/rpcs3/Input/wiimote_types.h new file mode 100644 index 0000000000..a7e3e2434f --- /dev/null +++ b/rpcs3/Input/wiimote_types.h @@ -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 +}; diff --git a/rpcs3/emucore.vcxproj b/rpcs3/emucore.vcxproj index 8df2ef3092..22f547cc11 100644 --- a/rpcs3/emucore.vcxproj +++ b/rpcs3/emucore.vcxproj @@ -40,7 +40,7 @@ Use - ..\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 + ..\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 MaxSpeed AL_LIBTYPE_STATIC;MINIUPNP_STATICLIB;HAVE_VULKAN;HAVE_SDL3;ZLIB_CONST;WOLFSSL_USER_SETTINGS;CURL_STATICLIB;%(PreprocessorDefinitions) AL_LIBTYPE_STATIC;MINIUPNP_STATICLIB;HAVE_VULKAN;HAVE_SDL3;ZLIB_CONST;WOLFSSL_USER_SETTINGS;CURL_STATICLIB;%(PreprocessorDefinitions) @@ -88,6 +88,7 @@ + @@ -602,6 +603,7 @@ + diff --git a/rpcs3/emucore.vcxproj.filters b/rpcs3/emucore.vcxproj.filters index 5ca602fbec..78fbbb5d9b 100644 --- a/rpcs3/emucore.vcxproj.filters +++ b/rpcs3/emucore.vcxproj.filters @@ -957,6 +957,9 @@ Emu\Io + + Emu\Io + Emu\Io @@ -2590,6 +2593,9 @@ Emu\Io + + Emu\Io + Emu\Io diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index cd5d7c8bc4..12ce1b0f1c 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -193,6 +193,7 @@ + @@ -236,6 +237,9 @@ true + + true + true @@ -539,6 +543,9 @@ true + + true + true @@ -825,6 +832,7 @@ + @@ -944,6 +952,7 @@ + @@ -969,6 +978,7 @@ + @@ -1150,6 +1160,7 @@ + @@ -1224,6 +1235,16 @@ .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp "$(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" + + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(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" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(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" + $(QTDIR)\bin\moc.exe;%(FullPath) Moc%27ing %(Identity)... @@ -2023,6 +2044,8 @@ + + @@ -2174,6 +2197,16 @@ .\QTGeneratedFiles\ui_%(Filename).h;%(Outputs) "$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)" + + $(QTDIR)\bin\uic.exe;%(AdditionalInputs) + Uic%27ing %(Identity)... + .\QTGeneratedFiles\ui_%(Filename).h;%(Outputs) + "$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)" + $(QTDIR)\bin\uic.exe;%(AdditionalInputs) + Uic%27ing %(Identity)... + .\QTGeneratedFiles\ui_%(Filename).h;%(Outputs) + "$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)" + $(QTDIR)\bin\uic.exe;%(AdditionalInputs) Uic%27ing %(Identity)... diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index 6c7841b4fc..05171ffff8 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -315,6 +315,9 @@ Io\MMJoystick + + Io + Io\XInput @@ -897,6 +900,9 @@ Generated Files\Release + + Io + Io @@ -954,6 +960,15 @@ Generated Files\Release + + Gui\settings + + + Generated Files\Debug + + + Generated Files\Release + Gui\ps_move_tracker_dialog @@ -1283,6 +1298,12 @@ Io\DS4 + + Io + + + Io + Io\XInput @@ -1397,6 +1418,9 @@ Gui\misc dialogs + + Io + Io @@ -1412,6 +1436,9 @@ Generated Files + + Generated Files + Generated Files @@ -1771,9 +1798,15 @@ Form Files + + Form Files + Gui\settings + + Gui\settings + Form Files diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index 6fe8d9db13..5647ac5aae 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -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" diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 687358d625..3978dac804 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -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); diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index 1f6a75c6f7..2209100e3d 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -256,6 +256,7 @@ + @@ -1425,6 +1426,11 @@ GunCon 3 + + + Wiimotes + + Top Shot Elite diff --git a/rpcs3/rpcs3qt/wiimote_settings_dialog.cpp b/rpcs3/rpcs3qt/wiimote_settings_dialog.cpp new file mode 100644 index 0000000000..7c79c1b394 --- /dev/null +++ b/rpcs3/rpcs3qt/wiimote_settings_dialog.cpp @@ -0,0 +1,288 @@ +#include "stdafx.h" +#include "wiimote_settings_dialog.h" +#include "Input/wiimote_handler.h" +#include "Emu/Io/wiimote_config.h" +#include +#include +#include +#include +#include + +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, 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 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(btn))); + } + + // Set current selection + const int index = m_boxes[i]->findData(QVariant::fromValue(static_cast(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 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(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*, 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(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{}; + + if (!running || index < 0 || static_cast(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(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 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(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(ui->wiimoteList->count()), states.size()); i++) + { + QListWidgetItem* item = ui->wiimoteList->item(static_cast(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); + } + } + } +} diff --git a/rpcs3/rpcs3qt/wiimote_settings_dialog.h b/rpcs3/rpcs3qt/wiimote_settings_dialog.h new file mode 100644 index 0000000000..1d8ee7c6a1 --- /dev/null +++ b/rpcs3/rpcs3qt/wiimote_settings_dialog.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#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; + std::vector m_boxes; + + 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 new file mode 100644 index 0000000000..33b2df2d04 --- /dev/null +++ b/rpcs3/rpcs3qt/wiimote_settings_dialog.ui @@ -0,0 +1,270 @@ + + + wiimote_settings_dialog + + + + 0 + 0 + 400 + 600 + + + + Wiimote Settings + + + + + + General Settings + + + + + + Enable Wiimote for GunCon Emulation + + + + + + + + + + Connected Wiimotes + + + + + + + + + + + + Wiimote Details + + + + + + Status: + + + + + + + Disconnected + + + + + + + Buttons: + + + + + + + 0x0000 + + + + + + + IR Data: + + + + + + + None + + + + + + + + + + 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): + + + + + + + + + + + + + IR Sensor View (All Pointers) + + + + + + + 256 + 192 + + + + background-color: black; border: 1px solid gray; + + + + + + Qt::AlignCenter + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close|QDialogButtonBox::RestoreDefaults + + + + + + + + + buttonBox + accepted() + wiimote_settings_dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + wiimote_settings_dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +