early-access version 2177

This commit is contained in:
pineappleEA
2021-11-02 05:02:57 +01:00
parent 2b3decfff3
commit 0d005353e8
115 changed files with 13315 additions and 3835 deletions

View File

@@ -0,0 +1,539 @@
// Copyright 2014 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include <fmt/format.h>
#include <libusb.h>
#include "common/logging/log.h"
#include "common/param_package.h"
#include "common/settings_input.h"
#include "common/thread.h"
#include "input_common/drivers/gc_adapter.h"
namespace InputCommon {
class LibUSBContext {
public:
explicit LibUSBContext() {
init_result = libusb_init(&ctx);
}
~LibUSBContext() {
libusb_exit(ctx);
}
LibUSBContext& operator=(const LibUSBContext&) = delete;
LibUSBContext(const LibUSBContext&) = delete;
LibUSBContext& operator=(LibUSBContext&&) noexcept = delete;
LibUSBContext(LibUSBContext&&) noexcept = delete;
[[nodiscard]] int InitResult() const noexcept {
return init_result;
}
[[nodiscard]] libusb_context* get() noexcept {
return ctx;
}
private:
libusb_context* ctx;
int init_result{};
};
class LibUSBDeviceHandle {
public:
explicit LibUSBDeviceHandle(libusb_context* ctx, uint16_t vid, uint16_t pid) noexcept {
handle = libusb_open_device_with_vid_pid(ctx, vid, pid);
}
~LibUSBDeviceHandle() noexcept {
if (handle) {
libusb_release_interface(handle, 1);
libusb_close(handle);
}
}
LibUSBDeviceHandle& operator=(const LibUSBDeviceHandle&) = delete;
LibUSBDeviceHandle(const LibUSBDeviceHandle&) = delete;
LibUSBDeviceHandle& operator=(LibUSBDeviceHandle&&) noexcept = delete;
LibUSBDeviceHandle(LibUSBDeviceHandle&&) noexcept = delete;
[[nodiscard]] libusb_device_handle* get() noexcept {
return handle;
}
private:
libusb_device_handle* handle{};
};
GCAdapter::GCAdapter(const std::string input_engine_) : InputEngine(input_engine_) {
if (usb_adapter_handle) {
return;
}
LOG_DEBUG(Input, "Initialization started");
libusb_ctx = std::make_unique<LibUSBContext>();
const int init_res = libusb_ctx->InitResult();
if (init_res == LIBUSB_SUCCESS) {
adapter_scan_thread =
std::jthread([this](std::stop_token stop_token) { AdapterScanThread(stop_token); });
} else {
LOG_ERROR(Input, "libusb could not be initialized. failed with error = {}", init_res);
}
}
GCAdapter::~GCAdapter() {
Reset();
}
void GCAdapter::AdapterInputThread(std::stop_token stop_token) {
LOG_DEBUG(Input, "Input thread started");
Common::SetCurrentThreadName("yuzu:input:GCAdapter");
s32 payload_size{};
AdapterPayload adapter_payload{};
adapter_scan_thread = {};
while (!stop_token.stop_requested()) {
libusb_interrupt_transfer(usb_adapter_handle->get(), input_endpoint, adapter_payload.data(),
static_cast<s32>(adapter_payload.size()), &payload_size, 16);
if (IsPayloadCorrect(adapter_payload, payload_size)) {
UpdateControllers(adapter_payload);
UpdateVibrations();
}
std::this_thread::yield();
}
if (restart_scan_thread) {
adapter_scan_thread =
std::jthread([this](std::stop_token token) { AdapterScanThread(token); });
restart_scan_thread = false;
}
}
bool GCAdapter::IsPayloadCorrect(const AdapterPayload& adapter_payload, s32 payload_size) {
if (payload_size != static_cast<s32>(adapter_payload.size()) ||
adapter_payload[0] != LIBUSB_DT_HID) {
LOG_DEBUG(Input, "Error reading payload (size: {}, type: {:02x})", payload_size,
adapter_payload[0]);
if (input_error_counter++ > 20) {
LOG_ERROR(Input, "Timeout, Is the adapter connected?");
adapter_input_thread.request_stop();
restart_scan_thread = true;
}
return false;
}
input_error_counter = 0;
return true;
}
void GCAdapter::UpdateControllers(const AdapterPayload& adapter_payload) {
for (std::size_t port = 0; port < pads.size(); ++port) {
const std::size_t offset = 1 + (9 * port);
const auto type = static_cast<ControllerTypes>(adapter_payload[offset] >> 4);
UpdatePadType(port, type);
if (DeviceConnected(port)) {
const u8 b1 = adapter_payload[offset + 1];
const u8 b2 = adapter_payload[offset + 2];
UpdateStateButtons(port, b1, b2);
UpdateStateAxes(port, adapter_payload);
}
}
}
void GCAdapter::UpdatePadType(std::size_t port, ControllerTypes pad_type) {
if (pads[port].type == pad_type) {
return;
}
// Device changed reset device and set new type
pads[port].axis_origin = {};
pads[port].reset_origin_counter = {};
pads[port].enable_vibration = {};
pads[port].rumble_amplitude = {};
pads[port].type = pad_type;
}
void GCAdapter::UpdateStateButtons(std::size_t port, [[maybe_unused]] u8 b1,
[[maybe_unused]] u8 b2) {
if (port >= pads.size()) {
return;
}
static constexpr std::array<PadButton, 8> b1_buttons{
PadButton::ButtonA, PadButton::ButtonB, PadButton::ButtonX, PadButton::ButtonY,
PadButton::ButtonLeft, PadButton::ButtonRight, PadButton::ButtonDown, PadButton::ButtonUp,
};
static constexpr std::array<PadButton, 4> b2_buttons{
PadButton::ButtonStart,
PadButton::TriggerZ,
PadButton::TriggerR,
PadButton::TriggerL,
};
for (std::size_t i = 0; i < b1_buttons.size(); ++i) {
const bool button_status = (b1 & (1U << i)) != 0;
const int button = static_cast<int>(b1_buttons[i]);
SetButton(pads[port].identifier, button, button_status);
}
for (std::size_t j = 0; j < b2_buttons.size(); ++j) {
const bool button_status = (b2 & (1U << j)) != 0;
const int button = static_cast<int>(b2_buttons[j]);
SetButton(pads[port].identifier, button, button_status);
}
}
void GCAdapter::UpdateStateAxes(std::size_t port, const AdapterPayload& adapter_payload) {
if (port >= pads.size()) {
return;
}
const std::size_t offset = 1 + (9 * port);
static constexpr std::array<PadAxes, 6> axes{
PadAxes::StickX, PadAxes::StickY, PadAxes::SubstickX,
PadAxes::SubstickY, PadAxes::TriggerLeft, PadAxes::TriggerRight,
};
for (const PadAxes axis : axes) {
const auto index = static_cast<std::size_t>(axis);
const u8 axis_value = adapter_payload[offset + 3 + index];
if (pads[port].reset_origin_counter <= 18) {
if (pads[port].axis_origin[index] != axis_value) {
pads[port].reset_origin_counter = 0;
}
pads[port].axis_origin[index] = axis_value;
pads[port].reset_origin_counter++;
}
const f32 axis_status = (axis_value - pads[port].axis_origin[index]) / 100.0f;
SetAxis(pads[port].identifier, static_cast<int>(index), axis_status);
}
}
void GCAdapter::AdapterScanThread(std::stop_token stop_token) {
Common::SetCurrentThreadName("yuzu:input:ScanGCAdapter");
usb_adapter_handle = nullptr;
pads = {};
while (!stop_token.stop_requested() && !Setup()) {
std::this_thread::sleep_for(std::chrono::seconds(2));
}
}
bool GCAdapter::Setup() {
constexpr u16 nintendo_vid = 0x057e;
constexpr u16 gc_adapter_pid = 0x0337;
usb_adapter_handle =
std::make_unique<LibUSBDeviceHandle>(libusb_ctx->get(), nintendo_vid, gc_adapter_pid);
if (!usb_adapter_handle->get()) {
return false;
}
if (!CheckDeviceAccess()) {
usb_adapter_handle = nullptr;
return false;
}
libusb_device* const device = libusb_get_device(usb_adapter_handle->get());
LOG_INFO(Input, "GC adapter is now connected");
// GC Adapter found and accessible, registering it
if (GetGCEndpoint(device)) {
rumble_enabled = true;
input_error_counter = 0;
output_error_counter = 0;
std::size_t port = 0;
for (GCController& pad : pads) {
pad.identifier = {
.guid = Common::UUID{Common::INVALID_UUID},
.port = port++,
.pad = 0,
};
PreSetController(pad.identifier);
}
adapter_input_thread =
std::jthread([this](std::stop_token stop_token) { AdapterInputThread(stop_token); });
return true;
}
return false;
}
bool GCAdapter::CheckDeviceAccess() {
s32 kernel_driver_error = libusb_kernel_driver_active(usb_adapter_handle->get(), 0);
if (kernel_driver_error == 1) {
kernel_driver_error = libusb_detach_kernel_driver(usb_adapter_handle->get(), 0);
if (kernel_driver_error != 0 && kernel_driver_error != LIBUSB_ERROR_NOT_SUPPORTED) {
LOG_ERROR(Input, "libusb_detach_kernel_driver failed with error = {}",
kernel_driver_error);
}
}
// This fixes payload problems from offbrand GCAdapters
const s32 control_transfer_error =
libusb_control_transfer(usb_adapter_handle->get(), 0x21, 11, 0x0001, 0, nullptr, 0, 1000);
if (control_transfer_error < 0) {
LOG_ERROR(Input, "libusb_control_transfer failed with error= {}", control_transfer_error);
}
if (kernel_driver_error && kernel_driver_error != LIBUSB_ERROR_NOT_SUPPORTED) {
usb_adapter_handle = nullptr;
return false;
}
const int interface_claim_error = libusb_claim_interface(usb_adapter_handle->get(), 0);
if (interface_claim_error) {
LOG_ERROR(Input, "libusb_claim_interface failed with error = {}", interface_claim_error);
usb_adapter_handle = nullptr;
return false;
}
return true;
}
bool GCAdapter::GetGCEndpoint(libusb_device* device) {
libusb_config_descriptor* config = nullptr;
const int config_descriptor_return = libusb_get_config_descriptor(device, 0, &config);
if (config_descriptor_return != LIBUSB_SUCCESS) {
LOG_ERROR(Input, "libusb_get_config_descriptor failed with error = {}",
config_descriptor_return);
return false;
}
for (u8 ic = 0; ic < config->bNumInterfaces; ic++) {
const libusb_interface* interfaceContainer = &config->interface[ic];
for (int i = 0; i < interfaceContainer->num_altsetting; i++) {
const libusb_interface_descriptor* interface = &interfaceContainer->altsetting[i];
for (u8 e = 0; e < interface->bNumEndpoints; e++) {
const libusb_endpoint_descriptor* endpoint = &interface->endpoint[e];
if ((endpoint->bEndpointAddress & LIBUSB_ENDPOINT_IN) != 0) {
input_endpoint = endpoint->bEndpointAddress;
} else {
output_endpoint = endpoint->bEndpointAddress;
}
}
}
}
// This transfer seems to be responsible for clearing the state of the adapter
// Used to clear the "busy" state of when the device is unexpectedly unplugged
unsigned char clear_payload = 0x13;
libusb_interrupt_transfer(usb_adapter_handle->get(), output_endpoint, &clear_payload,
sizeof(clear_payload), nullptr, 16);
return true;
}
Common::Input::VibrationError GCAdapter::SetRumble(const PadIdentifier& identifier,
const Common::Input::VibrationStatus vibration) {
const auto mean_amplitude = (vibration.low_amplitude + vibration.high_amplitude) * 0.5f;
const auto processed_amplitude =
static_cast<u8>((mean_amplitude + std::pow(mean_amplitude, 0.3f)) * 0.5f * 0x8);
pads[identifier.port].rumble_amplitude = processed_amplitude;
if (!rumble_enabled) {
return Common::Input::VibrationError::Disabled;
}
return Common::Input::VibrationError::None;
}
void GCAdapter::UpdateVibrations() {
// Use 8 states to keep the switching between on/off fast enough for
// a human to feel different vibration strenght
// More states == more rumble strengths == slower update time
constexpr u8 vibration_states = 8;
vibration_counter = (vibration_counter + 1) % vibration_states;
for (GCController& pad : pads) {
const bool vibrate = pad.rumble_amplitude > vibration_counter;
vibration_changed |= vibrate != pad.enable_vibration;
pad.enable_vibration = vibrate;
}
SendVibrations();
}
void GCAdapter::SendVibrations() {
if (!rumble_enabled || !vibration_changed) {
return;
}
s32 size{};
constexpr u8 rumble_command = 0x11;
const u8 p1 = pads[0].enable_vibration;
const u8 p2 = pads[1].enable_vibration;
const u8 p3 = pads[2].enable_vibration;
const u8 p4 = pads[3].enable_vibration;
std::array<u8, 5> payload = {rumble_command, p1, p2, p3, p4};
const int err =
libusb_interrupt_transfer(usb_adapter_handle->get(), output_endpoint, payload.data(),
static_cast<s32>(payload.size()), &size, 16);
if (err) {
LOG_DEBUG(Input, "Libusb write failed: {}", libusb_error_name(err));
if (output_error_counter++ > 5) {
LOG_ERROR(Input, "Output timeout, Rumble disabled");
rumble_enabled = false;
}
return;
}
output_error_counter = 0;
vibration_changed = false;
}
bool GCAdapter::DeviceConnected(std::size_t port) const {
return pads[port].type != ControllerTypes::None;
}
void GCAdapter::Reset() {
adapter_scan_thread = {};
adapter_input_thread = {};
usb_adapter_handle = nullptr;
pads = {};
libusb_ctx = nullptr;
}
std::vector<Common::ParamPackage> GCAdapter::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
for (std::size_t port = 0; port < pads.size(); ++port) {
if (!DeviceConnected(port)) {
continue;
}
Common::ParamPackage identifier{};
identifier.Set("engine", GetEngineName());
identifier.Set("display", fmt::format("Gamecube Controller {}", port + 1));
identifier.Set("port", static_cast<int>(port));
devices.emplace_back(identifier);
}
return devices;
}
ButtonMapping GCAdapter::GetButtonMappingForDevice(const Common::ParamPackage& params) {
// This list is missing ZL/ZR since those are not considered buttons.
// We will add those afterwards
// This list also excludes any button that can't be really mapped
static constexpr std::array<std::pair<Settings::NativeButton::Values, PadButton>, 12>
switch_to_gcadapter_button = {
std::pair{Settings::NativeButton::A, PadButton::ButtonA},
{Settings::NativeButton::B, PadButton::ButtonB},
{Settings::NativeButton::X, PadButton::ButtonX},
{Settings::NativeButton::Y, PadButton::ButtonY},
{Settings::NativeButton::Plus, PadButton::ButtonStart},
{Settings::NativeButton::DLeft, PadButton::ButtonLeft},
{Settings::NativeButton::DUp, PadButton::ButtonUp},
{Settings::NativeButton::DRight, PadButton::ButtonRight},
{Settings::NativeButton::DDown, PadButton::ButtonDown},
{Settings::NativeButton::SL, PadButton::TriggerL},
{Settings::NativeButton::SR, PadButton::TriggerR},
{Settings::NativeButton::R, PadButton::TriggerZ},
};
if (!params.Has("port")) {
return {};
}
ButtonMapping mapping{};
for (const auto& [switch_button, gcadapter_button] : switch_to_gcadapter_button) {
Common::ParamPackage button_params{};
button_params.Set("engine", GetEngineName());
button_params.Set("port", params.Get("port", 0));
button_params.Set("button", static_cast<int>(gcadapter_button));
mapping.insert_or_assign(switch_button, std::move(button_params));
}
// Add the missing bindings for ZL/ZR
static constexpr std::array<std::tuple<Settings::NativeButton::Values, PadButton, PadAxes>, 2>
switch_to_gcadapter_axis = {
std::tuple{Settings::NativeButton::ZL, PadButton::TriggerL, PadAxes::TriggerLeft},
{Settings::NativeButton::ZR, PadButton::TriggerR, PadAxes::TriggerRight},
};
for (const auto& [switch_button, gcadapter_buton, gcadapter_axis] : switch_to_gcadapter_axis) {
Common::ParamPackage button_params{};
button_params.Set("engine", GetEngineName());
button_params.Set("port", params.Get("port", 0));
button_params.Set("button", static_cast<s32>(gcadapter_buton));
button_params.Set("axis", static_cast<s32>(gcadapter_axis));
button_params.Set("threshold", 0.5f);
button_params.Set("range", 1.9f);
button_params.Set("direction", "+");
mapping.insert_or_assign(switch_button, std::move(button_params));
}
return mapping;
}
AnalogMapping GCAdapter::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("port")) {
return {};
}
AnalogMapping mapping = {};
Common::ParamPackage left_analog_params;
left_analog_params.Set("engine", GetEngineName());
left_analog_params.Set("port", params.Get("port", 0));
left_analog_params.Set("axis_x", static_cast<int>(PadAxes::StickX));
left_analog_params.Set("axis_y", static_cast<int>(PadAxes::StickY));
mapping.insert_or_assign(Settings::NativeAnalog::LStick, std::move(left_analog_params));
Common::ParamPackage right_analog_params;
right_analog_params.Set("engine", GetEngineName());
right_analog_params.Set("port", params.Get("port", 0));
right_analog_params.Set("axis_x", static_cast<int>(PadAxes::SubstickX));
right_analog_params.Set("axis_y", static_cast<int>(PadAxes::SubstickY));
mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params));
return mapping;
}
std::string GCAdapter::GetUIButtonName(const Common::ParamPackage& params) const {
PadButton button = static_cast<PadButton>(params.Get("button", 0));
switch (button) {
case PadButton::ButtonLeft:
return "left";
break;
case PadButton::ButtonRight:
return "right";
break;
case PadButton::ButtonDown:
return "down";
break;
case PadButton::ButtonUp:
return "up";
break;
case PadButton::TriggerZ:
return "Z";
break;
case PadButton::TriggerR:
return "R";
break;
case PadButton::TriggerL:
return "L";
break;
case PadButton::ButtonA:
return "A";
break;
case PadButton::ButtonB:
return "B";
break;
case PadButton::ButtonX:
return "X";
break;
case PadButton::ButtonY:
return "Y";
break;
case PadButton::ButtonStart:
return "start";
break;
default:
return "Unkown GC";
}
}
std::string GCAdapter::GetUIName(const Common::ParamPackage& params) const {
if (params.Has("button")) {
return fmt::format("Button {}", GetUIButtonName(params));
}
if (params.Has("axis")) {
return fmt::format("Axis {}", params.Get("axis", 0));
}
return "Bad GC Adapter";
}
} // namespace InputCommon

