JosJuice 7197e3abd0 Use structs for config callback IDs
This way you can't mix up regular config callback IDs and CPU thread
config callback IDs. (It would be rather bad if you did!)
2023-08-17 19:19:26 +02:00

671 lines
23 KiB
C++

// Copyright 2019 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "InputCommon/ControllerInterface/DualShockUDPClient/DualShockUDPClient.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <tuple>
#include <SFML/Network/SocketSelector.hpp>
#include <SFML/Network/UdpSocket.hpp>
#include <fmt/format.h>
#include "Common/Config/Config.h"
#include "Common/Flag.h"
#include "Common/Logging/Log.h"
#include "Common/MathUtil.h"
#include "Common/Random.h"
#include "Common/ScopeGuard.h"
#include "Common/StringUtil.h"
#include "Common/Thread.h"
#include "Core/CoreTiming.h"
#include "InputCommon/ControllerInterface/ControllerInterface.h"
#include "InputCommon/ControllerInterface/DualShockUDPClient/DualShockUDPProto.h"
namespace ciface::DualShockUDPClient
{
constexpr std::string_view DUALSHOCKUDP_SOURCE_NAME = "DSUClient";
namespace Settings
{
const Config::Info<std::string> SERVER_ADDRESS{
{Config::System::DualShockUDPClient, "Server", "IPAddress"}, ""};
const Config::Info<int> SERVER_PORT{{Config::System::DualShockUDPClient, "Server", "Port"}, 0};
const Config::Info<std::string> SERVERS{{Config::System::DualShockUDPClient, "Server", "Entries"},
""};
const Config::Info<bool> SERVERS_ENABLED{{Config::System::DualShockUDPClient, "Server", "Enabled"},
false};
} // namespace Settings
// Clock type used for querying timeframes
using SteadyClock = std::chrono::steady_clock;
class Device final : public Core::Device
{
private:
template <class T>
class Button final : public Input
{
public:
Button(const char* name, const T& buttons, T mask)
: m_name(name), m_buttons(buttons), m_mask(mask)
{
}
std::string GetName() const override { return m_name; }
ControlState GetState() const override { return (m_buttons & m_mask) != 0; }
private:
const char* const m_name;
const T& m_buttons;
const T m_mask;
};
template <class T>
class AnalogInput : public Input
{
public:
AnalogInput(const char* name, const T& input, ControlState range, ControlState offset = 0)
: m_name(name), m_input(input), m_range(range), m_offset(offset)
{
}
std::string GetName() const final override { return m_name; }
ControlState GetState() const final override
{
return (ControlState(m_input) + m_offset) / m_range;
}
private:
const char* m_name;
const T& m_input;
const ControlState m_range;
const ControlState m_offset;
};
class TouchInput final : public AnalogInput<int>
{
public:
using AnalogInput::AnalogInput;
bool IsDetectable() const override { return false; }
};
class MotionInput final : public AnalogInput<float>
{
public:
using AnalogInput::AnalogInput;
bool IsDetectable() const override { return false; }
};
using AccelerometerInput = MotionInput;
using GyroInput = MotionInput;
class BatteryInput final : public Input
{
public:
using BatteryState = Proto::DsBattery;
BatteryInput(const BatteryState& battery) : m_battery(battery) {}
std::string GetName() const override { return "Battery"; }
ControlState GetState() const override
{
switch (m_battery)
{
case BatteryState::Charging: // We don't actually know the battery level in this case
case BatteryState::Charged:
return BATTERY_INPUT_MAX_VALUE;
default:
return ControlState(m_battery) / ControlState(BatteryState::Full) * BATTERY_INPUT_MAX_VALUE;
}
}
bool IsDetectable() const override { return false; }
private:
const BatteryState& m_battery;
};
public:
void UpdateInput() override;
Device(std::string name, int index, std::string server_address, u16 server_port, u32 client_uid);
std::string GetName() const final override;
std::string GetSource() const final override;
std::optional<int> GetPreferredId() const final override;
// Always add these at the end, given their hotplug nature
int GetSortPriority() const override { return -5; }
private:
void ResetPadData();
const std::string m_name;
const int m_index;
sf::UdpSocket m_socket;
SteadyClock::time_point m_next_reregister = SteadyClock::time_point::min();
Proto::MessageType::PadDataResponse m_pad_data{};
Proto::Touch m_prev_touch{};
bool m_prev_touch_valid = false;
int m_touch_x = 0;
int m_touch_y = 0;
const std::string m_server_address;
const u16 m_server_port;
s16 m_touch_x_min = 0;
s16 m_touch_y_min = 0;
s16 m_touch_x_max = 0;
s16 m_touch_y_max = 0;
const u32 m_client_uid;
};
using MathUtil::GRAVITY_ACCELERATION;
constexpr auto SERVER_REREGISTER_INTERVAL = std::chrono::seconds{1};
constexpr auto SERVER_LISTPORTS_INTERVAL = std::chrono::seconds{1};
constexpr int TOUCH_X_AXIS_MAX = 1000;
constexpr int TOUCH_Y_AXIS_MAX = 500;
constexpr auto THREAD_MAX_WAIT_INTERVAL = std::chrono::milliseconds{250};
constexpr auto SERVER_UNRESPONSIVE_INTERVAL = std::chrono::seconds{1}; // Can be 0
constexpr u32 SERVER_ASKED_PADS = 4;
struct Server
{
Server(std::string description, std::string address, u16 port)
: m_description{std::move(description)}, m_address{std::move(address)}, m_port{port}
{
}
Server(const Server&) = delete;
Server(Server&& other) noexcept
{
m_description = std::move(other.m_description);
m_address = std::move(other.m_address);
m_port = other.m_port;
m_port_info = std::move(other.m_port_info);
}
Server& operator=(const Server&) = delete;
Server& operator=(Server&&) = delete;
~Server() = default;
std::string m_description;
std::string m_address;
u16 m_port = 0;
std::array<Proto::MessageType::PortInfo, Proto::PORT_COUNT> m_port_info{};
sf::UdpSocket m_socket;
SteadyClock::time_point m_disconnect_time = SteadyClock::now();
};
class InputBackend final : public ciface::InputBackend
{
public:
InputBackend(ControllerInterface* controller_interface);
~InputBackend();
void PopulateDevices() override;
private:
void ConfigChanged();
void Restart();
void HotplugThreadFunc();
void StartHotplugThread();
void StopHotplugThread();
bool m_servers_enabled = false;
std::vector<Server> m_servers;
u32 m_client_uid = 0;
SteadyClock::time_point m_next_listports_time;
std::thread m_hotplug_thread;
Common::Flag m_hotplug_thread_running;
Config::ConfigChangedCallbackID m_config_change_callback_id;
};
std::unique_ptr<ciface::InputBackend> CreateInputBackend(ControllerInterface* controller_interface)
{
return std::make_unique<InputBackend>(controller_interface);
}
static bool IsSameController(const Proto::MessageType::PortInfo& a,
const Proto::MessageType::PortInfo& b)
{
// compare everything but battery_status
return std::tie(a.pad_id, a.pad_state, a.model, a.connection_type, a.pad_mac_address) ==
std::tie(b.pad_id, b.pad_state, b.model, b.connection_type, b.pad_mac_address);
}
void InputBackend::HotplugThreadFunc()
{
Common::SetCurrentThreadName("DualShockUDPClient Hotplug Thread");
INFO_LOG_FMT(CONTROLLERINTERFACE, "DualShockUDPClient hotplug thread started");
Common::ScopeGuard thread_stop_guard{
[] { INFO_LOG_FMT(CONTROLLERINTERFACE, "DualShockUDPClient hotplug thread stopped"); }};
std::vector<bool> timed_out_servers(m_servers.size(), false);
while (m_hotplug_thread_running.IsSet())
{
using namespace std::chrono;
using namespace std::chrono_literals;
const auto now = SteadyClock::now();
if (now >= m_next_listports_time)
{
m_next_listports_time = now + SERVER_LISTPORTS_INTERVAL;
for (size_t i = 0; i < m_servers.size(); ++i)
{
auto& server = m_servers[i];
Proto::Message<Proto::MessageType::ListPorts> msg(m_client_uid);
auto& list_ports = msg.m_message;
// We ask for x possible devices. We will receive a message for every connected device.
list_ports.pad_request_count = SERVER_ASKED_PADS;
list_ports.pad_ids = {0, 1, 2, 3};
msg.Finish();
if (server.m_socket.send(&list_ports, sizeof list_ports, server.m_address, server.m_port) !=
sf::Socket::Status::Done)
{
ERROR_LOG_FMT(CONTROLLERINTERFACE, "DualShockUDPClient HotplugThreadFunc send failed");
}
timed_out_servers[i] = true;
}
}
sf::SocketSelector selector;
for (auto& server : m_servers)
{
selector.add(server.m_socket);
}
auto timeout = duration_cast<milliseconds>(m_next_listports_time - SteadyClock::now());
// Receive controller port info within a time from our request.
// Run this even if we sent no new requests, to disconnect devices,
// sleep (wait) the thread and catch old responses.
do
{
// Selector's wait treats a timeout of zero as infinite timeout, which we don't want,
// but we also don't want risk waiting for the whole SERVER_LISTPORTS_INTERVAL and hang
// the thead trying to close this one in case we received no answers.
const auto current_timeout = std::max(std::min(timeout, THREAD_MAX_WAIT_INTERVAL), 1ms);
timeout -= current_timeout;
// This will return at the first answer
if (selector.wait(sf::milliseconds(current_timeout.count())))
{
// Now check all the servers because we don't know which one(s) sent a reply
for (size_t i = 0; i < m_servers.size(); ++i)
{
auto& server = m_servers[i];
if (!selector.isReady(server.m_socket))
{
continue;
}
Proto::Message<Proto::MessageType::FromServer> msg;
std::size_t received_bytes;
sf::IpAddress sender;
u16 port;
if (server.m_socket.receive(&msg, sizeof(msg), received_bytes, sender, port) !=
sf::Socket::Status::Done)
{
continue;
}
if (auto port_info = msg.CheckAndCastTo<Proto::MessageType::PortInfo>())
{
server.m_disconnect_time = SteadyClock::now() + SERVER_UNRESPONSIVE_INTERVAL;
// We have receive at least one valid update, that's enough. This is needed to avoid
// false positive when checking for disconnection in case our thread waited too long
timed_out_servers[i] = false;
const bool port_changed =
!IsSameController(*port_info, server.m_port_info[port_info->pad_id]);
if (port_changed)
{
server.m_port_info[port_info->pad_id] = *port_info;
// Just remove and re-add all the devices for simplicity
GetControllerInterface().PlatformPopulateDevices([this] { PopulateDevices(); });
}
}
}
}
if (!m_hotplug_thread_running.IsSet()) // Avoid hanging the thread for too long
return;
} while (timeout > 0ms);
// If we have failed to receive any information from the server (or even send it),
// disconnect all devices from it (after enough time has elapsed, to avoid false positives).
for (size_t i = 0; i < m_servers.size(); ++i)
{
auto& server = m_servers[i];
if (timed_out_servers[i] && SteadyClock::now() >= server.m_disconnect_time)
{
bool any_connected = false;
for (size_t port_index = 0; port_index < server.m_port_info.size(); port_index++)
{
any_connected = any_connected ||
server.m_port_info[port_index].pad_state == Proto::DsState::Connected;
server.m_port_info[port_index] = {};
server.m_port_info[port_index].pad_id = static_cast<u8>(port_index);
}
// We can't only remove devices added by this server as we wouldn't know which they are
if (any_connected)
GetControllerInterface().PlatformPopulateDevices([this] { PopulateDevices(); });
}
}
}
}
void InputBackend::StartHotplugThread()
{
// Mark the thread as running.
if (!m_hotplug_thread_running.TestAndSet())
{
// It was already running.
return;
}
m_hotplug_thread = std::thread(&InputBackend::HotplugThreadFunc, this);
}
void InputBackend::StopHotplugThread()
{
// Tell the hotplug thread to stop.
if (!m_hotplug_thread_running.TestAndClear())
{
// It wasn't running, we're done.
return;
}
m_hotplug_thread.join();
for (auto& server : m_servers)
{
server.m_socket.unbind(); // interrupt blocking socket
}
}
// Also just start
void InputBackend::Restart()
{
INFO_LOG_FMT(CONTROLLERINTERFACE, "DualShockUDPClient Restart");
StopHotplugThread();
for (auto& server : m_servers)
{
for (size_t port_index = 0; port_index < server.m_port_info.size(); port_index++)
{
server.m_port_info[port_index] = {};
server.m_port_info[port_index].pad_id = static_cast<u8>(port_index);
}
}
// Only removes devices as servers have been cleaned
GetControllerInterface().PlatformPopulateDevices([this] { PopulateDevices(); });
m_client_uid = Common::Random::GenerateValue<u32>();
m_next_listports_time = SteadyClock::now();
if (m_servers_enabled && !m_servers.empty())
StartHotplugThread();
}
void InputBackend::ConfigChanged()
{
const bool servers_enabled = Config::Get(Settings::SERVERS_ENABLED);
const std::string servers_setting = Config::Get(Settings::SERVERS);
std::string new_servers_setting;
for (const auto& server : m_servers)
{
new_servers_setting +=
fmt::format("{}:{}:{};", server.m_description, server.m_address, server.m_port);
}
if (servers_enabled != m_servers_enabled || servers_setting != new_servers_setting)
{
// Stop the thread before writing to m_servers
StopHotplugThread();
m_servers_enabled = servers_enabled;
m_servers.clear();
const auto server_details = SplitString(servers_setting, ';');
for (const auto& server_detail : server_details)
{
const auto server_info = SplitString(server_detail, ':');
if (server_info.size() < 3)
continue;
const std::string description = server_info[0];
const std::string server_address = server_info[1];
const auto port = std::stoi(server_info[2]);
if (port >= std::numeric_limits<u16>::max())
{
continue;
}
u16 server_port = static_cast<u16>(port);
m_servers.emplace_back(description, server_address, server_port);
}
Restart();
}
}
InputBackend::InputBackend(ControllerInterface* controller_interface)
: ciface::InputBackend(controller_interface)
{
// The following is added for backwards compatibility
const auto server_address_setting = Config::Get(Settings::SERVER_ADDRESS);
const auto server_port_setting = Config::Get(Settings::SERVER_PORT);
if (!server_address_setting.empty() && server_port_setting != 0)
{
const auto& servers_setting = Config::Get(ciface::DualShockUDPClient::Settings::SERVERS);
Config::SetBaseOrCurrent(ciface::DualShockUDPClient::Settings::SERVERS,
servers_setting + fmt::format("{}:{}:{};", "DS4",
server_address_setting,
server_port_setting));
Config::SetBase(Settings::SERVER_ADDRESS, "");
Config::SetBase(Settings::SERVER_PORT, 0);
}
m_config_change_callback_id =
Config::AddConfigChangedCallback(std::bind(&InputBackend::ConfigChanged, this));
// Call it immediately to load settings
ConfigChanged();
}
// This can be called by the host thread as well as the hotplug thread, concurrently.
// So use PlatformPopulateDevices().
// m_servers is already safe because it can only be modified when the DSU thread is not running,
// from the main thread
void InputBackend::PopulateDevices()
{
INFO_LOG_FMT(CONTROLLERINTERFACE, "DualShockUDPClient PopulateDevices");
// m_servers has already been updated so we can't use it to know which devices we removed,
// also it's good to remove all of them before adding new ones so that their id will be set
// correctly if they have the same name
GetControllerInterface().RemoveDevice(
[](const auto* dev) { return dev->GetSource() == DUALSHOCKUDP_SOURCE_NAME; });
// Users might have created more than one server on the same IP/Port.
// Devices might end up being duplicated (if the server responds two all requests)
// but they won't conflict.
for (const auto& server : m_servers)
{
for (size_t port_index = 0; port_index < server.m_port_info.size(); port_index++)
{
const Proto::MessageType::PortInfo& port_info = server.m_port_info[port_index];
if (port_info.pad_state != Proto::DsState::Connected)
continue;
GetControllerInterface().AddDevice(
std::make_shared<Device>(server.m_description, static_cast<int>(port_index),
server.m_address, server.m_port, m_client_uid));
}
}
}
InputBackend::~InputBackend()
{
Config::RemoveConfigChangedCallback(m_config_change_callback_id);
StopHotplugThread();
}
Device::Device(std::string name, int index, std::string server_address, u16 server_port,
u32 client_uid)
: m_name{std::move(name)}, m_index{index}, m_server_address{std::move(server_address)},
m_server_port{server_port}, m_client_uid(client_uid)
{
m_socket.setBlocking(false);
AddInput(new AnalogInput<u8>("Pad W", m_pad_data.button_dpad_left_analog, 255));
AddInput(new AnalogInput<u8>("Pad S", m_pad_data.button_dpad_down_analog, 255));
AddInput(new AnalogInput<u8>("Pad E", m_pad_data.button_dpad_right_analog, 255));
AddInput(new AnalogInput<u8>("Pad N", m_pad_data.button_dpad_up_analog, 255));
AddInput(new AnalogInput<u8>("Square", m_pad_data.button_square_analog, 255));
AddInput(new AnalogInput<u8>("Cross", m_pad_data.button_cross_analog, 255));
AddInput(new AnalogInput<u8>("Circle", m_pad_data.button_circle_analog, 255));
AddInput(new AnalogInput<u8>("Triangle", m_pad_data.button_triangle_analog, 255));
AddInput(new AnalogInput<u8>("L1", m_pad_data.button_l1_analog, 255));
AddInput(new AnalogInput<u8>("R1", m_pad_data.button_r1_analog, 255));
AddInput(new AnalogInput<u8>("L2", m_pad_data.trigger_l2, 255));
AddInput(new AnalogInput<u8>("R2", m_pad_data.trigger_r2, 255));
AddInput(new Button<u8>("L3", m_pad_data.button_states1, 0x2));
AddInput(new Button<u8>("R3", m_pad_data.button_states1, 0x4));
AddInput(new Button<u8>("Share", m_pad_data.button_states1, 0x1));
AddInput(new Button<u8>("Options", m_pad_data.button_states1, 0x8));
AddInput(new Button<u8>("PS", m_pad_data.button_ps, 0x1));
AddInput(new Button<u8>("Touch Button", m_pad_data.button_touch, 0x1));
AddInput(new AnalogInput<u8>("Left X-", m_pad_data.left_stick_x, -128, -128));
AddInput(new AnalogInput<u8>("Left X+", m_pad_data.left_stick_x, 127, -128));
AddInput(new AnalogInput<u8>("Left Y-", m_pad_data.left_stick_y_inverted, -128, -128));
AddInput(new AnalogInput<u8>("Left Y+", m_pad_data.left_stick_y_inverted, 127, -128));
AddInput(new AnalogInput<u8>("Right X-", m_pad_data.right_stick_x, -128, -128));
AddInput(new AnalogInput<u8>("Right X+", m_pad_data.right_stick_x, 127, -128));
AddInput(new AnalogInput<u8>("Right Y-", m_pad_data.right_stick_y_inverted, -128, -128));
AddInput(new AnalogInput<u8>("Right Y+", m_pad_data.right_stick_y_inverted, 127, -128));
AddInput(new TouchInput("Touch X-", m_touch_x, -TOUCH_X_AXIS_MAX));
AddInput(new TouchInput("Touch X+", m_touch_x, TOUCH_X_AXIS_MAX));
AddInput(new TouchInput("Touch Y-", m_touch_y, -TOUCH_Y_AXIS_MAX));
AddInput(new TouchInput("Touch Y+", m_touch_y, TOUCH_Y_AXIS_MAX));
// Convert Gs to meters per second squared
constexpr auto accel_scale = 1.0 / GRAVITY_ACCELERATION;
AddInput(new AccelerometerInput("Accel Up", m_pad_data.accelerometer_y_g, -accel_scale));
AddInput(new AccelerometerInput("Accel Down", m_pad_data.accelerometer_y_g, accel_scale));
AddInput(new AccelerometerInput("Accel Left", m_pad_data.accelerometer_x_g, accel_scale));
AddInput(new AccelerometerInput("Accel Right", m_pad_data.accelerometer_x_g, -accel_scale));
AddInput(new AccelerometerInput("Accel Forward", m_pad_data.accelerometer_z_g, accel_scale));
AddInput(new AccelerometerInput("Accel Backward", m_pad_data.accelerometer_z_g, -accel_scale));
// Convert degrees per second to radians per second
constexpr auto gyro_scale = 360.0 / MathUtil::TAU;
AddInput(new GyroInput("Gyro Pitch Up", m_pad_data.gyro_pitch_deg_s, gyro_scale));
AddInput(new GyroInput("Gyro Pitch Down", m_pad_data.gyro_pitch_deg_s, -gyro_scale));
AddInput(new GyroInput("Gyro Roll Left", m_pad_data.gyro_roll_deg_s, -gyro_scale));
AddInput(new GyroInput("Gyro Roll Right", m_pad_data.gyro_roll_deg_s, gyro_scale));
AddInput(new GyroInput("Gyro Yaw Left", m_pad_data.gyro_yaw_deg_s, -gyro_scale));
AddInput(new GyroInput("Gyro Yaw Right", m_pad_data.gyro_yaw_deg_s, gyro_scale));
AddInput(new BatteryInput(m_pad_data.battery_status));
m_touch_x_min = 0;
m_touch_y_min = 0;
// DS4 touchpad max values
m_touch_x_max = 1919;
m_touch_y_max = 941;
ResetPadData();
}
void Device::ResetPadData()
{
m_pad_data = Proto::MessageType::PadDataResponse{};
// Make sure they start from resting values, not from 0
m_touch_x = m_touch_x_min + ((m_touch_x_max - m_touch_x_min) / 2.0);
m_touch_y = m_touch_y_min + ((m_touch_y_max - m_touch_y_min) / 2.0);
m_pad_data.left_stick_x = 128;
m_pad_data.left_stick_y_inverted = 128;
m_pad_data.right_stick_x = 128;
m_pad_data.right_stick_y_inverted = 128;
m_pad_data.touch1.x = m_touch_x;
m_pad_data.touch1.y = m_touch_y;
}
std::string Device::GetName() const
{
return m_name;
}
std::string Device::GetSource() const
{
return std::string(DUALSHOCKUDP_SOURCE_NAME);
}
void Device::UpdateInput()
{
// Regularly tell the UDP server to feed us controller data
const auto now = SteadyClock::now();
if (now >= m_next_reregister)
{
m_next_reregister = now + SERVER_REREGISTER_INTERVAL;
Proto::Message<Proto::MessageType::PadDataRequest> msg(m_client_uid);
auto& data_req = msg.m_message;
data_req.register_flags = Proto::RegisterFlags::PadID;
data_req.pad_id_to_register = m_index;
msg.Finish();
if (m_socket.send(&data_req, sizeof(data_req), m_server_address, m_server_port) !=
sf::Socket::Status::Done)
{
ERROR_LOG_FMT(CONTROLLERINTERFACE, "DualShockUDPClient UpdateInput send failed");
}
}
// Receive and handle controller data
Proto::Message<Proto::MessageType::FromServer> msg;
std::size_t received_bytes;
sf::IpAddress sender;
u16 port;
while (m_socket.receive(&msg, sizeof msg, received_bytes, sender, port) ==
sf::Socket::Status::Done)
{
if (auto pad_data = msg.CheckAndCastTo<Proto::MessageType::PadDataResponse>())
{
m_pad_data = *pad_data;
// Update touch pad relative coordinates
if (m_pad_data.touch1.id != m_prev_touch.id)
m_prev_touch_valid = false;
if (m_prev_touch_valid)
{
m_touch_x += m_pad_data.touch1.x - m_prev_touch.x;
m_touch_y += m_pad_data.touch1.y - m_prev_touch.y;
m_touch_x = std::clamp(m_touch_x, -TOUCH_X_AXIS_MAX, TOUCH_X_AXIS_MAX);
m_touch_y = std::clamp(m_touch_y, -TOUCH_Y_AXIS_MAX, TOUCH_Y_AXIS_MAX);
}
m_prev_touch = m_pad_data.touch1;
m_prev_touch_valid = true;
}
}
}
std::optional<int> Device::GetPreferredId() const
{
return m_index;
}
} // namespace ciface::DualShockUDPClient