yuzu-emu
/
yuzu-android
Archived
1
0
Fork 0

Merge pull request #6485 from MonsterDruide1/tas

input_common: TAS with script playback & recording
This commit is contained in:
bunnei 2021-09-18 16:30:06 -07:00 committed by GitHub
commit 6e376c27a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1426 additions and 21 deletions

View File

@ -21,6 +21,7 @@
#define SCREENSHOTS_DIR "screenshots"
#define SDMC_DIR "sdmc"
#define SHADER_DIR "shader"
#define TAS_DIR "tas"
// yuzu-specific files

View File

@ -116,6 +116,7 @@ private:
GenerateYuzuPath(YuzuPath::ScreenshotsDir, yuzu_path / SCREENSHOTS_DIR);
GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR);
GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR);
GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR);
}
~PathManagerImpl() = default;

View File

@ -23,6 +23,7 @@ enum class YuzuPath {
ScreenshotsDir, // Where yuzu screenshots are stored.
SDMCDir, // Where the emulated SDMC is stored.
ShaderDir, // Where shaders are stored.
TASDir, // Where TAS scripts are stored.
};
/**

View File

@ -512,6 +512,11 @@ struct Values {
"motion_device"};
BasicSetting<std::string> udp_input_servers{"127.0.0.1:26760", "udp_input_servers"};
BasicSetting<bool> pause_tas_on_load{true, "pause_tas_on_load"};
BasicSetting<bool> tas_enable{false, "tas_enable"};
BasicSetting<bool> tas_loop{false, "tas_loop"};
BasicSetting<bool> tas_swap_controllers{true, "tas_swap_controllers"};
BasicSetting<bool> mouse_panning{false, "mouse_panning"};
BasicRangedSetting<u8> mouse_panning_sensitivity{10, 1, 100, "mouse_panning_sensitivity"};
BasicSetting<bool> mouse_enabled{false, "mouse_enabled"};

View File

@ -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

View File

@ -5,6 +5,7 @@
#include <memory>
#include <thread>
#include "common/param_package.h"
#include "common/settings.h"
#include "input_common/analog_from_button.h"
#include "input_common/gcadapter/gc_adapter.h"
#include "input_common/gcadapter/gc_poller.h"
@ -13,6 +14,8 @@
#include "input_common/motion_from_button.h"
#include "input_common/mouse/mouse_input.h"
#include "input_common/mouse/mouse_poller.h"
#include "input_common/tas/tas_input.h"
#include "input_common/tas/tas_poller.h"
#include "input_common/touch_from_button.h"
#include "input_common/udp/client.h"
#include "input_common/udp/udp.h"
@ -60,6 +63,12 @@ struct InputSubsystem::Impl {
Input::RegisterFactory<Input::MotionDevice>("mouse", mousemotion);
mousetouch = std::make_shared<MouseTouchFactory>(mouse);
Input::RegisterFactory<Input::TouchDevice>("mouse", mousetouch);
tas = std::make_shared<TasInput::Tas>();
tasbuttons = std::make_shared<TasButtonFactory>(tas);
Input::RegisterFactory<Input::ButtonDevice>("tas", tasbuttons);
tasanalog = std::make_shared<TasAnalogFactory>(tas);
Input::RegisterFactory<Input::AnalogDevice>("tas", tasanalog);
}
void Shutdown() {
@ -94,6 +103,12 @@ struct InputSubsystem::Impl {
mouseanalog.reset();
mousemotion.reset();
mousetouch.reset();
Input::UnregisterFactory<Input::ButtonDevice>("tas");
Input::UnregisterFactory<Input::AnalogDevice>("tas");
tasbuttons.reset();
tasanalog.reset();
}
[[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const {
@ -101,6 +116,10 @@ struct InputSubsystem::Impl {
Common::ParamPackage{{"display", "Any"}, {"class", "any"}},
Common::ParamPackage{{"display", "Keyboard/Mouse"}, {"class", "keyboard"}},
};
if (Settings::values.tas_enable) {
devices.emplace_back(
Common::ParamPackage{{"display", "TAS Controller"}, {"class", "tas"}});
}
#ifdef HAVE_SDL2
auto sdl_devices = sdl->GetInputDevices();
devices.insert(devices.end(), sdl_devices.begin(), sdl_devices.end());
@ -120,6 +139,9 @@ struct InputSubsystem::Impl {
if (params.Get("class", "") == "gcpad") {
return gcadapter->GetAnalogMappingForDevice(params);
}
if (params.Get("class", "") == "tas") {
return tas->GetAnalogMappingForDevice(params);
}
#ifdef HAVE_SDL2
if (params.Get("class", "") == "sdl") {
return sdl->GetAnalogMappingForDevice(params);
@ -136,6 +158,9 @@ struct InputSubsystem::Impl {
if (params.Get("class", "") == "gcpad") {
return gcadapter->GetButtonMappingForDevice(params);
}
if (params.Get("class", "") == "tas") {
return tas->GetButtonMappingForDevice(params);
}
#ifdef HAVE_SDL2
if (params.Get("class", "") == "sdl") {
return sdl->GetButtonMappingForDevice(params);
@ -174,9 +199,12 @@ struct InputSubsystem::Impl {
std::shared_ptr<MouseAnalogFactory> mouseanalog;
std::shared_ptr<MouseMotionFactory> mousemotion;
std::shared_ptr<MouseTouchFactory> mousetouch;
std::shared_ptr<TasButtonFactory> tasbuttons;
std::shared_ptr<TasAnalogFactory> tasanalog;
std::shared_ptr<CemuhookUDP::Client> udp;
std::shared_ptr<GCAdapter::Adapter> gcadapter;
std::shared_ptr<MouseInput::Mouse> mouse;
std::shared_ptr<TasInput::Tas> tas;
};
InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {}
@ -207,6 +235,14 @@ const MouseInput::Mouse* InputSubsystem::GetMouse() const {
return impl->mouse.get();
}
TasInput::Tas* InputSubsystem::GetTas() {
return impl->tas.get();
}
const TasInput::Tas* InputSubsystem::GetTas() const {
return impl->tas.get();
}
std::vector<Common::ParamPackage> InputSubsystem::GetInputDevices() const {
return impl->GetInputDevices();
}
@ -287,6 +323,22 @@ const MouseTouchFactory* InputSubsystem::GetMouseTouch() const {
return impl->mousetouch.get();
}
TasButtonFactory* InputSubsystem::GetTasButtons() {
return impl->tasbuttons.get();
}
const TasButtonFactory* InputSubsystem::GetTasButtons() const {
return impl->tasbuttons.get();
}
TasAnalogFactory* InputSubsystem::GetTasAnalogs() {
return impl->tasanalog.get();
}
const TasAnalogFactory* InputSubsystem::GetTasAnalogs() const {
return impl->tasanalog.get();
}
void InputSubsystem::ReloadInputDevices() {
if (!impl->udp) {
return;

View File

@ -29,6 +29,10 @@ namespace MouseInput {
class Mouse;
}
namespace TasInput {
class Tas;
}
namespace InputCommon {
namespace Polling {
@ -64,6 +68,8 @@ class MouseButtonFactory;
class MouseAnalogFactory;
class MouseMotionFactory;
class MouseTouchFactory;
class TasButtonFactory;
class TasAnalogFactory;
class Keyboard;
/**
@ -103,6 +109,11 @@ public:
/// Retrieves the underlying mouse device.
[[nodiscard]] const MouseInput::Mouse* GetMouse() const;
/// Retrieves the underlying tas device.
[[nodiscard]] TasInput::Tas* GetTas();
/// Retrieves the underlying tas device.
[[nodiscard]] const TasInput::Tas* GetTas() const;
/**
* Returns all available input devices that this Factory can create a new device with.
* Each returned ParamPackage should have a `display` field used for display, a class field for
@ -144,30 +155,42 @@ public:
/// Retrieves the underlying udp touch handler.
[[nodiscard]] const UDPTouchFactory* GetUDPTouch() const;
/// Retrieves the underlying GameCube button handler.
/// Retrieves the underlying mouse button handler.
[[nodiscard]] MouseButtonFactory* GetMouseButtons();
/// Retrieves the underlying GameCube button handler.
/// Retrieves the underlying mouse button handler.
[[nodiscard]] const MouseButtonFactory* GetMouseButtons() const;
/// Retrieves the underlying udp touch handler.
/// Retrieves the underlying mouse analog handler.
[[nodiscard]] MouseAnalogFactory* GetMouseAnalogs();
/// Retrieves the underlying udp touch handler.
/// Retrieves the underlying mouse analog handler.
[[nodiscard]] const MouseAnalogFactory* GetMouseAnalogs() const;
/// Retrieves the underlying udp motion handler.
/// Retrieves the underlying mouse motion handler.
[[nodiscard]] MouseMotionFactory* GetMouseMotions();
/// Retrieves the underlying udp motion handler.
/// Retrieves the underlying mouse motion handler.
[[nodiscard]] const MouseMotionFactory* GetMouseMotions() const;
/// Retrieves the underlying udp touch handler.
/// Retrieves the underlying mouse touch handler.
[[nodiscard]] MouseTouchFactory* GetMouseTouch();
/// Retrieves the underlying udp touch handler.
/// Retrieves the underlying mouse touch handler.
[[nodiscard]] const MouseTouchFactory* GetMouseTouch() const;
/// Retrieves the underlying tas button handler.
[[nodiscard]] TasButtonFactory* GetTasButtons();
/// Retrieves the underlying tas button handler.
[[nodiscard]] const TasButtonFactory* GetTasButtons() const;
/// Retrieves the underlying tas analogs handler.
[[nodiscard]] TasAnalogFactory* GetTasAnalogs();
/// Retrieves the underlying tas analogs handler.
[[nodiscard]] const TasAnalogFactory* GetTasAnalogs() const;
/// Reloads the input devices
void ReloadInputDevices();

View File

@ -0,0 +1,455 @@
// Copyright 2021 yuzu Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include <cstring>
#include <regex>
#include "common/fs/file.h"
#include "common/fs/fs_types.h"
#include "common/fs/path_util.h"
#include "common/logging/log.h"
#include "common/settings.h"
#include "input_common/tas/tas_input.h"
namespace TasInput {
// Supported keywords and buttons from a TAS file
constexpr std::array<std::pair<std::string_view, TasButton>, 20> text_to_tas_button = {
std::pair{"KEY_A", TasButton::BUTTON_A},
{"KEY_B", TasButton::BUTTON_B},
{"KEY_X", TasButton::BUTTON_X},
{"KEY_Y", TasButton::BUTTON_Y},
{"KEY_LSTICK", TasButton::STICK_L},
{"KEY_RSTICK", TasButton::STICK_R},
{"KEY_L", TasButton::TRIGGER_L},
{"KEY_R", TasButton::TRIGGER_R},
{"KEY_PLUS", TasButton::BUTTON_PLUS},
{"KEY_MINUS", TasButton::BUTTON_MINUS},
{"KEY_DLEFT", TasButton::BUTTON_LEFT},
{"KEY_DUP", TasButton::BUTTON_UP},
{"KEY_DRIGHT", TasButton::BUTTON_RIGHT},
{"KEY_DDOWN", TasButton::BUTTON_DOWN},
{"KEY_SL", TasButton::BUTTON_SL},
{"KEY_SR", TasButton::BUTTON_SR},
{"KEY_CAPTURE", TasButton::BUTTON_CAPTURE},
{"KEY_HOME", TasButton::BUTTON_HOME},
{"KEY_ZL", TasButton::TRIGGER_ZL},
{"KEY_ZR", TasButton::TRIGGER_ZR},
};
Tas::Tas() {
if (!Settings::values.tas_enable) {
needs_reset = true;
return;
}
LoadTasFiles();
}
Tas::~Tas() {
Stop();
};
void Tas::LoadTasFiles() {
script_length = 0;
for (size_t i = 0; i < commands.size(); i++) {
LoadTasFile(i);
if (commands[i].size() > script_length) {
script_length = commands[i].size();
}
}
}
void Tas::LoadTasFile(size_t player_index) {
if (!commands[player_index].empty()) {
commands[player_index].clear();
}
std::string file =
Common::FS::ReadStringFromFile(Common::FS::GetYuzuPath(Common::FS::YuzuPath::TASDir) /
fmt::format("script0-{}.txt", player_index + 1),
Common::FS::FileType::BinaryFile);
std::stringstream command_line(file);
std::string line;
int frame_no = 0;
while (std::getline(command_line, line, '\n')) {
if (line.empty()) {
continue;
}
LOG_DEBUG(Input, "Loading line: {}", line);
std::smatch m;
std::stringstream linestream(line);
std::string segment;
std::vector<std::string> seglist;
while (std::getline(linestream, segment, ' ')) {
seglist.push_back(segment);
}
if (seglist.size() < 4) {
continue;
}
while (frame_no < std::stoi(seglist.at(0))) {
commands[player_index].push_back({});
frame_no++;
}
TASCommand command = {
.buttons = ReadCommandButtons(seglist.at(1)),
.l_axis = ReadCommandAxis(seglist.at(2)),
.r_axis = ReadCommandAxis(seglist.at(3)),
};
commands[player_index].push_back(command);
frame_no++;
}
LOG_INFO(Input, "TAS file loaded! {} frames", frame_no);
}
void Tas::WriteTasFile(std::u8string file_name) {
std::string output_text;
for (size_t frame = 0; frame < record_commands.size(); frame++) {
if (!output_text.empty()) {
output_text += "\n";
}
const TASCommand& line = record_commands[frame];
output_text += std::to_string(frame) + " " + WriteCommandButtons(line.buttons) + " " +
WriteCommandAxis(line.l_axis) + " " + WriteCommandAxis(line.r_axis);
}
const auto bytes_written = Common::FS::WriteStringToFile(
Common::FS::GetYuzuPath(Common::FS::YuzuPath::TASDir) / file_name,
Common::FS::FileType::TextFile, output_text);
if (bytes_written == output_text.size()) {
LOG_INFO(Input, "TAS file written to file!");
} else {
LOG_ERROR(Input, "Writing the TAS-file has failed! {} / {} bytes written", bytes_written,
output_text.size());
}
}
std::pair<float, float> Tas::FlipAxisY(std::pair<float, float> old) {
auto [x, y] = old;
return {x, -y};
}
void Tas::RecordInput(u32 buttons, const std::array<std::pair<float, float>, 2>& axes) {
last_input = {buttons, FlipAxisY(axes[0]), FlipAxisY(axes[1])};
}
std::tuple<TasState, size_t, size_t> Tas::GetStatus() const {
TasState state;
if (is_recording) {
return {TasState::Recording, 0, record_commands.size()};
}
if (is_running) {
state = TasState::Running;
} else {
state = TasState::Stopped;
}
return {state, current_command, script_length};
}
std::string Tas::DebugButtons(u32 buttons) const {
return fmt::format("{{ {} }}", TasInput::Tas::ButtonsToString(buttons));
}
std::string Tas::DebugJoystick(float x, float y) const {
return fmt::format("[ {} , {} ]", std::to_string(x), std::to_string(y));
}
std::string Tas::DebugInput(const TasData& data) const {
return fmt::format("{{ {} , {} , {} }}", DebugButtons(data.buttons),
DebugJoystick(data.axis[0], data.axis[1]),
DebugJoystick(data.axis[2], data.axis[3]));
}
std::string Tas::DebugInputs(const std::array<TasData, PLAYER_NUMBER>& arr) const {
std::string returns = "[ ";
for (size_t i = 0; i < arr.size(); i++) {
returns += DebugInput(arr[i]);
if (i != arr.size() - 1) {
returns += " , ";
}
}
return returns + "]";
}
std::string Tas::ButtonsToString(u32 button) const {
std::string returns;
for (auto [text_button, tas_button] : text_to_tas_button) {
if ((button & static_cast<u32>(tas_button)) != 0)
returns += fmt::format(", {}", text_button.substr(4));
}
return returns.empty() ? "" : returns.substr(2);
}
void Tas::UpdateThread() {
if (!Settings::values.tas_enable) {
if (is_running) {
Stop();
}
return;
}
if (is_recording) {
record_commands.push_back(last_input);
}
if (needs_reset) {
current_command = 0;
needs_reset = false;
LoadTasFiles();
LOG_DEBUG(Input, "tas_reset done");
}
if (!is_running) {
tas_data.fill({});
return;
}
if (current_command < script_length) {
LOG_DEBUG(Input, "Playing TAS {}/{}", current_command, script_length);
size_t frame = current_command++;
for (size_t i = 0; i < commands.size(); i++) {
if (frame < commands[i].size()) {
TASCommand command = commands[i][frame];
tas_data[i].buttons = command.buttons;
auto [l_axis_x, l_axis_y] = command.l_axis;
tas_data[i].axis[0] = l_axis_x;
tas_data[i].axis[1] = l_axis_y;
auto [r_axis_x, r_axis_y] = command.r_axis;
tas_data[i].axis[2] = r_axis_x;
tas_data[i].axis[3] = r_axis_y;
} else {
tas_data[i] = {};
}
}
} else {
is_running = Settings::values.tas_loop.GetValue();
current_command = 0;
tas_data.fill({});
if (!is_running) {
SwapToStoredController();
}
}
LOG_DEBUG(Input, "TAS inputs: {}", DebugInputs(tas_data));
}
TasAnalog Tas::ReadCommandAxis(const std::string& line) const {
std::stringstream linestream(line);
std::string segment;
std::vector<std::string> seglist;
while (std::getline(linestream, segment, ';')) {
seglist.push_back(segment);
}
const float x = std::stof(seglist.at(0)) / 32767.0f;
const float y = std::stof(seglist.at(1)) / 32767.0f;
return {x, y};
}
u32 Tas::ReadCommandButtons(const std::string& data) const {
std::stringstream button_text(data);
std::string line;
u32 buttons = 0;
while (std::getline(button_text, line, ';')) {
for (auto [text, tas_button] : text_to_tas_button) {
if (text == line) {
buttons |= static_cast<u32>(tas_button);
break;
}
}
}
return buttons;
}
std::string Tas::WriteCommandAxis(TasAnalog data) const {
auto [x, y] = data;
std::string line;
line += std::to_string(static_cast<int>(x * 32767));
line += ";";
line += std::to_string(static_cast<int>(y * 32767));
return line;
}
std::string Tas::WriteCommandButtons(u32 data) const {
if (data == 0) {
return "NONE";
}
std::string line;
u32 index = 0;
while (data > 0) {
if ((data & 1) == 1) {
for (auto [text, tas_button] : text_to_tas_button) {
if (tas_button == static_cast<TasButton>(1 << index)) {
if (line.size() > 0) {
line += ";";
}
line += text;
break;
}
}
}
index++;
data >>= 1;
}
return line;
}
void Tas::StartStop() {
if (!Settings::values.tas_enable) {
return;
}
if (is_running) {
Stop();
} else {
is_running = true;
SwapToTasController();
}
}
void Tas::Stop() {
is_running = false;
SwapToStoredController();
}
void Tas::SwapToTasController() {
if (!Settings::values.tas_swap_controllers) {
return;
}
auto& players = Settings::values.players.GetValue();
for (std::size_t index = 0; index < players.size(); index++) {
auto& player = players[index];
player_mappings[index] = player;
// Only swap active controllers
if (!player.connected) {
continue;
}
Common::ParamPackage tas_param;
tas_param.Set("pad", static_cast<u8>(index));
auto button_mapping = GetButtonMappingForDevice(tas_param);
auto analog_mapping = GetAnalogMappingForDevice(tas_param);
auto& buttons = player.buttons;
auto& analogs = player.analogs;
for (std::size_t i = 0; i < buttons.size(); ++i) {
buttons[i] = button_mapping[static_cast<Settings::NativeButton::Values>(i)].Serialize();
}
for (std::size_t i = 0; i < analogs.size(); ++i) {
analogs[i] = analog_mapping[static_cast<Settings::NativeAnalog::Values>(i)].Serialize();
}
}
is_old_input_saved = true;
Settings::values.is_device_reload_pending.store(true);
}
void Tas::SwapToStoredController() {
if (!is_old_input_saved) {
return;
}
auto& players = Settings::values.players.GetValue();
for (std::size_t index = 0; index < players.size(); index++) {
players[index] = player_mappings[index];
}
is_old_input_saved = false;
Settings::values.is_device_reload_pending.store(true);
}
void Tas::Reset() {
if (!Settings::values.tas_enable) {
return;
}
needs_reset = true;
}
bool Tas::Record() {
if (!Settings::values.tas_enable) {
return true;
}
is_recording = !is_recording;
return is_recording;
}
void Tas::SaveRecording(bool overwrite_file) {
if (is_recording) {
return;
}
if (record_commands.empty()) {
return;
}
WriteTasFile(u8"record.txt");
if (overwrite_file) {
WriteTasFile(u8"script0-1.txt");
}
needs_reset = true;
record_commands.clear();
}
InputCommon::ButtonMapping Tas::GetButtonMappingForDevice(
const Common::ParamPackage& params) const {
// This list is missing ZL/ZR since those are not considered buttons.
// We will add those afterwards
// This list also excludes any button that can't be really mapped
static constexpr std::array<std::pair<Settings::NativeButton::Values, TasButton>, 20>
switch_to_tas_button = {
std::pair{Settings::NativeButton::A, TasButton::BUTTON_A},
{Settings::NativeButton::B, TasButton::BUTTON_B},
{Settings::NativeButton::X, TasButton::BUTTON_X},
{Settings::NativeButton::Y, TasButton::BUTTON_Y},
{Settings::NativeButton::LStick, TasButton::STICK_L},
{Settings::NativeButton::RStick, TasButton::STICK_R},
{Settings::NativeButton::L, TasButton::TRIGGER_L},
{Settings::NativeButton::R, TasButton::TRIGGER_R},
{Settings::NativeButton::Plus, TasButton::BUTTON_PLUS},
{Settings::NativeButton::Minus, TasButton::BUTTON_MINUS},
{Settings::NativeButton::DLeft, TasButton::BUTTON_LEFT},
{Settings::NativeButton::DUp, TasButton::BUTTON_UP},
{Settings::NativeButton::DRight, TasButton::BUTTON_RIGHT},
{Settings::NativeButton::DDown, TasButton::BUTTON_DOWN},
{Settings::NativeButton::SL, TasButton::BUTTON_SL},
{Settings::NativeButton::SR, TasButton::BUTTON_SR},
{Settings::NativeButton::Screenshot, TasButton::BUTTON_CAPTURE},
{Settings::NativeButton::Home, TasButton::BUTTON_HOME},
{Settings::NativeButton::ZL, TasButton::TRIGGER_ZL},
{Settings::NativeButton::ZR, TasButton::TRIGGER_ZR},
};
InputCommon::ButtonMapping mapping{};
for (const auto& [switch_button, tas_button] : switch_to_tas_button) {
Common::ParamPackage button_params({{"engine", "tas"}});
button_params.Set("pad", params.Get("pad", 0));
button_params.Set("button", static_cast<int>(tas_button));
mapping.insert_or_assign(switch_button, std::move(button_params));
}
return mapping;
}
InputCommon::AnalogMapping Tas::GetAnalogMappingForDevice(
const Common::ParamPackage& params) const {
InputCommon::AnalogMapping mapping = {};
Common::ParamPackage left_analog_params;
left_analog_params.Set("engine", "tas");
left_analog_params.Set("pad", params.Get("pad", 0));
left_analog_params.Set("axis_x", static_cast<int>(TasAxes::StickX));
left_analog_params.Set("axis_y", static_cast<int>(TasAxes::StickY));
mapping.insert_or_assign(Settings::NativeAnalog::LStick, std::move(left_analog_params));
Common::ParamPackage right_analog_params;
right_analog_params.Set("engine", "tas");
right_analog_params.Set("pad", params.Get("pad", 0));
right_analog_params.Set("axis_x", static_cast<int>(TasAxes::SubstickX));
right_analog_params.Set("axis_y", static_cast<int>(TasAxes::SubstickY));
mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params));
return mapping;
}
const TasData& Tas::GetTasState(std::size_t pad) const {
return tas_data[pad];
}
} // namespace TasInput

View File

@ -0,0 +1,237 @@
// Copyright 2020 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <array>
#include "common/common_types.h"
#include "common/settings_input.h"
#include "core/frontend/input.h"
#include "input_common/main.h"
/*
To play back TAS scripts on Yuzu, select the folder with scripts in the configuration menu below
Tools -> Configure TAS. The file itself has normal text format and has to be called script0-1.txt
for controller 1, script0-2.txt for controller 2 and so forth (with max. 8 players).
A script file has the same format as TAS-nx uses, so final files will look like this:
1 KEY_B 0;0 0;0
6 KEY_ZL 0;0 0;0
41 KEY_ZL;KEY_Y 0;0 0;0
43 KEY_X;KEY_A 32767;0 0;0
44 KEY_A 32767;0 0;0
45 KEY_A 32767;0 0;0
46 KEY_A 32767;0 0;0
47 KEY_A 32767;0 0;0
After placing the file at the correct location, it can be read into Yuzu with the (default) hotkey
CTRL+F6 (refresh). In the bottom left corner, it will display the amount of frames the script file
has. Playback can be started or stopped using CTRL+F5.
However, for playback to actually work, the correct input device has to be selected: In the Controls
menu, select TAS from the device list for the controller that the script should be played on.
Recording a new script file is really simple: Just make sure that the proper device (not TAS) is
connected on P1, and press CTRL+F7 to start recording. When done, just press the same keystroke
again (CTRL+F7). The new script will be saved at the location previously selected, as the filename
record.txt.
For debugging purposes, the common controller debugger can be used (View -> Debugging -> Controller
P1).
*/
namespace TasInput {
constexpr size_t PLAYER_NUMBER = 8;
using TasAnalog = std::pair<float, float>;
enum class TasState {
Running,
Recording,
Stopped,
};
enum class TasButton : u32 {
BUTTON_A = 1U << 0,
BUTTON_B = 1U << 1,
BUTTON_X = 1U << 2,
BUTTON_Y = 1U << 3,
STICK_L = 1U << 4,
STICK_R = 1U << 5,
TRIGGER_L = 1U << 6,
TRIGGER_R = 1U << 7,
TRIGGER_ZL = 1U << 8,
TRIGGER_ZR = 1U << 9,
BUTTON_PLUS = 1U << 10,
BUTTON_MINUS = 1U << 11,
BUTTON_LEFT = 1U << 12,
BUTTON_UP = 1U << 13,
BUTTON_RIGHT = 1U << 14,
BUTTON_DOWN = 1U << 15,
BUTTON_SL = 1U << 16,
BUTTON_SR = 1U << 17,
BUTTON_HOME = 1U << 18,
BUTTON_CAPTURE = 1U << 19,
};
enum class TasAxes : u8 {
StickX,
StickY,
SubstickX,
SubstickY,
Undefined,
};
struct TasData {
u32 buttons{};
std::array<float, 4> axis{};
};
class Tas {
public:
Tas();
~Tas();
// Changes the input status that will be stored in each frame
void RecordInput(u32 buttons, const std::array<std::pair<float, float>, 2>& axes);
// Main loop that records or executes input
void UpdateThread();
// Sets the flag to start or stop the TAS command excecution and swaps controllers profiles
void StartStop();
// Stop the TAS and reverts any controller profile
void Stop();
// Sets the flag to reload the file and start from the begining in the next update
void Reset();
/**
* Sets the flag to enable or disable recording of inputs
* @return Returns true if the current recording status is enabled
*/
bool Record();
// Saves contents of record_commands on a file if overwrite is enabled player 1 will be
// overwritten with the recorded commands
void SaveRecording(bool overwrite_file);
/**
* Returns the current status values of TAS playback/recording
* @return Tuple of
* TasState indicating the current state out of Running, Recording or Stopped ;
* Current playback progress or amount of frames (so far) for Recording ;
* Total length of script file currently loaded or amount of frames (so far) for Recording
*/
std::tuple<TasState, size_t, size_t> GetStatus() const;
// Retuns an array of the default button mappings
InputCommon::ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) const;
// Retuns an array of the default analog mappings
InputCommon::AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) const;
[[nodiscard]] const TasData& GetTasState(std::size_t pad) const;
private:
struct TASCommand {
u32 buttons{};
TasAnalog l_axis{};
TasAnalog r_axis{};
};
// Loads TAS files from all players
void LoadTasFiles();
// Loads TAS file from the specified player
void LoadTasFile(size_t player_index);
// Writes a TAS file from the recorded commands
void WriteTasFile(std::u8string file_name);
/**
* Parses a string containing the axis values with the following format "x;y"
* X and Y have a range from -32767 to 32767
* @return Returns a TAS analog object with axis values with range from -1.0 to 1.0
*/
TasAnalog ReadCommandAxis(const std::string& line) const;
/**
* Parses a string containing the button values with the following format "a;b;c;d..."
* Each button is represented by it's text format specified in text_to_tas_button array
* @return Returns a u32 with each bit representing the status of a button
*/
u32 ReadCommandButtons(const std::string& line) const;
/**
* Converts an u32 containing the button status into the text equivalent
* @return Returns a string with the name of the buttons to be written to the file
*/
std::string WriteCommandButtons(u32 data) const;
/**
* Converts an TAS analog object containing the axis status into the text equivalent
* @return Returns a string with the value of the axis to be written to the file
*/
std::string WriteCommandAxis(TasAnalog data) const;
// Inverts the Y axis polarity
std::pair<float, float> FlipAxisY(std::pair<float, float> old);
/**
* Converts an u32 containing the button status into the text equivalent
* @return Returns a string with the name of the buttons to be printed on console
*/
std::string DebugButtons(u32 buttons) const;
/**
* Converts an TAS analog object containing the axis status into the text equivalent
* @return Returns a string with the value of the axis to be printed on console
*/
std::string DebugJoystick(float x, float y) const;
/**
* Converts the given TAS status into the text equivalent
* @return Returns a string with the value of the TAS status to be printed on console
*/
std::string DebugInput(const TasData& data) const;
/**
* Converts the given TAS status of multiple players into the text equivalent
* @return Returns a string with the value of the status of all TAS players to be printed on
* console
*/
std::string DebugInputs(const std::array<TasData, PLAYER_NUMBER>& arr) const;
/**
* Converts an u32 containing the button status into the text equivalent
* @return Returns a string with the name of the buttons
*/
std::string ButtonsToString(u32 button) const;
// Stores current controller configuration and sets a TAS controller for every active controller
// to the current config
void SwapToTasController();
// Sets the stored controller configuration to the current config
void SwapToStoredController();
size_t script_length{0};
std::array<TasData, PLAYER_NUMBER> tas_data;
bool is_old_input_saved{false};
bool is_recording{false};
bool is_running{false};
bool needs_reset{false};
std::array<std::vector<TASCommand>, PLAYER_NUMBER> commands{};
std::vector<TASCommand> record_commands{};
size_t current_command{0};
TASCommand last_input{}; // only used for recording
// Old settings for swapping controllers
std::array<Settings::PlayerInput, 10> player_mappings;
};
} // namespace TasInput