View File

@@ -0,0 +1,135 @@
// Copyright 2014 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#pragma once
#include <array>
#include <memory>
#include <mutex>
#include <stop_token>
#include <string>
#include <thread>
#include "input_common/input_engine.h"
struct libusb_context;
struct libusb_device;
struct libusb_device_handle;
namespace InputCommon {
class LibUSBContext;
class LibUSBDeviceHandle;
class GCAdapter : public InputCommon::InputEngine {
public:
explicit GCAdapter(const std::string input_engine_);
~GCAdapter();
Common::Input::VibrationError SetRumble(
const PadIdentifier& identifier, const Common::Input::VibrationStatus vibration) override;
/// Used for automapping features
std::vector<Common::ParamPackage> GetInputDevices() const override;
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
std::string GetUIName(const Common::ParamPackage& params) const override;
private:
enum class PadButton {
Undefined = 0x0000,
ButtonLeft = 0x0001,
ButtonRight = 0x0002,
ButtonDown = 0x0004,
ButtonUp = 0x0008,
TriggerZ = 0x0010,
TriggerR = 0x0020,
TriggerL = 0x0040,
ButtonA = 0x0100,
ButtonB = 0x0200,
ButtonX = 0x0400,
ButtonY = 0x0800,
ButtonStart = 0x1000,
};
enum class PadAxes : u8 {
StickX,
StickY,
SubstickX,
SubstickY,
TriggerLeft,
TriggerRight,
Undefined,
};
enum class ControllerTypes {
None,
Wired,
Wireless,
};
struct GCController {
ControllerTypes type = ControllerTypes::None;
PadIdentifier identifier{};
bool enable_vibration = false;
u8 rumble_amplitude{};
std::array<u8, 6> axis_origin{};
u8 reset_origin_counter{};
};
using AdapterPayload = std::array<u8, 37>;
void UpdatePadType(std::size_t port, ControllerTypes pad_type);
void UpdateControllers(const AdapterPayload& adapter_payload);
void UpdateStateButtons(std::size_t port, u8 b1, u8 b2);
void UpdateStateAxes(std::size_t port, const AdapterPayload& adapter_payload);
void AdapterInputThread(std::stop_token stop_token);
void AdapterScanThread(std::stop_token stop_token);
bool IsPayloadCorrect(const AdapterPayload& adapter_payload, s32 payload_size);
/// For use in initialization, querying devices to find the adapter
bool Setup();
/// Returns true if we successfully gain access to GC Adapter
bool CheckDeviceAccess();
/// Captures GC Adapter endpoint address
/// Returns true if the endpoint was set correctly
bool GetGCEndpoint(libusb_device* device);
/// Returns true if there is a device connected to port
bool DeviceConnected(std::size_t port) const;
/// For shutting down, clear all data, join all threads, release usb
void Reset();
void UpdateVibrations();
/// Updates vibration state of all controllers
void SendVibrations();
std::string GetUIButtonName(const Common::ParamPackage& params) const;
std::unique_ptr<LibUSBDeviceHandle> usb_adapter_handle;
std::array<GCController, 4> pads;
std::jthread adapter_input_thread;
std::jthread adapter_scan_thread;
bool restart_scan_thread{};
std::unique_ptr<LibUSBContext> libusb_ctx;
u8 input_endpoint{0};
u8 output_endpoint{0};
u8 input_error_counter{0};
u8 output_error_counter{0};
int vibration_counter{0};
bool rumble_enabled{true};
bool vibration_changed{true};
};
} // namespace InputCommon

View File

@@ -0,0 +1,41 @@
// Copyright 2021 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included
#include "common/param_package.h"
#include "input_common/drivers/keyboard.h"
namespace InputCommon {
constexpr PadIdentifier identifier = {
.guid = Common::UUID{Common::INVALID_UUID},
.port = 0,
.pad = 0,
};
Keyboard::Keyboard(const std::string& input_engine_) : InputEngine(input_engine_) {
PreSetController(identifier);
}
void Keyboard::PressKey(int key_code) {
SetButton(identifier, key_code, true);
}
void Keyboard::ReleaseKey(int key_code) {
SetButton(identifier, key_code, false);
}
void Keyboard::ReleaseAllKeys() {
ResetButtonState();
}
std::vector<Common::ParamPackage> Keyboard::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
devices.emplace_back(Common::ParamPackage{
{"engine", GetEngineName()},
{"display", "Keyboard Only"},
});
return devices;
}
} // namespace InputCommon

View File

@@ -0,0 +1,37 @@
// Copyright 2021 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included
#pragma once
#include "input_common/input_engine.h"
namespace InputCommon {
/**
* A button device factory representing a keyboard. It receives keyboard events and forward them
* to all button devices it created.
*/
class Keyboard final : public InputCommon::InputEngine {
public:
explicit Keyboard(const std::string& input_engine_);
/**
* Sets the status of all buttons bound with the key to pressed
* @param key_code the code of the key to press
*/
void PressKey(int key_code);
/**
* Sets the status of all buttons bound with the key to released
* @param key_code the code of the key to release
*/
void ReleaseKey(int key_code);
void ReleaseAllKeys();
/// Used for automapping features
std::vector<Common::ParamPackage> GetInputDevices() const override;
};
} // namespace InputCommon

View File

