Input: UDP Client to provide motion and touch controls
An implementation of the cemuhook motion/touch protocol, this adds the ability for users to connect several different devices to citra to send direct motion and touch data to citra. Co-Authored-By: jroweboy <jroweboy@gmail.com>
This commit is contained in:
parent
a167da4278
commit
ac3690f205
14 changed files with 904 additions and 4 deletions
|
@ -350,6 +350,13 @@ function(create_target_directory_groups target_name)
|
||||||
endforeach()
|
endforeach()
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
|
# Prevent boost from linking against libs when building
|
||||||
|
add_definitions(-DBOOST_ERROR_CODE_HEADER_ONLY
|
||||||
|
-DBOOST_SYSTEM_NO_LIB
|
||||||
|
-DBOOST_DATE_TIME_NO_LIB
|
||||||
|
-DBOOST_REGEX_NO_LIB
|
||||||
|
)
|
||||||
|
|
||||||
enable_testing()
|
enable_testing()
|
||||||
add_subdirectory(externals)
|
add_subdirectory(externals)
|
||||||
add_subdirectory(src)
|
add_subdirectory(src)
|
||||||
|
|
|
@ -28,6 +28,15 @@ public:
|
||||||
is_set = false;
|
is_set = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <class Duration>
|
||||||
|
bool WaitFor(const std::chrono::duration<Duration>& time) {
|
||||||
|
std::unique_lock<std::mutex> lk(mutex);
|
||||||
|
if (!condvar.wait_for(lk, time, [this] { return is_set; }))
|
||||||
|
return false;
|
||||||
|
is_set = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
template <class Clock, class Duration>
|
template <class Clock, class Duration>
|
||||||
bool WaitUntil(const std::chrono::time_point<Clock, Duration>& time) {
|
bool WaitUntil(const std::chrono::time_point<Clock, Duration>& time) {
|
||||||
std::unique_lock lk{mutex};
|
std::unique_lock lk{mutex};
|
||||||
|
|
|
@ -401,6 +401,9 @@ struct Values {
|
||||||
std::string motion_device;
|
std::string motion_device;
|
||||||
TouchscreenInput touchscreen;
|
TouchscreenInput touchscreen;
|
||||||
std::atomic_bool is_device_reload_pending{true};
|
std::atomic_bool is_device_reload_pending{true};
|
||||||
|
std::string udp_input_address;
|
||||||
|
u16 udp_input_port;
|
||||||
|
u8 udp_pad_index;
|
||||||
|
|
||||||
// Core
|
// Core
|
||||||
bool use_multi_core;
|
bool use_multi_core;
|
||||||
|
|
|
@ -9,6 +9,12 @@ add_library(input_common STATIC
|
||||||
motion_emu.h
|
motion_emu.h
|
||||||
sdl/sdl.cpp
|
sdl/sdl.cpp
|
||||||
sdl/sdl.h
|
sdl/sdl.h
|
||||||
|
udp/client.cpp
|
||||||
|
udp/client.h
|
||||||
|
udp/protocol.cpp
|
||||||
|
udp/protocol.h
|
||||||
|
udp/udp.cpp
|
||||||
|
udp/udp.h
|
||||||
)
|
)
|
||||||
|
|
||||||
if(SDL2_FOUND)
|
if(SDL2_FOUND)
|
||||||
|
@ -21,4 +27,4 @@ if(SDL2_FOUND)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
create_target_directory_groups(input_common)
|
create_target_directory_groups(input_common)
|
||||||
target_link_libraries(input_common PUBLIC core PRIVATE common)
|
target_link_libraries(input_common PUBLIC core PRIVATE common ${Boost_LIBRARIES})
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include "input_common/keyboard.h"
|
#include "input_common/keyboard.h"
|
||||||
#include "input_common/main.h"
|
#include "input_common/main.h"
|
||||||
#include "input_common/motion_emu.h"
|
#include "input_common/motion_emu.h"
|
||||||
|
#include "input_common/udp/udp.h"
|
||||||
#ifdef HAVE_SDL2
|
#ifdef HAVE_SDL2
|
||||||
#include "input_common/sdl/sdl.h"
|
#include "input_common/sdl/sdl.h"
|
||||||
#endif
|
#endif
|
||||||
|
@ -18,6 +19,7 @@ namespace InputCommon {
|
||||||
static std::shared_ptr<Keyboard> keyboard;
|
static std::shared_ptr<Keyboard> keyboard;
|
||||||
static std::shared_ptr<MotionEmu> motion_emu;
|
static std::shared_ptr<MotionEmu> motion_emu;
|
||||||
static std::unique_ptr<SDL::State> sdl;
|
static std::unique_ptr<SDL::State> sdl;
|
||||||
|
static std::unique_ptr<CemuhookUDP::State> udp;
|
||||||
|
|
||||||
void Init() {
|
void Init() {
|
||||||
keyboard = std::make_shared<Keyboard>();
|
keyboard = std::make_shared<Keyboard>();
|
||||||
|
@ -28,6 +30,8 @@ void Init() {
|
||||||
Input::RegisterFactory<Input::MotionDevice>("motion_emu", motion_emu);
|
Input::RegisterFactory<Input::MotionDevice>("motion_emu", motion_emu);
|
||||||
|
|
||||||
sdl = SDL::Init();
|
sdl = SDL::Init();
|
||||||
|
|
||||||
|
udp = CemuhookUDP::Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Shutdown() {
|
void Shutdown() {
|
||||||
|
@ -72,11 +76,13 @@ std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left,
|
||||||
namespace Polling {
|
namespace Polling {
|
||||||
|
|
||||||
std::vector<std::unique_ptr<DevicePoller>> GetPollers(DeviceType type) {
|
std::vector<std::unique_ptr<DevicePoller>> GetPollers(DeviceType type) {
|
||||||
|
std::vector<std::unique_ptr<DevicePoller>> pollers;
|
||||||
|
|
||||||
#ifdef HAVE_SDL2
|
#ifdef HAVE_SDL2
|
||||||
return sdl->GetPollers(type);
|
pollers = sdl->GetPollers(type);
|
||||||
#else
|
|
||||||
return {};
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
return pollers;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Polling
|
} // namespace Polling
|
||||||
|
|
283
src/input_common/udp/client.cpp
Normal file
283
src/input_common/udp/client.cpp
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstring>
|
||||||
|
#include <functional>
|
||||||
|
#include <thread>
|
||||||
|
#include <boost/asio.hpp>
|
||||||
|
#include <boost/bind.hpp>
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "input_common/udp/client.h"
|
||||||
|
#include "input_common/udp/protocol.h"
|
||||||
|
|
||||||
|
using boost::asio::ip::address_v4;
|
||||||
|
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, u8 pad_index, u32 client_id,
|
||||||
|
SocketCallback callback)
|
||||||
|
: client_id(client_id), timer(io_service),
|
||||||
|
send_endpoint(udp::endpoint(address_v4::from_string(host), port)),
|
||||||
|
socket(io_service, udp::endpoint(udp::v4(), 0)), pad_index(pad_index),
|
||||||
|
callback(std::move(callback)) {}
|
||||||
|
|
||||||
|
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:
|
||||||
|
void HandleReceive(const boost::system::error_code& error, 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& error) {
|
||||||
|
// Send a request for getting port info for the pad
|
||||||
|
Request::PortInfo port_info{1, {pad_index, 0, 0, 0}};
|
||||||
|
auto port_message = Request::Create(port_info, client_id);
|
||||||
|
std::memcpy(&send_buffer1, &port_message, PORT_INFO_SIZE);
|
||||||
|
std::size_t len = socket.send_to(boost::asio::buffer(send_buffer1), send_endpoint);
|
||||||
|
|
||||||
|
// Send a request for getting pad data for the pad
|
||||||
|
Request::PadData pad_data{Request::PadData::Flags::Id, pad_index, EMPTY_MAC_ADDRESS};
|
||||||
|
auto pad_message = Request::Create(pad_data, client_id);
|
||||||
|
std::memcpy(send_buffer2.data(), &pad_message, PAD_DATA_SIZE);
|
||||||
|
std::size_t len2 = socket.send_to(boost::asio::buffer(send_buffer2), send_endpoint);
|
||||||
|
StartSend(timer.expiry());
|
||||||
|
}
|
||||||
|
|
||||||
|
SocketCallback callback;
|
||||||
|
boost::asio::io_service io_service;
|
||||||
|
boost::asio::basic_waitable_timer<clock> timer;
|
||||||
|
udp::socket socket;
|
||||||
|
|
||||||
|
u32 client_id;
|
||||||
|
u8 pad_index;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
Client::Client(std::shared_ptr<DeviceStatus> status, const std::string& host, u16 port,
|
||||||
|
u8 pad_index, u32 client_id)
|
||||||
|
: status(status) {
|
||||||
|
StartCommunication(host, port, pad_index, client_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Client::~Client() {
|
||||||
|
socket->Stop();
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Client::ReloadSocket(const std::string& host, u16 port, u8 pad_index, u32 client_id) {
|
||||||
|
socket->Stop();
|
||||||
|
thread.join();
|
||||||
|
StartCommunication(host, port, pad_index, client_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Client::OnVersion(Response::Version data) {
|
||||||
|
LOG_TRACE(Input, "Version packet received: {}", data.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Client::OnPortInfo(Response::PortInfo data) {
|
||||||
|
LOG_TRACE(Input, "PortInfo packet received: {}", data.model);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Client::OnPadData(Response::PadData data) {
|
||||||
|
LOG_TRACE(Input, "PadData packet received");
|
||||||
|
if (data.packet_counter <= packet_sequence) {
|
||||||
|
LOG_WARNING(
|
||||||
|
Input,
|
||||||
|
"PadData packet dropped because its stale info. Current count: {} Packet count: {}",
|
||||||
|
packet_sequence, data.packet_counter);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
packet_sequence = data.packet_counter;
|
||||||
|
// TODO: Check how the Switch handles motions and how the CemuhookUDP motion
|
||||||
|
// directions correspond to the ones of the Switch
|
||||||
|
Common::Vec3f accel = Common::MakeVec<float>(data.accel.x, data.accel.y, data.accel.z);
|
||||||
|
Common::Vec3f gyro = Common::MakeVec<float>(data.gyro.pitch, data.gyro.yaw, data.gyro.roll);
|
||||||
|
{
|
||||||
|
std::lock_guard guard(status->update_mutex);
|
||||||
|
|
||||||
|
status->motion_status = {accel, gyro};
|
||||||
|
|
||||||
|
// TODO: add a setting for "click" touch. Click touch refers to a device that differentiates
|
||||||
|
// between a simple "tap" and a hard press that causes the touch screen to click.
|
||||||
|
bool is_active = data.touch_1.is_active != 0;
|
||||||
|
|
||||||
|
float x = 0;
|
||||||
|
float y = 0;
|
||||||
|
|
||||||
|
if (is_active && status->touch_calibration) {
|
||||||
|
u16 min_x = status->touch_calibration->min_x;
|
||||||
|
u16 max_x = status->touch_calibration->max_x;
|
||||||
|
u16 min_y = status->touch_calibration->min_y;
|
||||||
|
u16 max_y = status->touch_calibration->max_y;
|
||||||
|
|
||||||
|
x = (std::clamp(static_cast<u16>(data.touch_1.x), min_x, max_x) - min_x) /
|
||||||
|
static_cast<float>(max_x - min_x);
|
||||||
|
y = (std::clamp(static_cast<u16>(data.touch_1.y), min_y, max_y) - min_y) /
|
||||||
|
static_cast<float>(max_y - min_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
status->touch_status = {x, y, is_active};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Client::StartCommunication(const std::string& host, u16 port, u8 pad_index, u32 client_id) {
|
||||||
|
SocketCallback callback{[this](Response::Version version) { OnVersion(version); },
|
||||||
|
[this](Response::PortInfo info) { OnPortInfo(info); },
|
||||||
|
[this](Response::PadData data) { OnPadData(data); }};
|
||||||
|
LOG_INFO(Input, "Starting communication with UDP input server on {}:{}", host, port);
|
||||||
|
socket = std::make_unique<Socket>(host, port, pad_index, client_id, callback);
|
||||||
|
thread = std::thread{SocketLoop, this->socket.get()};
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestCommunication(const std::string& host, u16 port, u8 pad_index, u32 client_id,
|
||||||
|
std::function<void()> success_callback,
|
||||||
|
std::function<void()> failure_callback) {
|
||||||
|
std::thread([=] {
|
||||||
|
Common::Event success_event;
|
||||||
|
SocketCallback callback{[](Response::Version version) {}, [](Response::PortInfo info) {},
|
||||||
|
[&](Response::PadData data) { success_event.Set(); }};
|
||||||
|
Socket socket{host, port, pad_index, client_id, callback};
|
||||||
|
std::thread worker_thread{SocketLoop, &socket};
|
||||||
|
bool result = success_event.WaitFor(std::chrono::seconds(8));
|
||||||
|
socket.Stop();
|
||||||
|
worker_thread.join();
|
||||||
|
if (result)
|
||||||
|
success_callback();
|
||||||
|
else
|
||||||
|
failure_callback();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
CalibrationConfigurationJob::CalibrationConfigurationJob(
|
||||||
|
const std::string& host, u16 port, u8 pad_index, u32 client_id,
|
||||||
|
std::function<void(Status)> status_callback,
|
||||||
|
std::function<void(u16, u16, u16, u16)> data_callback) {
|
||||||
|
|
||||||
|
std::thread([=] {
|
||||||
|
constexpr u16 CALIBRATION_THRESHOLD = 100;
|
||||||
|
|
||||||
|
u16 min_x{UINT16_MAX}, min_y{UINT16_MAX};
|
||||||
|
u16 max_x, max_y;
|
||||||
|
|
||||||
|
Status current_status{Status::Initialized};
|
||||||
|
SocketCallback callback{[](Response::Version version) {}, [](Response::PortInfo info) {},
|
||||||
|
[&](Response::PadData data) {
|
||||||
|
if (current_status == Status::Initialized) {
|
||||||
|
// Receiving data means the communication is ready now
|
||||||
|
current_status = Status::Ready;
|
||||||
|
status_callback(current_status);
|
||||||
|
}
|
||||||
|
if (!data.touch_1.is_active)
|
||||||
|
return;
|
||||||
|
LOG_DEBUG(Input, "Current touch: {} {}", data.touch_1.x,
|
||||||
|
data.touch_1.y);
|
||||||
|
min_x = std::min(min_x, static_cast<u16>(data.touch_1.x));
|
||||||
|
min_y = std::min(min_y, static_cast<u16>(data.touch_1.y));
|
||||||
|
if (current_status == Status::Ready) {
|
||||||
|
// First touch - min data (min_x/min_y)
|
||||||
|
current_status = Status::Stage1Completed;
|
||||||
|
status_callback(current_status);
|
||||||
|
}
|
||||||
|
if (data.touch_1.x - min_x > CALIBRATION_THRESHOLD &&
|
||||||
|
data.touch_1.y - min_y > CALIBRATION_THRESHOLD) {
|
||||||
|
// Set the current position as max value and finishes
|
||||||
|
// configuration
|
||||||
|
max_x = data.touch_1.x;
|
||||||
|
max_y = data.touch_1.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, pad_index, client_id, 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
|
96
src/input_common/udp/client.h
Normal file
96
src/input_common/udp/client.h
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <tuple>
|
||||||
|
#include <vector>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/thread.h"
|
||||||
|
#include "common/vector_math.h"
|
||||||
|
|
||||||
|
namespace InputCommon::CemuhookUDP {
|
||||||
|
|
||||||
|
static constexpr u16 DEFAULT_PORT = 26760;
|
||||||
|
static constexpr const char* DEFAULT_ADDR = "127.0.0.1";
|
||||||
|
|
||||||
|
class Socket;
|
||||||
|
|
||||||
|
namespace Response {
|
||||||
|
struct PadData;
|
||||||
|
struct PortInfo;
|
||||||
|
struct Version;
|
||||||
|
} // namespace Response
|
||||||
|
|
||||||
|
struct DeviceStatus {
|
||||||
|
std::mutex update_mutex;
|
||||||
|
std::tuple<Common::Vec3<float>, Common::Vec3<float>> motion_status;
|
||||||
|
std::tuple<float, float, bool> touch_status;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Client {
|
||||||
|
public:
|
||||||
|
explicit Client(std::shared_ptr<DeviceStatus> status, const std::string& host = DEFAULT_ADDR,
|
||||||
|
u16 port = DEFAULT_PORT, u8 pad_index = 0, u32 client_id = 24872);
|
||||||
|
~Client();
|
||||||
|
void ReloadSocket(const std::string& host = "127.0.0.1", u16 port = 26760, u8 pad_index = 0,
|
||||||
|
u32 client_id = 24872);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void OnVersion(Response::Version);
|
||||||
|
void OnPortInfo(Response::PortInfo);
|
||||||
|
void OnPadData(Response::PadData);
|
||||||
|
void StartCommunication(const std::string& host, u16 port, u8 pad_index, u32 client_id);
|
||||||
|
|
||||||
|
std::unique_ptr<Socket> socket;
|
||||||
|
std::shared_ptr<DeviceStatus> status;
|
||||||
|
std::thread thread;
|
||||||
|
u64 packet_sequence = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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, u8 pad_index,
|
||||||
|
u32 client_id, 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, u8 pad_index, u32 client_id,
|
||||||
|
std::function<void()> success_callback,
|
||||||
|
std::function<void()> failure_callback);
|
||||||
|
|
||||||
|
} // namespace InputCommon::CemuhookUDP
|
79
src/input_common/udp/protocol.cpp
Normal file
79
src/input_common/udp/protocol.cpp
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstring>
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "input_common/udp/protocol.h"
|
||||||
|
|
||||||
|
namespace InputCommon::CemuhookUDP {
|
||||||
|
|
||||||
|
static const std::size_t GetSizeOfResponseType(Type t) {
|
||||||
|
switch (t) {
|
||||||
|
case Type::Version:
|
||||||
|
return sizeof(Response::Version);
|
||||||
|
case Type::PortInfo:
|
||||||
|
return sizeof(Response::PortInfo);
|
||||||
|
case Type::PadData:
|
||||||
|
return sizeof(Response::PadData);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Response {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Type if the packet is valid, else none
|
||||||
|
*
|
||||||
|
* Note: Modifies the buffer to zero out the crc (since thats the easiest way to check without
|
||||||
|
* copying the buffer)
|
||||||
|
*/
|
||||||
|
std::optional<Type> Validate(u8* data, std::size_t size) {
|
||||||
|
if (size < sizeof(Header)) {
|
||||||
|
LOG_DEBUG(Input, "Invalid UDP packet received");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
Header header;
|
||||||
|
std::memcpy(&header, data, sizeof(Header));
|
||||||
|
if (header.magic != SERVER_MAGIC) {
|
||||||
|
LOG_ERROR(Input, "UDP Packet has an unexpected magic value");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (header.protocol_version != PROTOCOL_VERSION) {
|
||||||
|
LOG_ERROR(Input, "UDP Packet protocol mismatch");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (header.type < Type::Version || header.type > Type::PadData) {
|
||||||
|
LOG_ERROR(Input, "UDP Packet is an unknown type");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Packet size must equal sizeof(Header) + sizeof(Data)
|
||||||
|
// and also verify that the packet info mentions the correct size. Since the spec includes the
|
||||||
|
// type of the packet as part of the data, we need to include it in size calculations here
|
||||||
|
// ie: payload_length == sizeof(T) + sizeof(Type)
|
||||||
|
const std::size_t data_len = GetSizeOfResponseType(header.type);
|
||||||
|
if (header.payload_length != data_len + sizeof(Type) || size < data_len + sizeof(Header)) {
|
||||||
|
LOG_ERROR(
|
||||||
|
Input,
|
||||||
|
"UDP Packet payload length doesn't match. Received: {} PayloadLength: {} Expected: {}",
|
||||||
|
size, header.payload_length, data_len + sizeof(Type));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const u32 crc32 = header.crc;
|
||||||
|
boost::crc_32_type result;
|
||||||
|
// zero out the crc in the buffer and then run the crc against it
|
||||||
|
std::memset(&data[offsetof(Header, crc)], 0, sizeof(u32_le));
|
||||||
|
|
||||||
|
result.process_bytes(data, data_len + sizeof(Header));
|
||||||
|
if (crc32 != result.checksum()) {
|
||||||
|
LOG_ERROR(Input, "UDP Packet CRC check failed. Offset: {}", offsetof(Header, crc));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return header.type;
|
||||||
|
}
|
||||||
|
} // namespace Response
|
||||||
|
|
||||||
|
} // namespace InputCommon::CemuhookUDP
|
249
src/input_common/udp/protocol.h
Normal file
249
src/input_common/udp/protocol.h
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <optional>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <vector>
|
||||||
|
#include <boost/crc.hpp>
|
||||||
|
#include "common/bit_field.h"
|
||||||
|
#include "common/swap.h"
|
||||||
|
|
||||||
|
namespace InputCommon::CemuhookUDP {
|
||||||
|
|
||||||
|
constexpr std::size_t MAX_PACKET_SIZE = 100;
|
||||||
|
constexpr u16 PROTOCOL_VERSION = 1001;
|
||||||
|
constexpr u32 CLIENT_MAGIC = 0x43555344; // DSUC (but flipped for LE)
|
||||||
|
constexpr u32 SERVER_MAGIC = 0x53555344; // DSUS (but flipped for LE)
|
||||||
|
|
||||||
|
enum class Type : u32 {
|
||||||
|
Version = 0x00100000,
|
||||||
|
PortInfo = 0x00100001,
|
||||||
|
PadData = 0x00100002,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Header {
|
||||||
|
u32_le magic;
|
||||||
|
u16_le protocol_version;
|
||||||
|
u16_le payload_length;
|
||||||
|
u32_le crc;
|
||||||
|
u32_le id;
|
||||||
|
///> In the protocol, the type of the packet is not part of the header, but its convenient to
|
||||||
|
///> include in the header so the callee doesn't have to duplicate the type twice when building
|
||||||
|
///> the data
|
||||||
|
Type type;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(Header) == 20, "UDP Message Header struct has wrong size");
|
||||||
|
static_assert(std::is_trivially_copyable_v<Header>, "UDP Message Header is not trivially copyable");
|
||||||
|
|
||||||
|
using MacAddress = std::array<u8, 6>;
|
||||||
|
constexpr MacAddress EMPTY_MAC_ADDRESS = {0, 0, 0, 0, 0, 0};
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
template <typename T>
|
||||||
|
struct Message {
|
||||||
|
Header header;
|
||||||
|
T data;
|
||||||
|
};
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
constexpr Type GetMessageType();
|
||||||
|
|
||||||
|
namespace Request {
|
||||||
|
|
||||||
|
struct Version {};
|
||||||
|
/**
|
||||||
|
* Requests the server to send information about what controllers are plugged into the ports
|
||||||
|
* In citra's case, we only have one controller, so for simplicity's sake, we can just send a
|
||||||
|
* request explicitly for the first controller port and leave it at that. In the future it would be
|
||||||
|
* nice to make this configurable
|
||||||
|
*/
|
||||||
|
constexpr u32 MAX_PORTS = 4;
|
||||||
|
struct PortInfo {
|
||||||
|
u32_le pad_count; ///> Number of ports to request data for
|
||||||
|
std::array<u8, MAX_PORTS> port;
|
||||||
|
};
|
||||||
|
static_assert(std::is_trivially_copyable_v<PortInfo>,
|
||||||
|
"UDP Request PortInfo is not trivially copyable");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the latest pad information from the server. If the server hasn't received this message
|
||||||
|
* from the client in a reasonable time frame, the server will stop sending updates. The default
|
||||||
|
* timeout seems to be 5 seconds.
|
||||||
|
*/
|
||||||
|
struct PadData {
|
||||||
|
enum class Flags : u8 {
|
||||||
|
AllPorts,
|
||||||
|
Id,
|
||||||
|
Mac,
|
||||||
|
};
|
||||||
|
/// Determines which method will be used as a look up for the controller
|
||||||
|
Flags flags;
|
||||||
|
/// Index of the port of the controller to retrieve data about
|
||||||
|
u8 port_id;
|
||||||
|
/// Mac address of the controller to retrieve data about
|
||||||
|
MacAddress mac;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(PadData) == 8, "UDP Request PadData struct has wrong size");
|
||||||
|
static_assert(std::is_trivially_copyable_v<PadData>,
|
||||||
|
"UDP Request PadData is not trivially copyable");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a message with the proper header data that can be sent to the server.
|
||||||
|
* @param T data Request body to send
|
||||||
|
* @param client_id ID of the udp client (usually not checked on the server)
|
||||||
|
*/
|
||||||
|
template <typename T>
|
||||||
|
Message<T> Create(const T data, const u32 client_id = 0) {
|
||||||
|
boost::crc_32_type crc;
|
||||||
|
Header header{
|
||||||
|
CLIENT_MAGIC, PROTOCOL_VERSION, sizeof(T) + sizeof(Type), 0, client_id, GetMessageType<T>(),
|
||||||
|
};
|
||||||
|
Message<T> message{header, data};
|
||||||
|
crc.process_bytes(&message, sizeof(Message<T>));
|
||||||
|
message.header.crc = crc.checksum();
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
} // namespace Request
|
||||||
|
|
||||||
|
namespace Response {
|
||||||
|
|
||||||
|
struct Version {
|
||||||
|
u16_le version;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(Version) == 2, "UDP Response Version struct has wrong size");
|
||||||
|
static_assert(std::is_trivially_copyable_v<Version>,
|
||||||
|
"UDP Response Version is not trivially copyable");
|
||||||
|
|
||||||
|
struct PortInfo {
|
||||||
|
u8 id;
|
||||||
|
u8 state;
|
||||||
|
u8 model;
|
||||||
|
u8 connection_type;
|
||||||
|
MacAddress mac;
|
||||||
|
u8 battery;
|
||||||
|
u8 is_pad_active;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(PortInfo) == 12, "UDP Response PortInfo struct has wrong size");
|
||||||
|
static_assert(std::is_trivially_copyable_v<PortInfo>,
|
||||||
|
"UDP Response PortInfo is not trivially copyable");
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
struct PadData {
|
||||||
|
PortInfo info;
|
||||||
|
u32_le packet_counter;
|
||||||
|
|
||||||
|
u16_le digital_button;
|
||||||
|
// The following union isn't trivially copyable but we don't use this input anyway.
|
||||||
|
// union DigitalButton {
|
||||||
|
// u16_le button;
|
||||||
|
// BitField<0, 1, u16> button_1; // Share
|
||||||
|
// BitField<1, 1, u16> button_2; // L3
|
||||||
|
// BitField<2, 1, u16> button_3; // R3
|
||||||
|
// BitField<3, 1, u16> button_4; // Options
|
||||||
|
// BitField<4, 1, u16> button_5; // Up
|
||||||
|
// BitField<5, 1, u16> button_6; // Right
|
||||||
|
// BitField<6, 1, u16> button_7; // Down
|
||||||
|
// BitField<7, 1, u16> button_8; // Left
|
||||||
|
// BitField<8, 1, u16> button_9; // L2
|
||||||
|
// BitField<9, 1, u16> button_10; // R2
|
||||||
|
// BitField<10, 1, u16> button_11; // L1
|
||||||
|
// BitField<11, 1, u16> button_12; // R1
|
||||||
|
// BitField<12, 1, u16> button_13; // Triangle
|
||||||
|
// BitField<13, 1, u16> button_14; // Circle
|
||||||
|
// BitField<14, 1, u16> button_15; // Cross
|
||||||
|
// BitField<15, 1, u16> button_16; // Square
|
||||||
|
// } digital_button;
|
||||||
|
|
||||||
|
u8 home;
|
||||||
|
/// If the device supports a "click" on the touchpad, this will change to 1 when a click happens
|
||||||
|
u8 touch_hard_press;
|
||||||
|
u8 left_stick_x;
|
||||||
|
u8 left_stick_y;
|
||||||
|
u8 right_stick_x;
|
||||||
|
u8 right_stick_y;
|
||||||
|
|
||||||
|
struct AnalogButton {
|
||||||
|
u8 button_8;
|
||||||
|
u8 button_7;
|
||||||
|
u8 button_6;
|
||||||
|
u8 button_5;
|
||||||
|
u8 button_12;
|
||||||
|
u8 button_11;
|
||||||
|
u8 button_10;
|
||||||
|
u8 button_9;
|
||||||
|
u8 button_16;
|
||||||
|
u8 button_15;
|
||||||
|
u8 button_14;
|
||||||
|
u8 button_13;
|
||||||
|
} analog_button;
|
||||||
|
|
||||||
|
struct TouchPad {
|
||||||
|
u8 is_active;
|
||||||
|
u8 id;
|
||||||
|
u16_le x;
|
||||||
|
u16_le y;
|
||||||
|
} touch_1, touch_2;
|
||||||
|
|
||||||
|
u64_le motion_timestamp;
|
||||||
|
|
||||||
|
struct Accelerometer {
|
||||||
|
float x;
|
||||||
|
float y;
|
||||||
|
float z;
|
||||||
|
} accel;
|
||||||
|
|
||||||
|
struct Gyroscope {
|
||||||
|
float pitch;
|
||||||
|
float yaw;
|
||||||
|
float roll;
|
||||||
|
} gyro;
|
||||||
|
};
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
static_assert(sizeof(PadData) == 80, "UDP Response PadData struct has wrong size ");
|
||||||
|
static_assert(std::is_trivially_copyable_v<PadData>,
|
||||||
|
"UDP Response PadData is not trivially copyable");
|
||||||
|
|
||||||
|
static_assert(sizeof(Message<PadData>) == MAX_PACKET_SIZE,
|
||||||
|
"UDP MAX_PACKET_SIZE is no longer larger than Message<PadData>");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Response Message from the data
|
||||||
|
* @param data array of bytes sent from the server
|
||||||
|
* @return boost::none if it failed to parse or Type if it succeeded. The client can then safely
|
||||||
|
* copy the data into the appropriate struct for that Type
|
||||||
|
*/
|
||||||
|
std::optional<Type> Validate(u8* data, std::size_t size);
|
||||||
|
|
||||||
|
} // namespace Response
|
||||||
|
|
||||||
|
template <>
|
||||||
|
constexpr Type GetMessageType<Request::Version>() {
|
||||||
|
return Type::Version;
|
||||||
|
}
|
||||||
|
template <>
|
||||||
|
constexpr Type GetMessageType<Request::PortInfo>() {
|
||||||
|
return Type::PortInfo;
|
||||||
|
}
|
||||||
|
template <>
|
||||||
|
constexpr Type GetMessageType<Request::PadData>() {
|
||||||
|
return Type::PadData;
|
||||||
|
}
|
||||||
|
template <>
|
||||||
|
constexpr Type GetMessageType<Response::Version>() {
|
||||||
|
return Type::Version;
|
||||||
|
}
|
||||||
|
template <>
|
||||||
|
constexpr Type GetMessageType<Response::PortInfo>() {
|
||||||
|
return Type::PortInfo;
|
||||||
|
}
|
||||||
|
template <>
|
||||||
|
constexpr Type GetMessageType<Response::PadData>() {
|
||||||
|
return Type::PadData;
|
||||||
|
}
|
||||||
|
} // namespace InputCommon::CemuhookUDP
|
96
src/input_common/udp/udp.cpp
Normal file
96
src/input_common/udp/udp.cpp
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
// 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/param_package.h"
|
||||||
|
#include "core/frontend/input.h"
|
||||||
|
#include "core/settings.h"
|
||||||
|
#include "input_common/udp/client.h"
|
||||||
|
#include "input_common/udp/udp.h"
|
||||||
|
|
||||||
|
namespace InputCommon::CemuhookUDP {
|
||||||
|
|
||||||
|
class UDPTouchDevice final : public Input::TouchDevice {
|
||||||
|
public:
|
||||||
|
explicit UDPTouchDevice(std::shared_ptr<DeviceStatus> status_) : status(std::move(status_)) {}
|
||||||
|
std::tuple<float, float, bool> GetStatus() const {
|
||||||
|
std::lock_guard guard(status->update_mutex);
|
||||||
|
return status->touch_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<DeviceStatus> status;
|
||||||
|
};
|
||||||
|
|
||||||
|
class UDPMotionDevice final : public Input::MotionDevice {
|
||||||
|
public:
|
||||||
|
explicit UDPMotionDevice(std::shared_ptr<DeviceStatus> status_) : status(std::move(status_)) {}
|
||||||
|
std::tuple<Common::Vec3<float>, Common::Vec3<float>> GetStatus() const {
|
||||||
|
std::lock_guard guard(status->update_mutex);
|
||||||
|
return status->motion_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<DeviceStatus> status;
|
||||||
|
};
|
||||||
|
|
||||||
|
class UDPTouchFactory final : public Input::Factory<Input::TouchDevice> {
|
||||||
|
public:
|
||||||
|
explicit UDPTouchFactory(std::shared_ptr<DeviceStatus> status_) : status(std::move(status_)) {}
|
||||||
|
|
||||||
|
std::unique_ptr<Input::TouchDevice> Create(const Common::ParamPackage& params) override {
|
||||||
|
{
|
||||||
|
std::lock_guard guard(status->update_mutex);
|
||||||
|
status->touch_calibration.emplace();
|
||||||
|
// These default values work well for DS4 but probably not other touch inputs
|
||||||
|
status->touch_calibration->min_x = params.Get("min_x", 100);
|
||||||
|
status->touch_calibration->min_y = params.Get("min_y", 50);
|
||||||
|
status->touch_calibration->max_x = params.Get("max_x", 1800);
|
||||||
|
status->touch_calibration->max_y = params.Get("max_y", 850);
|
||||||
|
}
|
||||||
|
return std::make_unique<UDPTouchDevice>(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<DeviceStatus> status;
|
||||||
|
};
|
||||||
|
|
||||||
|
class UDPMotionFactory final : public Input::Factory<Input::MotionDevice> {
|
||||||
|
public:
|
||||||
|
explicit UDPMotionFactory(std::shared_ptr<DeviceStatus> status_) : status(std::move(status_)) {}
|
||||||
|
|
||||||
|
std::unique_ptr<Input::MotionDevice> Create(const Common::ParamPackage& params) override {
|
||||||
|
return std::make_unique<UDPMotionDevice>(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<DeviceStatus> status;
|
||||||
|
};
|
||||||
|
|
||||||
|
State::State() {
|
||||||
|
auto status = std::make_shared<DeviceStatus>();
|
||||||
|
client =
|
||||||
|
std::make_unique<Client>(status, Settings::values.udp_input_address,
|
||||||
|
Settings::values.udp_input_port, Settings::values.udp_pad_index);
|
||||||
|
|
||||||
|
Input::RegisterFactory<Input::TouchDevice>("cemuhookudp",
|
||||||
|
std::make_shared<UDPTouchFactory>(status));
|
||||||
|
Input::RegisterFactory<Input::MotionDevice>("cemuhookudp",
|
||||||
|
std::make_shared<UDPMotionFactory>(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
State::~State() {
|
||||||
|
Input::UnregisterFactory<Input::TouchDevice>("cemuhookudp");
|
||||||
|
Input::UnregisterFactory<Input::MotionDevice>("cemuhookudp");
|
||||||
|
}
|
||||||
|
|
||||||
|
void State::ReloadUDPClient() {
|
||||||
|
client->ReloadSocket(Settings::values.udp_input_address, Settings::values.udp_input_port,
|
||||||
|
Settings::values.udp_pad_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<State> Init() {
|
||||||
|
return std::make_unique<State>();
|
||||||
|
}
|
||||||
|
} // namespace InputCommon::CemuhookUDP
|
27
src/input_common/udp/udp.h
Normal file
27
src/input_common/udp/udp.h
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include "input_common/main.h"
|
||||||
|
#include "input_common/udp/client.h"
|
||||||
|
|
||||||
|
namespace InputCommon::CemuhookUDP {
|
||||||
|
|
||||||
|
class UDPTouchDevice;
|
||||||
|
class UDPMotionDevice;
|
||||||
|
|
||||||
|
class State {
|
||||||
|
public:
|
||||||
|
State();
|
||||||
|
~State();
|
||||||
|
void ReloadUDPClient();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<Client> client;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unique_ptr<State> Init();
|
||||||
|
|
||||||
|
} // namespace InputCommon::CemuhookUDP
|
|
@ -10,6 +10,7 @@
|
||||||
#include "core/hle/service/acc/profile_manager.h"
|
#include "core/hle/service/acc/profile_manager.h"
|
||||||
#include "core/hle/service/hid/controllers/npad.h"
|
#include "core/hle/service/hid/controllers/npad.h"
|
||||||
#include "input_common/main.h"
|
#include "input_common/main.h"
|
||||||
|
#include "input_common/udp/client.h"
|
||||||
#include "yuzu/configuration/config.h"
|
#include "yuzu/configuration/config.h"
|
||||||
#include "yuzu/uisettings.h"
|
#include "yuzu/uisettings.h"
|
||||||
|
|
||||||
|
@ -429,6 +430,16 @@ void Config::ReadControlValues() {
|
||||||
QStringLiteral("engine:motion_emu,update_period:100,sensitivity:0.01"))
|
QStringLiteral("engine:motion_emu,update_period:100,sensitivity:0.01"))
|
||||||
.toString()
|
.toString()
|
||||||
.toStdString();
|
.toStdString();
|
||||||
|
Settings::values.udp_input_address =
|
||||||
|
ReadSetting(QStringLiteral("udp_input_address"),
|
||||||
|
QString::fromUtf8(InputCommon::CemuhookUDP::DEFAULT_ADDR))
|
||||||
|
.toString()
|
||||||
|
.toStdString();
|
||||||
|
Settings::values.udp_input_port = static_cast<u16>(
|
||||||
|
ReadSetting(QStringLiteral("udp_input_port"), InputCommon::CemuhookUDP::DEFAULT_PORT)
|
||||||
|
.toInt());
|
||||||
|
Settings::values.udp_pad_index =
|
||||||
|
static_cast<u8>(ReadSetting(QStringLiteral("udp_pad_index"), 0).toUInt());
|
||||||
|
|
||||||
qt_config->endGroup();
|
qt_config->endGroup();
|
||||||
}
|
}
|
||||||
|
@ -911,6 +922,12 @@ void Config::SaveControlValues() {
|
||||||
QString::fromStdString(Settings::values.motion_device),
|
QString::fromStdString(Settings::values.motion_device),
|
||||||
QStringLiteral("engine:motion_emu,update_period:100,sensitivity:0.01"));
|
QStringLiteral("engine:motion_emu,update_period:100,sensitivity:0.01"));
|
||||||
WriteSetting(QStringLiteral("keyboard_enabled"), Settings::values.keyboard_enabled, false);
|
WriteSetting(QStringLiteral("keyboard_enabled"), Settings::values.keyboard_enabled, false);
|
||||||
|
WriteSetting(QStringLiteral("udp_input_address"),
|
||||||
|
QString::fromStdString(Settings::values.udp_input_address),
|
||||||
|
QString::fromUtf8(InputCommon::CemuhookUDP::DEFAULT_ADDR));
|
||||||
|
WriteSetting(QStringLiteral("udp_input_port"), Settings::values.udp_input_port,
|
||||||
|
InputCommon::CemuhookUDP::DEFAULT_PORT);
|
||||||
|
WriteSetting(QStringLiteral("udp_pad_index"), Settings::values.udp_pad_index, 0);
|
||||||
|
|
||||||
qt_config->endGroup();
|
qt_config->endGroup();
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
#include "core/hle/service/acc/profile_manager.h"
|
#include "core/hle/service/acc/profile_manager.h"
|
||||||
#include "core/settings.h"
|
#include "core/settings.h"
|
||||||
#include "input_common/main.h"
|
#include "input_common/main.h"
|
||||||
|
#include "input_common/udp/client.h"
|
||||||
#include "yuzu_cmd/config.h"
|
#include "yuzu_cmd/config.h"
|
||||||
#include "yuzu_cmd/default_ini.h"
|
#include "yuzu_cmd/default_ini.h"
|
||||||
|
|
||||||
|
@ -297,6 +298,10 @@ void Config::ReadValues() {
|
||||||
sdl2_config->GetInteger("ControlsGeneral", "touch_diameter_x", 15);
|
sdl2_config->GetInteger("ControlsGeneral", "touch_diameter_x", 15);
|
||||||
Settings::values.touchscreen.diameter_y =
|
Settings::values.touchscreen.diameter_y =
|
||||||
sdl2_config->GetInteger("ControlsGeneral", "touch_diameter_y", 15);
|
sdl2_config->GetInteger("ControlsGeneral", "touch_diameter_y", 15);
|
||||||
|
Settings::values.udp_input_address = sdl2_config->GetString(
|
||||||
|
"Controls", "udp_input_address", InputCommon::CemuhookUDP::DEFAULT_ADDR);
|
||||||
|
Settings::values.udp_input_port = static_cast<u16>(sdl2_config->GetInteger(
|
||||||
|
"Controls", "udp_input_port", InputCommon::CemuhookUDP::DEFAULT_PORT));
|
||||||
|
|
||||||
std::transform(keyboard_keys.begin(), keyboard_keys.end(),
|
std::transform(keyboard_keys.begin(), keyboard_keys.end(),
|
||||||
Settings::values.keyboard_keys.begin(), InputCommon::GenerateKeyboardParam);
|
Settings::values.keyboard_keys.begin(), InputCommon::GenerateKeyboardParam);
|
||||||
|
|
|
@ -69,12 +69,29 @@ rstick=
|
||||||
# - "motion_emu" (default) for emulating motion input from mouse input. Required parameters:
|
# - "motion_emu" (default) for emulating motion input from mouse input. Required parameters:
|
||||||
# - "update_period": update period in milliseconds (default to 100)
|
# - "update_period": update period in milliseconds (default to 100)
|
||||||
# - "sensitivity": the coefficient converting mouse movement to tilting angle (default to 0.01)
|
# - "sensitivity": the coefficient converting mouse movement to tilting angle (default to 0.01)
|
||||||
|
# - "cemuhookudp" reads motion input from a udp server that uses cemuhook's udp protocol
|
||||||
motion_device=
|
motion_device=
|
||||||
|
|
||||||
# for touch input, the following devices are available:
|
# for touch input, the following devices are available:
|
||||||
# - "emu_window" (default) for emulating touch input from mouse input to the emulation window. No parameters required
|
# - "emu_window" (default) for emulating touch input from mouse input to the emulation window. No parameters required
|
||||||
|
# - "cemuhookudp" reads touch input from a udp server that uses cemuhook's udp protocol
|
||||||
|
# - "min_x", "min_y", "max_x", "max_y": defines the udp device's touch screen coordinate system
|
||||||
touch_device=
|
touch_device=
|
||||||
|
|
||||||
|
# Most desktop operating systems do not expose a way to poll the motion state of the controllers
|
||||||
|
# so as a way around it, cemuhook created a udp client/server protocol to broadcast the data directly
|
||||||
|
# from a controller device to the client program. Citra has a client that can connect and read
|
||||||
|
# from any cemuhook compatible motion program.
|
||||||
|
|
||||||
|
# IPv4 address of the udp input server (Default "127.0.0.1")
|
||||||
|
udp_input_address=
|
||||||
|
|
||||||
|
# Port of the udp input server. (Default 26760)
|
||||||
|
udp_input_port=
|
||||||
|
|
||||||
|
# The pad to request data on. Should be between 0 (Pad 1) and 3 (Pad 4). (Default 0)
|
||||||
|
udp_pad_index=
|
||||||
|
|
||||||
[Core]
|
[Core]
|
||||||
# Whether to use multi-core for CPU emulation
|
# Whether to use multi-core for CPU emulation
|
||||||
# 0 (default): Disabled, 1: Enabled
|
# 0 (default): Disabled, 1: Enabled
|
||||||
|
|
Loading…
Reference in a new issue