Files
archived-pcsx2/pcsx2/USB/usb-lightgun/guncon2.cpp

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(&center_x);
sw.Do(&center_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