@@ -0,0 +1,158 @@
// Copyright 2021 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included
#include <stop_token>
#include <thread>
#include <fmt/format.h>
#include "common/param_package.h"
#include "common/settings.h"
#include "common/thread.h"
#include "input_common/drivers/mouse.h"
namespace InputCommon {
constexpr int touch_axis_x = 10;
constexpr int touch_axis_y = 11;
constexpr PadIdentifier identifier = {
.guid = Common::UUID{Common::INVALID_UUID},
.port = 0,
.pad = 0,
};
Mouse::Mouse(const std::string input_engine_) : InputEngine(input_engine_) {
PreSetController(identifier);
update_thread = std::jthread([this](std::stop_token stop_token) { UpdateThread(stop_token); });
}
void Mouse::UpdateThread(std::stop_token stop_token) {
Common::SetCurrentThreadName("yuzu:input:Mouse");
constexpr int update_time = 10;
while (!stop_token.stop_requested()) {
if (Settings::values.mouse_panning) {
// Slow movement by 4%
last_mouse_change *= 0.96f;
const float sensitivity =
Settings::values.mouse_panning_sensitivity.GetValue() * 0.022f;
SetAxis(identifier, 0, last_mouse_change.x * sensitivity);
SetAxis(identifier, 1, -last_mouse_change.y * sensitivity);
}
if (mouse_panning_timout++ > 20) {
StopPanning();
}
std::this_thread::sleep_for(std::chrono::milliseconds(update_time));
}
}
void Mouse::MouseMove(int x, int y, f32 touch_x, f32 touch_y, int center_x, int center_y) {
SetAxis(identifier, touch_axis_x, touch_x);
SetAxis(identifier, touch_axis_y, touch_y);
if (Settings::values.mouse_panning) {
auto mouse_change =
(Common::MakeVec(x, y) - Common::MakeVec(center_x, center_y)).Cast<float>();
mouse_panning_timout = 0;
const auto move_distance = mouse_change.Length();
if (move_distance == 0) {
return;
}
// Make slow movements at least 3 units on lenght
if (move_distance < 3.0f) {
// Normalize value
mouse_change /= move_distance;
mouse_change *= 3.0f;
}
// Average mouse movements
last_mouse_change = (last_mouse_change * 0.91f) + (mouse_change * 0.09f);
const auto last_move_distance = last_mouse_change.Length();
// Make fast movements clamp to 8 units on lenght
if (last_move_distance > 8.0f) {
// Normalize value
last_mouse_change /= last_move_distance;
last_mouse_change *= 8.0f;
}
// Ignore average if it's less than 1 unit and use current movement value
if (last_move_distance < 1.0f) {
last_mouse_change = mouse_change / mouse_change.Length();
}
return;
}
if (button_pressed) {
const auto mouse_move = Common::MakeVec<int>(x, y) - mouse_origin;
const float sensitivity = Settings::values.mouse_panning_sensitivity.GetValue() * 0.0012f;
SetAxis(identifier, 0, static_cast<float>(mouse_move.x) * sensitivity);
SetAxis(identifier, 1, static_cast<float>(-mouse_move.y) * sensitivity);
}
}
void Mouse::PressButton(int x, int y, f32 touch_x, f32 touch_y, MouseButton button) {
SetAxis(identifier, touch_axis_x, touch_x);
SetAxis(identifier, touch_axis_y, touch_y);
SetButton(identifier, static_cast<int>(button), true);
// Set initial analog parameters
mouse_origin = {x, y};
last_mouse_position = {x, y};
button_pressed = true;
}
void Mouse::ReleaseButton(MouseButton button) {
SetButton(identifier, static_cast<int>(button), false);
if (!Settings::values.mouse_panning) {
SetAxis(identifier, 0, 0);
SetAxis(identifier, 1, 0);
}
button_pressed = false;
}
void Mouse::ReleaseAllButtons() {
ResetButtonState();
button_pressed = false;
}
void Mouse::StopPanning() {
last_mouse_change = {};
}
std::vector<Common::ParamPackage> Mouse::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
devices.emplace_back(Common::ParamPackage{
{"engine", GetEngineName()},
{"display", "Keyboard/Mouse"},
});
return devices;
}
AnalogMapping Mouse::GetAnalogMappingForDevice(
[[maybe_unused]] const Common::ParamPackage& params) {
// Only overwrite different buttons from default
AnalogMapping mapping = {};
Common::ParamPackage right_analog_params;
right_analog_params.Set("engine", GetEngineName());
right_analog_params.Set("axis_x", 0);
right_analog_params.Set("axis_y", 1);
right_analog_params.Set("threshold", 0.5f);
right_analog_params.Set("range", 1.0f);
right_analog_params.Set("deadzone", 0.0f);
mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params));
return mapping;
}
std::string Mouse::GetUIName(const Common::ParamPackage& params) const {
if (params.Has("button")) {
return fmt::format("Mouse {}", params.Get("button", 0));
}
return "Bad Mouse";
}
} // namespace InputCommon

View File

@@ -0,0 +1,73 @@
// Copyright 2021 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included
#pragma once
#include <stop_token>
#include <thread>
#include "common/vector_math.h"
#include "input_common/input_engine.h"
namespace InputCommon {
enum class MouseButton {
Left,
Right,
Wheel,
Backward,
Forward,
Task,
Extra,
Undefined,
};
/**
* A button device factory representing a keyboard. It receives keyboard events and forward them
* to all button devices it created.
*/
class Mouse final : public InputCommon::InputEngine {
public:
explicit Mouse(const std::string input_engine_);
/**
* Signals that mouse has moved.
* @param x the x-coordinate of the cursor
* @param y the y-coordinate of the cursor
* @param center_x the x-coordinate of the middle of the screen
* @param center_y the y-coordinate of the middle of the screen
*/
void MouseMove(int x, int y, f32 touch_x, f32 touch_y, int center_x, int center_y);
/**
* Sets the status of all buttons bound with the key to pressed
* @param key_code the code of the key to press
*/
void PressButton(int x, int y, f32 touch_x, f32 touch_y, MouseButton button);
/**
* Sets the status of all buttons bound with the key to released
* @param key_code the code of the key to release
*/
void ReleaseButton(MouseButton button);
void ReleaseAllButtons();
std::vector<Common::ParamPackage> GetInputDevices() const override;
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
std::string GetUIName(const Common::ParamPackage& params) const override;
private:
void UpdateThread(std::stop_token stop_token);
void StopPanning();
Common::Vec2<int> mouse_origin;
Common::Vec2<int> last_mouse_position;
Common::Vec2<float> last_mouse_change;
bool button_pressed;
int mouse_panning_timout{};
std::jthread update_thread;
};
} // namespace InputCommon

View File

