forked from suyu/suyu
Merge pull request #6485 from MonsterDruide1/tas
input_common: TAS with script playback & recording
This commit is contained in:
commit
6e376c27a3
27 changed files with 1426 additions and 21 deletions
|
@ -21,6 +21,7 @@
|
|||
#define SCREENSHOTS_DIR "screenshots"
|
||||
#define SDMC_DIR "sdmc"
|
||||
#define SHADER_DIR "shader"
|
||||
#define TAS_DIR "tas"
|
||||
|
||||
// yuzu-specific files
|
||||
|
||||
|
|
|
@ -116,6 +116,7 @@ private:
|
|||
GenerateYuzuPath(YuzuPath::ScreenshotsDir, yuzu_path / SCREENSHOTS_DIR);
|
||||
GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR);
|
||||
GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR);
|
||||
GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR);
|
||||
}
|
||||
|
||||
~PathManagerImpl() = default;
|
||||
|
|
|
@ -23,6 +23,7 @@ enum class YuzuPath {
|
|||
ScreenshotsDir, // Where yuzu screenshots are stored.
|
||||
SDMCDir, // Where the emulated SDMC is stored.
|
||||
ShaderDir, // Where shaders are stored.
|
||||
TASDir, // Where TAS scripts are stored.
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -512,6 +512,11 @@ struct Values {
|
|||
"motion_device"};
|
||||
BasicSetting<std::string> udp_input_servers{"127.0.0.1:26760", "udp_input_servers"};
|
||||
|
||||
BasicSetting<bool> pause_tas_on_load{true, "pause_tas_on_load"};
|
||||
BasicSetting<bool> tas_enable{false, "tas_enable"};
|
||||
BasicSetting<bool> tas_loop{false, "tas_loop"};
|
||||
BasicSetting<bool> tas_swap_controllers{true, "tas_swap_controllers"};
|
||||
|
||||
BasicSetting<bool> mouse_panning{false, "mouse_panning"};
|
||||
BasicRangedSetting<u8> mouse_panning_sensitivity{10, 1, 100, "mouse_panning_sensitivity"};
|
||||
BasicSetting<bool> mouse_enabled{false, "mouse_enabled"};
|
||||
|
|
|
@ -21,6 +21,10 @@ add_library(input_common STATIC
|
|||
mouse/mouse_poller.h
|
||||
sdl/sdl.cpp
|
||||
sdl/sdl.h
|
||||
tas/tas_input.cpp
|
||||
tas/tas_input.h
|
||||
tas/tas_poller.cpp
|
||||
tas/tas_poller.h
|
||||
udp/client.cpp
|
||||
udp/client.h
|
||||
udp/protocol.cpp
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include <memory>
|
||||
#include <thread>
|
||||
#include "common/param_package.h"
|
||||
#include "common/settings.h"
|
||||
#include "input_common/analog_from_button.h"
|
||||
#include "input_common/gcadapter/gc_adapter.h"
|
||||
#include "input_common/gcadapter/gc_poller.h"
|
||||
|
@ -13,6 +14,8 @@
|
|||
#include "input_common/motion_from_button.h"
|
||||
#include "input_common/mouse/mouse_input.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/udp/client.h"
|
||||
#include "input_common/udp/udp.h"
|
||||
|
@ -60,6 +63,12 @@ struct InputSubsystem::Impl {
|
|||
Input::RegisterFactory<Input::MotionDevice>("mouse", mousemotion);
|
||||
mousetouch = std::make_shared<MouseTouchFactory>(mouse);
|
||||
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() {
|
||||
|
@ -94,6 +103,12 @@ struct InputSubsystem::Impl {
|
|||
mouseanalog.reset();
|
||||
mousemotion.reset();
|
||||
mousetouch.reset();
|
||||
|
||||
Input::UnregisterFactory<Input::ButtonDevice>("tas");
|
||||
Input::UnregisterFactory<Input::AnalogDevice>("tas");
|
||||
|
||||
tasbuttons.reset();
|
||||
tasanalog.reset();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const {
|
||||
|
@ -101,6 +116,10 @@ struct InputSubsystem::Impl {
|
|||
Common::ParamPackage{{"display", "Any"}, {"class", "any"}},
|
||||
Common::ParamPackage{{"display", "Keyboard/Mouse"}, {"class", "keyboard"}},
|
||||
};
|
||||
if (Settings::values.tas_enable) {
|
||||
devices.emplace_back(
|
||||
Common::ParamPackage{{"display", "TAS Controller"}, {"class", "tas"}});
|
||||
}
|
||||
#ifdef HAVE_SDL2
|
||||
auto sdl_devices = sdl->GetInputDevices();
|
||||
devices.insert(devices.end(), sdl_devices.begin(), sdl_devices.end());
|
||||
|
@ -120,6 +139,9 @@ struct InputSubsystem::Impl {
|
|||
if (params.Get("class", "") == "gcpad") {
|
||||
return gcadapter->GetAnalogMappingForDevice(params);
|
||||
}
|
||||
if (params.Get("class", "") == "tas") {
|
||||
return tas->GetAnalogMappingForDevice(params);
|
||||
}
|
||||
#ifdef HAVE_SDL2
|
||||
if (params.Get("class", "") == "sdl") {
|
||||
return sdl->GetAnalogMappingForDevice(params);
|
||||
|
@ -136,6 +158,9 @@ struct InputSubsystem::Impl {
|
|||
if (params.Get("class", "") == "gcpad") {
|
||||
return gcadapter->GetButtonMappingForDevice(params);
|
||||
}
|
||||
if (params.Get("class", "") == "tas") {
|
||||
return tas->GetButtonMappingForDevice(params);
|
||||
}
|
||||
#ifdef HAVE_SDL2
|
||||
if (params.Get("class", "") == "sdl") {
|
||||
return sdl->GetButtonMappingForDevice(params);
|
||||
|
@ -174,9 +199,12 @@ struct InputSubsystem::Impl {
|
|||
std::shared_ptr<MouseAnalogFactory> mouseanalog;
|
||||
std::shared_ptr<MouseMotionFactory> mousemotion;
|
||||
std::shared_ptr<MouseTouchFactory> mousetouch;
|
||||
std::shared_ptr<TasButtonFactory> tasbuttons;
|
||||
std::shared_ptr<TasAnalogFactory> tasanalog;
|
||||
std::shared_ptr<CemuhookUDP::Client> udp;
|
||||
std::shared_ptr<GCAdapter::Adapter> gcadapter;
|
||||
std::shared_ptr<MouseInput::Mouse> mouse;
|
||||
std::shared_ptr<TasInput::Tas> tas;
|
||||
};
|
||||
|
||||
InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {}
|
||||
|
@ -207,6 +235,14 @@ const MouseInput::Mouse* InputSubsystem::GetMouse() const {
|
|||
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 {
|
||||
return impl->GetInputDevices();
|
||||
}
|
||||
|
@ -287,6 +323,22 @@ const MouseTouchFactory* InputSubsystem::GetMouseTouch() const {
|
|||
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() {
|
||||
if (!impl->udp) {
|
||||
return;
|
||||
|
|
|
@ -29,6 +29,10 @@ namespace MouseInput {
|
|||
class Mouse;
|
||||
}
|
||||
|
||||
namespace TasInput {
|
||||
class Tas;
|
||||
}
|
||||
|
||||
namespace InputCommon {
|
||||
namespace Polling {
|
||||
|
||||
|
@ -64,6 +68,8 @@ class MouseButtonFactory;
|
|||
class MouseAnalogFactory;
|
||||
class MouseMotionFactory;
|
||||
class MouseTouchFactory;
|
||||
class TasButtonFactory;
|
||||
class TasAnalogFactory;
|
||||
class Keyboard;
|
||||
|
||||
/**
|
||||
|
@ -103,6 +109,11 @@ public:
|
|||
/// Retrieves the underlying mouse device.
|
||||
[[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.
|
||||
* Each returned ParamPackage should have a `display` field used for display, a class field for
|
||||
|
@ -144,30 +155,42 @@ public:
|
|||
/// Retrieves the underlying udp touch handler.
|
||||
[[nodiscard]] const UDPTouchFactory* GetUDPTouch() const;
|
||||
|
||||
/// Retrieves the underlying GameCube button handler.
|
||||
/// Retrieves the underlying mouse button handler.
|
||||
[[nodiscard]] MouseButtonFactory* GetMouseButtons();
|
||||
|
||||
/// Retrieves the underlying GameCube button handler.
|
||||
/// Retrieves the underlying mouse button handler.
|
||||
[[nodiscard]] const MouseButtonFactory* GetMouseButtons() const;
|
||||
|
||||
/// Retrieves the underlying udp touch handler.
|
||||
/// Retrieves the underlying mouse analog handler.
|
||||
[[nodiscard]] MouseAnalogFactory* GetMouseAnalogs();
|
||||
|
||||
/// Retrieves the underlying udp touch handler.
|
||||
/// Retrieves the underlying mouse analog handler.
|
||||
[[nodiscard]] const MouseAnalogFactory* GetMouseAnalogs() const;
|
||||
|
||||
/// Retrieves the underlying udp motion handler.
|
||||
/// Retrieves the underlying mouse motion handler.
|
||||
[[nodiscard]] MouseMotionFactory* GetMouseMotions();
|
||||
|
||||
/// Retrieves the underlying udp motion handler.
|
||||
/// Retrieves the underlying mouse motion handler.
|
||||
[[nodiscard]] const MouseMotionFactory* GetMouseMotions() const;
|
||||
|
||||
/// Retrieves the underlying udp touch handler.
|
||||
/// Retrieves the underlying mouse touch handler.
|
||||
[[nodiscard]] MouseTouchFactory* GetMouseTouch();
|
||||
|
||||
/// Retrieves the underlying udp touch handler.
|
||||
/// Retrieves the underlying mouse touch handler.
|
||||
[[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 analogs handler.
|
||||
[[nodiscard]] TasAnalogFactory* GetTasAnalogs();
|
||||
|
||||
/// Retrieves the underlying tas analogs handler.
|
||||
[[nodiscard]] const TasAnalogFactory* GetTasAnalogs() const;
|
||||
|
||||
/// Reloads the input devices
|
||||
void ReloadInputDevices();
|
||||
|
||||
|
|
455
src/input_common/tas/tas_input.cpp
Normal file
455
src/input_common/tas/tas_input.cpp
Normal file
|
@ -0,0 +1,455 @@
|
|||
// Copyright 2021 yuzu Emulator Project
|
||||
// Licensed under GPLv2+
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <cstring>
|
||||
#include <regex>
|
||||
|
||||
#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 {
|
||||
|
||||
// Supported keywords and buttons from a TAS file
|
||||
constexpr std::array<std::pair<std::string_view, TasButton>, 20> text_to_tas_button = {
|
||||
std::pair{"KEY_A", TasButton::BUTTON_A},
|
||||
{"KEY_B", TasButton::BUTTON_B},
|
||||
{"KEY_X", TasButton::BUTTON_X},
|
||||
{"KEY_Y", TasButton::BUTTON_Y},
|
||||
{"KEY_LSTICK", TasButton::STICK_L},
|
||||
{"KEY_RSTICK", TasButton::STICK_R},
|
||||
{"KEY_L", TasButton::TRIGGER_L},
|
||||
{"KEY_R", TasButton::TRIGGER_R},
|
||||
{"KEY_PLUS", TasButton::BUTTON_PLUS},
|
||||
{"KEY_MINUS", TasButton::BUTTON_MINUS},
|
||||
{"KEY_DLEFT", TasButton::BUTTON_LEFT},
|
||||
{"KEY_DUP", TasButton::BUTTON_UP},
|
||||
{"KEY_DRIGHT", TasButton::BUTTON_RIGHT},
|
||||
{"KEY_DDOWN", TasButton::BUTTON_DOWN},
|
||||
{"KEY_SL", TasButton::BUTTON_SL},
|
||||
{"KEY_SR", TasButton::BUTTON_SR},
|
||||
{"KEY_CAPTURE", TasButton::BUTTON_CAPTURE},
|
||||
{"KEY_HOME", TasButton::BUTTON_HOME},
|
||||
{"KEY_ZL", TasButton::TRIGGER_ZL},
|
||||
{"KEY_ZR", TasButton::TRIGGER_ZR},
|
||||
};
|
||||
|
||||
Tas::Tas() {
|
||||
if (!Settings::values.tas_enable) {
|
||||
needs_reset = true;
|
||||
return;
|
||||
}
|
||||
LoadTasFiles();
|
||||
}
|
||||
|
||||
Tas::~Tas() {
|
||||
Stop();
|
||||
};
|
||||
|
||||
void Tas::LoadTasFiles() {
|
||||
script_length = 0;
|
||||
for (size_t i = 0; i < commands.size(); i++) {
|
||||
LoadTasFile(i);
|
||||
if (commands[i].size() > script_length) {
|
||||
script_length = commands[i].size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Tas::LoadTasFile(size_t player_index) {
|
||||
if (!commands[player_index].empty()) {
|
||||
commands[player_index].clear();
|
||||
}
|
||||
std::string file =
|
||||
Common::FS::ReadStringFromFile(Common::FS::GetYuzuPath(Common::FS::YuzuPath::TASDir) /
|
||||
fmt::format("script0-{}.txt", player_index + 1),
|
||||
Common::FS::FileType::BinaryFile);
|
||||
std::stringstream command_line(file);
|
||||
std::string line;
|
||||
int frame_no = 0;
|
||||
while (std::getline(command_line, line, '\n')) {
|
||||
if (line.empty()) {
|
||||
continue;
|
||||
}
|
||||
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 (frame_no < std::stoi(seglist.at(0))) {
|
||||
commands[player_index].push_back({});
|
||||
frame_no++;
|
||||
}
|
||||
|
||||
TASCommand command = {
|
||||
.buttons = ReadCommandButtons(seglist.at(1)),
|
||||
.l_axis = ReadCommandAxis(seglist.at(2)),
|
||||
.r_axis = ReadCommandAxis(seglist.at(3)),
|
||||
};
|
||||
commands[player_index].push_back(command);
|
||||
frame_no++;
|
||||
}
|
||||
LOG_INFO(Input, "TAS file loaded! {} frames", frame_no);
|
||||
}
|
||||
|
||||
void Tas::WriteTasFile(std::u8string file_name) {
|
||||
std::string output_text;
|
||||
for (size_t frame = 0; frame < record_commands.size(); frame++) {
|
||||
if (!output_text.empty()) {
|
||||
output_text += "\n";
|
||||
}
|
||||
const TASCommand& line = record_commands[frame];
|
||||
output_text += std::to_string(frame) + " " + WriteCommandButtons(line.buttons) + " " +
|
||||
WriteCommandAxis(line.l_axis) + " " + WriteCommandAxis(line.r_axis);
|
||||
}
|
||||
const auto bytes_written = Common::FS::WriteStringToFile(
|
||||
Common::FS::GetYuzuPath(Common::FS::YuzuPath::TASDir) / file_name,
|
||||
Common::FS::FileType::TextFile, output_text);
|
||||
if (bytes_written == output_text.size()) {
|
||||
LOG_INFO(Input, "TAS file written to file!");
|
||||
} else {
|
||||
LOG_ERROR(Input, "Writing the TAS-file has failed! {} / {} bytes written", bytes_written,
|
||||
output_text.size());
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<float, float> Tas::FlipAxisY(std::pair<float, float> old) {
|
||||
auto [x, y] = old;
|
||||
return {x, -y};
|
||||
}
|
||||
|
||||
void Tas::RecordInput(u32 buttons, const std::array<std::pair<float, float>, 2>& axes) {
|
||||
last_input = {buttons, FlipAxisY(axes[0]), FlipAxisY(axes[1])};
|
||||
}
|
||||
|
||||
std::tuple<TasState, size_t, size_t> Tas::GetStatus() const {
|
||||
TasState state;
|
||||
if (is_recording) {
|
||||
return {TasState::Recording, 0, record_commands.size()};
|
||||
}
|
||||
|
||||
if (is_running) {
|
||||
state = TasState::Running;
|
||||
} else {
|
||||
state = TasState::Stopped;
|
||||
}
|
||||
|
||||
return {state, current_command, script_length};
|
||||
}
|
||||
|
||||
std::string Tas::DebugButtons(u32 buttons) const {
|
||||
return fmt::format("{{ {} }}", TasInput::Tas::ButtonsToString(buttons));
|
||||
}
|
||||
|
||||
std::string Tas::DebugJoystick(float x, float y) const {
|
||||
return fmt::format("[ {} , {} ]", std::to_string(x), std::to_string(y));
|
||||
}
|
||||
|
||||
std::string Tas::DebugInput(const TasData& data) const {
|
||||
return fmt::format("{{ {} , {} , {} }}", DebugButtons(data.buttons),
|
||||
DebugJoystick(data.axis[0], data.axis[1]),
|
||||
DebugJoystick(data.axis[2], data.axis[3]));
|
||||
}
|
||||
|
||||
std::string Tas::DebugInputs(const std::array<TasData, PLAYER_NUMBER>& arr) const {
|
||||
std::string returns = "[ ";
|
||||
for (size_t i = 0; i < arr.size(); i++) {
|
||||
returns += DebugInput(arr[i]);
|
||||
if (i != arr.size() - 1) {
|
||||
returns += " , ";
|
||||
}
|
||||
}
|
||||
return returns + "]";
|
||||
}
|
||||
|
||||
std::string Tas::ButtonsToString(u32 button) const {
|
||||
std::string returns;
|
||||
for (auto [text_button, tas_button] : text_to_tas_button) {
|
||||
if ((button & static_cast<u32>(tas_button)) != 0)
|
||||
returns += fmt::format(", {}", text_button.substr(4));
|
||||
}
|
||||
return returns.empty() ? "" : returns.substr(2);
|
||||
}
|
||||
|
||||
void Tas::UpdateThread() {
|
||||
if (!Settings::values.tas_enable) {
|
||||
if (is_running) {
|
||||
Stop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_recording) {
|
||||
record_commands.push_back(last_input);
|
||||
}
|
||||
if (needs_reset) {
|
||||
current_command = 0;
|
||||
needs_reset = false;
|
||||
LoadTasFiles();
|
||||
LOG_DEBUG(Input, "tas_reset done");
|
||||
}
|
||||
|
||||
if (!is_running) {
|
||||
tas_data.fill({});
|
||||
return;
|
||||
}
|
||||
if (current_command < script_length) {
|
||||
LOG_DEBUG(Input, "Playing TAS {}/{}", current_command, script_length);
|
||||
size_t frame = current_command++;
|
||||
for (size_t i = 0; i < commands.size(); i++) {
|
||||
if (frame < commands[i].size()) {
|
||||
TASCommand command = commands[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] = {};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
is_running = Settings::values.tas_loop.GetValue();
|
||||
current_command = 0;
|
||||
tas_data.fill({});
|
||||
if (!is_running) {
|
||||
SwapToStoredController();
|
||||
}
|
||||
}
|
||||
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.0f;
|
||||
const float y = std::stof(seglist.at(1)) / 32767.0f;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void Tas::StartStop() {
|
||||
if (!Settings::values.tas_enable) {
|
||||
return;
|
||||
}
|
||||
if (is_running) {
|
||||
Stop();
|
||||
} else {
|
||||
is_running = true;
|
||||
SwapToTasController();
|
||||
}
|
||||
}
|
||||
|
||||
void Tas::Stop() {
|
||||
is_running = false;
|
||||
SwapToStoredController();
|
||||
}
|
||||
|
||||
void Tas::SwapToTasController() {
|
||||
if (!Settings::values.tas_swap_controllers) {
|
||||
return;
|
||||
}
|
||||
auto& players = Settings::values.players.GetValue();
|
||||
for (std::size_t index = 0; index < players.size(); index++) {
|
||||
auto& player = players[index];
|
||||
player_mappings[index] = player;
|
||||
|
||||
// Only swap active controllers
|
||||
if (!player.connected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Common::ParamPackage tas_param;
|
||||
tas_param.Set("pad", static_cast<u8>(index));
|
||||
auto button_mapping = GetButtonMappingForDevice(tas_param);
|
||||
auto analog_mapping = GetAnalogMappingForDevice(tas_param);
|
||||
auto& buttons = player.buttons;
|
||||
auto& analogs = player.analogs;
|
||||
|
||||
for (std::size_t i = 0; i < buttons.size(); ++i) {
|
||||
buttons[i] = button_mapping[static_cast<Settings::NativeButton::Values>(i)].Serialize();
|
||||
}
|
||||
for (std::size_t i = 0; i < analogs.size(); ++i) {
|
||||
analogs[i] = analog_mapping[static_cast<Settings::NativeAnalog::Values>(i)].Serialize();
|
||||
}
|
||||
}
|
||||
is_old_input_saved = true;
|
||||
Settings::values.is_device_reload_pending.store(true);
|
||||
}
|
||||
|
||||
void Tas::SwapToStoredController() {
|
||||
if (!is_old_input_saved) {
|
||||
return;
|
||||
}
|
||||
auto& players = Settings::values.players.GetValue();
|
||||
for (std::size_t index = 0; index < players.size(); index++) {
|
||||
players[index] = player_mappings[index];
|
||||
}
|
||||
is_old_input_saved = false;
|
||||
Settings::values.is_device_reload_pending.store(true);
|
||||
}
|
||||
|
||||
void Tas::Reset() {
|
||||
if (!Settings::values.tas_enable) {
|
||||
return;
|
||||
}
|
||||
needs_reset = true;
|
||||
}
|
||||
|
||||
bool Tas::Record() {
|
||||
if (!Settings::values.tas_enable) {
|
||||
return true;
|
||||
}
|
||||
is_recording = !is_recording;
|
||||
return is_recording;
|
||||
}
|
||||
|
||||
void Tas::SaveRecording(bool overwrite_file) {
|
||||
if (is_recording) {
|
||||
return;
|
||||
}
|
||||
if (record_commands.empty()) {
|
||||
return;
|
||||
}
|
||||
WriteTasFile(u8"record.txt");
|
||||
if (overwrite_file) {
|
||||
WriteTasFile(u8"script0-1.txt");
|
||||
}
|
||||
needs_reset = true;
|
||||
record_commands.clear();
|
||||
}
|
||||
|
||||
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
|
237
src/input_common/tas/tas_input.h
Normal file
237
src/input_common/tas/tas_input.h
Normal file
|
@ -0,0 +1,237 @@
|
|||
// Copyright 2020 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "common/settings_input.h"
|
||||
#include "core/frontend/input.h"
|
||||
#include "input_common/main.h"
|
||||
|
||||
/*
|
||||
To play back TAS scripts on Yuzu, select the folder with scripts in the configuration menu below
|
||||
Tools -> Configure TAS. The file itself has normal text format and has to be called script0-1.txt
|
||||
for controller 1, script0-2.txt for controller 2 and so forth (with max. 8 players).
|
||||
|
||||
A script file has the same format as TAS-nx uses, so final files will look like this:
|
||||
|
||||
1 KEY_B 0;0 0;0
|
||||
6 KEY_ZL 0;0 0;0
|
||||
41 KEY_ZL;KEY_Y 0;0 0;0
|
||||
43 KEY_X;KEY_A 32767;0 0;0
|
||||
44 KEY_A 32767;0 0;0
|
||||
45 KEY_A 32767;0 0;0
|
||||
46 KEY_A 32767;0 0;0
|
||||
47 KEY_A 32767;0 0;0
|
||||
|
||||
After placing the file at the correct location, it can be read into Yuzu with the (default) hotkey
|
||||
CTRL+F6 (refresh). In the bottom left corner, it will display the amount of frames the script file
|
||||
has. Playback can be started or stopped using CTRL+F5.
|
||||
|
||||
However, for playback to actually work, the correct input device has to be selected: In the Controls
|
||||
menu, select TAS from the device list for the controller that the script should be played on.
|
||||
|
||||
Recording a new script file is really simple: Just make sure that the proper device (not TAS) is
|
||||
connected on P1, and press CTRL+F7 to start recording. When done, just press the same keystroke
|
||||
again (CTRL+F7). The new script will be saved at the location previously selected, as the filename
|
||||
record.txt.
|
||||
|
||||
For debugging purposes, the common controller debugger can be used (View -> Debugging -> Controller
|
||||
P1).
|
||||
*/
|
||||
|
||||
namespace TasInput {
|
||||
|
||||
constexpr size_t PLAYER_NUMBER = 8;
|
||||
|
||||
using TasAnalog = std::pair<float, float>;
|
||||
|
||||
enum class TasState {
|
||||
Running,
|
||||
Recording,
|
||||
Stopped,
|
||||
};
|
||||
|
||||
enum class TasButton : u32 {
|
||||
BUTTON_A = 1U << 0,
|
||||
BUTTON_B = 1U << 1,
|
||||
BUTTON_X = 1U << 2,
|
||||
BUTTON_Y = 1U << 3,
|
||||
STICK_L = 1U << 4,
|
||||
STICK_R = 1U << 5,
|
||||
TRIGGER_L = 1U << 6,
|
||||
TRIGGER_R = 1U << 7,
|
||||
TRIGGER_ZL = 1U << 8,
|
||||
TRIGGER_ZR = 1U << 9,
|
||||
BUTTON_PLUS = 1U << 10,
|
||||
BUTTON_MINUS = 1U << 11,
|
||||
BUTTON_LEFT = 1U << 12,
|
||||
BUTTON_UP = 1U << 13,
|
||||
BUTTON_RIGHT = 1U << 14,
|
||||
BUTTON_DOWN = 1U << 15,
|
||||
BUTTON_SL = 1U << 16,
|
||||
BUTTON_SR = 1U << 17,
|
||||
BUTTON_HOME = 1U << 18,
|
||||
BUTTON_CAPTURE = 1U << 19,
|
||||
};
|
||||
|
||||
enum class TasAxes : u8 {
|
||||
StickX,
|
||||
StickY,
|
||||
SubstickX,
|
||||
SubstickY,
|
||||
Undefined,
|
||||
};
|
||||
|
||||
struct TasData {
|
||||
u32 buttons{};
|
||||
std::array<float, 4> axis{};
|
||||
};
|
||||
|
||||
class Tas {
|
||||
public:
|
||||
Tas();
|
||||
~Tas();
|
||||
|
||||
// Changes the input status that will be stored in each frame
|
||||
void RecordInput(u32 buttons, const std::array<std::pair<float, float>, 2>& axes);
|
||||
|
||||
// Main loop that records or executes input
|
||||
void UpdateThread();
|
||||
|
||||
// Sets the flag to start or stop the TAS command excecution and swaps controllers profiles
|
||||
void StartStop();
|
||||
|
||||
// Stop the TAS and reverts any controller profile
|
||||
void Stop();
|
||||
|
||||
// Sets the flag to reload the file and start from the begining in the next update
|
||||
void Reset();
|
||||
|
||||
/**
|
||||
* Sets the flag to enable or disable recording of inputs
|
||||
* @return Returns true if the current recording status is enabled
|
||||
*/
|
||||
bool Record();
|
||||
|
||||
// Saves contents of record_commands on a file if overwrite is enabled player 1 will be
|
||||
// overwritten with the recorded commands
|
||||
void SaveRecording(bool overwrite_file);
|
||||
|
||||
/**
|
||||
* Returns the current status values of TAS playback/recording
|
||||
* @return Tuple of
|
||||
* TasState indicating the current state out of Running, Recording or Stopped ;
|
||||
* Current playback progress or amount of frames (so far) for Recording ;
|
||||
* Total length of script file currently loaded or amount of frames (so far) for Recording
|
||||
*/
|
||||
std::tuple<TasState, size_t, size_t> GetStatus() const;
|
||||
|
||||
// Retuns an array of the default button mappings
|
||||
InputCommon::ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) const;
|
||||
|
||||
// Retuns an array of the default analog mappings
|
||||
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{};
|
||||
};
|
||||
|
||||
// Loads TAS files from all players
|
||||
void LoadTasFiles();
|
||||
|
||||
// Loads TAS file from the specified player
|
||||
void LoadTasFile(size_t player_index);
|
||||
|
||||
// Writes a TAS file from the recorded commands
|
||||
void WriteTasFile(std::u8string file_name);
|
||||
|
||||
/**
|
||||
* Parses a string containing the axis values with the following format "x;y"
|
||||
* X and Y have a range from -32767 to 32767
|
||||
* @return Returns a TAS analog object with axis values with range from -1.0 to 1.0
|
||||
*/
|
||||
TasAnalog ReadCommandAxis(const std::string& line) const;
|
||||
|
||||
/**
|
||||
* Parses a string containing the button values with the following format "a;b;c;d..."
|
||||
* Each button is represented by it's text format specified in text_to_tas_button array
|
||||
* @return Returns a u32 with each bit representing the status of a button
|
||||
*/
|
||||
u32 ReadCommandButtons(const std::string& line) const;
|
||||
|
||||
/**
|
||||
* Converts an u32 containing the button status into the text equivalent
|
||||
* @return Returns a string with the name of the buttons to be written to the file
|
||||
*/
|
||||
std::string WriteCommandButtons(u32 data) const;
|
||||
|
||||
/**
|
||||
* Converts an TAS analog object containing the axis status into the text equivalent
|
||||
* @return Returns a string with the value of the axis to be written to the file
|
||||
*/
|
||||
std::string WriteCommandAxis(TasAnalog data) const;
|
||||
|
||||
// Inverts the Y axis polarity
|
||||
std::pair<float, float> FlipAxisY(std::pair<float, float> old);
|
||||
|
||||
/**
|
||||
* Converts an u32 containing the button status into the text equivalent
|
||||
* @return Returns a string with the name of the buttons to be printed on console
|
||||
*/
|
||||
std::string DebugButtons(u32 buttons) const;
|
||||
|
||||
/**
|
||||
* Converts an TAS analog object containing the axis status into the text equivalent
|
||||
* @return Returns a string with the value of the axis to be printed on console
|
||||
*/
|
||||
std::string DebugJoystick(float x, float y) const;
|
||||
|
||||
/**
|
||||
* Converts the given TAS status into the text equivalent
|
||||
* @return Returns a string with the value of the TAS status to be printed on console
|
||||
*/
|
||||
std::string DebugInput(const TasData& data) const;
|
||||
|
||||
/**
|
||||
* Converts the given TAS status of multiple players into the text equivalent
|
||||
* @return Returns a string with the value of the status of all TAS players to be printed on
|
||||
* console
|
||||
*/
|
||||
std::string DebugInputs(const std::array<TasData, PLAYER_NUMBER>& arr) const;
|
||||
|
||||
/**
|
||||
* Converts an u32 containing the button status into the text equivalent
|
||||
* @return Returns a string with the name of the buttons
|
||||
*/
|
||||
std::string ButtonsToString(u32 button) const;
|
||||
|
||||
// Stores current controller configuration and sets a TAS controller for every active controller
|
||||
// to the current config
|
||||
void SwapToTasController();
|
||||
|
||||
// Sets the stored controller configuration to the current config
|
||||
void SwapToStoredController();
|
||||
|
||||
size_t script_length{0};
|
||||
std::array<TasData, PLAYER_NUMBER> tas_data;
|
||||
bool is_old_input_saved{false};
|
||||
bool is_recording{false};
|
||||
bool is_running{false};
|
||||
bool needs_reset{false};
|
||||
std::array<std::vector<TASCommand>, PLAYER_NUMBER> commands{};
|
||||
std::vector<TASCommand> record_commands{};
|
||||
size_t current_command{0};
|
||||
TASCommand last_input{}; // only used for recording
|
||||
|
||||
// Old settings for swapping controllers
|
||||
std::array<Settings::PlayerInput, 10> player_mappings;
|
||||
};
|
||||
} // namespace TasInput
|
101
src/input_common/tas/tas_poller.cpp
Normal file
101
src/input_common/tas/tas_poller.cpp
Normal file
|
@ -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
|
43
src/input_common/tas/tas_poller.h
Normal file
43
src/input_common/tas/tas_poller.h
Normal file
|
@ -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 tas bot. It receives tas 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 tas
|
||||
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
|
|
@ -108,6 +108,9 @@ add_executable(yuzu
|
|||
configuration/configure_system.cpp
|
||||
configuration/configure_system.h
|
||||
configuration/configure_system.ui
|
||||
configuration/configure_tas.cpp
|
||||
configuration/configure_tas.h
|
||||
configuration/configure_tas.ui
|
||||
configuration/configure_touch_from_button.cpp
|
||||
configuration/configure_touch_from_button.h
|
||||
configuration/configure_touch_from_button.ui
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
#include "input_common/keyboard.h"
|
||||
#include "input_common/main.h"
|
||||
#include "input_common/mouse/mouse_input.h"
|
||||
#include "input_common/tas/tas_input.h"
|
||||
#include "video_core/renderer_base.h"
|
||||
#include "video_core/video_core.h"
|
||||
#include "yuzu/bootmanager.h"
|
||||
|
@ -312,6 +313,7 @@ GRenderWindow::~GRenderWindow() {
|
|||
}
|
||||
|
||||
void GRenderWindow::OnFrameDisplayed() {
|
||||
input_subsystem->GetTas()->UpdateThread();
|
||||
if (!first_frame) {
|
||||
first_frame = true;
|
||||
emit FirstFrameDisplayed();
|
||||
|
|
|
@ -221,7 +221,7 @@ const std::array<int, Settings::NativeKeyboard::NumKeyboardMods> Config::default
|
|||
// This must be in alphabetical order according to action name as it must have the same order as
|
||||
// UISetting::values.shortcuts, which is alphabetically ordered.
|
||||
// clang-format off
|
||||
const std::array<UISettings::Shortcut, 18> Config::default_hotkeys{{
|
||||
const std::array<UISettings::Shortcut, 21> Config::default_hotkeys{{
|
||||
{QStringLiteral("Capture Screenshot"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+P"), Qt::WidgetWithChildrenShortcut}},
|
||||
{QStringLiteral("Change Docked Mode"), QStringLiteral("Main Window"), {QStringLiteral("F10"), Qt::ApplicationShortcut}},
|
||||
{QStringLiteral("Continue/Pause Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F4"), Qt::WindowShortcut}},
|
||||
|
@ -235,6 +235,9 @@ const std::array<UISettings::Shortcut, 18> Config::default_hotkeys{{
|
|||
{QStringLiteral("Mute Audio"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+M"), Qt::WindowShortcut}},
|
||||
{QStringLiteral("Restart Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F6"), Qt::WindowShortcut}},
|
||||
{QStringLiteral("Stop Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F5"), Qt::WindowShortcut}},
|
||||
{QStringLiteral("TAS Start/Stop"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F5"), Qt::ApplicationShortcut}},
|
||||
{QStringLiteral("TAS Reset"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F6"), Qt::ApplicationShortcut}},
|
||||
{QStringLiteral("TAS Record"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F7"), Qt::ApplicationShortcut}},
|
||||
{QStringLiteral("Toggle Filter Bar"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F"), Qt::WindowShortcut}},
|
||||
{QStringLiteral("Toggle Framerate Limit"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+U"), Qt::ApplicationShortcut}},
|
||||
{QStringLiteral("Toggle Mouse Panning"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F9"), Qt::ApplicationShortcut}},
|
||||
|
@ -564,6 +567,11 @@ void Config::ReadControlValues() {
|
|||
Settings::values.mouse_panning = false;
|
||||
ReadBasicSetting(Settings::values.mouse_panning_sensitivity);
|
||||
|
||||
ReadBasicSetting(Settings::values.tas_enable);
|
||||
ReadBasicSetting(Settings::values.tas_loop);
|
||||
ReadBasicSetting(Settings::values.tas_swap_controllers);
|
||||
ReadBasicSetting(Settings::values.pause_tas_on_load);
|
||||
|
||||
ReadGlobalSetting(Settings::values.use_docked_mode);
|
||||
|
||||
// Disable docked mode if handheld is selected
|
||||
|
@ -661,6 +669,13 @@ void Config::ReadDataStorageValues() {
|
|||
QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir)))
|
||||
.toString()
|
||||
.toStdString());
|
||||
FS::SetYuzuPath(FS::YuzuPath::TASDir,
|
||||
qt_config
|
||||
->value(QStringLiteral("tas_directory"),
|
||||
QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASDir)))
|
||||
.toString()
|
||||
.toStdString());
|
||||
|
||||
ReadBasicSetting(Settings::values.gamecard_inserted);
|
||||
ReadBasicSetting(Settings::values.gamecard_current_game);
|
||||
ReadBasicSetting(Settings::values.gamecard_path);
|
||||
|
@ -1188,6 +1203,11 @@ void Config::SaveControlValues() {
|
|||
WriteBasicSetting(Settings::values.emulate_analog_keyboard);
|
||||
WriteBasicSetting(Settings::values.mouse_panning_sensitivity);
|
||||
|
||||
WriteBasicSetting(Settings::values.tas_enable);
|
||||
WriteBasicSetting(Settings::values.tas_loop);
|
||||
WriteBasicSetting(Settings::values.tas_swap_controllers);
|
||||
WriteBasicSetting(Settings::values.pause_tas_on_load);
|
||||
|
||||
qt_config->endGroup();
|
||||
}
|
||||
|
||||
|
@ -1215,6 +1235,10 @@ void Config::SaveDataStorageValues() {
|
|||
WriteSetting(QStringLiteral("dump_directory"),
|
||||
QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir)),
|
||||
QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir)));
|
||||
WriteSetting(QStringLiteral("tas_directory"),
|
||||
QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASDir)),
|
||||
QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASDir)));
|
||||
|
||||
WriteBasicSetting(Settings::values.gamecard_inserted);
|
||||
WriteBasicSetting(Settings::values.gamecard_current_game);
|
||||
WriteBasicSetting(Settings::values.gamecard_path);
|
||||
|
|
|
@ -42,7 +42,7 @@ public:
|
|||
default_mouse_buttons;
|
||||
static const std::array<int, Settings::NativeKeyboard::NumKeyboardKeys> default_keyboard_keys;
|
||||
static const std::array<int, Settings::NativeKeyboard::NumKeyboardMods> default_keyboard_mods;
|
||||
static const std::array<UISettings::Shortcut, 18> default_hotkeys;
|
||||
static const std::array<UISettings::Shortcut, 21> default_hotkeys;
|
||||
|
||||
private:
|
||||
void Initialize(const std::string& config_name);
|
||||
|
|
|
@ -124,6 +124,19 @@ QString ButtonToText(const Common::ParamPackage& param) {
|
|||
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.Has("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 bool invert_x = param.Get("invert_x", "+") == "-";
|
||||
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") {
|
||||
return QObject::tr("[unused]");
|
||||
}
|
||||
|
@ -926,9 +940,9 @@ void ConfigureInputPlayer::UpdateUI() {
|
|||
|
||||
int slider_value;
|
||||
auto& param = analogs_param[analog_id];
|
||||
const bool is_controller = param.Get("engine", "") == "sdl" ||
|
||||
param.Get("engine", "") == "gcpad" ||
|
||||
param.Get("engine", "") == "mouse";
|
||||
const bool is_controller =
|
||||
param.Get("engine", "") == "sdl" || param.Get("engine", "") == "gcpad" ||
|
||||
param.Get("engine", "") == "mouse" || param.Get("engine", "") == "tas";
|
||||
|
||||
if (is_controller) {
|
||||
if (!param.Has("deadzone")) {
|
||||
|
@ -1045,8 +1059,12 @@ int ConfigureInputPlayer::GetIndexFromControllerType(Settings::ControllerType ty
|
|||
void ConfigureInputPlayer::UpdateInputDevices() {
|
||||
input_devices = input_subsystem->GetInputDevices();
|
||||
ui->comboDevices->clear();
|
||||
for (auto device : input_devices) {
|
||||
ui->comboDevices->addItem(QString::fromStdString(device.Get("display", "Unknown")), {});
|
||||
for (auto& device : input_devices) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -175,7 +175,7 @@ void PlayerControlPreview::ResetInputs() {
|
|||
}
|
||||
|
||||
void PlayerControlPreview::UpdateInput() {
|
||||
if (!is_enabled && !mapping_active) {
|
||||
if (!is_enabled && !mapping_active && !Settings::values.tas_enable) {
|
||||
return;
|
||||
}
|
||||
bool input_changed = false;
|
||||
|
@ -222,6 +222,19 @@ void PlayerControlPreview::UpdateInput() {
|
|||
|
||||
if (input_changed) {
|
||||
update();
|
||||
if (controller_callback.input != nullptr) {
|
||||
ControllerInput input{
|
||||
.axis_values = {std::pair<float, float>{
|
||||
axis_values[Settings::NativeAnalog::LStick].value.x(),
|
||||
axis_values[Settings::NativeAnalog::LStick].value.y()},
|
||||
std::pair<float, float>{
|
||||
axis_values[Settings::NativeAnalog::RStick].value.x(),
|
||||
axis_values[Settings::NativeAnalog::RStick].value.y()}},
|
||||
.button_values = button_values,
|
||||
.changed = true,
|
||||
};
|
||||
controller_callback.input(std::move(input));
|
||||
}
|
||||
}
|
||||
|
||||
if (mapping_active) {
|
||||
|
@ -229,6 +242,10 @@ void PlayerControlPreview::UpdateInput() {
|
|||
}
|
||||
}
|
||||
|
||||
void PlayerControlPreview::SetCallBack(ControllerCallback callback_) {
|
||||
controller_callback = std::move(callback_);
|
||||
}
|
||||
|
||||
void PlayerControlPreview::paintEvent(QPaintEvent* event) {
|
||||
QFrame::paintEvent(event);
|
||||
QPainter p(this);
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include <QPointer>
|
||||
#include "common/settings.h"
|
||||
#include "core/frontend/input.h"
|
||||
#include "yuzu/debugger/controller.h"
|
||||
|
||||
class QLabel;
|
||||
|
||||
|
@ -33,6 +34,7 @@ public:
|
|||
void BeginMappingAnalog(std::size_t button_id);
|
||||
void EndMapping();
|
||||
void UpdateInput();
|
||||
void SetCallBack(ControllerCallback callback_);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
|
@ -181,6 +183,7 @@ private:
|
|||
using StickArray =
|
||||
std::array<std::unique_ptr<Input::AnalogDevice>, Settings::NativeAnalog::NUM_STICKS_HID>;
|
||||
|
||||
ControllerCallback controller_callback;
|
||||
bool is_enabled{};
|
||||
bool mapping_active{};
|
||||
int blink_counter{};
|
||||
|
|
84
src/yuzu/configuration/configure_tas.cpp
Normal file
84
src/yuzu/configuration/configure_tas.cpp
Normal file
|
@ -0,0 +1,84 @@
|
|||
// Copyright 2021 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
#include "common/fs/fs.h"
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/settings.h"
|
||||
#include "ui_configure_tas.h"
|
||||
#include "yuzu/configuration/configure_tas.h"
|
||||
#include "yuzu/uisettings.h"
|
||||
|
||||
ConfigureTasDialog::ConfigureTasDialog(QWidget* parent)
|
||||
: QDialog(parent), ui(std::make_unique<Ui::ConfigureTas>()) {
|
||||
|
||||
ui->setupUi(this);
|
||||
|
||||
setFocusPolicy(Qt::ClickFocus);
|
||||
setWindowTitle(tr("TAS Configuration"));
|
||||
|
||||
connect(ui->tas_path_button, &QToolButton::pressed, this,
|
||||
[this] { SetDirectory(DirectoryTarget::TAS, ui->tas_path_edit); });
|
||||
|
||||
LoadConfiguration();
|
||||
}
|
||||
|
||||
ConfigureTasDialog::~ConfigureTasDialog() = default;
|
||||
|
||||
void ConfigureTasDialog::LoadConfiguration() {
|
||||
ui->tas_path_edit->setText(
|
||||
QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::TASDir)));
|
||||
ui->tas_enable->setChecked(Settings::values.tas_enable.GetValue());
|
||||
ui->tas_control_swap->setChecked(Settings::values.tas_swap_controllers.GetValue());
|
||||
ui->tas_loop_script->setChecked(Settings::values.tas_loop.GetValue());
|
||||
ui->tas_pause_on_load->setChecked(Settings::values.pause_tas_on_load.GetValue());
|
||||
}
|
||||
|
||||
void ConfigureTasDialog::ApplyConfiguration() {
|
||||
Common::FS::SetYuzuPath(Common::FS::YuzuPath::TASDir, ui->tas_path_edit->text().toStdString());
|
||||
Settings::values.tas_enable.SetValue(ui->tas_enable->isChecked());
|
||||
Settings::values.tas_swap_controllers.SetValue(ui->tas_control_swap->isChecked());
|
||||
Settings::values.tas_loop.SetValue(ui->tas_loop_script->isChecked());
|
||||
Settings::values.pause_tas_on_load.SetValue(ui->tas_pause_on_load->isChecked());
|
||||
}
|
||||
|
||||
void ConfigureTasDialog::SetDirectory(DirectoryTarget target, QLineEdit* edit) {
|
||||
QString caption;
|
||||
|
||||
switch (target) {
|
||||
case DirectoryTarget::TAS:
|
||||
caption = tr("Select TAS Load Directory...");
|
||||
break;
|
||||
}
|
||||
|
||||
QString str = QFileDialog::getExistingDirectory(this, caption, edit->text());
|
||||
|
||||
if (str.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (str.back() != QChar::fromLatin1('/')) {
|
||||
str.append(QChar::fromLatin1('/'));
|
||||
}
|
||||
|
||||
edit->setText(str);
|
||||
}
|
||||
|
||||
void ConfigureTasDialog::changeEvent(QEvent* event) {
|
||||
if (event->type() == QEvent::LanguageChange) {
|
||||
RetranslateUI();
|
||||
}
|
||||
|
||||
QDialog::changeEvent(event);
|
||||
}
|
||||
|
||||
void ConfigureTasDialog::RetranslateUI() {
|
||||
ui->retranslateUi(this);
|
||||
}
|
||||
|
||||
void ConfigureTasDialog::HandleApplyButtonClicked() {
|
||||
UISettings::values.configuration_applied = true;
|
||||
ApplyConfiguration();
|
||||
}
|
38
src/yuzu/configuration/configure_tas.h
Normal file
38
src/yuzu/configuration/configure_tas.h
Normal file
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2021 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
namespace Ui {
|
||||
class ConfigureTas;
|
||||
}
|
||||
|
||||
class ConfigureTasDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ConfigureTasDialog(QWidget* parent);
|
||||
~ConfigureTasDialog() override;
|
||||
|
||||
/// Save all button configurations to settings file
|
||||
void ApplyConfiguration();
|
||||
|
||||
private:
|
||||
enum class DirectoryTarget {
|
||||
TAS,
|
||||
};
|
||||
|
||||
void LoadConfiguration();
|
||||
|
||||
void SetDirectory(DirectoryTarget target, QLineEdit* edit);
|
||||
|
||||
void changeEvent(QEvent* event) override;
|
||||
void RetranslateUI();
|
||||
|
||||
void HandleApplyButtonClicked();
|
||||
|
||||
std::unique_ptr<Ui::ConfigureTas> ui;
|
||||
};
|
183
src/yuzu/configuration/configure_tas.ui
Normal file
183
src/yuzu/configuration/configure_tas.ui
Normal file
|
@ -0,0 +1,183 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ConfigureTas</class>
|
||||
<widget class="QDialog" name="ConfigureTas">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_1">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>TAS</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="1">
|
||||
<widget class="QLabel" name="label_1">
|
||||
<property name="text">
|
||||
<string>Reads controller input from scripts in the same format as TAS-nx scripts. For a more detailed explanation please consult the FAQ on the yuzu website.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="1">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>To check which hotkeys control the playback/recording, please refer to the Hotkey settings (General -> Hotkeys).</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="1">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>WARNING: This is an experimental feature. It will not play back scripts frame perfectly with the current, imperfect syncing method.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Settings</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="4">
|
||||
<widget class="QCheckBox" name="tas_enable">
|
||||
<property name="text">
|
||||
<string>Enable TAS features</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="4">
|
||||
<widget class="QCheckBox" name="tas_control_swap">
|
||||
<property name="text">
|
||||
<string>Automatic controller profile swapping</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="4">
|
||||
<widget class="QCheckBox" name="tas_loop_script">
|
||||
<property name="text">
|
||||
<string>Loop script</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="4">
|
||||
<widget class="QCheckBox" name="tas_pause_on_load">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Pause execution during loads</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Script Directory</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Path</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="QToolButton" name="tas_path_button">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="tas_path_edit"/>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Maximum</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>60</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>ConfigureTas</receiver>
|
||||
<slot>accept()</slot>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>ConfigureTas</receiver>
|
||||
<slot>reject()</slot>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
|
@ -99,7 +99,7 @@ void ConfigureVibration::SetVibrationDevices(std::size_t player_index) {
|
|||
const auto guid = param.Get("guid", "");
|
||||
const auto port = param.Get("port", "");
|
||||
|
||||
if (engine.empty() || engine == "keyboard" || engine == "mouse") {
|
||||
if (engine.empty() || engine == "keyboard" || engine == "mouse" || engine == "tas") {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,10 +6,13 @@
|
|||
#include <QLayout>
|
||||
#include <QString>
|
||||
#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/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"));
|
||||
setWindowTitle(tr("Controller P1"));
|
||||
resize(500, 350);
|
||||
|
@ -38,6 +41,9 @@ void ControllerDialog::refreshConfiguration() {
|
|||
constexpr std::size_t player = 0;
|
||||
widget->SetPlayerInputRaw(player, players[player].buttons, players[player].analogs);
|
||||
widget->SetControllerType(players[player].controller_type);
|
||||
ControllerCallback callback{[this](ControllerInput input) { InputController(input); }};
|
||||
widget->SetCallBack(callback);
|
||||
widget->repaint();
|
||||
widget->SetConnectedStatus(players[player].connected);
|
||||
}
|
||||
|
||||
|
@ -67,3 +73,13 @@ void ControllerDialog::hideEvent(QHideEvent* ev) {
|
|||
widget->SetConnectedStatus(false);
|
||||
QWidget::hideEvent(ev);
|
||||
}
|
||||
|
||||
void ControllerDialog::InputController(ControllerInput input) {
|
||||
u32 buttons = 0;
|
||||
int index = 0;
|
||||
for (bool btn : input.button_values) {
|
||||
buttons |= (btn ? 1U : 0U) << index;
|
||||
index++;
|
||||
}
|
||||
input_subsystem->GetTas()->RecordInput(buttons, input.axis_values);
|
||||
}
|
||||
|
|
|
@ -4,18 +4,35 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QWidget>
|
||||
#include "common/settings.h"
|
||||
|
||||
class QAction;
|
||||
class QHideEvent;
|
||||
class QShowEvent;
|
||||
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;
|
||||
};
|
||||
|
||||
class ControllerDialog : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
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.
|
||||
QAction* toggleViewAction();
|
||||
|
@ -26,6 +43,9 @@ protected:
|
|||
void hideEvent(QHideEvent* ev) override;
|
||||
|
||||
private:
|
||||
void InputController(ControllerInput input);
|
||||
QAction* toggle_view_action = nullptr;
|
||||
QFileSystemWatcher* watcher = nullptr;
|
||||
PlayerControlPreview* widget;
|
||||
InputCommon::InputSubsystem* input_subsystem;
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#include "common/nvidia_flags.h"
|
||||
#include "configuration/configure_input.h"
|
||||
#include "configuration/configure_per_game.h"
|
||||
#include "configuration/configure_tas.h"
|
||||
#include "configuration/configure_vibration.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
#include "core/file_sys/vfs_real.h"
|
||||
|
@ -102,6 +103,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
|
|||
#include "core/perf_stats.h"
|
||||
#include "core/telemetry_session.h"
|
||||
#include "input_common/main.h"
|
||||
#include "input_common/tas/tas_input.h"
|
||||
#include "util/overlay_dialog.h"
|
||||
#include "video_core/gpu.h"
|
||||
#include "video_core/renderer_base.h"
|
||||
|
@ -747,6 +749,11 @@ void GMainWindow::InitializeWidgets() {
|
|||
statusBar()->addPermanentWidget(label);
|
||||
}
|
||||
|
||||
tas_label = new QLabel();
|
||||
tas_label->setObjectName(QStringLiteral("TASlabel"));
|
||||
tas_label->setFocusPolicy(Qt::NoFocus);
|
||||
statusBar()->insertPermanentWidget(0, tas_label);
|
||||
|
||||
// Setup Dock button
|
||||
dock_status_button = new QPushButton();
|
||||
dock_status_button->setObjectName(QStringLiteral("TogglableStatusBarButton"));
|
||||
|
@ -841,7 +848,7 @@ void GMainWindow::InitializeDebugWidgets() {
|
|||
waitTreeWidget->hide();
|
||||
debug_menu->addAction(waitTreeWidget->toggleViewAction());
|
||||
|
||||
controller_dialog = new ControllerDialog(this);
|
||||
controller_dialog = new ControllerDialog(this, input_subsystem.get());
|
||||
controller_dialog->hide();
|
||||
debug_menu->addAction(controller_dialog->toggleViewAction());
|
||||
|
||||
|
@ -1014,6 +1021,28 @@ void GMainWindow::InitializeHotkeys() {
|
|||
render_window->setAttribute(Qt::WA_Hover, true);
|
||||
}
|
||||
});
|
||||
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Start/Stop"), this),
|
||||
&QShortcut::activated, this, [&] {
|
||||
if (!emulation_running) {
|
||||
return;
|
||||
}
|
||||
input_subsystem->GetTas()->StartStop();
|
||||
});
|
||||
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Reset"), this),
|
||||
&QShortcut::activated, this, [&] { input_subsystem->GetTas()->Reset(); });
|
||||
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Record"), this),
|
||||
&QShortcut::activated, this, [&] {
|
||||
if (!emulation_running) {
|
||||
return;
|
||||
}
|
||||
bool is_recording = input_subsystem->GetTas()->Record();
|
||||
if (!is_recording) {
|
||||
const auto res = QMessageBox::question(this, tr("TAS Recording"),
|
||||
tr("Overwrite file of player 1?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
input_subsystem->GetTas()->SaveRecording(res == QMessageBox::Yes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void GMainWindow::SetDefaultUIGeometry() {
|
||||
|
@ -1132,6 +1161,7 @@ void GMainWindow::ConnectMenuEvents() {
|
|||
connect(ui.action_Open_FAQ, &QAction::triggered, this, &GMainWindow::OnOpenFAQ);
|
||||
connect(ui.action_Restart, &QAction::triggered, this, [this] { BootGame(QString(game_path)); });
|
||||
connect(ui.action_Configure, &QAction::triggered, this, &GMainWindow::OnConfigure);
|
||||
connect(ui.action_Configure_Tas, &QAction::triggered, this, &GMainWindow::OnConfigureTas);
|
||||
connect(ui.action_Configure_Current_Game, &QAction::triggered, this,
|
||||
&GMainWindow::OnConfigurePerGame);
|
||||
|
||||
|
@ -1464,6 +1494,8 @@ void GMainWindow::ShutdownGame() {
|
|||
game_list->show();
|
||||
}
|
||||
game_list->SetFilterFocus();
|
||||
tas_label->clear();
|
||||
input_subsystem->GetTas()->Stop();
|
||||
|
||||
render_window->removeEventFilter(render_window);
|
||||
render_window->setAttribute(Qt::WA_Hover, false);
|
||||
|
@ -2698,6 +2730,19 @@ void GMainWindow::OnConfigure() {
|
|||
UpdateStatusButtons();
|
||||
}
|
||||
|
||||
void GMainWindow::OnConfigureTas() {
|
||||
const auto& system = Core::System::GetInstance();
|
||||
ConfigureTasDialog dialog(this);
|
||||
const auto result = dialog.exec();
|
||||
|
||||
if (result != QDialog::Accepted && !UISettings::values.configuration_applied) {
|
||||
Settings::RestoreGlobalState(system.IsPoweredOn());
|
||||
return;
|
||||
} else if (result == QDialog::Accepted) {
|
||||
dialog.ApplyConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
void GMainWindow::OnConfigurePerGame() {
|
||||
const u64 title_id = Core::System::GetInstance().CurrentProcess()->GetTitleID();
|
||||
OpenPerGameConfiguration(title_id, game_path.toStdString());
|
||||
|
@ -2874,12 +2919,32 @@ void GMainWindow::UpdateWindowTitle(std::string_view title_name, std::string_vie
|
|||
}
|
||||
}
|
||||
|
||||
QString GMainWindow::GetTasStateDescription() const {
|
||||
auto [tas_status, current_tas_frame, total_tas_frames] = input_subsystem->GetTas()->GetStatus();
|
||||
switch (tas_status) {
|
||||
case TasInput::TasState::Running:
|
||||
return tr("TAS state: Running %1/%2").arg(current_tas_frame).arg(total_tas_frames);
|
||||
case TasInput::TasState::Recording:
|
||||
return tr("TAS state: Recording %1").arg(total_tas_frames);
|
||||
case TasInput::TasState::Stopped:
|
||||
return tr("TAS state: Idle %1/%2").arg(current_tas_frame).arg(total_tas_frames);
|
||||
default:
|
||||
return tr("TAS State: Invalid");
|
||||
}
|
||||
}
|
||||
|
||||
void GMainWindow::UpdateStatusBar() {
|
||||
if (emu_thread == nullptr) {
|
||||
status_bar_update_timer.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Settings::values.tas_enable) {
|
||||
tas_label->setText(GetTasStateDescription());
|
||||
} else {
|
||||
tas_label->clear();
|
||||
}
|
||||
|
||||
auto& system = Core::System::GetInstance();
|
||||
auto results = system.GetAndResetPerfStats();
|
||||
auto& shader_notify = system.GPU().ShaderNotify();
|
||||
|
|
|
@ -259,6 +259,7 @@ private slots:
|
|||
void OnMenuInstallToNAND();
|
||||
void OnMenuRecentFile();
|
||||
void OnConfigure();
|
||||
void OnConfigureTas();
|
||||
void OnConfigurePerGame();
|
||||
void OnLoadAmiibo();
|
||||
void OnOpenYuzuFolder();
|
||||
|
@ -300,6 +301,7 @@ private:
|
|||
void OpenURL(const QUrl& url);
|
||||
void LoadTranslation();
|
||||
void OpenPerGameConfiguration(u64 title_id, const std::string& file_name);
|
||||
QString GetTasStateDescription() const;
|
||||
|
||||
Ui::MainWindow ui;
|
||||
|
||||
|
@ -318,6 +320,7 @@ private:
|
|||
QLabel* emu_speed_label = nullptr;
|
||||
QLabel* game_fps_label = nullptr;
|
||||
QLabel* emu_frametime_label = nullptr;
|
||||
QLabel* tas_label = nullptr;
|
||||
QPushButton* gpu_accuracy_button = nullptr;
|
||||
QPushButton* renderer_status_button = nullptr;
|
||||
QPushButton* dock_status_button = nullptr;
|
||||
|
|
|
@ -100,6 +100,7 @@
|
|||
<addaction name="action_Rederive"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_Capture_Screenshot"/>
|
||||
<addaction name="action_Configure_Tas"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menu_Help">
|
||||
<property name="title">
|
||||
|
@ -294,6 +295,11 @@
|
|||
<string>&Capture Screenshot</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_Configure_Tas">
|
||||
<property name="text">
|
||||
<string>Configure &TAS...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_Configure_Current_Game">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
|
|
Loading…
Reference in a new issue