View File

@ -0,0 +1,101 @@
// Copyright 2021 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <mutex>
#include <utility>
#include "common/settings.h"
#include "common/threadsafe_queue.h"
#include "input_common/tas/tas_input.h"
#include "input_common/tas/tas_poller.h"
namespace InputCommon {
class TasButton final : public Input::ButtonDevice {
public:
explicit TasButton(u32 button_, u32 pad_, const TasInput::Tas* tas_input_)
: button(button_), pad(pad_), tas_input(tas_input_) {}
bool GetStatus() const override {
return (tas_input->GetTasState(pad).buttons & button) != 0;
}
private:
const u32 button;
const u32 pad;
const TasInput::Tas* tas_input;
};
TasButtonFactory::TasButtonFactory(std::shared_ptr<TasInput::Tas> tas_input_)
: tas_input(std::move(tas_input_)) {}
std::unique_ptr<Input::ButtonDevice> TasButtonFactory::Create(const Common::ParamPackage& params) {
const auto button_id = params.Get("button", 0);
const auto pad = params.Get("pad", 0);
return std::make_unique<TasButton>(button_id, pad, tas_input.get());
}
class TasAnalog final : public Input::AnalogDevice {
public:
explicit TasAnalog(u32 pad_, u32 axis_x_, u32 axis_y_, const TasInput::Tas* tas_input_)
: pad(pad_), axis_x(axis_x_), axis_y(axis_y_), tas_input(tas_input_) {}
float GetAxis(u32 axis) const {
std::lock_guard lock{mutex};
return tas_input->GetTasState(pad).axis.at(axis);
}
std::pair<float, float> GetAnalog(u32 analog_axis_x, u32 analog_axis_y) const {
float x = GetAxis(analog_axis_x);
float y = GetAxis(analog_axis_y);
// Make sure the coordinates are in the unit circle,
// otherwise normalize it.
float r = x * x + y * y;
if (r > 1.0f) {
r = std::sqrt(r);
x /= r;
y /= r;
}
return {x, y};
}
std::tuple<float, float> GetStatus() const override {
return GetAnalog(axis_x, axis_y);
}
Input::AnalogProperties GetAnalogProperties() const override {
return {0.0f, 1.0f, 0.5f};
}
private:
const u32 pad;
const u32 axis_x;
const u32 axis_y;
const TasInput::Tas* tas_input;
mutable std::mutex mutex;
};
/// An analog device factory that creates analog devices from GC Adapter
TasAnalogFactory::TasAnalogFactory(std::shared_ptr<TasInput::Tas> tas_input_)
: tas_input(std::move(tas_input_)) {}
/**
* Creates analog device from joystick axes
* @param params contains parameters for creating the device:
* - "port": the nth gcpad on the adapter
* - "axis_x": the index of the axis to be bind as x-axis
* - "axis_y": the index of the axis to be bind as y-axis
*/
std::unique_ptr<Input::AnalogDevice> TasAnalogFactory::Create(const Common::ParamPackage& params) {
const auto pad = static_cast<u32>(params.Get("pad", 0));
const auto axis_x = static_cast<u32>(params.Get("axis_x", 0));
const auto axis_y = static_cast<u32>(params.Get("axis_y", 1));
return std::make_unique<TasAnalog>(pad, axis_x, axis_y, tas_input.get());
}
} // namespace InputCommon

