mirror of
https://github.com/PCSX2/pcsx2.git
synced 2026-01-31 01:15:24 +01:00
688 lines
24 KiB
C++
688 lines
24 KiB
C++
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
|
// SPDX-License-Identifier: GPL-3.0+
|
|
|
|
#include "GS/GS.h"
|
|
#include "Host.h"
|
|
#include "IconsPromptFont.h"
|
|
#include "ImGui/ImGuiManager.h"
|
|
#include "Input/InputManager.h"
|
|
#include "StateWrapper.h"
|
|
#include "USB/USB.h"
|
|
#include "USB/deviceproxy.h"
|
|
#include "USB/qemu-usb/USBinternal.h"
|
|
#include "USB/qemu-usb/desc.h"
|
|
#include "USB/usb-lightgun/guncon2.h"
|
|
#include "VMManager.h"
|
|
|
|
#include "common/Console.h"
|
|
#include "common/StringUtil.h"
|
|
|
|
#include <tuple>
|
|
|
|
namespace usb_lightgun
|
|
{
|
|
enum : u32
|
|
{
|
|
GUNCON2_FLAG_PROGRESSIVE = 0x0100,
|
|
|
|
GUNCON2_CALIBRATION_DELAY = 12,
|
|
GUNCON2_CALIBRATION_REPORT_DELAY = 5,
|
|
};
|
|
|
|
enum : u32
|
|
{
|
|
BID_C = 1,
|
|
BID_B = 2,
|
|
BID_A = 3,
|
|
BID_DPAD_UP = 4,
|
|
BID_DPAD_RIGHT = 5,
|
|
BID_DPAD_DOWN = 6,
|
|
BID_DPAD_LEFT = 7,
|
|
BID_TRIGGER = 13,
|
|
BID_SELECT = 14,
|
|
BID_START = 15,
|
|
BID_SHOOT_OFFSCREEN = 16,
|
|
BID_RECALIBRATE = 17,
|
|
BID_RELATIVE_LEFT = 18,
|
|
BID_RELATIVE_RIGHT = 19,
|
|
BID_RELATIVE_UP = 20,
|
|
BID_RELATIVE_DOWN = 21,
|
|
};
|
|
|
|
// Right pain in the arse. Different games seem to have different scales..
|
|
// Not worth putting these in the gamedb for such few games.
|
|
// Values are from the old nuvee plugin.
|
|
struct GameConfig
|
|
{
|
|
const char* serial;
|
|
float scale_x, scale_y;
|
|
u32 center_x, center_y;
|
|
u32 screen_width, screen_height;
|
|
};
|
|
|
|
static constexpr const GameConfig s_game_config[] = {
|
|
{"SLES-50930", 90.25f, 94.5f, 390, 132, 640, 256}, // Dino Stalker (E, English)
|
|
{"SLES-51095", 90.25f, 94.5f, 390, 132, 640, 256}, // Dino Stalker (E, French)
|
|
{"SLES-51096", 90.25f, 94.5f, 390, 132, 640, 256}, // Dino Stalker (E, German)
|
|
{"SLUS-20485", 90.25f, 92.5f, 390, 132, 640, 240}, // Dino Stalker (U)
|
|
{"SLUS-20389", 89.25f, 93.5f, 422, 141, 640, 240}, // Endgame (U)
|
|
{"SLES-50936", 112.0f, 100.0f, 320, 120, 512, 256}, // Endgame (E) (Guncon2 needs to be connected to USB port 2)
|
|
{"SLPM-65139", 90.0f, 91.5f, 320, 120, 640, 240}, // Gun Survivor 3: Dino Crisis (J)
|
|
{"SLES-52620", 89.5f, 112.3f, 390, 147, 640, 256}, // Guncom 2 (E)
|
|
{"SLES-51289", 84.5f, 89.0f, 456, 164, 640, 256}, // Gunfighter 2 - Jesse James (E)
|
|
{"SLPS-25165", 90.25f, 98.0f, 390, 138, 640, 240}, // Gunvari Collection (J) (480i)
|
|
// {"SLPS-25165", 86.75f, 96.0f, 454, 164, 640, 256}, // Gunvari Collection (J) (480p)
|
|
{"SCES-50889", 90.25f, 94.5f, 390, 169, 640, 256}, // Ninja Assault (E)
|
|
{"SLPS-20218", 90.0f, 92.0f, 320, 134, 640, 240}, // Ninja Assault (J)
|
|
{"SLUS-20492", 90.25f, 92.5f, 390, 132, 640, 240}, // Ninja Assault (U)
|
|
{"SLES-50650", 84.75f, 96.0f, 454, 164, 640, 240}, // Resident Evil Survivor 2 (E)
|
|
{"SLES-51448", 90.25f, 95.0f, 420, 132, 640, 240}, // Resident Evil - Dead Aim (E)
|
|
{"SLUS-20669", 90.25f, 93.5f, 420, 132, 640, 240}, // Resident Evil - Dead Aim (U)
|
|
{"SLUS-20619", 90.25f, 91.75f, 453, 154, 640, 256}, // Starsky & Hutch (U)
|
|
{"SCES-50300", 90.25f, 102.75f, 390, 138, 640, 256}, // Time Crisis II (E)
|
|
{"SLUS-20219", 90.25f, 97.5f, 390, 154, 640, 240}, // Time Crisis 2 (U)
|
|
{"SCES-51844", 90.25f, 102.75f, 390, 138, 640, 256}, // Time Crisis 3 (E)
|
|
{"SLUS-20645", 90.25f, 97.5f, 390, 154, 640, 240}, // Time Crisis 3 (U)
|
|
{"SCES-52530", 90.25f, 99.0f, 390, 153, 640, 256}, // Crisis Zone (E)
|
|
{"SLUS-20927", 90.25f, 99.0f, 390, 153, 640, 240}, // Time Crisis - Crisis Zone (U) (480i)
|
|
// {"SLUS-20927", 94.5f, 104.75f, 423, 407, 768, 768}, // Time Crisis - Crisis Zone (U) (480p)
|
|
{"SCES-50411", 89.8f, 99.9f, 421, 138, 640, 256}, // Vampire Night (E)
|
|
{"SLPS-25077", 90.0f, 97.5f, 422, 118, 640, 240}, // Vampire Night (J)
|
|
{"SLUS-20221", 89.8f, 102.5f, 422, 124, 640, 228}, // Vampire Night (U)
|
|
{"SLES-51229", 110.15f, 100.0f, 433, 159, 512, 256}, // Virtua Cop - Elite Edition (E,J) (480i)
|
|
// {"SLES-51229", 85.75f, 92.0f, 456, 164, 640, 256}, // Virtua Cop - Elite Edition (E,J) (480p)
|
|
};
|
|
|
|
static constexpr s32 DEFAULT_SCREEN_WIDTH = 640;
|
|
static constexpr s32 DEFAULT_SCREEN_HEIGHT = 240;
|
|
static constexpr float DEFAULT_CENTER_X = 320.0f;
|
|
static constexpr float DEFAULT_CENTER_Y = 120.0f;
|
|
static constexpr float DEFAULT_SCALE_X = 100.0f;
|
|
static constexpr float DEFAULT_SCALE_Y = 100.0f;
|
|
|
|
#pragma pack(push, 1)
|
|
union GunCon2Out
|
|
{
|
|
u8 bits[6];
|
|
|
|
struct
|
|
{
|
|
u16 buttons;
|
|
s16 pos_x;
|
|
s16 pos_y;
|
|
};
|
|
};
|
|
static_assert(sizeof(GunCon2Out) == 6);
|
|
#pragma pack(pop)
|
|
|
|
struct GunCon2State
|
|
{
|
|
explicit GunCon2State(u32 port_);
|
|
|
|
USBDevice dev{};
|
|
USBDesc desc{};
|
|
USBDescDevice desc_dev{};
|
|
|
|
u32 port = 0;
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// Configuration
|
|
//////////////////////////////////////////////////////////////////////////
|
|
bool has_relative_binds = false;
|
|
bool custom_config = false;
|
|
u32 screen_width = 640;
|
|
u32 screen_height = 240;
|
|
float center_x = 320;
|
|
float center_y = 120;
|
|
float scale_x = 1.0f;
|
|
float scale_y = 1.0f;
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// Host State (Not Saved)
|
|
//////////////////////////////////////////////////////////////////////////
|
|
u32 button_state = 0;
|
|
std::string cursor_path;
|
|
float cursor_scale = 1.0f;
|
|
u32 cursor_color = 0xFFFFFFFF;
|
|
float relative_pos[4] = {};
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// Device State (Saved)
|
|
//////////////////////////////////////////////////////////////////////////
|
|
s16 param_x = 0;
|
|
s16 param_y = 0;
|
|
u16 param_mode = 0;
|
|
|
|
u16 calibration_timer = 0;
|
|
s16 calibration_pos_x = 0;
|
|
s16 calibration_pos_y = 0;
|
|
|
|
bool auto_config_done = false;
|
|
|
|
void AutoConfigure();
|
|
|
|
std::tuple<s16, s16> CalculatePosition();
|
|
|
|
// 0..1, not -1..1.
|
|
std::pair<float, float> GetAbsolutePositionFromRelativeAxes() const;
|
|
u32 GetSoftwarePointerIndex() const;
|
|
void UpdateSoftwarePointerPosition();
|
|
};
|
|
|
|
static const USBDescStrings desc_strings = {
|
|
"Namco GunCon2",
|
|
};
|
|
|
|
/* mostly the same values as the Bochs USB Keyboard device */
|
|
static const uint8_t guncon2_dev_desc[] = {
|
|
/* bLength */ 0x12,
|
|
/* bDescriptorType */ 0x01,
|
|
/* bcdUSB */ WBVAL(0x0100),
|
|
/* bDeviceClass */ 0x00,
|
|
/* bDeviceSubClass */ 0x00,
|
|
/* bDeviceProtocol */ 0x00,
|
|
/* bMaxPacketSize0 */ 0x08,
|
|
/* idVendor */ WBVAL(0x0b9a),
|
|
/* idProduct */ WBVAL(0x016a),
|
|
/* bcdDevice */ WBVAL(0x0100),
|
|
/* iManufacturer */ 0x00,
|
|
/* iProduct */ 0x00,
|
|
/* iSerialNumber */ 0x00,
|
|
/* bNumConfigurations */ 0x01,
|
|
};
|
|
|
|
static const uint8_t guncon2_config_desc[] = {
|
|
0x09, // Length
|
|
0x02, // Type (Config)
|
|
0x19, 0x00, // Total size
|
|
|
|
0x01, // # interfaces
|
|
0x01, // Configuration #
|
|
0x00, // index of string descriptor
|
|
0x80, // Attributes (bus powered)
|
|
0x19, // Max power in mA
|
|
|
|
|
|
// Interface
|
|
0x09, // Length
|
|
0x04, // Type (Interface)
|
|
|
|
0x00, // Interface #
|
|
0x00, // Alternative #
|
|
0x01, // # endpoints
|
|
|
|
0xff, // Class
|
|
0x6a, // Subclass
|
|
0x00, // Protocol
|
|
0x00, // index of string descriptor
|
|
|
|
|
|
// Endpoint
|
|
0x07, // Length
|
|
0x05, // Type (Endpoint)
|
|
|
|
0x81, // Address
|
|
0x03, // Attributes (interrupt transfers)
|
|
0x08, 0x00, // Max packet size
|
|
|
|
0x08, // Polling interval (frame counts)
|
|
};
|
|
|
|
static void guncon2_handle_control(
|
|
USBDevice* dev, USBPacket* p, int request, int value, int index, int length, uint8_t* data)
|
|
{
|
|
GunCon2State* const us = USB_CONTAINER_OF(dev, GunCon2State, dev);
|
|
|
|
// Apply configuration on the first control packet.
|
|
// The ELF should be well and truely loaded by then.
|
|
if (!us->auto_config_done && !us->custom_config)
|
|
{
|
|
us->AutoConfigure();
|
|
us->auto_config_done = true;
|
|
}
|
|
|
|
DevCon.WriteLn("guncon2: req %04X val: %04X idx: %04X len: %d\n", request, value, index, length);
|
|
if (usb_desc_handle_control(dev, p, request, value, index, length, data) >= 0)
|
|
return;
|
|
|
|
if (request == (ClassInterfaceOutRequest | 0x09))
|
|
{
|
|
us->param_x = static_cast<u16>(data[0]) | (static_cast<u16>(data[1]) << 8);
|
|
us->param_y = static_cast<u16>(data[2]) | (static_cast<u16>(data[3]) << 8);
|
|
us->param_mode = static_cast<u16>(data[4]) | (static_cast<u16>(data[5]) << 8);
|
|
DevCon.WriteLn("GunCon2 Set Param %04X %d %d", us->param_mode, us->param_x, us->param_y);
|
|
return;
|
|
}
|
|
|
|
p->status = USB_RET_STALL;
|
|
}
|
|
|
|
static void guncon2_handle_data(USBDevice* dev, USBPacket* p)
|
|
{
|
|
GunCon2State* const us = USB_CONTAINER_OF(dev, GunCon2State, dev);
|
|
|
|
switch (p->pid)
|
|
{
|
|
case USB_TOKEN_IN:
|
|
{
|
|
if (p->ep->nr == 1)
|
|
{
|
|
const auto [pos_x, pos_y] = us->CalculatePosition();
|
|
|
|
// Time Crisis games do a "calibration" by displaying a black frame for a single frame,
|
|
// waiting for the gun to report (0, 0), and then computing an offset on the first non-zero
|
|
// value. So, after the trigger is pulled, we wait for a few frames, then send the (0, 0)
|
|
// report, then go back to normal values. To reduce error if the mouse is moving during
|
|
// these frames (unlikely), we store the fire position and keep returning that.
|
|
if (us->button_state & (1u << BID_RECALIBRATE) && us->calibration_timer == 0)
|
|
{
|
|
us->calibration_timer = GUNCON2_CALIBRATION_DELAY;
|
|
us->calibration_pos_x = pos_x;
|
|
us->calibration_pos_y = pos_y;
|
|
}
|
|
|
|
// Buttons are active low.
|
|
GunCon2Out out;
|
|
out.buttons = static_cast<u16>(~us->button_state) | (us->param_mode & GUNCON2_FLAG_PROGRESSIVE);
|
|
out.pos_x = pos_x;
|
|
out.pos_y = pos_y;
|
|
|
|
if (us->calibration_timer > 0)
|
|
{
|
|
// Force trigger down while calibrating.
|
|
out.buttons &= ~(1u << BID_TRIGGER);
|
|
out.pos_x = us->calibration_pos_x;
|
|
out.pos_y = us->calibration_pos_y;
|
|
us->calibration_timer--;
|
|
|
|
if (us->calibration_timer < GUNCON2_CALIBRATION_REPORT_DELAY)
|
|
{
|
|
out.pos_x = 0;
|
|
out.pos_y = 0;
|
|
}
|
|
}
|
|
else if (us->button_state & (1u << BID_SHOOT_OFFSCREEN))
|
|
{
|
|
// Offscreen shot - use 0,0.
|
|
out.buttons &= ~(1u << BID_TRIGGER);
|
|
out.pos_x = 0;
|
|
out.pos_y = 0;
|
|
}
|
|
|
|
usb_packet_copy(p, &out, sizeof(out));
|
|
break;
|
|
}
|
|
}
|
|
[[fallthrough]];
|
|
|
|
case USB_TOKEN_OUT:
|
|
default:
|
|
{
|
|
Console.Error("Unhandled GunCon2 request pid=%d ep=%u", p->pid, p->ep->nr);
|
|
p->status = USB_RET_STALL;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void usb_hid_unrealize(USBDevice* dev)
|
|
{
|
|
GunCon2State* us = USB_CONTAINER_OF(dev, GunCon2State, dev);
|
|
|
|
if (!us->cursor_path.empty())
|
|
ImGuiManager::ClearSoftwareCursor(us->GetSoftwarePointerIndex());
|
|
|
|
delete us;
|
|
}
|
|
|
|
GunCon2State::GunCon2State(u32 port_)
|
|
: port(port_)
|
|
{
|
|
}
|
|
|
|
void GunCon2State::AutoConfigure()
|
|
{
|
|
const std::string serial = VMManager::GetDiscSerial();
|
|
for (const GameConfig& gc : s_game_config)
|
|
{
|
|
if (serial != gc.serial)
|
|
continue;
|
|
|
|
Console.WriteLn(fmt::format("(GunCon2) Using automatic config for '{}'", serial));
|
|
Console.WriteLn(fmt::format(" Scale: {}x{}", gc.scale_x / 100.0f, gc.scale_y / 100.0f));
|
|
Console.WriteLn(fmt::format(" Center Position: {}x{}", gc.center_x, gc.center_y));
|
|
Console.WriteLn(fmt::format(" Screen Size: {}x{}", gc.screen_width, gc.screen_height));
|
|
|
|
scale_x = gc.scale_x / 100.0f;
|
|
scale_y = gc.scale_y / 100.0f;
|
|
center_x = static_cast<float>(gc.center_x);
|
|
center_y = static_cast<float>(gc.center_y);
|
|
screen_width = gc.screen_width;
|
|
screen_height = gc.screen_height;
|
|
return;
|
|
}
|
|
|
|
Console.Warning(fmt::format("(GunCon2) No automatic config found for '{}'.", serial));
|
|
}
|
|
|
|
std::tuple<s16, s16> GunCon2State::CalculatePosition()
|
|
{
|
|
float pointer_x, pointer_y;
|
|
const auto& [window_x, window_y] =
|
|
(has_relative_binds) ? GetAbsolutePositionFromRelativeAxes() : InputManager::GetPointerAbsolutePosition(0);
|
|
GSTranslateWindowToDisplayCoordinates(window_x, window_y, &pointer_x, &pointer_y);
|
|
|
|
s16 pos_x, pos_y;
|
|
if (pointer_x < 0.0f || pointer_y < 0.0f)
|
|
{
|
|
// off-screen
|
|
pos_x = 0;
|
|
pos_y = 0;
|
|
}
|
|
else
|
|
{
|
|
// scale to internal coordinate system and center
|
|
float fx = (pointer_x * static_cast<float>(screen_width)) - static_cast<float>(screen_width / 2u);
|
|
float fy = (pointer_y * static_cast<float>(screen_height)) - static_cast<float>(screen_height / 2u);
|
|
|
|
// apply curvature scale
|
|
fx *= scale_x;
|
|
fy *= scale_y;
|
|
|
|
// and re-center based on game center
|
|
s32 x = static_cast<s32>(std::round(fx + center_x));
|
|
s32 y = static_cast<s32>(std::round(fy + center_y));
|
|
|
|
// apply game-configured offset
|
|
if (param_mode & GUNCON2_FLAG_PROGRESSIVE)
|
|
{
|
|
x -= param_x / 2;
|
|
y -= param_y / 2;
|
|
}
|
|
else
|
|
{
|
|
x -= param_x;
|
|
y -= param_y;
|
|
}
|
|
|
|
// 0,0 is reserved for offscreen, so ensure we don't send that
|
|
pos_x = static_cast<s16>(std::max(x, 1));
|
|
pos_y = static_cast<s16>(std::max(y, 1));
|
|
}
|
|
|
|
return std::tie(pos_x, pos_y);
|
|
}
|
|
|
|
std::pair<float, float> GunCon2State::GetAbsolutePositionFromRelativeAxes() const
|
|
{
|
|
const float screen_rel_x = (((relative_pos[1] > 0.0f) ? relative_pos[1] : -relative_pos[0]) + 1.0f) * 0.5f;
|
|
const float screen_rel_y = (((relative_pos[3] > 0.0f) ? relative_pos[3] : -relative_pos[2]) + 1.0f) * 0.5f;
|
|
return std::make_pair(
|
|
screen_rel_x * ImGuiManager::GetWindowWidth(), screen_rel_y * ImGuiManager::GetWindowHeight());
|
|
}
|
|
|
|
u32 GunCon2State::GetSoftwarePointerIndex() const
|
|
{
|
|
return has_relative_binds ? (InputManager::MAX_POINTER_DEVICES + port) : 0;
|
|
}
|
|
|
|
void GunCon2State::UpdateSoftwarePointerPosition()
|
|
{
|
|
if (cursor_path.empty())
|
|
return;
|
|
|
|
const auto& [window_x, window_y] = GetAbsolutePositionFromRelativeAxes();
|
|
ImGuiManager::SetSoftwareCursorPosition(GetSoftwarePointerIndex(), window_x, window_y);
|
|
}
|
|
|
|
const char* GunCon2Device::Name() const
|
|
{
|
|
return TRANSLATE_NOOP("USB", "GunCon 2");
|
|
}
|
|
|
|
const char* GunCon2Device::TypeName() const
|
|
{
|
|
return "guncon2";
|
|
}
|
|
|
|
const char* GunCon2Device::IconName() const
|
|
{
|
|
return ICON_PF_GUNCON2;
|
|
}
|
|
|
|
USBDevice* GunCon2Device::CreateDevice(SettingsInterface& si, u32 port, u32 subtype) const
|
|
{
|
|
GunCon2State* s = new GunCon2State(port);
|
|
s->desc.full = &s->desc_dev;
|
|
s->desc.str = desc_strings;
|
|
|
|
if (usb_desc_parse_dev(guncon2_dev_desc, sizeof(guncon2_dev_desc), s->desc, s->desc_dev) < 0)
|
|
goto fail;
|
|
if (usb_desc_parse_config(guncon2_config_desc, sizeof(guncon2_config_desc), s->desc_dev) < 0)
|
|
goto fail;
|
|
|
|
s->dev.speed = USB_SPEED_FULL;
|
|
s->dev.klass.handle_attach = usb_desc_attach;
|
|
s->dev.klass.handle_control = guncon2_handle_control;
|
|
s->dev.klass.handle_data = guncon2_handle_data;
|
|
s->dev.klass.unrealize = usb_hid_unrealize;
|
|
s->dev.klass.usb_desc = &s->desc;
|
|
s->dev.klass.product_desc = s->desc.str[2];
|
|
|
|
usb_desc_init(&s->dev);
|
|
usb_ep_init(&s->dev);
|
|
|
|
UpdateSettings(&s->dev, si);
|
|
|
|
return &s->dev;
|
|
fail:
|
|
usb_hid_unrealize(&s->dev);
|
|
return nullptr;
|
|
}
|
|
|
|
void GunCon2Device::UpdateSettings(USBDevice* dev, SettingsInterface& si) const
|
|
{
|
|
GunCon2State* s = USB_CONTAINER_OF(dev, GunCon2State, dev);
|
|
|
|
s->custom_config = USB::GetConfigBool(si, s->port, TypeName(), "custom_config", false);
|
|
|
|
// Don't override auto config if we've set it.
|
|
if (!s->auto_config_done || s->custom_config)
|
|
{
|
|
s->screen_width = USB::GetConfigInt(si, s->port, TypeName(), "screen_width", DEFAULT_SCREEN_WIDTH);
|
|
s->screen_height = USB::GetConfigInt(si, s->port, TypeName(), "screen_height", DEFAULT_SCREEN_HEIGHT);
|
|
s->center_x = USB::GetConfigFloat(si, s->port, TypeName(), "center_x", DEFAULT_CENTER_X);
|
|
s->center_y = USB::GetConfigFloat(si, s->port, TypeName(), "center_y", DEFAULT_CENTER_Y);
|
|
s->scale_x = USB::GetConfigFloat(si, s->port, TypeName(), "scale_x", DEFAULT_SCALE_X) / 100.0f;
|
|
s->scale_y = USB::GetConfigFloat(si, s->port, TypeName(), "scale_y", DEFAULT_SCALE_Y) / 100.0f;
|
|
}
|
|
|
|
// Pointer settings.
|
|
const std::string pointer_binding = USB::GetConfigString(si, s->port, TypeName(), "Pointer", "");
|
|
std::string cursor_path(USB::GetConfigString(si, s->port, TypeName(), "cursor_path"));
|
|
const float cursor_scale = USB::GetConfigFloat(si, s->port, TypeName(), "cursor_scale", 1.0f);
|
|
u32 cursor_color = 0xFFFFFF;
|
|
if (std::string cursor_color_str(USB::GetConfigString(si, s->port, TypeName(), "cursor_color")); !cursor_color_str.empty())
|
|
{
|
|
// Strip the leading hash, if it's a CSS style colour.
|
|
const std::optional<u32> cursor_color_opt(
|
|
StringUtil::FromChars<u32>(cursor_color_str[0] == '#' ?
|
|
std::string_view(cursor_color_str).substr(1) : std::string_view(cursor_color_str), 16));
|
|
if (cursor_color_opt.has_value())
|
|
cursor_color = cursor_color_opt.value();
|
|
}
|
|
|
|
const s32 prev_pointer_index = s->GetSoftwarePointerIndex();
|
|
|
|
s->has_relative_binds = (USB::ConfigKeyExists(si, s->port, TypeName(), "RelativeLeft") ||
|
|
USB::ConfigKeyExists(si, s->port, TypeName(), "RelativeRight") ||
|
|
USB::ConfigKeyExists(si, s->port, TypeName(), "RelativeUp") ||
|
|
USB::ConfigKeyExists(si, s->port, TypeName(), "RelativeDown"));
|
|
|
|
const s32 new_pointer_index = s->GetSoftwarePointerIndex();
|
|
|
|
if (prev_pointer_index != new_pointer_index || s->cursor_path != cursor_path ||
|
|
s->cursor_scale != cursor_scale || s->cursor_color != cursor_color)
|
|
{
|
|
if (prev_pointer_index != new_pointer_index)
|
|
ImGuiManager::ClearSoftwareCursor(prev_pointer_index);
|
|
|
|
// Pointer changed, so need to update software cursor.
|
|
const bool had_software_cursor = !s->cursor_path.empty();
|
|
s->cursor_path = std::move(cursor_path);
|
|
s->cursor_scale = cursor_scale;
|
|
s->cursor_color = cursor_color;
|
|
if (!s->cursor_path.empty())
|
|
{
|
|
ImGuiManager::SetSoftwareCursor(new_pointer_index, s->cursor_path, s->cursor_scale, s->cursor_color);
|
|
s->UpdateSoftwarePointerPosition();
|
|
}
|
|
else if (had_software_cursor)
|
|
{
|
|
ImGuiManager::ClearSoftwareCursor(new_pointer_index);
|
|
}
|
|
}
|
|
}
|
|
|
|
float GunCon2Device::GetBindingValue(const USBDevice* dev, u32 bind_index) const
|
|
{
|
|
GunCon2State* s = USB_CONTAINER_OF(dev, GunCon2State, dev);
|
|
|
|
const u32 bit = 1u << bind_index;
|
|
return ((s->button_state & bit) != 0) ? 1.0f : 0.0f;
|
|
}
|
|
|
|
void GunCon2Device::SetBindingValue(USBDevice* dev, u32 bind_index, float value) const
|
|
{
|
|
GunCon2State* s = USB_CONTAINER_OF(dev, GunCon2State, dev);
|
|
|
|
if (bind_index < BID_RELATIVE_LEFT)
|
|
{
|
|
const u32 bit = 1u << bind_index;
|
|
if (value >= 0.5f)
|
|
s->button_state |= bit;
|
|
else
|
|
s->button_state &= ~bit;
|
|
}
|
|
else if (bind_index <= BID_RELATIVE_DOWN)
|
|
{
|
|
const u32 rel_index = bind_index - BID_RELATIVE_LEFT;
|
|
if (s->relative_pos[rel_index] != value)
|
|
{
|
|
s->relative_pos[rel_index] = value;
|
|
s->UpdateSoftwarePointerPosition();
|
|
}
|
|
}
|
|
}
|
|
|
|
std::span<const InputBindingInfo> GunCon2Device::Bindings(u32 subtype) const
|
|
{
|
|
static constexpr const InputBindingInfo bindings[] = {
|
|
//{"pointer", "Pointer/Aiming", InputBindingInfo::Type::Pointer, BID_POINTER_X, GenericInputBinding::Unknown},
|
|
{"Up", TRANSLATE_NOOP("USB", "D-Pad Up"), nullptr, InputBindingInfo::Type::Button, BID_DPAD_UP, GenericInputBinding::DPadUp},
|
|
{"Down", TRANSLATE_NOOP("USB", "D-Pad Down"), nullptr, InputBindingInfo::Type::Button, BID_DPAD_DOWN, GenericInputBinding::DPadDown},
|
|
{"Left", TRANSLATE_NOOP("USB", "D-Pad Left"), nullptr, InputBindingInfo::Type::Button, BID_DPAD_LEFT, GenericInputBinding::DPadLeft},
|
|
{"Right", TRANSLATE_NOOP("USB", "D-Pad Right"), nullptr, InputBindingInfo::Type::Button, BID_DPAD_RIGHT,
|
|
GenericInputBinding::DPadRight},
|
|
{"Trigger", TRANSLATE_NOOP("USB", "Trigger"), nullptr, InputBindingInfo::Type::Button, BID_TRIGGER, GenericInputBinding::R2},
|
|
{"ShootOffscreen", TRANSLATE_NOOP("USB", "Shoot Offscreen"), nullptr, InputBindingInfo::Type::Button, BID_SHOOT_OFFSCREEN,
|
|
GenericInputBinding::R1},
|
|
{"Recalibrate", TRANSLATE_NOOP("USB", "Calibration Shot"), nullptr, InputBindingInfo::Type::Button, BID_RECALIBRATE,
|
|
GenericInputBinding::Unknown},
|
|
{"A", TRANSLATE_NOOP("USB", "A"), nullptr, InputBindingInfo::Type::Button, BID_A, GenericInputBinding::Cross},
|
|
{"B", TRANSLATE_NOOP("USB", "B"), nullptr, InputBindingInfo::Type::Button, BID_B, GenericInputBinding::Circle},
|
|
{"C", TRANSLATE_NOOP("USB", "C"), nullptr, InputBindingInfo::Type::Button, BID_C, GenericInputBinding::Triangle},
|
|
{"Select", TRANSLATE_NOOP("USB", "Select"), nullptr, InputBindingInfo::Type::Button, BID_SELECT, GenericInputBinding::Select},
|
|
{"Start", TRANSLATE_NOOP("USB", "Start"), nullptr, InputBindingInfo::Type::Button, BID_START, GenericInputBinding::Start},
|
|
{"RelativeLeft", TRANSLATE_NOOP("USB", "Relative Left"), nullptr, InputBindingInfo::Type::HalfAxis, BID_RELATIVE_LEFT, GenericInputBinding::Unknown},
|
|
{"RelativeRight", TRANSLATE_NOOP("USB", "Relative Right"), nullptr, InputBindingInfo::Type::HalfAxis, BID_RELATIVE_RIGHT, GenericInputBinding::Unknown},
|
|
{"RelativeUp", TRANSLATE_NOOP("USB", "Relative Up"), nullptr, InputBindingInfo::Type::HalfAxis, BID_RELATIVE_UP, GenericInputBinding::Unknown},
|
|
{"RelativeDown", TRANSLATE_NOOP("USB", "Relative Down"), nullptr, InputBindingInfo::Type::HalfAxis, BID_RELATIVE_DOWN, GenericInputBinding::Unknown},
|
|
};
|
|
|
|
return bindings;
|
|
}
|
|
|
|
std::span<const SettingInfo> GunCon2Device::Settings(u32 subtype) const
|
|
{
|
|
static constexpr const SettingInfo info[] = {
|
|
{SettingInfo::Type::Path, "cursor_path", TRANSLATE_NOOP("USB", "Cursor Path"),
|
|
TRANSLATE_NOOP("USB", "Sets the crosshair image that this lightgun will use. Setting a crosshair image "
|
|
"will disable the system cursor."),
|
|
""},
|
|
{SettingInfo::Type::Float, "cursor_scale", TRANSLATE_NOOP("USB", "Cursor Scale"),
|
|
TRANSLATE_NOOP("USB", "Scales the crosshair image set above."), "1", "0.01", "10", "0.01", TRANSLATE_NOOP("USB", "%.0f%%"),
|
|
nullptr, nullptr, 100.0f},
|
|
{SettingInfo::Type::String, "cursor_color", TRANSLATE_NOOP("USB", "Cursor Color"),
|
|
TRANSLATE_NOOP("USB", "Applies a color to the chosen crosshair images, can be used for multiple "
|
|
"players. Specify in HTML/CSS format (e.g. #aabbcc)"),
|
|
"#ffffff"},
|
|
{SettingInfo::Type::Boolean, "custom_config", TRANSLATE_NOOP("USB", "Manual Screen Configuration"),
|
|
TRANSLATE_NOOP("USB",
|
|
"Forces the use of the screen parameters below, instead of automatic parameters if available."),
|
|
"false"},
|
|
{SettingInfo::Type::Float, "scale_x", TRANSLATE_NOOP("USB", "X Scale (Sensitivity)"),
|
|
TRANSLATE_NOOP("USB", "Scales the position to simulate CRT curvature."), "100", "0", "200", "0.1",
|
|
TRANSLATE_NOOP("USB", "%.2f%%"), nullptr, nullptr, 1.0f},
|
|
{SettingInfo::Type::Float, "scale_y", TRANSLATE_NOOP("USB", "Y Scale (Sensitivity)"),
|
|
TRANSLATE_NOOP("USB", "Scales the position to simulate CRT curvature."), "100", "0", "200", "0.1",
|
|
TRANSLATE_NOOP("USB", "%.2f%%"), nullptr, nullptr, 1.0f},
|
|
{SettingInfo::Type::Float, "center_x", TRANSLATE_NOOP("USB", "Center X"),
|
|
TRANSLATE_NOOP("USB", "Sets the horizontal center position of the simulated screen."), "320", "0",
|
|
"1024", "1", TRANSLATE_NOOP("USB", "%.0fpx"), nullptr, nullptr, 1.0f},
|
|
{SettingInfo::Type::Float, "center_y", TRANSLATE_NOOP("USB", "Center Y"),
|
|
TRANSLATE_NOOP("USB", "Sets the vertical center position of the simulated screen."), "120", "0", "1024",
|
|
"1", TRANSLATE_NOOP("USB", "%.0fpx"), nullptr, nullptr, 1.0f},
|
|
{SettingInfo::Type::Integer, "screen_width", TRANSLATE_NOOP("USB", "Screen Width"),
|
|
TRANSLATE_NOOP("USB", "Sets the width of the simulated screen."), "640", "1", "1024", "1", TRANSLATE_NOOP("USB", "%dpx"),
|
|
nullptr, nullptr, 1.0f},
|
|
{SettingInfo::Type::Integer, "screen_height", TRANSLATE_NOOP("USB", "Screen Height"),
|
|
TRANSLATE_NOOP("USB", "Sets the height of the simulated screen."), "240", "1", "1024", "1", TRANSLATE_NOOP("USB", "%dpx"),
|
|
nullptr, nullptr, 1.0f},
|
|
};
|
|
return info;
|
|
}
|
|
|
|
bool GunCon2Device::Freeze(USBDevice* dev, StateWrapper& sw) const
|
|
{
|
|
GunCon2State* s = USB_CONTAINER_OF(dev, GunCon2State, dev);
|
|
|
|
if (!sw.DoMarker("GunCon2Device"))
|
|
return false;
|
|
|
|
sw.Do(&s->param_x);
|
|
sw.Do(&s->param_y);
|
|
sw.Do(&s->param_mode);
|
|
sw.Do(&s->calibration_timer);
|
|
sw.Do(&s->calibration_pos_x);
|
|
sw.Do(&s->calibration_pos_y);
|
|
sw.Do(&s->auto_config_done);
|
|
|
|
float scale_x = s->scale_x;
|
|
float scale_y = s->scale_y;
|
|
float center_x = s->center_x;
|
|
float center_y = s->center_y;
|
|
u32 screen_width = s->screen_width;
|
|
u32 screen_height = s->screen_height;
|
|
sw.Do(&scale_x);
|
|
sw.Do(&scale_y);
|
|
sw.Do(¢er_x);
|
|
sw.Do(¢er_y);
|
|
sw.Do(&screen_width);
|
|
sw.Do(&screen_height);
|
|
|
|
// Only save automatic settings to state.
|
|
if (sw.IsReading() && !s->custom_config && s->auto_config_done)
|
|
{
|
|
s->scale_x = scale_x;
|
|
s->scale_y = scale_y;
|
|
s->center_x = center_x;
|
|
s->center_y = center_y;
|
|
s->screen_width = screen_width;
|
|
s->screen_height = screen_height;
|
|
}
|
|
|
|
return !sw.HasError();
|
|
}
|
|
} // namespace usb_lightgun
|