input_common/tas: Base playback & recording system

The base playback system supports up to 8 controllers (specified by `PLAYER_NUMBER` in `tas_input.h`), which all change their inputs simulataneously when `TAS::UpdateThread` is called.

The recording system uses the controller debugger to read the state of the first controller and forwards that data to the TASing system for recording. Currently, this process sadly is not frame-perfect and pixel-accurate.

Co-authored-by: Naii-the-Baf <sfabian200@gmail.com>
Co-authored-by: Narr-the-Reg <juangerman-13@hotmail.com>
This commit is contained in:
MonsterDruide1
2021-06-18 16:15:42 +02:00
parent 35f46fc079
commit b42c3ce21d
14 changed files with 818 additions and 9 deletions

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

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

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

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

View File

@ -4,18 +4,36 @@
#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;
std::function<void()> 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;
};

View File

@ -193,6 +193,7 @@ GMainWindow::GMainWindow()
config{std::make_unique<Config>()}, vfs{std::make_shared<FileSys::RealVfsFilesystem>()},
provider{std::make_unique<FileSys::ManualContentProvider>()} {
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());