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.
This commit is contained in:
MonsterDruide1 2021-06-18 16:32:46 +02:00
parent 3a7b37238b
commit 4297d2fea2
9 changed files with 140 additions and 107 deletions

View file

@ -19,6 +19,29 @@
namespace TasInput {
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() {
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<std::pair<float, float>, 2> axes) {
lastInput = {buttons, flipY(axes[0]), flipY(axes[1])};
}
std::pair<float, float> Tas::flipY(std::pair<float, float> old) const {
static std::pair<float, float> FlipY(std::pair<float, float> old) {
auto [x, y] = old;
return {x, -y};
}
std::string Tas::GetStatusDescription() {
void Tas::RecordInput(u32 buttons, const std::array<std::pair<float, float>, 2>& axes) {
last_input = {buttons, FlipY(axes[0]), FlipY(axes[1])};
}
std::tuple<TasState, size_t, size_t> 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<TasData, PLAYER_NUMBER> arr) {
static std::string DebugInputs(const std::array<TasData, PLAYER_NUMBER>& arr) {
std::string returns = "[ ";
for (size_t i = 0; i < arr.size(); i++) {
returns += debugInput(arr[i]);
if (i != arr.size() - 1)
returns += 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<std::string> 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<TasButton>(1 << index)) {
if (line.size() > 0)
if (line.size() > 0) {
line += ";";
}
line += text;
break;
}