@@ -0,0 +1,948 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "common/logging/log.h"
#include "common/math_util.h"
#include "common/param_package.h"
#include "common/settings.h"
#include "common/thread.h"
#include "common/vector_math.h"
#include "input_common/drivers/sdl_driver.h"
namespace InputCommon {
namespace {
std::string GetGUID(SDL_Joystick* joystick) {
const SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick);
char guid_str[33];
SDL_JoystickGetGUIDString(guid, guid_str, sizeof(guid_str));
return guid_str;
}
} // Anonymous namespace
static int SDLEventWatcher(void* user_data, SDL_Event* event) {
auto* const sdl_state = static_cast<SDLDriver*>(user_data);
sdl_state->HandleGameControllerEvent(*event);
return 0;
}
class SDLJoystick {
public:
SDLJoystick(std::string guid_, int port_, SDL_Joystick* joystick,
SDL_GameController* game_controller)
: guid{std::move(guid_)}, port{port_}, sdl_joystick{joystick, &SDL_JoystickClose},
sdl_controller{game_controller, &SDL_GameControllerClose} {
EnableMotion();
}
void EnableMotion() {
if (sdl_controller) {
SDL_GameController* controller = sdl_controller.get();
if (SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL) && !has_accel) {
SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE);
has_accel = true;
}
if (SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO) && !has_gyro) {
SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE);
has_gyro = true;
}
}
}
bool HasGyro() const {
return has_gyro;
}
bool HasAccel() const {
return has_accel;
}
bool UpdateMotion(SDL_ControllerSensorEvent event) {
constexpr float gravity_constant = 9.80665f;
std::lock_guard lock{mutex};
const u64 time_difference = event.timestamp - last_motion_update;
last_motion_update = event.timestamp;
switch (event.sensor) {
case SDL_SENSOR_ACCEL: {
motion.accel_x = -event.data[0] / gravity_constant;
motion.accel_y = event.data[2] / gravity_constant;
motion.accel_z = -event.data[1] / gravity_constant;
break;
}
case SDL_SENSOR_GYRO: {
motion.gyro_x = event.data[0] / (Common::PI * 2);
motion.gyro_y = -event.data[2] / (Common::PI * 2);
motion.gyro_z = event.data[1] / (Common::PI * 2);
break;
}
}
// Ignore duplicated timestamps
if (time_difference == 0) {
return false;
}
motion.delta_timestamp = time_difference * 1000;
return true;
}
BasicMotion GetMotion() {
return motion;
}
bool RumblePlay(const Common::Input::VibrationStatus vibration) {
constexpr u32 rumble_max_duration_ms = 1000;
if (sdl_controller) {
return SDL_GameControllerRumble(
sdl_controller.get(), static_cast<u16>(vibration.low_amplitude),
static_cast<u16>(vibration.high_amplitude), rumble_max_duration_ms) != -1;
} else if (sdl_joystick) {
return SDL_JoystickRumble(sdl_joystick.get(), static_cast<u16>(vibration.low_amplitude),
static_cast<u16>(vibration.high_amplitude),
rumble_max_duration_ms) != -1;
}
return false;
}
bool HasHDRumble() const {
if (sdl_controller) {
return (SDL_GameControllerGetType(sdl_controller.get()) ==
SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO);
}
return false;
}
/**
* The Pad identifier of the joystick
*/
const PadIdentifier GetPadIdentifier() const {
return {
.guid = Common::UUID{guid},
.port = static_cast<std::size_t>(port),
.pad = 0,
};
}
/**
* The guid of the joystick
*/
const std::string& GetGUID() const {
return guid;
}
/**
* The number of joystick from the same type that were connected before this joystick
*/
int GetPort() const {
return port;
}
SDL_Joystick* GetSDLJoystick() const {
return sdl_joystick.get();
}
SDL_GameController* GetSDLGameController() const {
return sdl_controller.get();
}
void SetSDLJoystick(SDL_Joystick* joystick, SDL_GameController* controller) {
sdl_joystick.reset(joystick);
sdl_controller.reset(controller);
}
bool IsJoyconLeft() const {
const std::string controller_name = GetControllerName();
if (std::strstr(controller_name.c_str(), "Joy-Con Left") != nullptr) {
return true;
}
if (std::strstr(controller_name.c_str(), "Joy-Con (L)") != nullptr) {
return true;
}
return false;
}
bool IsJoyconRight() const {
const std::string controller_name = GetControllerName();
if (std::strstr(controller_name.c_str(), "Joy-Con Right") != nullptr) {
return true;
}
if (std::strstr(controller_name.c_str(), "Joy-Con (R)") != nullptr) {
return true;
}
return false;
}
BatteryLevel GetBatteryLevel() {
const auto level = SDL_JoystickCurrentPowerLevel(sdl_joystick.get());
switch (level) {
case SDL_JOYSTICK_POWER_EMPTY:
return BatteryLevel::Empty;
case SDL_JOYSTICK_POWER_LOW:
return BatteryLevel::Critical;
case SDL_JOYSTICK_POWER_MEDIUM:
return BatteryLevel::Low;
case SDL_JOYSTICK_POWER_FULL:
return BatteryLevel::Medium;
case SDL_JOYSTICK_POWER_MAX:
return BatteryLevel::Full;
case SDL_JOYSTICK_POWER_UNKNOWN:
case SDL_JOYSTICK_POWER_WIRED:
default:
return BatteryLevel::Charging;
}
}
std::string GetControllerName() const {
if (sdl_controller) {
switch (SDL_GameControllerGetType(sdl_controller.get())) {
case SDL_CONTROLLER_TYPE_XBOX360:
return "XBox 360 Controller";
case SDL_CONTROLLER_TYPE_XBOXONE:
return "XBox One Controller";
default:
break;
}
const auto name = SDL_GameControllerName(sdl_controller.get());
if (name) {
return name;
}
}
if (sdl_joystick) {
const auto name = SDL_JoystickName(sdl_joystick.get());
if (name) {
return name;
}
}
return "Unknown";
}
bool IsYAxis(u8 index) {
if (!sdl_controller) {
return false;
}
const auto& binding_left_y =
SDL_GameControllerGetBindForAxis(sdl_controller.get(), SDL_CONTROLLER_AXIS_LEFTY);
const auto& binding_right_y =
SDL_GameControllerGetBindForAxis(sdl_controller.get(), SDL_CONTROLLER_AXIS_RIGHTY);
if (index == binding_left_y.value.axis) {
return true;
}
if (index == binding_right_y.value.axis) {
return true;
}
return false;
}
private:
std::string guid;
int port;
std::unique_ptr<SDL_Joystick, decltype(&SDL_JoystickClose)> sdl_joystick;
std::unique_ptr<SDL_GameController, decltype(&SDL_GameControllerClose)> sdl_controller;
mutable std::mutex mutex;
u64 last_motion_update{};
bool has_gyro{false};
bool has_accel{false};
BasicMotion motion;
};
std::shared_ptr<SDLJoystick> SDLDriver::GetSDLJoystickByGUID(const std::string& guid, int port) {
std::lock_guard lock{joystick_map_mutex};
const auto it = joystick_map.find(guid);
if (it != joystick_map.end()) {
while (it->second.size() <= static_cast<std::size_t>(port)) {
auto joystick = std::make_shared<SDLJoystick>(guid, static_cast<int>(it->second.size()),
nullptr, nullptr);
it->second.emplace_back(std::move(joystick));
}
return it->second[static_cast<std::size_t>(port)];
}
auto joystick = std::make_shared<SDLJoystick>(guid, 0, nullptr, nullptr);
return joystick_map[guid].emplace_back(std::move(joystick));
}
std::shared_ptr<SDLJoystick> SDLDriver::GetSDLJoystickBySDLID(SDL_JoystickID sdl_id) {
auto sdl_joystick = SDL_JoystickFromInstanceID(sdl_id);
const std::string guid = GetGUID(sdl_joystick);
std::lock_guard lock{joystick_map_mutex};
const auto map_it = joystick_map.find(guid);
if (map_it == joystick_map.end()) {
return nullptr;
}
const auto vec_it = std::find_if(map_it->second.begin(), map_it->second.end(),
[&sdl_joystick](const auto& joystick) {
return joystick->GetSDLJoystick() == sdl_joystick;
});
if (vec_it == map_it->second.end()) {
return nullptr;
}
return *vec_it;
}
void SDLDriver::InitJoystick(int joystick_index) {
SDL_Joystick* sdl_joystick = SDL_JoystickOpen(joystick_index);
SDL_GameController* sdl_gamecontroller = nullptr;
if (SDL_IsGameController(joystick_index)) {
sdl_gamecontroller = SDL_GameControllerOpen(joystick_index);
}
if (!sdl_joystick) {
LOG_ERROR(Input, "Failed to open joystick {}", joystick_index);
return;
}
const std::string guid = GetGUID(sdl_joystick);
std::lock_guard lock{joystick_map_mutex};
if (joystick_map.find(guid) == joystick_map.end()) {
auto joystick = std::make_shared<SDLJoystick>(guid, 0, sdl_joystick, sdl_gamecontroller);
PreSetController(joystick->GetPadIdentifier());
SetBattery(joystick->GetPadIdentifier(), joystick->GetBatteryLevel());
joystick_map[guid].emplace_back(std::move(joystick));
return;
}
auto& joystick_guid_list = joystick_map[guid];
const auto joystick_it =
std::find_if(joystick_guid_list.begin(), joystick_guid_list.end(),
[](const auto& joystick) { return !joystick->GetSDLJoystick(); });
if (joystick_it != joystick_guid_list.end()) {
(*joystick_it)->SetSDLJoystick(sdl_joystick, sdl_gamecontroller);
return;
}
const int port = static_cast<int>(joystick_guid_list.size());
auto joystick = std::make_shared<SDLJoystick>(guid, port, sdl_joystick, sdl_gamecontroller);
PreSetController(joystick->GetPadIdentifier());
SetBattery(joystick->GetPadIdentifier(), joystick->GetBatteryLevel());
joystick_guid_list.emplace_back(std::move(joystick));
}
void SDLDriver::CloseJoystick(SDL_Joystick* sdl_joystick) {
const std::string guid = GetGUID(sdl_joystick);
std::lock_guard lock{joystick_map_mutex};
// This call to guid is safe since the joystick is guaranteed to be in the map
const auto& joystick_guid_list = joystick_map[guid];
const auto joystick_it = std::find_if(joystick_guid_list.begin(), joystick_guid_list.end(),
[&sdl_joystick](const auto& joystick) {
return joystick->GetSDLJoystick() == sdl_joystick;
});
if (joystick_it != joystick_guid_list.end()) {
(*joystick_it)->SetSDLJoystick(nullptr, nullptr);
}
}
void SDLDriver::HandleGameControllerEvent(const SDL_Event& event) {
switch (event.type) {
case SDL_JOYBUTTONUP: {
if (const auto joystick = GetSDLJoystickBySDLID(event.jbutton.which)) {
const PadIdentifier identifier = joystick->GetPadIdentifier();
SetButton(identifier, event.jbutton.button, false);
}
break;
}
case SDL_JOYBUTTONDOWN: {
if (const auto joystick = GetSDLJoystickBySDLID(event.jbutton.which)) {
const PadIdentifier identifier = joystick->GetPadIdentifier();
SetButton(identifier, event.jbutton.button, true);
}
break;
}
case SDL_JOYHATMOTION: {
if (const auto joystick = GetSDLJoystickBySDLID(event.jhat.which)) {
const PadIdentifier identifier = joystick->GetPadIdentifier();
SetHatButton(identifier, event.jhat.hat, event.jhat.value);
}
break;
}
case SDL_JOYAXISMOTION: {
if (const auto joystick = GetSDLJoystickBySDLID(event.jaxis.which)) {
const PadIdentifier identifier = joystick->GetPadIdentifier();
// Vertical axis is inverted on nintendo compared to SDL
if (joystick->IsYAxis(event.jaxis.axis)) {
SetAxis(identifier, event.jaxis.axis, -event.jaxis.value / 32767.0f);
break;
}
SetAxis(identifier, event.jaxis.axis, event.jaxis.value / 32767.0f);
}
break;
}
case SDL_CONTROLLERSENSORUPDATE: {
if (auto joystick = GetSDLJoystickBySDLID(event.csensor.which)) {
if (joystick->UpdateMotion(event.csensor)) {
const PadIdentifier identifier = joystick->GetPadIdentifier();
SetMotion(identifier, 0, joystick->GetMotion());
};
}
break;
}
case SDL_JOYDEVICEREMOVED:
LOG_DEBUG(Input, "Controller removed with Instance_ID {}", event.jdevice.which);
CloseJoystick(SDL_JoystickFromInstanceID(event.jdevice.which));
break;
case SDL_JOYDEVICEADDED:
LOG_DEBUG(Input, "Controller connected with device index {}", event.jdevice.which);
InitJoystick(event.jdevice.which);
break;
}
}
void SDLDriver::CloseJoysticks() {
std::lock_guard lock{joystick_map_mutex};
joystick_map.clear();
}
SDLDriver::SDLDriver(const std::string& input_engine_) : InputEngine(input_engine_) {
Common::SetCurrentThreadName("yuzu:input:SDL");
if (!Settings::values.enable_raw_input) {
// Disable raw input. When enabled this setting causes SDL to die when a web applet opens
SDL_SetHint(SDL_HINT_JOYSTICK_RAWINPUT, "0");
}
// Prevent SDL from adding undesired axis
SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0");
// Enable HIDAPI rumble. This prevents SDL from disabling motion on PS4 and PS5 controllers
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
// Use hidapi driver for joycons. This will allow joycons to be detected as a GameController and
// not a generic one
SDL_SetHint("SDL_JOYSTICK_HIDAPI_JOY_CONS", "1");
// Turn off Pro controller home led
SDL_SetHint("SDL_JOYSTICK_HIDAPI_SWITCH_HOME_LED", "0");
// If the frontend is going to manage the event loop, then we don't start one here
start_thread = SDL_WasInit(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) == 0;
if (start_thread && SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) < 0) {
LOG_CRITICAL(Input, "SDL_Init failed with: {}", SDL_GetError());
return;
}
SDL_AddEventWatch(&SDLEventWatcher, this);
initialized = true;
if (start_thread) {
poll_thread = std::thread([this] {
using namespace std::chrono_literals;
while (initialized) {
SDL_PumpEvents();
std::this_thread::sleep_for(1ms);
}
});
}
// Because the events for joystick connection happens before we have our event watcher added, we
// can just open all the joysticks right here
for (int i = 0; i < SDL_NumJoysticks(); ++i) {
InitJoystick(i);
}
}
SDLDriver::~SDLDriver() {
CloseJoysticks();
SDL_DelEventWatch(&SDLEventWatcher, this);
initialized = false;
if (start_thread) {
poll_thread.join();
SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER);
}
}
std::vector<Common::ParamPackage> SDLDriver::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
std::unordered_map<int, std::shared_ptr<SDLJoystick>> joycon_pairs;
for (const auto& [key, value] : joystick_map) {
for (const auto& joystick : value) {
if (!joystick->GetSDLJoystick()) {
continue;
}
const std::string name =
fmt::format("{} {}", joystick->GetControllerName(), joystick->GetPort());
devices.emplace_back(Common::ParamPackage{
{"engine", GetEngineName()},
{"display", std::move(name)},
{"guid", joystick->GetGUID()},
{"port", std::to_string(joystick->GetPort())},
});
if (joystick->IsJoyconLeft()) {
joycon_pairs.insert_or_assign(joystick->GetPort(), joystick);
}
}
}
// Add dual controllers
for (const auto& [key, value] : joystick_map) {
for (const auto& joystick : value) {
if (joystick->IsJoyconRight()) {
if (!joycon_pairs.contains(joystick->GetPort())) {
continue;
}
const auto joystick2 = joycon_pairs.at(joystick->GetPort());
const std::string name =
fmt::format("{} {}", "Nintendo Dual Joy-Con", joystick->GetPort());
devices.emplace_back(Common::ParamPackage{
{"engine", GetEngineName()},
{"display", std::move(name)},
{"guid", joystick->GetGUID()},
{"guid2", joystick2->GetGUID()},
{"port", std::to_string(joystick->GetPort())},
});
}
}
}
return devices;
}
Common::Input::VibrationError SDLDriver::SetRumble(const PadIdentifier& identifier,
const Common::Input::VibrationStatus vibration) {
const auto joystick =
GetSDLJoystickByGUID(identifier.guid.Format(), static_cast<int>(identifier.port));
const auto process_amplitude_exp = [](f32 amplitude, f32 factor) {
return (amplitude + std::pow(amplitude, factor)) * 0.5f * 0xFFFF;
};
// Default exponential curve for rumble
f32 factor = 0.35f;
// If vibration is set as a linear output use a flatter value
if (vibration.type == Common::Input::VibrationAmplificationType::Linear) {
factor = 0.5f;
}
// Amplitude for HD rumble needs no modification
if (joystick->HasHDRumble()) {
factor = 1.0f;
}
const Common::Input::VibrationStatus new_vibration{
.low_amplitude = process_amplitude_exp(vibration.low_amplitude, factor),
.low_frequency = vibration.low_frequency,
.high_amplitude = process_amplitude_exp(vibration.high_amplitude, factor),
.high_frequency = vibration.high_frequency,
.type = Common::Input::VibrationAmplificationType::Exponential,
};
if (!joystick->RumblePlay(new_vibration)) {
return Common::Input::VibrationError::Unknown;
}
return Common::Input::VibrationError::None;
}
Common::ParamPackage SDLDriver::BuildAnalogParamPackageForButton(int port, std::string guid,
s32 axis, float value) const {
Common::ParamPackage params{};
params.Set("engine", GetEngineName());
params.Set("port", port);
params.Set("guid", std::move(guid));
params.Set("axis", axis);
params.Set("threshold", "0.5");
params.Set("invert", value < 0 ? "-" : "+");
return params;
}
Common::ParamPackage SDLDriver::BuildButtonParamPackageForButton(int port, std::string guid,
s32 button) const {
Common::ParamPackage params{};
params.Set("engine", GetEngineName());
params.Set("port", port);
params.Set("guid", std::move(guid));
params.Set("button", button);
return params;
}
Common::ParamPackage SDLDriver::BuildHatParamPackageForButton(int port, std::string guid, s32 hat,
u8 value) const {
Common::ParamPackage params{};
params.Set("engine", GetEngineName());
params.Set("port", port);
params.Set("guid", std::move(guid));
params.Set("hat", hat);
params.Set("direction", GetHatButtonName(value));
return params;
}
Common::ParamPackage SDLDriver::BuildMotionParam(int port, std::string guid) const {
Common::ParamPackage params{};
params.Set("engine", GetEngineName());
params.Set("motion", 0);
params.Set("port", port);
params.Set("guid", std::move(guid));
return params;
}
Common::ParamPackage SDLDriver::BuildParamPackageForBinding(
int port, const std::string& guid, const SDL_GameControllerButtonBind& binding) const {
switch (binding.bindType) {
case SDL_CONTROLLER_BINDTYPE_NONE:
break;
case SDL_CONTROLLER_BINDTYPE_AXIS:
return BuildAnalogParamPackageForButton(port, guid, binding.value.axis);
case SDL_CONTROLLER_BINDTYPE_BUTTON:
return BuildButtonParamPackageForButton(port, guid, binding.value.button);
case SDL_CONTROLLER_BINDTYPE_HAT:
return BuildHatParamPackageForButton(port, guid, binding.value.hat.hat,
static_cast<u8>(binding.value.hat.hat_mask));
}
return {};
}
Common::ParamPackage SDLDriver::BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
int axis_y, float offset_x,
float offset_y) const {
Common::ParamPackage params;
params.Set("engine", GetEngineName());
params.Set("port", static_cast<int>(identifier.port));
params.Set("guid", identifier.guid.Format());
params.Set("axis_x", axis_x);
params.Set("axis_y", axis_y);
params.Set("offset_x", offset_x);
params.Set("offset_y", offset_y);
params.Set("invert_x", "+");
params.Set("invert_y", "+");
return params;
}
ButtonMapping SDLDriver::GetButtonMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port")) {
return {};
}
const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0));
auto* controller = joystick->GetSDLGameController();
if (controller == nullptr) {
return {};
}
// This list is missing ZL/ZR since those are not considered buttons in SDL GameController.
// We will add those afterwards
// This list also excludes Screenshot since theres not really a mapping for that
ButtonBindings switch_to_sdl_button;
if (SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO) {
switch_to_sdl_button = GetNintendoButtonBinding(joystick);
} else {
switch_to_sdl_button = GetDefaultButtonBinding();
}
// Add the missing bindings for ZL/ZR
static constexpr ZButtonBindings switch_to_sdl_axis{{
{Settings::NativeButton::ZL, SDL_CONTROLLER_AXIS_TRIGGERLEFT},
{Settings::NativeButton::ZR, SDL_CONTROLLER_AXIS_TRIGGERRIGHT},
}};
// Parameters contain two joysticks return dual
if (params.Has("guid2")) {
const auto joystick2 = GetSDLJoystickByGUID(params.Get("guid2", ""), params.Get("port", 0));
if (joystick2->GetSDLGameController() != nullptr) {
return GetDualControllerMapping(joystick, joystick2, switch_to_sdl_button,
switch_to_sdl_axis);
}
}
return GetSingleControllerMapping(joystick, switch_to_sdl_button, switch_to_sdl_axis);
}
ButtonBindings SDLDriver::GetDefaultButtonBinding() const {
return {
std::pair{Settings::NativeButton::A, SDL_CONTROLLER_BUTTON_B},
{Settings::NativeButton::B, SDL_CONTROLLER_BUTTON_A},
{Settings::NativeButton::X, SDL_CONTROLLER_BUTTON_Y},
{Settings::NativeButton::Y, SDL_CONTROLLER_BUTTON_X},
{Settings::NativeButton::LStick, SDL_CONTROLLER_BUTTON_LEFTSTICK},
{Settings::NativeButton::RStick, SDL_CONTROLLER_BUTTON_RIGHTSTICK},
{Settings::NativeButton::L, SDL_CONTROLLER_BUTTON_LEFTSHOULDER},
{Settings::NativeButton::R, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
{Settings::NativeButton::Plus, SDL_CONTROLLER_BUTTON_START},
{Settings::NativeButton::Minus, SDL_CONTROLLER_BUTTON_BACK},
{Settings::NativeButton::DLeft, SDL_CONTROLLER_BUTTON_DPAD_LEFT},
{Settings::NativeButton::DUp, SDL_CONTROLLER_BUTTON_DPAD_UP},
{Settings::NativeButton::DRight, SDL_CONTROLLER_BUTTON_DPAD_RIGHT},
{Settings::NativeButton::DDown, SDL_CONTROLLER_BUTTON_DPAD_DOWN},
{Settings::NativeButton::SL, SDL_CONTROLLER_BUTTON_LEFTSHOULDER},
{Settings::NativeButton::SR, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
{Settings::NativeButton::Home, SDL_CONTROLLER_BUTTON_GUIDE},
};
}
ButtonBindings SDLDriver::GetNintendoButtonBinding(
const std::shared_ptr<SDLJoystick>& joystick) const {
// Default SL/SR mapping for pro controllers
auto sl_button = SDL_CONTROLLER_BUTTON_LEFTSHOULDER;
auto sr_button = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER;
if (joystick->IsJoyconLeft()) {
sl_button = SDL_CONTROLLER_BUTTON_PADDLE2;
sr_button = SDL_CONTROLLER_BUTTON_PADDLE4;
}
if (joystick->IsJoyconRight()) {
sl_button = SDL_CONTROLLER_BUTTON_PADDLE3;
sr_button = SDL_CONTROLLER_BUTTON_PADDLE1;
}
return {
std::pair{Settings::NativeButton::A, SDL_CONTROLLER_BUTTON_A},
{Settings::NativeButton::B, SDL_CONTROLLER_BUTTON_B},
{Settings::NativeButton::X, SDL_CONTROLLER_BUTTON_X},
{Settings::NativeButton::Y, SDL_CONTROLLER_BUTTON_Y},
{Settings::NativeButton::LStick, SDL_CONTROLLER_BUTTON_LEFTSTICK},
{Settings::NativeButton::RStick, SDL_CONTROLLER_BUTTON_RIGHTSTICK},
{Settings::NativeButton::L, SDL_CONTROLLER_BUTTON_LEFTSHOULDER},
{Settings::NativeButton::R, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
{Settings::NativeButton::Plus, SDL_CONTROLLER_BUTTON_START},
{Settings::NativeButton::Minus, SDL_CONTROLLER_BUTTON_BACK},
{Settings::NativeButton::DLeft, SDL_CONTROLLER_BUTTON_DPAD_LEFT},
{Settings::NativeButton::DUp, SDL_CONTROLLER_BUTTON_DPAD_UP},
{Settings::NativeButton::DRight, SDL_CONTROLLER_BUTTON_DPAD_RIGHT},
{Settings::NativeButton::DDown, SDL_CONTROLLER_BUTTON_DPAD_DOWN},
{Settings::NativeButton::SL, sl_button},
{Settings::NativeButton::SR, sr_button},
{Settings::NativeButton::Home, SDL_CONTROLLER_BUTTON_GUIDE},
};
}
ButtonMapping SDLDriver::GetSingleControllerMapping(
const std::shared_ptr<SDLJoystick>& joystick, const ButtonBindings& switch_to_sdl_button,
const ZButtonBindings& switch_to_sdl_axis) const {
ButtonMapping mapping;
mapping.reserve(switch_to_sdl_button.size() + switch_to_sdl_axis.size());
auto* controller = joystick->GetSDLGameController();
for (const auto& [switch_button, sdl_button] : switch_to_sdl_button) {
const auto& binding = SDL_GameControllerGetBindForButton(controller, sdl_button);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding));
}
for (const auto& [switch_button, sdl_axis] : switch_to_sdl_axis) {
const auto& binding = SDL_GameControllerGetBindForAxis(controller, sdl_axis);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding));
}
return mapping;
}
ButtonMapping SDLDriver::GetDualControllerMapping(const std::shared_ptr<SDLJoystick>& joystick,
const std::shared_ptr<SDLJoystick>& joystick2,
const ButtonBindings& switch_to_sdl_button,
const ZButtonBindings& switch_to_sdl_axis) const {
ButtonMapping mapping;
mapping.reserve(switch_to_sdl_button.size() + switch_to_sdl_axis.size());
auto* controller = joystick->GetSDLGameController();
auto* controller2 = joystick2->GetSDLGameController();
for (const auto& [switch_button, sdl_button] : switch_to_sdl_button) {
if (IsButtonOnLeftSide(switch_button)) {
const auto& binding = SDL_GameControllerGetBindForButton(controller2, sdl_button);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick2->GetPort(), joystick2->GetGUID(), binding));
continue;
}
const auto& binding = SDL_GameControllerGetBindForButton(controller, sdl_button);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding));
}
for (const auto& [switch_button, sdl_axis] : switch_to_sdl_axis) {
if (IsButtonOnLeftSide(switch_button)) {
const auto& binding = SDL_GameControllerGetBindForAxis(controller2, sdl_axis);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick2->GetPort(), joystick2->GetGUID(), binding));
continue;
}
const auto& binding = SDL_GameControllerGetBindForAxis(controller, sdl_axis);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding));
}
return mapping;
}
bool SDLDriver::IsButtonOnLeftSide(Settings::NativeButton::Values button) const {
switch (button) {
case Settings::NativeButton::DDown:
case Settings::NativeButton::DLeft:
case Settings::NativeButton::DRight:
case Settings::NativeButton::DUp:
case Settings::NativeButton::L:
case Settings::NativeButton::LStick:
case Settings::NativeButton::Minus:
case Settings::NativeButton::Screenshot:
case Settings::NativeButton::ZL:
return true;
default:
return false;
}
}
AnalogMapping SDLDriver::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port")) {
return {};
}
const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0));
const auto joystick2 = GetSDLJoystickByGUID(params.Get("guid2", ""), params.Get("port", 0));
auto* controller = joystick->GetSDLGameController();
if (controller == nullptr) {
return {};
}
AnalogMapping mapping = {};
const auto& binding_left_x =
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTX);
const auto& binding_left_y =
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTY);
if (params.Has("guid2")) {
const auto identifier = joystick2->GetPadIdentifier();
PreSetController(identifier);
PreSetAxis(identifier, binding_left_x.value.axis);
PreSetAxis(identifier, binding_left_y.value.axis);
const auto left_offset_x = -GetAxis(identifier, binding_left_x.value.axis);
const auto left_offset_y = -GetAxis(identifier, binding_left_y.value.axis);
mapping.insert_or_assign(Settings::NativeAnalog::LStick,
BuildParamPackageForAnalog(identifier, binding_left_x.value.axis,
binding_left_y.value.axis,
left_offset_x, left_offset_y));
} else {
const auto identifier = joystick->GetPadIdentifier();
PreSetController(identifier);
PreSetAxis(identifier, binding_left_x.value.axis);
PreSetAxis(identifier, binding_left_y.value.axis);
const auto left_offset_x = -GetAxis(identifier, binding_left_x.value.axis);
const auto left_offset_y = -GetAxis(identifier, binding_left_y.value.axis);
mapping.insert_or_assign(Settings::NativeAnalog::LStick,
BuildParamPackageForAnalog(identifier, binding_left_x.value.axis,
binding_left_y.value.axis,
left_offset_x, left_offset_y));
}
const auto& binding_right_x =
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTX);
const auto& binding_right_y =
SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTY);
const auto identifier = joystick->GetPadIdentifier();
PreSetController(identifier);
PreSetAxis(identifier, binding_right_x.value.axis);
PreSetAxis(identifier, binding_right_y.value.axis);
const auto right_offset_x = -GetAxis(identifier, binding_right_x.value.axis);
const auto right_offset_y = -GetAxis(identifier, binding_right_y.value.axis);
mapping.insert_or_assign(Settings::NativeAnalog::RStick,
BuildParamPackageForAnalog(identifier, binding_right_x.value.axis,
binding_right_y.value.axis, right_offset_x,
right_offset_y));
return mapping;
}
MotionMapping SDLDriver::GetMotionMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port")) {
return {};
}
const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0));
const auto joystick2 = GetSDLJoystickByGUID(params.Get("guid2", ""), params.Get("port", 0));
auto* controller = joystick->GetSDLGameController();
if (controller == nullptr) {
return {};
}
MotionMapping mapping = {};
joystick->EnableMotion();
if (joystick->HasGyro() || joystick->HasAccel()) {
mapping.insert_or_assign(Settings::NativeMotion::MotionRight,
BuildMotionParam(joystick->GetPort(), joystick->GetGUID()));
}
if (params.Has("guid2")) {
joystick2->EnableMotion();
if (joystick2->HasGyro() || joystick2->HasAccel()) {
mapping.insert_or_assign(Settings::NativeMotion::MotionLeft,
BuildMotionParam(joystick2->GetPort(), joystick2->GetGUID()));
}
} else {
if (joystick->HasGyro() || joystick->HasAccel()) {
mapping.insert_or_assign(Settings::NativeMotion::MotionLeft,
BuildMotionParam(joystick->GetPort(), joystick->GetGUID()));
}
}
return mapping;
}
std::string SDLDriver::GetUIName(const Common::ParamPackage& params) const {
if (params.Has("button")) {
// TODO(German77): Find how to substitue the values for real button names
return fmt::format("Button {}", params.Get("button", 0));
}
if (params.Has("hat")) {
return fmt::format("Hat {}", params.Get("direction", ""));
}
if (params.Has("axis")) {
return fmt::format("Axis {}", params.Get("axis", ""));
}
if (params.Has("axis_x") && params.Has("axis_y") && params.Has("axis_z")) {
return fmt::format("Axis {},{},{}", params.Get("axis_x", ""), params.Get("axis_y", ""),
params.Get("axis_z", ""));
}
if (params.Has("motion")) {
return "SDL motion";
}
return "Bad SDL";
}
std::string SDLDriver::GetHatButtonName(u8 direction_value) const {
switch (direction_value) {
case SDL_HAT_UP:
return "up";
case SDL_HAT_DOWN:
return "down";
case SDL_HAT_LEFT:
return "left";
case SDL_HAT_RIGHT:
return "right";
default:
return {};
}
}
u8 SDLDriver::GetHatButtonId(const std::string direction_name) const {
Uint8 direction;
if (direction_name == "up") {
direction = SDL_HAT_UP;
} else if (direction_name == "down") {
direction = SDL_HAT_DOWN;
} else if (direction_name == "left") {
direction = SDL_HAT_LEFT;
} else if (direction_name == "right") {
direction = SDL_HAT_RIGHT;
} else {
direction = 0;
}
return direction;
}
} // namespace InputCommon

