Wiimote to GunCon3

This commit is contained in:
Barış Hamil 2026-02-06 20:29:52 +03:00
parent d854ff03fe
commit cb01548ca2
20 changed files with 806 additions and 1 deletions

View file

@ -401,6 +401,7 @@ target_sources(rpcs3_emu PRIVATE
Io/GameTablet.cpp
Io/GHLtar.cpp
Io/GunCon3.cpp
Io/WiimoteManager.cpp
Io/Infinity.cpp
Io/interception.cpp
Io/KamenRider.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

@ -1,11 +1,15 @@
#include "stdafx.h"
#include "GunCon3.h"
#include "MouseHandler.h"
#include <cmath>
#include <climits>
#include "Emu/IdManager.h"
#include "Emu/Io/guncon3_config.h"
#include "Emu/Cell/lv2/sys_usbd.h"
#include "Emu/system_config.h"
#include "WiimoteManager.h"
#include "Input/pad_thread.h"
#include "Emu/RSX/Overlays/overlay_cursor.h"
LOG_CHANNEL(guncon3_log);
@ -127,6 +131,17 @@ 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 +189,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)
@ -207,6 +227,88 @@ void usb_device_guncon3::interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint,
GunCon3_data gc{};
gc.stick_ax = gc.stick_ay = gc.stick_bx = gc.stick_by = 0x7f;
if (auto* wm = WiimoteManager::get_instance())
{
auto states = wm->get_states();
// Determine which Wiimote to use based on our ordinal position among all GunCons
int my_wiimote_index = -1;
{
std::lock_guard lock(s_instances_mutex);
auto it = std::lower_bound(s_instances.begin(), s_instances.end(), this, [](auto* a, auto* b) {
return a->m_controller_index < b->m_controller_index;
});
// Since we sort by pointer adress/controller_index in add, and search by this ptr
// Actually lower_bound needs a value. std::find is safer for pointer identity.
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<size_t>(my_wiimote_index) < states.size())
{
const auto& ws = states[my_wiimote_index];
if (ws.buttons & 0x0400)
{
gc.btn_trigger = 1;
}
// Wiimote to GunCon3 Button Mapping
if (ws.buttons & 0x0800) gc.btn_a1 = 1; // Wiimote A -> A1
if (ws.buttons & 0x1000) gc.btn_a2 = 1; // Wiimote Minus -> A2
if (ws.buttons & 0x0010) gc.btn_c1 = 1; // Wiimote Plus -> C1
if (ws.buttons & 0x0200) gc.btn_b1 = 1; // Wiimote 1 -> B1
if (ws.buttons & 0x0100) gc.btn_b2 = 1; // Wiimote 2 -> B2
if (ws.buttons & 0x8000) gc.btn_b3 = 1; // Wiimote Home -> B3
if (ws.buttons & 0x0001) gc.btn_a3 = 1; // Wiimote Left (D-pad) -> A3
if (ws.buttons & 0x0002) gc.btn_c2 = 1; // Wiimote Right (D-pad)
if (ws.buttons & 0x0008) gc.btn_b1 = 1; // D-pad Up -> B1 (Alt)
if (ws.buttons & 0x0004) gc.btn_b2 = 1; // D-pad Down -> B2 (Alt)
if (ws.ir[0].x < 1023)
{
// Only use the primary pointer to avoid jumping between multiple IR points
s32 raw_x = ws.ir[0].x;
s32 raw_y = ws.ir[0].y;
// Map to GunCon3 range (-32768..32767)
// X calculation (Right = 32767, Left = -32768)
s32 x_res = 32767 - (raw_x * 65535 / 1023);
// Y calculation (Top = 32767, Bottom = -32768)
// Swapping to inverted mapping as per user feedback
s32 y_res = 32767 - (raw_y * 65535 / 767);
gc.gun_x = static_cast<int16_t>(std::clamp(x_res, -32768, 32767));
gc.gun_y = static_cast<int16_t>(std::clamp(y_res, -32768, 32767));
// Draw the actual GunCon3 output to the overlay
// Mapping GunCon3 range back to virtual_width/height
s16 ax = static_cast<s16>((gc.gun_x + 32768) * rsx::overlays::overlay::virtual_width / 65535);
s16 ay = static_cast<s16>((32767 - gc.gun_y) * rsx::overlays::overlay::virtual_height / 65535);
if (g_cfg.io.show_move_cursor)
{
// Use my_wiimote_index for color/cursor selection (0=Red, 1=Green...)
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)
{
// Calculate "Z" (distance) based on spread of first two points to emulate depth sensor
s32 dx = static_cast<s32>(ws.ir[0].x) - ws.ir[1].x;
s32 dy = static_cast<s32>(ws.ir[0].y) - ws.ir[1].y;
gc.gun_z = static_cast<int16_t>(std::sqrt(dx * dx + dy * dy));
}
}
guncon3_encode(&gc, buf, m_key.data());
return;
}
}
if (!is_input_allowed())
{
guncon3_encode(&gc, buf, m_key.data());

View file

@ -14,4 +14,7 @@ public:
private:
u32 m_controller_index;
std::array<u8, 8> m_key{};
static inline std::vector<usb_device_guncon3*> s_instances;
static inline std::mutex s_instances_mutex;
};

