From b42c3ce21db249d5e3bc04b4f73202e757da317c Mon Sep 17 00:00:00 2001 From: MonsterDruide1 <5958456@gmail.com> Date: Fri, 18 Jun 2021 16:15:42 +0200 Subject: [PATCH 01/11] 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 Co-authored-by: Narr-the-Reg --- src/common/settings.h | 7 + src/input_common/CMakeLists.txt | 4 + src/input_common/main.cpp | 48 +++ src/input_common/main.h | 23 ++ src/input_common/tas/tas_input.cpp | 340 ++++++++++++++++++ src/input_common/tas/tas_input.h | 163 +++++++++ src/input_common/tas/tas_poller.cpp | 101 ++++++ src/input_common/tas/tas_poller.h | 43 +++ .../configuration/configure_input_player.cpp | 30 +- .../configure_input_player_widget.cpp | 16 + .../configure_input_player_widget.h | 3 + src/yuzu/debugger/controller.cpp | 22 +- src/yuzu/debugger/controller.h | 24 +- src/yuzu/main.cpp | 3 +- 14 files changed, 818 insertions(+), 9 deletions(-) create mode 100644 src/input_common/tas/tas_input.cpp create mode 100644 src/input_common/tas/tas_input.h create mode 100644 src/input_common/tas/tas_poller.cpp create mode 100644 src/input_common/tas/tas_poller.h diff --git a/src/common/settings.h b/src/common/settings.h index b1bddb895..29b888f10 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -14,6 +14,7 @@ #include #include +#include #include "common/common_types.h" #include "common/settings_input.h" @@ -499,6 +500,7 @@ struct Values { // Controls InputSetting> players; + std::shared_ptr inputSubsystem = NULL; Setting use_docked_mode{true, "use_docked_mode"}; @@ -512,9 +514,14 @@ struct Values { "motion_device"}; BasicSetting udp_input_servers{"127.0.0.1:26760", "udp_input_servers"}; + BasicSetting tas_enable{false, "tas_enable"}; + BasicSetting tas_reset{ false, "tas_reset" }; + BasicSetting tas_record{ false, "tas_record" }; + BasicSetting mouse_panning{false, "mouse_panning"}; BasicRangedSetting mouse_panning_sensitivity{10, 1, 100, "mouse_panning_sensitivity"}; BasicSetting mouse_enabled{false, "mouse_enabled"}; + std::string mouse_device; MouseButtonsRaw mouse_buttons; diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index c4283a952..dd13d948f 100644 --- a/src/input_common/CMakeLists.txt +++ b/src/input_common/CMakeLists.txt @@ -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 diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp index ff23230f0..4f170493e 100644 --- a/src/input_common/main.cpp +++ b/src/input_common/main.cpp @@ -13,6 +13,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 +62,12 @@ struct InputSubsystem::Impl { Input::RegisterFactory("mouse", mousemotion); mousetouch = std::make_shared(mouse); Input::RegisterFactory("mouse", mousetouch); + + tas = std::make_shared(); + tasbuttons = std::make_shared(tas); + Input::RegisterFactory("tas", tasbuttons); + tasanalog = std::make_shared(tas); + Input::RegisterFactory("tas", tasanalog); } void Shutdown() { @@ -94,12 +102,19 @@ struct InputSubsystem::Impl { mouseanalog.reset(); mousemotion.reset(); mousetouch.reset(); + + Input::UnregisterFactory("tas"); + Input::UnregisterFactory("tas"); + + tasbuttons.reset(); + tasanalog.reset(); } [[nodiscard]] std::vector GetInputDevices() const { std::vector devices = { Common::ParamPackage{{"display", "Any"}, {"class", "any"}}, Common::ParamPackage{{"display", "Keyboard/Mouse"}, {"class", "keyboard"}}, + Common::ParamPackage{{"display", "TAS"}, {"class", "tas"}}, }; #ifdef HAVE_SDL2 auto sdl_devices = sdl->GetInputDevices(); @@ -120,6 +135,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 +154,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 +195,12 @@ struct InputSubsystem::Impl { std::shared_ptr mouseanalog; std::shared_ptr mousemotion; std::shared_ptr mousetouch; + std::shared_ptr tasbuttons; + std::shared_ptr tasanalog; std::shared_ptr udp; std::shared_ptr gcadapter; std::shared_ptr mouse; + std::shared_ptr tas; }; InputSubsystem::InputSubsystem() : impl{std::make_unique()} {} @@ -207,6 +231,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 InputSubsystem::GetInputDevices() const { return impl->GetInputDevices(); } @@ -287,6 +319,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; diff --git a/src/input_common/main.h b/src/input_common/main.h index 5d6f26385..1d06fc5f5 100644 --- a/src/input_common/main.h +++ b/src/input_common/main.h @@ -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 @@ -168,6 +179,18 @@ public: /// Retrieves the underlying udp 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 touch handler. + [[nodiscard]] TasAnalogFactory* GetTasAnalogs(); + + /// Retrieves the underlying tas touch handler. + [[nodiscard]] const TasAnalogFactory* GetTasAnalogs() const; + /// Reloads the input devices void ReloadInputDevices(); diff --git a/src/input_common/tas/tas_input.cpp b/src/input_common/tas/tas_input.cpp new file mode 100644 index 000000000..343641945 --- /dev/null +++ b/src/input_common/tas/tas_input.cpp @@ -0,0 +1,340 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include + +#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 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, 2> axes) { + lastInput = {buttons, flipY(axes[0]), flipY(axes[1])}; +} + +std::pair Tas::flipY(std::pair 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 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 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(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(x * 32767)); + line += ";"; + line += std::to_string(static_cast(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(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, 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(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(TasAxes::StickX)); + left_analog_params.Set("axis_y", static_cast(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(TasAxes::SubstickX)); + right_analog_params.Set("axis_y", static_cast(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 diff --git a/src/input_common/tas/tas_input.h b/src/input_common/tas/tas_input.h new file mode 100644 index 000000000..0a152a04f --- /dev/null +++ b/src/input_common/tas/tas_input.h @@ -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 +#include +#include + +#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; + +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, 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 axis{}; +}; + +class Tas { +public: + Tas(); + ~Tas(); + + static std::string buttonsToString(u32 button) { + std::string returns; + if ((button & static_cast(TasInput::TasButton::BUTTON_A)) != 0) + returns += ", A"; + if ((button & static_cast(TasInput::TasButton::BUTTON_B)) != 0) + returns += ", B"; + if ((button & static_cast(TasInput::TasButton::BUTTON_X)) != 0) + returns += ", X"; + if ((button & static_cast(TasInput::TasButton::BUTTON_Y)) != 0) + returns += ", Y"; + if ((button & static_cast(TasInput::TasButton::STICK_L)) != 0) + returns += ", STICK_L"; + if ((button & static_cast(TasInput::TasButton::STICK_R)) != 0) + returns += ", STICK_R"; + if ((button & static_cast(TasInput::TasButton::TRIGGER_L)) != 0) + returns += ", TRIGGER_L"; + if ((button & static_cast(TasInput::TasButton::TRIGGER_R)) != 0) + returns += ", TRIGGER_R"; + if ((button & static_cast(TasInput::TasButton::TRIGGER_ZL)) != 0) + returns += ", TRIGGER_ZL"; + if ((button & static_cast(TasInput::TasButton::TRIGGER_ZR)) != 0) + returns += ", TRIGGER_ZR"; + if ((button & static_cast(TasInput::TasButton::BUTTON_PLUS)) != 0) + returns += ", PLUS"; + if ((button & static_cast(TasInput::TasButton::BUTTON_MINUS)) != 0) + returns += ", MINUS"; + if ((button & static_cast(TasInput::TasButton::BUTTON_LEFT)) != 0) + returns += ", LEFT"; + if ((button & static_cast(TasInput::TasButton::BUTTON_UP)) != 0) + returns += ", UP"; + if ((button & static_cast(TasInput::TasButton::BUTTON_RIGHT)) != 0) + returns += ", RIGHT"; + if ((button & static_cast(TasInput::TasButton::BUTTON_DOWN)) != 0) + returns += ", DOWN"; + if ((button & static_cast(TasInput::TasButton::BUTTON_SL)) != 0) + returns += ", SL"; + if ((button & static_cast(TasInput::TasButton::BUTTON_SR)) != 0) + returns += ", SR"; + if ((button & static_cast(TasInput::TasButton::BUTTON_HOME)) != 0) + returns += ", HOME"; + if ((button & static_cast(TasInput::TasButton::BUTTON_CAPTURE)) != 0) + returns += ", CAPTURE"; + return returns.length() != 0 ? returns.substr(2) : ""; + } + + void RefreshTasFile(); + void LoadTasFiles(); + void RecordInput(u32 buttons, std::array, 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 flipY(std::pair old) const; + + size_t scriptLength{0}; + std::array tas_data; + bool update_thread_running{true}; + bool refresh_tas_fle{false}; + std::array, PLAYER_NUMBER> newCommands{}; + std::vector recordCommands{}; + std::size_t current_command{0}; + TASCommand lastInput{}; // only used for recording +}; +} // namespace TasInput diff --git a/src/input_common/tas/tas_poller.cpp b/src/input_common/tas/tas_poller.cpp new file mode 100644 index 000000000..15810d6b0 --- /dev/null +++ b/src/input_common/tas/tas_poller.cpp @@ -0,0 +1,101 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include + +#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 tas_input_) + : tas_input(std::move(tas_input_)) {} + +std::unique_ptr 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(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 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 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 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 TasAnalogFactory::Create(const Common::ParamPackage& params) { + const auto pad = static_cast(params.Get("pad", 0)); + const auto axis_x = static_cast(params.Get("axis_x", 0)); + const auto axis_y = static_cast(params.Get("axis_y", 1)); + + return std::make_unique(pad, axis_x, axis_y, tas_input.get()); +} + +} // namespace InputCommon diff --git a/src/input_common/tas/tas_poller.h b/src/input_common/tas/tas_poller.h new file mode 100644 index 000000000..1bc0d173b --- /dev/null +++ b/src/input_common/tas/tas_poller.h @@ -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 +#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 { +public: + explicit TasButtonFactory(std::shared_ptr 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 Create(const Common::ParamPackage& params) override; + +private: + std::shared_ptr tas_input; +}; + +/// An analog device factory that creates analog devices from mouse +class TasAnalogFactory final : public Input::Factory { +public: + explicit TasAnalogFactory(std::shared_ptr tas_input_); + + std::unique_ptr Create(const Common::ParamPackage& params) override; + +private: + std::shared_ptr tas_input; +}; + +} // namespace InputCommon diff --git a/src/yuzu/configuration/configure_input_player.cpp b/src/yuzu/configuration/configure_input_player.cpp index 7527c068b..88f4bf388 100644 --- a/src/yuzu/configuration/configure_input_player.cpp +++ b/src/yuzu/configuration/configure_input_player.cpp @@ -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(player_index)); + } } } diff --git a/src/yuzu/configuration/configure_input_player_widget.cpp b/src/yuzu/configuration/configure_input_player_widget.cpp index 9c890ed5d..e649e2169 100644 --- a/src/yuzu/configuration/configure_input_player_widget.cpp +++ b/src/yuzu/configuration/configure_input_player_widget.cpp @@ -220,8 +220,20 @@ void PlayerControlPreview::UpdateInput() { } } + ControllerInput input{}; if (input_changed) { 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) { @@ -229,6 +241,10 @@ void PlayerControlPreview::UpdateInput() { } } +void PlayerControlPreview::SetCallBack(ControllerCallback callback_) { + controller_callback = callback_; +} + void PlayerControlPreview::paintEvent(QPaintEvent* event) { QFrame::paintEvent(event); QPainter p(this); diff --git a/src/yuzu/configuration/configure_input_player_widget.h b/src/yuzu/configuration/configure_input_player_widget.h index f4a6a5e1b..f4bbfa528 100644 --- a/src/yuzu/configuration/configure_input_player_widget.h +++ b/src/yuzu/configuration/configure_input_player_widget.h @@ -9,6 +9,7 @@ #include #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, Settings::NativeAnalog::NUM_STICKS_HID>; + ControllerCallback controller_callback; bool is_enabled{}; bool mapping_active{}; int blink_counter{}; diff --git a/src/yuzu/debugger/controller.cpp b/src/yuzu/debugger/controller.cpp index c1fc69578..822d033d1 100644 --- a/src/yuzu/debugger/controller.cpp +++ b/src/yuzu/debugger/controller.cpp @@ -6,10 +6,13 @@ #include #include #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,17 @@ void ControllerDialog::hideEvent(QHideEvent* ev) { widget->SetConnectedStatus(false); 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); +} \ No newline at end of file diff --git a/src/yuzu/debugger/controller.h b/src/yuzu/debugger/controller.h index c54750070..659923e1b 100644 --- a/src/yuzu/debugger/controller.h +++ b/src/yuzu/debugger/controller.h @@ -4,18 +4,36 @@ #pragma once +#include #include +#include "common/settings.h" class QAction; class QHideEvent; class QShowEvent; class PlayerControlPreview; +namespace InputCommon { +class InputSubsystem; +} + +struct ControllerInput { + std::array, Settings::NativeAnalog::NUM_STICKS_HID> axis_values{}; + std::array button_values{}; + bool changed{}; +}; + +struct ControllerCallback { + std::function input; + std::function update; +}; + 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 +44,10 @@ protected: void hideEvent(QHideEvent* ev) override; private: + void RefreshTasFile(); + void InputController(ControllerInput input); QAction* toggle_view_action = nullptr; + QFileSystemWatcher* watcher = nullptr; PlayerControlPreview* widget; + InputCommon::InputSubsystem* input_subsystem; }; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index f4e49001d..22489231b 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -193,6 +193,7 @@ GMainWindow::GMainWindow() config{std::make_unique()}, vfs{std::make_shared()}, provider{std::make_unique()} { Common::Log::Initialize(); + Settings::values.inputSubsystem = input_subsystem; LoadTranslation(); setAcceptDrops(true); @@ -841,7 +842,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()); From f25d6ebc45dc02904ef3ca6ad8efea9a0853d194 Mon Sep 17 00:00:00 2001 From: MonsterDruide1 <5958456@gmail.com> Date: Fri, 18 Jun 2021 16:20:10 +0200 Subject: [PATCH 02/11] settings: File selector & other settings First of all, TASing requires a script to play back. The user can select the parent directory at `System -> Filesystem`, next to an option to pause TAS during loads: This requires a "hacky" setup deeper in the code and will be added in the last commit. Also, Hotkeys are being introduced: CTRL+F5 for playback start/stop, CTRL+F6 for re-reading the script and CTRL+F7 for recording a new script. --- src/common/fs/path_util.cpp | 2 + src/common/fs/path_util.h | 2 + src/common/settings.h | 3 ++ src/yuzu/configuration/config.cpp | 24 ++++++++- src/yuzu/configuration/config.h | 2 +- .../configuration/configure_filesystem.cpp | 10 ++++ src/yuzu/configuration/configure_filesystem.h | 1 + .../configuration/configure_filesystem.ui | 49 +++++++++++++++++++ src/yuzu/main.cpp | 13 +++++ 9 files changed, 104 insertions(+), 2 deletions(-) diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index 6cdd14f13..5f76adedb 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -116,6 +116,8 @@ private: GenerateYuzuPath(YuzuPath::ScreenshotsDir, yuzu_path / SCREENSHOTS_DIR); GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR); GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR); + + GenerateYuzuPath(YuzuPath::TASFile, fs::path{""}); } ~PathManagerImpl() = default; diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h index f956ac9a2..6079de4c6 100644 --- a/src/common/fs/path_util.h +++ b/src/common/fs/path_util.h @@ -23,6 +23,8 @@ enum class YuzuPath { ScreenshotsDir, // Where yuzu screenshots are stored. SDMCDir, // Where the emulated SDMC is stored. ShaderDir, // Where shaders are stored. + + TASFile, // Where the current script file is stored. }; /** diff --git a/src/common/settings.h b/src/common/settings.h index 29b888f10..884ea55f8 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -550,6 +550,9 @@ struct Values { BasicSetting gamecard_current_game{false, "gamecard_current_game"}; BasicSetting gamecard_path{std::string(), "gamecard_path"}; + // TAS + bool pauseTasOnLoad; + // Debugging bool record_frame_times; BasicSetting use_gdbstub{false, "use_gdbstub"}; diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 952e96769..99e318a8f 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -221,7 +221,7 @@ const std::array 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 Config::default_hotkeys{{ +const std::array 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 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,9 @@ void Config::ReadControlValues() { Settings::values.mouse_panning = false; ReadBasicSetting(Settings::values.mouse_panning_sensitivity); + ReadBasicSetting(Settings::values.tas_enable = false); + ReadBasicSetting(Settings::values.tas_reset = false); + ReadGlobalSetting(Settings::values.use_docked_mode); // Disable docked mode if handheld is selected @@ -661,9 +667,20 @@ void Config::ReadDataStorageValues() { QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir))) .toString() .toStdString()); + FS::SetYuzuPath( + FS::YuzuPath::TASFile, + qt_config + ->value(QStringLiteral("tas_path"), + QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASFile))) + .toString() + .toStdString()); + + ReadBasicSetting(Settings::values.pauseTasOnLoad); + ReadBasicSetting(Settings::values.gamecard_inserted); ReadBasicSetting(Settings::values.gamecard_current_game); ReadBasicSetting(Settings::values.gamecard_path); + qt_config->endGroup(); } @@ -1215,6 +1232,11 @@ void Config::SaveDataStorageValues() { WriteSetting(QStringLiteral("dump_directory"), QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir)), QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir))); + WriteSetting(QStringLiteral("tas_path"), + QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASFile)), + QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASFile))); + WriteSetting(QStringLiteral("tas_pause_on_load"), Settings::values.pauseTasOnLoad, true); + WriteBasicSetting(Settings::values.gamecard_inserted); WriteBasicSetting(Settings::values.gamecard_current_game); WriteBasicSetting(Settings::values.gamecard_path); diff --git a/src/yuzu/configuration/config.h b/src/yuzu/configuration/config.h index 4733227b6..3ee694e7c 100644 --- a/src/yuzu/configuration/config.h +++ b/src/yuzu/configuration/config.h @@ -42,7 +42,7 @@ public: default_mouse_buttons; static const std::array default_keyboard_keys; static const std::array default_keyboard_mods; - static const std::array default_hotkeys; + static const std::array default_hotkeys; private: void Initialize(const std::string& config_name); diff --git a/src/yuzu/configuration/configure_filesystem.cpp b/src/yuzu/configuration/configure_filesystem.cpp index 9cb317822..cd9ba0a90 100644 --- a/src/yuzu/configuration/configure_filesystem.cpp +++ b/src/yuzu/configuration/configure_filesystem.cpp @@ -26,6 +26,8 @@ ConfigureFilesystem::ConfigureFilesystem(QWidget* parent) [this] { SetDirectory(DirectoryTarget::Dump, ui->dump_path_edit); }); connect(ui->load_path_button, &QToolButton::pressed, this, [this] { SetDirectory(DirectoryTarget::Load, ui->load_path_edit); }); + connect(ui->tas_path_button, &QToolButton::pressed, this, + [this] { SetDirectory(DirectoryTarget::TAS, ui->tas_path_edit); }); connect(ui->reset_game_list_cache, &QPushButton::pressed, this, &ConfigureFilesystem::ResetMetadata); @@ -49,9 +51,12 @@ void ConfigureFilesystem::setConfiguration() { QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::DumpDir))); ui->load_path_edit->setText( QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::LoadDir))); + ui->tas_path_edit->setText( + QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::TASFile))); ui->gamecard_inserted->setChecked(Settings::values.gamecard_inserted.GetValue()); ui->gamecard_current_game->setChecked(Settings::values.gamecard_current_game.GetValue()); + ui->tas_pause_on_load->setChecked(Settings::values.pauseTasOnLoad); ui->dump_exefs->setChecked(Settings::values.dump_exefs.GetValue()); ui->dump_nso->setChecked(Settings::values.dump_nso.GetValue()); @@ -69,9 +74,11 @@ void ConfigureFilesystem::applyConfiguration() { ui->dump_path_edit->text().toStdString()); Common::FS::SetYuzuPath(Common::FS::YuzuPath::LoadDir, ui->load_path_edit->text().toStdString()); + Common::FS::SetYuzuPath(Common::FS::YuzuPath::TASFile, ui->tas_path_edit->text().toStdString()); Settings::values.gamecard_inserted = ui->gamecard_inserted->isChecked(); Settings::values.gamecard_current_game = ui->gamecard_current_game->isChecked(); + Settings::values.pauseTasOnLoad = ui->tas_pause_on_load->isChecked(); Settings::values.dump_exefs = ui->dump_exefs->isChecked(); Settings::values.dump_nso = ui->dump_nso->isChecked(); @@ -97,6 +104,9 @@ void ConfigureFilesystem::SetDirectory(DirectoryTarget target, QLineEdit* edit) case DirectoryTarget::Load: caption = tr("Select Mod Load Directory..."); break; + case DirectoryTarget::TAS: + caption = tr("Select TAS Directory..."); + break; } QString str; diff --git a/src/yuzu/configuration/configure_filesystem.h b/src/yuzu/configuration/configure_filesystem.h index 2147cd405..86dab8684 100644 --- a/src/yuzu/configuration/configure_filesystem.h +++ b/src/yuzu/configuration/configure_filesystem.h @@ -32,6 +32,7 @@ private: Gamecard, Dump, Load, + TAS, }; void SetDirectory(DirectoryTarget target, QLineEdit* edit); diff --git a/src/yuzu/configuration/configure_filesystem.ui b/src/yuzu/configuration/configure_filesystem.ui index 62b9abc7a..8ac7250fd 100644 --- a/src/yuzu/configuration/configure_filesystem.ui +++ b/src/yuzu/configuration/configure_filesystem.ui @@ -219,6 +219,55 @@ + + + + TAS Directories + + + + + + Path + + + + + + + ... + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 60 + 20 + + + + + + + + Pause TAS execution during loads (SMO - 1.3) + + + + + + diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 22489231b..f3529d151 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -1015,6 +1015,19 @@ void GMainWindow::InitializeHotkeys() { render_window->setAttribute(Qt::WA_Hover, true); } }); + connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Start/Stop"), this), + &QShortcut::activated, this, [&] { + Settings::values.tas_enable = !Settings::values.tas_enable; + LOG_INFO(Frontend, "Tas enabled {}", Settings::values.tas_enable); + }); + + connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Reset"), this), + &QShortcut::activated, this, [&] { Settings::values.tas_reset = true; }); + connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Record"), this), + &QShortcut::activated, this, [&] { + Settings::values.tas_record = !Settings::values.tas_record; + LOG_INFO(Frontend, "Tas recording {}", Settings::values.tas_record); + }); } void GMainWindow::SetDefaultUIGeometry() { From 3a7b37238bc85b8220660e8469543449095bc820 Mon Sep 17 00:00:00 2001 From: MonsterDruide1 <5958456@gmail.com> Date: Fri, 18 Jun 2021 16:21:45 +0200 Subject: [PATCH 03/11] main: TAS Playback state label During script playback/recording, the user has to see what happens currently. For that, a new label has been added to the bottom-left corner, always displaying the current state of the TASing system. --- src/yuzu/main.cpp | 9 +++++++++ src/yuzu/main.h | 1 + 2 files changed, 10 insertions(+) diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index f3529d151..10057b9ca 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -30,6 +30,8 @@ #include "core/hle/service/am/applet_oe.h" #include "core/hle/service/am/applets/applets.h" +#include "input_common/tas/tas_input.h" + // These are wrappers to avoid the calls to CreateDirectory and CreateFile because of the Windows // defines. static FileSys::VirtualDir VfsFilesystemCreateDirectoryWrapper( @@ -824,6 +826,12 @@ void GMainWindow::InitializeWidgets() { }); statusBar()->insertPermanentWidget(0, renderer_status_button); + TASlabel = new QLabel(); + TASlabel->setObjectName(QStringLiteral("TASlabel")); + TASlabel->setText(tr("TAS not running")); + TASlabel->setFocusPolicy(Qt::NoFocus); + statusBar()->insertPermanentWidget(0, TASlabel); + statusBar()->setVisible(true); setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}")); } @@ -2894,6 +2902,7 @@ void GMainWindow::UpdateStatusBar() { return; } + TASlabel->setText(tr(input_subsystem->GetTas()->GetStatusDescription().c_str())); auto& system = Core::System::GetInstance(); auto results = system.GetAndResetPerfStats(); auto& shader_notify = system.GPU().ShaderNotify(); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 38e66ccd0..edca661ac 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -318,6 +318,7 @@ private: QLabel* emu_speed_label = nullptr; QLabel* game_fps_label = nullptr; QLabel* emu_frametime_label = nullptr; + QLabel* TASlabel; QPushButton* gpu_accuracy_button = nullptr; QPushButton* renderer_status_button = nullptr; QPushButton* dock_status_button = nullptr; From 4297d2fea2228ff4afe2a7c244fb8b3f1a97491a Mon Sep 17 00:00:00 2001 From: MonsterDruide1 <5958456@gmail.com> Date: Fri, 18 Jun 2021 16:32:46 +0200 Subject: [PATCH 04/11] core: Hacky TAS syncing & load pausing To keep the TAS inputs synced to the game speed even through lag spikes and loading zones, deeper access is required. First, the `TAS::UpdateThread` has to be executed exactly once per frame. This is done by connecting it to the service method the game calls to pass parameters to the GPU: `Service::VI::QueueBuffer`. Second, the loading time of new subareas and/or kingdoms (SMO) can vary. To counteract that, the `CPU_BOOST_MODE` can be detected: In the `APM`-interface, the call to enabling/disabling the boost mode can be caught and forwarded to the TASing system, which can pause the script execution if neccessary and enabled in the settings. --- src/common/fs/fs_paths.h | 1 + src/common/fs/path_util.cpp | 3 +- src/common/settings.h | 9 +- src/core/hle/service/apm/apm_interface.cpp | 2 + src/core/hle/service/vi/vi.cpp | 3 + src/input_common/tas/tas_input.cpp | 142 +++++++++++------- src/input_common/tas/tas_input.h | 58 +++---- .../configuration/configure_filesystem.cpp | 2 +- src/yuzu/main.cpp | 27 +++- 9 files changed, 140 insertions(+), 107 deletions(-) diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h index b32614797..84968b8e0 100644 --- a/src/common/fs/fs_paths.h +++ b/src/common/fs/fs_paths.h @@ -21,6 +21,7 @@ #define SCREENSHOTS_DIR "screenshots" #define SDMC_DIR "sdmc" #define SHADER_DIR "shader" +#define TAS_DIR "scripts" // yuzu-specific files diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index 5f76adedb..97d026eb8 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -116,8 +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::TASFile, fs::path{""}); + GenerateYuzuPath(YuzuPath::TASFile, yuzu_path / TAS_DIR); } ~PathManagerImpl() = default; diff --git a/src/common/settings.h b/src/common/settings.h index 884ea55f8..7333a64dc 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -500,7 +500,6 @@ struct Values { // Controls InputSetting> players; - std::shared_ptr inputSubsystem = NULL; Setting use_docked_mode{true, "use_docked_mode"}; @@ -514,9 +513,12 @@ struct Values { "motion_device"}; BasicSetting udp_input_servers{"127.0.0.1:26760", "udp_input_servers"}; - BasicSetting tas_enable{false, "tas_enable"}; + BasicSetting pause_tas_on_load { false, "pause_tas_on_load" }; + BasicSetting tas_enable{ false, "tas_enable" }; BasicSetting tas_reset{ false, "tas_reset" }; BasicSetting tas_record{ false, "tas_record" }; + BasicSetting is_cpu_boxted{ false, " BasicSetting is_cpu_boxted{ false, "cpuBoosted" }; +" }; BasicSetting mouse_panning{false, "mouse_panning"}; BasicRangedSetting mouse_panning_sensitivity{10, 1, 100, "mouse_panning_sensitivity"}; @@ -550,9 +552,6 @@ struct Values { BasicSetting gamecard_current_game{false, "gamecard_current_game"}; BasicSetting gamecard_path{std::string(), "gamecard_path"}; - // TAS - bool pauseTasOnLoad; - // Debugging bool record_frame_times; BasicSetting use_gdbstub{false, "use_gdbstub"}; diff --git a/src/core/hle/service/apm/apm_interface.cpp b/src/core/hle/service/apm/apm_interface.cpp index e58bad083..724483107 100644 --- a/src/core/hle/service/apm/apm_interface.cpp +++ b/src/core/hle/service/apm/apm_interface.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include "common/logging/log.h" +#include "common/settings.h" #include "core/hle/ipc_helpers.h" #include "core/hle/service/apm/apm.h" #include "core/hle/service/apm/apm_controller.h" @@ -120,6 +121,7 @@ void APM_Sys::SetCpuBoostMode(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_APM, "called, mode={:08X}", mode); + Settings::values.is_cpu_boosted = (static_cast(mode) == 1); controller.SetFromCpuBoostMode(mode); IPC::ResponseBuilder rb{ctx, 2}; diff --git a/src/core/hle/service/vi/vi.cpp b/src/core/hle/service/vi/vi.cpp index 8e8fc40ca..f4eac0bca 100644 --- a/src/core/hle/service/vi/vi.cpp +++ b/src/core/hle/service/vi/vi.cpp @@ -32,6 +32,8 @@ #include "core/hle/service/vi/vi_s.h" #include "core/hle/service/vi/vi_u.h" +#include "input_common/tas/tas_input.h" + namespace Service::VI { constexpr ResultCode ERR_OPERATION_FAILED{ErrorModule::VI, 1}; @@ -595,6 +597,7 @@ private: IGBPQueueBufferResponseParcel response{1280, 720}; ctx.WriteBuffer(response.Serialize()); + Settings::values.input_subsystem->GetTas()->UpdateThread(); break; } case TransactionId::Query: { diff --git a/src/input_common/tas/tas_input.cpp b/src/input_common/tas/tas_input.cpp index 343641945..7320a7004 100644 --- a/src/input_common/tas/tas_input.cpp +++ b/src/input_common/tas/tas_input.cpp @@ -19,6 +19,29 @@ namespace TasInput { +constexpr std::array, 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() { LoadTasFiles(); } @@ -31,29 +54,31 @@ void Tas::RefreshTasFile() { refresh_tas_fle = true; } void Tas::LoadTasFiles() { - scriptLength = 0; - for (int i = 0; i < PLAYER_NUMBER; i++) { + script_length = 0; + for (size_t i = 0; i < PLAYER_NUMBER; i++) { LoadTasFile(i); - if (newCommands[i].size() > scriptLength) - scriptLength = newCommands[i].size(); + if (commands[i].size() > script_length) { + script_length = commands[i].size(); + } } } -void Tas::LoadTasFile(int playerIndex) { +void Tas::LoadTasFile(size_t player_index) { LOG_DEBUG(Input, "LoadTasFile()"); - if (!newCommands[playerIndex].empty()) { - newCommands[playerIndex].clear(); + if (!commands[player_index].empty()) { + commands[player_index].clear(); } std::string file = Common::FS::ReadStringFromFile( Common::FS::GetYuzuPathString(Common::FS::YuzuPath::TASFile) + "script0-" + - std::to_string(playerIndex + 1) + ".txt", + std::to_string(player_index + 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()) + if (line.empty()) { continue; + } LOG_DEBUG(Input, "Loading line: {}", line); std::smatch m; @@ -65,11 +90,12 @@ void Tas::LoadTasFile(int playerIndex) { seglist.push_back(segment); } - if (seglist.size() < 4) + if (seglist.size() < 4) { continue; + } while (frameNo < std::stoi(seglist.at(0))) { - newCommands[playerIndex].push_back(empty); + commands[player_index].push_back(empty); frameNo++; } @@ -78,7 +104,7 @@ void Tas::LoadTasFile(int playerIndex) { .l_axis = ReadCommandAxis(seglist.at(2)), .r_axis = ReadCommandAxis(seglist.at(3)), }; - newCommands[playerIndex].push_back(command); + commands[player_index].push_back(command); frameNo++; } LOG_INFO(Input, "TAS file loaded! {} frames", frameNo); @@ -87,84 +113,89 @@ void Tas::LoadTasFile(int playerIndex) { void Tas::WriteTasFile() { LOG_DEBUG(Input, "WriteTasFile()"); std::string output_text = ""; - for (int frame = 0; frame < (signed)recordCommands.size(); frame++) { - if (!output_text.empty()) + for (int frame = 0; frame < (signed)record_commands.size(); frame++) { + if (!output_text.empty()) { output_text += "\n"; - TASCommand line = recordCommands.at(frame); + } + TASCommand line = record_commands.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()) + if (bytesWritten == output_text.size()) { LOG_INFO(Input, "TAS file written to file!"); - else + } + else { LOG_ERROR(Input, "Writing the TAS-file has failed! {} / {} bytes written", bytesWritten, output_text.size()); + } } -void Tas::RecordInput(u32 buttons, std::array, 2> axes) { - lastInput = {buttons, flipY(axes[0]), flipY(axes[1])}; -} - -std::pair Tas::flipY(std::pair old) const { +static std::pair FlipY(std::pair old) { auto [x, y] = old; return {x, -y}; } -std::string Tas::GetStatusDescription() { +void Tas::RecordInput(u32 buttons, const std::array, 2>& axes) { + last_input = {buttons, FlipY(axes[0]), FlipY(axes[1])}; +} + +std::tuple Tas::GetStatus() { + TasState state; if (Settings::values.tas_record) { - return "Recording TAS: " + std::to_string(recordCommands.size()); + return {TasState::RECORDING, record_commands.size(), record_commands.size()}; + } else if (Settings::values.tas_enable) { + state = TasState::RUNNING; + } else { + state = TasState::STOPPED; } - 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); + + return {state, current_command, script_length}; } -std::string debugButtons(u32 buttons) { - return "{ " + TasInput::Tas::buttonsToString(buttons) + " }"; +static std::string DebugButtons(u32 buttons) { + return "{ " + TasInput::Tas::ButtonsToString(buttons) + " }"; } -std::string debugJoystick(float x, float y) { +static 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]) + " }"; +static std::string DebugInput(const 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 arr) { +static std::string DebugInputs(const std::array& arr) { std::string returns = "[ "; for (size_t i = 0; i < arr.size(); i++) { - returns += debugInput(arr[i]); - if (i != arr.size() - 1) + 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++) { + if (Settings::values.pause_tas_on_load && Settings::values.is_cpu_boosted) { + for (size_t i = 0; i < PLAYER_NUMBER; i++) { tas_data[i].buttons = 0; tas_data[i].axis = {}; } } if (Settings::values.tas_record) { - recordCommands.push_back(lastInput); + record_commands.push_back(last_input); } - if (!Settings::values.tas_record && !recordCommands.empty()) { + if (!Settings::values.tas_record && !record_commands.empty()) { WriteTasFile(); Settings::values.tas_reset = true; refresh_tas_fle = true; - recordCommands.clear(); + record_commands.clear(); } if (Settings::values.tas_reset) { current_command = 0; @@ -177,12 +208,12 @@ void Tas::UpdateThread() { LOG_DEBUG(Input, "tas_reset done"); } if (Settings::values.tas_enable) { - if ((signed)current_command < scriptLength) { - LOG_INFO(Input, "Playing TAS {}/{}", current_command, scriptLength); + if ((signed)current_command < script_length) { + LOG_INFO(Input, "Playing TAS {}/{}", current_command, script_length); size_t frame = current_command++; - for (int i = 0; i < PLAYER_NUMBER; i++) { - if (frame < newCommands[i].size()) { - TASCommand command = newCommands[i][frame]; + for (size_t i = 0; i < PLAYER_NUMBER; 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; @@ -198,22 +229,22 @@ void Tas::UpdateThread() { } else { Settings::values.tas_enable = false; current_command = 0; - for (int i = 0; i < PLAYER_NUMBER; i++) { + for (size_t i = 0; i < PLAYER_NUMBER; i++) { tas_data[i].buttons = 0; tas_data[i].axis = {}; } } } else { - for (int i = 0; i < PLAYER_NUMBER; i++) { + for (size_t i = 0; i < PLAYER_NUMBER; i++) { tas_data[i].buttons = 0; tas_data[i].axis = {}; } } } - LOG_DEBUG(Input, "TAS inputs: {}", debugInputs(tas_data)); + LOG_DEBUG(Input, "TAS inputs: {}", DebugInputs(tas_data)); } -TasAnalog Tas::ReadCommandAxis(const std::string line) const { +TasAnalog Tas::ReadCommandAxis(const std::string& line) const { std::stringstream linestream(line); std::string segment; std::vector seglist; @@ -228,7 +259,7 @@ TasAnalog Tas::ReadCommandAxis(const std::string line) const { return {x, y}; } -u32 Tas::ReadCommandButtons(const std::string data) const { +u32 Tas::ReadCommandButtons(const std::string& data) const { std::stringstream button_text(data); std::string line; u32 buttons = 0; @@ -262,8 +293,9 @@ std::string Tas::WriteCommandButtons(u32 data) const { if ((data & 1) == 1) { for (auto [text, tas_button] : text_to_tas_button) { if (tas_button == static_cast(1 << index)) { - if (line.size() > 0) + if (line.size() > 0) { line += ";"; + } line += text; break; } diff --git a/src/input_common/tas/tas_input.h b/src/input_common/tas/tas_input.h index 0a152a04f..8ee70bcaf 100644 --- a/src/input_common/tas/tas_input.h +++ b/src/input_common/tas/tas_input.h @@ -12,11 +12,17 @@ #include "core/frontend/input.h" #include "input_common/main.h" -#define PLAYER_NUMBER 8 - namespace TasInput { -using TasAnalog = std::tuple; +constexpr int PLAYER_NUMBER = 8; + +using TasAnalog = std::pair; + +enum class TasState { + RUNNING, + RECORDING, + STOPPED, +}; enum class TasButton : u32 { BUTTON_A = 0x000001, @@ -41,29 +47,6 @@ enum class TasButton : u32 { BUTTON_CAPTURE = 0x080000, }; -static const std::array, 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, @@ -82,7 +65,7 @@ public: Tas(); ~Tas(); - static std::string buttonsToString(u32 button) { + static std::string ButtonsToString(u32 button) { std::string returns; if ((button & static_cast(TasInput::TasButton::BUTTON_A)) != 0) returns += ", A"; @@ -124,14 +107,14 @@ public: returns += ", HOME"; if ((button & static_cast(TasInput::TasButton::BUTTON_CAPTURE)) != 0) returns += ", CAPTURE"; - return returns.length() != 0 ? returns.substr(2) : ""; + return returns.empty() ? "" : returns.substr(2); } void RefreshTasFile(); void LoadTasFiles(); - void RecordInput(u32 buttons, std::array, 2> axes); + void RecordInput(u32 buttons, const std::array, 2>& axes); void UpdateThread(); - std::string GetStatusDescription(); + std::tuple GetStatus(); InputCommon::ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) const; InputCommon::AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) const; @@ -143,21 +126,20 @@ private: TasAnalog l_axis{}; TasAnalog r_axis{}; }; - void LoadTasFile(int playerIndex); + void LoadTasFile(size_t player_index); void WriteTasFile(); - TasAnalog ReadCommandAxis(const std::string line) const; - u32 ReadCommandButtons(const std::string line) const; + 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 flipY(std::pair old) const; - size_t scriptLength{0}; + size_t script_length{0}; std::array tas_data; bool update_thread_running{true}; bool refresh_tas_fle{false}; - std::array, PLAYER_NUMBER> newCommands{}; - std::vector recordCommands{}; + std::array, PLAYER_NUMBER> commands{}; + std::vector record_commands{}; std::size_t current_command{0}; - TASCommand lastInput{}; // only used for recording + TASCommand last_input{}; // only used for recording }; } // namespace TasInput diff --git a/src/yuzu/configuration/configure_filesystem.cpp b/src/yuzu/configuration/configure_filesystem.cpp index cd9ba0a90..4636d476e 100644 --- a/src/yuzu/configuration/configure_filesystem.cpp +++ b/src/yuzu/configuration/configure_filesystem.cpp @@ -78,7 +78,7 @@ void ConfigureFilesystem::applyConfiguration() { Settings::values.gamecard_inserted = ui->gamecard_inserted->isChecked(); Settings::values.gamecard_current_game = ui->gamecard_current_game->isChecked(); - Settings::values.pauseTasOnLoad = ui->tas_pause_on_load->isChecked(); + Settings::values.pause_tas_on_load = ui->tas_pause_on_load->isChecked(); Settings::values.dump_exefs = ui->dump_exefs->isChecked(); Settings::values.dump_nso = ui->dump_nso->isChecked(); diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 10057b9ca..0ee0fd8cd 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -826,11 +826,11 @@ void GMainWindow::InitializeWidgets() { }); statusBar()->insertPermanentWidget(0, renderer_status_button); - TASlabel = new QLabel(); - TASlabel->setObjectName(QStringLiteral("TASlabel")); - TASlabel->setText(tr("TAS not running")); - TASlabel->setFocusPolicy(Qt::NoFocus); - statusBar()->insertPermanentWidget(0, TASlabel); + tas_label = new QLabel(); + tas_label->setObjectName(QStringLiteral("TASlabel")); + tas_label->setText(tr("TAS not running")); + tas_label->setFocusPolicy(Qt::NoFocus); + statusBar()->insertPermanentWidget(0, tas_label); statusBar()->setVisible(true); setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}")); @@ -2896,13 +2896,28 @@ void GMainWindow::UpdateWindowTitle(std::string_view title_name, std::string_vie } } +static std::string GetTasStateDescription(TasInput::TasState state) { + switch (state) { + case TasInput::TasState::RUNNING: + return "Running"; + case TasInput::TasState::RECORDING: + return "Recording"; + case TasInput::TasState::STOPPED: + return "Stopped"; + default: + return "INVALID STATE"; + } +} + void GMainWindow::UpdateStatusBar() { if (emu_thread == nullptr) { status_bar_update_timer.stop(); return; } - TASlabel->setText(tr(input_subsystem->GetTas()->GetStatusDescription().c_str())); + auto [tas_status, current_tas_frame, total_tas_frames] = input_subsystem->GetTas()->GetStatus(); + tas_label->setText(tr("%1 TAS %2/%3").arg(tr(GetTasStateDescription(tas_status).c_str())).arg(current_tas_frame).arg(total_tas_frames)); + auto& system = Core::System::GetInstance(); auto results = system.GetAndResetPerfStats(); auto& shader_notify = system.GPU().ShaderNotify(); From c01a872c8efa90065b6ba1a74079ddf6ec12058f Mon Sep 17 00:00:00 2001 From: german77 Date: Sat, 19 Jun 2021 14:38:49 -0500 Subject: [PATCH 05/11] config: Move TAS options to it's own menu --- src/common/fs/fs_paths.h | 2 +- src/common/fs/path_util.cpp | 2 +- src/common/fs/path_util.h | 3 +- src/core/hle/service/apm/apm_interface.cpp | 2 +- src/input_common/main.cpp | 6 +- src/input_common/tas/tas_input.cpp | 189 ++++++++++-------- src/input_common/tas/tas_input.h | 26 ++- src/yuzu/CMakeLists.txt | 3 + .../configuration/configure_filesystem.cpp | 9 - src/yuzu/configuration/configure_filesystem.h | 1 - .../configuration/configure_filesystem.ui | 49 ----- .../configure_input_player_widget.cpp | 4 +- src/yuzu/configuration/configure_tas.cpp | 84 ++++++++ src/yuzu/configuration/configure_tas.h | 38 ++++ src/yuzu/configuration/configure_tas.ui | 143 +++++++++++++ src/yuzu/debugger/controller.h | 1 - src/yuzu/main.cpp | 67 ++++--- src/yuzu/main.h | 1 + src/yuzu/main.ui | 6 + 19 files changed, 452 insertions(+), 184 deletions(-) create mode 100644 src/yuzu/configuration/configure_tas.cpp create mode 100644 src/yuzu/configuration/configure_tas.h create mode 100644 src/yuzu/configuration/configure_tas.ui diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h index 84968b8e0..5d447f108 100644 --- a/src/common/fs/fs_paths.h +++ b/src/common/fs/fs_paths.h @@ -21,7 +21,7 @@ #define SCREENSHOTS_DIR "screenshots" #define SDMC_DIR "sdmc" #define SHADER_DIR "shader" -#define TAS_DIR "scripts" +#define TAS_DIR "tas" // yuzu-specific files diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index 97d026eb8..43b79bd6d 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -116,7 +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::TASFile, yuzu_path / TAS_DIR); + GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR); } ~PathManagerImpl() = default; diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h index 6079de4c6..52e4670e2 100644 --- a/src/common/fs/path_util.h +++ b/src/common/fs/path_util.h @@ -23,8 +23,7 @@ enum class YuzuPath { ScreenshotsDir, // Where yuzu screenshots are stored. SDMCDir, // Where the emulated SDMC is stored. ShaderDir, // Where shaders are stored. - - TASFile, // Where the current script file is stored. + TASDir, // Where the current script file is stored. }; /** diff --git a/src/core/hle/service/apm/apm_interface.cpp b/src/core/hle/service/apm/apm_interface.cpp index 724483107..520ccfa88 100644 --- a/src/core/hle/service/apm/apm_interface.cpp +++ b/src/core/hle/service/apm/apm_interface.cpp @@ -121,7 +121,7 @@ void APM_Sys::SetCpuBoostMode(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_APM, "called, mode={:08X}", mode); - Settings::values.is_cpu_boosted = (static_cast(mode) == 1); + Settings::values.is_cpu_boosted = (mode == CpuBoostMode::Full); controller.SetFromCpuBoostMode(mode); IPC::ResponseBuilder rb{ctx, 2}; diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp index 4f170493e..3b9906b53 100644 --- a/src/input_common/main.cpp +++ b/src/input_common/main.cpp @@ -5,6 +5,7 @@ #include #include #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" @@ -114,8 +115,11 @@ struct InputSubsystem::Impl { std::vector devices = { Common::ParamPackage{{"display", "Any"}, {"class", "any"}}, Common::ParamPackage{{"display", "Keyboard/Mouse"}, {"class", "keyboard"}}, - Common::ParamPackage{{"display", "TAS"}, {"class", "tas"}}, }; + if (Settings::values.tas_enable) { + devices.push_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()); diff --git a/src/input_common/tas/tas_input.cpp b/src/input_common/tas/tas_input.cpp index 7320a7004..6efa1234a 100644 --- a/src/input_common/tas/tas_input.cpp +++ b/src/input_common/tas/tas_input.cpp @@ -67,14 +67,13 @@ void Tas::LoadTasFile(size_t player_index) { if (!commands[player_index].empty()) { commands[player_index].clear(); } - std::string file = Common::FS::ReadStringFromFile( - Common::FS::GetYuzuPathString(Common::FS::YuzuPath::TASFile) + "script0-" + - std::to_string(player_index + 1) + ".txt", - Common::FS::FileType::BinaryFile); + std::string file = + Common::FS::ReadStringFromFile(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::TASDir) + + "script0-" + std::to_string(player_index + 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}}; + int frame_no = 0; while (std::getline(command_line, line, '\n')) { if (line.empty()) { continue; @@ -94,9 +93,9 @@ void Tas::LoadTasFile(size_t player_index) { continue; } - while (frameNo < std::stoi(seglist.at(0))) { - commands[player_index].push_back(empty); - frameNo++; + while (frame_no < std::stoi(seglist.at(0))) { + commands[player_index].push_back({}); + frame_no++; } TASCommand command = { @@ -105,30 +104,29 @@ void Tas::LoadTasFile(size_t player_index) { .r_axis = ReadCommandAxis(seglist.at(3)), }; commands[player_index].push_back(command); - frameNo++; + frame_no++; } - LOG_INFO(Input, "TAS file loaded! {} frames", frameNo); + LOG_INFO(Input, "TAS file loaded! {} frames", frame_no); } void Tas::WriteTasFile() { LOG_DEBUG(Input, "WriteTasFile()"); - std::string output_text = ""; - for (int frame = 0; frame < (signed)record_commands.size(); frame++) { + std::string output_text; + for (size_t frame = 0; frame < record_commands.size(); frame++) { if (!output_text.empty()) { output_text += "\n"; } - TASCommand line = record_commands.at(frame); + const TASCommand& line = record_commands[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", + const size_t bytes_written = Common::FS::WriteStringToFile( + Common::FS::GetYuzuPathString(Common::FS::YuzuPath::TASDir) + "record.txt", Common::FS::FileType::TextFile, output_text); - if (bytesWritten == output_text.size()) { + 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", bytesWritten, + } else { + LOG_ERROR(Input, "Writing the TAS-file has failed! {} / {} bytes written", bytes_written, output_text.size()); } } @@ -142,30 +140,33 @@ void Tas::RecordInput(u32 buttons, const std::array, 2>& last_input = {buttons, FlipY(axes[0]), FlipY(axes[1])}; } -std::tuple Tas::GetStatus() { +std::tuple Tas::GetStatus() const { TasState state; - if (Settings::values.tas_record) { - return {TasState::RECORDING, record_commands.size(), record_commands.size()}; - } else if (Settings::values.tas_enable) { - state = TasState::RUNNING; + if (is_recording) { + return {TasState::Recording, 0, record_commands.size()}; + } + + if (is_running) { + state = TasState::Running; } else { - state = TasState::STOPPED; + state = TasState::Stopped; } return {state, current_command, script_length}; } static std::string DebugButtons(u32 buttons) { - return "{ " + TasInput::Tas::ButtonsToString(buttons) + " }"; + return fmt::format("{{ {} }}", TasInput::Tas::ButtonsToString(buttons)); } static std::string DebugJoystick(float x, float y) { - return "[ " + std::to_string(x) + "," + std::to_string(y) + " ]"; + return fmt::format("[ {} , {} ]", std::to_string(x), std::to_string(y)); } static std::string DebugInput(const TasData& data) { - return "{ " + DebugButtons(data.buttons) + " , " + DebugJoystick(data.axis[0], data.axis[1]) + - " , " + DebugJoystick(data.axis[2], data.axis[3]) + " }"; + return fmt::format("{{ {} , {} , {} }}", DebugButtons(data.buttons), + DebugJoystick(data.axis[0], data.axis[1]), + DebugJoystick(data.axis[2], data.axis[3])); } static std::string DebugInputs(const std::array& arr) { @@ -180,66 +181,54 @@ static std::string DebugInputs(const std::array& arr) { } void Tas::UpdateThread() { - if (update_thread_running) { - if (Settings::values.pause_tas_on_load && Settings::values.is_cpu_boosted) { - for (size_t i = 0; i < PLAYER_NUMBER; i++) { - tas_data[i].buttons = 0; - tas_data[i].axis = {}; - } - } + if (!update_thread_running) { + return; + } - if (Settings::values.tas_record) { - record_commands.push_back(last_input); - } - if (!Settings::values.tas_record && !record_commands.empty()) { - WriteTasFile(); - Settings::values.tas_reset = true; - refresh_tas_fle = true; - record_commands.clear(); - } - if (Settings::values.tas_reset) { - current_command = 0; - if (refresh_tas_fle) { - LoadTasFiles(); - refresh_tas_fle = false; - } - Settings::values.tas_reset = false; + if (is_recording) { + record_commands.push_back(last_input); + } + if (!is_recording && !record_commands.empty()) { + WriteTasFile(); + needs_reset = true; + refresh_tas_fle = true; + record_commands.clear(); + } + if (needs_reset) { + current_command = 0; + if (refresh_tas_fle) { LoadTasFiles(); - LOG_DEBUG(Input, "tas_reset done"); + refresh_tas_fle = false; } - if (Settings::values.tas_enable) { - if ((signed)current_command < script_length) { - LOG_INFO(Input, "Playing TAS {}/{}", current_command, script_length); - size_t frame = current_command++; - for (size_t i = 0; i < PLAYER_NUMBER; 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].buttons = 0; - tas_data[i].axis = {}; - } - } - } else { - Settings::values.tas_enable = false; - current_command = 0; - for (size_t i = 0; i < PLAYER_NUMBER; i++) { - tas_data[i].buttons = 0; - tas_data[i].axis = {}; + needs_reset = false; + LoadTasFiles(); + LOG_DEBUG(Input, "tas_reset done"); + } + if (is_running) { + if (current_command < script_length) { + LOG_INFO(Input, "Playing TAS {}/{}", current_command, script_length); + size_t frame = current_command++; + for (size_t i = 0; i < PLAYER_NUMBER; 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 { - for (size_t i = 0; i < PLAYER_NUMBER; i++) { - tas_data[i].buttons = 0; - tas_data[i].axis = {}; - } + is_running = Settings::values.tas_loop; + current_command = 0; + tas_data.fill({}); } + } else { + tas_data.fill({}); } LOG_DEBUG(Input, "TAS inputs: {}", DebugInputs(tas_data)); } @@ -284,8 +273,9 @@ std::string Tas::WriteCommandAxis(TasAnalog data) const { } std::string Tas::WriteCommandButtons(u32 data) const { - if (data == 0) + if (data == 0) { return "NONE"; + } std::string line; u32 index = 0; @@ -307,6 +297,37 @@ std::string Tas::WriteCommandButtons(u32 data) const { return line; } +void Tas::StartStop() { + is_running = !is_running; +} + +void Tas::Reset() { + needs_reset = true; +} + +void Tas::Record() { + is_recording = !is_recording; +<<<<<<< HEAD +======= + return is_recording; +} + +void Tas::SaveRecording(bool overwrite_file) { + if (is_recording) { + return; + } + if (record_commands.empty()) { + return; + } + WriteTasFile("record.txt"); + if (overwrite_file) { + WriteTasFile("script0-1.txt"); + } + needs_reset = true; + record_commands.clear(); +>>>>>>> 773d268db (config: disable pause on load) +} + InputCommon::ButtonMapping Tas::GetButtonMappingForDevice( const Common::ParamPackage& params) const { // This list is missing ZL/ZR since those are not considered buttons. diff --git a/src/input_common/tas/tas_input.h b/src/input_common/tas/tas_input.h index 8ee70bcaf..49ef10ff9 100644 --- a/src/input_common/tas/tas_input.h +++ b/src/input_common/tas/tas_input.h @@ -14,14 +14,14 @@ namespace TasInput { -constexpr int PLAYER_NUMBER = 8; +constexpr size_t PLAYER_NUMBER = 8; using TasAnalog = std::pair; enum class TasState { - RUNNING, - RECORDING, - STOPPED, + Running, + Recording, + Stopped, }; enum class TasButton : u32 { @@ -114,8 +114,19 @@ public: void LoadTasFiles(); void RecordInput(u32 buttons, const std::array, 2>& axes); void UpdateThread(); - std::tuple GetStatus(); + void StartStop(); + void Reset(); + void Record(); + + /** + * 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 GetStatus() const; 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; @@ -137,9 +148,12 @@ private: std::array tas_data; bool update_thread_running{true}; bool refresh_tas_fle{false}; + bool is_recording{false}; + bool is_running{false}; + bool needs_reset{false}; std::array, PLAYER_NUMBER> commands{}; std::vector record_commands{}; - std::size_t current_command{0}; + size_t current_command{0}; TASCommand last_input{}; // only used for recording }; } // namespace TasInput diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 19ba0dbba..b6dda283d 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -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 diff --git a/src/yuzu/configuration/configure_filesystem.cpp b/src/yuzu/configuration/configure_filesystem.cpp index 4636d476e..013de02db 100644 --- a/src/yuzu/configuration/configure_filesystem.cpp +++ b/src/yuzu/configuration/configure_filesystem.cpp @@ -26,8 +26,6 @@ ConfigureFilesystem::ConfigureFilesystem(QWidget* parent) [this] { SetDirectory(DirectoryTarget::Dump, ui->dump_path_edit); }); connect(ui->load_path_button, &QToolButton::pressed, this, [this] { SetDirectory(DirectoryTarget::Load, ui->load_path_edit); }); - connect(ui->tas_path_button, &QToolButton::pressed, this, - [this] { SetDirectory(DirectoryTarget::TAS, ui->tas_path_edit); }); connect(ui->reset_game_list_cache, &QPushButton::pressed, this, &ConfigureFilesystem::ResetMetadata); @@ -51,8 +49,6 @@ void ConfigureFilesystem::setConfiguration() { QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::DumpDir))); ui->load_path_edit->setText( QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::LoadDir))); - ui->tas_path_edit->setText( - QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::TASFile))); ui->gamecard_inserted->setChecked(Settings::values.gamecard_inserted.GetValue()); ui->gamecard_current_game->setChecked(Settings::values.gamecard_current_game.GetValue()); @@ -74,11 +70,9 @@ void ConfigureFilesystem::applyConfiguration() { ui->dump_path_edit->text().toStdString()); Common::FS::SetYuzuPath(Common::FS::YuzuPath::LoadDir, ui->load_path_edit->text().toStdString()); - Common::FS::SetYuzuPath(Common::FS::YuzuPath::TASFile, ui->tas_path_edit->text().toStdString()); Settings::values.gamecard_inserted = ui->gamecard_inserted->isChecked(); Settings::values.gamecard_current_game = ui->gamecard_current_game->isChecked(); - Settings::values.pause_tas_on_load = ui->tas_pause_on_load->isChecked(); Settings::values.dump_exefs = ui->dump_exefs->isChecked(); Settings::values.dump_nso = ui->dump_nso->isChecked(); @@ -104,9 +98,6 @@ void ConfigureFilesystem::SetDirectory(DirectoryTarget target, QLineEdit* edit) case DirectoryTarget::Load: caption = tr("Select Mod Load Directory..."); break; - case DirectoryTarget::TAS: - caption = tr("Select TAS Directory..."); - break; } QString str; diff --git a/src/yuzu/configuration/configure_filesystem.h b/src/yuzu/configuration/configure_filesystem.h index 86dab8684..2147cd405 100644 --- a/src/yuzu/configuration/configure_filesystem.h +++ b/src/yuzu/configuration/configure_filesystem.h @@ -32,7 +32,6 @@ private: Gamecard, Dump, Load, - TAS, }; void SetDirectory(DirectoryTarget target, QLineEdit* edit); diff --git a/src/yuzu/configuration/configure_filesystem.ui b/src/yuzu/configuration/configure_filesystem.ui index 8ac7250fd..62b9abc7a 100644 --- a/src/yuzu/configuration/configure_filesystem.ui +++ b/src/yuzu/configuration/configure_filesystem.ui @@ -219,55 +219,6 @@ - - - - TAS Directories - - - - - - Path - - - - - - - ... - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Maximum - - - - 60 - 20 - - - - - - - - Pause TAS execution during loads (SMO - 1.3) - - - - - - diff --git a/src/yuzu/configuration/configure_input_player_widget.cpp b/src/yuzu/configuration/configure_input_player_widget.cpp index e649e2169..e4383676a 100644 --- a/src/yuzu/configuration/configure_input_player_widget.cpp +++ b/src/yuzu/configuration/configure_input_player_widget.cpp @@ -232,7 +232,7 @@ void PlayerControlPreview::UpdateInput() { axis_values[Settings::NativeAnalog::RStick].value.x(), axis_values[Settings::NativeAnalog::RStick].value.y()}; input.button_values = button_values; - if (controller_callback.input != NULL) { + if (controller_callback.input != nullptr) { controller_callback.input(std::move(input)); } @@ -242,7 +242,7 @@ void PlayerControlPreview::UpdateInput() { } void PlayerControlPreview::SetCallBack(ControllerCallback callback_) { - controller_callback = callback_; + controller_callback = std::move(callback_); } void PlayerControlPreview::paintEvent(QPaintEvent* event) { diff --git a/src/yuzu/configuration/configure_tas.cpp b/src/yuzu/configuration/configure_tas.cpp new file mode 100644 index 000000000..f2f91d84a --- /dev/null +++ b/src/yuzu/configuration/configure_tas.cpp @@ -0,0 +1,84 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#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->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); + ui->tas_control_swap->setChecked(Settings::values.tas_swap_controllers); + ui->tas_loop_script->setChecked(Settings::values.tas_loop); + ui->tas_pause_on_load->setChecked(Settings::values.pause_tas_on_load); +} + +void ConfigureTasDialog::ApplyConfiguration() { + Common::FS::SetYuzuPath(Common::FS::YuzuPath::TASDir, ui->tas_path_edit->text().toStdString()); + Settings::values.tas_enable = ui->tas_enable->isChecked(); + Settings::values.tas_swap_controllers = ui->tas_control_swap->isChecked(); + Settings::values.tas_loop = ui->tas_loop_script->isChecked(); + Settings::values.pause_tas_on_load = 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.isNull() || 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(); +} diff --git a/src/yuzu/configuration/configure_tas.h b/src/yuzu/configuration/configure_tas.h new file mode 100644 index 000000000..1546bf16f --- /dev/null +++ b/src/yuzu/configuration/configure_tas.h @@ -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 + +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; +}; diff --git a/src/yuzu/configuration/configure_tas.ui b/src/yuzu/configuration/configure_tas.ui new file mode 100644 index 000000000..906e073ff --- /dev/null +++ b/src/yuzu/configuration/configure_tas.ui @@ -0,0 +1,143 @@ + + + ConfigureTas + + + + 0 + 0 + 800 + 300 + + + + Dialog + + + + + + + + TAS Settings + + + + + + Enable TAS features + + + + + + + false + + + Automatic controller profile swapping + + + + + + + Loop script + + + + + + + false + + + Pause execution during loads + + + + + + + + + + + + + + TAS Directories + + + + + + Path + + + + + + + ... + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 60 + 20 + + + + + + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + ConfigureTas + accept() + + + buttonBox + rejected() + ConfigureTas + reject() + + + diff --git a/src/yuzu/debugger/controller.h b/src/yuzu/debugger/controller.h index 659923e1b..f2f6653f7 100644 --- a/src/yuzu/debugger/controller.h +++ b/src/yuzu/debugger/controller.h @@ -25,7 +25,6 @@ struct ControllerInput { struct ControllerCallback { std::function input; - std::function update; }; class ControllerDialog : public QWidget { diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 0ee0fd8cd..820e31fa7 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -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" @@ -750,6 +751,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")); @@ -826,12 +832,6 @@ void GMainWindow::InitializeWidgets() { }); statusBar()->insertPermanentWidget(0, renderer_status_button); - tas_label = new QLabel(); - tas_label->setObjectName(QStringLiteral("TASlabel")); - tas_label->setText(tr("TAS not running")); - tas_label->setFocusPolicy(Qt::NoFocus); - statusBar()->insertPermanentWidget(0, tas_label); - statusBar()->setVisible(true); setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}")); } @@ -1024,18 +1024,11 @@ void GMainWindow::InitializeHotkeys() { } }); connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Start/Stop"), this), - &QShortcut::activated, this, [&] { - Settings::values.tas_enable = !Settings::values.tas_enable; - LOG_INFO(Frontend, "Tas enabled {}", Settings::values.tas_enable); - }); - + &QShortcut::activated, this, [&] { input_subsystem->GetTas()->StartStop(); }); connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Reset"), this), - &QShortcut::activated, this, [&] { Settings::values.tas_reset = true; }); + &QShortcut::activated, this, [&] { input_subsystem->GetTas()->Reset(); }); connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Record"), this), - &QShortcut::activated, this, [&] { - Settings::values.tas_record = !Settings::values.tas_record; - LOG_INFO(Frontend, "Tas recording {}", Settings::values.tas_record); - }); + &QShortcut::activated, this, [&] { input_subsystem->GetTas()->Record(); }); } void GMainWindow::SetDefaultUIGeometry() { @@ -1154,6 +1147,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); @@ -2720,6 +2714,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()); @@ -2898,14 +2905,14 @@ void GMainWindow::UpdateWindowTitle(std::string_view title_name, std::string_vie static std::string GetTasStateDescription(TasInput::TasState state) { switch (state) { - case TasInput::TasState::RUNNING: - return "Running"; - case TasInput::TasState::RECORDING: - return "Recording"; - case TasInput::TasState::STOPPED: - return "Stopped"; - default: - return "INVALID STATE"; + case TasInput::TasState::Running: + return "Running"; + case TasInput::TasState::Recording: + return "Recording"; + case TasInput::TasState::Stopped: + return "Stopped"; + default: + return "INVALID STATE"; } } @@ -2915,8 +2922,16 @@ void GMainWindow::UpdateStatusBar() { return; } - auto [tas_status, current_tas_frame, total_tas_frames] = input_subsystem->GetTas()->GetStatus(); - tas_label->setText(tr("%1 TAS %2/%3").arg(tr(GetTasStateDescription(tas_status).c_str())).arg(current_tas_frame).arg(total_tas_frames)); + if (Settings::values.tas_enable) { + auto [tas_status, current_tas_frame, total_tas_frames] = + input_subsystem->GetTas()->GetStatus(); + tas_label->setText(tr("%1 TAS %2/%3") + .arg(tr(GetTasStateDescription(tas_status).c_str())) + .arg(current_tas_frame) + .arg(total_tas_frames)); + } else { + tas_label->clear(); + } auto& system = Core::System::GetInstance(); auto results = system.GetAndResetPerfStats(); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index edca661ac..867a0003c 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -259,6 +259,7 @@ private slots: void OnMenuInstallToNAND(); void OnMenuRecentFile(); void OnConfigure(); + void OnConfigureTas(); void OnConfigurePerGame(); void OnLoadAmiibo(); void OnOpenYuzuFolder(); diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 048870687..31c1a20f3 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -72,6 +72,7 @@ + @@ -294,6 +295,11 @@ &Capture Screenshot + + + Configure &TAS... + + false From f078b15565c8cab08587b8f8629d878615705cfb Mon Sep 17 00:00:00 2001 From: MonsterDruide1 <5958456@gmail.com> Date: Sun, 20 Jun 2021 00:04:34 +0200 Subject: [PATCH 06/11] input_common/tas: Fallback to simple update --- src/common/settings.h | 7 +-- src/core/hle/service/apm/apm_interface.cpp | 2 - src/core/hle/service/vi/vi.cpp | 3 - src/input_common/tas/tas_input.cpp | 47 +++++++-------- src/input_common/tas/tas_input.h | 59 +++---------------- .../configure_input_player_widget.cpp | 3 + src/yuzu/debugger/controller.cpp | 16 +++-- src/yuzu/debugger/controller.h | 3 +- src/yuzu/main.cpp | 21 +++---- src/yuzu/main.h | 1 + 10 files changed, 60 insertions(+), 102 deletions(-) diff --git a/src/common/settings.h b/src/common/settings.h index 7333a64dc..cf12c325c 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -14,7 +14,6 @@ #include #include -#include #include "common/common_types.h" #include "common/settings_input.h" @@ -515,9 +514,9 @@ struct Values { BasicSetting pause_tas_on_load { false, "pause_tas_on_load" }; BasicSetting tas_enable{ false, "tas_enable" }; - BasicSetting tas_reset{ false, "tas_reset" }; - BasicSetting tas_record{ false, "tas_record" }; - BasicSetting is_cpu_boxted{ false, " BasicSetting is_cpu_boxted{ false, "cpuBoosted" }; + BasicSetting tas_loop{ false, "tas_loop" }; + BasicSetting tas_swap_controllers{ false, "tas_swap_controllers" }; + BasicSetting is_cpu_boosted{ false, "is_cpu_boosted" }; " }; BasicSetting mouse_panning{false, "mouse_panning"}; diff --git a/src/core/hle/service/apm/apm_interface.cpp b/src/core/hle/service/apm/apm_interface.cpp index 520ccfa88..e58bad083 100644 --- a/src/core/hle/service/apm/apm_interface.cpp +++ b/src/core/hle/service/apm/apm_interface.cpp @@ -3,7 +3,6 @@ // Refer to the license.txt file included. #include "common/logging/log.h" -#include "common/settings.h" #include "core/hle/ipc_helpers.h" #include "core/hle/service/apm/apm.h" #include "core/hle/service/apm/apm_controller.h" @@ -121,7 +120,6 @@ void APM_Sys::SetCpuBoostMode(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_APM, "called, mode={:08X}", mode); - Settings::values.is_cpu_boosted = (mode == CpuBoostMode::Full); controller.SetFromCpuBoostMode(mode); IPC::ResponseBuilder rb{ctx, 2}; diff --git a/src/core/hle/service/vi/vi.cpp b/src/core/hle/service/vi/vi.cpp index f4eac0bca..8e8fc40ca 100644 --- a/src/core/hle/service/vi/vi.cpp +++ b/src/core/hle/service/vi/vi.cpp @@ -32,8 +32,6 @@ #include "core/hle/service/vi/vi_s.h" #include "core/hle/service/vi/vi_u.h" -#include "input_common/tas/tas_input.h" - namespace Service::VI { constexpr ResultCode ERR_OPERATION_FAILED{ErrorModule::VI, 1}; @@ -597,7 +595,6 @@ private: IGBPQueueBufferResponseParcel response{1280, 720}; ctx.WriteBuffer(response.Serialize()); - Settings::values.input_subsystem->GetTas()->UpdateThread(); break; } case TransactionId::Query: { diff --git a/src/input_common/tas/tas_input.cpp b/src/input_common/tas/tas_input.cpp index 6efa1234a..baeb18c22 100644 --- a/src/input_common/tas/tas_input.cpp +++ b/src/input_common/tas/tas_input.cpp @@ -2,13 +2,8 @@ // Licensed under GPLv2+ // Refer to the license.txt file included. -#include #include -#include -#include #include -#include -#include #include "common/fs/file.h" #include "common/fs/fs_types.h" @@ -43,27 +38,25 @@ constexpr std::array, 20> text_to_tas_but }; Tas::Tas() { + if (!Settings::values.tas_enable) { + return; + } LoadTasFiles(); } -Tas::~Tas() { - update_thread_running = false; -} +Tas::~Tas() = default; -void Tas::RefreshTasFile() { - refresh_tas_fle = true; -} void Tas::LoadTasFiles() { script_length = 0; - for (size_t i = 0; i < PLAYER_NUMBER; i++) { + 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) { - LOG_DEBUG(Input, "LoadTasFile()"); if (!commands[player_index].empty()) { commands[player_index].clear(); } @@ -110,7 +103,6 @@ void Tas::LoadTasFile(size_t player_index) { } void Tas::WriteTasFile() { - LOG_DEBUG(Input, "WriteTasFile()"); std::string output_text; for (size_t frame = 0; frame < record_commands.size(); frame++) { if (!output_text.empty()) { @@ -131,13 +123,13 @@ void Tas::WriteTasFile() { } } -static std::pair FlipY(std::pair old) { +std::pair Tas::FlipAxisY(std::pair old) { auto [x, y] = old; return {x, -y}; } void Tas::RecordInput(u32 buttons, const std::array, 2>& axes) { - last_input = {buttons, FlipY(axes[0]), FlipY(axes[1])}; + last_input = {buttons, FlipAxisY(axes[0]), FlipAxisY(axes[1])}; } std::tuple Tas::GetStatus() const { @@ -155,21 +147,21 @@ std::tuple Tas::GetStatus() const { return {state, current_command, script_length}; } -static std::string DebugButtons(u32 buttons) { +std::string Tas::DebugButtons(u32 buttons) const { return fmt::format("{{ {} }}", TasInput::Tas::ButtonsToString(buttons)); } -static std::string DebugJoystick(float x, float y) { +std::string Tas::DebugJoystick(float x, float y) const { return fmt::format("[ {} , {} ]", std::to_string(x), std::to_string(y)); } -static std::string DebugInput(const TasData& data) { +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])); } -static std::string DebugInputs(const std::array& arr) { +std::string Tas::DebugInputs(const std::array& arr) const { std::string returns = "[ "; for (size_t i = 0; i < arr.size(); i++) { returns += DebugInput(arr[i]); @@ -180,8 +172,17 @@ static std::string DebugInputs(const std::array& arr) { 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(tas_button)) != 0) + returns += fmt::format(", {}", text_button.substr(4)); + } + return returns.empty() ? "" : returns.substr(2); +} + void Tas::UpdateThread() { - if (!update_thread_running) { + if (!Settings::values.tas_enable) { return; } @@ -206,9 +207,9 @@ void Tas::UpdateThread() { } if (is_running) { if (current_command < script_length) { - LOG_INFO(Input, "Playing TAS {}/{}", current_command, script_length); + LOG_DEBUG(Input, "Playing TAS {}/{}", current_command, script_length); size_t frame = current_command++; - for (size_t i = 0; i < PLAYER_NUMBER; i++) { + 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; diff --git a/src/input_common/tas/tas_input.h b/src/input_common/tas/tas_input.h index 49ef10ff9..e011e559e 100644 --- a/src/input_common/tas/tas_input.h +++ b/src/input_common/tas/tas_input.h @@ -5,8 +5,6 @@ #pragma once #include -#include -#include #include "common/common_types.h" #include "core/frontend/input.h" @@ -65,53 +63,6 @@ public: Tas(); ~Tas(); - static std::string ButtonsToString(u32 button) { - std::string returns; - if ((button & static_cast(TasInput::TasButton::BUTTON_A)) != 0) - returns += ", A"; - if ((button & static_cast(TasInput::TasButton::BUTTON_B)) != 0) - returns += ", B"; - if ((button & static_cast(TasInput::TasButton::BUTTON_X)) != 0) - returns += ", X"; - if ((button & static_cast(TasInput::TasButton::BUTTON_Y)) != 0) - returns += ", Y"; - if ((button & static_cast(TasInput::TasButton::STICK_L)) != 0) - returns += ", STICK_L"; - if ((button & static_cast(TasInput::TasButton::STICK_R)) != 0) - returns += ", STICK_R"; - if ((button & static_cast(TasInput::TasButton::TRIGGER_L)) != 0) - returns += ", TRIGGER_L"; - if ((button & static_cast(TasInput::TasButton::TRIGGER_R)) != 0) - returns += ", TRIGGER_R"; - if ((button & static_cast(TasInput::TasButton::TRIGGER_ZL)) != 0) - returns += ", TRIGGER_ZL"; - if ((button & static_cast(TasInput::TasButton::TRIGGER_ZR)) != 0) - returns += ", TRIGGER_ZR"; - if ((button & static_cast(TasInput::TasButton::BUTTON_PLUS)) != 0) - returns += ", PLUS"; - if ((button & static_cast(TasInput::TasButton::BUTTON_MINUS)) != 0) - returns += ", MINUS"; - if ((button & static_cast(TasInput::TasButton::BUTTON_LEFT)) != 0) - returns += ", LEFT"; - if ((button & static_cast(TasInput::TasButton::BUTTON_UP)) != 0) - returns += ", UP"; - if ((button & static_cast(TasInput::TasButton::BUTTON_RIGHT)) != 0) - returns += ", RIGHT"; - if ((button & static_cast(TasInput::TasButton::BUTTON_DOWN)) != 0) - returns += ", DOWN"; - if ((button & static_cast(TasInput::TasButton::BUTTON_SL)) != 0) - returns += ", SL"; - if ((button & static_cast(TasInput::TasButton::BUTTON_SR)) != 0) - returns += ", SR"; - if ((button & static_cast(TasInput::TasButton::BUTTON_HOME)) != 0) - returns += ", HOME"; - if ((button & static_cast(TasInput::TasButton::BUTTON_CAPTURE)) != 0) - returns += ", CAPTURE"; - return returns.empty() ? "" : returns.substr(2); - } - - void RefreshTasFile(); - void LoadTasFiles(); void RecordInput(u32 buttons, const std::array, 2>& axes); void UpdateThread(); @@ -137,6 +88,7 @@ private: TasAnalog l_axis{}; TasAnalog r_axis{}; }; + void LoadTasFiles(); void LoadTasFile(size_t player_index); void WriteTasFile(); TasAnalog ReadCommandAxis(const std::string& line) const; @@ -144,9 +96,16 @@ private: std::string WriteCommandButtons(u32 data) const; std::string WriteCommandAxis(TasAnalog data) const; + std::pair FlipAxisY(std::pair old); + + std::string DebugButtons(u32 buttons) const; + std::string DebugJoystick(float x, float y) const; + std::string DebugInput(const TasData& data) const; + std::string DebugInputs(const std::array& arr) const; + std::string ButtonsToString(u32 button) const; + size_t script_length{0}; std::array tas_data; - bool update_thread_running{true}; bool refresh_tas_fle{false}; bool is_recording{false}; bool is_running{false}; diff --git a/src/yuzu/configuration/configure_input_player_widget.cpp b/src/yuzu/configuration/configure_input_player_widget.cpp index e4383676a..b905fc73d 100644 --- a/src/yuzu/configuration/configure_input_player_widget.cpp +++ b/src/yuzu/configuration/configure_input_player_widget.cpp @@ -175,6 +175,9 @@ void PlayerControlPreview::ResetInputs() { } void PlayerControlPreview::UpdateInput() { + if (controller_callback.update != nullptr) { + controller_callback.update(std::move(true)); + } if (!is_enabled && !mapping_active) { return; } diff --git a/src/yuzu/debugger/controller.cpp b/src/yuzu/debugger/controller.cpp index 822d033d1..a745699bf 100644 --- a/src/yuzu/debugger/controller.cpp +++ b/src/yuzu/debugger/controller.cpp @@ -41,7 +41,8 @@ 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); }}; + ControllerCallback callback{[this](ControllerInput input) { InputController(input); }, + [this](bool update) { UpdateController(update); }}; widget->SetCallBack(callback); widget->repaint(); widget->SetConnectedStatus(players[player].connected); @@ -74,10 +75,6 @@ void ControllerDialog::hideEvent(QHideEvent* ev) { QWidget::hideEvent(ev); } -void ControllerDialog::RefreshTasFile() { - input_subsystem->GetTas()->RefreshTasFile(); -} - void ControllerDialog::InputController(ControllerInput input) { u32 buttons = 0; int index = 0; @@ -86,4 +83,11 @@ void ControllerDialog::InputController(ControllerInput input) { index++; } input_subsystem->GetTas()->RecordInput(buttons, input.axis_values); -} \ No newline at end of file +} + +void ControllerDialog::UpdateController(bool update) { + if (!update) { + return; + } + input_subsystem->GetTas()->UpdateThread(); +} diff --git a/src/yuzu/debugger/controller.h b/src/yuzu/debugger/controller.h index f2f6653f7..448d24b67 100644 --- a/src/yuzu/debugger/controller.h +++ b/src/yuzu/debugger/controller.h @@ -25,6 +25,7 @@ struct ControllerInput { struct ControllerCallback { std::function input; + std::function update; }; class ControllerDialog : public QWidget { @@ -43,8 +44,8 @@ protected: void hideEvent(QHideEvent* ev) override; private: - void RefreshTasFile(); void InputController(ControllerInput input); + void UpdateController(bool update); QAction* toggle_view_action = nullptr; QFileSystemWatcher* watcher = nullptr; PlayerControlPreview* widget; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 820e31fa7..6c2835a2f 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -196,7 +196,6 @@ GMainWindow::GMainWindow() config{std::make_unique()}, vfs{std::make_shared()}, provider{std::make_unique()} { Common::Log::Initialize(); - Settings::values.inputSubsystem = input_subsystem; LoadTranslation(); setAcceptDrops(true); @@ -2903,16 +2902,17 @@ void GMainWindow::UpdateWindowTitle(std::string_view title_name, std::string_vie } } -static std::string GetTasStateDescription(TasInput::TasState state) { - switch (state) { +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 "Running"; + return tr("TAS state: Running %1/%2").arg(current_tas_frame).arg(total_tas_frames); case TasInput::TasState::Recording: - return "Recording"; + return tr("TAS state: Recording %1").arg(total_tas_frames); case TasInput::TasState::Stopped: - return "Stopped"; + return tr("TAS state: Idle %1/%2").arg(current_tas_frame).arg(total_tas_frames); default: - return "INVALID STATE"; + return tr("INVALID TAS STATE"); } } @@ -2923,12 +2923,7 @@ void GMainWindow::UpdateStatusBar() { } if (Settings::values.tas_enable) { - auto [tas_status, current_tas_frame, total_tas_frames] = - input_subsystem->GetTas()->GetStatus(); - tas_label->setText(tr("%1 TAS %2/%3") - .arg(tr(GetTasStateDescription(tas_status).c_str())) - .arg(current_tas_frame) - .arg(total_tas_frames)); + tas_label->setText(GetTasStateDescription()); } else { tas_label->clear(); } diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 867a0003c..610b59ee5 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -301,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; From 9bb6580d89efb76534d9395bc052459d5f58e7c4 Mon Sep 17 00:00:00 2001 From: german77 Date: Sat, 26 Jun 2021 10:38:39 -0500 Subject: [PATCH 07/11] input_common/tas: overwrite file dialog --- src/input_common/tas/tas_input.cpp | 19 +++---------------- src/input_common/tas/tas_input.h | 6 +++--- src/yuzu/main.cpp | 11 ++++++++++- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/input_common/tas/tas_input.cpp b/src/input_common/tas/tas_input.cpp index baeb18c22..eb3327520 100644 --- a/src/input_common/tas/tas_input.cpp +++ b/src/input_common/tas/tas_input.cpp @@ -102,7 +102,7 @@ void Tas::LoadTasFile(size_t player_index) { LOG_INFO(Input, "TAS file loaded! {} frames", frame_no); } -void Tas::WriteTasFile() { +void Tas::WriteTasFile(std::string file_name) { std::string output_text; for (size_t frame = 0; frame < record_commands.size(); frame++) { if (!output_text.empty()) { @@ -113,7 +113,7 @@ void Tas::WriteTasFile() { WriteCommandAxis(line.l_axis) + " " + WriteCommandAxis(line.r_axis); } const size_t bytes_written = Common::FS::WriteStringToFile( - Common::FS::GetYuzuPathString(Common::FS::YuzuPath::TASDir) + "record.txt", + Common::FS::GetYuzuPathString(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!"); @@ -189,18 +189,8 @@ void Tas::UpdateThread() { if (is_recording) { record_commands.push_back(last_input); } - if (!is_recording && !record_commands.empty()) { - WriteTasFile(); - needs_reset = true; - refresh_tas_fle = true; - record_commands.clear(); - } if (needs_reset) { current_command = 0; - if (refresh_tas_fle) { - LoadTasFiles(); - refresh_tas_fle = false; - } needs_reset = false; LoadTasFiles(); LOG_DEBUG(Input, "tas_reset done"); @@ -306,10 +296,8 @@ void Tas::Reset() { needs_reset = true; } -void Tas::Record() { +bool Tas::Record() { is_recording = !is_recording; -<<<<<<< HEAD -======= return is_recording; } @@ -326,7 +314,6 @@ void Tas::SaveRecording(bool overwrite_file) { } needs_reset = true; record_commands.clear(); ->>>>>>> 773d268db (config: disable pause on load) } InputCommon::ButtonMapping Tas::GetButtonMappingForDevice( diff --git a/src/input_common/tas/tas_input.h b/src/input_common/tas/tas_input.h index e011e559e..e0462e858 100644 --- a/src/input_common/tas/tas_input.h +++ b/src/input_common/tas/tas_input.h @@ -68,7 +68,8 @@ public: void StartStop(); void Reset(); - void Record(); + bool Record(); + void SaveRecording(bool overwrite_file); /** * Returns the current status values of TAS playback/recording @@ -90,7 +91,7 @@ private: }; void LoadTasFiles(); void LoadTasFile(size_t player_index); - void WriteTasFile(); + void WriteTasFile(std::string file_name); TasAnalog ReadCommandAxis(const std::string& line) const; u32 ReadCommandButtons(const std::string& line) const; std::string WriteCommandButtons(u32 data) const; @@ -106,7 +107,6 @@ private: size_t script_length{0}; std::array tas_data; - bool refresh_tas_fle{false}; bool is_recording{false}; bool is_running{false}; bool needs_reset{false}; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 6c2835a2f..560de89af 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -1027,7 +1027,16 @@ void GMainWindow::InitializeHotkeys() { 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, [&] { input_subsystem->GetTas()->Record(); }); + &QShortcut::activated, this, [&] { + bool is_recording = input_subsystem->GetTas()->Record(); + if (!is_recording) { + QMessageBox::StandardButton reply; + reply = QMessageBox::question(this, tr("TAS Recording"), + tr("Overwrite file of player 1?"), + QMessageBox::Yes | QMessageBox::No); + input_subsystem->GetTas()->SaveRecording(reply == QMessageBox::Yes); + } + }); } void GMainWindow::SetDefaultUIGeometry() { From e6c4bf52f0eb2c9c78e983ffbc667891463d3253 Mon Sep 17 00:00:00 2001 From: german77 Date: Sun, 27 Jun 2021 14:02:38 -0500 Subject: [PATCH 08/11] input_common/tas: Add swap controller --- src/common/fs/path_util.h | 2 +- src/input_common/main.h | 20 +++--- src/input_common/tas/tas_input.cpp | 63 ++++++++++++++++--- src/input_common/tas/tas_input.h | 9 ++- src/yuzu/configuration/config.cpp | 5 ++ .../configure_input_player_widget.cpp | 32 +++++----- src/yuzu/configuration/configure_tas.ui | 3 - src/yuzu/main.cpp | 2 +- 8 files changed, 98 insertions(+), 38 deletions(-) diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h index 52e4670e2..0a9e3a145 100644 --- a/src/common/fs/path_util.h +++ b/src/common/fs/path_util.h @@ -23,7 +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 the current script file is stored. + TASDir, // Where TAS scripts are stored. }; /** diff --git a/src/input_common/main.h b/src/input_common/main.h index 1d06fc5f5..6390d3f09 100644 --- a/src/input_common/main.h +++ b/src/input_common/main.h @@ -155,28 +155,28 @@ 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. @@ -185,10 +185,10 @@ public: /// Retrieves the underlying tas button handler. [[nodiscard]] const TasButtonFactory* GetTasButtons() const; - /// Retrieves the underlying tas touch handler. + /// Retrieves the underlying tas analogs handler. [[nodiscard]] TasAnalogFactory* GetTasAnalogs(); - /// Retrieves the underlying tas touch handler. + /// Retrieves the underlying tas analogs handler. [[nodiscard]] const TasAnalogFactory* GetTasAnalogs() const; /// Reloads the input devices diff --git a/src/input_common/tas/tas_input.cpp b/src/input_common/tas/tas_input.cpp index eb3327520..aceb13adc 100644 --- a/src/input_common/tas/tas_input.cpp +++ b/src/input_common/tas/tas_input.cpp @@ -61,8 +61,8 @@ void Tas::LoadTasFile(size_t player_index) { commands[player_index].clear(); } std::string file = - Common::FS::ReadStringFromFile(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::TASDir) + - "script0-" + std::to_string(player_index + 1) + ".txt", + 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; @@ -102,7 +102,7 @@ void Tas::LoadTasFile(size_t player_index) { LOG_INFO(Input, "TAS file loaded! {} frames", frame_no); } -void Tas::WriteTasFile(std::string file_name) { +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()) { @@ -112,8 +112,8 @@ void Tas::WriteTasFile(std::string file_name) { output_text += std::to_string(frame) + " " + WriteCommandButtons(line.buttons) + " " + WriteCommandAxis(line.l_axis) + " " + WriteCommandAxis(line.r_axis); } - const size_t bytes_written = Common::FS::WriteStringToFile( - Common::FS::GetYuzuPathString(Common::FS::YuzuPath::TASDir) + file_name, + 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!"); @@ -217,6 +217,9 @@ void Tas::UpdateThread() { is_running = Settings::values.tas_loop; current_command = 0; tas_data.fill({}); + if (!is_running) { + SwapToStoredController(); + } } } else { tas_data.fill({}); @@ -290,6 +293,52 @@ std::string Tas::WriteCommandButtons(u32 data) const { void Tas::StartStop() { is_running = !is_running; + if (is_running) { + SwapToTasController(); + } else { + 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; + } + + auto tas_param = Common::ParamPackage{{"pad", static_cast(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(i)].Serialize(); + } + for (std::size_t i = 0; i < analogs.size(); ++i) { + analogs[i] = analog_mapping[static_cast(i)].Serialize(); + } + } + Settings::values.is_device_reload_pending.store(true); +} + +void Tas::SwapToStoredController() { + if (!Settings::values.tas_swap_controllers) { + return; + } + auto& players = Settings::values.players.GetValue(); + for (std::size_t index = 0; index < players.size(); index++) { + players[index] = player_mappings[index]; + } + Settings::values.is_device_reload_pending.store(true); } void Tas::Reset() { @@ -308,9 +357,9 @@ void Tas::SaveRecording(bool overwrite_file) { if (record_commands.empty()) { return; } - WriteTasFile("record.txt"); + WriteTasFile(u8"record.txt"); if (overwrite_file) { - WriteTasFile("script0-1.txt"); + WriteTasFile(u8"script0-1.txt"); } needs_reset = true; record_commands.clear(); diff --git a/src/input_common/tas/tas_input.h b/src/input_common/tas/tas_input.h index e0462e858..e1f351251 100644 --- a/src/input_common/tas/tas_input.h +++ b/src/input_common/tas/tas_input.h @@ -7,6 +7,7 @@ #include #include "common/common_types.h" +#include "common/settings_input.h" #include "core/frontend/input.h" #include "input_common/main.h" @@ -91,7 +92,7 @@ private: }; void LoadTasFiles(); void LoadTasFile(size_t player_index); - void WriteTasFile(std::string file_name); + void WriteTasFile(std::u8string file_name); TasAnalog ReadCommandAxis(const std::string& line) const; u32 ReadCommandButtons(const std::string& line) const; std::string WriteCommandButtons(u32 data) const; @@ -105,6 +106,9 @@ private: std::string DebugInputs(const std::array& arr) const; std::string ButtonsToString(u32 button) const; + void SwapToTasController(); + void SwapToStoredController(); + size_t script_length{0}; std::array tas_data; bool is_recording{false}; @@ -114,5 +118,8 @@ private: std::vector record_commands{}; size_t current_command{0}; TASCommand last_input{}; // only used for recording + + // Old settings for swapping controllers + std::array player_mappings; }; } // namespace TasInput diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 99e318a8f..a0dfa06df 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -1205,6 +1205,11 @@ void Config::SaveControlValues() { WriteBasicSetting(Settings::values.emulate_analog_keyboard); WriteBasicSetting(Settings::values.mouse_panning_sensitivity); + WriteSetting(QStringLiteral("enable_tas"), Settings::values.tas_enable, false); + WriteSetting(QStringLiteral("loop_tas"), Settings::values.tas_loop, false); + WriteSetting(QStringLiteral("swap_tas_controllers"), Settings::values.tas_swap_controllers, + true); + WriteSetting(QStringLiteral("tas_pause_on_load"), Settings::values.pause_tas_on_load, true); qt_config->endGroup(); } diff --git a/src/yuzu/configuration/configure_input_player_widget.cpp b/src/yuzu/configuration/configure_input_player_widget.cpp index b905fc73d..ba3720c03 100644 --- a/src/yuzu/configuration/configure_input_player_widget.cpp +++ b/src/yuzu/configuration/configure_input_player_widget.cpp @@ -175,10 +175,7 @@ void PlayerControlPreview::ResetInputs() { } void PlayerControlPreview::UpdateInput() { - if (controller_callback.update != nullptr) { - controller_callback.update(std::move(true)); - } - if (!is_enabled && !mapping_active) { + if (!is_enabled && !mapping_active && !Settings::values.tas_enable) { return; } bool input_changed = false; @@ -223,20 +220,25 @@ void PlayerControlPreview::UpdateInput() { } } - ControllerInput input{}; if (input_changed) { update(); - input.changed = true; + ControllerInput input{ + .axis_values = + {std::pair{axis_values[Settings::NativeAnalog::LStick].value.x(), + axis_values[Settings::NativeAnalog::LStick].value.y()}, + std::pair{axis_values[Settings::NativeAnalog::RStick].value.x(), + axis_values[Settings::NativeAnalog::RStick].value.y()}}, + .button_values = button_values, + .changed = true, + }; + + if (controller_callback.input != nullptr) { + controller_callback.input(std::move(input)); + } } - 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 != nullptr) { - controller_callback.input(std::move(input)); + + if (controller_callback.update != nullptr) { + controller_callback.update(std::move(true)); } if (mapping_active) { diff --git a/src/yuzu/configuration/configure_tas.ui b/src/yuzu/configuration/configure_tas.ui index 906e073ff..445904d8f 100644 --- a/src/yuzu/configuration/configure_tas.ui +++ b/src/yuzu/configuration/configure_tas.ui @@ -31,9 +31,6 @@ - - false - Automatic controller profile swapping diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 560de89af..7d12fcca7 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -2921,7 +2921,7 @@ QString GMainWindow::GetTasStateDescription() const { case TasInput::TasState::Stopped: return tr("TAS state: Idle %1/%2").arg(current_tas_frame).arg(total_tas_frames); default: - return tr("INVALID TAS STATE"); + return tr("TAS State: Invalid"); } } From 33a1d790e8a5f67c73d0eef4a141f936345f104f Mon Sep 17 00:00:00 2001 From: german77 Date: Mon, 5 Jul 2021 20:58:52 -0500 Subject: [PATCH 09/11] input_common/tas: Document the main class --- src/common/settings.h | 11 +- src/input_common/tas/tas_input.cpp | 3 +- src/input_common/tas/tas_input.h | 108 ++++++++++++++++++ src/input_common/tas/tas_poller.h | 4 +- src/yuzu/configuration/config.cpp | 39 +++---- .../configuration/configure_filesystem.cpp | 1 - .../configure_input_player_widget.cpp | 22 ++-- src/yuzu/configuration/configure_tas.cpp | 16 +-- 8 files changed, 153 insertions(+), 51 deletions(-) diff --git a/src/common/settings.h b/src/common/settings.h index cf12c325c..c53d5acc3 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -512,17 +512,14 @@ struct Values { "motion_device"}; BasicSetting udp_input_servers{"127.0.0.1:26760", "udp_input_servers"}; - BasicSetting pause_tas_on_load { false, "pause_tas_on_load" }; - BasicSetting tas_enable{ false, "tas_enable" }; - BasicSetting tas_loop{ false, "tas_loop" }; - BasicSetting tas_swap_controllers{ false, "tas_swap_controllers" }; - BasicSetting is_cpu_boosted{ false, "is_cpu_boosted" }; -" }; + BasicSetting pause_tas_on_load{true, "pause_tas_on_load"}; + BasicSetting tas_enable{false, "tas_enable"}; + BasicSetting tas_loop{false, "tas_loop"}; + BasicSetting tas_swap_controllers{true, "tas_swap_controllers"}; BasicSetting mouse_panning{false, "mouse_panning"}; BasicRangedSetting mouse_panning_sensitivity{10, 1, 100, "mouse_panning_sensitivity"}; BasicSetting mouse_enabled{false, "mouse_enabled"}; - std::string mouse_device; MouseButtonsRaw mouse_buttons; diff --git a/src/input_common/tas/tas_input.cpp b/src/input_common/tas/tas_input.cpp index aceb13adc..877d35088 100644 --- a/src/input_common/tas/tas_input.cpp +++ b/src/input_common/tas/tas_input.cpp @@ -14,6 +14,7 @@ namespace TasInput { +// Supported keywords and buttons from a TAS file constexpr std::array, 20> text_to_tas_button = { std::pair{"KEY_A", TasButton::BUTTON_A}, {"KEY_B", TasButton::BUTTON_B}, @@ -214,7 +215,7 @@ void Tas::UpdateThread() { } } } else { - is_running = Settings::values.tas_loop; + is_running = Settings::values.tas_loop.GetValue(); current_command = 0; tas_data.fill({}); if (!is_running) { diff --git a/src/input_common/tas/tas_input.h b/src/input_common/tas/tas_input.h index e1f351251..52d000db4 100644 --- a/src/input_common/tas/tas_input.h +++ b/src/input_common/tas/tas_input.h @@ -11,6 +11,38 @@ #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 +Emulation -> 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; @@ -64,12 +96,26 @@ public: Tas(); ~Tas(); + // Changes the input status that will be stored in each frame void RecordInput(u32 buttons, const std::array, 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(); + + // 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); /** @@ -80,7 +126,11 @@ public: * Total length of script file currently loaded or amount of frames (so far) for Recording */ std::tuple 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; @@ -90,23 +140,81 @@ private: 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 FlipAxisY(std::pair 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& 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}; diff --git a/src/input_common/tas/tas_poller.h b/src/input_common/tas/tas_poller.h index 1bc0d173b..09e426cef 100644 --- a/src/input_common/tas/tas_poller.h +++ b/src/input_common/tas/tas_poller.h @@ -11,7 +11,7 @@ namespace InputCommon { /** - * A button device factory representing a mouse. It receives mouse events and forward them + * 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 { @@ -29,7 +29,7 @@ private: std::shared_ptr tas_input; }; -/// An analog device factory that creates analog devices from mouse +/// An analog device factory that creates analog devices from tas class TasAnalogFactory final : public Input::Factory { public: explicit TasAnalogFactory(std::shared_ptr tas_input_); diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index a0dfa06df..27b67fd9e 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -567,8 +567,10 @@ void Config::ReadControlValues() { Settings::values.mouse_panning = false; ReadBasicSetting(Settings::values.mouse_panning_sensitivity); - ReadBasicSetting(Settings::values.tas_enable = false); - ReadBasicSetting(Settings::values.tas_reset = false); + 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); @@ -667,20 +669,16 @@ void Config::ReadDataStorageValues() { QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir))) .toString() .toStdString()); - FS::SetYuzuPath( - FS::YuzuPath::TASFile, - qt_config - ->value(QStringLiteral("tas_path"), - QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASFile))) - .toString() - .toStdString()); - - ReadBasicSetting(Settings::values.pauseTasOnLoad); + 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); - qt_config->endGroup(); } @@ -1205,11 +1203,11 @@ void Config::SaveControlValues() { WriteBasicSetting(Settings::values.emulate_analog_keyboard); WriteBasicSetting(Settings::values.mouse_panning_sensitivity); - WriteSetting(QStringLiteral("enable_tas"), Settings::values.tas_enable, false); - WriteSetting(QStringLiteral("loop_tas"), Settings::values.tas_loop, false); - WriteSetting(QStringLiteral("swap_tas_controllers"), Settings::values.tas_swap_controllers, - true); - WriteSetting(QStringLiteral("tas_pause_on_load"), Settings::values.pause_tas_on_load, true); + 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(); } @@ -1237,10 +1235,9 @@ void Config::SaveDataStorageValues() { WriteSetting(QStringLiteral("dump_directory"), QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir)), QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir))); - WriteSetting(QStringLiteral("tas_path"), - QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASFile)), - QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASFile))); - WriteSetting(QStringLiteral("tas_pause_on_load"), Settings::values.pauseTasOnLoad, true); + 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); diff --git a/src/yuzu/configuration/configure_filesystem.cpp b/src/yuzu/configuration/configure_filesystem.cpp index 013de02db..9cb317822 100644 --- a/src/yuzu/configuration/configure_filesystem.cpp +++ b/src/yuzu/configuration/configure_filesystem.cpp @@ -52,7 +52,6 @@ void ConfigureFilesystem::setConfiguration() { ui->gamecard_inserted->setChecked(Settings::values.gamecard_inserted.GetValue()); ui->gamecard_current_game->setChecked(Settings::values.gamecard_current_game.GetValue()); - ui->tas_pause_on_load->setChecked(Settings::values.pauseTasOnLoad); ui->dump_exefs->setChecked(Settings::values.dump_exefs.GetValue()); ui->dump_nso->setChecked(Settings::values.dump_nso.GetValue()); diff --git a/src/yuzu/configuration/configure_input_player_widget.cpp b/src/yuzu/configuration/configure_input_player_widget.cpp index ba3720c03..38c59263c 100644 --- a/src/yuzu/configuration/configure_input_player_widget.cpp +++ b/src/yuzu/configuration/configure_input_player_widget.cpp @@ -222,23 +222,23 @@ void PlayerControlPreview::UpdateInput() { if (input_changed) { update(); - ControllerInput input{ - .axis_values = - {std::pair{axis_values[Settings::NativeAnalog::LStick].value.x(), - axis_values[Settings::NativeAnalog::LStick].value.y()}, - std::pair{axis_values[Settings::NativeAnalog::RStick].value.x(), - axis_values[Settings::NativeAnalog::RStick].value.y()}}, - .button_values = button_values, - .changed = true, - }; - if (controller_callback.input != nullptr) { + ControllerInput input{ + .axis_values = {std::pair{ + axis_values[Settings::NativeAnalog::LStick].value.x(), + axis_values[Settings::NativeAnalog::LStick].value.y()}, + std::pair{ + 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 (controller_callback.update != nullptr) { - controller_callback.update(std::move(true)); + controller_callback.update(true); } if (mapping_active) { diff --git a/src/yuzu/configuration/configure_tas.cpp b/src/yuzu/configuration/configure_tas.cpp index f2f91d84a..00d6c1ba5 100644 --- a/src/yuzu/configuration/configure_tas.cpp +++ b/src/yuzu/configuration/configure_tas.cpp @@ -30,18 +30,18 @@ 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); - ui->tas_control_swap->setChecked(Settings::values.tas_swap_controllers); - ui->tas_loop_script->setChecked(Settings::values.tas_loop); - ui->tas_pause_on_load->setChecked(Settings::values.pause_tas_on_load); + 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 = ui->tas_enable->isChecked(); - Settings::values.tas_swap_controllers = ui->tas_control_swap->isChecked(); - Settings::values.tas_loop = ui->tas_loop_script->isChecked(); - Settings::values.pause_tas_on_load = ui->tas_pause_on_load->isChecked(); + 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) { From 5401cf6eb583092ed144d0f30fb6221a0ab25fed Mon Sep 17 00:00:00 2001 From: german77 Date: Fri, 9 Jul 2021 23:30:58 -0500 Subject: [PATCH 10/11] input_common/tas: new update method --- src/yuzu/bootmanager.cpp | 2 ++ .../configuration/configure_input_player_widget.cpp | 4 ---- src/yuzu/debugger/controller.cpp | 10 +--------- src/yuzu/debugger/controller.h | 2 -- src/yuzu/main.cpp | 3 +-- 5 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp index 2e0ade815..1519a46ed 100644 --- a/src/yuzu/bootmanager.cpp +++ b/src/yuzu/bootmanager.cpp @@ -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(); diff --git a/src/yuzu/configuration/configure_input_player_widget.cpp b/src/yuzu/configuration/configure_input_player_widget.cpp index 38c59263c..da328d904 100644 --- a/src/yuzu/configuration/configure_input_player_widget.cpp +++ b/src/yuzu/configuration/configure_input_player_widget.cpp @@ -237,10 +237,6 @@ void PlayerControlPreview::UpdateInput() { } } - if (controller_callback.update != nullptr) { - controller_callback.update(true); - } - if (mapping_active) { blink_counter = (blink_counter + 1) % 50; } diff --git a/src/yuzu/debugger/controller.cpp b/src/yuzu/debugger/controller.cpp index a745699bf..296000ed5 100644 --- a/src/yuzu/debugger/controller.cpp +++ b/src/yuzu/debugger/controller.cpp @@ -41,8 +41,7 @@ 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); }, - [this](bool update) { UpdateController(update); }}; + ControllerCallback callback{[this](ControllerInput input) { InputController(input); }}; widget->SetCallBack(callback); widget->repaint(); widget->SetConnectedStatus(players[player].connected); @@ -84,10 +83,3 @@ void ControllerDialog::InputController(ControllerInput input) { } input_subsystem->GetTas()->RecordInput(buttons, input.axis_values); } - -void ControllerDialog::UpdateController(bool update) { - if (!update) { - return; - } - input_subsystem->GetTas()->UpdateThread(); -} diff --git a/src/yuzu/debugger/controller.h b/src/yuzu/debugger/controller.h index 448d24b67..7742db58b 100644 --- a/src/yuzu/debugger/controller.h +++ b/src/yuzu/debugger/controller.h @@ -25,7 +25,6 @@ struct ControllerInput { struct ControllerCallback { std::function input; - std::function update; }; class ControllerDialog : public QWidget { @@ -45,7 +44,6 @@ protected: private: void InputController(ControllerInput input); - void UpdateController(bool update); QAction* toggle_view_action = nullptr; QFileSystemWatcher* watcher = nullptr; PlayerControlPreview* widget; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 7d12fcca7..ea77caad5 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -31,8 +31,6 @@ #include "core/hle/service/am/applet_oe.h" #include "core/hle/service/am/applets/applets.h" -#include "input_common/tas/tas_input.h" - // These are wrappers to avoid the calls to CreateDirectory and CreateFile because of the Windows // defines. static FileSys::VirtualDir VfsFilesystemCreateDirectoryWrapper( @@ -105,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" From 75d8ec1e9f474ce6c2bfc0b8ebe574ca44f9f3d8 Mon Sep 17 00:00:00 2001 From: german77 Date: Sun, 25 Jul 2021 20:52:19 -0500 Subject: [PATCH 11/11] UI: Relocate tas menu and add brief description --- src/input_common/main.cpp | 2 +- src/input_common/tas/tas_input.cpp | 92 ++++++++++++------- src/input_common/tas/tas_input.h | 48 +++++----- src/yuzu/configuration/configure_tas.cpp | 2 +- src/yuzu/configuration/configure_tas.ui | 47 +++++++++- .../configuration/configure_vibration.cpp | 2 +- src/yuzu/debugger/controller.cpp | 2 +- src/yuzu/main.cpp | 21 +++-- src/yuzu/main.h | 2 +- src/yuzu/main.ui | 2 +- 10 files changed, 150 insertions(+), 70 deletions(-) diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp index 3b9906b53..18d7d8817 100644 --- a/src/input_common/main.cpp +++ b/src/input_common/main.cpp @@ -117,7 +117,7 @@ struct InputSubsystem::Impl { Common::ParamPackage{{"display", "Keyboard/Mouse"}, {"class", "keyboard"}}, }; if (Settings::values.tas_enable) { - devices.push_back( + devices.emplace_back( Common::ParamPackage{{"display", "TAS Controller"}, {"class", "tas"}}); } #ifdef HAVE_SDL2 diff --git a/src/input_common/tas/tas_input.cpp b/src/input_common/tas/tas_input.cpp index 877d35088..1598092b6 100644 --- a/src/input_common/tas/tas_input.cpp +++ b/src/input_common/tas/tas_input.cpp @@ -40,12 +40,15 @@ constexpr std::array, 20> text_to_tas_but Tas::Tas() { if (!Settings::values.tas_enable) { + needs_reset = true; return; } LoadTasFiles(); } -Tas::~Tas() = default; +Tas::~Tas() { + Stop(); +}; void Tas::LoadTasFiles() { script_length = 0; @@ -184,6 +187,9 @@ std::string Tas::ButtonsToString(u32 button) const { void Tas::UpdateThread() { if (!Settings::values.tas_enable) { + if (is_running) { + Stop(); + } return; } @@ -196,34 +202,35 @@ void Tas::UpdateThread() { LoadTasFiles(); LOG_DEBUG(Input, "tas_reset done"); } - if (is_running) { - 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(); + + 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)); } @@ -237,8 +244,8 @@ TasAnalog Tas::ReadCommandAxis(const std::string& line) const { seglist.push_back(segment); } - const float x = std::stof(seglist.at(0)) / 32767.f; - const float y = std::stof(seglist.at(1)) / 32767.f; + const float x = std::stof(seglist.at(0)) / 32767.0f; + const float y = std::stof(seglist.at(1)) / 32767.0f; return {x, y}; } @@ -293,12 +300,20 @@ std::string Tas::WriteCommandButtons(u32 data) const { } void Tas::StartStop() { - is_running = !is_running; - if (is_running) { - SwapToTasController(); - } else { - SwapToStoredController(); + 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() { @@ -315,7 +330,8 @@ void Tas::SwapToTasController() { continue; } - auto tas_param = Common::ParamPackage{{"pad", static_cast(index)}}; + Common::ParamPackage tas_param; + tas_param.Set("pad", static_cast(index)); auto button_mapping = GetButtonMappingForDevice(tas_param); auto analog_mapping = GetAnalogMappingForDevice(tas_param); auto& buttons = player.buttons; @@ -328,25 +344,33 @@ void Tas::SwapToTasController() { analogs[i] = analog_mapping[static_cast(i)].Serialize(); } } + is_old_input_saved = true; Settings::values.is_device_reload_pending.store(true); } void Tas::SwapToStoredController() { - if (!Settings::values.tas_swap_controllers) { + 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; } diff --git a/src/input_common/tas/tas_input.h b/src/input_common/tas/tas_input.h index 52d000db4..3e2db8f00 100644 --- a/src/input_common/tas/tas_input.h +++ b/src/input_common/tas/tas_input.h @@ -13,8 +13,8 @@ /* To play back TAS scripts on Yuzu, select the folder with scripts in the configuration menu below -Emulation -> 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). +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: @@ -56,26 +56,26 @@ enum class TasState { }; 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, + 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 { @@ -105,6 +105,9 @@ public: // 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(); @@ -219,6 +222,7 @@ private: size_t script_length{0}; std::array tas_data; + bool is_old_input_saved{false}; bool is_recording{false}; bool is_running{false}; bool needs_reset{false}; diff --git a/src/yuzu/configuration/configure_tas.cpp b/src/yuzu/configuration/configure_tas.cpp index 00d6c1ba5..b666b175a 100644 --- a/src/yuzu/configuration/configure_tas.cpp +++ b/src/yuzu/configuration/configure_tas.cpp @@ -55,7 +55,7 @@ void ConfigureTasDialog::SetDirectory(DirectoryTarget target, QLineEdit* edit) { QString str = QFileDialog::getExistingDirectory(this, caption, edit->text()); - if (str.isNull() || str.isEmpty()) { + if (str.isEmpty()) { return; } diff --git a/src/yuzu/configuration/configure_tas.ui b/src/yuzu/configuration/configure_tas.ui index 445904d8f..8a3ecb834 100644 --- a/src/yuzu/configuration/configure_tas.ui +++ b/src/yuzu/configuration/configure_tas.ui @@ -19,7 +19,50 @@ - TAS Settings + TAS + + + + + + 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. + + + true + + + + + + + To check which hotkeys control the playback/recording, please refer to the Hotkey settings (General -> Hotkeys). + + + true + + + + + + + WARNING: This is an experimental feature. It will not play back scripts frame perfectly with the current, imperfect syncing method. + + + true + + + + + + + + + + + + + + Settings @@ -63,7 +106,7 @@ - TAS Directories + Script Directory diff --git a/src/yuzu/configuration/configure_vibration.cpp b/src/yuzu/configuration/configure_vibration.cpp index 9d92c4949..46a0f3025 100644 --- a/src/yuzu/configuration/configure_vibration.cpp +++ b/src/yuzu/configuration/configure_vibration.cpp @@ -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; } diff --git a/src/yuzu/debugger/controller.cpp b/src/yuzu/debugger/controller.cpp index 296000ed5..5a844409b 100644 --- a/src/yuzu/debugger/controller.cpp +++ b/src/yuzu/debugger/controller.cpp @@ -78,7 +78,7 @@ void ControllerDialog::InputController(ControllerInput input) { u32 buttons = 0; int index = 0; for (bool btn : input.button_values) { - buttons += (btn ? 1 : 0) << index; + buttons |= (btn ? 1U : 0U) << index; index++; } input_subsystem->GetTas()->RecordInput(buttons, input.axis_values); diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index ea77caad5..3c2824362 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -1022,18 +1022,25 @@ void GMainWindow::InitializeHotkeys() { } }); connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Start/Stop"), this), - &QShortcut::activated, this, [&] { input_subsystem->GetTas()->StartStop(); }); + &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) { - QMessageBox::StandardButton reply; - reply = QMessageBox::question(this, tr("TAS Recording"), - tr("Overwrite file of player 1?"), - QMessageBox::Yes | QMessageBox::No); - input_subsystem->GetTas()->SaveRecording(reply == QMessageBox::Yes); + 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); } }); } @@ -1487,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); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 610b59ee5..36eed6103 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -320,7 +320,7 @@ private: QLabel* emu_speed_label = nullptr; QLabel* game_fps_label = nullptr; QLabel* emu_frametime_label = nullptr; - QLabel* TASlabel; + QLabel* tas_label = nullptr; QPushButton* gpu_accuracy_button = nullptr; QPushButton* renderer_status_button = nullptr; QPushButton* dock_status_button = nullptr; diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 31c1a20f3..653c010d8 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -72,7 +72,6 @@ - @@ -101,6 +100,7 @@ +