View File

@@ -0,0 +1,117 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <atomic>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <SDL.h>
#include "common/common_types.h"
#include "input_common/input_engine.h"
union SDL_Event;
using SDL_GameController = struct _SDL_GameController;
using SDL_Joystick = struct _SDL_Joystick;
using SDL_JoystickID = s32;
using ButtonBindings =
std::array<std::pair<Settings::NativeButton::Values, SDL_GameControllerButton>, 17>;
using ZButtonBindings =
std::array<std::pair<Settings::NativeButton::Values, SDL_GameControllerAxis>, 2>;
namespace InputCommon {
class SDLJoystick;
class SDLDriver : public InputCommon::InputEngine {
public:
/// Initializes and registers SDL device factories
SDLDriver(const std::string& input_engine_);
/// Unregisters SDL device factories and shut them down.
~SDLDriver() override;
/// Handle SDL_Events for joysticks from SDL_PollEvent
void HandleGameControllerEvent(const SDL_Event& event);
/// Get the nth joystick with the corresponding GUID
std::shared_ptr<SDLJoystick> GetSDLJoystickBySDLID(SDL_JoystickID sdl_id);
/**
* Check how many identical joysticks (by guid) were connected before the one with sdl_id and so
* tie it to a SDLJoystick with the same guid and that port
*/
std::shared_ptr<SDLJoystick> GetSDLJoystickByGUID(const std::string& guid, int port);
std::vector<Common::ParamPackage> GetInputDevices() const override;
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& params) override;
std::string GetUIName(const Common::ParamPackage& params) const override;
std::string GetHatButtonName(u8 direction_value) const override;
u8 GetHatButtonId(const std::string direction_name) const override;
Common::Input::VibrationError SetRumble(
const PadIdentifier& identifier, const Common::Input::VibrationStatus vibration) override;
private:
void InitJoystick(int joystick_index);
void CloseJoystick(SDL_Joystick* sdl_joystick);
/// Needs to be called before SDL_QuitSubSystem.
void CloseJoysticks();
Common::ParamPackage BuildAnalogParamPackageForButton(int port, std::string guid, s32 axis,
float value = 0.1f) const;
Common::ParamPackage BuildButtonParamPackageForButton(int port, std::string guid,
s32 button) const;
Common::ParamPackage BuildHatParamPackageForButton(int port, std::string guid, s32 hat,
u8 value) const;
Common::ParamPackage BuildMotionParam(int port, std::string guid) const;
Common::ParamPackage BuildParamPackageForBinding(
int port, const std::string& guid, const SDL_GameControllerButtonBind& binding) const;
Common::ParamPackage BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
int axis_y, float offset_x,
float offset_y) const;
/// Returns the default button bindings list for generic controllers
ButtonBindings GetDefaultButtonBinding() const;
/// Returns the default button bindings list for nintendo controllers
ButtonBindings GetNintendoButtonBinding(const std::shared_ptr<SDLJoystick>& joystick) const;
/// Returns the button mappings from a single controller
ButtonMapping GetSingleControllerMapping(const std::shared_ptr<SDLJoystick>& joystick,
const ButtonBindings& switch_to_sdl_button,
const ZButtonBindings& switch_to_sdl_axis) const;
/// Returns the button mappings from two different controllers
ButtonMapping GetDualControllerMapping(const std::shared_ptr<SDLJoystick>& joystick,
const std::shared_ptr<SDLJoystick>& joystick2,
const ButtonBindings& switch_to_sdl_button,
const ZButtonBindings& switch_to_sdl_axis) const;
/// Returns true if the button is on the left joycon
bool IsButtonOnLeftSide(Settings::NativeButton::Values button) const;
/// Map of GUID of a list of corresponding virtual Joysticks
std::unordered_map<std::string, std::vector<std::shared_ptr<SDLJoystick>>> joystick_map;
std::mutex joystick_map_mutex;
bool start_thread = false;
std::atomic<bool> initialized = false;
std::thread poll_thread;
};
} // namespace InputCommon

