Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 117 additions & 63 deletions docs/p2tas.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Cheats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
#include "Features/Routing/EntityInspector.hpp"
#include "Features/Speedrun/SpeedrunTimer.hpp"
#include "Features/Tas/TasParser.hpp"
#include "Features/Tas/TasTools/AutoJumpTool.hpp"
#include "Features/Tas/TasTools/JumpTool.hpp"
#include "Features/Tas/TasTools/StrafeTool.hpp"
#include "Features/Timer/Timer.hpp"
#include "Features/WorkshopList.hpp"
Expand Down
7 changes: 3 additions & 4 deletions src/Features/Tas/TasController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,8 @@ void TasController::SetButtonState(TasControllerInput i, bool state) {
std::chrono::time_point<std::chrono::high_resolution_clock> g_lastControllerMove;

void TasController::ControllerMove(int nSlot, float flFrametime, CUserCmd *cmd) {
// ControllerMove is executed several times for one tick, idk why,
// but only once with tick_count bigger than 0. Working only
// on these seems to work fine, so I assume these are correct.
// ControllerMove is executed several times for one tick. Most of them
// are called from ExtraMouseSamples with 0 tick count. We want to filter them out.
if (cmd->tick_count == 0) return;

// doing some debugs to test the behaviour of the real controller
Expand Down Expand Up @@ -141,7 +140,7 @@ void TasController::ControllerMove(int nSlot, float flFrametime, CUserCmd *cmd)

//console->Print("TasController::ControllerMove (%d, ", cmd->tick_count);

tasPlayer->FetchInputs(nSlot, this);
tasPlayer->FetchInputs(nSlot, this, cmd);

//TAS is now controlling inputs. Reset everything we can.
cmd->forwardmove = 0;
Expand Down
8 changes: 8 additions & 0 deletions src/Features/Tas/TasParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,14 @@ float TasParser::toFloat(std::string str) {
return x;
}

bool TasParser::hasSuffix(const std::string &str, const std::string &suffix) {
return str.size() > suffix.length() && str.substr(str.size() - suffix.length()) == suffix;
}

float TasParser::toFloatAssumeSuffix(std::string str, const std::string &suffix) {
return TasParser::toFloat(str.substr(0, str.size() - suffix.size()));
}

void TasParser::SaveRawScriptToFile(TasScript script) {
std::string fixedName = script.path;
size_t lastdot = fixedName.find_last_of(".");
Expand Down
20 changes: 19 additions & 1 deletion src/Features/Tas/TasParser.hpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#pragma once
#include "TasScript.hpp"

#define MAX_SCRIPT_VERSION 8
#define MAX_SCRIPT_VERSION 9

#include <iostream>
#include <string>
Expand All @@ -18,11 +18,29 @@ struct TasParserException : public std::exception {
const char *what() const throw() { return msg.c_str(); }
};

struct TasParserArgumentCountException : public TasParserException {
TasParserArgumentCountException(TasTool* tool, int count)
: TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", tool->GetName(), count)) {
}
};

struct TasParserArgumentException : public TasParserException {
TasParserArgumentException(TasTool* tool, std::string paramName, std::string arg)
: TasParserException(Utils::ssprintf("Wrong %s argument for tool %s: %s", paramName.c_str(), tool->GetName(), arg.c_str())) {
}

TasParserArgumentException(TasTool *tool, std::string arg)
: TasParserException(Utils::ssprintf("Wrong argument for tool %s: %s", tool->GetName(), arg.c_str())) {
}
};

namespace TasParser {
TasScript ParseFile(TasScript &script, std::string filePath);
TasScript ParseScript(TasScript &script, std::string scriptName, std::string scriptString);
void SaveRawScriptToFile(TasScript script);
std::string SaveRawScriptToString(TasScript script);
int toInt(std::string &str);
float toFloat(std::string str);
bool hasSuffix(const std::string &str, const std::string &suffix);
float toFloatAssumeSuffix(std::string str, const std::string &suffix);
};
201 changes: 142 additions & 59 deletions src/Features/Tas/TasPlayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

#include <climits>
#include <fstream>
#include "TasTools/AutoJumpTool.hpp"
#include "TasTools/JumpTool.hpp"

Variable sar_tas_debug("sar_tas_debug", "0", 0, 2, "Debug TAS information. 0 - none, 1 - basic, 2 - all.\n");
Variable sar_tas_dump_usercmd("sar_tas_dump_usercmd", "0", "Dump TAS-generated usercmds to a file.\n");
Expand Down Expand Up @@ -136,6 +136,19 @@ TasPlayer::~TasPlayer() {
//framebulkQueue[1].clear();
}

bool TasPlayer::IsUsingTools() const {
if (!sar_tas_tools_enabled.GetBool()) {
return false;
}

if (sar_tas_tools_force.GetBool()) {
return true;
}

return (playbackInfo.slots[0].IsActive() && !playbackInfo.slots[0].IsRaw())
|| (playbackInfo.slots[1].IsActive() && !playbackInfo.slots[1].IsRaw());
}

void TasPlayer::Activate(TasPlaybackInfo info) {

if (!info.HasActiveSlot()) return;
Expand All @@ -154,8 +167,8 @@ void TasPlayer::Activate(TasPlaybackInfo info) {
for (int slot = 0; slot < 2; ++slot) {
playbackInfo.slots[slot].ClearGeneratedContent();

currentInputFramebulkIndex[slot] = 0;
currentToolsFramebulkIndex[slot] = 0;
currentRequestRawFramebulkIndex[slot] = 0;

}

active = true;
Expand Down Expand Up @@ -350,6 +363,29 @@ TasFramebulk TasPlayer::GetRawFramebulkAt(int slot, int tick, unsigned& cachedIn
return playbackInfo.slots[slot].framebulks[cachedIndex];
}

TasFramebulk &TasPlayer::RequestProcessedFramebulkAt(int slot, int tick) {
auto &processed = playbackInfo.slots[slot].processedFramebulks;
auto processedCount = processed.size();

if (processedCount == 0 || processed.back().tick < tick) {
TasFramebulk fb = GetRawFramebulkAt(slot, tick, currentRequestRawFramebulkIndex[slot]);
processed.push_back(fb);
return processed.back();
}

// if it already exists, it should be near the end, as we usually request the newest ones
for (int index = processedCount - 1; index >= 0; --index) {
if (processed[index].tick == tick) {
return processed[index];
} else if (processed[index].tick < tick) {
break;
}
}

console->Warning("TAS processed framebulk for tick %d not found! This should not happen!\n", tick);
return processed.back();
}

TasPlayerInfo TasPlayer::GetPlayerInfo(int slot, void *player, CUserCmd *cmd, bool clientside) {
TasPlayerInfo pi;

Expand Down Expand Up @@ -427,7 +463,7 @@ TasPlayerInfo TasPlayer::GetPlayerInfo(int slot, void *player, CUserCmd *cmd, bo

// predict the result of autojump tool so other tools can react appropriately.
FOR_TAS_SCRIPT_VERSIONS_SINCE(8) {
if (autoJumpTool[slot].GetCurrentParams().enabled && autoJumpTool[slot].ShouldJump(pi)) {
if (autoJumpTool[slot].WillJump(pi) || jumpTool[slot].WillJump(pi)) {
pi.willBeGrounded = false;
}
}
Expand Down Expand Up @@ -540,7 +576,7 @@ void TasPlayer::SaveProcessedFramebulks() {
we assume the response time for our "virtual controller" to be
non-existing and just let it parse inputs corresponding to given tick.
*/
void TasPlayer::FetchInputs(int slot, TasController *controller) {
void TasPlayer::FetchInputs(int slot, TasController *controller, CUserCmd* cmd) {
// Slight hack! Input fetching (including SteamControllerMove) is
// called through _Host_RunFrame_Input, which is called *before*
// GameFrame (that being called via _Host_RunFrame_Server). Therefore,
Expand All @@ -549,36 +585,23 @@ void TasPlayer::FetchInputs(int slot, TasController *controller) {
// said than done since the input fetching code is only run when the
// client is connected, so to match the behaviour we'd probably need
// to actually hook at _Host_RunFrame_Input or CL_Move.
int tick = currentTick + 1;
int tasTick = currentTick + 1;

TasFramebulk fb = GetRawFramebulkAt(slot, tick, currentInputFramebulkIndex[slot]);
auto player = server->GetPlayer(slot + 1);

int fbTick = fb.tick;

if (sar_tas_debug.GetInt() > 0 && fbTick == tick) {
console->Print("%s\n", fb.ToString().c_str());
if (tasTick == 1) {
SamplePreProcessedFramebulk(slot, 0, player, cmd);
}

TasFramebulk fb = SamplePreProcessedFramebulk(slot, tasTick, player, cmd);

controller->SetViewAnalog(fb.viewAnalog.x, fb.viewAnalog.y);
controller->SetMoveAnalog(fb.moveAnalog.x, fb.moveAnalog.y);
for (int i = 0; i < TAS_CONTROLLER_INPUT_COUNT; i++) {
controller->SetButtonState((TasControllerInput)i, fb.buttonStates[i]);
}

if (tick == 1) {
// on tick 1, we'll run the commands from the bulk at tick 0 because
// of the annoying off-by-one thing explained above
TasFramebulk fb0 = GetRawFramebulkAt(slot, 0);
for (std::string cmd : fb0.commands) {
controller->AddCommandToQueue(cmd);
}
}

// add commands only for tick when framebulk is placed. Don't preserve it to other ticks.
if (tick == fbTick) {
for (std::string cmd : fb.commands) {
controller->AddCommandToQueue(cmd);
}
for (std::string cmd : fb.commands) {
controller->AddCommandToQueue(cmd);
}
}

Expand All @@ -591,6 +614,52 @@ static bool IsTaunting(ClientEnt *player) {
return false;
}

TasFramebulk TasPlayer::SamplePreProcessedFramebulk(int slot, int tasTick, void *player, CUserCmd *cmd) {
TasFramebulk& fb = RequestProcessedFramebulkAt(slot, tasTick);

auto fbTick = fb.tick;
fb.tick = tasTick;
bool framebulkUpdated = (fbTick == tasTick);

if (sar_tas_debug.GetInt() > 0 && framebulkUpdated) {
console->Print("(TAS: rawtick) %s\n", fb.ToString().c_str());
}

if (tasTick == 0 || !framebulkUpdated) {
std::vector<std::string> emptyCommands;
fb.commands = emptyCommands;
}

if (!framebulkUpdated) {
std::vector<TasToolCommand> emptyToolCmds;
fb.toolCmds = emptyToolCmds;
}

if (tasTick == 1) {
// on tick 1, we'll run the commands from the bulk at tick 0 because
// of the annoying off-by-one thing explained in FetchInputs
TasFramebulk fb0 = GetRawFramebulkAt(slot, 0);

if (fb0.tick == 0) {
for (std::string cmd : fb0.commands) {
fb.commands.push_back(cmd);
}
}
}

if (IsUsingTools()) {
UpdateTools(slot, fb, PRE_PROCESSING);
auto pInfo = GetPlayerInfo(slot, server->GetPlayer(slot + 1), cmd);
ApplyTools(fb, pInfo, PRE_PROCESSING);

if (sar_tas_debug.GetInt() > 0) {
console->Print("(TAS: pretick) %s\n", fb.ToString().c_str());
}
}

return fb;
}

// special tools have to be parsed in input processing part.
// because of alternateticks, a pair of inputs are created and then executed at the same time,
// meaning that second tick in pair reads outdated info.
Expand All @@ -603,19 +672,9 @@ void TasPlayer::PostProcess(int slot, void *player, CUserCmd *cmd) {
// every other way of getting time is incorrect due to alternateticks
int tasTick = FetchCurrentPlayerTickBase(player) - startTick;

TasFramebulk fb = GetRawFramebulkAt(slot, tasTick, currentToolsFramebulkIndex[slot]);

// update all tools that needs to be updated
auto fbTick = fb.tick;
fb.tick = tasTick;
if (fbTick == tasTick) {
for (TasToolCommand cmd : fb.toolCmds) {
auto tool = TasTool::GetInstanceByName(slot, cmd.tool->GetName());
if (tool == nullptr) continue;
tool->SetParams(cmd.params);
}
}
TasFramebulk& fb = RequestProcessedFramebulkAt(slot, tasTick);

UpdateTools(slot, fb, POST_PROCESSING);
auto playerInfo = GetPlayerInfo(slot, player, cmd);

float orig_forward = cmd->forwardmove;
Expand All @@ -634,21 +693,7 @@ void TasPlayer::PostProcess(int slot, void *player, CUserCmd *cmd) {
return;
}

// applying tools
if (playbackInfo.slots[slot].header.version >= 3) {
// use priority list for newer versions. technically all tools should be in the list
for (std::string toolName : TasTool::priorityList) {
auto tool = TasTool::GetInstanceByName(slot, toolName);
if (tool == nullptr) continue;
tool->Apply(fb, playerInfo);
}
} else {
// use old "earliest first" ordering system (partially also present in TasTool::SetParams)
for (TasTool *tool : TasTool::GetList(slot)) {
tool->Apply(fb, playerInfo);
}
}

ApplyTools(fb, playerInfo, POST_PROCESSING);

// make sure none of the framebulk is NaN
if (std::isnan(fb.moveAnalog.x)) fb.moveAnalog.x = 0;
Expand Down Expand Up @@ -717,13 +762,9 @@ void TasPlayer::PostProcess(int slot, void *player, CUserCmd *cmd) {
SE(player)->fieldOff<CUserCmd>("m_hViewModel", 8) /* m_LastCmd */ = *cmd;
}

// put processed framebulk in the list
if (fbTick != tasTick) {
std::vector<std::string> empty;
fb.commands = empty;
if (sar_tas_debug.GetInt() > 0) {
console->Print("(TAS: posttick) %s\n", fb.ToString().c_str());
}
playbackInfo.slots[slot].processedFramebulks.push_back(fb);

tasPlayer->DumpUsercmd(slot, cmd, tasTick, "processed");
}

Expand Down Expand Up @@ -756,6 +797,48 @@ void TasPlayer::ApplyMoveAnalog(Vector moveAnalog, CUserCmd *cmd) {
}
}

void TasPlayer::UpdateTools(int slot, const TasFramebulk &fb, TasToolProcessingType processType) {
for (TasToolCommand cmd : fb.toolCmds) {
auto tool = TasTool::GetInstanceByName(slot, cmd.tool->GetName());
if (!CanProcessTool(tool, processType)) continue;
tool->SetParams(cmd.params);
}
}

void TasPlayer::ApplyTools(TasFramebulk &fb, const TasPlayerInfo &pInfo, TasToolProcessingType processType) {
int slot = pInfo.slot;

FOR_TAS_SCRIPT_VERSIONS_UNTIL(2) {
// use old "earliest first" ordering system (partially also present in TasTool::SetParams)
for (TasTool *tool : TasTool::GetList(slot)) {
if (!CanProcessTool(tool, processType)) continue;
tool->Apply(fb, pInfo);
}
return;
}

// use priority list for newer versions. technically all tools should be in the list
for (std::string toolName : TasTool::priorityList) {
auto tool = TasTool::GetInstanceByName(slot, toolName);
if (!CanProcessTool(tool, processType)) continue;
tool->Apply(fb, pInfo);
}
}

bool TasPlayer::CanProcessTool(TasTool *tool, TasToolProcessingType processType) {
if (tool == nullptr) {
return false;
}

int slot = tool->GetSlot();
FOR_TAS_SCRIPT_VERSIONS_UNTIL(8) {
// old scripts process all tools in post processing
return processType == POST_PROCESSING;
}

return tool->CanProcess(processType);
}

void TasPlayer::DumpUsercmd(int slot, const CUserCmd *cmd, int tick, const char *source) {
if (!sar_tas_dump_usercmd.GetBool()) return;
std::string str = Utils::ssprintf("%s,%d,%.6f,%.6f,%08X,%.6f,%.6f,%.6f", source, tick, cmd->forwardmove, cmd->sidemove, cmd->buttons, cmd->viewangles.x, cmd->viewangles.y, cmd->viewangles.z);
Expand Down
Loading