View file

@ -0,0 +1,259 @@
#include "stdafx.h"
#include "WiimoteManager.h"
#include "Emu/System.h"
#include "Emu/system_config.h"
#include <algorithm>
// 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;
WiimoteDevice::WiimoteDevice(hid_device_info* info)
: m_path(info->path)
, m_serial(info->serial_number ? info->serial_number : L"")
{
m_handle = hid_open_path(info->path);
if (m_handle)
{
// 1. Connectivity Test (Matching wiimote_test)
u8 status_req[] = { 0x15, 0x00 };
if (hid_write(m_handle, status_req, sizeof(status_req)) < 0)
{
hid_close(m_handle);
m_handle = nullptr;
return;
}
// 2. Full Initialization
if (initialize_ir())
{
m_state.connected = true;
}
else
{
hid_close(m_handle);
m_handle = nullptr;
}
}
}
WiimoteDevice::~WiimoteDevice()
{
if (m_handle) hid_close(m_handle);
}
bool WiimoteDevice::initialize_ir()
{
auto write_reg = [&](u32 addr, const std::vector<u8>& data) {
u8 buf[22] = {0};
buf[0] = 0x16; // Write register
buf[1] = 0x04;
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(20));
return true;
};
// 1. Enable IR logic / Pixel Clock
u8 ir_on1[] = { 0x13, 0x04 };
hid_write(m_handle, ir_on1, 2);
std::this_thread::sleep_for(std::chrono::milliseconds(20));
u8 ir_on2[] = { 0x1a, 0x04 };
hid_write(m_handle, ir_on2, 2);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// 2. Enable IR Camera (Matching wiimote_test order)
if (!write_reg(0xb00030, {0x08})) return false;
// 3. Sensitivity Level 3 (Exactly matching wiimote_test)
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. Reporting mode: Buttons + Accel + IR
u8 mode[] = { 0x12, 0x00, 0x33 };
if (hid_write(m_handle, mode, sizeof(mode)) < 0) return false;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return true;
}
bool WiimoteDevice::update()
{
if (!m_handle) 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 (int j = 0; j < 4; j++)
{
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) return false;
return true;
}
static WiimoteManager* s_instance = nullptr;
WiimoteManager::WiimoteManager()
{
if (!s_instance)
s_instance = this;
}
WiimoteManager::~WiimoteManager()
{
stop();
if (s_instance == this)
s_instance = nullptr;
}
WiimoteManager* WiimoteManager::get_instance()
{
return s_instance;
}
void WiimoteManager::start()
{
if (m_running) return;
// Note: hid_init() is not thread-safe. ideally should be called once at app startup.
if (hid_init() != 0) return;
m_running = true;
m_thread = std::thread(&WiimoteManager::thread_proc, this);
}
void WiimoteManager::stop()
{
m_running = false;
if (m_thread.joinable()) m_thread.join();
hid_exit();
}
size_t WiimoteManager::get_device_count()
{
std::shared_lock lock(m_mutex);
return m_devices.size();
}
std::vector<WiimoteState> WiimoteManager::get_states()
{
std::shared_lock lock(m_mutex);
std::vector<WiimoteState> states;
states.reserve(m_devices.size());
for (const auto& dev : m_devices)
{
states.push_back(dev->get_state());
}
return states;
}
void WiimoteManager::thread_proc()
{
u32 counter = 0;
while (m_running)
{
// Scan every 2 seconds
if (counter++ % 200 == 0)
{
auto scan_and_add = [&](u16 vid, u16 pid_start, u16 pid_end)
{
hid_device_info* devs = hid_enumerate(vid, 0);
hid_device_info* cur = devs;
while (cur)
{
if (cur->product_id >= pid_start && cur->product_id <= pid_end)
{
bool already_owned = false;
{
std::shared_lock lock(m_mutex);
for (const auto& d : m_devices)
{
if (d->get_path() == cur->path)
{
already_owned = true;
break;
}
}
}
if (!already_owned)
{
auto dev = std::make_unique<WiimoteDevice>(cur);
if (dev->get_state().connected)
{
std::unique_lock lock(m_mutex);
m_devices.push_back(std::move(dev));
}
}
}
cur = cur->next;
}
hid_free_enumeration(devs);
};
// Generic Wiimote / DolphinBar Mode 4 (Normal)
scan_and_add(VID_NINTENDO, PID_WIIMOTE, PID_WIIMOTE);
// Wiimote Plus
scan_and_add(VID_NINTENDO, PID_WIIMOTE_PLUS, PID_WIIMOTE_PLUS);
// Mayflash DolphinBar Mode 4 (Custom VID/PIDs)
// Supports up to 4 players (1800, 1801, 1802, 1803)
scan_and_add(VID_MAYFLASH, PID_DOLPHINBAR_START, PID_DOLPHINBAR_END);
}
// Update all devices at 100Hz
{
std::unique_lock lock(m_mutex);
m_devices.erase(std::remove_if(m_devices.begin(), m_devices.end(), [](const auto& d)
{
return !const_cast<WiimoteDevice&>(*d).update();
}), m_devices.end());
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}

View file

@ -0,0 +1,70 @@
#pragma once
#include "util/types.hpp"
#include "Utilities/Thread.h"
#include "Utilities/mutex.h"
#include <hidapi/hidapi.h>
#include <vector>
#include <string>
#include <memory>
#include <thread>
#include <shared_mutex>
#include <chrono>
struct WiimoteIRPoint
{
u16 x = 1023;
u16 y = 1023;
u8 size = 0;
};
struct WiimoteState
{
u16 buttons = 0;
s16 acc_x = 0, acc_y = 0, acc_z = 0;
WiimoteIRPoint ir[4];
bool connected = false;
};
class WiimoteDevice
{
public:
WiimoteDevice(hid_device_info* info);
~WiimoteDevice();
bool update();
const WiimoteState& get_state() const { return m_state; }
std::string get_path() const { return m_path; }
std::wstring get_serial() const { return m_serial; }
private:
hid_device* m_handle = nullptr;
std::string m_path;
std::wstring m_serial;
WiimoteState m_state;
bool initialize_ir();
};
class WiimoteManager
{
public:
WiimoteManager();
~WiimoteManager();
static WiimoteManager* get_instance();
void start();
void stop();
std::vector<WiimoteState> get_states();
size_t get_device_count();
private:
std::thread m_thread;
atomic_t<bool> m_running{false};
std::vector<std::unique_ptr<WiimoteDevice>> m_devices;
shared_mutex m_mutex;
void thread_proc();
};

View file

@ -458,6 +458,18 @@ struct AnalogSensor
{}
};
struct ir_point
{
u16 x = 1023;
u16 y = 1023;
u16 size = 0;
};
struct ir_data
{
std::array<ir_point, 4> points;
};
struct VibrateMotor
{
bool is_large_motor = false;
@ -519,6 +531,7 @@ struct Pad
std::vector<Button> m_buttons_external;
std::array<AnalogStick, 4> m_sticks_external{};
ir_data m_ir{};
std::vector<std::shared_ptr<Pad>> copilots;

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

@ -276,6 +276,7 @@ struct cfg_root : cfg::node
cfg::_bool keep_pads_connected{this, "Keep pads connected", false, true};
cfg::uint<0, 100'000> pad_sleep{this, "Pad handler sleep (microseconds)", 1'000, true};
cfg::_bool background_input_enabled{this, "Background input enabled", true, true};
cfg::_bool wiimote_continuous_scanning{this, "Wiimote continuous scanning", false, true};
cfg::_bool show_move_cursor{this, "Show move cursor", false, true};
cfg::_bool paint_move_spheres{this, "Paint move spheres", false, true};
cfg::_bool allow_move_hue_set_by_game{this, "Allow move hue set by game", false, true};

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
@ -125,6 +126,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

View file

@ -164,6 +164,7 @@ enum class emu_settings_type
CameraFlip,
CameraID,
Move,
WiimoteScan,
Buzz,
Turntable,
GHLtar,
@ -368,6 +369,7 @@ inline static const std::map<emu_settings_type, cfg_location> settings_location
{ emu_settings_type::LockOvlIptToP1, { "Input/Output", "Lock overlay input to player one"}},
{ emu_settings_type::PadHandlerMode, { "Input/Output", "Pad handler mode"}},
{ emu_settings_type::PadConnection, { "Input/Output", "Keep pads connected" }},
{ emu_settings_type::WiimoteScan, { "Input/Output", "Wiimote continuous scanning" }},
{ emu_settings_type::KeyboardHandler, { "Input/Output", "Keyboard"}},
{ emu_settings_type::MouseHandler, { "Input/Output", "Mouse"}},
{ emu_settings_type::Camera, { "Input/Output", "Camera"}},

View file

@ -127,6 +127,8 @@ bool gui_application::Init()
}
m_emu_settings = std::make_shared<emu_settings>();
m_wiimote_manager = std::make_unique<WiimoteManager>();
m_wiimote_manager->start();
m_gui_settings = std::make_shared<gui_settings>();
m_persistent_settings = std::make_shared<persistent_settings>();

