From 352b019f637f4ac009772eaa451d9e1a4697a714 Mon Sep 17 00:00:00 2001 From: Chris Hausky Date: Thu, 23 Oct 2025 22:32:56 +0200 Subject: [PATCH 01/15] feat: add jump tool for tas --- src/Cheats.cpp | 2 +- src/Features/Tas/TasPlayer.cpp | 4 +- src/Features/Tas/TasTools/AutoJumpTool.cpp | 55 ----------------- src/Features/Tas/TasTools/AutoJumpTool.hpp | 30 ---------- src/Features/Tas/TasTools/JumpTool.cpp | 70 ++++++++++++++++++++++ src/Features/Tas/TasTools/JumpTool.hpp | 37 ++++++++++++ src/Features/Tas/TasTools/StrafeTool.cpp | 4 +- 7 files changed, 112 insertions(+), 90 deletions(-) delete mode 100644 src/Features/Tas/TasTools/AutoJumpTool.cpp delete mode 100644 src/Features/Tas/TasTools/AutoJumpTool.hpp create mode 100644 src/Features/Tas/TasTools/JumpTool.cpp create mode 100644 src/Features/Tas/TasTools/JumpTool.hpp diff --git a/src/Cheats.cpp b/src/Cheats.cpp index 3cd1769d..885e5b9d 100644 --- a/src/Cheats.cpp +++ b/src/Cheats.cpp @@ -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" diff --git a/src/Features/Tas/TasPlayer.cpp b/src/Features/Tas/TasPlayer.cpp index e1097241..3c84941d 100644 --- a/src/Features/Tas/TasPlayer.cpp +++ b/src/Features/Tas/TasPlayer.cpp @@ -17,7 +17,7 @@ #include #include -#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"); @@ -427,7 +427,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; } } diff --git a/src/Features/Tas/TasTools/AutoJumpTool.cpp b/src/Features/Tas/TasTools/AutoJumpTool.cpp deleted file mode 100644 index 215b847e..00000000 --- a/src/Features/Tas/TasTools/AutoJumpTool.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include "AutoJumpTool.hpp" - -#include "Modules/Engine.hpp" -#include "Modules/Server.hpp" -#include "Features/Tas/TasParser.hpp" -#include "Features/Tas/TasPlayer.hpp" - -AutoJumpTool autoJumpTool[2] = {{0}, {1}}; - -void AutoJumpTool::Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo) { - if (this->updated) { - hasJumpedLastTick = false; - } - - if (params.enabled) { - if (ShouldJump(pInfo)) { - bulk.buttonStates[TasControllerInput::Jump] = true; - if (params.ducked) { - bulk.buttonStates[TasControllerInput::Crouch] = true; - } - hasJumpedLastTick = true; - } else { - bulk.buttonStates[TasControllerInput::Jump] = false; - if (params.ducked) { - bulk.buttonStates[TasControllerInput::Crouch] = false; - } - hasJumpedLastTick = false; - } - } else { - hasJumpedLastTick = false; - } -} - -bool AutoJumpTool::ShouldJump(const TasPlayerInfo &pInfo) { - return pInfo.grounded && !pInfo.ducked && !hasJumpedLastTick; -} - -std::shared_ptr AutoJumpTool::ParseParams(std::vector vp) { - if (vp.size() != 1) - throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), vp.size())); - - bool ducked = false; - bool enabled = false; - - if (vp[0] == "on" || vp[0] == "unducked" || vp[0] == "unduck") { - enabled = true; - } else if (vp[0] == "ducked" || vp[0] == "duck") { - enabled = true; - ducked = true; - } else if (vp[0] != "off") { - throw TasParserException(Utils::ssprintf("Bad parameter for tool %s: %s", this->GetName(), vp[0].c_str())); - } - - return std::make_shared(enabled, ducked); -} diff --git a/src/Features/Tas/TasTools/AutoJumpTool.hpp b/src/Features/Tas/TasTools/AutoJumpTool.hpp deleted file mode 100644 index 09ad8d41..00000000 --- a/src/Features/Tas/TasTools/AutoJumpTool.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once -#include "../TasTool.hpp" - -struct AutoJumpToolParams : public TasToolParams { - AutoJumpToolParams() - : TasToolParams() { - } - AutoJumpToolParams(bool enabled, bool ducked) - : TasToolParams(enabled) - , ducked(ducked) { - } - - bool ducked = false; -}; - -class AutoJumpTool : public TasToolWithParams { -public: - AutoJumpTool(int slot) - : TasToolWithParams("autojump", slot) { - } - - virtual std::shared_ptr ParseParams(std::vector); - virtual void Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo); - bool ShouldJump(const TasPlayerInfo &pInfo); - -private: - bool hasJumpedLastTick = false; -}; - -extern AutoJumpTool autoJumpTool[2]; diff --git a/src/Features/Tas/TasTools/JumpTool.cpp b/src/Features/Tas/TasTools/JumpTool.cpp new file mode 100644 index 00000000..0fbc35a9 --- /dev/null +++ b/src/Features/Tas/TasTools/JumpTool.cpp @@ -0,0 +1,70 @@ +#include "JumpTool.hpp" + +#include "Modules/Engine.hpp" +#include "Modules/Server.hpp" +#include "Features/Tas/TasParser.hpp" +#include "Features/Tas/TasPlayer.hpp" + +JumpTool autoJumpTool[2] = {{0, true}, {1, true}}; +JumpTool jumpTool[2] = {{0, false}, {1, false}}; + +void JumpTool::Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo) { + if (this->updated) { + hasJumpedLastTick = false; + } + + if (params.enabled) { + bool shouldJump; + + if (this->automatic) { + shouldJump = ShouldJump(pInfo); + } else { + shouldJump = true; + params.enabled = false; + } + + SetJumpInput(bulk, shouldJump); + } else { + hasJumpedLastTick = false; + } +} + +bool JumpTool::WillJump(const TasPlayerInfo &pInfo) { + return params.enabled && ShouldJump(pInfo); +} + +bool JumpTool::ShouldJump(const TasPlayerInfo &pInfo) { + return pInfo.grounded && !pInfo.ducked && !hasJumpedLastTick; +} + +void JumpTool::SetJumpInput(TasFramebulk &bulk, bool jump) { + bulk.buttonStates[TasControllerInput::Jump] = jump; + if (params.ducked) { + bulk.buttonStates[TasControllerInput::Crouch] = jump; + } + hasJumpedLastTick = jump; +} + +std::shared_ptr JumpTool::ParseParams(std::vector vp) { + if (vp.size() == 0 && !this->automatic) { + // allow 0 parameters for non-automated jump tool + return std::make_shared(true, false); + } + + if (vp.size() != 1) + throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), vp.size())); + + bool ducked = false; + bool enabled = false; + + if (vp[0] == "on" || vp[0] == "unducked" || vp[0] == "unduck") { + enabled = true; + } else if (vp[0] == "ducked" || vp[0] == "duck") { + enabled = true; + ducked = true; + } else if (vp[0] != "off") { + throw TasParserException(Utils::ssprintf("Bad parameter for tool %s: %s", this->GetName(), vp[0].c_str())); + } + + return std::make_shared(enabled, ducked); +} diff --git a/src/Features/Tas/TasTools/JumpTool.hpp b/src/Features/Tas/TasTools/JumpTool.hpp new file mode 100644 index 00000000..37830a7d --- /dev/null +++ b/src/Features/Tas/TasTools/JumpTool.hpp @@ -0,0 +1,37 @@ +#pragma once +#include "../TasTool.hpp" + +struct JumpToolsParams : public TasToolParams { + JumpToolsParams() + : TasToolParams() { + } + JumpToolsParams(bool enabled, bool ducked) + : TasToolParams(enabled) + , ducked(ducked) { + } + + bool ducked = false; +}; + +class JumpTool : public TasToolWithParams { +public: + JumpTool(int slot, bool automatic) + : TasToolWithParams(automatic ? "autojump" : "jump", slot) + , automatic(automatic) { + } + + virtual std::shared_ptr ParseParams(std::vector); + virtual void Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo); + + bool WillJump(const TasPlayerInfo &pInfo); +private: + bool ShouldJump(const TasPlayerInfo &pInfo); + void SetJumpInput(TasFramebulk &bulk, bool jump); + +private: + bool automatic; + bool hasJumpedLastTick = false; +}; + +extern JumpTool autoJumpTool[2]; +extern JumpTool jumpTool[2]; diff --git a/src/Features/Tas/TasTools/StrafeTool.cpp b/src/Features/Tas/TasTools/StrafeTool.cpp index c03f2004..2f80108b 100644 --- a/src/Features/Tas/TasTools/StrafeTool.cpp +++ b/src/Features/Tas/TasTools/StrafeTool.cpp @@ -1,7 +1,7 @@ #include "StrafeTool.hpp" #include "../TasParser.hpp" -#include "AutoJumpTool.hpp" +#include "JumpTool.hpp" #include "Modules/Client.hpp" #include "Modules/Console.hpp" #include "Modules/Engine.hpp" @@ -23,7 +23,7 @@ void AutoStrafeTool::Apply(TasFramebulk &fb, const TasPlayerInfo &pInfo) { FOR_TAS_SCRIPT_VERSIONS_UNTIL(7) { // handled by TasPlayer in newer versions - if (autoJumpTool[this->slot].GetCurrentParams().enabled) { + if (autoJumpTool[this->slot].GetCurrentParams().enabled || jumpTool[this->slot].GetCurrentParams().enabled) { // if autojump is enabled, we're never grounded. fakePlayerInfo.willBeGrounded = false; } From 47442a57621feeef32489c8604b79dff96b8d845 Mon Sep 17 00:00:00 2001 From: Chris Hausky Date: Thu, 30 Oct 2025 12:52:11 +0100 Subject: [PATCH 02/15] feat: add additional parameters to strafe tool --- src/Features/Tas/TasTools/StrafeTool.cpp | 56 +++++++++++++++++++----- src/Features/Tas/TasTools/StrafeTool.hpp | 8 +++- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/Features/Tas/TasTools/StrafeTool.cpp b/src/Features/Tas/TasTools/StrafeTool.cpp index 2f80108b..119d981a 100644 --- a/src/Features/Tas/TasTools/StrafeTool.cpp +++ b/src/Features/Tas/TasTools/StrafeTool.cpp @@ -57,11 +57,11 @@ void AutoStrafeTool::Apply(TasFramebulk &fb, const TasPlayerInfo &pInfo) { this->updated = false; } - bool shouldStrafe = true; + bool reachedTargetValues = false; FOR_TAS_SCRIPT_VERSIONS_SINCE(8) { - shouldStrafe = !TryReachTargetValues(fb, fakePlayerInfo); + reachedTargetValues = TryReachTargetValues(fb, fakePlayerInfo); } - if (shouldStrafe) { + if (!reachedTargetValues) { ApplyStrafe(fb, fakePlayerInfo); } @@ -133,8 +133,8 @@ bool AutoStrafeTool::TryReachTargetValues(TasFramebulk &bulk, const TasPlayerInf float playerYawRad = DEG2RAD(pInfo.angles.y); float wishDirAngleRad = absoluteWishDirAngleRad - playerYawRad; - float forwardMove = cosf(wishDirAngleRad); - float sideMove = -sinf(wishDirAngleRad); + float forwardMove = cosf(wishDirAngleRad) * params.force; + float sideMove = -sinf(wishDirAngleRad) * params.force; Vector velocityAfterMove = GetVelocityAfterMove(pInfo, forwardMove, sideMove); Vector afterMoveDelta = velocityAfterMove - velocity; @@ -172,7 +172,7 @@ void AutoStrafeTool::ApplyStrafe(TasFramebulk &fb, const TasPlayerInfo &pInfo) { } } - float angle = velAngle + RAD2DEG(this->GetStrafeAngle(pInfo, params)); + float angle = velAngle + RAD2DEG(this->GetStrafeAngle(pInfo, params)) * params.turnRate; FOR_TAS_SCRIPT_VERSIONS_SINCE(8) { // Deal with airlock: we're strafing, so always try to maximize how much @@ -225,6 +225,8 @@ void AutoStrafeTool::ApplyStrafe(TasFramebulk &fb, const TasPlayerInfo &pInfo) { QAngle newAngle = {0, angle + lookAngle, 0}; fb.viewAnalog.x -= newAngle.y - pInfo.angles.y; } + + fb.moveAnalog *= params.force; } // returns player's velocity after its been affected by ground friction @@ -366,7 +368,7 @@ float AutoStrafeTool::GetFastestStrafeAngle(const TasPlayerInfo &player) { if (velocity.Length2D() == 0) return 0; - Vector wishDir(0, 1); + Vector wishDir = Vector(0, 1) * params.force; float maxSpeed = GetMaxSpeed(player, wishDir); float maxAccel = GetMaxAccel(player, wishDir); @@ -385,7 +387,8 @@ float AutoStrafeTool::GetTargetStrafeAngle(const TasPlayerInfo &player, float ta float currentSpeed = vel.Length2D(); if (currentSpeed == 0) return 0; - float maxAccel = GetMaxAccel(player, {0, 1}); + Vector wishDir = Vector(0, 1) * params.force; + float maxAccel = GetMaxAccel(player, wishDir); // Assuming that it is possible to achieve a velocity of a given length, // I'm using a law of cosines to get the right angle for acceleration. @@ -425,7 +428,7 @@ float AutoStrafeTool::GetTurningStrafeAngle(const TasPlayerInfo &player) { if (velocity.Length2D() == 0) return 0; - Vector wishDir(0, 1); + Vector wishDir = Vector(0, 1) * params.force; float maxAccel = GetMaxAccel(player, wishDir); // In order to maximize the angle between old and new velocity, the angle between @@ -480,6 +483,9 @@ float AutoStrafeTool::GetStrafeAngle(const TasPlayerInfo &player, AutoStrafePara sidemove = sinOld(ang); } + forwardmove *= params.force; + sidemove *= params.force; + float predictedVel = GetVelocityAfterMove(player, forwardmove, sidemove).Length2D(); if ((speedDiff > 0 && predictedVel > params.strafeSpeed.speed) || (speedDiff < 0 && predictedVel < params.strafeSpeed.speed)) { passedTargetSpeed = true; @@ -522,7 +528,7 @@ int AutoStrafeTool::GetTurningDirection(const TasPlayerInfo &pInfo, float desAng // using the math from max angle change strafing to determine whether // line following is too "wobbly" Vector velocity = GetGroundFrictionVelocity(pInfo); - float maxAccel = GetMaxAccel(pInfo, Vector(0, 1)); + float maxAccel = GetMaxAccel(pInfo, Vector(0, 1) * params.force); float maxRotAng = RAD2DEG(asinf(maxAccel / velocity.Length2D())); // scale maxRotAng by surfaceFriction and make it slightly bigger, as the range @@ -585,7 +591,7 @@ int AutoStrafeTool::GetTurningDirection(const TasPlayerInfo &pInfo, float desAng } if (shouldPreventSpeedLock) { - Vector wishDir(0, 1); + Vector wishDir = Vector(0, 1) * params.force; float maxSpeed = GetMaxSpeed(pInfo, wishDir); float maxAccel = GetMaxAccel(pInfo, wishDir); @@ -632,6 +638,8 @@ std::shared_ptr AutoStrafeTool::ParseParams(std::vector(); @@ -658,6 +666,9 @@ std::shared_ptr AutoStrafeTool::ParseParams(std::vector 3 && param.substr(param.size() - 3, 3) == "ups") { speed.type = SPECIFIED; speed.speed = TasParser::toFloat(param.substr(0, param.size() - 3)); + } else if (param == "min") { + speed.type = SPECIFIED; + speed.speed = 0.0f; } //dir (using large numbers for left and right because angle is clamped to range -180 and 180) @@ -686,10 +697,31 @@ std::shared_ptr AutoStrafeTool::ParseParams(std::vectorGetName(), key.c_str())); + } + } + //unknown parameter... else throw TasParserException(Utils::ssprintf("Bad parameter for tool %s: %s", this->GetName(), param.c_str())); } - return std::make_shared(type, dir, speed, noPitchLock, antiSpeedLock); + return std::make_shared(type, dir, speed, noPitchLock, antiSpeedLock, turnRate, force); } diff --git a/src/Features/Tas/TasTools/StrafeTool.hpp b/src/Features/Tas/TasTools/StrafeTool.hpp index 5ade77bd..11c58a1d 100644 --- a/src/Features/Tas/TasTools/StrafeTool.hpp +++ b/src/Features/Tas/TasTools/StrafeTool.hpp @@ -39,17 +39,21 @@ struct AutoStrafeParams : public TasToolParams { AutoStrafeSpeed strafeSpeed = {CURRENT}; bool noPitchLock = false; bool antiSpeedLock = true; + float turnRate = 1.0f; + float force = 1.0f; AutoStrafeParams() : TasToolParams() {} - AutoStrafeParams(AutoStrafeType type, AutoStrafeDirection dir, AutoStrafeSpeed speed, bool noPitchLock, bool antiSpeedLock) + AutoStrafeParams(AutoStrafeType type, AutoStrafeDirection dir, AutoStrafeSpeed speed, bool noPitchLock, bool antiSpeedLock, float turnRate, float force) : TasToolParams(true) , strafeType(type) , strafeDir(dir) , strafeSpeed(speed) , noPitchLock(noPitchLock) - , antiSpeedLock(antiSpeedLock) { + , antiSpeedLock(antiSpeedLock) + , turnRate(turnRate) + , force(force) { } }; From 4158b551f2bf7c882a99716071acc5fc0dee6a33 Mon Sep 17 00:00:00 2001 From: Chris Hausky Date: Fri, 31 Oct 2025 14:52:29 +0100 Subject: [PATCH 03/15] feat: allow pre-processing of some tools this resolves issues where some basic tools (like use or cmd) would not be processed correctly due to being executed late into user cmd processing (which is required for other tools because of altticks handling) --- src/Features/Tas/TasController.cpp | 7 +- src/Features/Tas/TasParser.hpp | 2 +- src/Features/Tas/TasPlayer.cpp | 180 ++++++++++++------ src/Features/Tas/TasPlayer.hpp | 31 +-- src/Features/Tas/TasTool.cpp | 11 +- src/Features/Tas/TasTool.hpp | 21 +- .../Tas/TasTools/AbsoluteMoveTool.cpp | 5 +- .../Tas/TasTools/AbsoluteMoveTool.hpp | 3 +- src/Features/Tas/TasTools/AutoAimTool.hpp | 2 +- src/Features/Tas/TasTools/CheckTool.hpp | 2 +- src/Features/Tas/TasTools/CommandTool.cpp | 9 +- src/Features/Tas/TasTools/CommandTool.hpp | 2 +- src/Features/Tas/TasTools/DecelTool.hpp | 2 +- src/Features/Tas/TasTools/DuckTool.hpp | 2 +- src/Features/Tas/TasTools/JumpTool.hpp | 2 +- src/Features/Tas/TasTools/LookTool.cpp | 5 +- src/Features/Tas/TasTools/LookTool.hpp | 4 +- src/Features/Tas/TasTools/MoveTool.cpp | 5 +- src/Features/Tas/TasTools/MoveTool.hpp | 4 +- src/Features/Tas/TasTools/SetAngleTool.hpp | 2 +- src/Features/Tas/TasTools/ShootTool.hpp | 3 +- src/Features/Tas/TasTools/StopTool.hpp | 2 +- src/Features/Tas/TasTools/StrafeTool.hpp | 2 +- src/Features/Tas/TasTools/UseTool.hpp | 3 +- src/Features/Tas/TasTools/ZoomTool.hpp | 2 +- 25 files changed, 194 insertions(+), 119 deletions(-) diff --git a/src/Features/Tas/TasController.cpp b/src/Features/Tas/TasController.cpp index 3c407fac..d8e6cfb2 100644 --- a/src/Features/Tas/TasController.cpp +++ b/src/Features/Tas/TasController.cpp @@ -110,9 +110,8 @@ void TasController::SetButtonState(TasControllerInput i, bool state) { std::chrono::time_point 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 @@ -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; diff --git a/src/Features/Tas/TasParser.hpp b/src/Features/Tas/TasParser.hpp index 43c7c379..3e1a1ac7 100644 --- a/src/Features/Tas/TasParser.hpp +++ b/src/Features/Tas/TasParser.hpp @@ -1,7 +1,7 @@ #pragma once #include "TasScript.hpp" -#define MAX_SCRIPT_VERSION 8 +#define MAX_SCRIPT_VERSION 9 #include #include diff --git a/src/Features/Tas/TasPlayer.cpp b/src/Features/Tas/TasPlayer.cpp index 3c84941d..23a6bd0c 100644 --- a/src/Features/Tas/TasPlayer.cpp +++ b/src/Features/Tas/TasPlayer.cpp @@ -154,8 +154,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; @@ -350,6 +350,28 @@ 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); +} + TasPlayerInfo TasPlayer::GetPlayerInfo(int slot, void *player, CUserCmd *cmd, bool clientside) { TasPlayerInfo pi; @@ -540,7 +562,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, @@ -549,36 +571,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; - - TasFramebulk fb = GetRawFramebulkAt(slot, tick, currentInputFramebulkIndex[slot]); + int tasTick = currentTick + 1; - int fbTick = fb.tick; + auto player = server->GetPlayer(slot + 1); - 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); } } @@ -591,6 +600,52 @@ static bool IsTaunting(ClientEnt *player) { return false; } +TasFramebulk TasPlayer::SamplePreProcessedFramebulk(int slot, int tasTick, void *player, CUserCmd *cmd) { + + auto pInfo = GetPlayerInfo(slot, server->GetPlayer(slot + 1), 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 emptyCommands; + fb.commands = emptyCommands; + } + + if (!framebulkUpdated) { + std::vector 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()) { + 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. @@ -603,18 +658,7 @@ 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); auto playerInfo = GetPlayerInfo(slot, player, cmd); @@ -634,21 +678,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; @@ -717,13 +747,9 @@ void TasPlayer::PostProcess(int slot, void *player, CUserCmd *cmd) { SE(player)->fieldOff("m_hViewModel", 8) /* m_LastCmd */ = *cmd; } - // put processed framebulk in the list - if (fbTick != tasTick) { - std::vector 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"); } @@ -756,6 +782,46 @@ void TasPlayer::ApplyMoveAnalog(Vector moveAnalog, CUserCmd *cmd) { } } +void TasPlayer::ApplyTools(TasFramebulk &fb, const TasPlayerInfo &pInfo, TasToolProcessingType processType) { + int slot = pInfo.slot; + + for (TasToolCommand cmd : fb.toolCmds) { + auto tool = TasTool::GetInstanceByName(slot, cmd.tool->GetName()); + if (!CanProcessTool(tool, processType)) continue; + tool->SetParams(cmd.params); + } + + 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); diff --git a/src/Features/Tas/TasPlayer.hpp b/src/Features/Tas/TasPlayer.hpp index f9536829..9644d414 100644 --- a/src/Features/Tas/TasPlayer.hpp +++ b/src/Features/Tas/TasPlayer.hpp @@ -1,15 +1,15 @@ #pragma once -#include "TasScript.hpp" #include "Command.hpp" #include "Features/Feature.hpp" #include "Features/Tas/TasController.hpp" #include "Features/Tas/TasTool.hpp" -#include "Utils/SDK.hpp" -#include "Variable.hpp" -#include "Modules/Engine.hpp" #include "Modules/Client.hpp" +#include "Modules/Engine.hpp" #include "Modules/Server.hpp" +#include "TasScript.hpp" +#include "Utils/SDK.hpp" +#include "Variable.hpp" #define TAS_SCRIPTS_DIR "tas" #define TAS_SCRIPT_EXT "p2tas" @@ -38,7 +38,7 @@ struct TasPlaybackInfo { if (coopControlSlot >= 0 && slots[1 - coopControlSlot].IsActive()) { return slots[1 - coopControlSlot]; } - return slots[0].IsActive() ? slots[0] : slots[1]; + return slots[0].IsActive() ? slots[0] : slots[1]; } inline TasScriptHeader GetMainHeader() const { return GetMainScript().header; } }; @@ -72,11 +72,11 @@ class TasPlayer : public Feature { int currentTick = 0; // tick position of script player, relative to its starting point. int lastTick = 0; // last tick of script, relative to its starting point - int wasEnginePaused = false; // Used to check if we need to revert incrementing a tick + int wasEnginePaused = false; // Used to check if we need to revert incrementing a tick // used to cache last used framebulk to quickly access it for playback - unsigned currentInputFramebulkIndex[2]; - unsigned currentToolsFramebulkIndex[2]; + unsigned currentRequestRawFramebulkIndex[2]; + public: void Update(); void UpdateServer(); @@ -89,9 +89,8 @@ class TasPlayer : public Feature { inline bool IsReady() const { return ready; }; inline bool IsRunning() const { return active && startTick != -1; } inline bool IsPaused() const { return paused; } - inline bool IsUsingTools() const { - return (playbackInfo.slots[0].IsActive() && !playbackInfo.slots[0].IsRaw()) - || (playbackInfo.slots[1].IsActive() && !playbackInfo.slots[1].IsRaw()); + inline bool IsUsingTools() const { + return (playbackInfo.slots[0].IsActive() && !playbackInfo.slots[0].IsRaw()) || (playbackInfo.slots[1].IsActive() && !playbackInfo.slots[1].IsRaw()); } inline int GetScriptVersion(int slot) const { return playbackInfo.slots[slot].header.version; } @@ -102,8 +101,8 @@ class TasPlayer : public Feature { void Activate(TasPlaybackInfo info); void Start(); void PostStart(); - void Stop(bool interrupted=false); - void Replay(bool automatic=false); + void Stop(bool interrupted = false); + void Replay(bool automatic = false); void Pause(); void Resume(); @@ -111,6 +110,7 @@ class TasPlayer : public Feature { TasFramebulk GetRawFramebulkAt(int slot, int tick); TasFramebulk GetRawFramebulkAt(int slot, int tick, unsigned &cachedIndex); + TasFramebulk &RequestProcessedFramebulkAt(int slot, int tick); TasPlayerInfo GetPlayerInfo(int slot, void *player, CUserCmd *cmd, bool clientside = false); int FetchCurrentPlayerTickBase(void *player, bool clientside = false); @@ -119,9 +119,12 @@ class TasPlayer : public Feature { void SaveUsercmdDebugs(int slot); void SavePlayerInfoDebugs(int slot); - void FetchInputs(int slot, TasController *controller); + void FetchInputs(int slot, TasController *controller, CUserCmd *cmd); + TasFramebulk SamplePreProcessedFramebulk(int slot, int tasTick, void *player, CUserCmd *cmd); void PostProcess(int slot, void *player, CUserCmd *cmd); void ApplyMoveAnalog(Vector moveAnalog, CUserCmd *cmd); + void ApplyTools(TasFramebulk &fb, const TasPlayerInfo &pInfo, TasToolProcessingType processType); + bool CanProcessTool(TasTool *tool, TasToolProcessingType processType); void DumpUsercmd(int slot, const CUserCmd *cmd, int tick, const char *source); void DumpPlayerInfo(int slot, int tick, Vector pos, Vector eye_pos, QAngle ang); diff --git a/src/Features/Tas/TasTool.cpp b/src/Features/Tas/TasTool.cpp index 59e0698e..9925f891 100644 --- a/src/Features/Tas/TasTool.cpp +++ b/src/Features/Tas/TasTool.cpp @@ -1,6 +1,7 @@ #include "TasTool.hpp" #include "TasPlayer.hpp" + #include std::list &TasTool::GetList(int slot) { @@ -26,6 +27,7 @@ std::vector TasTool::priorityList = { "setang", "autoaim", "look", + "jump", "autojump", "absmov", "move", @@ -33,11 +35,12 @@ std::vector TasTool::priorityList = { "decel", }; -TasTool::TasTool(const char *name, int slot) +TasTool::TasTool(const char *name, TasToolProcessingType processingType, int slot) : name(name) + , processingType(processingType) , slot(slot) { this->GetList(slot).push_back(this); - + // in case the tool is not defined in the priority list, put it in the back of it std::string nameStr = name; if (std::find(priorityList.begin(), priorityList.end(), nameStr) == priorityList.end()) { @@ -48,10 +51,6 @@ TasTool::TasTool(const char *name, int slot) TasTool::~TasTool() { } -const char *TasTool::GetName() { - return this->name; -} - void TasTool::SetParams(std::shared_ptr params) { this->paramsPtr = params; this->updated = true; diff --git a/src/Features/Tas/TasTool.hpp b/src/Features/Tas/TasTool.hpp index 75d19b20..6367251a 100644 --- a/src/Features/Tas/TasTool.hpp +++ b/src/Features/Tas/TasTool.hpp @@ -2,8 +2,8 @@ #include #include -#include #include +#include // A bunch of stuff accidentally used sin/cos directly, which led to // inconsistent scripts between Windows and Linux since Windows has @@ -25,22 +25,32 @@ struct TasToolParams { struct TasFramebulk; struct TasPlayerInfo; +enum TasToolProcessingType { + PRE_PROCESSING, + POST_PROCESSING, +}; + class TasTool { protected: const char *name; + TasToolProcessingType processingType; std::shared_ptr paramsPtr = nullptr; bool updated = false; int slot; + public: - TasTool(const char *name, int slot); + TasTool(const char *name, TasToolProcessingType processingType, int slot); ~TasTool(); - const char *GetName(); + const char *GetName() const { return name; } + inline int GetSlot() const { return slot; } + bool CanProcess(TasToolProcessingType type) const { return this->processingType == type; } virtual std::shared_ptr ParseParams(std::vector) = 0; virtual void Apply(TasFramebulk &fb, const TasPlayerInfo &pInfo) = 0; virtual void Reset() = 0; virtual void SetParams(std::shared_ptr params); + public: static std::list &GetList(int slot); static TasTool *GetInstanceByName(int slot, std::string name); @@ -52,9 +62,10 @@ template class TasToolWithParams : public TasTool { protected: Params params; + public: - TasToolWithParams(const char *name, int slot) - : TasTool(name, slot){}; + TasToolWithParams(const char *name, TasToolProcessingType processingType, int slot) + : TasTool(name, processingType, slot) {}; virtual void Reset() { this->paramsPtr = std::make_shared(); diff --git a/src/Features/Tas/TasTools/AbsoluteMoveTool.cpp b/src/Features/Tas/TasTools/AbsoluteMoveTool.cpp index a7a5e045..025193bd 100644 --- a/src/Features/Tas/TasTools/AbsoluteMoveTool.cpp +++ b/src/Features/Tas/TasTools/AbsoluteMoveTool.cpp @@ -5,10 +5,7 @@ #include "Features/Tas/TasParser.hpp" #include "MoveTool.hpp" -AbsoluteMoveTool tasAbsoluteMoveTool[2] = { - { "absmov", 0 }, - { "absmov", 1 }, -}; +AbsoluteMoveTool tasAbsoluteMoveTool[2] = { {0}, {1} }; void AbsoluteMoveTool::Apply(TasFramebulk &fb, const TasPlayerInfo &playerInfo) { if (!params.enabled) diff --git a/src/Features/Tas/TasTools/AbsoluteMoveTool.hpp b/src/Features/Tas/TasTools/AbsoluteMoveTool.hpp index 640bff19..dcf03170 100644 --- a/src/Features/Tas/TasTools/AbsoluteMoveTool.hpp +++ b/src/Features/Tas/TasTools/AbsoluteMoveTool.hpp @@ -20,8 +20,7 @@ class AbsoluteMoveTool : public TasToolWithParams { private: AbsoluteMoveToolParams amParams; public: - AbsoluteMoveTool(const char *name, int slot) - : TasToolWithParams(name, slot){}; + AbsoluteMoveTool(int slot) : TasToolWithParams("absmov", POST_PROCESSING, slot) {}; virtual std::shared_ptr ParseParams(std::vector); virtual void Apply(TasFramebulk &fb, const TasPlayerInfo &pInfo); }; diff --git a/src/Features/Tas/TasTools/AutoAimTool.hpp b/src/Features/Tas/TasTools/AutoAimTool.hpp index 17c35f43..56cbfe8f 100644 --- a/src/Features/Tas/TasTools/AutoAimTool.hpp +++ b/src/Features/Tas/TasTools/AutoAimTool.hpp @@ -34,7 +34,7 @@ class AutoAimTool : public TasToolWithParams { int elapsedTicks; public: AutoAimTool(int slot) - : TasToolWithParams("autoaim", slot) + : TasToolWithParams("autoaim", POST_PROCESSING, slot) , elapsedTicks(0) {} virtual std::shared_ptr ParseParams(std::vector); diff --git a/src/Features/Tas/TasTools/CheckTool.hpp b/src/Features/Tas/TasTools/CheckTool.hpp index e7801162..5b50de08 100644 --- a/src/Features/Tas/TasTools/CheckTool.hpp +++ b/src/Features/Tas/TasTools/CheckTool.hpp @@ -31,7 +31,7 @@ struct CheckToolParams : public TasToolParams { class CheckTool : public TasToolWithParams { public: CheckTool(int slot) - : TasToolWithParams("check", slot) + : TasToolWithParams("check", POST_PROCESSING, slot) {} virtual std::shared_ptr ParseParams(std::vector); diff --git a/src/Features/Tas/TasTools/CommandTool.cpp b/src/Features/Tas/TasTools/CommandTool.cpp index 01b8a5d8..108152f0 100644 --- a/src/Features/Tas/TasTools/CommandTool.cpp +++ b/src/Features/Tas/TasTools/CommandTool.cpp @@ -9,9 +9,14 @@ CommandTool commandTool[2] = {{0}, {1}}; void CommandTool::Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo) { if (params.enabled) { - // FetchInputs happens before tools in the tick, so we don't double-execute bulk.commands.push_back(params.command); - engine->ExecuteCommand(params.command.c_str(), true); + + FOR_TAS_SCRIPT_VERSIONS_UNTIL(8) { + // Commands in frame bulk are called by FetchInput + // but before version 9 they were added in post processing, so they need to be executed here. + engine->ExecuteCommand(params.command.c_str(), true); + } + params.enabled = false; } } diff --git a/src/Features/Tas/TasTools/CommandTool.hpp b/src/Features/Tas/TasTools/CommandTool.hpp index 0438497f..9f58fe6e 100644 --- a/src/Features/Tas/TasTools/CommandTool.hpp +++ b/src/Features/Tas/TasTools/CommandTool.hpp @@ -16,7 +16,7 @@ struct CommandToolParams : public TasToolParams { class CommandTool : public TasToolWithParams { public: CommandTool(int slot) - : TasToolWithParams("cmd", slot) { + : TasToolWithParams("cmd", PRE_PROCESSING, slot) { } virtual std::shared_ptr ParseParams(std::vector); diff --git a/src/Features/Tas/TasTools/DecelTool.hpp b/src/Features/Tas/TasTools/DecelTool.hpp index 8ff5fde5..e8339e68 100644 --- a/src/Features/Tas/TasTools/DecelTool.hpp +++ b/src/Features/Tas/TasTools/DecelTool.hpp @@ -17,7 +17,7 @@ struct DecelParams : public TasToolParams { class DecelTool : public TasToolWithParams { public: DecelTool(int slot) - : TasToolWithParams("decel", slot) {} + : TasToolWithParams("decel", POST_PROCESSING, slot) {} virtual std::shared_ptr ParseParams(std::vector); virtual void Apply(TasFramebulk &fb, const TasPlayerInfo &pInfo); }; diff --git a/src/Features/Tas/TasTools/DuckTool.hpp b/src/Features/Tas/TasTools/DuckTool.hpp index fc456776..eab6ec51 100644 --- a/src/Features/Tas/TasTools/DuckTool.hpp +++ b/src/Features/Tas/TasTools/DuckTool.hpp @@ -16,7 +16,7 @@ struct DuckToolParams : public TasToolParams { class DuckTool : public TasToolWithParams { public: DuckTool(int slot) - : TasToolWithParams("duck", slot) { + : TasToolWithParams("duck", PRE_PROCESSING, slot) { } virtual std::shared_ptr ParseParams(std::vector); diff --git a/src/Features/Tas/TasTools/JumpTool.hpp b/src/Features/Tas/TasTools/JumpTool.hpp index 37830a7d..1029a20d 100644 --- a/src/Features/Tas/TasTools/JumpTool.hpp +++ b/src/Features/Tas/TasTools/JumpTool.hpp @@ -16,7 +16,7 @@ struct JumpToolsParams : public TasToolParams { class JumpTool : public TasToolWithParams { public: JumpTool(int slot, bool automatic) - : TasToolWithParams(automatic ? "autojump" : "jump", slot) + : TasToolWithParams(automatic ? "autojump" : "jump", POST_PROCESSING, slot) , automatic(automatic) { } diff --git a/src/Features/Tas/TasTools/LookTool.cpp b/src/Features/Tas/TasTools/LookTool.cpp index be727a78..985d5abc 100644 --- a/src/Features/Tas/TasTools/LookTool.cpp +++ b/src/Features/Tas/TasTools/LookTool.cpp @@ -6,10 +6,7 @@ #include -LookTool tasLookTool[2] = { - {"look", 0}, - {"look", 1}, -}; +LookTool tasLookTool[2] = { {0}, {1} }; void LookTool::Apply(TasFramebulk &fb, const TasPlayerInfo &playerInfo) { if (!params.enabled) diff --git a/src/Features/Tas/TasTools/LookTool.hpp b/src/Features/Tas/TasTools/LookTool.hpp index 3900cb8c..c4332ebb 100644 --- a/src/Features/Tas/TasTools/LookTool.hpp +++ b/src/Features/Tas/TasTools/LookTool.hpp @@ -22,8 +22,8 @@ class LookTool : public TasToolWithParams { private: int remainingTime; public: - LookTool(const char *name, int slot) - : TasToolWithParams(name, slot){}; + LookTool(int slot) + : TasToolWithParams("look", PRE_PROCESSING, slot) {}; virtual std::shared_ptr ParseParams(std::vector); virtual void Apply(TasFramebulk &fb, const TasPlayerInfo &pInfo); }; diff --git a/src/Features/Tas/TasTools/MoveTool.cpp b/src/Features/Tas/TasTools/MoveTool.cpp index b14a1aa1..2bd61353 100644 --- a/src/Features/Tas/TasTools/MoveTool.cpp +++ b/src/Features/Tas/TasTools/MoveTool.cpp @@ -5,10 +5,7 @@ #include "Modules/Server.hpp" #include "Features/Tas/TasParser.hpp" -MoveTool tasMoveTool[2] = { - { "move", 0 }, - { "move", 1 }, -}; +MoveTool tasMoveTool[2] = { {0}, {1} }; const std::map directionLookup = { {"forward", Vector(0, 1)}, diff --git a/src/Features/Tas/TasTools/MoveTool.hpp b/src/Features/Tas/TasTools/MoveTool.hpp index 13bdf3b1..7491a98f 100644 --- a/src/Features/Tas/TasTools/MoveTool.hpp +++ b/src/Features/Tas/TasTools/MoveTool.hpp @@ -18,8 +18,8 @@ struct MoveToolParams : public TasToolParams { class MoveTool : public TasToolWithParams { public: - MoveTool(const char *name, int slot) - : TasToolWithParams(name, slot){}; + MoveTool(int slot) + : TasToolWithParams("move", PRE_PROCESSING, slot) {}; virtual std::shared_ptr ParseParams(std::vector); virtual void Apply(TasFramebulk &fb, const TasPlayerInfo &pInfo); }; diff --git a/src/Features/Tas/TasTools/SetAngleTool.hpp b/src/Features/Tas/TasTools/SetAngleTool.hpp index 5ec2f18e..a7eef915 100644 --- a/src/Features/Tas/TasTools/SetAngleTool.hpp +++ b/src/Features/Tas/TasTools/SetAngleTool.hpp @@ -29,7 +29,7 @@ class SetAngleTool : public TasToolWithParams { int elapsedTicks; public: SetAngleTool(int slot) - : TasToolWithParams("setang", slot) + : TasToolWithParams("setang", POST_PROCESSING, slot) , elapsedTicks(0) {} virtual std::shared_ptr ParseParams(std::vector); diff --git a/src/Features/Tas/TasTools/ShootTool.hpp b/src/Features/Tas/TasTools/ShootTool.hpp index 476da30a..64489741 100644 --- a/src/Features/Tas/TasTools/ShootTool.hpp +++ b/src/Features/Tas/TasTools/ShootTool.hpp @@ -21,7 +21,8 @@ struct ShootToolParams : public TasToolParams { class ShootTool : public TasToolWithParams { public: - ShootTool(int slot) : TasToolWithParams("shoot", slot) {} + ShootTool(int slot) + : TasToolWithParams("shoot", PRE_PROCESSING, slot) {} virtual std::shared_ptr ParseParams(std::vector); virtual void Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo); diff --git a/src/Features/Tas/TasTools/StopTool.hpp b/src/Features/Tas/TasTools/StopTool.hpp index 6929a4a5..33a7935e 100644 --- a/src/Features/Tas/TasTools/StopTool.hpp +++ b/src/Features/Tas/TasTools/StopTool.hpp @@ -13,7 +13,7 @@ struct StopToolParams : public TasToolParams { class StopTool : public TasToolWithParams { public: StopTool(int slot) - : TasToolWithParams("stop", slot) { + : TasToolWithParams("stop", POST_PROCESSING, slot) { } virtual std::shared_ptr ParseParams(std::vector); diff --git a/src/Features/Tas/TasTools/StrafeTool.hpp b/src/Features/Tas/TasTools/StrafeTool.hpp index 11c58a1d..a67694f8 100644 --- a/src/Features/Tas/TasTools/StrafeTool.hpp +++ b/src/Features/Tas/TasTools/StrafeTool.hpp @@ -66,7 +66,7 @@ class AutoStrafeTool : public TasToolWithParams { int lastTurnDir = 0; public: AutoStrafeTool(int slot) - : TasToolWithParams("strafe", slot){}; + : TasToolWithParams("strafe", POST_PROCESSING, slot) {}; virtual std::shared_ptr ParseParams(std::vector); virtual void Apply(TasFramebulk &fb, const TasPlayerInfo &pInfo); diff --git a/src/Features/Tas/TasTools/UseTool.hpp b/src/Features/Tas/TasTools/UseTool.hpp index 9825fcc6..fcc9544d 100644 --- a/src/Features/Tas/TasTools/UseTool.hpp +++ b/src/Features/Tas/TasTools/UseTool.hpp @@ -15,7 +15,8 @@ struct UseToolParams : public TasToolParams { class UseTool : public TasToolWithParams { public: - UseTool(int slot) : TasToolWithParams("use", slot) {} + UseTool(int slot) + : TasToolWithParams("use", PRE_PROCESSING, slot) {} virtual std::shared_ptr ParseParams(std::vector); virtual void Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo); diff --git a/src/Features/Tas/TasTools/ZoomTool.hpp b/src/Features/Tas/TasTools/ZoomTool.hpp index ded053f4..241a5dfb 100644 --- a/src/Features/Tas/TasTools/ZoomTool.hpp +++ b/src/Features/Tas/TasTools/ZoomTool.hpp @@ -22,7 +22,7 @@ struct ZoomToolParams : public TasToolParams { class ZoomTool : public TasToolWithParams { public: ZoomTool(int slot) - : TasToolWithParams("zoom", slot) { + : TasToolWithParams("zoom", PRE_PROCESSING, slot) { } virtual std::shared_ptr ParseParams(std::vector); From c14797f8a3e80ba387b038b19ed5d99667e412a9 Mon Sep 17 00:00:00 2001 From: Chris Hausky Date: Sat, 1 Nov 2025 11:13:26 +0100 Subject: [PATCH 04/15] fix: correct execution order to fix tas playback tools have to be updated before player info is fetched, as it requires most recent tools state --- src/Features/Tas/TasPlayer.cpp | 13 ++++++++----- src/Features/Tas/TasPlayer.hpp | 1 + 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Features/Tas/TasPlayer.cpp b/src/Features/Tas/TasPlayer.cpp index 23a6bd0c..16d10bbc 100644 --- a/src/Features/Tas/TasPlayer.cpp +++ b/src/Features/Tas/TasPlayer.cpp @@ -601,8 +601,6 @@ static bool IsTaunting(ClientEnt *player) { } TasFramebulk TasPlayer::SamplePreProcessedFramebulk(int slot, int tasTick, void *player, CUserCmd *cmd) { - - auto pInfo = GetPlayerInfo(slot, server->GetPlayer(slot + 1), cmd); TasFramebulk& fb = RequestProcessedFramebulkAt(slot, tasTick); auto fbTick = fb.tick; @@ -636,6 +634,8 @@ TasFramebulk TasPlayer::SamplePreProcessedFramebulk(int slot, int tasTick, void } 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) { @@ -660,6 +660,7 @@ void TasPlayer::PostProcess(int slot, void *player, CUserCmd *cmd) { TasFramebulk& fb = RequestProcessedFramebulkAt(slot, tasTick); + UpdateTools(slot, fb, POST_PROCESSING); auto playerInfo = GetPlayerInfo(slot, player, cmd); float orig_forward = cmd->forwardmove; @@ -782,14 +783,16 @@ void TasPlayer::ApplyMoveAnalog(Vector moveAnalog, CUserCmd *cmd) { } } -void TasPlayer::ApplyTools(TasFramebulk &fb, const TasPlayerInfo &pInfo, TasToolProcessingType processType) { - int slot = pInfo.slot; - +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) diff --git a/src/Features/Tas/TasPlayer.hpp b/src/Features/Tas/TasPlayer.hpp index 9644d414..ef0645d8 100644 --- a/src/Features/Tas/TasPlayer.hpp +++ b/src/Features/Tas/TasPlayer.hpp @@ -123,6 +123,7 @@ class TasPlayer : public Feature { TasFramebulk SamplePreProcessedFramebulk(int slot, int tasTick, void *player, CUserCmd *cmd); void PostProcess(int slot, void *player, CUserCmd *cmd); void ApplyMoveAnalog(Vector moveAnalog, CUserCmd *cmd); + void UpdateTools(int slot, const TasFramebulk &fb, TasToolProcessingType processType); void ApplyTools(TasFramebulk &fb, const TasPlayerInfo &pInfo, TasToolProcessingType processType); bool CanProcessTool(TasTool *tool, TasToolProcessingType processType); void DumpUsercmd(int slot, const CUserCmd *cmd, int tick, const char *source); From dbfee9b0af538ea19982875c88380bd27057f0fc Mon Sep 17 00:00:00 2001 From: Chris Hausky Date: Sat, 1 Nov 2025 11:20:53 +0100 Subject: [PATCH 05/15] feat: add missing implementation to tas commands --- src/Features/Tas/TasPlayer.cpp | 13 +++++++++++++ src/Features/Tas/TasPlayer.hpp | 5 ++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Features/Tas/TasPlayer.cpp b/src/Features/Tas/TasPlayer.cpp index 16d10bbc..373ddc5b 100644 --- a/src/Features/Tas/TasPlayer.cpp +++ b/src/Features/Tas/TasPlayer.cpp @@ -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; diff --git a/src/Features/Tas/TasPlayer.hpp b/src/Features/Tas/TasPlayer.hpp index ef0645d8..d645374d 100644 --- a/src/Features/Tas/TasPlayer.hpp +++ b/src/Features/Tas/TasPlayer.hpp @@ -89,11 +89,10 @@ class TasPlayer : public Feature { inline bool IsReady() const { return ready; }; inline bool IsRunning() const { return active && startTick != -1; } inline bool IsPaused() const { return paused; } - inline bool IsUsingTools() const { - return (playbackInfo.slots[0].IsActive() && !playbackInfo.slots[0].IsRaw()) || (playbackInfo.slots[1].IsActive() && !playbackInfo.slots[1].IsRaw()); - } inline int GetScriptVersion(int slot) const { return playbackInfo.slots[slot].header.version; } + bool IsUsingTools() const; + void PlayFile(std::string slot0scriptPath, std::string slot1scriptPath); void PlayScript(std::string slot0name, std::string slot0script, std::string slot1name, std::string slot1script); void PlaySingleCoop(std::string file, int slot); From 5517b971a3e2cbb7275a4a43beb1e31cb580e1d2 Mon Sep 17 00:00:00 2001 From: Chris Hausky Date: Wed, 17 Dec 2025 01:17:00 +0100 Subject: [PATCH 06/15] feat: improve offset param for strafe tas tool --- src/Features/Tas/TasParser.cpp | 8 ++ src/Features/Tas/TasParser.hpp | 2 + src/Features/Tas/TasTools/StrafeTool.cpp | 94 ++++++++++++++++-------- src/Features/Tas/TasTools/StrafeTool.hpp | 18 ++++- 4 files changed, 88 insertions(+), 34 deletions(-) diff --git a/src/Features/Tas/TasParser.cpp b/src/Features/Tas/TasParser.cpp index 5d1824ef..59f9dedd 100644 --- a/src/Features/Tas/TasParser.cpp +++ b/src/Features/Tas/TasParser.cpp @@ -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("."); diff --git a/src/Features/Tas/TasParser.hpp b/src/Features/Tas/TasParser.hpp index 3e1a1ac7..5618b2d3 100644 --- a/src/Features/Tas/TasParser.hpp +++ b/src/Features/Tas/TasParser.hpp @@ -25,4 +25,6 @@ namespace TasParser { 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); }; diff --git a/src/Features/Tas/TasTools/StrafeTool.cpp b/src/Features/Tas/TasTools/StrafeTool.cpp index 119d981a..9e0da1ae 100644 --- a/src/Features/Tas/TasTools/StrafeTool.cpp +++ b/src/Features/Tas/TasTools/StrafeTool.cpp @@ -172,7 +172,7 @@ void AutoStrafeTool::ApplyStrafe(TasFramebulk &fb, const TasPlayerInfo &pInfo) { } } - float angle = velAngle + RAD2DEG(this->GetStrafeAngle(pInfo, params)) * params.turnRate; + float angle = velAngle + RAD2DEG(this->GetStrafeAngle(pInfo, params)); FOR_TAS_SCRIPT_VERSIONS_SINCE(8) { // Deal with airlock: we're strafing, so always try to maximize how much @@ -442,6 +442,22 @@ float AutoStrafeTool::GetTurningStrafeAngle(const TasPlayerInfo &player) { return acosf(cosAng); } +// get horizontal angle of wishdir that would give the biggest turning angle while understrafing +float AutoStrafeTool::GetMaxUnderstrafeAngle(const TasPlayerInfo &player) { + Vector velocity = GetGroundFrictionVelocity(player); + + if (velocity.Length2D() == 0) return 0; + + Vector wishDir = Vector(0, 1) * params.force; + float maxSpeed = GetMaxSpeed(player, wishDir); + + // Solution is the angle where dot product of velocity and normalized wishdir is equal to max speed. + float cosAng = maxSpeed / velocity.Length2D(); + if (cosAng > 1) cosAng = 1; + + return acosf(cosAng); +} + // get horizontal angle of wishdir that does correct thing based on given parameters // angle is relative to your current velocity direction. @@ -496,6 +512,7 @@ float AutoStrafeTool::GetStrafeAngle(const TasPlayerInfo &player, AutoStrafePara ang = GetTargetStrafeAngle(player, params.strafeSpeed.speed, turningDir); } + ang += GetSteeringOffsetValue(player, params.steeringOffset) * turningDir; return ang; } @@ -630,6 +647,21 @@ void AutoStrafeTool::FollowLine(const TasPlayerInfo &pInfo) { this->followLinePoint = pInfo.position; } +float AutoStrafeTool::GetSteeringOffsetValue(const TasPlayerInfo &pInfo, AutoStrafeSteeringOffset &offset) { + if (offset.type == DEGREES) { + return DEG2RAD(offset.value); + } + + bool understrafe = offset.value < 0; + float absOffsetScalar = fabsf(offset.value); + + float baseAngle = GetFastestStrafeAngle(pInfo); + float unitAngle = understrafe ? GetMaxUnderstrafeAngle(pInfo) : GetTurningStrafeAngle(pInfo); + + float unitDeltaAngle = unitAngle - baseAngle; + return unitDeltaAngle * absOffsetScalar; +} + std::shared_ptr AutoStrafeTool::ParseParams(std::vector vp) { AutoStrafeType type = VECTORIAL; @@ -638,7 +670,7 @@ std::shared_ptr AutoStrafeTool::ParseParams(std::vector AutoStrafeTool::ParseParams(std::vector 3 && param.substr(param.size() - 3, 3) == "ups") { + } else if (TasParser::hasSuffix(param, "ups")) { speed.type = SPECIFIED; - speed.speed = TasParser::toFloat(param.substr(0, param.size() - 3)); + speed.speed = TasParser::toFloatAssumeSuffix(param, "ups"); } else if (param == "min") { speed.type = SPECIFIED; speed.speed = 0.0f; @@ -684,44 +731,29 @@ std::shared_ptr AutoStrafeTool::ParseParams(std::vector 3 && param.substr(param.size() - 3, 3) == "deg") { + } else if (TasParser::hasSuffix(param, "deg")) { dir.type = SPECIFIED; - dir.angle = TasParser::toFloat(param.substr(0, param.size() - 3)); + dir.angle = TasParser::toFloatAssumeSuffix(param, "deg"); } + // flags else if (param == "nopitchlock") { noPitchLock = true; } - else if (param == "letspeedlock") { antiSpeedLock = false; } - //named value parameters - else if (param.find('=') != std::string::npos) { - size_t eqPos = param.find('='); - std::string key = param.substr(0, eqPos); - std::string value = param.substr(eqPos + 1); - - if (key == "turnrate") { - turnRate = TasParser::toFloat(value); - } else if (key == "force") { - force = TasParser::toFloat(value); - } else if (key == "velocity" || key == "vel") { - speed.type = SPECIFIED; - speed.speed = TasParser::toFloat(value); - } else if (key == "angle" || key == "ang") { - dir.angle = SPECIFIED; - dir.angle = TasParser::toFloat(value); - } else { - throw TasParserException(Utils::ssprintf("Unknown named parameter for tool %s: %s", this->GetName(), key.c_str())); - } - } - - //unknown parameter... - else + // suffix-less number (force) + else try { + force = TasParser::toFloat(param); + } + + catch (...) { + // unknown parameter... throw TasParserException(Utils::ssprintf("Bad parameter for tool %s: %s", this->GetName(), param.c_str())); + } } - return std::make_shared(type, dir, speed, noPitchLock, antiSpeedLock, turnRate, force); + return std::make_shared(type, dir, speed, noPitchLock, antiSpeedLock, steeringOffset, force); } diff --git a/src/Features/Tas/TasTools/StrafeTool.hpp b/src/Features/Tas/TasTools/StrafeTool.hpp index a67694f8..e1d28f7c 100644 --- a/src/Features/Tas/TasTools/StrafeTool.hpp +++ b/src/Features/Tas/TasTools/StrafeTool.hpp @@ -21,6 +21,11 @@ enum AutoStrafeParamType { CURRENT, }; +enum AutoStrafeSteeringOffsetType { + DEGREES, + SCALAR, +}; + struct AutoStrafeDirection { AutoStrafeParamType type; bool useVelAngle; @@ -32,6 +37,11 @@ struct AutoStrafeSpeed { float speed; }; +struct AutoStrafeSteeringOffset { + AutoStrafeSteeringOffsetType type; + float value; +}; + struct AutoStrafeParams : public TasToolParams { AutoStrafeType strafeType = DISABLED; @@ -39,20 +49,20 @@ struct AutoStrafeParams : public TasToolParams { AutoStrafeSpeed strafeSpeed = {CURRENT}; bool noPitchLock = false; bool antiSpeedLock = true; - float turnRate = 1.0f; + AutoStrafeSteeringOffset steeringOffset = {DEGREES, 0.0f}; float force = 1.0f; AutoStrafeParams() : TasToolParams() {} - AutoStrafeParams(AutoStrafeType type, AutoStrafeDirection dir, AutoStrafeSpeed speed, bool noPitchLock, bool antiSpeedLock, float turnRate, float force) + AutoStrafeParams(AutoStrafeType type, AutoStrafeDirection dir, AutoStrafeSpeed speed, bool noPitchLock, bool antiSpeedLock, AutoStrafeSteeringOffset steeringOffset, float force) : TasToolParams(true) , strafeType(type) , strafeDir(dir) , strafeSpeed(speed) , noPitchLock(noPitchLock) , antiSpeedLock(antiSpeedLock) - , turnRate(turnRate) + , steeringOffset(steeringOffset) , force(force) { } }; @@ -80,6 +90,7 @@ class AutoStrafeTool : public TasToolWithParams { float GetFastestStrafeAngle(const TasPlayerInfo &player); float GetTargetStrafeAngle(const TasPlayerInfo &player, float targetSpeed, float turningDir); float GetTurningStrafeAngle(const TasPlayerInfo &player); + float GetMaxUnderstrafeAngle(const TasPlayerInfo &player); private: void UpdateTargetValuesMarkedCurrent(TasFramebulk &fb, const TasPlayerInfo &pInfo); @@ -91,6 +102,7 @@ class AutoStrafeTool : public TasToolWithParams { int GetTurningDirection(const TasPlayerInfo &pInfo, float desAngle); void FollowLine(const TasPlayerInfo &pInfo); + float GetSteeringOffsetValue(const TasPlayerInfo &pInfo, AutoStrafeSteeringOffset &offset); }; extern AutoStrafeTool autoStrafeTool[2]; From d55b4735f8740fca8e284c68b942bd4345436371 Mon Sep 17 00:00:00 2001 From: Chris Hausky Date: Wed, 17 Dec 2025 10:30:33 +0100 Subject: [PATCH 07/15] feat: clean up tas error handling and display --- src/Features/Tas/TasParser.hpp | 16 ++++++++++++++++ src/Features/Tas/TasTools/AbsoluteMoveTool.cpp | 6 +++--- src/Features/Tas/TasTools/AutoAimTool.cpp | 14 +++++++------- src/Features/Tas/TasTools/CommandTool.cpp | 2 +- src/Features/Tas/TasTools/DecelTool.cpp | 4 ++-- src/Features/Tas/TasTools/DuckTool.cpp | 4 ++-- src/Features/Tas/TasTools/JumpTool.cpp | 7 ++++--- src/Features/Tas/TasTools/LookTool.cpp | 5 ++--- src/Features/Tas/TasTools/MoveTool.cpp | 6 +++--- src/Features/Tas/TasTools/SetAngleTool.cpp | 14 +++++++------- src/Features/Tas/TasTools/ShootTool.cpp | 4 ++-- src/Features/Tas/TasTools/StopTool.cpp | 2 +- src/Features/Tas/TasTools/StrafeTool.cpp | 6 ++++-- src/Features/Tas/TasTools/UseTool.cpp | 4 ++-- src/Features/Tas/TasTools/ZoomTool.cpp | 4 ++-- 15 files changed, 58 insertions(+), 40 deletions(-) diff --git a/src/Features/Tas/TasParser.hpp b/src/Features/Tas/TasParser.hpp index 5618b2d3..b3d2992f 100644 --- a/src/Features/Tas/TasParser.hpp +++ b/src/Features/Tas/TasParser.hpp @@ -18,6 +18,22 @@ 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); diff --git a/src/Features/Tas/TasTools/AbsoluteMoveTool.cpp b/src/Features/Tas/TasTools/AbsoluteMoveTool.cpp index 025193bd..2d939389 100644 --- a/src/Features/Tas/TasTools/AbsoluteMoveTool.cpp +++ b/src/Features/Tas/TasTools/AbsoluteMoveTool.cpp @@ -52,7 +52,7 @@ void AbsoluteMoveTool::Apply(TasFramebulk &fb, const TasPlayerInfo &playerInfo) std::shared_ptr AbsoluteMoveTool::ParseParams(std::vector vp) { if (vp.size() != 1 && vp.size() != 2) - throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), vp.size())); + throw TasParserArgumentCountException(this, vp.size()); if (vp[0] == "off") return std::make_shared(); @@ -61,7 +61,7 @@ std::shared_ptr AbsoluteMoveTool::ParseParams(std::vectorGetName(), vp[0].c_str())); + throw TasParserArgumentException(this, "direction", vp[0]); } float strength; @@ -70,7 +70,7 @@ std::shared_ptr AbsoluteMoveTool::ParseParams(std::vector 1) strength = 1; if (strength < 0) strength = 0; } catch (...) { - throw TasParserException(Utils::ssprintf("Bad strength for tool %s: %s", this->GetName(), vp[0].c_str())); + throw TasParserArgumentException(this, "strength", vp[1]); } return std::make_shared(angle, strength); diff --git a/src/Features/Tas/TasTools/AutoAimTool.cpp b/src/Features/Tas/TasTools/AutoAimTool.cpp index c24022fd..83671008 100644 --- a/src/Features/Tas/TasTools/AutoAimTool.cpp +++ b/src/Features/Tas/TasTools/AutoAimTool.cpp @@ -12,14 +12,14 @@ std::shared_ptr AutoAimTool::ParseParams(std::vector bool usesEntitySelector = args.size() > 0 && args[0] == "ent"; if (args.size() < (usesEntitySelector ? 2 : 1) || args.size() > (usesEntitySelector ? 4 : 5)) { - throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), args.size())); + throw TasParserArgumentCountException(this, args.size()); } if (args.size() == 1 && args[0] == "off") { return std::make_shared(); } else if (!usesEntitySelector && args.size() < 3) { - throw TasParserException(Utils::ssprintf("Bad argument for tool %s: %s", this->GetName(), args[0].c_str())); + throw TasParserArgumentCountException(this, args.size()); } int ticks; @@ -30,7 +30,7 @@ std::shared_ptr AutoAimTool::ParseParams(std::vector try { ticks = args.size() >= ticksPos + 1 ? std::stoi(args[ticksPos]) : 1; } catch (...) { - throw TasParserException(Utils::ssprintf("Bad tick value for tool %s: %s", this->GetName(), args[ticksPos].c_str())); + throw TasParserArgumentException(this, "ticks", args[ticksPos]); } // easing type @@ -38,7 +38,7 @@ std::shared_ptr AutoAimTool::ParseParams(std::vector try { easingType = AngleToolsUtils::ParseEasingType(args.size() >= typePos + 1 ? args[typePos] : "linear"); } catch (...) { - throw TasParserException(Utils::ssprintf("Bad interpolation value for tool %s: %s", this->GetName(), args[typePos].c_str())); + throw TasParserArgumentException(this, "interpolation", args[typePos]); } if (usesEntitySelector) { @@ -51,19 +51,19 @@ std::shared_ptr AutoAimTool::ParseParams(std::vector try { x = std::stof(args[0]); } catch (...) { - throw TasParserException(Utils::ssprintf("Bad x value for tool %s: %s", this->GetName(), args[0].c_str())); + throw TasParserArgumentException(this, "x value", args[0]); } try { y = std::stof(args[1]); } catch (...) { - throw TasParserException(Utils::ssprintf("Bad y value for tool %s: %s", this->GetName(), args[1].c_str())); + throw TasParserArgumentException(this, "y value", args[1]); } try { z = std::stof(args[2]); } catch (...) { - throw TasParserException(Utils::ssprintf("Bad z value for tool %s: %s", this->GetName(), args[2].c_str())); + throw TasParserArgumentException(this, "z value", args[2]); } return std::make_shared(Vector{x, y, z}, ticks, easingType); diff --git a/src/Features/Tas/TasTools/CommandTool.cpp b/src/Features/Tas/TasTools/CommandTool.cpp index 108152f0..78c7559c 100644 --- a/src/Features/Tas/TasTools/CommandTool.cpp +++ b/src/Features/Tas/TasTools/CommandTool.cpp @@ -23,7 +23,7 @@ void CommandTool::Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo) { std::shared_ptr CommandTool::ParseParams(std::vector vp) { if (vp.size() == 0) - throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), vp.size())); + throw TasParserArgumentCountException(this, vp.size()); std::string command; diff --git a/src/Features/Tas/TasTools/DecelTool.cpp b/src/Features/Tas/TasTools/DecelTool.cpp index 3e8f093f..3fd3eb5a 100644 --- a/src/Features/Tas/TasTools/DecelTool.cpp +++ b/src/Features/Tas/TasTools/DecelTool.cpp @@ -44,7 +44,7 @@ void DecelTool::Apply(TasFramebulk &bulk, const TasPlayerInfo &playerInfo) { std::shared_ptr DecelTool::ParseParams(std::vector args) { if (args.size() != 1) { - throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), args.size())); + throw TasParserArgumentCountException(this, args.size()); } if (args[0] == "off") { return std::make_shared(false); @@ -55,7 +55,7 @@ std::shared_ptr DecelTool::ParseParams(std::vector a try { targetVel = atof(args[0].c_str()); } catch (...) { - throw TasParserException(Utils::ssprintf("Bad target velocity for tool %s: %s", this->GetName(), args[0].c_str())); + throw TasParserArgumentException(this, "target velocity", args[0]); } return std::make_shared(targetVel); diff --git a/src/Features/Tas/TasTools/DuckTool.cpp b/src/Features/Tas/TasTools/DuckTool.cpp index f5f32181..82edd683 100644 --- a/src/Features/Tas/TasTools/DuckTool.cpp +++ b/src/Features/Tas/TasTools/DuckTool.cpp @@ -29,7 +29,7 @@ void DuckTool::Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo) { std::shared_ptr DuckTool::ParseParams(std::vector vp) { if (vp.size() != 1) - throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), vp.size())); + TasParserArgumentCountException(this, vp.size()); bool enabled = true; int time = INT32_MAX; @@ -43,7 +43,7 @@ std::shared_ptr DuckTool::ParseParams(std::vector vp try { time = std::stoi(vp[0]); } catch (...) { - throw TasParserException(Utils::ssprintf("Incorrect parameter for tool %s: %s", this->GetName(), vp[0].c_str())); + throw TasParserArgumentException(this, vp[0]); } } diff --git a/src/Features/Tas/TasTools/JumpTool.cpp b/src/Features/Tas/TasTools/JumpTool.cpp index 0fbc35a9..5c4b71b1 100644 --- a/src/Features/Tas/TasTools/JumpTool.cpp +++ b/src/Features/Tas/TasTools/JumpTool.cpp @@ -51,8 +51,9 @@ std::shared_ptr JumpTool::ParseParams(std::vector vp return std::make_shared(true, false); } - if (vp.size() != 1) - throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), vp.size())); + if (vp.size() != 1) { + throw TasParserArgumentCountException(this, vp.size()); + } bool ducked = false; bool enabled = false; @@ -63,7 +64,7 @@ std::shared_ptr JumpTool::ParseParams(std::vector vp enabled = true; ducked = true; } else if (vp[0] != "off") { - throw TasParserException(Utils::ssprintf("Bad parameter for tool %s: %s", this->GetName(), vp[0].c_str())); + throw TasParserArgumentException(this, vp[0]); } return std::make_shared(enabled, ducked); diff --git a/src/Features/Tas/TasTools/LookTool.cpp b/src/Features/Tas/TasTools/LookTool.cpp index 985d5abc..69b449be 100644 --- a/src/Features/Tas/TasTools/LookTool.cpp +++ b/src/Features/Tas/TasTools/LookTool.cpp @@ -32,7 +32,7 @@ void LookTool::Apply(TasFramebulk &fb, const TasPlayerInfo &playerInfo) { std::shared_ptr LookTool::ParseParams(std::vector vp) { if (vp.size() == 0) { - return std::make_shared(); + throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), vp.size())); } float pitchDelta = 0.0f; @@ -76,8 +76,7 @@ std::shared_ptr LookTool::ParseParams(std::vector vp // try to just parse a number as time if it's not any known parameter else { if (timeAssigned) { - // unknown parameter - throw TasParserException(Utils::ssprintf("Bad parameter for tool %s: %s", this->GetName(), param.c_str())); + throw TasParserArgumentException(this, param); } tickCount = TasParser::toFloat(param); timeAssigned = true; diff --git a/src/Features/Tas/TasTools/MoveTool.cpp b/src/Features/Tas/TasTools/MoveTool.cpp index 2bd61353..6ea31173 100644 --- a/src/Features/Tas/TasTools/MoveTool.cpp +++ b/src/Features/Tas/TasTools/MoveTool.cpp @@ -36,7 +36,7 @@ void MoveTool::Apply(TasFramebulk &fb, const TasPlayerInfo &playerInfo) { std::shared_ptr MoveTool::ParseParams(std::vector vp) { if (vp.size() == 0) { - return std::make_shared(); + throw TasParserArgumentCountException(this, vp.size()); } float forwardMove = 0.0f; @@ -87,14 +87,14 @@ std::shared_ptr MoveTool::ParseParams(std::vector vp } else if (numberCount == 2) { forwardMove = number; } else if (numberCount > 2) { - throw TasParserException(Utils::ssprintf("Too many parameters for tool %s: %s", this->GetName(), param.c_str())); + throw TasParserArgumentCountException(this, vp.size()); } } else { if (numberCount == 1) { scale = number; scaleAssigned = true; } else { - throw TasParserException(Utils::ssprintf("Too many parameters for tool %s: %s", this->GetName(), param.c_str())); + throw TasParserArgumentCountException(this, vp.size()); } } } diff --git a/src/Features/Tas/TasTools/SetAngleTool.cpp b/src/Features/Tas/TasTools/SetAngleTool.cpp index 53ce27f0..43b7e7b8 100644 --- a/src/Features/Tas/TasTools/SetAngleTool.cpp +++ b/src/Features/Tas/TasTools/SetAngleTool.cpp @@ -46,7 +46,7 @@ void SetAngleTool::Apply(TasFramebulk &bulk, const TasPlayerInfo &playerInfo) { std::shared_ptr SetAngleTool::ParseParams(std::vector vp) { if (vp.size() < 1 || vp.size() > 4) - throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), vp.size())); + throw TasParserArgumentCountException(this, vp.size()); std::string target; float pitch; @@ -56,24 +56,24 @@ std::shared_ptr SetAngleTool::ParseParams(std::vector 3) throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), vp.size())); + if (vp.size() > 3) throw TasParserArgumentCountException(this, vp.size()); target = vp[0]; i = 1; } else { - if (vp.size() < 2) throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), vp.size())); + if (vp.size() < 2) throw TasParserArgumentCountException(this, vp.size()); // pitch try { pitch = std::stof(vp[0]); } catch (...) { - throw TasParserException(Utils::ssprintf("Bad pitch value for tool %s: %s", this->GetName(), vp[0].c_str())); + throw TasParserArgumentException(this, "pitch", vp[0]); } // yaw try { yaw = std::stof(vp[1]); } catch (...) { - throw TasParserException(Utils::ssprintf("Bad yaw value for tool %s: %s", this->GetName(), vp[1].c_str())); + throw TasParserArgumentException(this, "yaw", vp[1]); } } @@ -81,14 +81,14 @@ std::shared_ptr SetAngleTool::ParseParams(std::vector= (size_t)(i + 1) ? std::stoi(vp[i]) : 1; } catch (...) { - throw TasParserException(Utils::ssprintf("Bad tick value for tool %s: %s", this->GetName(), vp[i].c_str())); + throw TasParserArgumentException(this, "tick", vp[i]); } // easing type try { easingType = AngleToolsUtils::ParseEasingType(vp.size() >= (size_t)(i + 2) ? vp[i + 1] : "linear"); } catch (...) { - throw TasParserException(Utils::ssprintf("Bad interpolation value for tool %s: %s", this->GetName(), vp[i + 1].c_str())); + throw TasParserArgumentException(this, "interpolation", vp[i + 1]); } return std::make_shared(target, pitch, yaw, ticks, easingType); diff --git a/src/Features/Tas/TasTools/ShootTool.cpp b/src/Features/Tas/TasTools/ShootTool.cpp index 2c729677..9f21c49b 100644 --- a/src/Features/Tas/TasTools/ShootTool.cpp +++ b/src/Features/Tas/TasTools/ShootTool.cpp @@ -33,7 +33,7 @@ void ShootTool::Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo) { std::shared_ptr ShootTool::ParseParams(std::vector vp) { if (vp.size() < 1 || vp.size() > 2) - throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), vp.size())); + throw TasParserArgumentCountException(this, vp.size()); bool enabled = true; TasControllerInput button; @@ -57,7 +57,7 @@ std::shared_ptr ShootTool::ParseParams(std::vector v spam = true; } else { - throw TasParserException(Utils::ssprintf("Incorrect parameter for tool %s: %s", this->GetName(), param.c_str())); + throw TasParserArgumentException(this, param); } } diff --git a/src/Features/Tas/TasTools/StopTool.cpp b/src/Features/Tas/TasTools/StopTool.cpp index 86891ccb..58b6eb42 100644 --- a/src/Features/Tas/TasTools/StopTool.cpp +++ b/src/Features/Tas/TasTools/StopTool.cpp @@ -29,7 +29,7 @@ void StopTool::Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo) { std::shared_ptr StopTool::ParseParams(std::vector vp) { if (vp.size() > 0) - throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), vp.size())); + throw TasParserArgumentCountException(this, vp.size()); return std::make_shared(true); } diff --git a/src/Features/Tas/TasTools/StrafeTool.cpp b/src/Features/Tas/TasTools/StrafeTool.cpp index 9e0da1ae..ec2d0a7b 100644 --- a/src/Features/Tas/TasTools/StrafeTool.cpp +++ b/src/Features/Tas/TasTools/StrafeTool.cpp @@ -674,7 +674,7 @@ std::shared_ptr AutoStrafeTool::ParseParams(std::vector(); + throw TasParserArgumentCountException(this, vp.size()); } for (std::string param : vp) { @@ -687,6 +687,8 @@ std::shared_ptr AutoStrafeTool::ParseParams(std::vector AutoStrafeTool::ParseParams(std::vectorGetName(), param.c_str())); + throw TasParserArgumentException(this, param); } } diff --git a/src/Features/Tas/TasTools/UseTool.cpp b/src/Features/Tas/TasTools/UseTool.cpp index d4a3a364..795f6e37 100644 --- a/src/Features/Tas/TasTools/UseTool.cpp +++ b/src/Features/Tas/TasTools/UseTool.cpp @@ -28,7 +28,7 @@ void UseTool::Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo) { std::shared_ptr UseTool::ParseParams(std::vector vp) { if (vp.size() > 1) - throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), vp.size())); + throw TasParserArgumentCountException(this, vp.size()); if (vp.size() == 0) { return std::make_shared(true, false); @@ -43,7 +43,7 @@ std::shared_ptr UseTool::ParseParams(std::vector vp) else if (vp[0] == "spam") { spam = true; } else { - throw TasParserException(Utils::ssprintf("Incorrect parameter for tool %s: %s", this->GetName(), vp[0].c_str())); + throw TasParserArgumentException(this, vp[0]); } return std::make_shared(enabled, spam); diff --git a/src/Features/Tas/TasTools/ZoomTool.cpp b/src/Features/Tas/TasTools/ZoomTool.cpp index 03c5253d..016f8603 100644 --- a/src/Features/Tas/TasTools/ZoomTool.cpp +++ b/src/Features/Tas/TasTools/ZoomTool.cpp @@ -29,7 +29,7 @@ void ZoomTool::Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo) { std::shared_ptr ZoomTool::ParseParams(std::vector vp) { if (vp.size() != 1) - throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), vp.size())); + throw TasParserArgumentCountException(this, vp.size()); ZoomType type; @@ -42,7 +42,7 @@ std::shared_ptr ZoomTool::ParseParams(std::vector vp else if (vp[0] == "toggle") { type = ZoomType::Toggle; } else { - throw TasParserException(Utils::ssprintf("Bad parameter for tool %s: %s", this->GetName(), vp[0].c_str())); + throw TasParserArgumentException(this, vp[0]); } return std::make_shared(true, type); From 4f5b3162b682d09f99fd3484bc8953f9c56dad6f Mon Sep 17 00:00:00 2001 From: Chris Hausky Date: Wed, 17 Dec 2025 14:24:44 +0100 Subject: [PATCH 08/15] feat: extend stop tas tool functionality --- src/Features/Tas/TasTool.cpp | 3 +- src/Features/Tas/TasTool.hpp | 19 ++++++++++-- .../Tas/TasTools/AbsoluteMoveTool.hpp | 2 +- src/Features/Tas/TasTools/AutoAimTool.hpp | 2 +- src/Features/Tas/TasTools/CheckTool.hpp | 2 +- src/Features/Tas/TasTools/CommandTool.hpp | 2 +- src/Features/Tas/TasTools/DecelTool.hpp | 2 +- src/Features/Tas/TasTools/DuckTool.hpp | 2 +- src/Features/Tas/TasTools/JumpTool.hpp | 2 +- src/Features/Tas/TasTools/LookTool.hpp | 2 +- src/Features/Tas/TasTools/MoveTool.hpp | 2 +- src/Features/Tas/TasTools/SetAngleTool.hpp | 2 +- src/Features/Tas/TasTools/ShootTool.hpp | 2 +- src/Features/Tas/TasTools/StopTool.cpp | 29 +++++++++++++++++-- src/Features/Tas/TasTools/StopTool.hpp | 9 ++++-- src/Features/Tas/TasTools/StrafeTool.hpp | 2 +- src/Features/Tas/TasTools/UseTool.hpp | 2 +- src/Features/Tas/TasTools/ZoomTool.hpp | 2 +- 18 files changed, 64 insertions(+), 24 deletions(-) diff --git a/src/Features/Tas/TasTool.cpp b/src/Features/Tas/TasTool.cpp index 9925f891..239607b3 100644 --- a/src/Features/Tas/TasTool.cpp +++ b/src/Features/Tas/TasTool.cpp @@ -35,9 +35,10 @@ std::vector TasTool::priorityList = { "decel", }; -TasTool::TasTool(const char *name, TasToolProcessingType processingType, int slot) +TasTool::TasTool(const char *name, TasToolProcessingType processingType, TasToolBulkType bulkType, int slot) : name(name) , processingType(processingType) + , bulkType(bulkType) , slot(slot) { this->GetList(slot).push_back(this); diff --git a/src/Features/Tas/TasTool.hpp b/src/Features/Tas/TasTool.hpp index 6367251a..85e6c823 100644 --- a/src/Features/Tas/TasTool.hpp +++ b/src/Features/Tas/TasTool.hpp @@ -30,21 +30,34 @@ enum TasToolProcessingType { POST_PROCESSING, }; +enum TasToolBulkType { + NONE = 0, + MOVEMENT = 1 << 1, + VIEWANGLES = 1 << 2, + BUTTONS = 1 << 3, + COMMANDS = 1 << 4, + META = 1 << 5, + + ALL_TYPES_MASK = MOVEMENT | VIEWANGLES | BUTTONS | COMMANDS | META, +}; + class TasTool { protected: const char *name; TasToolProcessingType processingType; + TasToolBulkType bulkType; std::shared_ptr paramsPtr = nullptr; bool updated = false; int slot; public: - TasTool(const char *name, TasToolProcessingType processingType, int slot); + TasTool(const char *name, TasToolProcessingType processingType, TasToolBulkType bulkType, int slot); ~TasTool(); const char *GetName() const { return name; } inline int GetSlot() const { return slot; } bool CanProcess(TasToolProcessingType type) const { return this->processingType == type; } + TasToolBulkType GetBulkType() const { return this->bulkType; } virtual std::shared_ptr ParseParams(std::vector) = 0; virtual void Apply(TasFramebulk &fb, const TasPlayerInfo &pInfo) = 0; @@ -64,8 +77,8 @@ class TasToolWithParams : public TasTool { Params params; public: - TasToolWithParams(const char *name, TasToolProcessingType processingType, int slot) - : TasTool(name, processingType, slot) {}; + TasToolWithParams(const char *name, TasToolProcessingType processingType, TasToolBulkType bulkType, int slot) + : TasTool(name, processingType, bulkType, slot) {}; virtual void Reset() { this->paramsPtr = std::make_shared(); diff --git a/src/Features/Tas/TasTools/AbsoluteMoveTool.hpp b/src/Features/Tas/TasTools/AbsoluteMoveTool.hpp index dcf03170..765b7b69 100644 --- a/src/Features/Tas/TasTools/AbsoluteMoveTool.hpp +++ b/src/Features/Tas/TasTools/AbsoluteMoveTool.hpp @@ -20,7 +20,7 @@ class AbsoluteMoveTool : public TasToolWithParams { private: AbsoluteMoveToolParams amParams; public: - AbsoluteMoveTool(int slot) : TasToolWithParams("absmov", POST_PROCESSING, slot) {}; + AbsoluteMoveTool(int slot) : TasToolWithParams("absmov", POST_PROCESSING, MOVEMENT, slot) {}; virtual std::shared_ptr ParseParams(std::vector); virtual void Apply(TasFramebulk &fb, const TasPlayerInfo &pInfo); }; diff --git a/src/Features/Tas/TasTools/AutoAimTool.hpp b/src/Features/Tas/TasTools/AutoAimTool.hpp index 56cbfe8f..d7df54ac 100644 --- a/src/Features/Tas/TasTools/AutoAimTool.hpp +++ b/src/Features/Tas/TasTools/AutoAimTool.hpp @@ -34,7 +34,7 @@ class AutoAimTool : public TasToolWithParams { int elapsedTicks; public: AutoAimTool(int slot) - : TasToolWithParams("autoaim", POST_PROCESSING, slot) + : TasToolWithParams("autoaim", POST_PROCESSING, VIEWANGLES, slot) , elapsedTicks(0) {} virtual std::shared_ptr ParseParams(std::vector); diff --git a/src/Features/Tas/TasTools/CheckTool.hpp b/src/Features/Tas/TasTools/CheckTool.hpp index 5b50de08..5700a763 100644 --- a/src/Features/Tas/TasTools/CheckTool.hpp +++ b/src/Features/Tas/TasTools/CheckTool.hpp @@ -31,7 +31,7 @@ struct CheckToolParams : public TasToolParams { class CheckTool : public TasToolWithParams { public: CheckTool(int slot) - : TasToolWithParams("check", POST_PROCESSING, slot) + : TasToolWithParams("check", POST_PROCESSING, META, slot) {} virtual std::shared_ptr ParseParams(std::vector); diff --git a/src/Features/Tas/TasTools/CommandTool.hpp b/src/Features/Tas/TasTools/CommandTool.hpp index 9f58fe6e..a8b300e6 100644 --- a/src/Features/Tas/TasTools/CommandTool.hpp +++ b/src/Features/Tas/TasTools/CommandTool.hpp @@ -16,7 +16,7 @@ struct CommandToolParams : public TasToolParams { class CommandTool : public TasToolWithParams { public: CommandTool(int slot) - : TasToolWithParams("cmd", PRE_PROCESSING, slot) { + : TasToolWithParams("cmd", PRE_PROCESSING, COMMANDS, slot) { } virtual std::shared_ptr ParseParams(std::vector); diff --git a/src/Features/Tas/TasTools/DecelTool.hpp b/src/Features/Tas/TasTools/DecelTool.hpp index e8339e68..24e1c5e1 100644 --- a/src/Features/Tas/TasTools/DecelTool.hpp +++ b/src/Features/Tas/TasTools/DecelTool.hpp @@ -17,7 +17,7 @@ struct DecelParams : public TasToolParams { class DecelTool : public TasToolWithParams { public: DecelTool(int slot) - : TasToolWithParams("decel", POST_PROCESSING, slot) {} + : TasToolWithParams("decel", POST_PROCESSING, MOVEMENT, slot) {} virtual std::shared_ptr ParseParams(std::vector); virtual void Apply(TasFramebulk &fb, const TasPlayerInfo &pInfo); }; diff --git a/src/Features/Tas/TasTools/DuckTool.hpp b/src/Features/Tas/TasTools/DuckTool.hpp index eab6ec51..cbc3f5dc 100644 --- a/src/Features/Tas/TasTools/DuckTool.hpp +++ b/src/Features/Tas/TasTools/DuckTool.hpp @@ -16,7 +16,7 @@ struct DuckToolParams : public TasToolParams { class DuckTool : public TasToolWithParams { public: DuckTool(int slot) - : TasToolWithParams("duck", PRE_PROCESSING, slot) { + : TasToolWithParams("duck", PRE_PROCESSING, BUTTONS, slot) { } virtual std::shared_ptr ParseParams(std::vector); diff --git a/src/Features/Tas/TasTools/JumpTool.hpp b/src/Features/Tas/TasTools/JumpTool.hpp index 1029a20d..e385dbe9 100644 --- a/src/Features/Tas/TasTools/JumpTool.hpp +++ b/src/Features/Tas/TasTools/JumpTool.hpp @@ -16,7 +16,7 @@ struct JumpToolsParams : public TasToolParams { class JumpTool : public TasToolWithParams { public: JumpTool(int slot, bool automatic) - : TasToolWithParams(automatic ? "autojump" : "jump", POST_PROCESSING, slot) + : TasToolWithParams(automatic ? "autojump" : "jump", POST_PROCESSING, BUTTONS, slot) , automatic(automatic) { } diff --git a/src/Features/Tas/TasTools/LookTool.hpp b/src/Features/Tas/TasTools/LookTool.hpp index c4332ebb..222bf30d 100644 --- a/src/Features/Tas/TasTools/LookTool.hpp +++ b/src/Features/Tas/TasTools/LookTool.hpp @@ -23,7 +23,7 @@ class LookTool : public TasToolWithParams { int remainingTime; public: LookTool(int slot) - : TasToolWithParams("look", PRE_PROCESSING, slot) {}; + : TasToolWithParams("look", PRE_PROCESSING, VIEWANGLES, slot) {}; virtual std::shared_ptr ParseParams(std::vector); virtual void Apply(TasFramebulk &fb, const TasPlayerInfo &pInfo); }; diff --git a/src/Features/Tas/TasTools/MoveTool.hpp b/src/Features/Tas/TasTools/MoveTool.hpp index 7491a98f..ae36f1fc 100644 --- a/src/Features/Tas/TasTools/MoveTool.hpp +++ b/src/Features/Tas/TasTools/MoveTool.hpp @@ -19,7 +19,7 @@ struct MoveToolParams : public TasToolParams { class MoveTool : public TasToolWithParams { public: MoveTool(int slot) - : TasToolWithParams("move", PRE_PROCESSING, slot) {}; + : TasToolWithParams("move", PRE_PROCESSING, MOVEMENT, slot) {}; virtual std::shared_ptr ParseParams(std::vector); virtual void Apply(TasFramebulk &fb, const TasPlayerInfo &pInfo); }; diff --git a/src/Features/Tas/TasTools/SetAngleTool.hpp b/src/Features/Tas/TasTools/SetAngleTool.hpp index a7eef915..735b95ad 100644 --- a/src/Features/Tas/TasTools/SetAngleTool.hpp +++ b/src/Features/Tas/TasTools/SetAngleTool.hpp @@ -29,7 +29,7 @@ class SetAngleTool : public TasToolWithParams { int elapsedTicks; public: SetAngleTool(int slot) - : TasToolWithParams("setang", POST_PROCESSING, slot) + : TasToolWithParams("setang", POST_PROCESSING, VIEWANGLES, slot) , elapsedTicks(0) {} virtual std::shared_ptr ParseParams(std::vector); diff --git a/src/Features/Tas/TasTools/ShootTool.hpp b/src/Features/Tas/TasTools/ShootTool.hpp index 64489741..04f30c78 100644 --- a/src/Features/Tas/TasTools/ShootTool.hpp +++ b/src/Features/Tas/TasTools/ShootTool.hpp @@ -22,7 +22,7 @@ struct ShootToolParams : public TasToolParams { class ShootTool : public TasToolWithParams { public: ShootTool(int slot) - : TasToolWithParams("shoot", PRE_PROCESSING, slot) {} + : TasToolWithParams("shoot", PRE_PROCESSING, BUTTONS, slot) {} virtual std::shared_ptr ParseParams(std::vector); virtual void Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo); diff --git a/src/Features/Tas/TasTools/StopTool.cpp b/src/Features/Tas/TasTools/StopTool.cpp index 58b6eb42..5876768c 100644 --- a/src/Features/Tas/TasTools/StopTool.cpp +++ b/src/Features/Tas/TasTools/StopTool.cpp @@ -11,6 +11,10 @@ void StopTool::Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo) { if (params.enabled) { for (TasTool *tool : TasTool::GetList(pInfo.slot)) { + if ((tool->GetBulkType() & params.typesToDisableMask) == 0) { + continue; + } + bool toolJustRequested = false; for (auto toolCmd : bulk.toolCmds) { if (tool == toolCmd.tool) { @@ -28,8 +32,27 @@ void StopTool::Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo) { } std::shared_ptr StopTool::ParseParams(std::vector vp) { - if (vp.size() > 0) - throw TasParserArgumentCountException(this, vp.size()); + uint32_t typesToDisableMask = TasToolBulkType::NONE; + + if (vp.size() == 0) { + typesToDisableMask = TasToolBulkType::ALL_TYPES_MASK; + } + + for (const auto &arg : vp) { + if (arg == "movement" || arg == "moving" || arg == "move") { + typesToDisableMask |= TasToolBulkType::MOVEMENT; + } else if (arg == "viewangles" || arg == "angles" || arg == "ang" || arg == "looking" || arg == "look") { + typesToDisableMask |= TasToolBulkType::VIEWANGLES; + } else if (arg == "buttons" || arg == "inputs" || arg == "pressing" || arg == "press") { + typesToDisableMask |= TasToolBulkType::BUTTONS; + } else if (arg == "commands" || arg == "cmd") { + typesToDisableMask |= TasToolBulkType::COMMANDS; + } else if (arg == "all" || arg == "everything") { + typesToDisableMask = TasToolBulkType::ALL_TYPES_MASK; + } else { + throw TasParserArgumentException(this, arg); + } + } - return std::make_shared(true); + return std::make_shared(true, typesToDisableMask); } diff --git a/src/Features/Tas/TasTools/StopTool.hpp b/src/Features/Tas/TasTools/StopTool.hpp index 33a7935e..486b16f7 100644 --- a/src/Features/Tas/TasTools/StopTool.hpp +++ b/src/Features/Tas/TasTools/StopTool.hpp @@ -2,18 +2,21 @@ #include "../TasTool.hpp" struct StopToolParams : public TasToolParams { + uint32_t typesToDisableMask; + StopToolParams() : TasToolParams() { } - StopToolParams(bool enabled) - : TasToolParams(enabled) { + StopToolParams(bool enabled, uint32_t typesToDisableMask) + : TasToolParams(enabled) + , typesToDisableMask(typesToDisableMask) { } }; class StopTool : public TasToolWithParams { public: StopTool(int slot) - : TasToolWithParams("stop", POST_PROCESSING, slot) { + : TasToolWithParams("stop", POST_PROCESSING, META, slot) { } virtual std::shared_ptr ParseParams(std::vector); diff --git a/src/Features/Tas/TasTools/StrafeTool.hpp b/src/Features/Tas/TasTools/StrafeTool.hpp index e1d28f7c..fc356f8a 100644 --- a/src/Features/Tas/TasTools/StrafeTool.hpp +++ b/src/Features/Tas/TasTools/StrafeTool.hpp @@ -76,7 +76,7 @@ class AutoStrafeTool : public TasToolWithParams { int lastTurnDir = 0; public: AutoStrafeTool(int slot) - : TasToolWithParams("strafe", POST_PROCESSING, slot) {}; + : TasToolWithParams("strafe", POST_PROCESSING, MOVEMENT, slot) {}; virtual std::shared_ptr ParseParams(std::vector); virtual void Apply(TasFramebulk &fb, const TasPlayerInfo &pInfo); diff --git a/src/Features/Tas/TasTools/UseTool.hpp b/src/Features/Tas/TasTools/UseTool.hpp index fcc9544d..eaf131de 100644 --- a/src/Features/Tas/TasTools/UseTool.hpp +++ b/src/Features/Tas/TasTools/UseTool.hpp @@ -16,7 +16,7 @@ struct UseToolParams : public TasToolParams { class UseTool : public TasToolWithParams { public: UseTool(int slot) - : TasToolWithParams("use", PRE_PROCESSING, slot) {} + : TasToolWithParams("use", PRE_PROCESSING, BUTTONS, slot) {} virtual std::shared_ptr ParseParams(std::vector); virtual void Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo); diff --git a/src/Features/Tas/TasTools/ZoomTool.hpp b/src/Features/Tas/TasTools/ZoomTool.hpp index 241a5dfb..bc815cc4 100644 --- a/src/Features/Tas/TasTools/ZoomTool.hpp +++ b/src/Features/Tas/TasTools/ZoomTool.hpp @@ -22,7 +22,7 @@ struct ZoomToolParams : public TasToolParams { class ZoomTool : public TasToolWithParams { public: ZoomTool(int slot) - : TasToolWithParams("zoom", PRE_PROCESSING, slot) { + : TasToolWithParams("zoom", PRE_PROCESSING, BUTTONS, slot) { } virtual std::shared_ptr ParseParams(std::vector); From 99b848fa9dd5c7e3bdc68e0386a6c6d457f7ebb6 Mon Sep 17 00:00:00 2001 From: Chris Hausky Date: Wed, 17 Dec 2025 15:29:17 +0100 Subject: [PATCH 09/15] fix: ensure proper shoot tool functionality --- src/Features/Tas/TasTools/ShootTool.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Features/Tas/TasTools/ShootTool.hpp b/src/Features/Tas/TasTools/ShootTool.hpp index 04f30c78..93d7687a 100644 --- a/src/Features/Tas/TasTools/ShootTool.hpp +++ b/src/Features/Tas/TasTools/ShootTool.hpp @@ -22,7 +22,7 @@ struct ShootToolParams : public TasToolParams { class ShootTool : public TasToolWithParams { public: ShootTool(int slot) - : TasToolWithParams("shoot", PRE_PROCESSING, BUTTONS, slot) {} + : TasToolWithParams("shoot", POST_PROCESSING, BUTTONS, slot) {} virtual std::shared_ptr ParseParams(std::vector); virtual void Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo); From 74bcad5ec01a0ff7f3f31d310249790f39864f71 Mon Sep 17 00:00:00 2001 From: Chris Hausky Date: Thu, 18 Dec 2025 19:01:24 +0100 Subject: [PATCH 10/15] feat: add easier way to interrupt setang tas tool --- src/Features/Tas/TasTools/SetAngleTool.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Features/Tas/TasTools/SetAngleTool.cpp b/src/Features/Tas/TasTools/SetAngleTool.cpp index 43b7e7b8..67b7c84a 100644 --- a/src/Features/Tas/TasTools/SetAngleTool.cpp +++ b/src/Features/Tas/TasTools/SetAngleTool.cpp @@ -55,6 +55,11 @@ std::shared_ptr SetAngleTool::ParseParams(std::vector(); + } + if (vp[0] == "ahead") { if (vp.size() > 3) throw TasParserArgumentCountException(this, vp.size()); target = vp[0]; From 6ccdd6b0594a2ab1768819f478d52fab8781b5db Mon Sep 17 00:00:00 2001 From: Chris Hausky Date: Thu, 18 Dec 2025 19:01:46 +0100 Subject: [PATCH 11/15] docs: update and improve TAS documentation --- docs/p2tas.md | 180 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 117 insertions(+), 63 deletions(-) diff --git a/docs/p2tas.md b/docs/p2tas.md index f6f0c2d4..94f41516 100644 --- a/docs/p2tas.md +++ b/docs/p2tas.md @@ -1,30 +1,62 @@ # P2TAS Documentation +## Table of Contents +1. [Introduction](#introduction) +2. [SAR's TAS environment specification](#sars-tas-environment-specification) +3. [`.p2tas` file structure](#p2tas-file-structure) +4. [Automation tools](#automation-tools) +8. [Version history](#version-history) + + ## Introduction -P2TAS is a scripting language used for defining a sequence of time-stamped actions to be executed by a virtual controller within Portal 2 (and other Portal 2-based games supported by SAR), for the purpose of creating tool-assisted speedruns. +P2TAS is a scripting language and a set of tools used for defining and playing back a sequence of time-stamped actions to be executed within Portal 2 (and other Portal 2-based games supported by SAR), for the purpose of creating tool-assisted speedruns. + +The following document serves as a documentation of SAR's behaviour for the purpose of TAS playback, as well as P2TAS scripting language and features available through it. + +## SAR's TAS environment specification + +### Script playback +Tool-assisted speedrunning in Portal 2 is done through a playback of pre-programmed sequence of inputs declared by our custom P2TAS scripting language. SAR is handling both interpretation of these scripts and their playback, which includes injection of player inputs and their proper timing. Although SAR is filled with other features, some of which make TAS creation process easier, they're not required for a successful TAS script playback. + +### The virtual controller +SAR implements a virtual controller by injecting code into the Steam Controller's input fetching function. In raw script playback, this is the only injection point required for TAS script playback, ensuring minimal interference with game logic. + +This controller supports: -The following document will explain the P2TAS syntax, as well as technical details of how P2TAS files are processed and executed by the plugin. +- **Digital inputs**: jump, crouch, use, zoom, primary/secondary attack (blue and orange portal) +- **Analog inputs**: 2D movement and view angle controls with floating-point precision +- **Console commands**: Arbitrary command execution at specific ticks (as a way to replicate key binds) -## TAS Virtual Controller specifications +The virtual controller has effectively instant response time, allowing different inputs on consecutive ticks despite the existence of `sv_alternateticks` - something confirmed to be replicable with physical hardware (Steam Controller with ~1ms response time). -SAR uses its own virtual controller to execute actions defined by P2TAS files. It was implemented by injecting custom code into a Steam Controller's input processing function. +### Automation tools and raw playback +SAR's TAS environment includes a set of automation tools (like autojump, autoaim or autostrafer) that calculate complex input sequences based on real-time state of the game. In order for them to work, SAR injects into additional game functions to read player's state properly, as they're not yet available at the input fetching stage. -The controller allows the following input methods: +Because of that, two distinct playback modes exist: +- **Tools playback** - scripts are processed with all tools specified in the script. This is a default playback mode and should be used in a creation process of a TAS script. It is not guaranteed to procude fully legitimate gameplay. By default, tools playback should produce a raw script, automatically saved as a file with `_raw.p2tas` suffix. -- digital inputs (jumping, crouching, "use" action, zooming, primary/secondary attack), -- floating-point precision analog inputs (movement and camera view), -- console command execution. +- **Raw playback** - only pre-calculated inputs are interpreted with no tools processing. Every script with `_raw.p2tas` suffix is expected to be a raw script and will be played as such. Raw scripts are guaranteed to produce legitimate gameplay, as they're not using any other extra input injection methods other than the virtual controller, so they should be used for final verification and demo recording. -All of inputs are executed with tick-based precision, even despite the alternate-ticks mechanism. +### RNG Manipulation -The following features of the virtual controller has been proven possible to replicate by physical hardware. +Portal 2 TASing is famously annoying due to randomness - both game-based and physics based - which can lead to inconsistent results from a playback of a single script. In the past, there were several attempts of artificially manipulating RNG to resolve script playback into a single possibility. So far, there's a limited amount of RNG manipulation available, mostly related to simple gameplay RNG like gel spread, catapulted prop torque and view punch. -## File structure +SAR can generate files storing details of RNG state using console command `sar_rng_save`, which then can be used in a P2TAS script by referencing this file in the header of the script to reproduce exact same RNG state. While this solution is not ideal in terms of legitimacy, it is not discouraged, as a final result is still based on state of the game that once existed. -Script written in P2TAS is stored as a text file with `.p2tas` extension, which is then interpreted by SourceAutoRecord plugin when a playback of said file is requested. +## `.p2tas` file structure -The file is expected to start with a header, and then be followed by at least one tickbulk. +Script written in P2TAS is stored as a text file with `.p2tas` extension. The file is expected to start with a header, and then be followed by at least one tickbulk. + +``` +version +start {next} {save/next/cm/now} +[rngmanip [path]] + + +[tickbulk] +... +``` The parser is case-sensitive. All keywords are lowercase. @@ -81,7 +113,7 @@ Everything except console commands and tool commands is persistent, meaning that 20>|0 0||| // stopped rotating, but still moving and ducking. ``` -Additionally, as shown above, tickbulk parts are optional. If persistent parts are left empty, they will preserve the behaviour of the same part in last defined framebulk. This also means that pipelines are optional as well, and the following framebulk is valid: +Additionally, as shown above, tickbulk parts are optional. If persistent parts are left empty, they will preserve the behaviour of the same part in last defined tickbulk. This also means that pipelines are optional as well, and the following tickbulk is valid: ```cs 69> // yup, that's valid @@ -140,64 +172,76 @@ P2TAS supports single-line comments, starting with `//` and running until the en Whitespaces are used to separate tokens and are useless beyond this point. This means you can put as many spaces, newlines and indents as you want, anywhere you want, as long as it doesn't actually affect the tokens themselves. -## Automation Tools +## Automation tools To make certain actions easier to perform, SourceAutoRecord includes automation tools which can be controlled with tool commands in a tickbulk. These tools fetch game data in real time to produce appropriate controller inputs. -Tools work every tick, meaning that you only have to set its parameters once with tool commands for a continuous behaviour. Consequently, if a tool has a continuous behaviour that never ends by itself, it has to be turned off by sending an appropriate tool command. +Tools can have a continuous behaviour, affecting player inputs every tick, meaning that you only have to set its parameters once with tool commands for it to work. Consequently, if a tool has a continuous behaviour that never ends by itself, it has to be turned off by sending an appropriate tool command. Below is a complete list of automation tools supported by SourceAutoRecord. ### `strafe` tool ```cs -strafe {vec/ang/veccam/off} {max/keep/ups} {forward/forwardvel/left/right/deg} {nopitchlock} {letspeedlock} +strafe ``` -A tool for automated strafing. It will attempt to gain the biggest acceleration in given tick, while trying to change movement direction towards the specified one. +A tool for automated strafing. It's the most complicated automation tool in P2TAS. It will attempt to find the most optimal movement direction which satisfy criteria specified by parameter (whether it's greatest acceleration towards specified direction or something else) and apply it to player's inputs. -Autostrafe tool will always prioritize accelerating first, unless there's no longer need for accelerating. Since there are almost always two ideal movement directions for the perfect acceleration, it will choose direction based on currently set target direction. If that direction has been reached, the autostrafer will toggle into a "line mode", where it will attempt its best to follow a straight line towards set target direction. +Strafe tool accepts any non-zero number of parameters in unspecified order. Here's a list of behaviours which can be manipulated with those parameters: -Strafe tool accepts any number of parameters. Here's a list of valid parameters: - -- Switching strafe modes: +- **Input type** - The goal of autostrafer is to find a movement direction. However, this direction can be achieved by the player in multiple ways. These paramaters can control the input mode: - `vec` (default) - vectorial strafing - using analog movement input to control the direction of movement, while view angles remains mostly unaffected. - - `ang` - angular strafing - using view angle input to control the direction of movement, while the movement is oscillated between left and right. Useful for strafing on propulsion gel. - - `veccam` - same as vectorial strafing, but the view angles will be modified to point along current horizontal velocity. - - `off` - disables autostrafer. -- Manipulating target speed: - - `max` (default) - always accelerates as much as possible + - `ang` - angular strafing - using view angle input to control the direction of movement, while the movement is oscillated between left and right. Useful for strafing on propulsion gel, as moving on it causes your side movement acceleration to be scaled down. + - `veccam` - same as vectorial strafing, but the view angles will be modified to point along current horizontal velocity. Occasionally useful for better visual presentation. + +- **Target velocity** - By default, the tool will attempt to maximize acceleration for every tick. Same behaviour occurs when target velocity is set to be higher than a current one. If they're equal, the tool will attempt to strafe towards target direction while keeping the same velocity, taking ground friction into consideration. If target velocity is lower, it will try to maximize velocity direction change in every tick, effectively making velocity slightly lower (for better deceleration, consider using `decel` tool.). These paramaters can control target velocity: + - `max` (default) - always accelerates as much as possible. + - `min` - sets target horizontal velocity to 0 units per second. - `keep` - attempts to keep horizontal speed from the moment of sending the tool command. - - `ups` - attempts to reach given speed, and then keep it. It will either accelerate as efficiently as possible, or decelerate by finding the most efficient rotation towards the target direction as possible (doesn't decelerate as fast as possible. For that, use `decel`). -- Manipulating target direction: + - `ups` - sets target horizontal velocity to given value, in units per second. + +- **Target direction** - Because of the very nature of strafing, there are two possible strafe directions. Every tick, the tool will decide between those two directions, preferring the one that brings player's velocity direction closer to target direction given through parameters. After the target is reached, the tool will define a straight line it will try to follow until external factors (like colliding with something) will cause it to deviate too far from it, in which case, it'll fall back to default mode. These parameters can control target direction: - `forward` - strafes towards the currently faced direction. - `forwardvel` (default) - strafes towards current velocity direction. - `left` - forces autostrafer to always strafe left. - `right` - forces autostrafer to always strafe right. - - `deg` - sets target direction based on given angle. It corresponds to a look direction for given yaw angle. -- Additional behavior flags: - - `nopitchlock` - prevents the autostrafer from locking view angle pitch in a range between -30 and 30 degrees, which is the range where pitch doesn't affect player's movement. Using this flag and going past this range makes strafing sub-optimal, but allows for greater aim range. + - `deg` - sets target direction based on given angle in degrees. It corresponds to a look direction for given yaw angle (second angle number in `cl_showpos`) + +- **Extra behaviour** - Autostrafer has several internal mechanisms which work around some game's limitations in order for a tool to function properly. These behaviours can be tweaked with these parameters: + - `nopitchlock` - prevents the autostrafer from locking view pitch angle in a range between -30 and 30 degrees, which is the range where pitch angle doesn't affect player's movement. Using this flag and going past this range makes strafing sub-optimal, but allows for greater aim range. - `letspeedlock` - allows the strafer to become soft-speedlocked when strafing above 300ups midair. This sacrifices speed, but allows slightly better turning towards a diagonal direction when affected by a speedlock. -Parameters marked as default define the default behavior of the tool (as in, the behavior which occurs when no other parameters in given category are given). +**Movement input manipulation** - For more advanced usage, it is sometimes necessary to make a tiny adjustment of the movement input vector, even if it means sacrificing strafe accuracy. These parameters can be used for that: + - `usdeg`, `osdeg` - sets an offset, in degrees, for a calculated movement direction. `usdeg` will understrafe, bringing movement vector closer to velocity vector and causing weaker turn, while `osdeg` will overstrafe, bringing movement vector further away from velocity vector and causing bigger turn. + - `us`, `os` - controls understrafing and overstrafing, similarly to `usdeg` and `osdeg`, however, these accept scalar values, where unit value for both (`1us` and `1os`) are equivalent of bringing movement direction yielding most acceleration to the closest direction yielding no acceleration in both ways. + - `` - using suffix-less number parameter is treated as an input strenght - movement input vector will be scaled by that number. + + +Additional notes: +- Parameters marked as default define the default behavior of the tool (as in, the behavior which occurs when no other parameters in given category are given). +- The tool is continuous, meaning it has to be disabled with `off` parameter. It can also be enabled with `on` parameter, which will use default settings. +- When the tool is about to reach both target velocity and target direction, it will calculate perfect movement input to reach them and then silently stop, but will continue once any of these change. Examples of usage: -- `strafe max right` - vectorial-strafes with the greatest possible acceleration clockwise. +- `strafe on` - enables autostrafer with default settings (vectorial strafing with the greatest acceleration towards current looking direction) +- `strafe right` - vectorial-strafes with the greatest possible acceleration clockwise. - `strafe ang forwardvel` - angular-strafes with the greatest possible acceleration towards the current velocity direction. - `strafe 30deg max nopitchlock` - vectorial-strafes with the greatest possible acceleration towards 30 degrees without locking pitch angle between -30 and 30 degrees. +- `strafe 255ups 0.1os` - vectorial-strafes to reach velocity of 255ups ### `autojump` tool ```cs -autojump {on/ducked/duck/off} +autojump {on/unducked/ducked/duck/off} ``` Autojump tool, as the name suggests, will automatically manipulate the jump input in order to perform tick-perfect jumps from the ground, avoiding any ground friction. It's normally used in combination with the `strafe` tool to achieve perfect bunnyhopping. This tool accepts one of the following parameters: -- `on` - enables autojumping. +- `on` or `unducked` - enables autojumping. - `ducked` or `duck` - enables ducked autojumping, which holds crouch button when jumping, giving a small boost in the jump height. - `off` - disables autojumping. @@ -209,9 +253,9 @@ absmov {deg/off} [scale] This tool allows to move in an absolute direction defined by an angle in degrees (which corresponds to a look direction for a given yaw angle). -It takes at most two parameters. First parameter is expected to be a number defining an angle of movement as described above. The second parameter is optional, and defines a scalar value of movement analog vector (as in, how much to move in a given direction) in a range from 0.0 to 1.0. +It takes at most two parameters. First parameter is expected to be a number defining an angle of movement as described above. The second parameter is an optional input strenght, between 0.0 and 1.0 (default). -The tool can be disabled by passing `off` as a parameter. +The tool is continuous can be disabled by passing `off` as a parameter. ### `setang` tool @@ -221,13 +265,15 @@ setang { /ahead} [time] [easing_type] Similarly to a `setang` console command, this tool can set view angles to a desired pitch and yaw values. You can also use `ahead` as a parameter to set the view angles to look the direction you're currently moving. In addition, this tool allows a smooth interpolation from current view angles to desired ones. -The tool takes at least one parameter when looking `ahead`, or two which describe the desired pitch and yaw angle, in this order. Optional next parameter describes a time in ticks for how long the view angle should be interpolated to desired angles before reaching them. Optional final parameter describes an easing function of interpolation: +The tool takes at least one parameter when looking `ahead`, or two which describe the desired pitch and yaw angle, in this order. The following optional parameter describes a time in ticks for how long the view angle should be interpolated to desired angles before reaching them. The interpolation takes external factors into consideration (like passing through portals) and will always end up on given absolute angles. Optional final parameter describes an easing function of interpolation: - `linear` - interpolates linearly. Default if no interpolation method is given. - `sine` or `sin` - interpolates using a sine easing. - `cubic` - interpolates using a cubic easing. - `exp` or `exponential` - interpolates using an exponential easing. +When time parameter is used, this tool is continuous. It will disable itself once the angle has been reached, but it can be interrupted by using `off` parameter or with another use of `setang` command. + Examples of usage: - `setang ahead` - looks the current direction of movement instantly. @@ -244,8 +290,9 @@ autoaim {ent / /off} [time] [easing_type] Works similar to `setang` tool, except the tool is continuously aiming towards given entity or point until the tool is disabled. If first parameter is not `ent`, it expects the first three parameters to be X, Y and Z coordinates of a point to aim towards. Otherwise, it expects a second parameter to be a string representing an entity selector (an entity classname or targetname, optionally followed by a square-bracket array accessor describing which entity in a list of ones matching the selector to use for this tool). -Again, similarly to `setang` tool, it takes interpolation time and easing type as optional parameters. -The tool can be disabled by passing `off` as a first parameter. +Similarly to `setang` tool, it takes interpolation time and easing type as optional parameters. + +This tool is always continuous, even after finishing interpolation. It can be disabled by passing `off` as a first parameter. Example of usage: @@ -259,15 +306,24 @@ Example of usage: decel {/off} ``` -This tool decelerates the player as fast as possible towards the speed, in units per second, specified by a first parameter. It can be disabled any time by passing `off` as a parameter instead of speed number. +This tool decelerates the player as fast as possible towards the desired horizontal velocity, in units per second. It's a continuous tool that disables itself once target velocity has been reached, but it can be disabled any time by passing `off` as a parameter. ### `check` tool ```cs -check {pos } {posepsilon } {ang } {angepsilon } {holding [entity_selector]} +check ``` -This tool can be used to perform a check on player's position or view angles. If position or view angles differ by more than given epsilon, the TAS script is restarted. It can also be used to check if the player is holding an item, with an optional entity selector. The tool will restart until the number of automatic restarts surpasses the number defined by the `sar_tas_check_max_replays` console variable. +This tool can be used to perform a check on player's properties at the tick of activation to ensure specific outcome of a TAS script (as a way to work around inconsistency issues). When given condition fails, it automatically restarts the script until condition is passed or it reaches maximum number of replayes (which can be tweaked with `sar_tas_check_max_replays` console variable.) + +Here's a list of possible conditions: +- `pos ` - checks if player is at given world coordinates. +- `posepsilon ` - defines maximum allowed distance from player to given coordinates in units (0.5 by default). +- `ang ` - checks if the player aims towards given angles (in degrees). +- `angepsilon ` - defines maximum allowed distance from current player angles to given ones, in degrees (0.2 by default). +- `vel ` - checks if player's accumulated velocity from given components is equal to given value. +- `velepsilon ` - defines maximum allowed difference between player's accumulated velocity from components given in `vel` parameter and the speed value defined in that parameter (1.0 by default). +- `holding [entity_selector]` - checks if player is holding a prop. Optionally, accepts a selector to check for a specific entity. Behaviour of the selector is similar to the one from [`autoaim` tool](#autoaim-tool). ### `duck` tool @@ -307,7 +363,7 @@ A tool-based alternative to digital inputs for shooting blue and orange portal. cmd ``` -A tool-based alternative to console command part of a tickbulk. It will simply execute the command given as a parameter (whitespaces allowed). +A tool-based alternative to console command part of a tickbulk. It will simply execute the command given as a parameter, whitespace allowed. It does not allow multiple commands, as command separator (`;`) is also used as a command separator for TAS tools. Use multiple `cmd` commands or a command tickbulk part instead. ### `move` tool @@ -333,28 +389,17 @@ Additionally, time parameter in ticks can be given, which will disable the tool ### `stop` tool ```cs -stop +stop [types] ``` -Stops all tools that were activated in the past tickbulks. - -## Raw Scripts - -Automation tools fetch data from the game in real-time in order to produce their inputs. However, Portal 2 uses an alternate ticks system, which results in user inputs being fetched twice before using them to process two ticks at once. This means that, when creating user inputs for a second tick in pair, we will have an outdated information about the game's state, preventing automation tools from creating accurate inputs. - -The solution to this problem we've decided upon is to allow less legitimate input injection method for automation tools. However, in order to keep the legitimacy of our system, we've created a system for generating raw scripts. - -Raw scripts are P2TAS scripts which do not contain any automation tools. When P2TAS script with automation tools is played back, a raw script is generated based on inputs from automation tools. - -Raw scripts are usually identified by a `_raw` suffix in the script's file name. When it's present, TAS script player will ensure that no automation tools are being executed. - -## RNG Manipulation - -Portal 2 TASing is famously annoying due to randomness - both game-based and physics based. There are currently efforts of artificially manipulating RNG to resolve script playback into a single possibility. So far, there's a limited amount of RNG manipulation available, mostly related to gel spread. +Stops all tools that were activated in the past tickbulks. Optionally, it accepts any number of parameters specifying types of tools to disable. Here's a list of available types: +- `movement` (or `moving`, `move`) - disables tools related to movement +- `viewangles` (or `angles`, `ang`, `looking`, `look`) - disables tools related to changing your angle (this does NOT include `strafe` tool) +- `buttons` (or `inputs`, `pressing`, `press`) - disables tools related to pressing digital inputs +- `all` (or `everything`) - default behaviour, disables every tool. -SAR can generate files storing details of RNG state using console command `sar_rng_save`, which then can be used in a P2TAS script by referencing this file in the header of the script to reproduce exact same RNG state. -## Version History +## Version history - Version 1: - Initial release. @@ -377,3 +422,12 @@ SAR can generate files storing details of RNG state using console command `sar_r - Pitchlock to the correct sign. - Version 7: - Fix autostrafer air control limit with `sar_aircontrol` set. +- Version 8: + - Improve autostrafer to lock onto target speed and direction instead of wiggling. + - Improve autostrafer to handle soft speedcap better. + - Improve autostrafer to respect pitch while keeping velocity in `nopitchlock` mode. + - Fix `decel` tool not respecting jump-based tools. +- Version 9: + - Fix `use` tool by processing some tools during input fetching. + +Last updated during version 9 release. From ba855f30c5f3af27b9b636c8e13f4d0419742f13 Mon Sep 17 00:00:00 2001 From: Chris Hausky Date: Thu, 18 Dec 2025 19:19:48 +0100 Subject: [PATCH 12/15] docs: fix mistakes in tas documentation --- docs/p2tas.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/p2tas.md b/docs/p2tas.md index 94f41516..f74cfbed 100644 --- a/docs/p2tas.md +++ b/docs/p2tas.md @@ -34,7 +34,7 @@ The virtual controller has effectively instant response time, allowing different SAR's TAS environment includes a set of automation tools (like autojump, autoaim or autostrafer) that calculate complex input sequences based on real-time state of the game. In order for them to work, SAR injects into additional game functions to read player's state properly, as they're not yet available at the input fetching stage. Because of that, two distinct playback modes exist: -- **Tools playback** - scripts are processed with all tools specified in the script. This is a default playback mode and should be used in a creation process of a TAS script. It is not guaranteed to procude fully legitimate gameplay. By default, tools playback should produce a raw script, automatically saved as a file with `_raw.p2tas` suffix. +- **Tools playback** - scripts are processed with all tools specified in the script. This is a default playback mode and should be used in a creation process of a TAS script. It is not guaranteed to produce fully legitimate gameplay. By default, tools playback should produce a raw script, automatically saved as a file with `_raw.p2tas` suffix. - **Raw playback** - only pre-calculated inputs are interpreted with no tools processing. Every script with `_raw.p2tas` suffix is expected to be a raw script and will be played as such. Raw scripts are guaranteed to produce legitimate gameplay, as they're not using any other extra input injection methods other than the virtual controller, so they should be used for final verification and demo recording. @@ -215,7 +215,7 @@ Strafe tool accepts any non-zero number of parameters in unspecified order. Here **Movement input manipulation** - For more advanced usage, it is sometimes necessary to make a tiny adjustment of the movement input vector, even if it means sacrificing strafe accuracy. These parameters can be used for that: - `usdeg`, `osdeg` - sets an offset, in degrees, for a calculated movement direction. `usdeg` will understrafe, bringing movement vector closer to velocity vector and causing weaker turn, while `osdeg` will overstrafe, bringing movement vector further away from velocity vector and causing bigger turn. - `us`, `os` - controls understrafing and overstrafing, similarly to `usdeg` and `osdeg`, however, these accept scalar values, where unit value for both (`1us` and `1os`) are equivalent of bringing movement direction yielding most acceleration to the closest direction yielding no acceleration in both ways. - - `` - using suffix-less number parameter is treated as an input strenght - movement input vector will be scaled by that number. + - `` - using suffix-less number parameter is treated as an input strength - movement input vector will be scaled by that number. Additional notes: @@ -229,7 +229,7 @@ Examples of usage: - `strafe right` - vectorial-strafes with the greatest possible acceleration clockwise. - `strafe ang forwardvel` - angular-strafes with the greatest possible acceleration towards the current velocity direction. - `strafe 30deg max nopitchlock` - vectorial-strafes with the greatest possible acceleration towards 30 degrees without locking pitch angle between -30 and 30 degrees. -- `strafe 255ups 0.1os` - vectorial-strafes to reach velocity of 255ups +- `strafe 255ups 0.1osdeg` - vectorial-strafes towards current looking direction to reach velocity of 255ups, while doing an overstrafe of 0.1 degrees. ### `autojump` tool @@ -253,7 +253,7 @@ absmov {deg/off} [scale] This tool allows to move in an absolute direction defined by an angle in degrees (which corresponds to a look direction for a given yaw angle). -It takes at most two parameters. First parameter is expected to be a number defining an angle of movement as described above. The second parameter is an optional input strenght, between 0.0 and 1.0 (default). +It takes at most two parameters. First parameter is expected to be a number defining an angle of movement as described above. The second parameter is an optional input strength, between 0.0 and 1.0 (default). The tool is continuous can be disabled by passing `off` as a parameter. From d7c93e27826406951f4963a082fb02edb635c358 Mon Sep 17 00:00:00 2001 From: Chris Hausky Date: Thu, 18 Dec 2025 19:32:28 +0100 Subject: [PATCH 13/15] fix: correct error handling in some tas tools --- src/Features/Tas/TasTools/AutoAimTool.cpp | 24 ++++++++++++--------- src/Features/Tas/TasTools/DuckTool.cpp | 2 +- src/Features/Tas/TasTools/LookTool.cpp | 2 +- src/Features/Tas/TasTools/SetAngleTool.cpp | 25 +++++++++++++--------- 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/Features/Tas/TasTools/AutoAimTool.cpp b/src/Features/Tas/TasTools/AutoAimTool.cpp index 83671008..c0d87bd6 100644 --- a/src/Features/Tas/TasTools/AutoAimTool.cpp +++ b/src/Features/Tas/TasTools/AutoAimTool.cpp @@ -22,23 +22,27 @@ std::shared_ptr AutoAimTool::ParseParams(std::vector throw TasParserArgumentCountException(this, args.size()); } - int ticks; - AngleToolsUtils::EasingType easingType; + int ticks = 1; + AngleToolsUtils::EasingType easingType = AngleToolsUtils::EasingType::Linear; // ticks unsigned ticksPos = usesEntitySelector ? 2 : 3; - try { - ticks = args.size() >= ticksPos + 1 ? std::stoi(args[ticksPos]) : 1; - } catch (...) { - throw TasParserArgumentException(this, "ticks", args[ticksPos]); + if (args.size() >= ticksPos + 1) { + try { + ticks = std::stoi(args[ticksPos]); + } catch (...) { + throw TasParserArgumentException(this, "ticks", args[ticksPos]); + } } // easing type unsigned typePos = usesEntitySelector ? 3 : 4; - try { - easingType = AngleToolsUtils::ParseEasingType(args.size() >= typePos + 1 ? args[typePos] : "linear"); - } catch (...) { - throw TasParserArgumentException(this, "interpolation", args[typePos]); + if (args.size() >= typePos + 1) { + try { + easingType = AngleToolsUtils::ParseEasingType(args[typePos]); + } catch (...) { + throw TasParserArgumentException(this, "interpolation", args[typePos]); + } } if (usesEntitySelector) { diff --git a/src/Features/Tas/TasTools/DuckTool.cpp b/src/Features/Tas/TasTools/DuckTool.cpp index 82edd683..e81ee7d7 100644 --- a/src/Features/Tas/TasTools/DuckTool.cpp +++ b/src/Features/Tas/TasTools/DuckTool.cpp @@ -29,7 +29,7 @@ void DuckTool::Apply(TasFramebulk &bulk, const TasPlayerInfo &pInfo) { std::shared_ptr DuckTool::ParseParams(std::vector vp) { if (vp.size() != 1) - TasParserArgumentCountException(this, vp.size()); + throw TasParserArgumentCountException(this, vp.size()); bool enabled = true; int time = INT32_MAX; diff --git a/src/Features/Tas/TasTools/LookTool.cpp b/src/Features/Tas/TasTools/LookTool.cpp index 69b449be..c9a75377 100644 --- a/src/Features/Tas/TasTools/LookTool.cpp +++ b/src/Features/Tas/TasTools/LookTool.cpp @@ -32,7 +32,7 @@ void LookTool::Apply(TasFramebulk &fb, const TasPlayerInfo &playerInfo) { std::shared_ptr LookTool::ParseParams(std::vector vp) { if (vp.size() == 0) { - throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), vp.size())); + throw TasParserArgumentCountException(this, vp.size()); } float pitchDelta = 0.0f; diff --git a/src/Features/Tas/TasTools/SetAngleTool.cpp b/src/Features/Tas/TasTools/SetAngleTool.cpp index 67b7c84a..a140dfe8 100644 --- a/src/Features/Tas/TasTools/SetAngleTool.cpp +++ b/src/Features/Tas/TasTools/SetAngleTool.cpp @@ -51,8 +51,8 @@ std::shared_ptr SetAngleTool::ParseParams(std::vector SetAngleTool::ParseParams(std::vector= (size_t)(i + 1) ? std::stoi(vp[i]) : 1; - } catch (...) { - throw TasParserArgumentException(this, "tick", vp[i]); + if (vp.size() >= i + 1) { + try { + ticks = std::stoi(vp[i]); + } catch (...) { + throw TasParserArgumentException(this, "tick", vp[i]); + } } // easing type - try { - easingType = AngleToolsUtils::ParseEasingType(vp.size() >= (size_t)(i + 2) ? vp[i + 1] : "linear"); - } catch (...) { - throw TasParserArgumentException(this, "interpolation", vp[i + 1]); + if (vp.size() >= i + 2) { + try { + easingType = AngleToolsUtils::ParseEasingType(vp[i + 1]); + } catch (...) { + throw TasParserArgumentException(this, "interpolation", vp[i + 1]); + } } + return std::make_shared(target, pitch, yaw, ticks, easingType); } From 64e539784a2ecb0973199f9ba77f723b85733b86 Mon Sep 17 00:00:00 2001 From: Chris Hausky Date: Thu, 18 Dec 2025 19:32:47 +0100 Subject: [PATCH 14/15] refactor: rename jumptoolsparams --- src/Features/Tas/TasTools/JumpTool.cpp | 4 ++-- src/Features/Tas/TasTools/JumpTool.hpp | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Features/Tas/TasTools/JumpTool.cpp b/src/Features/Tas/TasTools/JumpTool.cpp index 5c4b71b1..8793904a 100644 --- a/src/Features/Tas/TasTools/JumpTool.cpp +++ b/src/Features/Tas/TasTools/JumpTool.cpp @@ -48,7 +48,7 @@ void JumpTool::SetJumpInput(TasFramebulk &bulk, bool jump) { std::shared_ptr JumpTool::ParseParams(std::vector vp) { if (vp.size() == 0 && !this->automatic) { // allow 0 parameters for non-automated jump tool - return std::make_shared(true, false); + return std::make_shared(true, false); } if (vp.size() != 1) { @@ -67,5 +67,5 @@ std::shared_ptr JumpTool::ParseParams(std::vector vp throw TasParserArgumentException(this, vp[0]); } - return std::make_shared(enabled, ducked); + return std::make_shared(enabled, ducked); } diff --git a/src/Features/Tas/TasTools/JumpTool.hpp b/src/Features/Tas/TasTools/JumpTool.hpp index e385dbe9..3f0d6817 100644 --- a/src/Features/Tas/TasTools/JumpTool.hpp +++ b/src/Features/Tas/TasTools/JumpTool.hpp @@ -1,11 +1,11 @@ #pragma once #include "../TasTool.hpp" -struct JumpToolsParams : public TasToolParams { - JumpToolsParams() +struct JumpToolParams : public TasToolParams { + JumpToolParams() : TasToolParams() { } - JumpToolsParams(bool enabled, bool ducked) + JumpToolParams(bool enabled, bool ducked) : TasToolParams(enabled) , ducked(ducked) { } @@ -13,7 +13,7 @@ struct JumpToolsParams : public TasToolParams { bool ducked = false; }; -class JumpTool : public TasToolWithParams { +class JumpTool : public TasToolWithParams { public: JumpTool(int slot, bool automatic) : TasToolWithParams(automatic ? "autojump" : "jump", POST_PROCESSING, BUTTONS, slot) From f14fb479cf9d8c1f43fc1a5564b8aeccd6ae666d Mon Sep 17 00:00:00 2001 From: Chris Hausky Date: Thu, 18 Dec 2025 19:37:51 +0100 Subject: [PATCH 15/15] fix: prevent lack of return value in tas tools --- src/Features/Tas/TasPlayer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Features/Tas/TasPlayer.cpp b/src/Features/Tas/TasPlayer.cpp index 373ddc5b..211875ce 100644 --- a/src/Features/Tas/TasPlayer.cpp +++ b/src/Features/Tas/TasPlayer.cpp @@ -383,6 +383,7 @@ TasFramebulk &TasPlayer::RequestProcessedFramebulkAt(int slot, int tick) { } 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) {