View File

@@ -0,0 +1,321 @@
// Copyright 2021 yuzu Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include <cstring>
#include <regex>
#include <fmt/format.h>
#include "common/fs/file.h"
#include "common/fs/fs_types.h"
#include "common/fs/path_util.h"
#include "common/logging/log.h"
#include "common/settings.h"
#include "input_common/drivers/tas_input.h"
namespace InputCommon::TasInput {
enum TasAxes : u8 {
StickX,
StickY,
SubstickX,
SubstickY,
Undefined,
};
// Supported keywords and buttons from a TAS file
constexpr std::array<std::pair<std::string_view, TasButton>, 20> text_to_tas_button = {
std::pair{"KEY_A", TasButton::BUTTON_A},
{"KEY_B", TasButton::BUTTON_B},
{"KEY_X", TasButton::BUTTON_X},
{"KEY_Y", TasButton::BUTTON_Y},
{"KEY_LSTICK", TasButton::STICK_L},
{"KEY_RSTICK", TasButton::STICK_R},
{"KEY_L", TasButton::TRIGGER_L},
{"KEY_R", TasButton::TRIGGER_R},
{"KEY_PLUS", TasButton::BUTTON_PLUS},
{"KEY_MINUS", TasButton::BUTTON_MINUS},
{"KEY_DLEFT", TasButton::BUTTON_LEFT},
{"KEY_DUP", TasButton::BUTTON_UP},
{"KEY_DRIGHT", TasButton::BUTTON_RIGHT},
{"KEY_DDOWN", TasButton::BUTTON_DOWN},
{"KEY_SL", TasButton::BUTTON_SL},
{"KEY_SR", TasButton::BUTTON_SR},
{"KEY_CAPTURE", TasButton::BUTTON_CAPTURE},
{"KEY_HOME", TasButton::BUTTON_HOME},
{"KEY_ZL", TasButton::TRIGGER_ZL},
{"KEY_ZR", TasButton::TRIGGER_ZR},
};
Tas::Tas(const std::string input_engine_) : InputCommon::InputEngine(input_engine_) {
for (size_t player_index = 0; player_index < PLAYER_NUMBER; player_index++) {
PadIdentifier identifier{
.guid = Common::UUID{},
.port = player_index,
.pad = 0,
};
PreSetController(identifier);
}
ClearInput();
if (!Settings::values.tas_enable) {
needs_reset = true;
return;
}
LoadTasFiles();
}
Tas::~Tas() {
Stop();
};
void Tas::LoadTasFiles() {
script_length = 0;
for (size_t i = 0; i < commands.size(); i++) {
LoadTasFile(i);
if (commands[i].size() > script_length) {
script_length = commands[i].size();
}
}
}
void Tas::LoadTasFile(size_t player_index) {
if (!commands[player_index].empty()) {
commands[player_index].clear();
}
std::string file =
Common::FS::ReadStringFromFile(Common::FS::GetYuzuPath(Common::FS::YuzuPath::TASDir) /
fmt::format("script0-{}.txt", player_index + 1),
Common::FS::FileType::BinaryFile);
std::stringstream command_line(file);
std::string line;
int frame_no = 0;
while (std::getline(command_line, line, '\n')) {
if (line.empty()) {
continue;
}
std::smatch m;
std::stringstream linestream(line);
std::string segment;
std::vector<std::string> seglist;
while (std::getline(linestream, segment, ' ')) {
seglist.push_back(segment);
}
if (seglist.size() < 4) {
continue;
}
while (frame_no < std::stoi(seglist.at(0))) {
commands[player_index].push_back({});
frame_no++;
}
TASCommand command = {
.buttons = ReadCommandButtons(seglist.at(1)),
.l_axis = ReadCommandAxis(seglist.at(2)),
.r_axis = ReadCommandAxis(seglist.at(3)),
};
commands[player_index].push_back(command);
frame_no++;
}
LOG_INFO(Input, "TAS file loaded! {} frames", frame_no);
}
void Tas::WriteTasFile(std::u8string file_name) {
std::string output_text;
for (size_t frame = 0; frame < record_commands.size(); frame++) {
const TASCommand& line = record_commands[frame];
output_text += fmt::format("{} {} {} {}\n", frame, WriteCommandButtons(line.buttons),
WriteCommandAxis(line.l_axis), WriteCommandAxis(line.r_axis));
}
const auto bytes_written = Common::FS::WriteStringToFile(
Common::FS::GetYuzuPath(Common::FS::YuzuPath::TASDir) / file_name,
Common::FS::FileType::TextFile, output_text);
if (bytes_written == output_text.size()) {
LOG_INFO(Input, "TAS file written to file!");
} else {
LOG_ERROR(Input, "Writing the TAS-file has failed! {} / {} bytes written", bytes_written,
output_text.size());
}
}
void Tas::RecordInput(u64 buttons, TasAnalog left_axis, TasAnalog right_axis) {
last_input = {
.buttons = buttons,
.l_axis = FlipAxisY(left_axis),
.r_axis = FlipAxisY(right_axis),
};
}
TasAnalog Tas::FlipAxisY(TasAnalog old) {
return {
.x = old.x,
.y = -old.y,
};
}
std::tuple<TasState, size_t, size_t> Tas::GetStatus() const {
TasState state;
if (is_recording) {
return {TasState::Recording, 0, record_commands.size()};
}
if (is_running) {
state = TasState::Running;
} else {
state = TasState::Stopped;
}
return {state, current_command, script_length};
}
void Tas::UpdateThread() {
if (!Settings::values.tas_enable) {
if (is_running) {
Stop();
}
return;
}
if (is_recording) {
record_commands.push_back(last_input);
}
if (needs_reset) {
current_command = 0;
needs_reset = false;
LoadTasFiles();
LOG_DEBUG(Input, "tas_reset done");
}
if (!is_running) {
ClearInput();
return;
}
if (current_command < script_length) {
LOG_DEBUG(Input, "Playing TAS {}/{}", current_command, script_length);
const size_t frame = current_command++;
for (size_t player_index = 0; player_index < commands.size(); player_index++) {
TASCommand command{};
if (frame < commands[player_index].size()) {
command = commands[player_index][frame];
}
PadIdentifier identifier{
.guid = Common::UUID{},
.port = player_index,
.pad = 0,
};
for (std::size_t i = 0; i < sizeof(command.buttons) * 8; ++i) {
const bool button_status = (command.buttons & (1LLU << i)) != 0;
const int button = static_cast<int>(i);
SetButton(identifier, button, button_status);
}
SetAxis(identifier, TasAxes::StickX, command.l_axis.x);
SetAxis(identifier, TasAxes::StickY, command.l_axis.y);
SetAxis(identifier, TasAxes::SubstickX, command.r_axis.x);
SetAxis(identifier, TasAxes::SubstickY, command.r_axis.y);
}
} else {
is_running = Settings::values.tas_loop.GetValue();
current_command = 0;
ClearInput();
}
}
void Tas::ClearInput() {
ResetButtonState();
ResetAnalogState();
}
TasAnalog Tas::ReadCommandAxis(const std::string& line) const {
std::stringstream linestream(line);
std::string segment;
std::vector<std::string> seglist;
while (std::getline(linestream, segment, ';')) {
seglist.push_back(segment);
}
const float x = std::stof(seglist.at(0)) / 32767.0f;
const float y = std::stof(seglist.at(1)) / 32767.0f;
return {x, y};
}
u64 Tas::ReadCommandButtons(const std::string& data) const {
std::stringstream button_text(data);
std::string line;
u64 buttons = 0;
while (std::getline(button_text, line, ';')) {
for (auto [text, tas_button] : text_to_tas_button) {
if (text == line) {
buttons |= static_cast<u64>(tas_button);
break;
}
}
}
return buttons;
}
std::string Tas::WriteCommandButtons(u64 buttons) const {
std::string returns = "";
for (auto [text_button, tas_button] : text_to_tas_button) {
if ((buttons & static_cast<u64>(tas_button)) != 0) {
returns += fmt::format("{};", text_button);
}
}
return returns.empty() ? "NONE" : returns;
}
std::string Tas::WriteCommandAxis(TasAnalog analog) const {
return fmt::format("{};{}", analog.x * 32767, analog.y * 32767);
}
void Tas::StartStop() {
if (!Settings::values.tas_enable) {
return;
}
if (is_running) {
Stop();
} else {
is_running = true;
}
}
void Tas::Stop() {
is_running = false;
}
void Tas::Reset() {
if (!Settings::values.tas_enable) {
return;
}
needs_reset = true;
}
bool Tas::Record() {
if (!Settings::values.tas_enable) {
return true;
}
is_recording = !is_recording;
return is_recording;
}
void Tas::SaveRecording(bool overwrite_file) {
if (is_recording) {
return;
}
if (record_commands.empty()) {
return;
}
WriteTasFile(u8"record.txt");
if (overwrite_file) {
WriteTasFile(u8"script0-1.txt");
}
needs_reset = true;
record_commands.clear();
}
} // namespace InputCommon::TasInput