View file

@ -34,6 +34,8 @@ extern std::unique_ptr<raw_mouse_handler> g_raw_mouse_handler; // Only used for
/** RPCS3 GUI Application Class
* The main point of this class is to do application initialization, to hold the main and game windows and to initialize callbacks.
*/
#include "Emu/Io/WiimoteManager.h"
class gui_application : public QApplication, public main_application
{
Q_OBJECT
@ -111,6 +113,7 @@ private:
std::deque<std::unique_ptr<QSoundEffect>> m_sound_effects{};
std::unique_ptr<WiimoteManager> m_wiimote_manager;
std::shared_ptr<emu_settings> m_emu_settings;
std::shared_ptr<gui_settings> m_gui_settings;
std::shared_ptr<persistent_settings> m_persistent_settings;

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"
@ -2992,6 +2993,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

@ -1220,6 +1220,9 @@ settings_dialog::settings_dialog(std::shared_ptr<gui_settings> gui_settings, std
m_emu_settings->EnhanceCheckBox(ui->padConnectionBox, emu_settings_type::PadConnection);
SubscribeTooltip(ui->padConnectionBox, tooltips.settings.pad_connection);
m_emu_settings->EnhanceCheckBox(ui->wiimoteScanBox, emu_settings_type::WiimoteScan);
SubscribeTooltip(ui->wiimoteScanBox, tooltips.settings.wiimote_scan);
m_emu_settings->EnhanceCheckBox(ui->showMoveCursorBox, emu_settings_type::ShowMoveCursor);
SubscribeTooltip(ui->showMoveCursorBox, tooltips.settings.show_move_cursor);

View file

@ -1785,6 +1785,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="wiimoteScanBox">
<property name="text">
<string>Wiimote Continuous Scanning</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showMoveCursorBox">
<property name="text">

View file

@ -233,6 +233,7 @@ public:
const QString pad_mode = tr("Single-threaded: All pad handlers run on the same thread sequentially.\nMulti-threaded: Each pad handler has its own thread.\nOnly use multi-threaded if you can spare the extra threads.");
const QString pad_connection = tr("Shows all configured pads as always connected ingame even if they are physically disconnected.");
const QString wiimote_scan = tr("Enables continuous scanning for Wiimotes. Required for some adapters like Mayflash DolphinBar to work correctly when hot-plugging.");
const QString keyboard_handler = tr("Some games support native keyboard input.\nBasic will work in these cases.");
const QString mouse_handler = tr("Some games support native mouse input.\nBasic or Raw will work in these cases.");
const QString music_handler = tr("Currently only used for cellMusic emulation.\nSelect Qt to use the default output device of your operating system.\nThis may not be able to play all audio formats.");

View file

@ -0,0 +1,125 @@
#include "stdafx.h"
#include "wiimote_settings_dialog.h"
#include "Emu/System.h"
#include "Emu/Io/WiimoteManager.h"
#include <QTimer>
#include <QPainter>
#include <QPixmap>
wiimote_settings_dialog::wiimote_settings_dialog(QWidget* parent)
: QDialog(parent)
, ui(new Ui::wiimote_settings_dialog)
{
ui->setupUi(this);
update_list();
connect(ui->scanButton, &QPushButton::clicked, this, [this] { update_list(); });
QTimer* timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &wiimote_settings_dialog::update_state);
timer->start(50);
}
wiimote_settings_dialog::~wiimote_settings_dialog() = default;
void wiimote_settings_dialog::update_state()
{
int index = ui->wiimoteList->currentRow();
auto* wm = WiimoteManager::get_instance();
if (!wm || index < 0)
{
ui->connectionStatus->setText(tr("N/A"));
ui->buttonState->setText(tr("N/A"));
ui->irData->setText(tr("N/A"));
return;
}
auto states = wm->get_states();
if (static_cast<size_t>(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 pressedButtons;
if (state.buttons & 0x0001) pressedButtons << tr("Left");
if (state.buttons & 0x0002) pressedButtons << tr("Right");
if (state.buttons & 0x0004) pressedButtons << tr("Down");
if (state.buttons & 0x0008) pressedButtons << tr("Up");
if (state.buttons & 0x0010) pressedButtons << tr("Plus");
if (state.buttons & 0x0100) pressedButtons << tr("2");
if (state.buttons & 0x0200) pressedButtons << tr("1");
if (state.buttons & 0x0400) pressedButtons << tr("B");
if (state.buttons & 0x0800) pressedButtons << tr("A");
if (state.buttons & 0x1000) pressedButtons << tr("Minus");
if (state.buttons & 0x8000) pressedButtons << tr("Home");
QString buttonText = QString("0x%1").arg(state.buttons, 4, 16, QChar('0')).toUpper();
if (!pressedButtons.isEmpty())
{
buttonText += " (" + pressedButtons.join(", ") + ")";
}
ui->buttonState->setText(buttonText);
QString irText;
QPixmap pixmap(ui->irVisual->size());
pixmap.fill(Qt::black);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing);
// Draw center crosshair
painter.setPen(QPen(Qt::darkGray, 1, Qt::DashLine));
painter.drawLine(pixmap.width() / 2, 0, pixmap.width() / 2, pixmap.height());
painter.drawLine(0, pixmap.height() / 2, pixmap.width(), pixmap.height() / 2);
static const QColor colors[] = { Qt::red, Qt::green, Qt::blue, Qt::yellow };
for (int i = 0; i < 4; ++i)
{
if (state.ir[i].size > 0 && state.ir[i].x < 1023 && state.ir[i].y < 1023)
{
irText += 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 pixmap coordinates
// Wiimote X/Y are inverted relative to pointing direction
float x = ((1023 - state.ir[i].x) / 1023.0f) * pixmap.width();
float y = (state.ir[i].y / 767.0f) * pixmap.height();
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(irText.isEmpty() ? tr("No IR data") : irText);
}
void wiimote_settings_dialog::update_list()
{
ui->wiimoteList->clear();
auto* wm = WiimoteManager::get_instance();
if (!wm)
{
ui->wiimoteList->addItem(tr("Wiimote Manager not initialized."));
return;
}
size_t count = wm->get_device_count();
if (count == 0)
{
ui->wiimoteList->addItem(tr("No Wiimotes found."));
}
else
{
for (size_t i = 0; i < count; i++)
{
ui->wiimoteList->addItem(tr("Wiimote #%1").arg(i + 1));
}
ui->wiimoteList->setCurrentRow(0);
}
}

View file

@ -0,0 +1,17 @@
#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();
private:
std::unique_ptr<Ui::wiimote_settings_dialog> ui;
void update_list();
void update_state();
};

View file

@ -0,0 +1,179 @@
<?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="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="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>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="scanButton">
<property name="text">
<string>Refresh Scan</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</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>