input_common/tas: Base playback & recording system
The base playback system supports up to 8 controllers (specified by `PLAYER_NUMBER` in `tas_input.h`), which all change their inputs simulataneously when `TAS::UpdateThread` is called. The recording system uses the controller debugger to read the state of the first controller and forwards that data to the TASing system for recording. Currently, this process sadly is not frame-perfect and pixel-accurate. Co-authored-by: Naii-the-Baf <sfabian200@gmail.com> Co-authored-by: Narr-the-Reg <juangerman-13@hotmail.com>
This commit is contained in:
parent
35f46fc079
commit
b42c3ce21d
|
@ -14,6 +14,7 @@
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <input_common/main.h>
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "common/settings_input.h"
|
#include "common/settings_input.h"
|
||||||
|
|
||||||
|
@ -499,6 +500,7 @@ struct Values {
|
||||||
|
|
||||||
// Controls
|
// Controls
|
||||||
InputSetting<std::array<PlayerInput, 10>> players;
|
InputSetting<std::array<PlayerInput, 10>> players;
|
||||||
|
std::shared_ptr<InputCommon::InputSubsystem> inputSubsystem = NULL;
|
||||||
|
|
||||||
Setting<bool> use_docked_mode{true, "use_docked_mode"};
|
Setting<bool> use_docked_mode{true, "use_docked_mode"};
|
||||||
|
|
||||||
|
@ -512,9 +514,14 @@ struct Values {
|
||||||
"motion_device"};
|
"motion_device"};
|
||||||
BasicSetting<std::string> udp_input_servers{"127.0.0.1:26760", "udp_input_servers"};
|
BasicSetting<std::string> udp_input_servers{"127.0.0.1:26760", "udp_input_servers"};
|
||||||
|
|
||||||
|
BasicSetting<bool> tas_enable{false, "tas_enable"};
|
||||||
|
BasicSetting<bool> tas_reset{ false, "tas_reset" };
|
||||||
|
BasicSetting<bool> tas_record{ false, "tas_record" };
|
||||||
|
|
||||||
BasicSetting<bool> mouse_panning{false, "mouse_panning"};
|
BasicSetting<bool> mouse_panning{false, "mouse_panning"};
|
||||||
BasicRangedSetting<u8> mouse_panning_sensitivity{10, 1, 100, "mouse_panning_sensitivity"};
|
BasicRangedSetting<u8> mouse_panning_sensitivity{10, 1, 100, "mouse_panning_sensitivity"};
|
||||||
BasicSetting<bool> mouse_enabled{false, "mouse_enabled"};
|
BasicSetting<bool> mouse_enabled{false, "mouse_enabled"};
|
||||||
|
|
||||||
std::string mouse_device;
|
std::string mouse_device;
|
||||||
MouseButtonsRaw mouse_buttons;
|
MouseButtonsRaw mouse_buttons;
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,10 @@ add_library(input_common STATIC
|
||||||
mouse/mouse_poller.h
|
mouse/mouse_poller.h
|
||||||
sdl/sdl.cpp
|
sdl/sdl.cpp
|
||||||
sdl/sdl.h
|
sdl/sdl.h
|
||||||
|
tas/tas_input.cpp
|
||||||
|
tas/tas_input.h
|
||||||
|
tas/tas_poller.cpp
|
||||||
|
tas/tas_poller.h
|
||||||
udp/client.cpp
|
udp/client.cpp
|
||||||
udp/client.h
|
udp/client.h
|
||||||
udp/protocol.cpp
|
udp/protocol.cpp
|
||||||
|
|
|
@ -13,6 +13,8 @@
|
||||||
#include "input_common/motion_from_button.h"
|
#include "input_common/motion_from_button.h"
|
||||||
#include "input_common/mouse/mouse_input.h"
|
#include "input_common/mouse/mouse_input.h"
|
||||||
#include "input_common/mouse/mouse_poller.h"
|
#include "input_common/mouse/mouse_poller.h"
|
||||||
|
#include "input_common/tas/tas_input.h"
|
||||||
|
#include "input_common/tas/tas_poller.h"
|
||||||
#include "input_common/touch_from_button.h"
|
#include "input_common/touch_from_button.h"
|
||||||
#include "input_common/udp/client.h"
|
#include "input_common/udp/client.h"
|
||||||
#include "input_common/udp/udp.h"
|
#include "input_common/udp/udp.h"
|
||||||
|
@ -60,6 +62,12 @@ struct InputSubsystem::Impl {
|
||||||
Input::RegisterFactory<Input::MotionDevice>("mouse", mousemotion);
|
Input::RegisterFactory<Input::MotionDevice>("mouse", mousemotion);
|
||||||
mousetouch = std::make_shared<MouseTouchFactory>(mouse);
|
mousetouch = std::make_shared<MouseTouchFactory>(mouse);
|
||||||
Input::RegisterFactory<Input::TouchDevice>("mouse", mousetouch);
|
Input::RegisterFactory<Input::TouchDevice>("mouse", mousetouch);
|
||||||
|
|
||||||
|
tas = std::make_shared<TasInput::Tas>();
|
||||||
|
tasbuttons = std::make_shared<TasButtonFactory>(tas);
|
||||||
|
Input::RegisterFactory<Input::ButtonDevice>("tas", tasbuttons);
|
||||||
|
tasanalog = std::make_shared<TasAnalogFactory>(tas);
|
||||||
|
Input::RegisterFactory<Input::AnalogDevice>("tas", tasanalog);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Shutdown() {
|
void Shutdown() {
|
||||||
|
@ -94,12 +102,19 @@ struct InputSubsystem::Impl {
|
||||||
mouseanalog.reset();
|
mouseanalog.reset();
|
||||||
mousemotion.reset();
|
mousemotion.reset();
|
||||||
mousetouch.reset();
|
mousetouch.reset();
|
||||||
|
|
||||||
|
Input::UnregisterFactory<Input::ButtonDevice>("tas");
|
||||||
|
Input::UnregisterFactory<Input::AnalogDevice>("tas");
|
||||||
|
|
||||||
|
tasbuttons.reset();
|
||||||
|
tasanalog.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const {
|
[[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const {
|
||||||
std::vector<Common::ParamPackage> devices = {
|
std::vector<Common::ParamPackage> devices = {
|
||||||
Common::ParamPackage{{"display", "Any"}, {"class", "any"}},
|
Common::ParamPackage{{"display", "Any"}, {"class", "any"}},
|
||||||
Common::ParamPackage{{"display", "Keyboard/Mouse"}, {"class", "keyboard"}},
|
Common::ParamPackage{{"display", "Keyboard/Mouse"}, {"class", "keyboard"}},
|
||||||
|
Common::ParamPackage{{"display", "TAS"}, {"class", "tas"}},
|
||||||
};
|
};
|
||||||
#ifdef HAVE_SDL2
|
#ifdef HAVE_SDL2
|
||||||
auto sdl_devices = sdl->GetInputDevices();
|
auto sdl_devices = sdl->GetInputDevices();
|
||||||
|
@ -120,6 +135,9 @@ struct InputSubsystem::Impl {
|
||||||
if (params.Get("class", "") == "gcpad") {
|
if (params.Get("class", "") == "gcpad") {
|
||||||
return gcadapter->GetAnalogMappingForDevice(params);
|
return gcadapter->GetAnalogMappingForDevice(params);
|
||||||
}
|
}
|
||||||
|
if (params.Get("class", "") == "tas") {
|
||||||
|
return tas->GetAnalogMappingForDevice(params);
|
||||||
|
}
|
||||||
#ifdef HAVE_SDL2
|
#ifdef HAVE_SDL2
|
||||||
if (params.Get("class", "") == "sdl") {
|
if (params.Get("class", "") == "sdl") {
|
||||||
return sdl->GetAnalogMappingForDevice(params);
|
return sdl->GetAnalogMappingForDevice(params);
|
||||||
|
@ -136,6 +154,9 @@ struct InputSubsystem::Impl {
|
||||||
if (params.Get("class", "") == "gcpad") {
|
if (params.Get("class", "") == "gcpad") {
|
||||||
return gcadapter->GetButtonMappingForDevice(params);
|
return gcadapter->GetButtonMappingForDevice(params);
|
||||||
}
|
}
|
||||||
|
if (params.Get("class", "") == "tas") {
|
||||||
|
return tas->GetButtonMappingForDevice(params);
|
||||||
|
}
|
||||||
#ifdef HAVE_SDL2
|
#ifdef HAVE_SDL2
|
||||||
if (params.Get("class", "") == "sdl") {
|
if (params.Get("class", "") == "sdl") {
|
||||||
return sdl->GetButtonMappingForDevice(params);
|
return sdl->GetButtonMappingForDevice(params);
|
||||||
|
@ -174,9 +195,12 @@ struct InputSubsystem::Impl {
|
||||||
std::shared_ptr<MouseAnalogFactory> mouseanalog;
|
std::shared_ptr<MouseAnalogFactory> mouseanalog;
|
||||||
std::shared_ptr<MouseMotionFactory> mousemotion;
|
std::shared_ptr<MouseMotionFactory> mousemotion;
|
||||||
std::shared_ptr<MouseTouchFactory> mousetouch;
|
std::shared_ptr<MouseTouchFactory> mousetouch;
|
||||||
|
std::shared_ptr<TasButtonFactory> tasbuttons;
|
||||||
|
std::shared_ptr<TasAnalogFactory> tasanalog;
|
||||||
std::shared_ptr<CemuhookUDP::Client> udp;
|
std::shared_ptr<CemuhookUDP::Client> udp;
|
||||||
std::shared_ptr<GCAdapter::Adapter> gcadapter;
|
std::shared_ptr<GCAdapter::Adapter> gcadapter;
|
||||||
std::shared_ptr<MouseInput::Mouse> mouse;
|
std::shared_ptr<MouseInput::Mouse> mouse;
|
||||||
|
std::shared_ptr<TasInput::Tas> tas;
|
||||||
};
|
};
|
||||||
|
|
||||||
InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {}
|
InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {}
|
||||||
|
@ -207,6 +231,14 @@ const MouseInput::Mouse* InputSubsystem::GetMouse() const {
|
||||||
return impl->mouse.get();
|
return impl->mouse.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TasInput::Tas* InputSubsystem::GetTas() {
|
||||||
|
return impl->tas.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
const TasInput::Tas* InputSubsystem::GetTas() const {
|
||||||
|
return impl->tas.get();
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<Common::ParamPackage> InputSubsystem::GetInputDevices() const {
|
std::vector<Common::ParamPackage> InputSubsystem::GetInputDevices() const {
|
||||||
return impl->GetInputDevices();
|
return impl->GetInputDevices();
|
||||||
}
|
}
|
||||||
|
@ -287,6 +319,22 @@ const MouseTouchFactory* InputSubsystem::GetMouseTouch() const {
|
||||||
return impl->mousetouch.get();
|
return impl->mousetouch.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TasButtonFactory* InputSubsystem::GetTasButtons() {
|
||||||
|
return impl->tasbuttons.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
const TasButtonFactory* InputSubsystem::GetTasButtons() const {
|
||||||
|
return impl->tasbuttons.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
TasAnalogFactory* InputSubsystem::GetTasAnalogs() {
|
||||||
|
return impl->tasanalog.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
const TasAnalogFactory* InputSubsystem::GetTasAnalogs() const {
|
||||||
|
return impl->tasanalog.get();
|
||||||
|
}
|
||||||
|
|
||||||
void InputSubsystem::ReloadInputDevices() {
|
void InputSubsystem::ReloadInputDevices() {
|
||||||
if (!impl->udp) {
|
if (!impl->udp) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -29,6 +29,10 @@ namespace MouseInput {
|
||||||
class Mouse;
|
class Mouse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace TasInput {
|
||||||
|
class Tas;
|
||||||
|
}
|
||||||
|
|
||||||
namespace InputCommon {
|
namespace InputCommon {
|
||||||
namespace Polling {
|
namespace Polling {
|
||||||
|
|
||||||
|
@ -64,6 +68,8 @@ class MouseButtonFactory;
|
||||||
class MouseAnalogFactory;
|
class MouseAnalogFactory;
|
||||||
class MouseMotionFactory;
|
class MouseMotionFactory;
|
||||||
class MouseTouchFactory;
|
class MouseTouchFactory;
|
||||||
|
class TasButtonFactory;
|
||||||
|
class TasAnalogFactory;
|
||||||
class Keyboard;
|
class Keyboard;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -103,6 +109,11 @@ public:
|
||||||
/// Retrieves the underlying mouse device.
|
/// Retrieves the underlying mouse device.
|
||||||
[[nodiscard]] const MouseInput::Mouse* GetMouse() const;
|
[[nodiscard]] const MouseInput::Mouse* GetMouse() const;
|
||||||
|
|
||||||
|
/// Retrieves the underlying tas device.
|
||||||
|
[[nodiscard]] TasInput::Tas* GetTas();
|
||||||
|
|
||||||
|
/// Retrieves the underlying tas device.
|
||||||
|
[[nodiscard]] const TasInput::Tas* GetTas() const;
|
||||||
/**
|
/**
|
||||||
* Returns all available input devices that this Factory can create a new device with.
|
* Returns all available input devices that this Factory can create a new device with.
|
||||||
* Each returned ParamPackage should have a `display` field used for display, a class field for
|
* Each returned ParamPackage should have a `display` field used for display, a class field for
|
||||||
|
@ -168,6 +179,18 @@ public:
|
||||||
/// Retrieves the underlying udp touch handler.
|
/// Retrieves the underlying udp touch handler.
|
||||||
[[nodiscard]] const MouseTouchFactory* GetMouseTouch() const;
|
[[nodiscard]] const MouseTouchFactory* GetMouseTouch() const;
|
||||||
|
|
||||||
|
/// Retrieves the underlying tas button handler.
|
||||||
|
[[nodiscard]] TasButtonFactory* GetTasButtons();
|
||||||
|
|
||||||
|
/// Retrieves the underlying tas button handler.
|
||||||
|
[[nodiscard]] const TasButtonFactory* GetTasButtons() const;
|
||||||
|
|
||||||
|
/// Retrieves the underlying tas touch handler.
|
||||||
|
[[nodiscard]] TasAnalogFactory* GetTasAnalogs();
|
||||||
|
|
||||||
|
/// Retrieves the underlying tas touch handler.
|
||||||
|
[[nodiscard]] const TasAnalogFactory* GetTasAnalogs() const;
|
||||||
|
|
||||||
/// Reloads the input devices
|
/// Reloads the input devices
|
||||||
void ReloadInputDevices();
|
void ReloadInputDevices();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,340 @@
|
||||||
|
// Copyright 2021 yuzu Emulator Project
|
||||||
|
// Licensed under GPLv2+
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstring>
|
||||||
|
#include <functional>
|
||||||
|
#include <random>
|
||||||
|
#include <regex>
|
||||||
|
#include <thread>
|
||||||
|
#include <boost/asio.hpp>
|
||||||
|
|
||||||
|
#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/tas/tas_input.h"
|
||||||
|
|
||||||
|
namespace TasInput {
|
||||||
|
|
||||||
|
Tas::Tas() {
|
||||||
|
LoadTasFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
Tas::~Tas() {
|
||||||
|
update_thread_running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tas::RefreshTasFile() {
|
||||||
|
refresh_tas_fle = true;
|
||||||
|
}
|
||||||
|
void Tas::LoadTasFiles() {
|
||||||
|
scriptLength = 0;
|
||||||
|
for (int i = 0; i < PLAYER_NUMBER; i++) {
|
||||||
|
LoadTasFile(i);
|
||||||
|
if (newCommands[i].size() > scriptLength)
|
||||||
|
scriptLength = newCommands[i].size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void Tas::LoadTasFile(int playerIndex) {
|
||||||
|
LOG_DEBUG(Input, "LoadTasFile()");
|
||||||
|
if (!newCommands[playerIndex].empty()) {
|
||||||
|
newCommands[playerIndex].clear();
|
||||||
|
}
|
||||||
|
std::string file = Common::FS::ReadStringFromFile(
|
||||||
|
Common::FS::GetYuzuPathString(Common::FS::YuzuPath::TASFile) + "script0-" +
|
||||||
|
std::to_string(playerIndex + 1) + ".txt",
|
||||||
|
Common::FS::FileType::BinaryFile);
|
||||||
|
std::stringstream command_line(file);
|
||||||
|
std::string line;
|
||||||
|
int frameNo = 0;
|
||||||
|
TASCommand empty = {.buttons = 0, .l_axis = {0.f, 0.f}, .r_axis = {0.f, 0.f}};
|
||||||
|
while (std::getline(command_line, line, '\n')) {
|
||||||
|
if (line.empty())
|
||||||
|
continue;
|
||||||
|
LOG_DEBUG(Input, "Loading line: {}", line);
|
||||||
|
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 (frameNo < std::stoi(seglist.at(0))) {
|
||||||
|
newCommands[playerIndex].push_back(empty);
|
||||||
|
frameNo++;
|
||||||
|
}
|
||||||
|
|
||||||
|
TASCommand command = {
|
||||||
|
.buttons = ReadCommandButtons(seglist.at(1)),
|
||||||
|
.l_axis = ReadCommandAxis(seglist.at(2)),
|
||||||
|
.r_axis = ReadCommandAxis(seglist.at(3)),
|
||||||
|
};
|
||||||
|
newCommands[playerIndex].push_back(command);
|
||||||
|
frameNo++;
|
||||||
|
}
|
||||||
|
LOG_INFO(Input, "TAS file loaded! {} frames", frameNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tas::WriteTasFile() {
|
||||||
|
LOG_DEBUG(Input, "WriteTasFile()");
|
||||||
|
std::string output_text = "";
|
||||||
|
for (int frame = 0; frame < (signed)recordCommands.size(); frame++) {
|
||||||
|
if (!output_text.empty())
|
||||||
|
output_text += "\n";
|
||||||
|
TASCommand line = recordCommands.at(frame);
|
||||||
|
output_text += std::to_string(frame) + " " + WriteCommandButtons(line.buttons) + " " +
|
||||||
|
WriteCommandAxis(line.l_axis) + " " + WriteCommandAxis(line.r_axis);
|
||||||
|
}
|
||||||
|
size_t bytesWritten = Common::FS::WriteStringToFile(
|
||||||
|
Common::FS::GetYuzuPathString(Common::FS::YuzuPath::TASFile) + "record.txt",
|
||||||
|
Common::FS::FileType::TextFile, output_text);
|
||||||
|
if (bytesWritten == output_text.size())
|
||||||
|
LOG_INFO(Input, "TAS file written to file!");
|
||||||
|
else
|
||||||
|
LOG_ERROR(Input, "Writing the TAS-file has failed! {} / {} bytes written", bytesWritten,
|
||||||
|
output_text.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tas::RecordInput(u32 buttons, std::array<std::pair<float, float>, 2> axes) {
|
||||||
|
lastInput = {buttons, flipY(axes[0]), flipY(axes[1])};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<float, float> Tas::flipY(std::pair<float, float> old) const {
|
||||||
|
auto [x, y] = old;
|
||||||
|
return {x, -y};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Tas::GetStatusDescription() {
|
||||||
|
if (Settings::values.tas_record) {
|
||||||
|
return "Recording TAS: " + std::to_string(recordCommands.size());
|
||||||
|
}
|
||||||
|
if (Settings::values.tas_enable) {
|
||||||
|
return "Playing TAS: " + std::to_string(current_command) + "/" +
|
||||||
|
std::to_string(scriptLength);
|
||||||
|
}
|
||||||
|
return "TAS not running: " + std::to_string(current_command) + "/" +
|
||||||
|
std::to_string(scriptLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string debugButtons(u32 buttons) {
|
||||||
|
return "{ " + TasInput::Tas::buttonsToString(buttons) + " }";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string debugJoystick(float x, float y) {
|
||||||
|
return "[ " + std::to_string(x) + "," + std::to_string(y) + " ]";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string debugInput(TasData data) {
|
||||||
|
return "{ " + debugButtons(data.buttons) + " , " + debugJoystick(data.axis[0], data.axis[1]) +
|
||||||
|
" , " + debugJoystick(data.axis[2], data.axis[3]) + " }";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string debugInputs(std::array<TasData, PLAYER_NUMBER> arr) {
|
||||||
|
std::string returns = "[ ";
|
||||||
|
for (size_t i = 0; i < arr.size(); i++) {
|
||||||
|
returns += debugInput(arr[i]);
|
||||||
|
if (i != arr.size() - 1)
|
||||||
|
returns += " , ";
|
||||||
|
}
|
||||||
|
return returns + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tas::UpdateThread() {
|
||||||
|
if (update_thread_running) {
|
||||||
|
if (Settings::values.pauseTasOnLoad && Settings::values.cpuBoosted) {
|
||||||
|
for (int i = 0; i < PLAYER_NUMBER; i++) {
|
||||||
|
tas_data[i].buttons = 0;
|
||||||
|
tas_data[i].axis = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings::values.tas_record) {
|
||||||
|
recordCommands.push_back(lastInput);
|
||||||
|
}
|
||||||
|
if (!Settings::values.tas_record && !recordCommands.empty()) {
|
||||||
|
WriteTasFile();
|
||||||
|
Settings::values.tas_reset = true;
|
||||||
|
refresh_tas_fle = true;
|
||||||
|
recordCommands.clear();
|
||||||
|
}
|
||||||
|
if (Settings::values.tas_reset) {
|
||||||
|
current_command = 0;
|
||||||
|
if (refresh_tas_fle) {
|
||||||
|
LoadTasFiles();
|
||||||
|
refresh_tas_fle = false;
|
||||||
|
}
|
||||||
|
Settings::values.tas_reset = false;
|
||||||
|
LoadTasFiles();
|
||||||
|
LOG_DEBUG(Input, "tas_reset done");
|
||||||
|
}
|
||||||
|
if (Settings::values.tas_enable) {
|
||||||
|
if ((signed)current_command < scriptLength) {
|
||||||
|
LOG_INFO(Input, "Playing TAS {}/{}", current_command, scriptLength);
|
||||||
|
size_t frame = current_command++;
|
||||||
|
for (int i = 0; i < PLAYER_NUMBER; i++) {
|
||||||
|
if (frame < newCommands[i].size()) {
|
||||||
|
TASCommand command = newCommands[i][frame];
|
||||||
|
tas_data[i].buttons = command.buttons;
|
||||||
|
auto [l_axis_x, l_axis_y] = command.l_axis;
|
||||||
|
tas_data[i].axis[0] = l_axis_x;
|
||||||
|
tas_data[i].axis[1] = l_axis_y;
|
||||||
|
auto [r_axis_x, r_axis_y] = command.r_axis;
|
||||||
|
tas_data[i].axis[2] = r_axis_x;
|
||||||
|
tas_data[i].axis[3] = r_axis_y;
|
||||||
|
} else {
|
||||||
|
tas_data[i].buttons = 0;
|
||||||
|
tas_data[i].axis = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Settings::values.tas_enable = false;
|
||||||
|
current_command = 0;
|
||||||
|
for (int i = 0; i < PLAYER_NUMBER; i++) {
|
||||||
|
tas_data[i].buttons = 0;
|
||||||
|
tas_data[i].axis = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < PLAYER_NUMBER; i++) {
|
||||||
|
tas_data[i].buttons = 0;
|
||||||
|
tas_data[i].axis = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG_DEBUG(Input, "TAS inputs: {}", debugInputs(tas_data));
|
||||||
|
}
|
||||||
|
|
||||||
|
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.f;
|
||||||
|
const float y = std::stof(seglist.at(1)) / 32767.f;
|
||||||
|
|
||||||
|
return {x, y};
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 Tas::ReadCommandButtons(const std::string data) const {
|
||||||
|
std::stringstream button_text(data);
|
||||||
|
std::string line;
|
||||||
|
u32 buttons = 0;
|
||||||
|
while (std::getline(button_text, line, ';')) {
|
||||||
|
for (auto [text, tas_button] : text_to_tas_button) {
|
||||||
|
if (text == line) {
|
||||||
|
buttons |= static_cast<u32>(tas_button);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Tas::WriteCommandAxis(TasAnalog data) const {
|
||||||
|
auto [x, y] = data;
|
||||||
|
std::string line;
|
||||||
|
line += std::to_string(static_cast<int>(x * 32767));
|
||||||
|
line += ";";
|
||||||
|
line += std::to_string(static_cast<int>(y * 32767));
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Tas::WriteCommandButtons(u32 data) const {
|
||||||
|
if (data == 0)
|
||||||
|
return "NONE";
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
u32 index = 0;
|
||||||
|
while (data > 0) {
|
||||||
|
if ((data & 1) == 1) {
|
||||||
|
for (auto [text, tas_button] : text_to_tas_button) {
|
||||||
|
if (tas_button == static_cast<TasButton>(1 << index)) {
|
||||||
|
if (line.size() > 0)
|
||||||
|
line += ";";
|
||||||
|
line += text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
data >>= 1;
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputCommon::ButtonMapping Tas::GetButtonMappingForDevice(
|
||||||
|
const Common::ParamPackage& params) const {
|
||||||
|
// 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, TasButton>, 20>
|
||||||
|
switch_to_tas_button = {
|
||||||
|
std::pair{Settings::NativeButton::A, TasButton::BUTTON_A},
|
||||||
|
{Settings::NativeButton::B, TasButton::BUTTON_B},
|
||||||
|
{Settings::NativeButton::X, TasButton::BUTTON_X},
|
||||||
|
{Settings::NativeButton::Y, TasButton::BUTTON_Y},
|
||||||
|
{Settings::NativeButton::LStick, TasButton::STICK_L},
|
||||||
|
{Settings::NativeButton::RStick, TasButton::STICK_R},
|
||||||
|
{Settings::NativeButton::L, TasButton::TRIGGER_L},
|
||||||
|
{Settings::NativeButton::R, TasButton::TRIGGER_R},
|
||||||
|
{Settings::NativeButton::Plus, TasButton::BUTTON_PLUS},
|
||||||
|
{Settings::NativeButton::Minus, TasButton::BUTTON_MINUS},
|
||||||
|
{Settings::NativeButton::DLeft, TasButton::BUTTON_LEFT},
|
||||||
|
{Settings::NativeButton::DUp, TasButton::BUTTON_UP},
|
||||||
|
{Settings::NativeButton::DRight, TasButton::BUTTON_RIGHT},
|
||||||
|
{Settings::NativeButton::DDown, TasButton::BUTTON_DOWN},
|
||||||
|
{Settings::NativeButton::SL, TasButton::BUTTON_SL},
|
||||||
|
{Settings::NativeButton::SR, TasButton::BUTTON_SR},
|
||||||
|
{Settings::NativeButton::Screenshot, TasButton::BUTTON_CAPTURE},
|
||||||
|
{Settings::NativeButton::Home, TasButton::BUTTON_HOME},
|
||||||
|
{Settings::NativeButton::ZL, TasButton::TRIGGER_ZL},
|
||||||
|
{Settings::NativeButton::ZR, TasButton::TRIGGER_ZR},
|
||||||
|
};
|
||||||
|
|
||||||
|
InputCommon::ButtonMapping mapping{};
|
||||||
|
for (const auto& [switch_button, tas_button] : switch_to_tas_button) {
|
||||||
|
Common::ParamPackage button_params({{"engine", "tas"}});
|
||||||
|
button_params.Set("pad", params.Get("pad", 0));
|
||||||
|
button_params.Set("button", static_cast<int>(tas_button));
|
||||||
|
mapping.insert_or_assign(switch_button, std::move(button_params));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputCommon::AnalogMapping Tas::GetAnalogMappingForDevice(
|
||||||
|
const Common::ParamPackage& params) const {
|
||||||
|
|
||||||
|
InputCommon::AnalogMapping mapping = {};
|
||||||
|
Common::ParamPackage left_analog_params;
|
||||||
|
left_analog_params.Set("engine", "tas");
|
||||||
|
left_analog_params.Set("pad", params.Get("pad", 0));
|
||||||
|
left_analog_params.Set("axis_x", static_cast<int>(TasAxes::StickX));
|
||||||
|
left_analog_params.Set("axis_y", static_cast<int>(TasAxes::StickY));
|
||||||
|
mapping.insert_or_assign(Settings::NativeAnalog::LStick, std::move(left_analog_params));
|
||||||
|
Common::ParamPackage right_analog_params;
|
||||||
|
right_analog_params.Set("engine", "tas");
|
||||||
|
right_analog_params.Set("pad", params.Get("pad", 0));
|
||||||
|
right_analog_params.Set("axis_x", static_cast<int>(TasAxes::SubstickX));
|
||||||
|
right_analog_params.Set("axis_y", static_cast<int>(TasAxes::SubstickY));
|
||||||
|
mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params));
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TasData& Tas::GetTasState(std::size_t pad) const {
|
||||||
|
return tas_data[pad];
|
||||||
|
}
|
||||||
|
} // namespace TasInput
|
|
@ -0,0 +1,163 @@
|
||||||
|
// Copyright 2020 yuzu Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "core/frontend/input.h"
|
||||||
|
#include "input_common/main.h"
|
||||||
|
|
||||||
|
#define PLAYER_NUMBER 8
|
||||||
|
|
||||||
|
namespace TasInput {
|
||||||
|
|
||||||
|
using TasAnalog = std::tuple<float, float>;
|
||||||
|
|
||||||
|
enum class TasButton : u32 {
|
||||||
|
BUTTON_A = 0x000001,
|
||||||
|
BUTTON_B = 0x000002,
|
||||||
|
BUTTON_X = 0x000004,
|
||||||
|
BUTTON_Y = 0x000008,
|
||||||
|
STICK_L = 0x000010,
|
||||||
|
STICK_R = 0x000020,
|
||||||
|
TRIGGER_L = 0x000040,
|
||||||
|
TRIGGER_R = 0x000080,
|
||||||
|
TRIGGER_ZL = 0x000100,
|
||||||
|
TRIGGER_ZR = 0x000200,
|
||||||
|
BUTTON_PLUS = 0x000400,
|
||||||
|
BUTTON_MINUS = 0x000800,
|
||||||
|
BUTTON_LEFT = 0x001000,
|
||||||
|
BUTTON_UP = 0x002000,
|
||||||
|
BUTTON_RIGHT = 0x004000,
|
||||||
|
BUTTON_DOWN = 0x008000,
|
||||||
|
BUTTON_SL = 0x010000,
|
||||||
|
BUTTON_SR = 0x020000,
|
||||||
|
BUTTON_HOME = 0x040000,
|
||||||
|
BUTTON_CAPTURE = 0x080000,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const std::array<std::pair<std::string, 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},
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class TasAxes : u8 {
|
||||||
|
StickX,
|
||||||
|
StickY,
|
||||||
|
SubstickX,
|
||||||
|
SubstickY,
|
||||||
|
Undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TasData {
|
||||||
|
u32 buttons{};
|
||||||
|
std::array<float, 4> axis{};
|
||||||
|
};
|
||||||
|
|
||||||
|
class Tas {
|
||||||
|
public:
|
||||||
|
Tas();
|
||||||
|
~Tas();
|
||||||
|
|
||||||
|
static std::string buttonsToString(u32 button) {
|
||||||
|
std::string returns;
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::BUTTON_A)) != 0)
|
||||||
|
returns += ", A";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::BUTTON_B)) != 0)
|
||||||
|
returns += ", B";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::BUTTON_X)) != 0)
|
||||||
|
returns += ", X";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::BUTTON_Y)) != 0)
|
||||||
|
returns += ", Y";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::STICK_L)) != 0)
|
||||||
|
returns += ", STICK_L";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::STICK_R)) != 0)
|
||||||
|
returns += ", STICK_R";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::TRIGGER_L)) != 0)
|
||||||
|
returns += ", TRIGGER_L";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::TRIGGER_R)) != 0)
|
||||||
|
returns += ", TRIGGER_R";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::TRIGGER_ZL)) != 0)
|
||||||
|
returns += ", TRIGGER_ZL";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::TRIGGER_ZR)) != 0)
|
||||||
|
returns += ", TRIGGER_ZR";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::BUTTON_PLUS)) != 0)
|
||||||
|
returns += ", PLUS";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::BUTTON_MINUS)) != 0)
|
||||||
|
returns += ", MINUS";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::BUTTON_LEFT)) != 0)
|
||||||
|
returns += ", LEFT";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::BUTTON_UP)) != 0)
|
||||||
|
returns += ", UP";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::BUTTON_RIGHT)) != 0)
|
||||||
|
returns += ", RIGHT";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::BUTTON_DOWN)) != 0)
|
||||||
|
returns += ", DOWN";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::BUTTON_SL)) != 0)
|
||||||
|
returns += ", SL";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::BUTTON_SR)) != 0)
|
||||||
|
returns += ", SR";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::BUTTON_HOME)) != 0)
|
||||||
|
returns += ", HOME";
|
||||||
|
if ((button & static_cast<u32>(TasInput::TasButton::BUTTON_CAPTURE)) != 0)
|
||||||
|
returns += ", CAPTURE";
|
||||||
|
return returns.length() != 0 ? returns.substr(2) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefreshTasFile();
|
||||||
|
void LoadTasFiles();
|
||||||
|
void RecordInput(u32 buttons, std::array<std::pair<float, float>, 2> axes);
|
||||||
|
void UpdateThread();
|
||||||
|
std::string GetStatusDescription();
|
||||||
|
|
||||||
|
InputCommon::ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) const;
|
||||||
|
InputCommon::AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) const;
|
||||||
|
[[nodiscard]] const TasData& GetTasState(std::size_t pad) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct TASCommand {
|
||||||
|
u32 buttons{};
|
||||||
|
TasAnalog l_axis{};
|
||||||
|
TasAnalog r_axis{};
|
||||||
|
};
|
||||||
|
void LoadTasFile(int playerIndex);
|
||||||
|
void WriteTasFile();
|
||||||
|
TasAnalog ReadCommandAxis(const std::string line) const;
|
||||||
|
u32 ReadCommandButtons(const std::string line) const;
|
||||||
|
std::string WriteCommandButtons(u32 data) const;
|
||||||
|
std::string WriteCommandAxis(TasAnalog data) const;
|
||||||
|
std::pair<float, float> flipY(std::pair<float, float> old) const;
|
||||||
|
|
||||||
|
size_t scriptLength{0};
|
||||||
|
std::array<TasData, PLAYER_NUMBER> tas_data;
|
||||||
|
bool update_thread_running{true};
|
||||||
|
bool refresh_tas_fle{false};
|
||||||
|
std::array<std::vector<TASCommand>, PLAYER_NUMBER> newCommands{};
|
||||||
|
std::vector<TASCommand> recordCommands{};
|
||||||
|
std::size_t current_command{0};
|
||||||
|
TASCommand lastInput{}; // only used for recording
|
||||||
|
};
|
||||||
|
} // namespace TasInput
|
|
@ -0,0 +1,101 @@
|
||||||
|
// Copyright 2021 yuzu Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "common/settings.h"
|
||||||
|
#include "common/threadsafe_queue.h"
|
||||||
|
#include "input_common/tas/tas_input.h"
|
||||||
|
#include "input_common/tas/tas_poller.h"
|
||||||
|
|
||||||
|
namespace InputCommon {
|
||||||
|
|
||||||
|
class TasButton final : public Input::ButtonDevice {
|
||||||
|
public:
|
||||||
|
explicit TasButton(u32 button_, u32 pad_, const TasInput::Tas* tas_input_)
|
||||||
|
: button(button_), pad(pad_), tas_input(tas_input_) {}
|
||||||
|
|
||||||
|
bool GetStatus() const override {
|
||||||
|
return (tas_input->GetTasState(pad).buttons & button) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const u32 button;
|
||||||
|
const u32 pad;
|
||||||
|
const TasInput::Tas* tas_input;
|
||||||
|
};
|
||||||
|
|
||||||
|
TasButtonFactory::TasButtonFactory(std::shared_ptr<TasInput::Tas> tas_input_)
|
||||||
|
: tas_input(std::move(tas_input_)) {}
|
||||||
|
|
||||||
|
std::unique_ptr<Input::ButtonDevice> TasButtonFactory::Create(const Common::ParamPackage& params) {
|
||||||
|
const auto button_id = params.Get("button", 0);
|
||||||
|
const auto pad = params.Get("pad", 0);
|
||||||
|
|
||||||
|
return std::make_unique<TasButton>(button_id, pad, tas_input.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
class TasAnalog final : public Input::AnalogDevice {
|
||||||
|
public:
|
||||||
|
explicit TasAnalog(u32 pad_, u32 axis_x_, u32 axis_y_, const TasInput::Tas* tas_input_)
|
||||||
|
: pad(pad_), axis_x(axis_x_), axis_y(axis_y_), tas_input(tas_input_) {}
|
||||||
|
|
||||||
|
float GetAxis(u32 axis) const {
|
||||||
|
std::lock_guard lock{mutex};
|
||||||
|
return tas_input->GetTasState(pad).axis.at(axis);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<float, float> GetAnalog(u32 analog_axis_x, u32 analog_axis_y) const {
|
||||||
|
float x = GetAxis(analog_axis_x);
|
||||||
|
float y = GetAxis(analog_axis_y);
|
||||||
|
|
||||||
|
// Make sure the coordinates are in the unit circle,
|
||||||
|
// otherwise normalize it.
|
||||||
|
float r = x * x + y * y;
|
||||||
|
if (r > 1.0f) {
|
||||||
|
r = std::sqrt(r);
|
||||||
|
x /= r;
|
||||||
|
y /= r;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {x, y};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::tuple<float, float> GetStatus() const override {
|
||||||
|
return GetAnalog(axis_x, axis_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
Input::AnalogProperties GetAnalogProperties() const override {
|
||||||
|
return {0.0f, 1.0f, 0.5f};
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const u32 pad;
|
||||||
|
const u32 axis_x;
|
||||||
|
const u32 axis_y;
|
||||||
|
const TasInput::Tas* tas_input;
|
||||||
|
mutable std::mutex mutex;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// An analog device factory that creates analog devices from GC Adapter
|
||||||
|
TasAnalogFactory::TasAnalogFactory(std::shared_ptr<TasInput::Tas> tas_input_)
|
||||||
|
: tas_input(std::move(tas_input_)) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates analog device from joystick axes
|
||||||
|
* @param params contains parameters for creating the device:
|
||||||
|
* - "port": the nth gcpad on the adapter
|
||||||
|
* - "axis_x": the index of the axis to be bind as x-axis
|
||||||
|
* - "axis_y": the index of the axis to be bind as y-axis
|
||||||
|
*/
|
||||||
|
std::unique_ptr<Input::AnalogDevice> TasAnalogFactory::Create(const Common::ParamPackage& params) {
|
||||||
|
const auto pad = static_cast<u32>(params.Get("pad", 0));
|
||||||
|
const auto axis_x = static_cast<u32>(params.Get("axis_x", 0));
|
||||||
|
const auto axis_y = static_cast<u32>(params.Get("axis_y", 1));
|
||||||
|
|
||||||
|
return std::make_unique<TasAnalog>(pad, axis_x, axis_y, tas_input.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace InputCommon
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright 2021 yuzu Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include "core/frontend/input.h"
|
||||||
|
#include "input_common/tas/tas_input.h"
|
||||||
|
|
||||||
|
namespace InputCommon {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A button device factory representing a mouse. It receives mouse events and forward them
|
||||||
|
* to all button devices it created.
|
||||||
|
*/
|
||||||
|
class TasButtonFactory final : public Input::Factory<Input::ButtonDevice> {
|
||||||
|
public:
|
||||||
|
explicit TasButtonFactory(std::shared_ptr<TasInput::Tas> tas_input_);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a button device from a button press
|
||||||
|
* @param params contains parameters for creating the device:
|
||||||
|
* - "code": the code of the key to bind with the button
|
||||||
|
*/
|
||||||
|
std::unique_ptr<Input::ButtonDevice> Create(const Common::ParamPackage& params) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<TasInput::Tas> tas_input;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// An analog device factory that creates analog devices from mouse
|
||||||
|
class TasAnalogFactory final : public Input::Factory<Input::AnalogDevice> {
|
||||||
|
public:
|
||||||
|
explicit TasAnalogFactory(std::shared_ptr<TasInput::Tas> tas_input_);
|
||||||
|
|
||||||
|
std::unique_ptr<Input::AnalogDevice> Create(const Common::ParamPackage& params) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<TasInput::Tas> tas_input;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace InputCommon
|
|
@ -124,6 +124,19 @@ QString ButtonToText(const Common::ParamPackage& param) {
|
||||||
return GetKeyName(param.Get("code", 0));
|
return GetKeyName(param.Get("code", 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (param.Get("engine", "") == "tas") {
|
||||||
|
if (param.Has("axis")) {
|
||||||
|
const QString axis_str = QString::fromStdString(param.Get("axis", ""));
|
||||||
|
|
||||||
|
return QObject::tr("TAS Axis %1").arg(axis_str);
|
||||||
|
}
|
||||||
|
if (param.Has("button")) {
|
||||||
|
const QString button_str = QString::number(int(std::log2(param.Get("button", 0))));
|
||||||
|
return QObject::tr("TAS Btn %1").arg(button_str);
|
||||||
|
}
|
||||||
|
return GetKeyName(param.Get("code", 0));
|
||||||
|
}
|
||||||
|
|
||||||
if (param.Get("engine", "") == "cemuhookudp") {
|
if (param.Get("engine", "") == "cemuhookudp") {
|
||||||
if (param.Has("pad_index")) {
|
if (param.Has("pad_index")) {
|
||||||
const QString motion_str = QString::fromStdString(param.Get("pad_index", ""));
|
const QString motion_str = QString::fromStdString(param.Get("pad_index", ""));
|
||||||
|
@ -187,7 +200,8 @@ QString AnalogToText(const Common::ParamPackage& param, const std::string& dir)
|
||||||
const QString axis_y_str = QString::fromStdString(param.Get("axis_y", ""));
|
const QString axis_y_str = QString::fromStdString(param.Get("axis_y", ""));
|
||||||
const bool invert_x = param.Get("invert_x", "+") == "-";
|
const bool invert_x = param.Get("invert_x", "+") == "-";
|
||||||
const bool invert_y = param.Get("invert_y", "+") == "-";
|
const bool invert_y = param.Get("invert_y", "+") == "-";
|
||||||
if (engine_str == "sdl" || engine_str == "gcpad" || engine_str == "mouse") {
|
if (engine_str == "sdl" || engine_str == "gcpad" || engine_str == "mouse" ||
|
||||||
|
engine_str == "tas") {
|
||||||
if (dir == "modifier") {
|
if (dir == "modifier") {
|
||||||
return QObject::tr("[unused]");
|
return QObject::tr("[unused]");
|
||||||
}
|
}
|
||||||
|
@ -926,9 +940,9 @@ void ConfigureInputPlayer::UpdateUI() {
|
||||||
|
|
||||||
int slider_value;
|
int slider_value;
|
||||||
auto& param = analogs_param[analog_id];
|
auto& param = analogs_param[analog_id];
|
||||||
const bool is_controller = param.Get("engine", "") == "sdl" ||
|
const bool is_controller =
|
||||||
param.Get("engine", "") == "gcpad" ||
|
param.Get("engine", "") == "sdl" || param.Get("engine", "") == "gcpad" ||
|
||||||
param.Get("engine", "") == "mouse";
|
param.Get("engine", "") == "mouse" || param.Get("engine", "") == "tas";
|
||||||
|
|
||||||
if (is_controller) {
|
if (is_controller) {
|
||||||
if (!param.Has("deadzone")) {
|
if (!param.Has("deadzone")) {
|
||||||
|
@ -1045,8 +1059,12 @@ int ConfigureInputPlayer::GetIndexFromControllerType(Settings::ControllerType ty
|
||||||
void ConfigureInputPlayer::UpdateInputDevices() {
|
void ConfigureInputPlayer::UpdateInputDevices() {
|
||||||
input_devices = input_subsystem->GetInputDevices();
|
input_devices = input_subsystem->GetInputDevices();
|
||||||
ui->comboDevices->clear();
|
ui->comboDevices->clear();
|
||||||
for (auto device : input_devices) {
|
for (auto& device : input_devices) {
|
||||||
ui->comboDevices->addItem(QString::fromStdString(device.Get("display", "Unknown")), {});
|
const std::string display = device.Get("display", "Unknown");
|
||||||
|
ui->comboDevices->addItem(QString::fromStdString(display), {});
|
||||||
|
if (display == "TAS") {
|
||||||
|
device.Set("pad", static_cast<u8>(player_index));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -220,8 +220,20 @@ void PlayerControlPreview::UpdateInput() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ControllerInput input{};
|
||||||
if (input_changed) {
|
if (input_changed) {
|
||||||
update();
|
update();
|
||||||
|
input.changed = true;
|
||||||
|
}
|
||||||
|
input.axis_values[Settings::NativeAnalog::LStick] = {
|
||||||
|
axis_values[Settings::NativeAnalog::LStick].value.x(),
|
||||||
|
axis_values[Settings::NativeAnalog::LStick].value.y()};
|
||||||
|
input.axis_values[Settings::NativeAnalog::RStick] = {
|
||||||
|
axis_values[Settings::NativeAnalog::RStick].value.x(),
|
||||||
|
axis_values[Settings::NativeAnalog::RStick].value.y()};
|
||||||
|
input.button_values = button_values;
|
||||||
|
if (controller_callback.input != NULL) {
|
||||||
|
controller_callback.input(std::move(input));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mapping_active) {
|
if (mapping_active) {
|
||||||
|
@ -229,6 +241,10 @@ void PlayerControlPreview::UpdateInput() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlayerControlPreview::SetCallBack(ControllerCallback callback_) {
|
||||||
|
controller_callback = callback_;
|
||||||
|
}
|
||||||
|
|
||||||
void PlayerControlPreview::paintEvent(QPaintEvent* event) {
|
void PlayerControlPreview::paintEvent(QPaintEvent* event) {
|
||||||
QFrame::paintEvent(event);
|
QFrame::paintEvent(event);
|
||||||
QPainter p(this);
|
QPainter p(this);
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
#include "core/frontend/input.h"
|
#include "core/frontend/input.h"
|
||||||
|
#include "yuzu/debugger/controller.h"
|
||||||
|
|
||||||
class QLabel;
|
class QLabel;
|
||||||
|
|
||||||
|
@ -33,6 +34,7 @@ public:
|
||||||
void BeginMappingAnalog(std::size_t button_id);
|
void BeginMappingAnalog(std::size_t button_id);
|
||||||
void EndMapping();
|
void EndMapping();
|
||||||
void UpdateInput();
|
void UpdateInput();
|
||||||
|
void SetCallBack(ControllerCallback callback_);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent* event) override;
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
@ -181,6 +183,7 @@ private:
|
||||||
using StickArray =
|
using StickArray =
|
||||||
std::array<std::unique_ptr<Input::AnalogDevice>, Settings::NativeAnalog::NUM_STICKS_HID>;
|
std::array<std::unique_ptr<Input::AnalogDevice>, Settings::NativeAnalog::NUM_STICKS_HID>;
|
||||||
|
|
||||||
|
ControllerCallback controller_callback;
|
||||||
bool is_enabled{};
|
bool is_enabled{};
|
||||||
bool mapping_active{};
|
bool mapping_active{};
|
||||||
int blink_counter{};
|
int blink_counter{};
|
||||||
|
|
|
@ -6,10 +6,13 @@
|
||||||
#include <QLayout>
|
#include <QLayout>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
|
#include "input_common/main.h"
|
||||||
|
#include "input_common/tas/tas_input.h"
|
||||||
#include "yuzu/configuration/configure_input_player_widget.h"
|
#include "yuzu/configuration/configure_input_player_widget.h"
|
||||||
#include "yuzu/debugger/controller.h"
|
#include "yuzu/debugger/controller.h"
|
||||||
|
|
||||||
ControllerDialog::ControllerDialog(QWidget* parent) : QWidget(parent, Qt::Dialog) {
|
ControllerDialog::ControllerDialog(QWidget* parent, InputCommon::InputSubsystem* input_subsystem_)
|
||||||
|
: QWidget(parent, Qt::Dialog), input_subsystem{input_subsystem_} {
|
||||||
setObjectName(QStringLiteral("Controller"));
|
setObjectName(QStringLiteral("Controller"));
|
||||||
setWindowTitle(tr("Controller P1"));
|
setWindowTitle(tr("Controller P1"));
|
||||||
resize(500, 350);
|
resize(500, 350);
|
||||||
|
@ -38,6 +41,9 @@ void ControllerDialog::refreshConfiguration() {
|
||||||
constexpr std::size_t player = 0;
|
constexpr std::size_t player = 0;
|
||||||
widget->SetPlayerInputRaw(player, players[player].buttons, players[player].analogs);
|
widget->SetPlayerInputRaw(player, players[player].buttons, players[player].analogs);
|
||||||
widget->SetControllerType(players[player].controller_type);
|
widget->SetControllerType(players[player].controller_type);
|
||||||
|
ControllerCallback callback{[this](ControllerInput input) { InputController(input); }};
|
||||||
|
widget->SetCallBack(callback);
|
||||||
|
widget->repaint();
|
||||||
widget->SetConnectedStatus(players[player].connected);
|
widget->SetConnectedStatus(players[player].connected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,3 +73,17 @@ void ControllerDialog::hideEvent(QHideEvent* ev) {
|
||||||
widget->SetConnectedStatus(false);
|
widget->SetConnectedStatus(false);
|
||||||
QWidget::hideEvent(ev);
|
QWidget::hideEvent(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ControllerDialog::RefreshTasFile() {
|
||||||
|
input_subsystem->GetTas()->RefreshTasFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ControllerDialog::InputController(ControllerInput input) {
|
||||||
|
u32 buttons = 0;
|
||||||
|
int index = 0;
|
||||||
|
for (bool btn : input.button_values) {
|
||||||
|
buttons += (btn ? 1 : 0) << index;
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
input_subsystem->GetTas()->RecordInput(buttons, input.axis_values);
|
||||||
|
}
|
|
@ -4,18 +4,36 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <QFileSystemWatcher>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
#include "common/settings.h"
|
||||||
|
|
||||||
class QAction;
|
class QAction;
|
||||||
class QHideEvent;
|
class QHideEvent;
|
||||||
class QShowEvent;
|
class QShowEvent;
|
||||||
class PlayerControlPreview;
|
class PlayerControlPreview;
|
||||||
|
|
||||||
|
namespace InputCommon {
|
||||||
|
class InputSubsystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ControllerInput {
|
||||||
|
std::array<std::pair<float, float>, Settings::NativeAnalog::NUM_STICKS_HID> axis_values{};
|
||||||
|
std::array<bool, Settings::NativeButton::NumButtons> button_values{};
|
||||||
|
bool changed{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ControllerCallback {
|
||||||
|
std::function<void(ControllerInput)> input;
|
||||||
|
std::function<void()> update;
|
||||||
|
};
|
||||||
|
|
||||||
class ControllerDialog : public QWidget {
|
class ControllerDialog : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ControllerDialog(QWidget* parent = nullptr);
|
explicit ControllerDialog(QWidget* parent = nullptr,
|
||||||
|
InputCommon::InputSubsystem* input_subsystem_ = nullptr);
|
||||||
|
|
||||||
/// Returns a QAction that can be used to toggle visibility of this dialog.
|
/// Returns a QAction that can be used to toggle visibility of this dialog.
|
||||||
QAction* toggleViewAction();
|
QAction* toggleViewAction();
|
||||||
|
@ -26,6 +44,10 @@ protected:
|
||||||
void hideEvent(QHideEvent* ev) override;
|
void hideEvent(QHideEvent* ev) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void RefreshTasFile();
|
||||||
|
void InputController(ControllerInput input);
|
||||||
QAction* toggle_view_action = nullptr;
|
QAction* toggle_view_action = nullptr;
|
||||||
|
QFileSystemWatcher* watcher = nullptr;
|
||||||
PlayerControlPreview* widget;
|
PlayerControlPreview* widget;
|
||||||
|
InputCommon::InputSubsystem* input_subsystem;
|
||||||
};
|
};
|
||||||
|
|
|
@ -193,6 +193,7 @@ GMainWindow::GMainWindow()
|
||||||
config{std::make_unique<Config>()}, vfs{std::make_shared<FileSys::RealVfsFilesystem>()},
|
config{std::make_unique<Config>()}, vfs{std::make_shared<FileSys::RealVfsFilesystem>()},
|
||||||
provider{std::make_unique<FileSys::ManualContentProvider>()} {
|
provider{std::make_unique<FileSys::ManualContentProvider>()} {
|
||||||
Common::Log::Initialize();
|
Common::Log::Initialize();
|
||||||
|
Settings::values.inputSubsystem = input_subsystem;
|
||||||
LoadTranslation();
|
LoadTranslation();
|
||||||
|
|
||||||
setAcceptDrops(true);
|
setAcceptDrops(true);
|
||||||
|
@ -841,7 +842,7 @@ void GMainWindow::InitializeDebugWidgets() {
|
||||||
waitTreeWidget->hide();
|
waitTreeWidget->hide();
|
||||||
debug_menu->addAction(waitTreeWidget->toggleViewAction());
|
debug_menu->addAction(waitTreeWidget->toggleViewAction());
|
||||||
|
|
||||||
controller_dialog = new ControllerDialog(this);
|
controller_dialog = new ControllerDialog(this, input_subsystem.get());
|
||||||
controller_dialog->hide();
|
controller_dialog->hide();
|
||||||
debug_menu->addAction(controller_dialog->toggleViewAction());
|
debug_menu->addAction(controller_dialog->toggleViewAction());
|
||||||
|
|
||||||
|
|
Reference in New Issue