View File

@@ -0,0 +1,199 @@
// Copyright 2020 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <array>
#include "common/common_types.h"
#include "common/settings_input.h"
#include "input_common/input_engine.h"
#include "input_common/main.h"
/*
To play back TAS scripts on Yuzu, select the folder with scripts in the configuration menu below
Tools -> Configure TAS. The file itself has normal text format and has to be called script0-1.txt
for controller 1, script0-2.txt for controller 2 and so forth (with max. 8 players).
A script file has the same format as TAS-nx uses, so final files will look like this:
1 KEY_B 0;0 0;0
6 KEY_ZL 0;0 0;0
41 KEY_ZL;KEY_Y 0;0 0;0
43 KEY_X;KEY_A 32767;0 0;0
44 KEY_A 32767;0 0;0
45 KEY_A 32767;0 0;0
46 KEY_A 32767;0 0;0
47 KEY_A 32767;0 0;0
After placing the file at the correct location, it can be read into Yuzu with the (default) hotkey
CTRL+F6 (refresh). In the bottom left corner, it will display the amount of frames the script file
has. Playback can be started or stopped using CTRL+F5.
However, for playback to actually work, the correct input device has to be selected: In the Controls
menu, select TAS from the device list for the controller that the script should be played on.
Recording a new script file is really simple: Just make sure that the proper device (not TAS) is
connected on P1, and press CTRL+F7 to start recording. When done, just press the same keystroke
again (CTRL+F7). The new script will be saved at the location previously selected, as the filename
record.txt.
For debugging purposes, the common controller debugger can be used (View -> Debugging -> Controller
P1).
*/
namespace InputCommon::TasInput {
constexpr size_t PLAYER_NUMBER = 10;
enum class TasButton : u64 {
BUTTON_A = 1U << 0,
BUTTON_B = 1U << 1,
BUTTON_X = 1U << 2,
BUTTON_Y = 1U << 3,
STICK_L = 1U << 4,
STICK_R = 1U << 5,
TRIGGER_L = 1U << 6,
TRIGGER_R = 1U << 7,
TRIGGER_ZL = 1U << 8,
TRIGGER_ZR = 1U << 9,
BUTTON_PLUS = 1U << 10,
BUTTON_MINUS = 1U << 11,
BUTTON_LEFT = 1U << 12,
BUTTON_UP = 1U << 13,
BUTTON_RIGHT = 1U << 14,
BUTTON_DOWN = 1U << 15,
BUTTON_SL = 1U << 16,
BUTTON_SR = 1U << 17,
BUTTON_HOME = 1U << 18,
BUTTON_CAPTURE = 1U << 19,
};
struct TasAnalog {
float x{};
float y{};
};
enum class TasState {
Running,
Recording,
Stopped,
};
class Tas final : public InputCommon::InputEngine {
public:
explicit Tas(const std::string input_engine_);
~Tas();
/**
* Changes the input status that will be stored in each frame
* @param buttons: bitfield with the status of the buttons
* @param left_axis: value of the left axis
* @param right_axis: value of the right axis
*/
void RecordInput(u64 buttons, TasAnalog left_axis, TasAnalog right_axis);
// Main loop that records or executes input
void UpdateThread();
// Sets the flag to start or stop the TAS command excecution and swaps controllers profiles
void StartStop();
// Stop the TAS and reverts any controller profile
void Stop();
// Sets the flag to reload the file and start from the begining in the next update
void Reset();
/**
* Sets the flag to enable or disable recording of inputs
* @return Returns true if the current recording status is enabled
*/
bool Record();
/**
* Saves contents of record_commands on a file
* @param overwrite_file: Indicates if player 1 should be overwritten
*/
void SaveRecording(bool overwrite_file);
/**
* Returns the current status values of TAS playback/recording
* @return Tuple of
* TasState indicating the current state out of Running ;
* Current playback progress ;
* Total length of script file currently loaded or being recorded
*/
std::tuple<TasState, size_t, size_t> GetStatus() const;
private:
struct TASCommand {
u64 buttons{};
TasAnalog l_axis{};
TasAnalog r_axis{};
};
/// Loads TAS files from all players
void LoadTasFiles();
/** Loads TAS file from the specified player
* @param player_index: player number where data is going to be stored
*/
void LoadTasFile(size_t player_index);
/** Writes a TAS file from the recorded commands
* @param file_name: name of the file to be written
*/
void WriteTasFile(std::u8string file_name);
/** Inverts the Y axis polarity
* @param old: value of the axis
* @return new value of the axis
*/
TasAnalog FlipAxisY(TasAnalog old);
/**
* Parses a string containing the axis values. X and Y have a range from -32767 to 32767
* @param line: string containing axis values with the following format "x;y"
* @return Returns a TAS analog object with axis values with range from -1.0 to 1.0
*/
TasAnalog ReadCommandAxis(const std::string& line) const;
/**
* Parses a string containing the button values. Each button is represented by it's text format
* specified in text_to_tas_button array
* @param line: string containing button name with the following format "a;b;c;d..."
* @return Returns a u64 with each bit representing the status of a button
*/
u64 ReadCommandButtons(const std::string& line) const;
/**
* Reset state of all players
*/
void ClearInput();
/**
* Converts an u64 containing the button status into the text equivalent
* @param buttons: bitfield with the status of the buttons
* @return Returns a string with the name of the buttons to be written to the file
*/
std::string WriteCommandButtons(u64 buttons) const;
/**
* Converts an TAS analog object containing the axis status into the text equivalent
* @param data: value of the axis
* @return A string with the value of the axis to be written to the file
*/
std::string WriteCommandAxis(TasAnalog data) const;
size_t script_length{0};
bool is_recording{false};
bool is_running{false};
bool needs_reset{false};
std::array<std::vector<TASCommand>, PLAYER_NUMBER> commands{};
std::vector<TASCommand> record_commands{};
size_t current_command{0};
TASCommand last_input{}; // only used for recording
};
} // namespace InputCommon::TasInput

View File

@@ -0,0 +1,53 @@
// Copyright 2021 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included
#include "common/param_package.h"
#include "input_common/drivers/touch_screen.h"
namespace InputCommon {
constexpr PadIdentifier identifier = {
.guid = Common::UUID{Common::INVALID_UUID},
.port = 0,
.pad = 0,
};
TouchScreen::TouchScreen(const std::string input_engine_) : InputEngine(input_engine_) {
PreSetController(identifier);
}
void TouchScreen::TouchMoved(float x, float y, std::size_t finger) {
if (finger >= 16) {
return;
}
TouchPressed(x, y, finger);
}
void TouchScreen::TouchPressed(float x, float y, std::size_t finger) {
if (finger >= 16) {
return;
}
SetButton(identifier, static_cast<int>(finger), true);
SetAxis(identifier, static_cast<int>(finger * 2), x);
SetAxis(identifier, static_cast<int>(finger * 2 + 1), y);
}
void TouchScreen::TouchReleased(std::size_t finger) {
if (finger >= 16) {
return;
}
SetButton(identifier, static_cast<int>(finger), false);
SetAxis(identifier, static_cast<int>(finger * 2), 0.0f);
SetAxis(identifier, static_cast<int>(finger * 2 + 1), 0.0f);
}
void TouchScreen::ReleaseAllTouch() {
for (int index = 0; index < 16; ++index) {
SetButton(identifier, index, false);
SetAxis(identifier, index * 2, 0.0f);
SetAxis(identifier, index * 2 + 1, 0.0f);
}
}
} // namespace InputCommon

View File

@@ -0,0 +1,44 @@
// Copyright 2021 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included
#pragma once
#include "input_common/input_engine.h"
namespace InputCommon {
/**
* A button device factory representing a keyboard. It receives keyboard events and forward them
* to all button devices it created.
*/
class TouchScreen final : public InputCommon::InputEngine {
public:
explicit TouchScreen(const std::string input_engine_);
/**
* Signals that mouse has moved.
* @param x the x-coordinate of the cursor
* @param y the y-coordinate of the cursor
* @param center_x the x-coordinate of the middle of the screen
* @param center_y the y-coordinate of the middle of the screen
*/
void TouchMoved(float x, float y, std::size_t finger);
/**
* Sets the status of all buttons bound with the key to pressed
* @param key_code the code of the key to press
*/
void TouchPressed(float x, float y, std::size_t finger);
/**
* Sets the status of all buttons bound with the key to released
* @param key_code the code of the key to release
*/
void TouchReleased(std::size_t finger);
/// Resets all inputs to their initial value
void ReleaseAllTouch();
};
} // namespace InputCommon

View File

