diff --git a/docs/p2tas.md b/docs/p2tas.md index f6f0c2d4..f74cfbed 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 produce 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 strength - 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.1osdeg` - vectorial-strafes towards current looking direction to reach velocity of 255ups, while doing an overstrafe of 0.1 degrees. ### `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 strength, 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. 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/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.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 43c7c379..b3d2992f 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 @@ -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); @@ -25,4 +41,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/TasPlayer.cpp b/src/Features/Tas/TasPlayer.cpp index e1097241..211875ce 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"); @@ -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; @@ -154,8 +167,8 @@ void TasPlayer::Activate(TasPlaybackInfo info) { for (int slot = 0; slot < 2; ++slot) { playbackInfo.slots[slot].ClearGeneratedContent(); - currentInputFramebulkIndex[slot] = 0; - currentToolsFramebulkIndex[slot] = 0; + currentRequestRawFramebulkIndex[slot] = 0; + } active = true; @@ -350,6 +363,29 @@ TasFramebulk TasPlayer::GetRawFramebulkAt(int slot, int tick, unsigned& cachedIn return playbackInfo.slots[slot].framebulks[cachedIndex]; } +TasFramebulk &TasPlayer::RequestProcessedFramebulkAt(int slot, int tick) { + auto &processed = playbackInfo.slots[slot].processedFramebulks; + auto processedCount = processed.size(); + + if (processedCount == 0 || processed.back().tick < tick) { + TasFramebulk fb = GetRawFramebulkAt(slot, tick, currentRequestRawFramebulkIndex[slot]); + processed.push_back(fb); + return processed.back(); + } + + // if it already exists, it should be near the end, as we usually request the newest ones + for (int index = processedCount - 1; index >= 0; --index) { + if (processed[index].tick == tick) { + return processed[index]; + } else if (processed[index].tick < tick) { + break; + } + } + + console->Warning("TAS processed framebulk for tick %d not found! This should not happen!\n", tick); + return processed.back(); +} + TasPlayerInfo TasPlayer::GetPlayerInfo(int slot, void *player, CUserCmd *cmd, bool clientside) { TasPlayerInfo pi; @@ -427,7 +463,7 @@ TasPlayerInfo TasPlayer::GetPlayerInfo(int slot, void *player, CUserCmd *cmd, bo // predict the result of autojump tool so other tools can react appropriately. FOR_TAS_SCRIPT_VERSIONS_SINCE(8) { - if (autoJumpTool[slot].GetCurrentParams().enabled && autoJumpTool[slot].ShouldJump(pi)) { + if (autoJumpTool[slot].WillJump(pi) || jumpTool[slot].WillJump(pi)) { pi.willBeGrounded = false; } } @@ -540,7 +576,7 @@ void TasPlayer::SaveProcessedFramebulks() { we assume the response time for our "virtual controller" to be non-existing and just let it parse inputs corresponding to given tick. */ -void TasPlayer::FetchInputs(int slot, TasController *controller) { +void TasPlayer::FetchInputs(int slot, TasController *controller, CUserCmd* cmd) { // Slight hack! Input fetching (including SteamControllerMove) is // called through _Host_RunFrame_Input, which is called *before* // GameFrame (that being called via _Host_RunFrame_Server). Therefore, @@ -549,36 +585,23 @@ void TasPlayer::FetchInputs(int slot, TasController *controller) { // said than done since the input fetching code is only run when the // client is connected, so to match the behaviour we'd probably need // to actually hook at _Host_RunFrame_Input or CL_Move. - int tick = currentTick + 1; + int tasTick = currentTick + 1; - TasFramebulk fb = GetRawFramebulkAt(slot, tick, currentInputFramebulkIndex[slot]); + auto player = server->GetPlayer(slot + 1); - int fbTick = fb.tick; - - if (sar_tas_debug.GetInt() > 0 && fbTick == tick) { - console->Print("%s\n", fb.ToString().c_str()); + if (tasTick == 1) { + SamplePreProcessedFramebulk(slot, 0, player, cmd); } + TasFramebulk fb = SamplePreProcessedFramebulk(slot, tasTick, player, cmd); + controller->SetViewAnalog(fb.viewAnalog.x, fb.viewAnalog.y); controller->SetMoveAnalog(fb.moveAnalog.x, fb.moveAnalog.y); for (int i = 0; i < TAS_CONTROLLER_INPUT_COUNT; i++) { controller->SetButtonState((TasControllerInput)i, fb.buttonStates[i]); } - - if (tick == 1) { - // on tick 1, we'll run the commands from the bulk at tick 0 because - // of the annoying off-by-one thing explained above - TasFramebulk fb0 = GetRawFramebulkAt(slot, 0); - for (std::string cmd : fb0.commands) { - controller->AddCommandToQueue(cmd); - } - } - - // add commands only for tick when framebulk is placed. Don't preserve it to other ticks. - if (tick == fbTick) { - for (std::string cmd : fb.commands) { - controller->AddCommandToQueue(cmd); - } + for (std::string cmd : fb.commands) { + controller->AddCommandToQueue(cmd); } } @@ -591,6 +614,52 @@ static bool IsTaunting(ClientEnt *player) { return false; } +TasFramebulk TasPlayer::SamplePreProcessedFramebulk(int slot, int tasTick, void *player, CUserCmd *cmd) { + TasFramebulk& fb = RequestProcessedFramebulkAt(slot, tasTick); + + auto fbTick = fb.tick; + fb.tick = tasTick; + bool framebulkUpdated = (fbTick == tasTick); + + if (sar_tas_debug.GetInt() > 0 && framebulkUpdated) { + console->Print("(TAS: rawtick) %s\n", fb.ToString().c_str()); + } + + if (tasTick == 0 || !framebulkUpdated) { + std::vector 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()) { + UpdateTools(slot, fb, PRE_PROCESSING); + auto pInfo = GetPlayerInfo(slot, server->GetPlayer(slot + 1), cmd); + ApplyTools(fb, pInfo, PRE_PROCESSING); + + if (sar_tas_debug.GetInt() > 0) { + console->Print("(TAS: pretick) %s\n", fb.ToString().c_str()); + } + } + + return fb; +} + // special tools have to be parsed in input processing part. // because of alternateticks, a pair of inputs are created and then executed at the same time, // meaning that second tick in pair reads outdated info. @@ -603,19 +672,9 @@ void TasPlayer::PostProcess(int slot, void *player, CUserCmd *cmd) { // every other way of getting time is incorrect due to alternateticks int tasTick = FetchCurrentPlayerTickBase(player) - startTick; - TasFramebulk fb = GetRawFramebulkAt(slot, tasTick, currentToolsFramebulkIndex[slot]); - - // update all tools that needs to be updated - auto fbTick = fb.tick; - fb.tick = tasTick; - if (fbTick == tasTick) { - for (TasToolCommand cmd : fb.toolCmds) { - auto tool = TasTool::GetInstanceByName(slot, cmd.tool->GetName()); - if (tool == nullptr) continue; - tool->SetParams(cmd.params); - } - } + TasFramebulk& fb = RequestProcessedFramebulkAt(slot, tasTick); + UpdateTools(slot, fb, POST_PROCESSING); auto playerInfo = GetPlayerInfo(slot, player, cmd); float orig_forward = cmd->forwardmove; @@ -634,21 +693,7 @@ void TasPlayer::PostProcess(int slot, void *player, CUserCmd *cmd) { return; } - // applying tools - if (playbackInfo.slots[slot].header.version >= 3) { - // use priority list for newer versions. technically all tools should be in the list - for (std::string toolName : TasTool::priorityList) { - auto tool = TasTool::GetInstanceByName(slot, toolName); - if (tool == nullptr) continue; - tool->Apply(fb, playerInfo); - } - } else { - // use old "earliest first" ordering system (partially also present in TasTool::SetParams) - for (TasTool *tool : TasTool::GetList(slot)) { - tool->Apply(fb, playerInfo); - } - } - + ApplyTools(fb, playerInfo, POST_PROCESSING); // make sure none of the framebulk is NaN if (std::isnan(fb.moveAnalog.x)) fb.moveAnalog.x = 0; @@ -717,13 +762,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 +797,48 @@ void TasPlayer::ApplyMoveAnalog(Vector moveAnalog, CUserCmd *cmd) { } } +void TasPlayer::UpdateTools(int slot, const TasFramebulk &fb, TasToolProcessingType processType) { + for (TasToolCommand cmd : fb.toolCmds) { + auto tool = TasTool::GetInstanceByName(slot, cmd.tool->GetName()); + if (!CanProcessTool(tool, processType)) continue; + tool->SetParams(cmd.params); + } +} + +void TasPlayer::ApplyTools(TasFramebulk &fb, const TasPlayerInfo &pInfo, TasToolProcessingType processType) { + int slot = pInfo.slot; + + FOR_TAS_SCRIPT_VERSIONS_UNTIL(2) { + // use old "earliest first" ordering system (partially also present in TasTool::SetParams) + for (TasTool *tool : TasTool::GetList(slot)) { + if (!CanProcessTool(tool, processType)) continue; + tool->Apply(fb, pInfo); + } + return; + } + + // use priority list for newer versions. technically all tools should be in the list + for (std::string toolName : TasTool::priorityList) { + auto tool = TasTool::GetInstanceByName(slot, toolName); + if (!CanProcessTool(tool, processType)) continue; + tool->Apply(fb, pInfo); + } +} + +bool TasPlayer::CanProcessTool(TasTool *tool, TasToolProcessingType processType) { + if (tool == nullptr) { + return false; + } + + int slot = tool->GetSlot(); + FOR_TAS_SCRIPT_VERSIONS_UNTIL(8) { + // old scripts process all tools in post processing + return processType == POST_PROCESSING; + } + + return tool->CanProcess(processType); +} + void TasPlayer::DumpUsercmd(int slot, const CUserCmd *cmd, int tick, const char *source) { if (!sar_tas_dump_usercmd.GetBool()) return; std::string str = Utils::ssprintf("%s,%d,%.6f,%.6f,%08X,%.6f,%.6f,%.6f", source, tick, cmd->forwardmove, cmd->sidemove, cmd->buttons, cmd->viewangles.x, cmd->viewangles.y, cmd->viewangles.z); diff --git a/src/Features/Tas/TasPlayer.hpp b/src/Features/Tas/TasPlayer.hpp index f9536829..d645374d 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,12 +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); @@ -102,8 +100,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 +109,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 +118,13 @@ 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 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); 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..239607b3 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,13 @@ std::vector TasTool::priorityList = { "decel", }; -TasTool::TasTool(const char *name, 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); - + // 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 +52,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..85e6c823 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,45 @@ struct TasToolParams { struct TasFramebulk; struct TasPlayerInfo; +enum TasToolProcessingType { + PRE_PROCESSING, + 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, int slot); + TasTool(const char *name, TasToolProcessingType processingType, TasToolBulkType bulkType, 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; } + TasToolBulkType GetBulkType() const { return this->bulkType; } 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 +75,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, 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.cpp b/src/Features/Tas/TasTools/AbsoluteMoveTool.cpp index a7a5e045..2d939389 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) @@ -55,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(); @@ -64,7 +61,7 @@ std::shared_ptr AbsoluteMoveTool::ParseParams(std::vectorGetName(), vp[0].c_str())); + throw TasParserArgumentException(this, "direction", vp[0]); } float strength; @@ -73,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/AbsoluteMoveTool.hpp b/src/Features/Tas/TasTools/AbsoluteMoveTool.hpp index 640bff19..765b7b69 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, 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.cpp b/src/Features/Tas/TasTools/AutoAimTool.cpp index c24022fd..c0d87bd6 100644 --- a/src/Features/Tas/TasTools/AutoAimTool.cpp +++ b/src/Features/Tas/TasTools/AutoAimTool.cpp @@ -12,33 +12,37 @@ 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; - 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 TasParserException(Utils::ssprintf("Bad tick value for tool %s: %s", this->GetName(), args[ticksPos].c_str())); + 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 TasParserException(Utils::ssprintf("Bad interpolation value for tool %s: %s", this->GetName(), args[typePos].c_str())); + if (args.size() >= typePos + 1) { + try { + easingType = AngleToolsUtils::ParseEasingType(args[typePos]); + } catch (...) { + throw TasParserArgumentException(this, "interpolation", args[typePos]); + } } if (usesEntitySelector) { @@ -51,19 +55,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/AutoAimTool.hpp b/src/Features/Tas/TasTools/AutoAimTool.hpp index 17c35f43..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", slot) + : TasToolWithParams("autoaim", POST_PROCESSING, VIEWANGLES, slot) , elapsedTicks(0) {} virtual std::shared_ptr ParseParams(std::vector); 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/CheckTool.hpp b/src/Features/Tas/TasTools/CheckTool.hpp index e7801162..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", slot) + : TasToolWithParams("check", POST_PROCESSING, META, 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..78c7559c 100644 --- a/src/Features/Tas/TasTools/CommandTool.cpp +++ b/src/Features/Tas/TasTools/CommandTool.cpp @@ -9,16 +9,21 @@ 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; } } 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/CommandTool.hpp b/src/Features/Tas/TasTools/CommandTool.hpp index 0438497f..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", slot) { + : TasToolWithParams("cmd", PRE_PROCESSING, COMMANDS, slot) { } virtual std::shared_ptr ParseParams(std::vector); 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/DecelTool.hpp b/src/Features/Tas/TasTools/DecelTool.hpp index 8ff5fde5..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", 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.cpp b/src/Features/Tas/TasTools/DuckTool.cpp index f5f32181..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) - throw TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), vp.size())); + throw 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/DuckTool.hpp b/src/Features/Tas/TasTools/DuckTool.hpp index fc456776..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", slot) { + : TasToolWithParams("duck", PRE_PROCESSING, BUTTONS, slot) { } virtual std::shared_ptr ParseParams(std::vector); diff --git a/src/Features/Tas/TasTools/JumpTool.cpp b/src/Features/Tas/TasTools/JumpTool.cpp new file mode 100644 index 00000000..8793904a --- /dev/null +++ b/src/Features/Tas/TasTools/JumpTool.cpp @@ -0,0 +1,71 @@ +#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 TasParserArgumentCountException(this, 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 TasParserArgumentException(this, vp[0]); + } + + 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..3f0d6817 --- /dev/null +++ b/src/Features/Tas/TasTools/JumpTool.hpp @@ -0,0 +1,37 @@ +#pragma once +#include "../TasTool.hpp" + +struct JumpToolParams : public TasToolParams { + JumpToolParams() + : TasToolParams() { + } + JumpToolParams(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", POST_PROCESSING, BUTTONS, 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/LookTool.cpp b/src/Features/Tas/TasTools/LookTool.cpp index be727a78..c9a75377 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) @@ -35,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 TasParserArgumentCountException(this, vp.size()); } float pitchDelta = 0.0f; @@ -79,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/LookTool.hpp b/src/Features/Tas/TasTools/LookTool.hpp index 3900cb8c..222bf30d 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, 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.cpp b/src/Features/Tas/TasTools/MoveTool.cpp index b14a1aa1..6ea31173 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)}, @@ -39,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; @@ -90,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/MoveTool.hpp b/src/Features/Tas/TasTools/MoveTool.hpp index 13bdf3b1..ae36f1fc 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, 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.cpp b/src/Features/Tas/TasTools/SetAngleTool.cpp index 53ce27f0..a140dfe8 100644 --- a/src/Features/Tas/TasTools/SetAngleTool.cpp +++ b/src/Features/Tas/TasTools/SetAngleTool.cpp @@ -46,50 +46,60 @@ 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; float yaw; - int ticks; - AngleToolsUtils::EasingType easingType; + int ticks = 1; + AngleToolsUtils::EasingType easingType = AngleToolsUtils::EasingType::Linear; int i = 2; + if (vp[0] == "off") { + if (vp.size() != 1) throw TasParserArgumentCountException(this, vp.size()); + return std::make_shared(); + } + if (vp[0] == "ahead") { - if (vp.size() > 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]); } } // ticks - try { - ticks = vp.size() >= (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())); + 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 TasParserException(Utils::ssprintf("Bad interpolation value for tool %s: %s", this->GetName(), vp[i + 1].c_str())); + 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); } diff --git a/src/Features/Tas/TasTools/SetAngleTool.hpp b/src/Features/Tas/TasTools/SetAngleTool.hpp index 5ec2f18e..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", slot) + : TasToolWithParams("setang", POST_PROCESSING, VIEWANGLES, slot) , elapsedTicks(0) {} virtual std::shared_ptr ParseParams(std::vector); 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/ShootTool.hpp b/src/Features/Tas/TasTools/ShootTool.hpp index 476da30a..93d7687a 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", POST_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 86891ccb..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 TasParserException(Utils::ssprintf("Wrong argument count for tool %s: %d", this->GetName(), 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 6929a4a5..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", slot) { + : TasToolWithParams("stop", POST_PROCESSING, META, slot) { } virtual std::shared_ptr ParseParams(std::vector); diff --git a/src/Features/Tas/TasTools/StrafeTool.cpp b/src/Features/Tas/TasTools/StrafeTool.cpp index c03f2004..ec2d0a7b 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; } @@ -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; @@ -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 @@ -439,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. @@ -480,6 +499,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; @@ -490,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; } @@ -522,7 +545,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 +608,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); @@ -624,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; @@ -632,9 +670,11 @@ std::shared_ptr AutoStrafeTool::ParseParams(std::vector(); + throw TasParserArgumentCountException(this, vp.size()); } for (std::string param : vp) { @@ -647,6 +687,23 @@ 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; } //dir (using large numbers for left and right because angle is clamped to range -180 and 180) @@ -673,23 +733,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; } - //unknown parameter... - else - throw TasParserException(Utils::ssprintf("Bad parameter for tool %s: %s", this->GetName(), param.c_str())); + // suffix-less number (force) + else try { + force = TasParser::toFloat(param); + } + + catch (...) { + // unknown parameter... + throw TasParserArgumentException(this, param); + } } - return std::make_shared(type, dir, speed, noPitchLock, antiSpeedLock); + 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 5ade77bd..fc356f8a 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,17 +49,21 @@ struct AutoStrafeParams : public TasToolParams { AutoStrafeSpeed strafeSpeed = {CURRENT}; bool noPitchLock = false; bool antiSpeedLock = true; + AutoStrafeSteeringOffset steeringOffset = {DEGREES, 0.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, AutoStrafeSteeringOffset steeringOffset, float force) : TasToolParams(true) , strafeType(type) , strafeDir(dir) , strafeSpeed(speed) , noPitchLock(noPitchLock) - , antiSpeedLock(antiSpeedLock) { + , antiSpeedLock(antiSpeedLock) + , steeringOffset(steeringOffset) + , force(force) { } }; @@ -62,7 +76,7 @@ class AutoStrafeTool : public TasToolWithParams { int lastTurnDir = 0; public: AutoStrafeTool(int slot) - : TasToolWithParams("strafe", slot){}; + : TasToolWithParams("strafe", POST_PROCESSING, MOVEMENT, slot) {}; virtual std::shared_ptr ParseParams(std::vector); virtual void Apply(TasFramebulk &fb, const TasPlayerInfo &pInfo); @@ -76,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); @@ -87,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]; 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/UseTool.hpp b/src/Features/Tas/TasTools/UseTool.hpp index 9825fcc6..eaf131de 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, 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.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); diff --git a/src/Features/Tas/TasTools/ZoomTool.hpp b/src/Features/Tas/TasTools/ZoomTool.hpp index ded053f4..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", slot) { + : TasToolWithParams("zoom", PRE_PROCESSING, BUTTONS, slot) { } virtual std::shared_ptr ParseParams(std::vector);