View File

@ -0,0 +1,43 @@
// Copyright 2021 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <memory>
#include "core/frontend/input.h"
#include "input_common/tas/tas_input.h"
namespace InputCommon {
/**
* A button device factory representing a tas bot. It receives tas events and forward them
* to all button devices it created.
*/
class TasButtonFactory final : public Input::Factory<Input::ButtonDevice> {
public:
explicit TasButtonFactory(std::shared_ptr<TasInput::Tas> tas_input_);
/**
* Creates a button device from a button press
* @param params contains parameters for creating the device:
* - "code": the code of the key to bind with the button
*/
std::unique_ptr<Input::ButtonDevice> Create(const Common::ParamPackage& params) override;
private:
std::shared_ptr<TasInput::Tas> tas_input;
};
/// An analog device factory that creates analog devices from tas
class TasAnalogFactory final : public Input::Factory<Input::AnalogDevice> {
public:
explicit TasAnalogFactory(std::shared_ptr<TasInput::Tas> tas_input_);
std::unique_ptr<Input::AnalogDevice> Create(const Common::ParamPackage& params) override;
private:
std::shared_ptr<TasInput::Tas> tas_input;
};
} // namespace InputCommon

View File

@ -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

View File

@ -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();

View File

@ -221,7 +221,7 @@ const std::array<int, Settings::NativeKeyboard::NumKeyboardMods> Config::default
// This must be in alphabetical order according to action name as it must have the same order as
// UISetting::values.shortcuts, which is alphabetically ordered.
// clang-format off
const std::array<UISettings::Shortcut, 18> Config::default_hotkeys{{
const std::array<UISettings::Shortcut, 21> Config::default_hotkeys{{
{QStringLiteral("Capture Screenshot"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+P"), Qt::WidgetWithChildrenShortcut}},
{QStringLiteral("Change Docked Mode"), QStringLiteral("Main Window"), {QStringLiteral("F10"), Qt::ApplicationShortcut}},
{QStringLiteral("Continue/Pause Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F4"), Qt::WindowShortcut}},
@ -235,6 +235,9 @@ const std::array<UISettings::Shortcut, 18> Config::default_hotkeys{{
{QStringLiteral("Mute Audio"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+M"), Qt::WindowShortcut}},
{QStringLiteral("Restart Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F6"), Qt::WindowShortcut}},
{QStringLiteral("Stop Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F5"), Qt::WindowShortcut}},
{QStringLiteral("TAS Start/Stop"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F5"), Qt::ApplicationShortcut}},
{QStringLiteral("TAS Reset"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F6"), Qt::ApplicationShortcut}},
{QStringLiteral("TAS Record"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F7"), Qt::ApplicationShortcut}},
{QStringLiteral("Toggle Filter Bar"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F"), Qt::WindowShortcut}},
{QStringLiteral("Toggle Framerate Limit"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+U"), Qt::ApplicationShortcut}},
{QStringLiteral("Toggle Mouse Panning"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F9"), Qt::ApplicationShortcut}},
@ -564,6 +567,11 @@ void Config::ReadControlValues() {
Settings::values.mouse_panning = false;
ReadBasicSetting(Settings::values.mouse_panning_sensitivity);
ReadBasicSetting(Settings::values.tas_enable);
ReadBasicSetting(Settings::values.tas_loop);
ReadBasicSetting(Settings::values.tas_swap_controllers);
ReadBasicSetting(Settings::values.pause_tas_on_load);
ReadGlobalSetting(Settings::values.use_docked_mode);
// Disable docked mode if handheld is selected
@ -661,6 +669,13 @@ void Config::ReadDataStorageValues() {
QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir)))
.toString()
.toStdString());
FS::SetYuzuPath(FS::YuzuPath::TASDir,
qt_config
->value(QStringLiteral("tas_directory"),
QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASDir)))
.toString()
.toStdString());
ReadBasicSetting(Settings::values.gamecard_inserted);
ReadBasicSetting(Settings::values.gamecard_current_game);
ReadBasicSetting(Settings::values.gamecard_path);
@ -1188,6 +1203,11 @@ void Config::SaveControlValues() {
WriteBasicSetting(Settings::values.emulate_analog_keyboard);
WriteBasicSetting(Settings::values.mouse_panning_sensitivity);
WriteBasicSetting(Settings::values.tas_enable);
WriteBasicSetting(Settings::values.tas_loop);
WriteBasicSetting(Settings::values.tas_swap_controllers);
WriteBasicSetting(Settings::values.pause_tas_on_load);
qt_config->endGroup();
}
@ -1215,6 +1235,10 @@ void Config::SaveDataStorageValues() {
WriteSetting(QStringLiteral("dump_directory"),
QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir)),
QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir)));
WriteSetting(QStringLiteral("tas_directory"),
QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASDir)),
QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASDir)));
WriteBasicSetting(Settings::values.gamecard_inserted);
WriteBasicSetting(Settings::values.gamecard_current_game);
WriteBasicSetting(Settings::values.gamecard_path);

View File

@ -42,7 +42,7 @@ public:
default_mouse_buttons;
static const std::array<int, Settings::NativeKeyboard::NumKeyboardKeys> default_keyboard_keys;
static const std::array<int, Settings::NativeKeyboard::NumKeyboardMods> default_keyboard_mods;
static const std::array<UISettings::Shortcut, 18> default_hotkeys;
static const std::array<UISettings::Shortcut, 21> default_hotkeys;
private:
void Initialize(const std::string& config_name);

View File

@ -124,6 +124,19 @@ QString ButtonToText(const Common::ParamPackage& param) {
return GetKeyName(param.Get("code", 0));
}
if (param.Get("engine", "") == "tas") {
if (param.Has("axis")) {
const QString axis_str = QString::fromStdString(param.Get("axis", ""));
return QObject::tr("TAS Axis %1").arg(axis_str);
}
if (param.Has("button")) {
const QString button_str = QString::number(int(std::log2(param.Get("button", 0))));
return QObject::tr("TAS Btn %1").arg(button_str);
}
return GetKeyName(param.Get("code", 0));
}
if (param.Get("engine", "") == "cemuhookudp") {
if (param.Has("pad_index")) {
const QString motion_str = QString::fromStdString(param.Get("pad_index", ""));
@ -187,7 +200,8 @@ QString AnalogToText(const Common::ParamPackage& param, const std::string& dir)
const QString axis_y_str = QString::fromStdString(param.Get("axis_y", ""));
const bool invert_x = param.Get("invert_x", "+") == "-";
const bool invert_y = param.Get("invert_y", "+") == "-";
if (engine_str == "sdl" || engine_str == "gcpad" || engine_str == "mouse") {
if (engine_str == "sdl" || engine_str == "gcpad" || engine_str == "mouse" ||
engine_str == "tas") {
if (dir == "modifier") {
return QObject::tr("[unused]");
}
@ -926,9 +940,9 @@ void ConfigureInputPlayer::UpdateUI() {
int slider_value;
auto& param = analogs_param[analog_id];
const bool is_controller = param.Get("engine", "") == "sdl" ||
param.Get("engine", "") == "gcpad" ||
param.Get("engine", "") == "mouse";
const bool is_controller =
param.Get("engine", "") == "sdl" || param.Get("engine", "") == "gcpad" ||
param.Get("engine", "") == "mouse" || param.Get("engine", "") == "tas";
if (is_controller) {
if (!param.Has("deadzone")) {
@ -1045,8 +1059,12 @@ int ConfigureInputPlayer::GetIndexFromControllerType(Settings::ControllerType ty
void ConfigureInputPlayer::UpdateInputDevices() {
input_devices = input_subsystem->GetInputDevices();
ui->comboDevices->clear();
for (auto device : input_devices) {
ui->comboDevices->addItem(QString::fromStdString(device.Get("display", "Unknown")), {});
for (auto& device : input_devices) {
const std::string display = device.Get("display", "Unknown");
ui->comboDevices->addItem(QString::fromStdString(display), {});
if (display == "TAS") {
device.Set("pad", static_cast<u8>(player_index));
}
}
}

View File

@ -175,7 +175,7 @@ void PlayerControlPreview::ResetInputs() {
}
void PlayerControlPreview::UpdateInput() {
if (!is_enabled && !mapping_active) {
if (!is_enabled && !mapping_active && !Settings::values.tas_enable) {
return;
}
bool input_changed = false;
@ -222,6 +222,19 @@ void PlayerControlPreview::UpdateInput() {
if (input_changed) {
update();
if (controller_callback.input != nullptr) {
ControllerInput input{
.axis_values = {std::pair<float, float>{
axis_values[Settings::NativeAnalog::LStick].value.x(),
axis_values[Settings::NativeAnalog::LStick].value.y()},
std::pair<float, float>{
axis_values[Settings::NativeAnalog::RStick].value.x(),
axis_values[Settings::NativeAnalog::RStick].value.y()}},
.button_values = button_values,
.changed = true,
};
controller_callback.input(std::move(input));
}
}
if (mapping_active) {
@ -229,6 +242,10 @@ void PlayerControlPreview::UpdateInput() {
}
}
void PlayerControlPreview::SetCallBack(ControllerCallback callback_) {
controller_callback = std::move(callback_);
}
void PlayerControlPreview::paintEvent(QPaintEvent* event) {
QFrame::paintEvent(event);
QPainter p(this);

View File

@ -9,6 +9,7 @@
#include <QPointer>
#include "common/settings.h"
#include "core/frontend/input.h"
#include "yuzu/debugger/controller.h"
class QLabel;
@ -33,6 +34,7 @@ public:
void BeginMappingAnalog(std::size_t button_id);
void EndMapping();
void UpdateInput();
void SetCallBack(ControllerCallback callback_);
protected:
void paintEvent(QPaintEvent* event) override;
@ -181,6 +183,7 @@ private:
using StickArray =
std::array<std::unique_ptr<Input::AnalogDevice>, Settings::NativeAnalog::NUM_STICKS_HID>;
ControllerCallback controller_callback;
bool is_enabled{};
bool mapping_active{};
int blink_counter{};

View File

@ -0,0 +1,84 @@
// Copyright 2021 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <QFileDialog>
#include <QMessageBox>
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/settings.h"
#include "ui_configure_tas.h"
#include "yuzu/configuration/configure_tas.h"
#include "yuzu/uisettings.h"
ConfigureTasDialog::ConfigureTasDialog(QWidget* parent)
: QDialog(parent), ui(std::make_unique<Ui::ConfigureTas>()) {
ui->setupUi(this);
setFocusPolicy(Qt::ClickFocus);
setWindowTitle(tr("TAS Configuration"));
connect(ui->tas_path_button, &QToolButton::pressed, this,
[this] { SetDirectory(DirectoryTarget::TAS, ui->tas_path_edit); });
LoadConfiguration();
}
ConfigureTasDialog::~ConfigureTasDialog() = default;
void ConfigureTasDialog::LoadConfiguration() {
ui->tas_path_edit->setText(
QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::TASDir)));
ui->tas_enable->setChecked(Settings::values.tas_enable.GetValue());
ui->tas_control_swap->setChecked(Settings::values.tas_swap_controllers.GetValue());
ui->tas_loop_script->setChecked(Settings::values.tas_loop.GetValue());
ui->tas_pause_on_load->setChecked(Settings::values.pause_tas_on_load.GetValue());
}
void ConfigureTasDialog::ApplyConfiguration() {
Common::FS::SetYuzuPath(Common::FS::YuzuPath::TASDir, ui->tas_path_edit->text().toStdString());
Settings::values.tas_enable.SetValue(ui->tas_enable->isChecked());
Settings::values.tas_swap_controllers.SetValue(ui->tas_control_swap->isChecked());
Settings::values.tas_loop.SetValue(ui->tas_loop_script->isChecked());
Settings::values.pause_tas_on_load.SetValue(ui->tas_pause_on_load->isChecked());
}
void ConfigureTasDialog::SetDirectory(DirectoryTarget target, QLineEdit* edit) {
QString caption;
switch (target) {
case DirectoryTarget::TAS:
caption = tr("Select TAS Load Directory...");
break;
}
QString str = QFileDialog::getExistingDirectory(this, caption, edit->text());
if (str.isEmpty()) {
return;
}
if (str.back() != QChar::fromLatin1('/')) {
str.append(QChar::fromLatin1('/'));
}
edit->setText(str);
}
void ConfigureTasDialog::changeEvent(QEvent* event) {
if (event->type() == QEvent::LanguageChange) {
RetranslateUI();
}
QDialog::changeEvent(event);
}
void ConfigureTasDialog::RetranslateUI() {
ui->retranslateUi(this);
}
void ConfigureTasDialog::HandleApplyButtonClicked() {
UISettings::values.configuration_applied = true;
ApplyConfiguration();
}

View File

@ -0,0 +1,38 @@
// Copyright 2021 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <QDialog>
namespace Ui {
class ConfigureTas;
}
class ConfigureTasDialog : public QDialog {
Q_OBJECT
public:
explicit ConfigureTasDialog(QWidget* parent);
~ConfigureTasDialog() override;
/// Save all button configurations to settings file
void ApplyConfiguration();
private:
enum class DirectoryTarget {
TAS,
};
void LoadConfiguration();
void SetDirectory(DirectoryTarget target, QLineEdit* edit);
void changeEvent(QEvent* event) override;
void RetranslateUI();
void HandleApplyButtonClicked();
std::unique_ptr<Ui::ConfigureTas> ui;
};

View File

@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ConfigureTas</class>
<widget class="QDialog" name="ConfigureTas">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_1">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>TAS</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="1">
<widget class="QLabel" name="label_1">
<property name="text">
<string>Reads controller input from scripts in the same format as TAS-nx scripts. For a more detailed explanation please consult the FAQ on the yuzu website.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" colspan="1">
<widget class="QLabel" name="label_2">
<property name="text">
<string>To check which hotkeys control the playback/recording, please refer to the Hotkey settings (General -> Hotkeys).</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0" colspan="1">
<widget class="QLabel" name="label_2">
<property name="text">
<string>WARNING: This is an experimental feature. It will not play back scripts frame perfectly with the current, imperfect syncing method.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="4">
<widget class="QCheckBox" name="tas_enable">
<property name="text">
<string>Enable TAS features</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="4">
<widget class="QCheckBox" name="tas_control_swap">
<property name="text">
<string>Automatic controller profile swapping</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="4">
<widget class="QCheckBox" name="tas_loop_script">
<property name="text">
<string>Loop script</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="4">
<widget class="QCheckBox" name="tas_pause_on_load">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Pause execution during loads</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Script Directory</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Path</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QToolButton" name="tas_path_button">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLineEdit" name="tas_path_edit"/>
</item>
<item row="0" column="1">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Maximum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>60</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ConfigureTas</receiver>
<slot>accept()</slot>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ConfigureTas</receiver>
<slot>reject()</slot>
</connection>
</connections>
</ui>

View File

@ -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;
}

View File

@ -6,10 +6,13 @@
#include <QLayout>
#include <QString>
#include "common/settings.h"
#include "input_common/main.h"
#include "input_common/tas/tas_input.h"
#include "yuzu/configuration/configure_input_player_widget.h"
#include "yuzu/debugger/controller.h"
ControllerDialog::ControllerDialog(QWidget* parent) : QWidget(parent, Qt::Dialog) {
ControllerDialog::ControllerDialog(QWidget* parent, InputCommon::InputSubsystem* input_subsystem_)
: QWidget(parent, Qt::Dialog), input_subsystem{input_subsystem_} {
setObjectName(QStringLiteral("Controller"));
setWindowTitle(tr("Controller P1"));
resize(500, 350);
@ -38,6 +41,9 @@ void ControllerDialog::refreshConfiguration() {
constexpr std::size_t player = 0;
widget->SetPlayerInputRaw(player, players[player].buttons, players[player].analogs);
widget->SetControllerType(players[player].controller_type);
ControllerCallback callback{[this](ControllerInput input) { InputController(input); }};
widget->SetCallBack(callback);
widget->repaint();
widget->SetConnectedStatus(players[player].connected);
}
@ -67,3 +73,13 @@ void ControllerDialog::hideEvent(QHideEvent* ev) {
widget->SetConnectedStatus(false);
QWidget::hideEvent(ev);
}
void ControllerDialog::InputController(ControllerInput input) {
u32 buttons = 0;
int index = 0;
for (bool btn : input.button_values) {
buttons |= (btn ? 1U : 0U) << index;
index++;
}
input_subsystem->GetTas()->RecordInput(buttons, input.axis_values);
}

View File

@ -4,18 +4,35 @@
#pragma once
#include <QFileSystemWatcher>
#include <QWidget>
#include "common/settings.h"
class QAction;
class QHideEvent;
class QShowEvent;
class PlayerControlPreview;
namespace InputCommon {
class InputSubsystem;
}
struct ControllerInput {
std::array<std::pair<float, float>, Settings::NativeAnalog::NUM_STICKS_HID> axis_values{};
std::array<bool, Settings::NativeButton::NumButtons> button_values{};
bool changed{};
};
struct ControllerCallback {
std::function<void(ControllerInput)> input;
};
class ControllerDialog : public QWidget {
Q_OBJECT
public:
explicit ControllerDialog(QWidget* parent = nullptr);
explicit ControllerDialog(QWidget* parent = nullptr,
InputCommon::InputSubsystem* input_subsystem_ = nullptr);
/// Returns a QAction that can be used to toggle visibility of this dialog.
QAction* toggleViewAction();
@ -26,6 +43,9 @@ protected:
void hideEvent(QHideEvent* ev) override;
private:
void InputController(ControllerInput input);
QAction* toggle_view_action = nullptr;
QFileSystemWatcher* watcher = nullptr;
PlayerControlPreview* widget;
InputCommon::InputSubsystem* input_subsystem;
};

View File

@ -19,6 +19,7 @@
#include "common/nvidia_flags.h"
#include "configuration/configure_input.h"
#include "configuration/configure_per_game.h"
#include "configuration/configure_tas.h"
#include "configuration/configure_vibration.h"
#include "core/file_sys/vfs.h"
#include "core/file_sys/vfs_real.h"
@ -102,6 +103,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "core/perf_stats.h"
#include "core/telemetry_session.h"
#include "input_common/main.h"
#include "input_common/tas/tas_input.h"
#include "util/overlay_dialog.h"
#include "video_core/gpu.h"
#include "video_core/renderer_base.h"
@ -747,6 +749,11 @@ void GMainWindow::InitializeWidgets() {
statusBar()->addPermanentWidget(label);
}
tas_label = new QLabel();
tas_label->setObjectName(QStringLiteral("TASlabel"));
tas_label->setFocusPolicy(Qt::NoFocus);
statusBar()->insertPermanentWidget(0, tas_label);
// Setup Dock button
dock_status_button = new QPushButton();
dock_status_button->setObjectName(QStringLiteral("TogglableStatusBarButton"));
@ -841,7 +848,7 @@ void GMainWindow::InitializeDebugWidgets() {
waitTreeWidget->hide();
debug_menu->addAction(waitTreeWidget->toggleViewAction());
controller_dialog = new ControllerDialog(this);
controller_dialog = new ControllerDialog(this, input_subsystem.get());
controller_dialog->hide();
debug_menu->addAction(controller_dialog->toggleViewAction());
@ -1014,6 +1021,28 @@ void GMainWindow::InitializeHotkeys() {
render_window->setAttribute(Qt::WA_Hover, true);
}
});
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Start/Stop"), this),
&QShortcut::activated, this, [&] {
if (!emulation_running) {
return;
}
input_subsystem->GetTas()->StartStop();
});
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Reset"), this),
&QShortcut::activated, this, [&] { input_subsystem->GetTas()->Reset(); });
connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Record"), this),
&QShortcut::activated, this, [&] {
if (!emulation_running) {
return;
}
bool is_recording = input_subsystem->GetTas()->Record();
if (!is_recording) {
const auto res = QMessageBox::question(this, tr("TAS Recording"),
tr("Overwrite file of player 1?"),
QMessageBox::Yes | QMessageBox::No);
input_subsystem->GetTas()->SaveRecording(res == QMessageBox::Yes);
}
});
}
void GMainWindow::SetDefaultUIGeometry() {
@ -1132,6 +1161,7 @@ void GMainWindow::ConnectMenuEvents() {
connect(ui.action_Open_FAQ, &QAction::triggered, this, &GMainWindow::OnOpenFAQ);
connect(ui.action_Restart, &QAction::triggered, this, [this] { BootGame(QString(game_path)); });
connect(ui.action_Configure, &QAction::triggered, this, &GMainWindow::OnConfigure);
connect(ui.action_Configure_Tas, &QAction::triggered, this, &GMainWindow::OnConfigureTas);
connect(ui.action_Configure_Current_Game, &QAction::triggered, this,
&GMainWindow::OnConfigurePerGame);
@ -1464,6 +1494,8 @@ void GMainWindow::ShutdownGame() {
game_list->show();
}
game_list->SetFilterFocus();
tas_label->clear();
input_subsystem->GetTas()->Stop();
render_window->removeEventFilter(render_window);
render_window->setAttribute(Qt::WA_Hover, false);
@ -2698,6 +2730,19 @@ void GMainWindow::OnConfigure() {
UpdateStatusButtons();
}
void GMainWindow::OnConfigureTas() {
const auto& system = Core::System::GetInstance();
ConfigureTasDialog dialog(this);
const auto result = dialog.exec();
if (result != QDialog::Accepted && !UISettings::values.configuration_applied) {
Settings::RestoreGlobalState(system.IsPoweredOn());
return;
} else if (result == QDialog::Accepted) {
dialog.ApplyConfiguration();
}
}
void GMainWindow::OnConfigurePerGame() {
const u64 title_id = Core::System::GetInstance().CurrentProcess()->GetTitleID();
OpenPerGameConfiguration(title_id, game_path.toStdString());
@ -2874,12 +2919,32 @@ void GMainWindow::UpdateWindowTitle(std::string_view title_name, std::string_vie
}
}
QString GMainWindow::GetTasStateDescription() const {
auto [tas_status, current_tas_frame, total_tas_frames] = input_subsystem->GetTas()->GetStatus();
switch (tas_status) {
case TasInput::TasState::Running:
return tr("TAS state: Running %1/%2").arg(current_tas_frame).arg(total_tas_frames);
case TasInput::TasState::Recording:
return tr("TAS state: Recording %1").arg(total_tas_frames);
case TasInput::TasState::Stopped:
return tr("TAS state: Idle %1/%2").arg(current_tas_frame).arg(total_tas_frames);
default:
return tr("TAS State: Invalid");
}
}
void GMainWindow::UpdateStatusBar() {
if (emu_thread == nullptr) {
status_bar_update_timer.stop();
return;
}
if (Settings::values.tas_enable) {
tas_label->setText(GetTasStateDescription());
} else {
tas_label->clear();
}
auto& system = Core::System::GetInstance();
auto results = system.GetAndResetPerfStats();
auto& shader_notify = system.GPU().ShaderNotify();

View File

@ -259,6 +259,7 @@ private slots:
void OnMenuInstallToNAND();
void OnMenuRecentFile();
void OnConfigure();
void OnConfigureTas();
void OnConfigurePerGame();
void OnLoadAmiibo();
void OnOpenYuzuFolder();
@ -300,6 +301,7 @@ private:
void OpenURL(const QUrl& url);
void LoadTranslation();
void OpenPerGameConfiguration(u64 title_id, const std::string& file_name);
QString GetTasStateDescription() const;
Ui::MainWindow ui;
@ -318,6 +320,7 @@ private:
QLabel* emu_speed_label = nullptr;
QLabel* game_fps_label = nullptr;
QLabel* emu_frametime_label = nullptr;
QLabel* tas_label = nullptr;
QPushButton* gpu_accuracy_button = nullptr;
QPushButton* renderer_status_button = nullptr;
QPushButton* dock_status_button = nullptr;

View File

@ -100,6 +100,7 @@
<addaction name="action_Rederive"/>
<addaction name="separator"/>
<addaction name="action_Capture_Screenshot"/>
<addaction name="action_Configure_Tas"/>
</widget>
<widget class="QMenu" name="menu_Help">
<property name="title">
@ -294,6 +295,11 @@
<string>&amp;Capture Screenshot</string>
</property>
</action>
<action name="action_Configure_Tas">
<property name="text">
<string>Configure &amp;TAS...</string>
</property>
</action>
<action name="action_Configure_Current_Game">
<property name="enabled">
<bool>false</bool>