@@ -0,0 +1,401 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <random>
#include <boost/asio.hpp>
#include <fmt/format.h>
#include "common/logging/log.h"
#include "common/param_package.h"
#include "common/settings.h"
#include "input_common/drivers/udp_client.h"
#include "input_common/helpers/udp_protocol.h"
using boost::asio::ip::udp;
namespace InputCommon::CemuhookUDP {
struct SocketCallback {
std::function<void(Response::Version)> version;
std::function<void(Response::PortInfo)> port_info;
std::function<void(Response::PadData)> pad_data;
};
class Socket {
public:
using clock = std::chrono::system_clock;
explicit Socket(const std::string& host, u16 port, SocketCallback callback_)
: callback(std::move(callback_)), timer(io_service),
socket(io_service, udp::endpoint(udp::v4(), 0)), client_id(GenerateRandomClientId()) {
boost::system::error_code ec{};
auto ipv4 = boost::asio::ip::make_address_v4(host, ec);
if (ec.value() != boost::system::errc::success) {
LOG_ERROR(Input, "Invalid IPv4 address \"{}\" provided to socket", host);
ipv4 = boost::asio::ip::address_v4{};
}
send_endpoint = {udp::endpoint(ipv4, port)};
}
void Stop() {
io_service.stop();
}
void Loop() {
io_service.run();
}
void StartSend(const clock::time_point& from) {
timer.expires_at(from + std::chrono::seconds(3));
timer.async_wait([this](const boost::system::error_code& error) { HandleSend(error); });
}
void StartReceive() {
socket.async_receive_from(
boost::asio::buffer(receive_buffer), receive_endpoint,
[this](const boost::system::error_code& error, std::size_t bytes_transferred) {
HandleReceive(error, bytes_transferred);
});
}
private:
u32 GenerateRandomClientId() const {
std::random_device device;
return device();
}
void HandleReceive(const boost::system::error_code&, std::size_t bytes_transferred) {
if (auto type = Response::Validate(receive_buffer.data(), bytes_transferred)) {
switch (*type) {
case Type::Version: {
Response::Version version;
std::memcpy(&version, &receive_buffer[sizeof(Header)], sizeof(Response::Version));
callback.version(std::move(version));
break;
}
case Type::PortInfo: {
Response::PortInfo port_info;
std::memcpy(&port_info, &receive_buffer[sizeof(Header)],
sizeof(Response::PortInfo));
callback.port_info(std::move(port_info));
break;
}
case Type::PadData: {
Response::PadData pad_data;
std::memcpy(&pad_data, &receive_buffer[sizeof(Header)], sizeof(Response::PadData));
callback.pad_data(std::move(pad_data));
break;
}
}
}
StartReceive();
}
void HandleSend(const boost::system::error_code&) {
boost::system::error_code _ignored{};
// Send a request for getting port info for the pad
const Request::PortInfo port_info{4, {0, 1, 2, 3}};
const auto port_message = Request::Create(port_info, client_id);
std::memcpy(&send_buffer1, &port_message, PORT_INFO_SIZE);
socket.send_to(boost::asio::buffer(send_buffer1), send_endpoint, {}, _ignored);
// Send a request for getting pad data for the pad
const Request::PadData pad_data{
Request::PadData::Flags::AllPorts,
0,
EMPTY_MAC_ADDRESS,
};
const auto pad_message = Request::Create(pad_data, client_id);
std::memcpy(send_buffer2.data(), &pad_message, PAD_DATA_SIZE);
socket.send_to(boost::asio::buffer(send_buffer2), send_endpoint, {}, _ignored);
StartSend(timer.expiry());
}
SocketCallback callback;
boost::asio::io_service io_service;
boost::asio::basic_waitable_timer<clock> timer;
udp::socket socket;
const u32 client_id;
static constexpr std::size_t PORT_INFO_SIZE = sizeof(Message<Request::PortInfo>);
static constexpr std::size_t PAD_DATA_SIZE = sizeof(Message<Request::PadData>);
std::array<u8, PORT_INFO_SIZE> send_buffer1;
std::array<u8, PAD_DATA_SIZE> send_buffer2;
udp::endpoint send_endpoint;
std::array<u8, MAX_PACKET_SIZE> receive_buffer;
udp::endpoint receive_endpoint;
};
static void SocketLoop(Socket* socket) {
socket->StartReceive();
socket->StartSend(Socket::clock::now());
socket->Loop();
}
UDPClient::UDPClient(const std::string& input_engine_) : InputEngine(input_engine_) {
LOG_INFO(Input, "Udp Initialization started");
ReloadSockets();
}
UDPClient::~UDPClient() {
Reset();
}
UDPClient::ClientConnection::ClientConnection() = default;
UDPClient::ClientConnection::~ClientConnection() = default;
void UDPClient::ReloadSockets() {
Reset();
std::stringstream servers_ss(Settings::values.udp_input_servers.GetValue());
std::string server_token;
std::size_t client = 0;
while (std::getline(servers_ss, server_token, ',')) {
if (client == MAX_UDP_CLIENTS) {
break;
}
std::stringstream server_ss(server_token);
std::string token;
std::getline(server_ss, token, ':');
std::string udp_input_address = token;
std::getline(server_ss, token, ':');
char* temp;
const u16 udp_input_port = static_cast<u16>(std::strtol(token.c_str(), &temp, 0));
if (*temp != '\0') {
LOG_ERROR(Input, "Port number is not valid {}", token);
continue;
}
const std::size_t client_number = GetClientNumber(udp_input_address, udp_input_port);
if (client_number != MAX_UDP_CLIENTS) {
LOG_ERROR(Input, "Duplicated UDP servers found");
continue;
}
StartCommunication(client++, udp_input_address, udp_input_port);
}
}
std::size_t UDPClient::GetClientNumber(std::string_view host, u16 port) const {
for (std::size_t client = 0; client < clients.size(); client++) {
if (clients[client].active == -1) {
continue;
}
if (clients[client].host == host && clients[client].port == port) {
return client;
}
}
return MAX_UDP_CLIENTS;
}
void UDPClient::OnVersion([[maybe_unused]] Response::Version data) {
LOG_TRACE(Input, "Version packet received: {}", data.version);
}
void UDPClient::OnPortInfo([[maybe_unused]] Response::PortInfo data) {
LOG_TRACE(Input, "PortInfo packet received: {}", data.model);
}
void UDPClient::OnPadData(Response::PadData data, std::size_t client) {
const std::size_t pad_index = (client * PADS_PER_CLIENT) + data.info.id;
if (pad_index >= pads.size()) {
LOG_ERROR(Input, "Invalid pad id {}", data.info.id);
return;
}
LOG_TRACE(Input, "PadData packet received");
if (data.packet_counter == pads[pad_index].packet_sequence) {
LOG_WARNING(
Input,
"PadData packet dropped because its stale info. Current count: {} Packet count: {}",
pads[pad_index].packet_sequence, data.packet_counter);
pads[pad_index].connected = false;
return;
}
clients[client].active = 1;
pads[pad_index].connected = true;
pads[pad_index].packet_sequence = data.packet_counter;
const auto now = std::chrono::steady_clock::now();
const auto time_difference = static_cast<u64>(
std::chrono::duration_cast<std::chrono::microseconds>(now - pads[pad_index].last_update)
.count());
pads[pad_index].last_update = now;
// Gyroscope values are not it the correct scale from better joy.
// Dividing by 312 allows us to make one full turn = 1 turn
// This must be a configurable valued called sensitivity
const float gyro_scale = 1.0f / 312.0f;
const BasicMotion motion{
.gyro_x = data.gyro.pitch * gyro_scale,
.gyro_y = data.gyro.roll * gyro_scale,
.gyro_z = -data.gyro.yaw * gyro_scale,
.accel_x = data.accel.x,
.accel_y = -data.accel.z,
.accel_z = data.accel.y,
.delta_timestamp = time_difference,
};
const PadIdentifier identifier = GetPadIdentifier(pad_index);
SetMotion(identifier, 0, motion);
for (std::size_t id = 0; id < data.touch.size(); ++id) {
const auto touch_pad = data.touch[id];
const int touch_id = static_cast<int>(client * 2 + id);
// TODO: Use custom calibration per device
const Common::ParamPackage touch_param(Settings::values.touch_device.GetValue());
const u16 min_x = static_cast<u16>(touch_param.Get("min_x", 100));
const u16 min_y = static_cast<u16>(touch_param.Get("min_y", 50));
const u16 max_x = static_cast<u16>(touch_param.Get("max_x", 1800));
const u16 max_y = static_cast<u16>(touch_param.Get("max_y", 850));
const f32 x =
static_cast<f32>(std::clamp(static_cast<u16>(touch_pad.x), min_x, max_x) - min_x) /
static_cast<f32>(max_x - min_x);
const f32 y =
static_cast<f32>(std::clamp(static_cast<u16>(touch_pad.y), min_y, max_y) - min_y) /
static_cast<f32>(max_y - min_y);
if (touch_pad.is_active) {
SetAxis(identifier, touch_id * 2, x);
SetAxis(identifier, touch_id * 2 + 1, y);
SetButton(identifier, touch_id, true);
continue;
}
SetAxis(identifier, touch_id * 2, 0);
SetAxis(identifier, touch_id * 2 + 1, 0);
SetButton(identifier, touch_id, false);
}
}
void UDPClient::StartCommunication(std::size_t client, const std::string& host, u16 port) {
SocketCallback callback{[this](Response::Version version) { OnVersion(version); },
[this](Response::PortInfo info) { OnPortInfo(info); },
[this, client](Response::PadData data) { OnPadData(data, client); }};
LOG_INFO(Input, "Starting communication with UDP input server on {}:{}", host, port);
clients[client].uuid = GetHostUUID(host);
clients[client].host = host;
clients[client].port = port;
clients[client].active = 0;
clients[client].socket = std::make_unique<Socket>(host, port, callback);
clients[client].thread = std::thread{SocketLoop, clients[client].socket.get()};
for (std::size_t index = 0; index < PADS_PER_CLIENT; ++index) {
const PadIdentifier identifier = GetPadIdentifier(client * PADS_PER_CLIENT + index);
PreSetController(identifier);
}
}
const PadIdentifier UDPClient::GetPadIdentifier(std::size_t pad_index) const {
const std::size_t client = pad_index / PADS_PER_CLIENT;
return {
.guid = clients[client].uuid,
.port = static_cast<std::size_t>(clients[client].port),
.pad = pad_index,
};
}
const Common::UUID UDPClient::GetHostUUID(const std::string host) const {
const auto ip = boost::asio::ip::address_v4::from_string(host);
const auto hex_host = fmt::format("{:06x}", ip.to_ulong());
return Common::UUID{hex_host};
}
void UDPClient::Reset() {
for (auto& client : clients) {
if (client.thread.joinable()) {
client.active = -1;
client.socket->Stop();
client.thread.join();
}
}
}
void TestCommunication(const std::string& host, u16 port,
const std::function<void()>& success_callback,
const std::function<void()>& failure_callback) {
std::thread([=] {
Common::Event success_event;
SocketCallback callback{
.version = [](Response::Version) {},
.port_info = [](Response::PortInfo) {},
.pad_data = [&](Response::PadData) { success_event.Set(); },
};
Socket socket{host, port, std::move(callback)};
std::thread worker_thread{SocketLoop, &socket};
const bool result =
success_event.WaitUntil(std::chrono::steady_clock::now() + std::chrono::seconds(10));
socket.Stop();
worker_thread.join();
if (result) {
success_callback();
} else {
failure_callback();
}
}).detach();
}
CalibrationConfigurationJob::CalibrationConfigurationJob(
const std::string& host, u16 port, std::function<void(Status)> status_callback,
std::function<void(u16, u16, u16, u16)> data_callback) {
std::thread([=, this] {
Status current_status{Status::Initialized};
SocketCallback callback{
[](Response::Version) {}, [](Response::PortInfo) {},
[&](Response::PadData data) {
static constexpr u16 CALIBRATION_THRESHOLD = 100;
static constexpr u16 MAX_VALUE = UINT16_MAX;
if (current_status == Status::Initialized) {
// Receiving data means the communication is ready now
current_status = Status::Ready;
status_callback(current_status);
}
const auto& touchpad_0 = data.touch[0];
if (touchpad_0.is_active == 0) {
return;
}
LOG_DEBUG(Input, "Current touch: {} {}", touchpad_0.x, touchpad_0.y);
const u16 min_x = std::min(MAX_VALUE, static_cast<u16>(touchpad_0.x));
const u16 min_y = std::min(MAX_VALUE, static_cast<u16>(touchpad_0.y));
if (current_status == Status::Ready) {
// First touch - min data (min_x/min_y)
current_status = Status::Stage1Completed;
status_callback(current_status);
}
if (touchpad_0.x - min_x > CALIBRATION_THRESHOLD &&
touchpad_0.y - min_y > CALIBRATION_THRESHOLD) {
// Set the current position as max value and finishes configuration
const u16 max_x = touchpad_0.x;
const u16 max_y = touchpad_0.y;
current_status = Status::Completed;
data_callback(min_x, min_y, max_x, max_y);
status_callback(current_status);
complete_event.Set();
}
}};
Socket socket{host, port, std::move(callback)};
std::thread worker_thread{SocketLoop, &socket};
complete_event.Wait();
socket.Stop();
worker_thread.join();
}).detach();
}
CalibrationConfigurationJob::~CalibrationConfigurationJob() {
Stop();
}
void CalibrationConfigurationJob::Stop() {
complete_event.Set();
}
} // namespace InputCommon::CemuhookUDP

View File

@@ -0,0 +1,129 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <optional>
#include "common/common_types.h"
#include "common/thread.h"
#include "input_common/input_engine.h"
namespace InputCommon::CemuhookUDP {
class Socket;
namespace Response {
struct PadData;
struct PortInfo;
struct TouchPad;
struct Version;
} // namespace Response
enum class PadTouch {
Click,
Undefined,
};
struct UDPPadStatus {
std::string host{"127.0.0.1"};
u16 port{26760};
std::size_t pad_index{};
};
struct DeviceStatus {
std::mutex update_mutex;
// calibration data for scaling the device's touch area to 3ds
struct CalibrationData {
u16 min_x{};
u16 min_y{};
u16 max_x{};
u16 max_y{};
};
std::optional<CalibrationData> touch_calibration;
};
/**
* A button device factory representing a keyboard. It receives keyboard events and forward them
* to all button devices it created.
*/
class UDPClient final : public InputCommon::InputEngine {
public:
explicit UDPClient(const std::string& input_engine_);
~UDPClient();
void ReloadSockets();
private:
struct PadData {
std::size_t pad_index{};
bool connected{};
DeviceStatus status;
u64 packet_sequence{};
std::chrono::time_point<std::chrono::steady_clock> last_update;
};
struct ClientConnection {
ClientConnection();
~ClientConnection();
Common::UUID uuid{"7F000001"};
std::string host{"127.0.0.1"};
u16 port{26760};
s8 active{-1};
std::unique_ptr<Socket> socket;
std::thread thread;
};
// For shutting down, clear all data, join all threads, release usb
void Reset();
// Translates configuration to client number
std::size_t GetClientNumber(std::string_view host, u16 port) const;
void OnVersion(Response::Version);
void OnPortInfo(Response::PortInfo);
void OnPadData(Response::PadData, std::size_t client);
void StartCommunication(std::size_t client, const std::string& host, u16 port);
const PadIdentifier GetPadIdentifier(std::size_t pad_index) const;
const Common::UUID GetHostUUID(const std::string host) const;
// Allocate clients for 8 udp servers
static constexpr std::size_t MAX_UDP_CLIENTS = 8;
static constexpr std::size_t PADS_PER_CLIENT = 4;
std::array<PadData, MAX_UDP_CLIENTS * PADS_PER_CLIENT> pads{};
std::array<ClientConnection, MAX_UDP_CLIENTS> clients{};
};
/// An async job allowing configuration of the touchpad calibration.
class CalibrationConfigurationJob {
public:
enum class Status {
Initialized,
Ready,
Stage1Completed,
Completed,
};
/**
* Constructs and starts the job with the specified parameter.
*
* @param status_callback Callback for job status updates
* @param data_callback Called when calibration data is ready
*/
explicit CalibrationConfigurationJob(const std::string& host, u16 port,
std::function<void(Status)> status_callback,
std::function<void(u16, u16, u16, u16)> data_callback);
~CalibrationConfigurationJob();
void Stop();
private:
Common::Event complete_event;
};
void TestCommunication(const std::string& host, u16 port,
const std::function<void()>& success_callback,
const std::function<void()>& failure_callback);
} // namespace InputCommon::CemuhookUDP