From 39d2a82c2d107ad0066d5be053e82dc463c72d24 Mon Sep 17 00:00:00 2001 From: BenCat07 Date: Mon, 28 Dec 2020 12:44:25 +0100 Subject: [PATCH] Rewrite navbot/navparser Fix and improve capture and pathing logic Fixes some bugs with the capture logic and makes bots able to prioritize areas properly and fixes crouch jumps Apply 1 suggestion(s) to 1 file(s) Combine all Capture related files into one file attempt at remaking navbot big improvements, we're slowly getting somewhere! Switch from bounding box traces to double traces lightcat changes Many improvements. Vischeck cache, etc Fix bug related to the Dropdown code WIP i dont even know what this is, just comitting so the current state is out there Improve navigation and fix a ton of bugs - Fix findClosestNavArea - Apply Dropdowns properly - Improve pathing logic to not hit walls (unless nav mesh is stupid) - Don't try to jump with revved Minigun Add runtime vischecking and repathing Also make the priority system work Update debug message to reclect center point and properly restore z Fix a bunch of issues in the nav logic Account for brass beast speed Skip a crumb when possible Fix jump height + add logic to prevent getting stuck Add very basic navbot behaviour and improve stuck detection system - Look at path - Get Health - Get Ammo - Roam Improve Navbot to a semi-usable level - Universal Blacklist system - Re-added stay near - Avoid walking into enemy range - Snipe Sentries Fix some garbage prioritizing in aimbot More Navbot changes - Readd Autojump - Readd best weapon logic - Make Staynear take way less resources - Make bot ignore free blacklist if Ubered/Invincible Make sentry priority better Add capping and fix water movement Make bots escape danger and add a way to draw danger Fix a few things (See Desc) - Make roaming no longer path around the entire map, but instead choose nearby spots. - Make enemy danger not include People marked as CAT/Friend (Don't bump into them either though) - Fix inaccurate visuals on blacklist Make snipers bots more aggressive Fix antiaim being enabled with no pitch and yaw this casued issues with look at path for example Fix issues with aimbot + look at path Tweak navbot Settings and fix followbot Make ammo/health search take dispensers into account Co-authored-by: TotallyNotElite --- data/menu/nullifiedcat/movement.xml | 25 +- external/TF2_NavFile_Reader | 2 +- include/CaptureLogic.hpp | 85 + include/common.hpp | 1 - include/core/netvars.hpp | 14 + include/hacks/Aimbot.hpp | 1 + include/hacks/NavBot.hpp | 6 +- include/helpers.hpp | 2 +- include/localplayer.hpp | 2 + include/navparser.hpp | 123 +- include/sdk/CGameRules.h | 2 + src/CMakeLists.txt | 1 + src/CaptureLogic.cpp | 492 ++++++ src/controlpointcontroller.cpp | 259 +++ src/core/logging.cpp | 5 +- src/core/netvars.cpp | 26 + src/flagcontroller.cpp | 156 ++ src/hacks/Aimbot.cpp | 10 + src/hacks/AntiAim.cpp | 3 + src/hacks/CatBot.cpp | 16 +- src/hacks/FollowBot.cpp | 37 +- src/hacks/Misc.cpp | 2 +- src/hacks/NavBot.cpp | 2246 ++++++++++++--------------- src/helpers.cpp | 26 +- src/hooks/CreateMove.cpp | 4 - src/localplayer.cpp | 11 +- src/navparser.cpp | 1633 ++++++++++--------- src/payloadcontroller.cpp | 69 + src/settings/SettingsIO.cpp | 8 +- src/targethelper.cpp | 17 +- 30 files changed, 3196 insertions(+), 2088 deletions(-) create mode 100644 include/CaptureLogic.hpp create mode 100644 src/CaptureLogic.cpp create mode 100644 src/controlpointcontroller.cpp create mode 100644 src/flagcontroller.cpp create mode 100644 src/payloadcontroller.cpp diff --git a/data/menu/nullifiedcat/movement.xml b/data/menu/nullifiedcat/movement.xml index b179a0b9..b3da529f 100755 --- a/data/menu/nullifiedcat/movement.xml +++ b/data/menu/nullifiedcat/movement.xml @@ -30,20 +30,25 @@ - - - + + + - - - - + + + + + + + + - - - + + + diff --git a/external/TF2_NavFile_Reader b/external/TF2_NavFile_Reader index 5f2940c4..55ae8047 160000 --- a/external/TF2_NavFile_Reader +++ b/external/TF2_NavFile_Reader @@ -1 +1 @@ -Subproject commit 5f2940c4707127db1581e5708eecca1d6130610f +Subproject commit 55ae8047a24203cec834f54fd67ee6d571f497f6 diff --git a/include/CaptureLogic.hpp b/include/CaptureLogic.hpp new file mode 100644 index 00000000..7f9b6aab --- /dev/null +++ b/include/CaptureLogic.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include + +// Tf2 flag types +enum ETFFlagType +{ + TF_FLAGTYPE_CTF = 0, + TF_FLAGTYPE_ATTACK_DEFEND, + TF_FLAGTYPE_TERRITORY_CONTROL, + TF_FLAGTYPE_INVADE, + TF_FLAGTYPE_RESOURCE_CONTROL, + TF_FLAGTYPE_ROBOT_DESTRUCTION, + TF_FLAGTYPE_PLAYER_DESTRUCTION + + // + // ADD NEW ITEMS HERE TO AVOID BREAKING DEMOS + // +}; + +// Flag Drop status +enum ETFFlagStatus +{ + TF_FLAGINFO_HOME = 0, + TF_FLAGINFO_STOLEN, + TF_FLAGINFO_DROPPED +}; + +struct flag_info +{ + CachedEntity *ent{ nullptr }; + std::optional spawn_pos; + int team{ TEAM_UNK }; + flag_info(){}; + flag_info(CachedEntity *ent, Vector spawn_pos, int team) + { + this->ent = ent; + this->spawn_pos = spawn_pos; + this->team = team; + } +}; + +struct pl_info +{ + CachedEntity *ent; + std::optional position; + pl_info(){}; +}; + +#define MAX_CONTROL_POINTS 8 +#define MAX_PREVIOUS_POINTS 3 +struct cp_info +{ + // Index in the ObjectiveResource + int cp_index{ -1 }; + std::optional position; + // For BLU and RED to show who can and who cannnot cap + std::array can_cap{}; + cp_info(){}; +}; + +namespace flagcontroller +{ +// Use incase you don't get the needed information from the functions below +flag_info getFlag(int team); + +Vector getPosition(CachedEntity *flag); +std::optional getPosition(int team); +CachedEntity *getCarrier(CachedEntity *flag); +CachedEntity *getCarrier(int team); +ETFFlagStatus getStatus(CachedEntity *flag); +ETFFlagStatus getStatus(int team); +} // namespace flagcontroller + +namespace plcontroller +{ +// Get the closest Control Payload +std::optional getClosestPayload(Vector source, int team); +} // namespace plcontroller + +namespace cpcontroller +{ +// Get the closest Control Point we can cap +std::optional getClosestControlPoint(Vector source, int team); +} // namespace cpcontroller diff --git a/include/common.hpp b/include/common.hpp index 464f2dbb..3421581e 100644 --- a/include/common.hpp +++ b/include/common.hpp @@ -73,7 +73,6 @@ #include "core/sharedobj.hpp" #include "init.hpp" #include "reclasses/reclasses.hpp" -#include #include "HookTools.hpp" #include "bytepatch.hpp" diff --git a/include/core/netvars.hpp b/include/core/netvars.hpp index ba1d551f..292d4c45 100644 --- a/include/core/netvars.hpp +++ b/include/core/netvars.hpp @@ -83,6 +83,7 @@ public: offset_t m_bPlacing; offset_t m_bBuilding; offset_t m_bPlasmaDisable; + offset_t m_bCarryDeploy; // teleporter offset_t m_iTeleState; // teleport state [1 = idle, 2 = active, 3 = teleporting, 4 = charging] @@ -205,6 +206,19 @@ public: offset_t m_iHealingAssist_Resource; offset_t m_iPlayerLevel_Resource; + offset_t m_nFlagType; + offset_t m_nFlagStatus; + + offset_t m_bTeamCanCap; + offset_t m_iNumControlPoints; + offset_t m_vCPPositions; + offset_t m_iOwningTeam; + offset_t m_bCPLocked; + offset_t m_bPlayingMiniRounds; + offset_t m_bInMiniRound; + offset_t m_iPreviousPoints; + offset_t m_iBaseControlPoints; + offset_t m_iPlayerIndex; offset_t m_hTargetPlayer; }; diff --git a/include/hacks/Aimbot.hpp b/include/hacks/Aimbot.hpp index a4042885..eb0f2a54 100644 --- a/include/hacks/Aimbot.hpp +++ b/include/hacks/Aimbot.hpp @@ -36,6 +36,7 @@ bool BacktrackVisCheck(CachedEntity *entity); void Reset(); // Stuff to make storing functions easy +bool isAiming(); CachedEntity *CurrentTarget(); bool ShouldAim(); CachedEntity *RetrieveBestTarget(bool aimkey_state); diff --git a/include/hacks/NavBot.hpp b/include/hacks/NavBot.hpp index ea9d1b7b..81a369ec 100644 --- a/include/hacks/NavBot.hpp +++ b/include/hacks/NavBot.hpp @@ -1,4 +1,4 @@ -#pragma once +/*#pragma once #include #include @@ -19,7 +19,8 @@ enum task : uint8_t dispenser, followbot, outofbounds, - engineer + engineer, + capture }; enum engineer_task : uint8_t @@ -70,3 +71,4 @@ struct bot_class_config float max; }; } // namespace hacks::tf2::NavBot +*/ \ No newline at end of file diff --git a/include/helpers.hpp b/include/helpers.hpp index 267dbbdd..165c1c90 100644 --- a/include/helpers.hpp +++ b/include/helpers.hpp @@ -202,7 +202,7 @@ float GetFov(Vector ang, Vector src, Vector dst); void ReplaceString(std::string &input, const std::string &what, const std::string &with_what); void ReplaceSpecials(std::string &input); -std::pair ComputeMove(const Vector &a, const Vector &b); +Vector ComputeMove(const Vector &a, const Vector &b); std::pair ComputeMovePrecise(const Vector &a, const Vector &b); void WalkTo(const Vector &vector); diff --git a/include/localplayer.hpp b/include/localplayer.hpp index d3a99bce..2e148266 100644 --- a/include/localplayer.hpp +++ b/include/localplayer.hpp @@ -34,6 +34,8 @@ public: char life_state; int clazz; bool bZoomed; + bool bRevving; + bool bRevved; float flZoomBegin; bool holding_sniper_rifle; bool holding_sapper; diff --git a/include/navparser.hpp b/include/navparser.hpp index 238035c6..98f9bc3f 100644 --- a/include/navparser.hpp +++ b/include/navparser.hpp @@ -1,42 +1,101 @@ #pragma once #include -#include "mathlib/vector.h" +#include +#include "CNavFile.h" -class CNavFile; -class CNavArea; - -namespace nav +enum Priority_list { - -enum init_status : uint8_t -{ - off = 0, - unavailable, - initing, - on + patrol = 5, + lowprio_health, + staynear, + snipe_sentry, + followbot, + ammo, + capture, + health, + danger, }; -// Call prepare first and check its return value -extern std::unique_ptr navfile; +namespace navparser +{ +constexpr float PLAYER_WIDTH = 49; +constexpr float HALF_PLAYER_WIDTH = PLAYER_WIDTH / 2.0f; +constexpr float PLAYER_JUMP_HEIGHT = 72.0f; -// Current path priority -extern int curr_priority; -// Check if ready to recieve another NavTo (to avoid overwriting of -// instructions) -extern bool ReadyForCommands; -// Ignore. For level init only -extern std::atomic status; +#define TICKCOUNT_TIMESTAMP(seconds) (g_GlobalVars->tickcount + int(seconds / g_GlobalVars->interval_per_tick)) -// Nav to vector -bool navTo(const Vector &destination, int priority = 5, bool should_repath = true, bool nav_to_local = true, bool is_repath = false); -// Find closest to vector area -CNavArea *findClosestNavSquare(const Vector &vec); -// Check and init navparser -bool prepare(); -// Clear current path -void clearInstructions(); -// Check if area is safe from stickies and sentries -bool isSafe(CNavArea *area); +// Basic Blacklist reasons, you can add your own externally and use them +enum BlacklistReason_enum +{ + SENTRY, + STICKY, + ENEMY_NORMAL, + ENEMY_DORMANT, + // Always last + BLACKLIST_LENGTH +}; -} // namespace nav +class BlacklistReason +{ +public: + BlacklistReason_enum value; + int time = 0; + void operator=(BlacklistReason_enum const &reason) + { + this->value = reason; + } + BlacklistReason() + { + this->value = (BlacklistReason_enum) -1; + this->time = 0; + } + BlacklistReason(BlacklistReason_enum reason) + { + this->value = reason; + this->time = 0; + } + BlacklistReason(BlacklistReason_enum reason, int time) + { + this->value = reason; + this->time = time; + } +}; + +struct Crumb +{ + CNavArea *navarea; + Vector vec; +}; + +namespace NavEngine +{ + +// Is the Nav engine ready to run? +bool isReady(); +// Are we currently pathing? +bool isPathing(); +CNavFile *getNavFile(); +// Get closest nav square to target vector +CNavArea *findClosestNavSquare(const Vector origin); +// Get the path nodes +std::vector *getCrumbs(); +bool navTo(const Vector &destination, int priority = 5, bool should_repath = true, bool nav_to_local = true, bool is_repath = true); +// Use when something unexpected happens, e.g. vischeck fails +void abandonPath(); +// Use to cancel pathing completely +void cancelPath(); + +// Return the whole thing +std::unordered_map *getFreeBlacklist(); +// Return a specific category, we keep the same indexes to provide single element erasing +std::unordered_map getFreeBlacklist(BlacklistReason reason); + +// Clear whole blacklist +void clearFreeBlacklist(); +// Clear by category +void clearFreeBlacklist(BlacklistReason reason); + +extern int current_priority; +} // namespace NavEngine +} // namespace navparser diff --git a/include/sdk/CGameRules.h b/include/sdk/CGameRules.h index 1ffa240a..b586104a 100755 --- a/include/sdk/CGameRules.h +++ b/include/sdk/CGameRules.h @@ -13,4 +13,6 @@ public: int roundmode; // 48 | 4 bytes | 52 int pad1[1]; // 52 | 4 bytes | 56 int winning_team; // 56 | 4 bytes | 60 + char pad2[974]; // 60 | 974 bytes | 1034 + bool isPVEMode; // 1034 | 1 byte | 1035 }; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8273bbc7..68ebad50 100755 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,5 @@ set(files "${CMAKE_CURRENT_LIST_DIR}/angles.cpp" + "${CMAKE_CURRENT_LIST_DIR}/CaptureLogic.cpp" "${CMAKE_CURRENT_LIST_DIR}/chatlog.cpp" "${CMAKE_CURRENT_LIST_DIR}/chatstack.cpp" "${CMAKE_CURRENT_LIST_DIR}/conditions.cpp" diff --git a/src/CaptureLogic.cpp b/src/CaptureLogic.cpp new file mode 100644 index 00000000..ca95d644 --- /dev/null +++ b/src/CaptureLogic.cpp @@ -0,0 +1,492 @@ +#include "CaptureLogic.hpp" +#include "common.hpp" + +namespace flagcontroller +{ + +std::array flags; +bool is_ctf = true; + +// Check if a flag is good or not +bool isGoodFlag(CachedEntity *flag) +{ + if (CE_INVALID(flag) || flag->m_iClassID() != CL_CLASS(CCaptureFlag)) + return false; + return true; +} + +void Update() +{ + // Not ctf, no need to update + if (!is_ctf) + return; + // Find flags if missing + if (!flags[0].ent || !flags[1].ent) + for (int i = g_IEngine->GetMaxClients() + 1; i < MAX_ENTITIES; i++) + { + CachedEntity *ent = ENTITY(i); + // We cannot identify a bad entity as a flag due to the unreliability of it + if (CE_BAD(ent) || ent->m_iClassID() != CL_CLASS(CCaptureFlag)) + continue; + + // Store flags + if (!flags[0].ent) + flags[0].ent = ent; + else if (ent != flags[0].ent) + flags[1].ent = ent; + } + // Update flag data + for (auto &flag : flags) + { + // Not inited + if (!flag.ent) + continue; + + // Bad Flag, reset + if (!isGoodFlag(flag.ent)) + { + flag = flag_info(); + continue; + } + + // Cannot use "bad" flag, but it is still potentially valid + if (CE_BAD(flag.ent)) + continue; + + int flag_type = CE_INT(flag.ent, netvar.m_nFlagType); + + // Only CTF support for now + if (flag_type != TF_FLAGTYPE_CTF) + continue; + + // Assign team if missing + if (flag.team == TEAM_UNK) + flag.team = flag.ent->m_iTeam(); + + // Assign spawn point if it is missing and the flag is at spawn + if (!flag.spawn_pos) + { + int flag_status = CE_INT(flag.ent, netvar.m_nFlagStatus); + + // Flag is home + if (flag_status == TF_FLAGINFO_HOME) + flag.spawn_pos = flag.ent->m_vecOrigin(); + } + } +} + +void LevelInit() +{ + // Resez everything + for (auto &flag : flags) + flag = flag_info(); + is_ctf = true; +} + +// Get the info for the flag +flag_info getFlag(int team) +{ + for (auto &flag : flags) + { + if (flag.team == team) + return flag; + } + // None found + return flag_info(); +} + +// Get the Position of a flag on a specific team +Vector getPosition(CachedEntity *flag) +{ + return flag->m_vecOrigin(); +} + +std::optional getPosition(int team) +{ + auto flag = getFlag(team); + if (isGoodFlag(flag.ent)) + return getPosition(flag.ent); + // No good flag + return std::nullopt; +} + +// Get the person carrying the flag +CachedEntity *getCarrier(CachedEntity *flag) +{ + int entidx = HandleToIDX(CE_INT(flag, netvar.m_hOwnerEntity)); + // None/Invalid + if (IDX_BAD(entidx)) + return nullptr; + CachedEntity *carrier = ENTITY(entidx); + // Carrier is invalid + if (CE_BAD(carrier) || carrier->m_Type() != ENTITY_PLAYER) + return nullptr; + return carrier; +} + +// Wrapper for when you don't have the entity +CachedEntity *getCarrier(int team) +{ + auto flag = getFlag(team); + // Only use good flags + if (isGoodFlag(flag.ent)) + return getCarrier(flag.ent); + return nullptr; +} + +// Get the status of the flag (Home, being carried, dropped) +ETFFlagStatus getStatus(CachedEntity *flag) +{ + return (ETFFlagStatus) CE_INT(flag, netvar.m_nFlagStatus); +} + +ETFFlagStatus getStatus(int team) +{ + auto flag = getFlag(team); + // Only use good flags + if (isGoodFlag(flag.ent)) + return getStatus(flag.ent); + // Mark as home if nothing is found + return TF_FLAGINFO_HOME; +} +} // namespace flagcontroller + +namespace plcontroller +{ + +// Array that controls all the payloads for each team. Red team is first, then comes blue team. +static std::array, 2> payloads; +static Timer update_payloads{}; + +void Update() +{ + // We should update the payload list + if (update_payloads.test_and_set(3000)) + { + // Reset entries + for (auto &entry : payloads) + entry.clear(); + + for (int i = g_IEngine->GetMaxClients() + 1; i < MAX_ENTITIES; i++) + { + CachedEntity *ent = ENTITY(i); + // Not the object we need or invalid (team) + if (CE_BAD(ent) || ent->m_iClassID() != CL_CLASS(CObjectCartDispenser) || ent->m_iTeam() < TEAM_RED || ent->m_iTeam() > TEAM_BLU) + continue; + int team = ent->m_iTeam(); + + // Add new entry for the team + payloads.at(team - TEAM_RED).push_back(ent); + } + } +} +std::optional getClosestPayload(Vector source, int team) +{ + // Invalid team + if (team < TEAM_RED || team > TEAM_BLU) + return std::nullopt; + // Convert to index + int index = team - TEAM_RED; + auto entry = payloads[index]; + + float best_distance = FLT_MAX; + std::optional best_pos; + + // Find best payload + for (auto payload : entry) + { + if (CE_BAD(payload) || payload->m_iClassID() != CL_CLASS(CObjectCartDispenser)) + continue; + if (payload->m_vecOrigin().DistTo(source) < best_distance) + { + best_pos = payload->m_vecOrigin(); + best_distance = payload->m_vecOrigin().DistTo(source); + } + } + return best_pos; +} + +void LevelInit() +{ + for (auto &entry : payloads) + entry.clear(); +} +} // namespace plcontroller + +namespace cpcontroller +{ + +std::array controlpoint_data; +CachedEntity *objective_resource = nullptr; + +struct point_ignore +{ + std::string mapname; + int point_idx; + point_ignore(std::string str, int idx) : mapname{ str }, point_idx{ idx } {}; +}; + +// TODO: Find a way to fix these edge-cases. +// clang-format off +std::array ignore_points +{ + point_ignore("cp_steel", 4) +}; +// clang-format on + +// This function updates the Entity used for the Object resource +void UpdateObjectiveResource() +{ + // Already set and valid + if (CE_GOOD(objective_resource) && objective_resource->m_iClassID() == CL_CLASS(CTFObjectiveResource)) + return; + // Find ObjectiveResource and gamerules + for (int i = g_IEngine->GetMaxClients() + 1; i < MAX_ENTITIES; i++) + { + CachedEntity *ent = ENTITY(i); + if (CE_BAD(ent) || ent->m_iClassID() != CL_CLASS(CTFObjectiveResource)) + continue; + // Found it + objective_resource = ent; + break; + } +} + +// A Bunch of defines for netvars that don't deserve their own function +#define GET_NUM_CONTROL_POINTS() (CE_INT(objective_resource, netvar.m_iNumControlPoints)) +#define GET_OWNING_TEAM(index) ((&CE_INT(objective_resource, netvar.m_iOwningTeam))[index]) +#define GET_BASE_CONTROL_POINT_FOR_TEAM(team) ((&CE_INT(objective_resource, netvar.m_iBaseControlPoints))[team]) +#define GET_CP_LOCKED(index) ((&CE_VAR(objective_resource, netvar.m_bCPLocked, bool))[index]) +#define IN_MINI_ROUND(index) ((&CE_VAR(objective_resource, netvar.m_bInMiniRound, bool))[index]) + +bool TeamCanCapPoint(int index, int team) +{ + int arr_index = index + team * MAX_CONTROL_POINTS; + return (&CE_VAR(objective_resource, netvar.m_bTeamCanCap, bool))[arr_index]; +} + +int GetPreviousPointForPoint(int index, int team, int previndex) +{ + int iIntIndex = previndex + (index * MAX_PREVIOUS_POINTS) + (team * MAX_CONTROL_POINTS * MAX_PREVIOUS_POINTS); + return (&CE_INT(objective_resource, netvar.m_iPreviousPoints))[iIntIndex]; +} + +int GetFarthestOwnedControlPoint(int team) +{ + int iOwnedEnd = GET_BASE_CONTROL_POINT_FOR_TEAM(team); + if (iOwnedEnd == -1) + return -1; + + int iNumControlPoints = GET_NUM_CONTROL_POINTS(); + int iWalk = 1; + int iEnemyEnd = iNumControlPoints - 1; + if (iOwnedEnd != 0) + { + iWalk = -1; + iEnemyEnd = 0; + } + + // Walk towards the other side, and find the farthest owned point + int iFarthestPoint = iOwnedEnd; + for (int iPoint = iOwnedEnd; iPoint != iEnemyEnd; iPoint += iWalk) + { + // If we've hit a point we don't own, we're done + if (GET_OWNING_TEAM(iPoint) != team) + break; + + iFarthestPoint = iPoint; + } + + return iFarthestPoint; +} + +// Can we cap this point? +bool isPointUseable(int index, int team) +{ + // We Own it, can't cap it + if (GET_OWNING_TEAM(index) == team) + return false; + + // Can we cap the point? + if (!TeamCanCapPoint(index, team)) + return false; + + // We are playing a sectioned map, check if the CP is in it + if (CE_VAR(objective_resource, netvar.m_bPlayingMiniRounds, bool) && !IN_MINI_ROUND(index)) + return false; + + // Is the point locked? + if (GET_CP_LOCKED(index)) + return false; + + // Linear cap means that it won't require previous points, bail + static auto tf_caplinear = g_ICvar->FindVar("tf_caplinear"); + if (tf_caplinear && !tf_caplinear->GetBool()) + return true; + + // Any previous points necessary? + int iPointNeeded = GetPreviousPointForPoint(index, team, 0); + + // Points set to require themselves are always cappable + if (iPointNeeded == index) + return true; + + // No required points specified? Require all previous points. + if (iPointNeeded == -1) + { + // No Mini rounds + if (!CE_VAR(objective_resource, netvar.m_bPlayingMiniRounds, bool)) + { + // No custom previous point, team must own all previous points + int iFarthestPoint = GetFarthestOwnedControlPoint(team); + return (abs(iFarthestPoint - index) <= 1); + } + // We got a section map + else + { + // Tf2 itself does not seem to have any more code for this, so here goes + return true; + } + } + + // Loop through each previous point and see if the team owns it + for (int iPrevPoint = 0; iPrevPoint < MAX_PREVIOUS_POINTS; iPrevPoint++) + { + iPointNeeded = GetPreviousPointForPoint(index, team, iPrevPoint); + if (iPointNeeded != -1) + { + // We don't own the needed points + if (GET_OWNING_TEAM(iPointNeeded) != team) + return false; + } + } + return true; +} + +// Don't constantly update the cap status +static Timer capstatus_update{}; +// Update the control points +void UpdateControlPoints() +{ + // No objective ressource, can't run + if (!objective_resource) + return; + int num_cp = CE_INT(objective_resource, netvar.m_iNumControlPoints); + // No control points + if (!num_cp) + return; + // Clear the invalid controlpoints + if (num_cp <= MAX_CONTROL_POINTS) + for (int i = num_cp; i < MAX_CONTROL_POINTS; i++) + controlpoint_data.at(i) = cp_info(); + + for (int i = 0; i < num_cp; i++) + { + auto &data = controlpoint_data.at(i); + data.cp_index = i; + + // Update position (m_vCPPositions[index]) + data.position = (&CE_VAR(objective_resource, netvar.m_vCPPositions, Vector))[i]; + } + + if (capstatus_update.test_and_set(1000)) + for (int i = 0; i < num_cp; i++) + { + auto &data = controlpoint_data.at(i); + // Check accessibility for both teams, requires alot of checks + data.can_cap.at(0) = isPointUseable(i, TEAM_RED); + data.can_cap.at(1) = isPointUseable(i, TEAM_BLU); + } +} + +// Get the closest controlpoint to cap +std::optional getClosestControlPoint(Vector source, int team) +{ + // No resource for it + if (!objective_resource) + return std::nullopt; + // Check if it's a cp map + static auto tf_gamemode_cp = g_ICvar->FindVar("tf_gamemode_cp"); + if (!tf_gamemode_cp) + { + tf_gamemode_cp = g_ICvar->FindVar("tf_gamemode_cp"); + return std::nullopt; + } + if (!tf_gamemode_cp->GetBool()) + return std::nullopt; + + // Map team to 0-1 and check If Valid + int team_idx = team - TEAM_RED; + if (team_idx < 0 || team_idx > 1) + return std::nullopt; + + // No controlpoints + if (!GET_NUM_CONTROL_POINTS()) + return std::nullopt; + + int ignore_index = -1; + // Do the points need checking because of the map? + auto levelname = GetLevelName(); + for (auto &ignore : ignore_points) + { + // Try to find map name in bad point array + if (levelname.find(ignore.mapname) != levelname.npos) + ignore_index = ignore.point_idx; + } + + // Find the best and closest control point + std::optional best_cp; + float best_distance = FLT_MAX; + for (auto &cp : controlpoint_data) + { + // Ignore this point + if (cp.cp_index == ignore_index) + continue; + // They can cap + if (cp.can_cap.at(team_idx)) + { + // Is it closer? + if (cp.position && (*cp.position).DistTo(source) < best_distance) + { + best_distance = (*cp.position).DistTo(source); + best_cp = cp.position; + } + } + } + + return best_cp; +} + +void LevelInit() +{ + for (auto &cp : controlpoint_data) + cp = cp_info(); + objective_resource = nullptr; +} + +void Update() +{ + UpdateControlPoints(); + UpdateObjectiveResource(); +} +} // namespace cpcontroller + +// Main handlers +void CreateMove() +{ + flagcontroller::Update(); + plcontroller::Update(); + cpcontroller::Update(); +} + +void LevelInit() +{ + flagcontroller::LevelInit(); + plcontroller::LevelInit(); + cpcontroller::LevelInit(); +} + +static InitRoutine init([]() { + EC::Register(EC::CreateMove, CreateMove, "capturelogic_update"); + EC::Register(EC::LevelInit, LevelInit, "capturelogic_levelinit"); +}); diff --git a/src/controlpointcontroller.cpp b/src/controlpointcontroller.cpp new file mode 100644 index 00000000..e7ca4e17 --- /dev/null +++ b/src/controlpointcontroller.cpp @@ -0,0 +1,259 @@ +#include "common.hpp" +#include "controlpointcontroller.hpp" +namespace cpcontroller +{ + +std::array controlpoint_data; +CachedEntity *objective_resource = nullptr; + +struct point_ignore +{ + std::string mapname; + int point_idx; + point_ignore(std::string str, int idx) : mapname{ str }, point_idx{ idx } {}; +}; + +// TODO: Find a way to fix these edge-cases. +// clang-format off +std::array ignore_points +{ + point_ignore("cp_steel", 4) +}; +// clang-format on + +// This function updates the Entity used for the Object resource +void UpdateObjectiveResource() +{ + // Already set and valid + if (CE_GOOD(objective_resource) && objective_resource->m_iClassID() == CL_CLASS(CTFObjectiveResource)) + return; + // Find ObjectiveResource and gamerules + for (int i = g_IEngine->GetMaxClients() + 1; i < MAX_ENTITIES; i++) + { + CachedEntity *ent = ENTITY(i); + if (CE_BAD(ent) || ent->m_iClassID() != CL_CLASS(CTFObjectiveResource)) + continue; + // Found it + objective_resource = ent; + break; + } +} + +// A Bunch of defines for netvars that don't deserve their own function +#define GET_NUM_CONTROL_POINTS() (CE_INT(objective_resource, netvar.m_iNumControlPoints)) +#define GET_OWNING_TEAM(index) ((&CE_INT(objective_resource, netvar.m_iOwningTeam))[index]) +#define GET_BASE_CONTROL_POINT_FOR_TEAM(team) ((&CE_INT(objective_resource, netvar.m_iBaseControlPoints))[team]) +#define GET_CP_LOCKED(index) ((&CE_VAR(objective_resource, netvar.m_bCPLocked, bool))[index]) +#define IN_MINI_ROUND(index) ((&CE_VAR(objective_resource, netvar.m_bInMiniRound, bool))[index]) + +bool TeamCanCapPoint(int index, int team) +{ + int arr_index = index + team * MAX_CONTROL_POINTS; + return (&CE_VAR(objective_resource, netvar.m_bTeamCanCap, bool))[arr_index]; +} + +int GetPreviousPointForPoint(int index, int team, int previndex) +{ + int iIntIndex = previndex + (index * MAX_PREVIOUS_POINTS) + (team * MAX_CONTROL_POINTS * MAX_PREVIOUS_POINTS); + return (&CE_INT(objective_resource, netvar.m_iPreviousPoints))[iIntIndex]; +} + +int GetFarthestOwnedControlPoint(int team) +{ + int iOwnedEnd = GET_BASE_CONTROL_POINT_FOR_TEAM(team); + if (iOwnedEnd == -1) + return -1; + + int iNumControlPoints = GET_NUM_CONTROL_POINTS(); + int iWalk = 1; + int iEnemyEnd = iNumControlPoints - 1; + if (iOwnedEnd != 0) + { + iWalk = -1; + iEnemyEnd = 0; + } + + // Walk towards the other side, and find the farthest owned point + int iFarthestPoint = iOwnedEnd; + for (int iPoint = iOwnedEnd; iPoint != iEnemyEnd; iPoint += iWalk) + { + // If we've hit a point we don't own, we're done + if (GET_OWNING_TEAM(iPoint) != team) + break; + + iFarthestPoint = iPoint; + } + + return iFarthestPoint; +} + +// Can we cap this point? +bool isPointUseable(int index, int team) +{ + // We Own it, can't cap it + if (GET_OWNING_TEAM(index) == team) + return false; + + // Can we cap the point? + if (!TeamCanCapPoint(index, team)) + return false; + + // We are playing a sectioned map, check if the CP is in it + if (CE_VAR(objective_resource, netvar.m_bPlayingMiniRounds, bool) && !IN_MINI_ROUND(index)) + return false; + + // Is the point locked? + if (GET_CP_LOCKED(index)) + return false; + + // Linear cap means that it won't require previous points, bail + static auto tf_caplinear = g_ICvar->FindVar("tf_caplinear"); + if (tf_caplinear && !tf_caplinear->GetBool()) + return true; + + // Any previous points necessary? + int iPointNeeded = GetPreviousPointForPoint(index, team, 0); + + // Points set to require themselves are always cappable + if (iPointNeeded == index) + return true; + + // No required points specified? Require all previous points. + if (iPointNeeded == -1) + { + // No Mini rounds + if (!CE_VAR(objective_resource, netvar.m_bPlayingMiniRounds, bool)) + { + // No custom previous point, team must own all previous points + int iFarthestPoint = GetFarthestOwnedControlPoint(team); + return (abs(iFarthestPoint - index) <= 1); + } + // We got a section map + else + { + // Tf2 itself does not seem to have any more code for this, so here goes + return true; + } + } + + // Loop through each previous point and see if the team owns it + for (int iPrevPoint = 0; iPrevPoint < MAX_PREVIOUS_POINTS; iPrevPoint++) + { + iPointNeeded = GetPreviousPointForPoint(index, team, iPrevPoint); + if (iPointNeeded != -1) + { + // We don't own the needed points + if (GET_OWNING_TEAM(iPointNeeded) != team) + return false; + } + } + return true; +} + +// Don't constantly update the cap status +static Timer capstatus_update{}; +// Update the control points +void UpdateControlPoints() +{ + // No objective ressource, can't run + if (!objective_resource) + return; + int num_cp = CE_INT(objective_resource, netvar.m_iNumControlPoints); + // No control points + if (!num_cp) + return; + // Clear the invalid controlpoints + if (num_cp <= MAX_CONTROL_POINTS) + for (int i = num_cp; i < MAX_CONTROL_POINTS; i++) + controlpoint_data.at(i) = cp_info(); + + for (int i = 0; i < num_cp; i++) + { + auto &data = controlpoint_data.at(i); + data.cp_index = i; + + // Update position (m_vCPPositions[index]) + data.position = (&CE_VAR(objective_resource, netvar.m_vCPPositions, Vector))[i]; + } + + if (capstatus_update.test_and_set(1000)) + for (int i = 0; i < num_cp; i++) + { + auto &data = controlpoint_data.at(i); + // Check accessibility for both teams, requires alot of checks + data.can_cap.at(0) = isPointUseable(i, TEAM_RED); + data.can_cap.at(1) = isPointUseable(i, TEAM_BLU); + } +} + +// Get the closest controlpoint to cap +std::optional getClosestControlPoint(Vector source, int team) +{ + // No resource for it + if (!objective_resource) + return std::nullopt; + // Check if it's a cp map + static auto tf_gamemode_cp = g_ICvar->FindVar("tf_gamemode_cp"); + if (!tf_gamemode_cp) + { + tf_gamemode_cp = g_ICvar->FindVar("tf_gamemode_cp"); + return std::nullopt; + } + if (!tf_gamemode_cp->GetBool()) + return std::nullopt; + + // Map team to 0-1 and check If Valid + int team_idx = team - TEAM_RED; + if (team_idx < 0 || team_idx > 1) + return std::nullopt; + + // No controlpoints + if (!GET_NUM_CONTROL_POINTS()) + return std::nullopt; + + int ignore_index = -1; + // Do the points need checking because of the map? + auto levelname = GetLevelName(); + for (auto &ignore : ignore_points) + { + // Try to find map name in bad point array + if (levelname.find(ignore.mapname) != levelname.npos) + ignore_index = ignore.point_idx; + } + + // Find the best and closest control point + std::optional best_cp; + float best_distance = FLT_MAX; + for (auto &cp : controlpoint_data) + { + // Ignore this point + if (cp.cp_index == ignore_index) + continue; + // They can cap + if (cp.can_cap.at(team_idx)) + { + // Is it closer? + if (cp.position && (*cp.position).DistTo(source) < best_distance) + { + best_distance = (*cp.position).DistTo(source); + best_cp = cp.position; + } + } + } + + return best_cp; +} + +void LevelInit() +{ + for (auto &cp : controlpoint_data) + cp = cp_info(); + objective_resource = nullptr; +} + +static InitRoutine init([]() { + EC::Register(EC::CreateMove, UpdateObjectiveResource, "cpcontroller_updateent"); + EC::Register(EC::CreateMove, UpdateControlPoints, "cpcontroller_updatecp"); + EC::Register(EC::LevelInit, LevelInit, "levelinit_cocontroller"); +}); +} // namespace cpcontroller diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 945e09a7..8403bf15 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -68,9 +68,8 @@ void logging::Info(const char *fmt, ...) // Fill buffer int size = vsnprintf(result, 512, fmt, list); va_end(list); - if(size < 0) + if (size < 0) return; - Log(result, false); #endif } @@ -88,7 +87,7 @@ void logging::File(const char *fmt, ...) // Fill buffer int size = vsnprintf(result, 512, fmt, list); va_end(list); - if(size < 0) + if (size < 0) return; Log(result, true); diff --git a/src/core/netvars.cpp b/src/core/netvars.cpp index 4cd83028..f5903bbe 100644 --- a/src/core/netvars.cpp +++ b/src/core/netvars.cpp @@ -111,6 +111,17 @@ void NetVars::Init() this->m_bMiniBuilding = gNetvars.get_offset("DT_BaseObject", "m_bMiniBuilding"); this->m_bPlasmaDisable = gNetvars.get_offset("DT_BaseObject", "m_bPlasmaDisable"); + // any building + this->iUpgradeLevel = gNetvars.get_offset("DT_BaseObject", "m_iUpgradeLevel"); + this->m_hBuilder = gNetvars.get_offset("DT_BaseObject", "m_hBuilder"); + this->m_bCanPlace = gNetvars.get_offset("DT_BaseObject", "m_bServerOverridePlacement"); + this->m_bBuilding = gNetvars.get_offset("DT_BaseObject", "m_bBuilding"); + this->m_bCarryDeploy = gNetvars.get_offset("DT_BaseObject", "m_bCarryDeploy"); + this->m_iObjectType = gNetvars.get_offset("DT_BaseObject", "m_iObjectType"); + this->m_bHasSapper = gNetvars.get_offset("DT_BaseObject", "m_bHasSapper"); + this->m_bPlacing = gNetvars.get_offset("DT_BaseObject", "m_bPlacing"); + this->m_bMiniBuilding = gNetvars.get_offset("DT_BaseObject", "m_bMiniBuilding"); + // teleporter this->m_iTeleState = gNetvars.get_offset("DT_ObjectTeleporter", "m_iState"); this->m_flTeleRechargeTime = gNetvars.get_offset("DT_ObjectTeleporter", "m_flRechargeTime"); @@ -119,6 +130,21 @@ void NetVars::Init() this->m_flTeleYawToExit = gNetvars.get_offset("DT_ObjectTeleporter", "m_flYawToExit"); this->m_bMatchBuilding = gNetvars.get_offset("DT_ObjectTeleporter", "m_bMatchBuilding"); + // CTF Flag + this->m_nFlagType = gNetvars.get_offset("DT_CaptureFlag", "m_nType"); + this->m_nFlagStatus = gNetvars.get_offset("DT_CaptureFlag", "m_nFlagStatus"); + + // ObjectiveResource + this->m_bTeamCanCap = gNetvars.get_offset("DT_BaseTeamObjectiveResource", "m_bTeamCanCap"); + this->m_iNumControlPoints = gNetvars.get_offset("DT_BaseTeamObjectiveResource", "m_iNumControlPoints"); + this->m_vCPPositions = gNetvars.get_offset("DT_BaseTeamObjectiveResource", "m_vCPPositions[0]"); + this->m_iOwningTeam = gNetvars.get_offset("DT_BaseTeamObjectiveResource", "m_iOwner"); + this->m_bCPLocked = gNetvars.get_offset("DT_BaseTeamObjectiveResource", "m_bCPLocked"); + this->m_bPlayingMiniRounds = gNetvars.get_offset("DT_BaseTeamObjectiveResource", "m_bPlayingMiniRounds"); + this->m_bInMiniRound = gNetvars.get_offset("DT_BaseTeamObjectiveResource", "m_bInMiniRound"); + this->m_iPreviousPoints = gNetvars.get_offset("DT_BaseTeamObjectiveResource", "m_iPreviousPoints"); + this->m_iBaseControlPoints = gNetvars.get_offset("DT_BaseTeamObjectiveResource", "m_iBaseControlPoints"); + this->m_DmgRadius = gNetvars.get_offset("DT_BaseGrenade", "m_DmgRadius"); this->iPipeType = gNetvars.get_offset("DT_TFProjectile_Pipebomb", "m_iType"); this->iBuildingHealth = gNetvars.get_offset("DT_BaseObject", "m_iHealth"); diff --git a/src/flagcontroller.cpp b/src/flagcontroller.cpp new file mode 100644 index 00000000..8878de10 --- /dev/null +++ b/src/flagcontroller.cpp @@ -0,0 +1,156 @@ +#include "flagcontroller.hpp" +#include "common.hpp" + +namespace flagcontroller +{ + +std::array flags; +bool is_ctf = true; + +// Check if a flag is good or not +bool isGoodFlag(CachedEntity *flag) +{ + if (CE_INVALID(flag) || flag->m_iClassID() != CL_CLASS(CCaptureFlag)) + return false; + return true; +} + +void Update() +{ + // Not ctf, no need to update + if (!is_ctf) + return; + // Find flags if missing + if (!flags[0].ent || !flags[1].ent) + for (int i = g_IEngine->GetMaxClients() + 1; i < MAX_ENTITIES; i++) + { + CachedEntity *ent = ENTITY(i); + // We cannot identify a bad entity as a flag due to the unreliability of it + if (CE_BAD(ent) || ent->m_iClassID() != CL_CLASS(CCaptureFlag)) + continue; + + // Store flags + if (!flags[0].ent) + flags[0].ent = ent; + else if (ent != flags[0].ent) + flags[1].ent = ent; + } + // Update flag data + for (auto &flag : flags) + { + // Not inited + if (!flag.ent) + continue; + + // Bad Flag, reset + if (!isGoodFlag(flag.ent)) + { + flag = flag_info(); + continue; + } + + // Cannot use "bad" flag, but it is still potentially valid + if (CE_BAD(flag.ent)) + continue; + + int flag_type = CE_INT(flag.ent, netvar.m_nFlagType); + + // Only CTF support for now + if (flag_type != TF_FLAGTYPE_CTF) + continue; + + // Assign team if missing + if (flag.team == TEAM_UNK) + flag.team = flag.ent->m_iTeam(); + + // Assign spawn point if it is missing and the flag is at spawn + if (!flag.spawn_pos) + { + int flag_status = CE_INT(flag.ent, netvar.m_nFlagStatus); + + // Flag is home + if (flag_status == TF_FLAGINFO_HOME) + flag.spawn_pos = flag.ent->m_vecOrigin(); + } + } +} + +void LevelInit() +{ + // Resez everything + for (auto &flag : flags) + flag = flag_info(); + is_ctf = true; +} + +// Get the info for the flag +flag_info getFlag(int team) +{ + for (auto &flag : flags) + { + if (flag.team == team) + return flag; + } + // None found + return flag_info(); +} + +// Get the Position of a flag on a specific team +Vector getPosition(CachedEntity *flag) +{ + return flag->m_vecOrigin(); +} + +std::optional getPosition(int team) +{ + auto flag = getFlag(team); + if (isGoodFlag(flag.ent)) + return getPosition(flag.ent); + // No good flag + return std::nullopt; +} + +// Get the person carrying the flag +CachedEntity *getCarrier(CachedEntity *flag) +{ + int entidx = HandleToIDX(CE_INT(flag, netvar.m_hOwnerEntity)); + // None/Invalid + if (IDX_BAD(entidx)) + return nullptr; + CachedEntity *carrier = ENTITY(entidx); + // Carrier is invalid + if (CE_BAD(carrier) || carrier->m_Type() != ENTITY_PLAYER) + return nullptr; + return carrier; +} + +// Wrapper for when you don't have the entity +CachedEntity *getCarrier(int team) +{ + auto flag = getFlag(team); + // Only use good flags + if (isGoodFlag(flag.ent)) + return getCarrier(flag.ent); + return nullptr; +} + +// Get the status of the flag (Home, being carried, dropped) +ETFFlagStatus getStatus(CachedEntity *flag) +{ + return (ETFFlagStatus) CE_INT(flag, netvar.m_nFlagStatus); +} + +ETFFlagStatus getStatus(int team) +{ + auto flag = getFlag(team); + // Only use good flags + if (isGoodFlag(flag.ent)) + return getStatus(flag.ent); + // Mark as home if nothing is found + return TF_FLAGINFO_HOME; +} +static InitRoutine init([]() { + EC::Register(EC::CreateMove, Update, "flagcontroller_update"); + EC::Register(EC::LevelInit, LevelInit, "flagcontroller_levelinit"); +}); +} // namespace flagcontroller diff --git a/src/hacks/Aimbot.cpp b/src/hacks/Aimbot.cpp index c22ce13c..5ac67f1a 100644 --- a/src/hacks/Aimbot.cpp +++ b/src/hacks/Aimbot.cpp @@ -223,6 +223,7 @@ static void doAutoZoom(bool target_found) // Current Entity CachedEntity *target_last = 0; +bool aimed_this_tick = false; // If slow aimbot allows autoshoot bool slow_can_shoot = false; @@ -243,6 +244,8 @@ static void CreateMove() if (CE_BAD(LOCAL_E) || !LOCAL_E->m_bAlivePlayer() || CE_BAD(LOCAL_W)) enable = false; + aimed_this_tick = false; + if (!enable) { target_last = nullptr; @@ -989,6 +992,7 @@ void Aim(CachedEntity *entity) if (data) hacks::tf2::backtrack::SetBacktrackData(entity, *data); } + aimed_this_tick = true; // Finish function return; } @@ -1522,6 +1526,12 @@ float EffectiveTargetingRange() return (float) max_range; } +// Used mostly by navbot to not accidentally look at path when aiming +bool isAiming() +{ + return aimed_this_tick; +} + // A function used by gui elements to determine the current target CachedEntity *CurrentTarget() { diff --git a/src/hacks/AntiAim.cpp b/src/hacks/AntiAim.cpp index fe6a66d6..00c51e03 100644 --- a/src/hacks/AntiAim.cpp +++ b/src/hacks/AntiAim.cpp @@ -369,6 +369,9 @@ void ProcessUserCmd(CUserCmd *cmd) return; if (!ShouldAA(cmd)) return; + // Not running + if (!pitch && !yaw) + return; static bool keepmode = true; keepmode = !keepmode; float &p = cmd->viewangles.x; diff --git a/src/hacks/CatBot.cpp b/src/hacks/CatBot.cpp index d2f76579..029a9a91 100644 --- a/src/hacks/CatBot.cpp +++ b/src/hacks/CatBot.cpp @@ -327,7 +327,7 @@ Upgradeinfo PickUpgrade() } static std::vector spot_list; // Upgrade Navigation -void NavUpgrade() +/*void NavUpgrade() { std::string lvlname = g_IEngine->GetLevelName(); std::vector potential_spots{}; @@ -359,6 +359,7 @@ void NavUpgrade() return; } } + static bool run = false; static Timer run_delay; static Timer buy_upgrade; @@ -479,16 +480,17 @@ void MvM_Autoupgrade(KeyValues *event) run_delay.update(); } } - +*/ void SendNetMsg(INetMessage &msg) { + /* if (!strcmp(msg.GetName(), "clc_CmdKeyValues")) { if ((KeyValues *) (((unsigned *) &msg)[4])) MvM_Autoupgrade((KeyValues *) (((unsigned *) &msg)[4])); - } + }*/ } - +/* class CatBotEventListener : public IGameEventListener2 { void FireGameEvent(IGameEvent *event) override @@ -509,7 +511,7 @@ CatBotEventListener &listener() { static CatBotEventListener object{}; return object; -} +}*/ class CatBotEventListener2 : public IGameEventListener2 { @@ -912,7 +914,7 @@ void update() void init() { - g_IEventManager2->AddListener(&listener(), "player_death", false); + // g_IEventManager2->AddListener(&listener(), "player_death", false); g_IEventManager2->AddListener(&listener2(), "vote_maps_changed", false); } @@ -924,7 +926,7 @@ void level_init() void shutdown() { - g_IEventManager2->RemoveListener(&listener()); + // g_IEventManager2->RemoveListener(&listener()); g_IEventManager2->RemoveListener(&listener2()); } diff --git a/src/hacks/FollowBot.cpp b/src/hacks/FollowBot.cpp index 833b17f4..55fe0272 100644 --- a/src/hacks/FollowBot.cpp +++ b/src/hacks/FollowBot.cpp @@ -37,8 +37,6 @@ static settings::Boolean ignore_textmode{ "follow-bot.ignore-textmode", "true" } static settings::Boolean mimic_crouch{ "follow-bot.mimic-crouch", "true" }; static settings::Boolean autozoom_if_idle{ "follow-bot.autozoom-if-idle", "true" }; -namespace nb = hacks::tf2::NavBot; - static Timer navBotInterval{}; static unsigned steamid = 0x0; @@ -270,7 +268,7 @@ static bool startFollow(CachedEntity *entity, bool useNavbot) } if (useNavbot) { - if (nav::navTo(entity->m_vecOrigin(), 8, true, false)) + if (navparser::NavEngine::navTo(entity->m_vecOrigin(), Priority_list::followbot, true, false)) { navtarget = true; return true; @@ -310,14 +308,14 @@ static void cm() if (!enable || CE_BAD(LOCAL_E) || !LOCAL_E->m_bAlivePlayer() || CE_BAD(LOCAL_W)) { follow_target = 0; - if (nb::task::current_task == nb::task::followbot) - nb::task::current_task = nb::task::none; + if (navparser::NavEngine::current_priority == Priority_list::followbot) + navparser::NavEngine::cancelPath(); return; } if (!inited) init(); - if (nb::task::current_task == nb::task::health || nb::task::current_task == nb::task::ammo) + if (navparser::NavEngine::current_priority > Priority_list::followbot) { follow_target = 0; return; @@ -343,7 +341,7 @@ static void cm() crouch_timer.update(); } - bool isNavBotCM = navBotInterval.test_and_set(3000) && nav::prepare(); + bool isNavBotCM = navBotInterval.test_and_set(3000) && navparser::NavEngine::isReady(); bool foundPreferredTarget = false; // Target Selection @@ -467,18 +465,6 @@ static void cm() } if (entity->m_bEnemy()) continue; - // const model_t *model = ENTITY(follow_target)->InternalEntity()->GetModel(); - // FIXME follow cart/point - /*if (followcart && model && - (lagexploit::pointarr[0] || lagexploit::pointarr[1] || - lagexploit::pointarr[2] || lagexploit::pointarr[3] || - lagexploit::pointarr[4]) && - (model == lagexploit::pointarr[0] || - model == lagexploit::pointarr[1] || - model == lagexploit::pointarr[2] || - model == lagexploit::pointarr[3] || - model == lagexploit::pointarr[4])) - follow_target = entity->m_IDX;*/ // favor closer entitys if (CE_GOOD(entity)) { @@ -512,7 +498,7 @@ static void cm() if (navtarget) { auto ent = ENTITY(follow_target); - if (!nav::prepare()) + if (!navparser::NavEngine::isReady()) { follow_target = 0; navtarget = 0; @@ -531,7 +517,7 @@ static void cm() } if (pos && navtimer.test_and_set(800)) { - if (nav::navTo(*pos, 8, true, false)) + if (navparser::NavEngine::navTo(*pos, Priority_list::followbot, true, false)) navinactivity.update(); } } @@ -539,7 +525,6 @@ static void cm() { follow_target = 0; } - nb::task::current_task = nb::task::followbot; return; } } @@ -547,13 +532,11 @@ static void cm() // last check for entity before we continue if (!follow_target) { - if (nb::task::current_task == nb::task::followbot) - nb::task::current_task = nb::task::none; + if (navparser::NavEngine::current_priority == Priority_list::followbot) + navparser::NavEngine::cancelPath(); return; } - - nb::task::current_task = nb::task::followbot; - nav::clearInstructions(); + navparser::NavEngine::cancelPath(); CachedEntity *followtar = ENTITY(follow_target); // wtf is this needed diff --git a/src/hacks/Misc.cpp b/src/hacks/Misc.cpp index 89bad42e..58a61683 100644 --- a/src/hacks/Misc.cpp +++ b/src/hacks/Misc.cpp @@ -630,7 +630,7 @@ void DumpRecvTable(CachedEntity *ent, RecvTable *table, int depth, const char *f logging::Info("TABLE %s IN DEPTH %d: %s [0x%04x] = %i | %u | %hd | %hu", table ? table->GetName() : "none", depth, prop->GetName(), prop->GetOffset(), CE_INT(ent, acc_offset + prop->GetOffset()), CE_VAR(ent, acc_offset + prop->GetOffset(), unsigned int), CE_VAR(ent, acc_offset + prop->GetOffset(), short), CE_VAR(ent, acc_offset + prop->GetOffset(), unsigned short)); break; case SendPropType::DPT_String: - logging::Info("TABLE %s IN DEPTH %d: %s [0x%04x] = %s", table ? table->GetName() : "none", depth, prop->GetName(), prop->GetOffset(), CE_VAR(ent, prop->GetOffset(), char *)); + logging::Info("TABLE %s IN DEPTH %d: %s [0x%04x] = 'not yet supported'", table ? table->GetName() : "none", depth, prop->GetName(), prop->GetOffset()); break; case SendPropType::DPT_Vector: logging::Info("TABLE %s IN DEPTH %d: %s [0x%04x] = (%f, %f, %f)", table ? table->GetName() : "none", depth, prop->GetName(), prop->GetOffset(), CE_FLOAT(ent, acc_offset + prop->GetOffset()), CE_FLOAT(ent, acc_offset + prop->GetOffset() + 4), CE_FLOAT(ent, acc_offset + prop->GetOffset() + 8)); diff --git a/src/hacks/NavBot.cpp b/src/hacks/NavBot.cpp index 4fc0ee0d..c60c7599 100644 --- a/src/hacks/NavBot.cpp +++ b/src/hacks/NavBot.cpp @@ -1,1075 +1,83 @@ -#include "common.hpp" +#include "Settings.hpp" +#include "init.hpp" +#include "HookTools.hpp" +#include "interfaces.hpp" #include "navparser.hpp" -#include "NavBot.hpp" +#include "playerresource.h" +#include "localplayer.hpp" +#include "sdk.hpp" +#include "entitycache.hpp" +#include "CaptureLogic.hpp" #include "PlayerTools.hpp" #include "Aimbot.hpp" -#include "Misc.hpp" -#include "teamroundtimer.hpp" -#include "MiscAimbot.hpp" +#include "navparser.hpp" namespace hacks::tf2::NavBot { -// -Rvars- static settings::Boolean enabled("navbot.enabled", "false"); +static settings::Boolean search_health("navbot.search-health", "true"); +static settings::Boolean search_ammo("navbot.search-ammo", "true"); static settings::Boolean stay_near("navbot.stay-near", "true"); -static settings::Boolean heavy_mode("navbot.other-mode", "false"); -static settings::Boolean spy_mode("navbot.spy-mode", "false"); -static settings::Boolean engineer_mode("navbot.engineer-mode", "false"); -static settings::Boolean get_health("navbot.get-health-and-ammo", "true"); -static settings::Float jump_distance("navbot.autojump.trigger-distance", "300"); +static settings::Boolean capture_objectives("navbot.capture-objectives", "true"); +static settings::Boolean snipe_sentries("navbot.snipe-sentries", "true"); +static settings::Boolean snipe_sentries_shortrange("navbot.snipe-sentries.shortrange", "false"); +static settings::Boolean escape_danger("navbot.escape-danger", "true"); +static settings::Boolean escape_danger_ctf_cap("navbot.escape-danger.ctf-cap", "false"); +static settings::Boolean enable_slight_danger_when_capping("navbot.escape-danger.slight-danger.capping", "false"); static settings::Boolean autojump("navbot.autojump.enabled", "false"); static settings::Boolean primary_only("navbot.primary-only", "true"); -static settings::Int spy_ignore_time("navbot.spy-ignore-time", "5000"); +static settings::Float jump_distance("navbot.autojump.trigger-distance", "300"); +static settings::Int blacklist_delay("navbot.proximity-blacklist.delay", "500"); +static settings::Boolean blacklist_dormat("navbot.proximity-blacklist.dormant", "false"); +static settings::Int blacklist_delay_dormat("navbot.proximity-blacklist.delay-dormant", "1000"); +static settings::Int blacklist_slightdanger_limit("navbot.proximity-blacklist.slight-danger.amount", "2"); +#if ENABLE_VISUALS +static settings::Boolean draw_danger("navbot.draw-danger", "false"); +#endif -// -Forward declarations- -bool init(bool first_cm); -static bool navToSniperSpot(); -static bool navToBuildingSpot(); -static bool stayNear(); -static bool stayNearEngineer(); -static bool getDispenserHealthAndAmmo(int metal = -1); -static bool getHealthAndAmmo(int metal = -1); -static void autoJump(); -static void updateSlot(); -static void update_building_spots(); -static bool engineerLogic(); -static std::pair getNearestPlayerDistance(bool vischeck = true); -using task::current_engineer_task; -using task::current_task; +// Allow for custom danger configs, mainly for debugging purposes +static settings::Boolean danger_config_custom("navbot.danger-config.enabled", "false"); +static settings::Boolean danger_config_custom_prefer_far("navbot.danger-config.perfer_far", "true"); +static settings::Float danger_config_custom_min_full_danger("navbot.danger-config.min_full_danger", "300"); +static settings::Float danger_config_custom_min_slight_danger("navbot.danger-config.min_slight_danger", "500"); +static settings::Float danger_config_custom_max_slight_danger("navbot.danger-config.max_slight_danger", "3000"); -// -Variables- -static std::vector> sniper_spots; -static std::vector> building_spots; -static std::vector blacklisted_build_spots; -// Our Buildings. We need this so we can remove them on object_destroyed. -static std::vector local_buildings; -// Needed for blacklisting -static CNavArea *current_build_area; -// How long should the bot wait until pathing again? -static Timer wait_until_path{}; -// Engineer version of above -static Timer wait_until_path_engineer{}; -// Time before following target cloaked spy again -static std::array spy_cloak{}; -// Don't spam spy path thanks -static Timer spy_path{}; -// Big wait between updating Engineer building spots -static Timer engineer_update{}; -// Recheck Building Area -static Timer engineer_recheck{}; -// Timer for resetting Build attempts -static Timer build_timer{}; -// Timer for wait between rotation and checking if we can place -static Timer rotation_timer{}; -// Uses to check how long until we should resend the "build" command -static Timer build_command_timer{}; -// Dispenser Nav cooldown -static Timer dispenser_nav_timer{}; -// Last Yaw used for building -static float build_current_rotation = -180.0f; -// How many times have we tried to place? -static int build_attempts = 0; -// Enum for Building types -enum Building +// Controls the bot parameters like distance from enemy +struct bot_class_config { - None = -1, - Dispenser = 0, - TP_Entrace, - Sentry, - TP_Exit + float min_full_danger; + float min_slight_danger; + float max; + bool prefer_far; }; -// Successfully built? (Unknown == Bot is still trying to build and isn't sure if it will work or not yet) -enum success_build +constexpr bot_class_config CONFIG_SHORT_RANGE = { 140.0f, 400.0f, 600.0f, false }; +constexpr bot_class_config CONFIG_MID_RANGE = { 200.0f, 500.0f, 3000.0f, true }; +constexpr bot_class_config CONFIG_LONG_RANGE = { 300.0f, 500.0f, 4000.0f, true }; +bot_class_config selected_config = CONFIG_MID_RANGE; + +static Timer health_cooldown{}; +static Timer ammo_cooldown{}; +// Should we search health at all? +bool shouldSearchHealth(bool low_priority = false) { - Failure = 0, - Unknown, - Success -}; - -// What is the bot currently doing -namespace task -{ -Task current_task = task::none; -engineer_task current_engineer_task = engineer_task::nothing; -} // namespace task - -constexpr bot_class_config DIST_SPY{ 10.0f, 50.0f, 1000.0f }; -constexpr bot_class_config DIST_OTHER{ 100.0f, 200.0f, 300.0f }; -constexpr bot_class_config DIST_SNIPER{ 1000.0f, 1500.0f, 3000.0f }; -constexpr bot_class_config DIST_ENGINEER{ 600.0f, 1000.0f, 2500.0f }; - -// Gunslinger Engineers really don't care at all -constexpr bot_class_config DIST_GUNSLINGER_ENGINEER{ 50.0f, 200.0f, 900.0f }; - -inline bool HasGunslinger(CachedEntity *ent) -{ - return HasWeapon(ent, 142); -} -static void CreateMove() -{ - if (CE_BAD(LOCAL_E) || !LOCAL_E->m_bAlivePlayer() || !LOCAL_E->m_bAlivePlayer()) - return; - if (!init(false)) - return; - // blocking actions implement their own functions and shouldn't be interrupted by anything else - bool blocking = std::find(task::blocking_tasks.begin(), task::blocking_tasks.end(), task::current_task.id) != task::blocking_tasks.end(); - - if (!nav::ReadyForCommands || blocking) - wait_until_path.update(); - else - current_task = task::none; - // Check if we should path at all - if (!blocking || task::current_task == task::engineer) - { - round_states round_state = g_pTeamRoundTimer->GetRoundState(); - // Still in setuptime, if on fitting team, then do not path yet - if (round_state == RT_STATE_SETUP && g_pLocalPlayer->team == TEAM_BLU) - { - // Clear instructions - if (!nav::ReadyForCommands) - nav::clearInstructions(); - return; - } - } - - if (autojump) - autoJump(); - if (primary_only) - updateSlot(); - if (engineer_mode) - { - if (CE_GOOD(LOCAL_E)) - { - for (int i = g_IEngine->GetMaxClients() + 1; i < MAX_ENTITIES; i++) - { - CachedEntity *ent = ENTITY(i); - if (!ent || CE_BAD(ent) || ent->m_bEnemy() || !ent->m_bAlivePlayer()) - continue; - if (HandleToIDX(CE_INT(ent, netvar.m_hBuilder)) != LOCAL_E->m_IDX) - continue; - if (std::find(local_buildings.begin(), local_buildings.end(), ent) == local_buildings.end()) - local_buildings.push_back(ent); - } - update_building_spots(); - } - } - - if (get_health) - { - int metal = -1; - if (engineer_mode && g_pLocalPlayer->clazz == tf_engineer) - metal = CE_INT(LOCAL_E, netvar.m_iAmmo + 12); - if ((dispenser_nav_timer.test_and_set(1000) && getDispenserHealthAndAmmo(metal))) - return; - if (getHealthAndAmmo(metal)) - return; - } - - // Engineer stuff - if (engineer_mode && g_pLocalPlayer->clazz == tf_engineer) - { - if (engineerLogic()) - return; - } - // Reset Engi task stuff if not engineer - else if (!engineer_mode && task::current_task == task::engineer) - { - task::current_task = task::none; - task::current_engineer_task = task::nothing; - } - - if (blocking) - return; - // Spy can just walk into the enemy - if (spy_mode) - { - if (spy_path.check(1000)) - { - auto nearest = getNearestPlayerDistance(false); - if (CE_VALID(nearest.first) && nearest.first->m_vecDormantOrigin()) - { - if (current_task != task::stay_near) - { - if (nav::navTo(*nearest.first->m_vecDormantOrigin(), 6, true, false)) - { - spy_path.update(); - current_task = task::stay_near; - return; - } - } - else - { - if (nav::navTo(*nearest.first->m_vecDormantOrigin(), 6, false, false)) - { - spy_path.update(); - return; - } - } - } - } - if (current_task == task::stay_near) - return; - } - // Try to stay near enemies to increase efficiency - if ((stay_near || heavy_mode) && !spy_mode) - if (stayNear()) - return; - // We don't have anything else to do. Just nav to sniper spots. - if (navToSniperSpot()) - return; - // Uhh... Just stand around I guess? -} - -bool init(bool first_cm) -{ - static bool inited = false; - if (first_cm) - inited = false; - if (!enabled) + // Priority too high + if (navparser::NavEngine::current_priority > health) return false; - if (!nav::prepare()) - return false; - if (!inited) - { - blacklisted_build_spots.clear(); - local_buildings.clear(); - sniper_spots.clear(); - // Add all sniper spots to vector - for (auto &area : nav::navfile->m_areas) - { - for (auto hide : area.m_hidingSpots) - if (hide.IsGoodSniperSpot() || hide.IsIdealSniperSpot() || hide.IsExposed()) - sniper_spots.emplace_back(&area, hide.m_pos); - } - inited = true; - } - return true; + float health_percent = LOCAL_E->m_iHealth() / (float) g_pPlayerResource->GetMaxHealth(LOCAL_E); + // Get health when below 65%, or below 80% and just patroling + return health_percent < 0.64f || (low_priority && (navparser::NavEngine::current_priority <= patrol || navparser::NavEngine::current_priority == lowprio_health) && health_percent <= 0.80f); } -struct area_struct -{ - // The Area - CNavArea *navarea; - // Distance away from enemies - float min_distance; - // Valid enemies to area - std::vector enemy_list; -}; - -void update_building_spots() -{ - if (engineer_update.test_and_set(10000)) - { - building_spots.clear(); - // Store in here to reduce operations - std::vector enemy_positions; - // Stores valid areas, the float is the minimum distance away from enemies, needed for sorting later - std::vector areas; - - for (int i = 1; i <= g_IEngine->GetMaxClients(); i++) - { - CachedEntity *ent = ENTITY(i); - // Grab only Enemies and only if they are in soundcache - if (!ent || CE_INVALID(ent) || !ent->m_bAlivePlayer() || !ent->m_bEnemy()) - continue; - if (ent->m_vecDormantOrigin()) - enemy_positions.push_back(*ent->m_vecDormantOrigin()); - } - auto config = &DIST_ENGINEER; - if (HasGunslinger(LOCAL_E)) - config = &DIST_GUNSLINGER_ENGINEER; - for (auto &area : nav::navfile->m_areas) - { - // Blacklisted for building - if (std::find(blacklisted_build_spots.begin(), blacklisted_build_spots.end(), &area) != blacklisted_build_spots.end()) - continue; - - // These positions we should vischeck - std::vector vischeck_positions; - - // Minimum distance the area was away from enemies - float min_dist_away = FLT_MAX; - - // Area Center - auto area_pos = area.m_center; - // Don't want to instantly hit the floor - area_pos.z += 42.0f; - - // Found enemy below min/above max range away from area - bool enemy_found = false; - bool out_of_reach = true; - - for (auto &pos : enemy_positions) - { - auto dist = area_pos.DistTo(pos); - if (dist < config->min) - { - enemy_found = true; - break; - } - // Found someone within min and max range - if (dist < config->max) - { - out_of_reach = false; - // Should vischeck this one - vischeck_positions.push_back(&pos); - if (dist < min_dist_away) - min_dist_away = dist; - } - } - // Too close/Too far away - if (enemy_found || out_of_reach) - continue; - - // Area is valid (Distance wise) - areas.push_back({ &area, min_dist_away, vischeck_positions }); - } - - // Sort, be as close to preferred as possible - std::sort(areas.begin(), areas.end(), [&](area_struct a, area_struct b) { return std::abs(a.min_distance - config->preferred) < std::abs(b.min_distance - config->preferred); }); - - // Still need to do vischeck stuff - for (auto &area : areas) - { - // Is the enemy able to see the area? - bool can_see_area = false; - // Area Center - auto area_pos = area.navarea->m_center; - // Don't want to instantly hit the floor - area_pos.z += 42.0f; - // Loop all valid enemies - for (auto pos : area.enemy_list) - { - if (IsVectorVisible(area_pos, *pos)) - { - can_see_area = true; - break; - } - } - // Someone can see the area. Abort! (Gunslinger Engineer does not care) - if (can_see_area && !HasGunslinger(LOCAL_E)) - continue; - // All good! - building_spots.push_back(std::pair(area.navarea, area.navarea->m_center)); - } - } -} - -static bool navToSniperSpot() -{ - // Don't path if you already have commands. But also don't error out. - if (!nav::ReadyForCommands || current_task != task::none) - return true; - // Wait arround a bit before pathing again - if (!wait_until_path.check(2000)) - return false; - // Max 10 attempts - for (int attempts = 0; attempts < 10; attempts++) - { - // Get a random sniper spot - auto random = select_randomly(sniper_spots.begin(), sniper_spots.end()); - // Check if spot is considered safe (no sentry, no sticky) - if (!nav::isSafe(random.base()->first)) - continue; - // Try to nav there - if (nav::navTo(random.base()->second, 1, true, true, false)) - { - current_task = { task::sniper_spot, 1 }; - } - } - return false; -} - -static bool navToBuildingSpot() -{ - // Don't path if you already have commands. But also don't error out. - if (!nav::ReadyForCommands || (current_task != task::engineer && current_task != task::none)) - return true; - // Wait a bit before pathing again - if (!wait_until_path_engineer.test_and_set(2000)) - return false; - // Max 10 attempts - for (int attempts = 0; attempts < 10 && attempts < building_spots.size(); attempts++) - { - // Get a Building spot - auto &area = building_spots[attempts]; - // Check if spot is considered safe (no sentry, no sticky) - if (!nav::isSafe(area.first)) - continue; - // Try to nav there - if (nav::navTo(area.second, 5, true, true, false)) - { - current_task = { task::engineer, 5 }; - current_build_area = area.first; - current_engineer_task = task::engineer_task::goto_build_spot; - return true; - } - } - return false; -} - -static Building selectBuilding() -{ - int metal = CE_INT(LOCAL_E, netvar.m_iAmmo + 12); - int metal_sentry = 130; - - // We have a mini sentry, costs less - if (HasGunslinger(LOCAL_E)) - metal_sentry = 100; - - // Do we already have these? - bool sentry_built = false; - bool dispenser_built = false; - // Loop all buildings - for (auto &building : local_buildings) - { - if (building->m_iClassID() == CL_CLASS(CObjectSentrygun)) - sentry_built = true; - else if (building->m_iClassID() == CL_CLASS(CObjectDispenser)) - dispenser_built = true; - } - - if (metal >= metal_sentry && !sentry_built) - return Sentry; - else if (metal >= 100 && !dispenser_built) - return Dispenser; - return None; -} - -static success_build buildBuilding() -{ - int metal = CE_INT(LOCAL_E, netvar.m_iAmmo + 12); - // Out of Metal - if (metal < 100) - return Failure; - // Last building - static Building last_building = selectBuilding(); - // Get best building to build right now - Building building = selectBuilding(); - // Reset Rotation on these conditions - if (building != last_building || build_timer.check(1000)) - { - // We changed the target building, means it was successful! - if (!build_timer.check(1000)) - return Success; - - // No building - if (building == None) - return Failure; - - build_attempts = 0; - build_current_rotation = -180.0f; - build_timer.update(); - } - // No building - if (building == None) - return Failure; - - last_building = building; - build_timer.update(); - if (rotation_timer.test_and_set(300)) - { - // Look slightly downwards for building process - current_user_cmd->viewangles.x = 20.0f; - // Set Yaw - current_user_cmd->viewangles.y = build_current_rotation; - // Rotate - build_current_rotation += 20.0f; - build_attempts++; - // Put building in hand if not already - if (hacks::shared::misc::getCarriedBuilding() == -1 && build_command_timer.test_and_set(50)) - g_IEngine->ClientCmd_Unrestricted(("build " + std::to_string(building)).c_str()); - } - else if (rotation_timer.check(200)) - { - if (hacks::shared::misc::getCarriedBuilding() != -1) - { - int carried_building = hacks::shared::misc::getCarriedBuilding(); - // It works! Place building - if (CE_INT(ENTITY(carried_building), netvar.m_bCanPlace)) - current_user_cmd->buttons |= IN_ATTACK; - } - } - // Bad area - if (build_attempts >= 14) - { - blacklisted_build_spots.push_back(current_build_area); - return Failure; - } - return Unknown; -} - -static bool navToBuilding(CachedEntity *target = nullptr) -{ - if (local_buildings.size()) - { - int priority = 5; - if (current_engineer_task == task::staynear_engineer) - priority = 7; - // Just grab target and nav there - if (target) - if (nav::navTo(target->m_vecOrigin(), priority, true)) - { - current_task = { task::engineer, priority }; - current_engineer_task = task::engineer_task::goto_building; - return true; - } - // Nav to random building - for (auto &building : local_buildings) - { - if (nav::navTo(building->m_vecOrigin(), priority, true)) - { - current_task = { task::engineer, priority }; - current_engineer_task = task::engineer_task::goto_building; - return true; - } - } - } - return false; -} - -static bool engineerLogic() -{ - std::vector new_building_list; - for (auto &building : local_buildings) - if (CE_VALID(building) && !CE_INT(building, netvar.m_bPlacing)) - new_building_list.push_back(building); - local_buildings = new_building_list; - - // Overwrites and Not yet running engineer task - if ((current_task != task::engineer || current_engineer_task == task::engineer_task::nothing || current_engineer_task == task::engineer_task::staynear_engineer) && current_task != task::health && current_task != task::ammo) - { - // Already have a building - if (local_buildings.size()) - { - int metal = CE_INT(LOCAL_E, netvar.m_iAmmo + 12); - if (metal) - for (auto &building : local_buildings) - // Hey hit the building thanks (gunslinger engineer shouldn't care) - if (hacks::tf2::misc_aimbot::ShouldHitBuilding(building) && !HasGunslinger(LOCAL_E)) - { - if (navToBuilding(building)) - return true; - } - - // Gunslinger engineer should run at people, given their building isn't too far away - if (HasGunslinger(LOCAL_E)) - { - // Deconstruct too far away buildings - for (auto &building : local_buildings) - { - // Too far away, destroy it - if (building->m_vecOrigin().DistTo(LOCAL_E->m_vecOrigin()) >= 1800.0f) - { - Building building_type = None; - switch (building->m_iClassID()) - { - case CL_CLASS(CObjectDispenser): - { - building_type = Dispenser; - break; - } - case CL_CLASS(CObjectTeleporter): - { - // We cannot reliably detect entrance and exit, so just destruct both but mark as "Entrance" - building_type = TP_Entrace; - break; - } - case CL_CLASS(CObjectSentrygun): - { - building_type = Sentry; - break; - } - } - // If we have a valid building - if (building_type != None) - { - // Destroy exit too because we have no idea what is what - if (building_type == TP_Entrace) - g_IEngine->ClientCmd_Unrestricted("destroy 3"); - g_IEngine->ClientCmd_Unrestricted(("destroy " + std::to_string(building_type)).c_str()); - } - } - } - stayNearEngineer(); - } - - else if (selectBuilding() != None) - { - // If we're near our buildings and have the metal, build another one - for (auto &building : local_buildings) - if (building->m_vecOrigin().DistTo(LOCAL_E->m_vecOrigin()) <= 300.0f) - { - current_task = { task::engineer, 4 }; - current_engineer_task = task::engineer_task::build_building; - return true; - } - // We're too far away, go to building - else if (navToBuilding()) - return true; - } - // If it's metal we're missing, get some metal - /*else if (metal < 100) - { - if ((dispenser_nav_timer.test_and_set(1000) && getDispenserHealthAndAmmo(metal)) || getHealthAndAmmo(metal)) - return true; - }*/ - // Else just Roam around the map and kill people - else if (stayNearEngineer()) - return true; - } - // Nav to a Building spot - else if (navToBuildingSpot()) - { - engineer_recheck.update(); - return false; - } - } - // Metal - int metal = CE_INT(LOCAL_E, netvar.m_iAmmo + 12); - /*if ((dispenser_nav_timer.test_and_set(1000) && getDispenserHealthAndAmmo(metal)) || getHealthAndAmmo(metal)) - return true;*/ - switch (current_engineer_task) - { - // Upgrade/repair - case (task::engineer_task::upgradeorrepair_building): - { - if (metal) - if (local_buildings.size()) - for (auto &building : local_buildings) - { - if (building->m_vecOrigin().DistTo(LOCAL_E->m_vecOrigin()) <= 300.0f) - { - if (hacks::tf2::misc_aimbot::ShouldHitBuilding(building)) - break; - } - } - - current_task = { task::engineer, 4 }; - current_engineer_task = task::engineer_task::nothing; - } - // Going to existing building - case (task::engineer_task::goto_building): - { - if (nav::ReadyForCommands) - { - bool found = false; - if (local_buildings.size()) - for (auto &building : local_buildings) - { - if (building->m_vecOrigin().DistTo(LOCAL_E->m_vecOrigin()) <= 300.0f) - { - if (metal && hacks::tf2::misc_aimbot::ShouldHitBuilding(building)) - { - current_task = { task::engineer, 4 }; - current_engineer_task = task::engineer_task::upgradeorrepair_building; - } - if (current_engineer_task != task::engineer_task::upgradeorrepair_building) - { - current_task = { task::engineer, 4 }; - current_engineer_task = task::engineer_task::nothing; - } - found = true; - } - } - if (found) - break; - - current_task = { task::engineer, 4 }; - current_engineer_task = task::engineer_task::nothing; - } - break; - } - // Going to spot to build - case (task::engineer_task::goto_build_spot): - { - // We Arrived, time to (try) to build! - if (nav::ReadyForCommands) - { - current_task = { task::engineer, 4 }; - current_engineer_task = task::engineer_task::build_building; - } - else if (engineer_recheck.test_and_set(15000)) - { - if (navToBuildingSpot()) - return true; - } - break; - } - // Build building - case (task::engineer_task::build_building): - { - auto status = buildBuilding(); - // Failed, Get a new Task - if (status == Failure) - { - current_task = { task::engineer, 4 }; - current_engineer_task = task::engineer_task::nothing; - } - else if (status == Success) - { - - current_task = { task::engineer, 4 }; - current_engineer_task = task::engineer_task::nothing; - return true; - } - // Still building - else if (status == Unknown) - return true; - break; - } - default: - break; - } - return false; -} - -static std::pair getNearestPlayerDistance(bool vischeck) -{ - float distance = FLT_MAX; - CachedEntity *best_ent = nullptr; - for (int i = 1; i <= g_IEngine->GetMaxClients(); i++) - { - CachedEntity *ent = ENTITY(i); - if (CE_VALID(ent) && ent->m_vecDormantOrigin() && ent->m_bAlivePlayer() && ent->m_bEnemy() && g_pLocalPlayer->v_Origin.DistTo(ent->m_vecOrigin()) < distance && player_tools::shouldTarget(ent) && (!vischeck || VisCheckEntFromEnt(LOCAL_E, ent))) - { - if (hacks::shared::aimbot::ignore_cloak && IsPlayerInvisible(ent)) - { - spy_cloak[i].update(); - continue; - } - if (!spy_cloak[i].check(*spy_ignore_time)) - continue; - distance = g_pLocalPlayer->v_Origin.DistTo(*ent->m_vecDormantOrigin()); - best_ent = ent; - } - } - return { best_ent, distance }; -} - -namespace stayNearHelpers -{ -// Check if the location is close enough/far enough and has a visual to target -static bool isValidNearPosition(Vector vec, Vector target, const bot_class_config &config) -{ - vec.z += 40; - target.z += 40; - float dist = vec.DistTo(target); - if (dist < config.min || dist > config.max) - return false; - if (!IsVectorVisible(vec, target, true, LOCAL_E, MASK_PLAYERSOLID)) - return false; - // Check if safe - if (!nav::isSafe(nav::findClosestNavSquare(target))) - return false; - return true; -} - -// Returns true if began pathing -static bool stayNearPlayer(CachedEntity *&ent, const bot_class_config &config, CNavArea *&result, bool engineer = false) -{ - if (!CE_VALID(ent)) - return false; - auto position = ent->m_vecDormantOrigin(); - if (!position) - return false; - // Get some valid areas - std::vector areas; - for (auto &area : nav::navfile->m_areas) - { - if (!isValidNearPosition(area.m_center, *position, config)) - continue; - areas.push_back(&area); - } - if (areas.empty()) - return false; - - const Vector ent_orig = *position; - // Area dist to target should be as close as possible to config.preferred - std::sort(areas.begin(), areas.end(), [&](CNavArea *a, CNavArea *b) { return std::abs(a->m_center.DistTo(ent_orig) - config.preferred) < std::abs(b->m_center.DistTo(ent_orig) - config.preferred); }); - - size_t size = 20; - if (areas.size() < size) - size = areas.size(); - - // Get some areas that are close to the player - std::vector preferred_areas(areas.begin(), areas.end()); - preferred_areas.resize(size / 2); - if (preferred_areas.empty()) - return false; - std::sort(preferred_areas.begin(), preferred_areas.end(), [](CNavArea *a, CNavArea *b) { return a->m_center.DistTo(g_pLocalPlayer->v_Origin) < b->m_center.DistTo(g_pLocalPlayer->v_Origin); }); - - preferred_areas.resize(size / 4); - if (preferred_areas.empty()) - return false; - for (auto &i : preferred_areas) - { - if (nav::navTo(i->m_center, 7, true, false)) - { - result = i; - if (engineer) - { - current_task = { task::engineer, 4 }; - current_engineer_task = task::staynear_engineer; - } - else - current_task = task::stay_near; - return true; - } - } - - for (size_t attempts = 0; attempts < size / 4; attempts++) - { - auto it = select_randomly(areas.begin(), areas.end()); - if (nav::navTo((*it.base())->m_center, 7, true, false)) - { - result = *it.base(); - if (engineer) - { - current_task = { task::engineer, 4 }; - current_engineer_task = task::staynear_engineer; - } - else - current_task = task::stay_near; - return true; - } - } - return false; -} - -// Loop thru all players and find one we can path to -static bool stayNearPlayers(const bot_class_config &config, CachedEntity *&result_ent, CNavArea *&result_area) -{ - std::vector players; - for (int i = 1; i <= g_IEngine->GetMaxClients(); i++) - { - CachedEntity *ent = ENTITY(i); - if (CE_INVALID(ent) || !g_pPlayerResource->isAlive(ent->m_IDX) || !ent->m_bEnemy() || !player_tools::shouldTarget(ent)) - continue; - if (hacks::shared::aimbot::ignore_cloak && IsPlayerInvisible(ent)) - { - spy_cloak[i].update(); - continue; - } - if (!spy_cloak[i].check(*spy_ignore_time)) - continue; - if (ent->m_vecDormantOrigin()) - players.push_back(ent); - } - if (players.empty()) - return false; - std::sort(players.begin(), players.end(), [](CachedEntity *a, CachedEntity *b) { - Vector position_a = *a->m_vecDormantOrigin(), position_b = *b->m_vecDormantOrigin(); - return position_a.DistTo(g_pLocalPlayer->v_Origin) < position_b.DistTo(g_pLocalPlayer->v_Origin); - }); - for (auto player : players) - { - if (stayNearPlayer(player, config, result_area)) - { - result_ent = player; - return true; - } - } - return false; -} -} // namespace stayNearHelpers - -// stayNear()'s Little Texan brother -static bool stayNearEngineer() -{ - static CachedEntity *last_target = nullptr; - static CNavArea *last_area = nullptr; - - // What distances do we have to use? - bot_class_config config = DIST_ENGINEER; - if (HasGunslinger(LOCAL_E)) - config = DIST_GUNSLINGER_ENGINEER; - - // Check if someone is too close to us and then target them instead - std::pair nearest = getNearestPlayerDistance(); - if (nearest.first && nearest.first != last_target && nearest.second < config.min) - if (stayNearHelpers::stayNearPlayer(nearest.first, config, last_area, true)) - { - last_target = nearest.first; - return true; - } - bool valid_dormant = false; - if (CE_VALID(last_target) && RAW_ENT(last_target)->IsDormant()) - { - if (last_target->m_vecDormantOrigin()) - valid_dormant = true; - } - if (current_task == task::stay_near) - { - static Timer invalid_area_time{}; - static Timer invalid_target_time{}; - // Do we already have a stay near target? Check if its still good. - if (CE_GOOD(last_target) || valid_dormant) - invalid_target_time.update(); - else - invalid_area_time.update(); - // Check if we still have LOS and are close enough/far enough - Vector position; - if (CE_GOOD(last_target) || valid_dormant) - { - position = *last_target->m_vecDormantOrigin(); - } - if ((CE_GOOD(last_target) || valid_dormant) && stayNearHelpers::isValidNearPosition(last_area->m_center, position, config)) - invalid_area_time.update(); - - if ((CE_GOOD(last_target) || valid_dormant) && (!g_pPlayerResource->isAlive(last_target->m_IDX) || !last_target->m_bEnemy() || !player_tools::shouldTarget(last_target) || !spy_cloak[last_target->m_IDX].check(*spy_ignore_time) || (hacks::shared::aimbot::ignore_cloak && IsPlayerInvisible(last_target)))) - { - if (hacks::shared::aimbot::ignore_cloak && IsPlayerInvisible(last_target)) - spy_cloak[last_target->m_IDX].update(); - nav::clearInstructions(); - current_engineer_task = task::engineer_task::nothing; - } - else if (invalid_area_time.test_and_set(300)) - { - current_engineer_task = task::engineer_task::nothing; - } - else if (invalid_target_time.test_and_set(5000)) - { - current_engineer_task = task::engineer_task::nothing; - } - } - // Are we doing nothing? Check if our current location can still attack our - // last target - if (current_engineer_task != task::engineer_task::staynear_engineer && (CE_GOOD(last_target) || valid_dormant) && g_pPlayerResource->isAlive(last_target->m_IDX) && last_target->m_bEnemy()) - { - if (hacks::shared::aimbot::ignore_cloak && IsPlayerInvisible(last_target)) - spy_cloak[last_target->m_IDX].update(); - if (spy_cloak[last_target->m_IDX].check(*spy_ignore_time)) - { - Vector position = *last_target->m_vecDormantOrigin(); - - if (stayNearHelpers::isValidNearPosition(g_pLocalPlayer->v_Origin, position, config)) - return true; - // If not, can we try pathing to our last target again? - if (stayNearHelpers::stayNearPlayer(last_target, config, last_area, true)) - return true; - } - last_target = nullptr; - } - - static Timer wait_until_stay_near{}; - if (current_engineer_task == task::engineer_task::staynear_engineer) - return true; - else if (wait_until_stay_near.test_and_set(4000)) - { - // We're doing nothing? Do something! - return stayNearHelpers::stayNearPlayers(config, last_target, last_area); - } - - return false; -} -// Main stay near function -static bool stayNear() -{ - static CachedEntity *last_target = nullptr; - static CNavArea *last_area = nullptr; - - // What distances do we have to use? - const bot_class_config *config; - if (spy_mode) - { - config = &DIST_SPY; - } - else if (heavy_mode) - { - config = &DIST_OTHER; - } - else - { - config = &DIST_SNIPER; - } - - // Check if someone is too close to us and then target them instead - std::pair nearest = getNearestPlayerDistance(); - if (nearest.first && nearest.first != last_target && nearest.second < config->min) - if (stayNearHelpers::stayNearPlayer(nearest.first, *config, last_area)) - { - last_target = nearest.first; - return true; - } - bool valid_dormant = false; - if (CE_VALID(last_target) && RAW_ENT(last_target)->IsDormant()) - { - if (last_target->m_vecDormantOrigin()) - valid_dormant = true; - } - if (current_task == task::stay_near) - { - static Timer invalid_area_time{}; - static Timer invalid_target_time{}; - // Do we already have a stay near target? Check if its still good. - if (CE_GOOD(last_target) || valid_dormant) - invalid_target_time.update(); - else - invalid_area_time.update(); - // Check if we still have LOS and are close enough/far enough - Vector position; - if (CE_GOOD(last_target) || valid_dormant) - { - position = *last_target->m_vecDormantOrigin(); - } - if ((CE_GOOD(last_target) || valid_dormant) && stayNearHelpers::isValidNearPosition(last_area->m_center, position, *config)) - invalid_area_time.update(); - - if ((CE_GOOD(last_target) || valid_dormant) && (!g_pPlayerResource->isAlive(last_target->m_IDX) || !last_target->m_bEnemy() || !player_tools::shouldTarget(last_target) || !spy_cloak[last_target->m_IDX].check(*spy_ignore_time) || (hacks::shared::aimbot::ignore_cloak && IsPlayerInvisible(last_target)))) - { - if (hacks::shared::aimbot::ignore_cloak && IsPlayerInvisible(last_target)) - spy_cloak[last_target->m_IDX].update(); - nav::clearInstructions(); - current_task = task::none; - } - else if (invalid_area_time.test_and_set(300)) - { - current_task = task::none; - } - else if (invalid_target_time.test_and_set(5000)) - { - current_task = task::none; - } - } - // Are we doing nothing? Check if our current location can still attack our - // last target - if (current_task != task::stay_near && (CE_GOOD(last_target) || valid_dormant) && g_pPlayerResource->isAlive(last_target->m_IDX) && last_target->m_bEnemy()) - { - if (hacks::shared::aimbot::ignore_cloak && IsPlayerInvisible(last_target)) - spy_cloak[last_target->m_IDX].update(); - if (spy_cloak[last_target->m_IDX].check(*spy_ignore_time)) - { - Vector position = *last_target->m_vecDormantOrigin(); - - if (stayNearHelpers::isValidNearPosition(g_pLocalPlayer->v_Origin, position, *config)) - return true; - // If not, can we try pathing to our last target again? - if (stayNearHelpers::stayNearPlayer(last_target, *config, last_area)) - return true; - } - last_target = nullptr; - } - - static Timer wait_until_stay_near{}; - if (current_task == task::stay_near) - { - return true; - } - else if (wait_until_stay_near.test_and_set(1000)) - { - // We're doing nothing? Do something! - return stayNearHelpers::stayNearPlayers(*config, last_target, last_area); - } - - return false; -} - -static inline bool hasLowAmmo() +// Should we search ammo at all? +bool shouldSearchAmmo() { if (CE_BAD(LOCAL_W)) return false; + // Priority too high + if (navparser::NavEngine::current_priority > ammo) + return false; + int *weapon_list = (int *) ((uint64_t)(RAW_ENT(LOCAL_E)) + netvar.hMyWeapons); if (!weapon_list) return false; @@ -1089,195 +97,823 @@ static inline bool hasLowAmmo() return false; } -static std::vector getDispensers() +// Get Valid Dispensers (Used for health/ammo) +std::vector getDispensers() { - std::vector dispensers; - for (int i = 1; i <= HIGHEST_ENTITY; i++) + std::vector entities; + for (int i = g_IEngine->GetMaxClients() + 1; i < MAX_ENTITIES; i++) { CachedEntity *ent = ENTITY(i); - if (CE_INVALID(ent)) + if (CE_BAD(ent) || ent->m_iClassID() != CL_CLASS(CObjectDispenser) || ent->m_iTeam() != g_pLocalPlayer->team) continue; - if (!ent->m_vecDormantOrigin()) + if (CE_BYTE(ent, netvar.m_bCarryDeploy) || CE_BYTE(ent, netvar.m_bHasSapper) || CE_BYTE(ent, netvar.m_bBuilding)) continue; - if (ent->m_iClassID() != CL_CLASS(CObjectDispenser) || ent->m_bEnemy()) + + // This fixes the fact that players can just place dispensers in unreachable locations + auto local_nav = navparser::NavEngine::findClosestNavSquare(ent->m_vecOrigin()); + if (local_nav->getNearestPoint(ent->m_vecOrigin().AsVector2D()).DistTo(ent->m_vecOrigin()) > 300.0f || local_nav->getNearestPoint(ent->m_vecOrigin().AsVector2D()).z - ent->m_vecOrigin().z > navparser::PLAYER_JUMP_HEIGHT) continue; - if (CE_BYTE(ent, netvar.m_bHasSapper)) - continue; - if (CE_BYTE(ent, netvar.m_bBuilding)) - continue; - if (CE_BYTE(ent, netvar.m_bPlacing)) - continue; - dispensers.push_back(*ent->m_vecDormantOrigin()); + entities.push_back(ent); } - std::sort(dispensers.begin(), dispensers.end(), [](Vector &a, Vector &b) { return g_pLocalPlayer->v_Origin.DistTo(a) < g_pLocalPlayer->v_Origin.DistTo(b); }); - return dispensers; + // Sort by distance, closer is better + std::sort(entities.begin(), entities.end(), [](CachedEntity *a, CachedEntity *b) { return a->m_flDistance() < b->m_flDistance(); }); + return entities; } -static bool getDispenserHealthAndAmmo(int metal) +// Get entities of given itemtypes (Used for health/ammo) +std::vector getEntities(const std::vector &itemtypes) { - // Timeout for standing next to dispenser - static Timer dispenser_timeout{}; - // Cooldown after standing next to one for too long - static Timer dispenser_cooldown{}; - float health = static_cast(LOCAL_E->m_iHealth()) / LOCAL_E->m_iMaxHealth(); - bool lowAmmo = hasLowAmmo(); - if (metal != -1) + std::vector entities; + for (int i = g_IEngine->GetMaxClients() + 1; i < MAX_ENTITIES; i++) { - lowAmmo = metal < 100 && selectBuilding() == None; - if (current_engineer_task == task::engineer_task::upgradeorrepair_building) - lowAmmo = metal == 0; - } - // Check if we should cancel this task - if (current_task == task::dispenser) - { - if (health > 0.99f && !lowAmmo) + CachedEntity *ent = ENTITY(i); + if (CE_BAD(ent)) + continue; + for (auto &itemtype : itemtypes) { - nav::clearInstructions(); - current_task = task::none; - return false; + if (ent->m_ItemType() == itemtype) + { + entities.push_back(ent); + break; + } } - if (health > 0.64f && !lowAmmo) - current_task.priority = 3; } + // Sort by distance, closer is better + std::sort(entities.begin(), entities.end(), [](CachedEntity *a, CachedEntity *b) { return a->m_flDistance() < b->m_flDistance(); }); + return entities; +} - // Check if we're standing next to a dispenser for too long. - if (current_task == task::dispenser) +// Find health if needed +bool getHealth(bool low_priority = false) +{ + Priority_list priority = low_priority ? lowprio_health : health; + if (!health_cooldown.check(1000)) + return navparser::NavEngine::current_priority == priority; + if (shouldSearchHealth(low_priority)) { + // Already pathing, only try to repath every 2s + if (navparser::NavEngine::current_priority == priority) + { + static Timer repath_timer; + if (!repath_timer.test_and_set(2000)) + return true; + } + auto healthpacks = getEntities({ ITEM_HEALTH_SMALL, ITEM_HEALTH_MEDIUM, ITEM_HEALTH_LARGE }); + auto dispensers = getDispensers(); + + auto total_ents = healthpacks; + + // Add dispensers and sort list again + if (!dispensers.empty()) + { + total_ents.reserve(healthpacks.size() + dispensers.size()); + total_ents.insert(total_ents.end(), dispensers.begin(), dispensers.end()); + std::sort(total_ents.begin(), total_ents.end(), [](CachedEntity *a, CachedEntity *b) { return a->m_flDistance() < b->m_flDistance(); }); + } + + for (auto healthpack : total_ents) + // If we succeeed, don't try to path to other packs + if (navparser::NavEngine::navTo(healthpack->m_vecOrigin(), priority, true, healthpack->m_vecOrigin().DistToSqr(g_pLocalPlayer->v_Origin) > 200.0f * 200.0f)) + return true; + health_cooldown.update(); + } + else if (navparser::NavEngine::current_priority == priority) + navparser::NavEngine::cancelPath(); + return false; +} + +// Find ammo if needed +bool getAmmo() +{ + if (!ammo_cooldown.check(1000)) + return navparser::NavEngine::current_priority == ammo; + if (shouldSearchAmmo()) + { + // Already pathing, only try to repath every 2s + if (navparser::NavEngine::current_priority == ammo) + { + static Timer repath_timer; + if (!repath_timer.test_and_set(2000)) + return true; + } + auto ammopacks = getEntities({ ITEM_AMMO_SMALL, ITEM_AMMO_MEDIUM, ITEM_AMMO_LARGE }); auto dispensers = getDispensers(); - // If near enough to dispenser - if (dispensers.size() && dispensers[0].DistTo(g_pLocalPlayer->v_Origin) < 60.0f) + + auto total_ents = ammopacks; + + // Add dispensers and sort list again + if (!dispensers.empty()) { - // Standing next to it for too long - if (dispenser_timeout.check(10000)) - { - dispenser_cooldown.update(); - current_task = task::none; - nav::clearInstructions(); - return false; - } - } - else - { - dispenser_timeout.update(); + total_ents.reserve(ammopacks.size() + dispensers.size()); + total_ents.insert(total_ents.end(), dispensers.begin(), dispensers.end()); + std::sort(total_ents.begin(), total_ents.end(), [](CachedEntity *a, CachedEntity *b) { return a->m_flDistance() < b->m_flDistance(); }); } + for (auto ammopack : total_ents) + // If we succeeed, don't try to path to other packs + if (navparser::NavEngine::navTo(ammopack->m_vecOrigin(), ammo, true, ammopack->m_vecOrigin().DistToSqr(g_pLocalPlayer->v_Origin) > 200.0f * 200.0f)) + return true; + ammo_cooldown.update(); } - - // If Low ammo/Health - if (current_task != task::dispenser) - if (health < 0.64f || (current_task.priority < 3 && health < 0.99f) || lowAmmo) - { - std::vector dispensers = getDispensers(); - - for (auto &dispenser : dispensers) - { - // Nav To Dispenser - if (nav::navTo(dispenser, health < 0.64f || lowAmmo ? 11 : 3, true, false)) - { - // On Success, update task - current_task = { task::dispenser, health < 0.64f || lowAmmo ? 10 : 3 }; - } - } - } - if (current_task == task::dispenser && current_task.priority == 10) - return true; - else - return false; + else if (navparser::NavEngine::current_priority == ammo) + navparser::NavEngine::cancelPath(); + return false; } -static bool getHealthAndAmmo(int metal) +// Former is position, latter is until which tick it is ignored +std::vector> sniper_spots; + +// Used for time between refreshing sniperspots +static Timer refresh_sniperspots_timer{}; +void refreshSniperSpots() { - float health = static_cast(LOCAL_E->m_iHealth()) / LOCAL_E->m_iMaxHealth(); - bool lowAmmo = hasLowAmmo(); - if (metal != -1) - { - lowAmmo = metal < 100 && selectBuilding() == None; - if (current_engineer_task == task::engineer_task::upgradeorrepair_building) - lowAmmo = metal == 0; - } - // Check if we should cancel this task - if (current_task == task::health || current_task == task::ammo) - { - if (health > 0.99f && !lowAmmo) - { - nav::clearInstructions(); - current_task = task::none; - return false; - } - if (health > 0.64f && !lowAmmo) - current_task.priority = 3; - } - - // If Low Ammo/Health - if (current_task != task::health && current_task != task::ammo) - if (health < 0.64f || (current_task.priority < 3 && health < 0.99f) || lowAmmo) - { - bool gethealth; - if (health < 0.64f) - gethealth = true; - else if (lowAmmo) - gethealth = false; - else - gethealth = true; - - if (gethealth) - { - std::vector healthpacks; - for (int i = 1; i <= HIGHEST_ENTITY; i++) - { - CachedEntity *ent = ENTITY(i); - if (CE_BAD(ent)) - continue; - if (ent->m_ItemType() != ITEM_HEALTH_SMALL && ent->m_ItemType() != ITEM_HEALTH_MEDIUM && ent->m_ItemType() != ITEM_HEALTH_LARGE) - continue; - healthpacks.push_back(ent->m_vecOrigin()); - } - std::sort(healthpacks.begin(), healthpacks.end(), [](Vector &a, Vector &b) { return g_pLocalPlayer->v_Origin.DistTo(a) < g_pLocalPlayer->v_Origin.DistTo(b); }); - for (auto &pack : healthpacks) - { - if (nav::navTo(pack, health < 0.64f || lowAmmo ? 10 : 3, true, false)) - { - current_task = { task::health, health < 0.64f ? 10 : 3 }; - return true; - } - } - } - else - { - std::vector ammopacks; - for (int i = 1; i <= HIGHEST_ENTITY; i++) - { - CachedEntity *ent = ENTITY(i); - if (CE_BAD(ent)) - continue; - if (ent->m_ItemType() != ITEM_AMMO_SMALL && ent->m_ItemType() != ITEM_AMMO_MEDIUM && ent->m_ItemType() != ITEM_AMMO_LARGE) - continue; - ammopacks.push_back(ent->m_vecOrigin()); - } - std::sort(ammopacks.begin(), ammopacks.end(), [](Vector &a, Vector &b) { return g_pLocalPlayer->v_Origin.DistTo(a) < g_pLocalPlayer->v_Origin.DistTo(b); }); - for (auto &pack : ammopacks) - { - if (nav::navTo(pack, health < 0.64f || lowAmmo ? 9 : 3, true, false)) - { - current_task = { task::ammo, 10 }; - return true; - } - } - } - } - if ((current_task == task::health || current_task == task::ammo) && current_task.priority == 10) - return true; - else - return false; -} - -static void autoJump() -{ - static Timer last_jump{}; - if (!last_jump.test_and_set(200)) + if (!refresh_sniperspots_timer.test_and_set(60000)) return; - if (getNearestPlayerDistance().second <= *jump_distance) - current_user_cmd->buttons |= IN_JUMP | IN_DUCK; + sniper_spots.clear(); + + // Search all nav areas for valid sniper spots + for (auto &area : navparser::NavEngine::getNavFile()->m_areas) + for (auto &hiding_spot : area.m_hidingSpots) + // Spots actually marked for sniping + if (hiding_spot.IsExposed() || hiding_spot.IsGoodSniperSpot() || hiding_spot.IsIdealSniperSpot()) + sniper_spots.emplace_back(hiding_spot.m_pos, 0); +} + +#if ENABLE_VISUALS +std::vector slight_danger_drawlist_normal; +std::vector slight_danger_drawlist_dormant; +#endif +static Timer blacklist_update_timer{}; +static Timer dormant_update_timer{}; +void updateEnemyBlacklist() +{ + bool should_run_normal = blacklist_update_timer.test_and_set(*blacklist_delay); + bool should_run_dormant = blacklist_dormat && dormant_update_timer.test_and_set(*blacklist_delay_dormat); + // Don't run since we do not care here + if (!should_run_dormant && !should_run_normal) + return; + + // Clear blacklist for normal entities + if (should_run_normal) + navparser::NavEngine::clearFreeBlacklist(navparser::ENEMY_NORMAL); + // Clear blacklist for dormant entities + if (should_run_dormant) + navparser::NavEngine::clearFreeBlacklist(navparser::ENEMY_DORMANT); + + // Store the danger of the invidual nav areas + std::unordered_map dormant_slight_danger; + std::unordered_map normal_slight_danger; + + // This is used to cache Dangerous areas between ents + std::unordered_map> ent_marked_dormant_slight_danger; + std::unordered_map> ent_marked_normal_slight_danger; + + std::vector> checked_origins; + for (int i = 1; i <= g_IEngine->GetMaxClients(); i++) + { + CachedEntity *ent = ENTITY(i); + // Entity is generally invalid, ignore + if (CE_INVALID(ent) || !g_pPlayerResource->isAlive(i)) + continue; + // On our team, do not care + if (g_pPlayerResource->GetTeam(i) == g_pLocalPlayer->team) + continue; + + bool is_dormant = CE_BAD(ent); + // Should not run on dormant and entity is dormant, ignore. + if (!should_run_dormant && is_dormant) + continue; + // Should not run on normal entity and entity is not dormant, ignore + else if (!should_run_normal && !is_dormant) + continue; + + // Avoid excessive calls by ignoring new checks if people are too close to eachother + auto origin = ent->m_vecDormantOrigin(); + if (!origin) + continue; + bool should_check = true; + + // Find already dangerous marked areas by other entities + auto to_loop = is_dormant ? &ent_marked_dormant_slight_danger : &ent_marked_normal_slight_danger; + + // Add new danger entries + auto to_mark = is_dormant ? &dormant_slight_danger : &normal_slight_danger; + + for (auto &checked_origin : checked_origins) + { + // If this origin is closer than a quarter of the min HU (or less than 100 HU) to a cached one, don't go through + // all nav areas again DistToSqr is much faster than DistTo which is why we use it here + auto distance = selected_config.min_slight_danger; + + distance *= 0.25f; + distance = std::max(100.0f, distance); + + // Square the distance + distance *= distance; + + if ((*origin).DistToSqr(checked_origin.second) < distance) + { + should_check = false; + + bool is_absolute_danger = distance < selected_config.min_full_danger; + if (!is_absolute_danger && (enable_slight_danger_when_capping || navparser::NavEngine::current_priority != capture)) + for (auto &area : (*to_loop)[checked_origin.first]) + { + (*to_mark)[area]++; + if ((*to_mark)[area] >= *blacklist_slightdanger_limit) + (*navparser::NavEngine::getFreeBlacklist())[area] = is_dormant ? navparser::ENEMY_DORMANT : navparser::ENEMY_NORMAL; + } + + break; + } + } + if (!should_check) + continue; + + // Now check which areas they are close to + for (CNavArea &nav_area : navparser::NavEngine::getNavFile()->m_areas) + { + float distance = nav_area.m_center.DistTo(*origin); + float slight_danger_dist = selected_config.min_slight_danger; + float absolute_danger_dist = selected_config.min_full_danger; + + // Not dangerous, Still don't bump + if (!player_tools::shouldTarget(ent)) + { + slight_danger_dist = navparser::PLAYER_WIDTH * 1.2f; + absolute_danger_dist = navparser::PLAYER_WIDTH * 1.2f; + } + + // Too close to count as slight danger + bool is_absolute_danger = distance < absolute_danger_dist; + if (distance < slight_danger_dist) + { + // Add as marked area + (*to_loop)[ent].push_back(&nav_area); + + // Just slightly dangerous, only mark as such if it's clear + if (!is_absolute_danger && (enable_slight_danger_when_capping || navparser::NavEngine::current_priority != capture)) + { + (*to_mark)[&nav_area]++; + if ((*to_mark)[&nav_area] < *blacklist_slightdanger_limit) + continue; + } + (*navparser::NavEngine::getFreeBlacklist())[&nav_area] = is_dormant ? navparser::ENEMY_DORMANT : navparser::ENEMY_NORMAL; + } + } + checked_origins.emplace_back(ent, *origin); + } +#if ENABLE_VISUALS + if (should_run_dormant) + slight_danger_drawlist_dormant.clear(); + if (should_run_normal) + slight_danger_drawlist_normal.clear(); + + // Store slight danger areas for drawing + if (!normal_slight_danger.empty()) + { + for (auto &area : normal_slight_danger) + if (area.second < *blacklist_slightdanger_limit) + slight_danger_drawlist_normal.push_back(area.first->m_center); + } + if (!dormant_slight_danger.empty()) + { + for (auto &area : dormant_slight_danger) + if (area.second < *blacklist_slightdanger_limit) + slight_danger_drawlist_dormant.push_back(area.first->m_center); + } +#endif +} + +// Roam around map +bool doRoam() +{ + static Timer fail_timer; + // No sniper spots :shrug: + if (sniper_spots.empty()) + return false; + // Failed recently, wait a while + if (!fail_timer.check(1000)) + return false; + // Don't overwrite current roam + if (navparser::NavEngine::current_priority == patrol) + return false; + + // Get closest sniper spots + std::sort(sniper_spots.begin(), sniper_spots.end(), [](std::pair a, std::pair b) { return a.first.DistTo(g_pLocalPlayer->v_Origin) < b.first.DistTo(g_pLocalPlayer->v_Origin); }); + + bool tried_pathing = false; + for (auto &sniper_spot : sniper_spots) + { + // Timed out + if (sniper_spot.second > g_GlobalVars->tickcount) + continue; + + tried_pathing = true; + + // Ignore for spot for 30s + sniper_spot.second = TICKCOUNT_TIMESTAMP(30); + if (navparser::NavEngine::navTo(sniper_spot.first, patrol)) + return true; + } + + // Every sniper spot is on cooldown, refresh cooldowns + if (!tried_pathing) + for (auto &spot : sniper_spots) + spot.second = 0; + // Failed, time out + fail_timer.update(); + + return false; +} + +// Check if an area is valid for stay near. the Third parameter is to save some performance. +bool isAreaValidForStayNear(Vector ent_origin, CNavArea *area, bool fix_local_z = true) +{ + if (fix_local_z) + ent_origin.z += navparser::PLAYER_JUMP_HEIGHT; + auto area_origin = area->m_center; + area_origin.z += navparser::PLAYER_JUMP_HEIGHT; + + // Do all the distance checks + float distance = ent_origin.DistToSqr(area_origin); + + // Too close + if (distance < selected_config.min_full_danger * selected_config.min_full_danger) + return false; + // Blacklisted + if (navparser::NavEngine::getFreeBlacklist()->find(area) != navparser::NavEngine::getFreeBlacklist()->end()) + return false; + // Too far away + if (distance > selected_config.max * selected_config.max) + return false; + // Attempt to vischeck + if (!IsVectorVisibleNavigation(ent_origin, area_origin)) + return false; + return true; +} + +// Actual logic, used to de-duplicate code +bool stayNearTarget(CachedEntity *ent) +{ + auto ent_origin = ent->m_vecDormantOrigin(); + // No origin recorded, don't bother + if (!ent_origin) + return false; + + // Add the vischeck height + ent_origin->z += navparser::PLAYER_JUMP_HEIGHT; + + // Use std::pair to avoid using the distance functions more than once + std::vector> good_areas{}; + + for (auto &area : navparser::NavEngine::getNavFile()->m_areas) + { + auto area_origin = area.m_center; + + // Is this area valid for stay near purposes? + if (!isAreaValidForStayNear(*ent_origin, &area, false)) + continue; + + float distance = (*ent_origin).DistToSqr(area_origin); + // Good area found + good_areas.push_back(std::pair(&area, distance)); + } + // Sort based on distance + if (selected_config.prefer_far) + std::sort(good_areas.begin(), good_areas.end(), [](std::pair a, std::pair b) { return a.second > b.second; }); + else + std::sort(good_areas.begin(), good_areas.end(), [](std::pair a, std::pair b) { return a.second < b.second; }); + + // If we're not already pathing we should reallign with the center of the area + bool should_path_to_local = !navparser::NavEngine::isPathing(); + // Try to path to all the good areas, based on distance + for (auto &area : good_areas) + if (navparser::NavEngine::navTo(area.first->m_center, staynear, true, should_path_to_local)) + return true; + + return false; +} + +// A bunch of basic checks to ensure we don't try to target an invalid entity +bool isStayNearTargetValid(CachedEntity *ent) +{ + return CE_VALID(ent) && g_pPlayerResource->isAlive(ent->m_IDX) && ent->m_IDX != g_pLocalPlayer->entity_idx && g_pLocalPlayer->team != ent->m_iTeam() && player_tools::shouldTarget(ent) && !IsPlayerInvisible(ent) && !IsPlayerInvulnerable(ent); +} + +// Try to stay near enemies and stalk them (or in case of sniper, try to stay far from them +// and snipe them) +bool stayNear() +{ + PROF_SECTION(stayNear) + static Timer staynear_cooldown{}; + static CachedEntity *previous_target = nullptr; + + // Stay near is expensive so we have to cache. We achieve this by only checking a pre-determined amount of players every + // CreateMove + constexpr int MAX_STAYNEAR_CHECKS_RANGE = 3; + constexpr int MAX_STAYNEAR_CHECKS_CLOSE = 2; + static int lowest_check_index = 0; + + // Stay near is off + if (!stay_near) + return false; + // Don't constantly path, it's slow. + // Far range classes do not need to repath nearly as often as close range ones. + if (!staynear_cooldown.test_and_set(selected_config.prefer_far ? 2000 : 500)) + return navparser::NavEngine::current_priority == staynear; + + // Too high priority, so don't try + if (navparser::NavEngine::current_priority > staynear) + return false; + + // Check and use our previous target if available + if (isStayNearTargetValid(previous_target)) + { + auto ent_origin = previous_target->m_vecDormantOrigin(); + if (ent_origin) + { + // Check if current target area is valid + if (navparser::NavEngine::isPathing()) + { + auto crumbs = navparser::NavEngine::getCrumbs(); + // We cannot just use the last crumb, as it is always nullptr + if (crumbs->size() > 1) + { + auto last_crumb = (*crumbs)[crumbs->size() - 2]; + // Area is still valid, stay on it + if (isAreaValidForStayNear(*ent_origin, last_crumb.navarea)) + return true; + } + } + // Else Check our origin for validity (Only for ranged classes) + else if (selected_config.prefer_far && isAreaValidForStayNear(*ent_origin, navparser::NavEngine::findClosestNavSquare(LOCAL_E->m_vecOrigin()))) + return true; + } + // Else we try to path again + if (stayNearTarget(previous_target)) + return true; + // Failed, invalidate previous target and try others + previous_target = nullptr; + } + + auto advance_count = selected_config.prefer_far ? MAX_STAYNEAR_CHECKS_RANGE : MAX_STAYNEAR_CHECKS_CLOSE; + + // Ensure it is in bounds and also wrap around + if (lowest_check_index > g_IEngine->GetMaxClients()) + lowest_check_index = 0; + + int calls = 0; + // Test all entities + for (int i = lowest_check_index; i <= g_IEngine->GetMaxClients(); i++) + { + if (calls >= advance_count) + break; + calls++; + lowest_check_index++; + CachedEntity *ent = ENTITY(i); + if (!isStayNearTargetValid(ent)) + { + calls--; + continue; + } + // Succeeded pathing + if (stayNearTarget(ent)) + { + previous_target = ent; + return true; + } + } + // Stay near failed to find any good targets, add extra delay + staynear_cooldown.last += std::chrono::seconds(3); + return false; +} + +// Basically the same as isAreaValidForStayNear, but some restrictions lifted. +bool isAreaValidForSnipe(Vector ent_origin, Vector area_origin, bool fix_sentry_z = true) +{ + if (fix_sentry_z) + ent_origin.z += 40.0f; + area_origin.z += navparser::PLAYER_JUMP_HEIGHT; + + float distance = ent_origin.DistToSqr(area_origin); + // Too close to be valid + if (distance <= (1100.0f + navparser::HALF_PLAYER_WIDTH) * (1100.0f + navparser::HALF_PLAYER_WIDTH)) + return false; + // Fails vischeck, bad + if (!IsVectorVisibleNavigation(area_origin, ent_origin)) + return false; + return true; +} + +// Try to snipe the sentry +bool tryToSnipe(CachedEntity *ent) +{ + auto ent_origin = GetBuildingPosition(ent); + // Add some z to dormant sentries as it only returns origin + if (CE_BAD(ent)) + ent_origin.z += 40.0f; + + std::vector> good_areas; + for (auto &area : navparser::NavEngine::getNavFile()->m_areas) + { + // Not usable + if (!isAreaValidForSnipe(ent_origin, area.m_center, false)) + continue; + good_areas.push_back(std::pair(&area, area.m_center.DistToSqr(ent_origin))); + } + + // Sort based on distance + if (selected_config.prefer_far) + std::sort(good_areas.begin(), good_areas.end(), [](std::pair a, std::pair b) { return a.second > b.second; }); + else + std::sort(good_areas.begin(), good_areas.end(), [](std::pair a, std::pair b) { return a.second < b.second; }); + + for (auto &area : good_areas) + if (navparser::NavEngine::navTo(area.first->m_center, snipe_sentry)) + return true; + return false; +} + +// Is our target valid? +bool isSnipeTargetValid(CachedEntity *ent) +{ + return CE_VALID(ent) && ent->m_bAlivePlayer() && ent->m_iTeam() != g_pLocalPlayer->team && ent->m_iClassID() == CL_CLASS(CObjectSentrygun); +} + +// Try to Snipe sentries +bool snipeSentries() +{ + static Timer sentry_snipe_cooldown; + static CachedEntity *previous_target = nullptr; + + if (!snipe_sentries) + return false; + + // Sentries don't move often, so we can use a slightly longer timer + if (!sentry_snipe_cooldown.test_and_set(2000)) + return navparser::NavEngine::current_priority == snipe_sentry || isSnipeTargetValid(previous_target); + + if (isSnipeTargetValid(previous_target)) + { + auto crumbs = navparser::NavEngine::getCrumbs(); + // We cannot just use the last crumb, as it is always nullptr + if (crumbs->size() > 1) + { + auto last_crumb = (*crumbs)[crumbs->size() - 2]; + // Area is still valid, stay on it + if (isAreaValidForSnipe(GetBuildingPosition(previous_target), last_crumb.navarea->m_center)) + return true; + } + if (tryToSnipe(previous_target)) + return true; + } + + // Make sure we don't try to do it on shortrange classes unless specified + if (!snipe_sentries_shortrange && (g_pLocalPlayer->clazz == tf_scout || g_pLocalPlayer->clazz == tf_pyro)) + return false; + + for (int i = g_IEngine->GetMaxClients() + 1; i < MAX_ENTITIES; i++) + { + CachedEntity *ent = ENTITY(i); + // Invalid sentry + if (!isSnipeTargetValid(ent)) + continue; + // Succeeded in trying to snipe it + if (tryToSnipe(ent)) + { + previous_target = ent; + return true; + } + } + return false; +} + +enum capture_type +{ + no_capture, + ctf, + payload, + controlpoints +}; + +static capture_type current_capturetype = no_capture; +// Overwrite to return true for payload carts as an example +static bool overwrite_capture = false; +// Doomsday is a ctf + payload map which breaks capturing... +static bool is_doomsday = false; + +std::optional getCtfGoal(int our_team, int enemy_team) +{ + // Get Flag related information + auto status = flagcontroller::getStatus(enemy_team); + auto position = flagcontroller::getPosition(enemy_team); + auto carrier = flagcontroller::getCarrier(enemy_team); + + // No flag :( + if (!position) + return std::nullopt; + + current_capturetype = ctf; + + // Flag is taken by us + if (status == TF_FLAGINFO_STOLEN) + { + // CTF is the current capture type. + if (carrier == LOCAL_E) + { + // Return our capture point location + auto team_flag = flagcontroller::getFlag(our_team); + return team_flag.spawn_pos; + } + } + // Get the flag if not taken by us already + else + { + return position; + } + return std::nullopt; +} + +std::optional getPayloadGoal(int our_team) +{ + auto position = plcontroller::getClosestPayload(g_pLocalPlayer->v_Origin, our_team); + // No payloads found :( + if (!position) + return std::nullopt; + current_capturetype = payload; + + // Adjust position so it's not floating high up, provided the local player is close. + if (LOCAL_E->m_vecOrigin().DistTo(*position) <= 150.0f) + (*position).z = LOCAL_E->m_vecOrigin().z; + // If close enough, don't move (mostly due to lifts) + if ((*position).DistTo(LOCAL_E->m_vecOrigin()) <= 50.0f) + { + overwrite_capture = true; + return std::nullopt; + } + else + return position; +} + +std::optional getControlPointGoal(int our_team) +{ + static Vector previous_position(0.0f); + static Vector randomized_position(0.0f); + + auto position = cpcontroller::getClosestControlPoint(g_pLocalPlayer->v_Origin, our_team); + // No points found :( + if (!position) + return std::nullopt; + + // Randomize where on the point we walk a bit so bots don't just stand there + if (previous_position != *position || !navparser::NavEngine::isPathing()) + { + previous_position = *position; + randomized_position = *position; + randomized_position.x += RandomFloat(0.0f, 100.0f); + randomized_position.y += RandomFloat(0.0f, 100.0f); + } + + current_capturetype = controlpoints; + // Try to navigate + return randomized_position; +} + +// Try to capture objectives +bool captureObjectives() +{ + static Timer capture_timer; + static Vector previous_target(0.0f); + // Not active or on a doomsday map + if (!capture_objectives || is_doomsday || !capture_timer.check(2000)) + return false; + + // Priority too high, don't try + if (navparser::NavEngine::current_priority > capture) + return false; + + // Where we want to go + std::optional target; + + int our_team = g_pLocalPlayer->team; + int enemy_team = our_team == TEAM_BLU ? TEAM_RED : TEAM_BLU; + + current_capturetype = no_capture; + overwrite_capture = false; + + // Run ctf logic + target = getCtfGoal(our_team, enemy_team); + // Not ctf, run payload + if (current_capturetype == no_capture) + { + target = getPayloadGoal(our_team); + // Not payload, run control points + if (current_capturetype == no_capture) + { + target = getControlPointGoal(our_team); + } + } + + // Overwritten, for example because we are currently on the payload, cancel any sort of pathing and return true + if (overwrite_capture) + { + navparser::NavEngine::cancelPath(); + return true; + } + // No target, bail and set on cooldown + else if (!target) + { + capture_timer.update(); + return false; + } + // If priority is not capturing or we have a new target, try to path there + else if (navparser::NavEngine::current_priority != capture || *target != previous_target) + { + if (navparser::NavEngine::navTo(*target, capture, true, !navparser::NavEngine::isPathing())) + { + previous_target = *target; + return true; + } + else + capture_timer.update(); + } + return false; +} + +// Run away from dangerous areas +bool escapeDanger() +{ + if (!escape_danger) + return false; + // Don't escape while we have the intel + if (!escape_danger_ctf_cap) + { + auto flag_carrier = flagcontroller::getCarrier(g_pLocalPlayer->team); + if (flag_carrier == LOCAL_E) + return false; + } + // Priority too high + if (navparser::NavEngine::current_priority > danger) + return false; + + auto *local_nav = navparser::NavEngine::findClosestNavSquare(g_pLocalPlayer->v_Origin); + auto blacklist = navparser::NavEngine::getFreeBlacklist(); + + // In danger, try to run + if (blacklist->find(local_nav) != blacklist->end()) + { + static CNavArea *target_area = nullptr; + // Already running and our target is still valid + if (navparser::NavEngine::current_priority == danger && blacklist->find(target_area) == blacklist->end()) + return true; + + std::vector nav_areas_ptr; + // Copy a ptr list (sadly cat_nav_init exists so this cannot be only done once) + for (auto &nav_area : navparser::NavEngine::getNavFile()->m_areas) + nav_areas_ptr.push_back(&nav_area); + + // Sort by distance + std::sort(nav_areas_ptr.begin(), nav_areas_ptr.end(), [](CNavArea *a, CNavArea *b) { return a->m_center.DistToSqr(g_pLocalPlayer->v_Origin) < b->m_center.DistToSqr(g_pLocalPlayer->v_Origin); }); + + int calls = 0; + // Try to path away + for (auto area : nav_areas_ptr) + { + if (blacklist->find(area) == blacklist->end()) + { + // only try the 5 closest valid areas though, something is wrong if this fails + calls++; + if (calls > 5) + break; + if (navparser::NavEngine::navTo(area->m_center, danger)) + { + target_area = area; + return true; + } + } + } + } + // No longer in danger + else if (navparser::NavEngine::current_priority == danger) + navparser::NavEngine::cancelPath(); + return false; +} + +static std::pair getNearestPlayerDistance() +{ + float distance = FLT_MAX; + CachedEntity *best_ent = nullptr; + for (int i = 1; i <= g_IEngine->GetMaxClients(); i++) + { + CachedEntity *ent = ENTITY(i); + if (CE_VALID(ent) && ent->m_vecDormantOrigin() && ent->m_bAlivePlayer() && ent->m_bEnemy() && g_pLocalPlayer->v_Origin.DistTo(ent->m_vecOrigin()) < distance && player_tools::shouldTarget(ent) && !IsPlayerInvisible(ent)) + { + distance = g_pLocalPlayer->v_Origin.DistTo(*ent->m_vecDormantOrigin()); + best_ent = ent; + } + } + return { best_ent, distance }; } enum slots @@ -1286,13 +922,28 @@ enum slots secondary = 2, melee = 3 }; + +static int slot = primary; +static std::pair nearest; + +static void autoJump() +{ + if (!autojump) + return; + static Timer last_jump{}; + if (!last_jump.test_and_set(200) || CE_BAD(nearest.first)) + return; + + if (nearest.second <= *jump_distance) + current_user_cmd->buttons |= IN_JUMP | IN_DUCK; +} + static slots getBestSlot(slots active_slot) { - auto nearest = getNearestPlayerDistance(false); + nearest = getNearestPlayerDistance(); switch (g_pLocalPlayer->clazz) { case tf_scout: - return primary; case tf_heavy: return primary; case tf_medic: @@ -1308,6 +959,10 @@ static slots getBestSlot(slots active_slot) } case tf_sniper: { + // Have a Huntsman, Always use primary + if (HasWeapon(LOCAL_E, 56) || HasWeapon(LOCAL_E, 1005) || HasWeapon(LOCAL_E, 1092)) + return primary; + if (nearest.second <= 300 && nearest.first->m_iHealth() < 75) return secondary; else if (nearest.second <= 400 && nearest.first->m_iHealth() < 75) @@ -1324,18 +979,14 @@ static slots getBestSlot(slots active_slot) else return secondary; } - case tf_engineer: + case tf_soldier: { - if (current_task == task::engineer) - { - // We cannot build the building if we keep switching away from the PDA - if (current_engineer_task == task::engineer_task::build_building) - return active_slot; - // Use wrench to repair/upgrade - if (current_engineer_task == task::engineer_task::upgradeorrepair_building) - return melee; - } - return primary; + if (nearest.second <= 200) + return secondary; + else if (nearest.second <= 300) + return active_slot; + else + return primary; } default: { @@ -1354,13 +1005,12 @@ static void updateSlot() static Timer slot_timer{}; if (!slot_timer.test_and_set(300)) return; - if (CE_GOOD(LOCAL_E) && CE_GOOD(LOCAL_W) && !g_pLocalPlayer->life_state) + if (CE_GOOD(LOCAL_E) && !HasCondition(LOCAL_E) && CE_GOOD(LOCAL_W) && LOCAL_E->m_bAlivePlayer()) { IClientEntity *weapon = RAW_ENT(LOCAL_W); - // IsBaseCombatWeapon() if (re::C_BaseCombatWeapon::IsBaseCombatWeapon(weapon)) { - int slot = re::C_BaseCombatWeapon::GetSlot(weapon) + 1; + slot = re::C_BaseCombatWeapon::GetSlot(weapon) + 1; int newslot = getBestSlot(static_cast(slot)); if (slot != newslot) g_IEngine->ClientCmd_Unrestricted(format("slot", newslot).c_str()); @@ -1368,41 +1018,115 @@ static void updateSlot() } } -class ObjectDestroyListener : public IGameEventListener2 +void CreateMove() { - virtual void FireGameEvent(IGameEvent *event) - { - if (!isHackActive() || !engineer_mode) - return; - // Get index of destroyed object - int index = event->GetInt("index"); - // Destroyed Entity - CachedEntity *ent = ENTITY(index); - // Get Entry in the vector - auto it = std::find(local_buildings.begin(), local_buildings.end(), ent); - // If found, erase - if (it != local_buildings.end()) - local_buildings.erase(it); - } -}; + if (!enabled || !navparser::NavEngine::isReady()) + return; + if (CE_BAD(LOCAL_E) || !LOCAL_E->m_bAlivePlayer() || HasCondition(LOCAL_E)) + return; -ObjectDestroyListener &listener() -{ - static ObjectDestroyListener object{}; - return object; + refreshSniperSpots(); + + if (danger_config_custom) + { + selected_config = { *danger_config_custom_min_full_danger, *danger_config_custom_min_slight_danger, *danger_config_custom_max_slight_danger, *danger_config_custom_prefer_far }; + } + else + { + // Update the distance config + switch (g_pLocalPlayer->clazz) + { + case tf_scout: + case tf_heavy: + selected_config = CONFIG_SHORT_RANGE; + break; + case tf_sniper: + selected_config = g_pLocalPlayer->weapon()->m_iClassID() == CL_CLASS(CTFCompoundBow) ? CONFIG_MID_RANGE : CONFIG_LONG_RANGE; + break; + default: + selected_config = CONFIG_MID_RANGE; + } + } + + updateSlot(); + autoJump(); + updateEnemyBlacklist(); + + // Try to escape danger first of all + if (escapeDanger()) + return; + // Second priority should be getting health + else if (getHealth()) + return; + // If we aren't getting health, get ammo + else if (getAmmo()) + return; + // Try to capture objectives + else if (captureObjectives()) + return; + // Try to snipe sentries + else if (snipeSentries()) + return; + // Try to stalk enemies + else if (stayNear()) + return; + // Try to get health with a lower prioritiy + else if (getHealth(true)) + return; + // We have nothing else to do, roam + else if (doRoam()) + return; } -static InitRoutine runinit([]() { - g_IEventManager2->AddListener(&listener(), "object_destroyed", false); - EC::Register(EC::CreateMove, CreateMove, "navbot", EC::early); - EC::Register( - EC::Shutdown, []() { g_IEventManager2->RemoveListener(&listener()); }, "navbot_shutdown"); +void LevelInit() +{ + // Make it run asap + refresh_sniperspots_timer.last -= std::chrono::seconds(60); + sniper_spots.clear(); + is_doomsday = false; + + // Doomsday sucks + // TODO: add proper doomsday implementation + auto map_name = std::string(g_IEngine->GetLevelName()); + if (g_IEngine->GetLevelName() && map_name.find("sd_doomsday") != map_name.npos) + is_doomsday = true; +} +#if ENABLE_VISUALS +void Draw() +{ + if (!draw_danger || !navparser::NavEngine::isReady()) + return; + for (auto &area : slight_danger_drawlist_normal) + { + Vector out; + + if (draw::WorldToScreen(area, out)) + draw::Rectangle(out.x - 2.0f, out.y - 2.0f, 4.0f, 4.0f, colors::orange); + } + for (auto &area : slight_danger_drawlist_dormant) + { + Vector out; + + if (draw::WorldToScreen(area, out)) + draw::Rectangle(out.x - 2.0f, out.y - 2.0f, 4.0f, 4.0f, colors::orange); + } + for (auto &area : *navparser::NavEngine::getFreeBlacklist()) + { + Vector out; + + if (draw::WorldToScreen(area.first->m_center, out)) + draw::Rectangle(out.x - 2.0f, out.y - 2.0f, 4.0f, 4.0f, colors::red); + } +} +#endif + +static InitRoutine init([]() { + EC::Register(EC::CreateMove, CreateMove, "navbot_cm"); + EC::Register(EC::LevelInit, LevelInit, "navbot_levelinit"); +#if ENABLE_VISUALS + EC::Register(EC::Draw, Draw, "navbot_draw"); +#endif + LevelInit(); }); -void change(settings::VariableBase &, bool) -{ - nav::clearInstructions(); -} - -static InitRoutine routine([]() { enabled.installChangeCallback(change); }); } // namespace hacks::tf2::NavBot diff --git a/src/helpers.cpp b/src/helpers.cpp index 14c250df..68ae8c7d 100644 --- a/src/helpers.cpp +++ b/src/helpers.cpp @@ -120,8 +120,9 @@ void WalkTo(const Vector &vector) // Calculate how to get to a vector auto result = ComputeMove(LOCAL_E->m_vecOrigin(), vector); // Push our move to usercmd - current_user_cmd->forwardmove = result.first; - current_user_cmd->sidemove = result.second; + current_user_cmd->forwardmove = result.x; + current_user_cmd->sidemove = result.y; + current_user_cmd->upmove = result.z; } // Function to get the corner location that a vischeck to an entity is possible @@ -436,7 +437,6 @@ bool canReachVector(Vector loc, Vector dest) std::string GetLevelName() { - std::string name(g_IEngine->GetLevelName()); size_t slash = name.find('/'); if (slash == std::string::npos) @@ -465,21 +465,30 @@ std::pair ComputeMovePrecise(const Vector &a, const Vector &b) return { cos(yaw) * speed, -sin(yaw) * speed }; } -std::pair ComputeMove(const Vector &a, const Vector &b) +Vector ComputeMove(const Vector &a, const Vector &b) { Vector diff = (b - a); if (diff.Length() == 0.0f) - return { 0, 0 }; + return Vector(0.0f); const float x = diff.x; const float y = diff.y; Vector vsilent(x, y, 0); float speed = sqrt(vsilent.x * vsilent.x + vsilent.y * vsilent.y); Vector ang; VectorAngles(vsilent, ang); - float yaw = DEG2RAD(ang.y - current_user_cmd->viewangles.y); + float yaw = DEG2RAD(ang.y - current_user_cmd->viewangles.y); + float pitch = DEG2RAD(ang.x - current_user_cmd->viewangles.x); if (g_pLocalPlayer->bUseSilentAngles) - yaw = DEG2RAD(ang.y - g_pLocalPlayer->v_OrigViewangles.y); - return { cos(yaw) * 450.0f, -sin(yaw) * 450.0f }; + { + yaw = DEG2RAD(ang.y - g_pLocalPlayer->v_OrigViewangles.y); + pitch = DEG2RAD(ang.x - g_pLocalPlayer->v_OrigViewangles.x); + } + Vector move = { cos(yaw) * 450.0f, -sin(yaw) * 450.0f, -cos(pitch) * 450.0f }; + + // Only apply upmove in water + if (!(g_ITrace->GetPointContents(g_pLocalPlayer->v_Eye) & CONTENTS_WATER)) + move.z = current_user_cmd->upmove; + return move; } ConCommand *CreateConCommand(const char *name, FnCommandCallback_t callback, const char *help) @@ -1446,7 +1455,6 @@ Vector GetForwardVector(Vector origin, Vector viewangles, float distance, Cached // Compensate for punch angle if (punch_entity && should_correct_punch) angle += VectorToQAngle(CE_VECTOR(punch_entity, netvar.vecPunchAngle)); - trace_t trace; sy = sinf(DEG2RAD(angle[1])); cy = cosf(DEG2RAD(angle[1])); diff --git a/src/hooks/CreateMove.cpp b/src/hooks/CreateMove.cpp index ff754c4c..c9029175 100644 --- a/src/hooks/CreateMove.cpp +++ b/src/hooks/CreateMove.cpp @@ -239,10 +239,6 @@ DEFINE_HOOKED_METHOD(CreateMove, bool, void *this_, float input_sample_time, CUs if (firstcm) { DelayTimer.update(); - // hacks::tf2::NavBot::Init(); - // hacks::tf2::NavBot::initonce(); - nav::status = nav::off; - hacks::tf2::NavBot::init(true); if (identify) { sendIdentifyMessage(false); diff --git a/src/localplayer.cpp b/src/localplayer.cpp index 6c4bd9d5..cf8406aa 100644 --- a/src/localplayer.cpp +++ b/src/localplayer.cpp @@ -88,14 +88,23 @@ void LocalPlayer::Update() holding_sniper_rifle = false; holding_sapper = false; weapon_melee_damage_tick = false; + bRevving = false; + bRevved = false; wep = weapon(); if (CE_GOOD(wep)) { weapon_mode = GetWeaponModeloc(); if (wep->m_iClassID() == CL_CLASS(CTFSniperRifle) || wep->m_iClassID() == CL_CLASS(CTFSniperRifleDecap)) holding_sniper_rifle = true; - if (wep->m_iClassID() == CL_CLASS(CTFWeaponBuilder) || wep->m_iClassID() == CL_CLASS(CTFWeaponSapper)) + else if (wep->m_iClassID() == CL_CLASS(CTFWeaponBuilder) || wep->m_iClassID() == CL_CLASS(CTFWeaponSapper)) holding_sapper = true; + else if (wep->m_iClassID() == CL_CLASS(CTFMinigun)) + { + if (CE_INT(LOCAL_W, netvar.iWeaponState) == 2 || CE_INT(LOCAL_W, netvar.iWeaponState) == 1) + bRevving = true; + else if (CE_INT(LOCAL_W, netvar.iWeaponState) == 3) + bRevved = true; + } // Detect when a melee hit will result in damage, useful for aimbot and antiaim if (CE_FLOAT(wep, netvar.flNextPrimaryAttack) > g_GlobalVars->curtime && weapon_mode == weapon_melee) { diff --git a/src/navparser.cpp b/src/navparser.cpp index 4d467915..126898ae 100644 --- a/src/navparser.cpp +++ b/src/navparser.cpp @@ -1,861 +1,1048 @@ +/* + This file is part of Cathook. + + Cathook is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Cathook is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Cathook. If not, see . +*/ + +// Codeowners: TotallyNotElite + #include "common.hpp" -#include "navparser.hpp" -#include #include "micropather.h" -#include -#include -#include -#include -#include -#include "MiscAimbot.hpp" +#include "CNavFile.h" +#include "teamroundtimer.hpp" #include "Aimbot.hpp" +#include "MiscAimbot.hpp" +#include "navparser.hpp" +#if ENABLE_VISUALS +#include "drawing.hpp" +#endif -namespace nav +#include +#include + +namespace navparser { +static settings::Boolean enabled("nav.enabled", "false"); +static settings::Boolean draw("nav.draw", "false"); +static settings::Boolean look{ "nav.look-at-path", "false" }; +static settings::Boolean draw_debug_areas("nav.draw.debug-areas", "false"); +static settings::Boolean log_pathing{ "nav.log", "false" }; +static settings::Int stuck_time{ "nav.stuck-time", "1000" }; +static settings::Int vischeck_cache_time{ "nav.vischeck-cache.time", "240" }; +static settings::Boolean vischeck_runtime{ "nav.vischeck-runtime.enabled", "true" }; +static settings::Int vischeck_time{ "nav.vischeck-runtime.delay", "2000" }; +static settings::Int stuck_detect_time{ "nav.anti-stuck.detection-time", "5" }; +// How long until accumulated "Stuck time" expires +static settings::Int stuck_expire_time{ "nav.anti-stuck.expire-time", "10" }; +// How long we should blacklist the node after being stuck for too long? +static settings::Int stuck_blacklist_time{ "nav.anti-stuck.blacklist-time", "120" }; +static settings::Int sticky_ignore_time{ "nav.ignore.sticky-time", "15" }; -static settings::Boolean enabled{ "misc.pathing", "true" }; -// Whether or not to run vischecks at pathtime -static settings::Boolean vischecks{ "misc.pathing.pathtime-vischecks", "true" }; -static settings::Boolean vischeckBlock{ "misc.pathing.pathtime-vischeck-block", "false" }; -static settings::Boolean draw{ "misc.pathing.draw", "false" }; -static settings::Boolean look{ "misc.pathing.look-at-path", "false" }; -static settings::Int stuck_time{ "misc.pathing.stuck-time", "4000" }; -static settings::Int unreachable_time{ "misc.pathing.unreachable-time", "1000" }; -static settings::Boolean log_pathing{ "misc.pathing.log", "false" }; - -// Score based on how much the area was used by other players, in seconds -static std::unordered_map area_score; -static std::vector crumbs; -static Vector startPoint, endPoint; - -enum ignore_status : uint8_t +// Cast a Ray and return if it hit +static bool CastRay(Vector origin, Vector endpos, unsigned mask, ITraceFilter *filter) { - // Status is unknown - unknown = 0, - // Something like Z check failed, these are unchanging - const_ignored, - // LOS between areas is given - vischeck_success, - // LOS if we ignore entities - vischeck_blockedentity, - // No LOS between areas - vischeck_failed, - // Failed to actually walk thru connection - explicit_ignored, - // Danger like sentry gun or sticky - danger_found + trace_t trace; + Ray_t ray; + + ray.Init(origin, endpos); + + // This was found to be So inefficient that it is literally unusable for our purposes. it is almost 1000x slower than the above. + // ray.Init(origin, target, -right * HALF_PLAYER_WIDTH, right * HALF_PLAYER_WIDTH); + + PROF_SECTION(IEVV_TraceRay); + g_ITrace->TraceRay(ray, mask, filter, &trace); + + return trace.DidHit(); +} + +// Vischeck that considers player width +static bool IsPlayerPassableNavigation(Vector origin, Vector target, unsigned int mask = MASK_PLAYERSOLID) +{ + Vector tr = target - origin; + Vector angles; + VectorAngles(tr, angles); + + Vector forward, right, up; + AngleVectors3(VectorToQAngle(angles), &forward, &right, &up); + right.z = 0; + + // We want to keep the same angle for these two bounding box traces + Vector relative_endpos = forward * tr.Length(); + + Vector left_ray_origin = origin - right * HALF_PLAYER_WIDTH; + Vector left_ray_endpos = left_ray_origin + relative_endpos; + + // Left ray hit something + if (CastRay(left_ray_origin, left_ray_endpos, mask, &trace::filter_navigation)) + return false; + + Vector right_ray_origin = origin + right * HALF_PLAYER_WIDTH; + Vector right_ray_endpos = right_ray_origin + relative_endpos; + + // Return if the right ray hit something + return !CastRay(right_ray_origin, right_ray_endpos, mask, &trace::filter_navigation); +} + +enum class NavState +{ + Unavailable = 0, + Active }; -void ResetPather(); -void repath(); - -struct ignoredata +struct CachedConnection { - ignore_status status{ unknown }; - float stucktime{ 0.0f }; - Timer ignoreTimeout{}; + int expire_tick; + bool vischeck_state; }; -Vector GetClosestCornerToArea(CNavArea *CornerOf, const Vector &target) +struct CachedStucktime { - std::array corners{ - CornerOf->m_nwCorner, // NW - CornerOf->m_seCorner, // SE - { CornerOf->m_seCorner.x, CornerOf->m_nwCorner.y, CornerOf->m_nwCorner.z }, // NE - { CornerOf->m_nwCorner.x, CornerOf->m_seCorner.y, CornerOf->m_seCorner.z } // SW + int expire_tick; + int time_stuck; +}; + +struct ConnectionInfo +{ + enum State + { + // Tried using this connection, failed for some reason + STUCK, }; + int expire_tick; + State state; +}; - Vector *bestVec = &corners[0], *bestVec2 = bestVec; - float bestDist = corners[0].DistTo(target), bestDist2 = bestDist; - - for (size_t i = 1; i < corners.size(); i++) +// Returns corrected "current_pos" +Vector handleDropdown(Vector current_pos, Vector next_pos) +{ + Vector to_target = (next_pos - current_pos); + // Only do it if we'd fall quite a bit + if (-to_target.z > PLAYER_JUMP_HEIGHT) { - float dist = corners[i].DistTo(target); - if (dist < bestDist) - { - bestVec = &corners[i]; - bestDist = dist; - } - if (corners[i] == *bestVec2) - continue; - - if (dist < bestDist2) - { - bestVec2 = &corners[i]; - bestDist2 = dist; - } + to_target.z = 0; + to_target.NormalizeInPlace(); + Vector angles; + VectorAngles(to_target, angles); + // We need to really make sure we fall, so we go two times as far out as we should have to + current_pos = GetForwardVector(current_pos, angles, PLAYER_WIDTH * 2.0f); } - return (*bestVec + *bestVec2) / 2; + return current_pos; } -float getZBetweenAreas(CNavArea *start, CNavArea *end) +class navPoints { - float z1 = GetClosestCornerToArea(start, end->m_center).z; - float z2 = GetClosestCornerToArea(end, start->m_center).z; +public: + Vector current; + Vector center; + // The above but on the "next" vector, used for height checks. + Vector center_next; + Vector next; + navPoints(Vector A, Vector B, Vector C, Vector D) : current(A), center(B), center_next(C), next(D){}; +}; - return z2 - z1; -} +// This function ensures that vischeck and pathing use the same logic. +navPoints determinePoints(CNavArea *current, CNavArea *next) +{ + auto area_center = current->m_center; + auto next_center = next->m_center; + // Gets a vector on the edge of the current area that is as close as possible to the center of the next area + auto area_closest = current->getNearestPoint(next_center.AsVector2D()); + // Do the same for the other area + auto next_closest = next->getNearestPoint(area_center.AsVector2D()); -static std::unordered_map, ignoredata, boost::hash>> ignores; -namespace ignoremanager -{ -static ignore_status vischeck(CNavArea *begin, CNavArea *end) -{ - Vector first = begin->m_center; - Vector second = end->m_center; - first.z += 70; - second.z += 70; - // Is world blocking it? - if (IsVectorVisibleNavigation(first, second, MASK_PLAYERSOLID)) + // Use one of them as a center point, the one that is either x or y alligned with a center + // Of the areas. + // This will avoid walking into walls. + auto center_point = area_closest; + + // Determine if alligned, if not, use the other one as the center point + if (center_point.x != area_center.x && center_point.y != area_center.y && center_point.x != next_center.x && center_point.y != next_center.y) { - // Is something else blocking it? - if (!IsVectorVisible(first, second, true, LOCAL_E, MASK_PLAYERSOLID)) - return vischeck_blockedentity; + center_point = next_closest; + // Use the point closest to next_closest on the "original" mesh for z + center_point.z = current->getNearestPoint(next_closest.AsVector2D()).z; + } + + // Nearest point to center on "next"m used for height checks + auto center_next = next->getNearestPoint(center_point.AsVector2D()); + + return navPoints(area_center, center_point, center_next, next_center); +}; + +class Map : public micropather::Graph +{ +public: + CNavFile navfile; + NavState state; + micropather::MicroPather pather{ this, 3000, 6, true }; + std::string mapname; + std::unordered_map, CachedConnection, boost::hash>> vischeck_cache; + std::unordered_map, CachedStucktime, boost::hash>> connection_stuck_time; + // This is a pure blacklist that does not get cleared and is for free usage internally and externally, e.g. blacklisting where enemies are standing + // This blacklist only gets cleared on map change, and can be used time independantly. + // the enum is the Blacklist reason, so you can easily edit it + std::unordered_map free_blacklist; + // When the local player stands on one of the nav squares the free blacklist should NOT run + bool free_blacklist_blocked = false; + + Map(const char *mapname) : navfile(mapname), mapname(mapname) + { + if (!navfile.m_isOK) + state = NavState::Unavailable; else - return vischeck_success; + state = NavState::Active; } - return vischeck_failed; -} -static ignore_status runIgnoreChecks(CNavArea *begin, CNavArea *end) -{ - // No z check Should be done for stairs as they can go very far up - if (getZBetweenAreas(begin, end) > 70) - return const_ignored; - if (!vischecks) - return vischeck_success; - return vischeck(begin, end); -} -static void updateDanger() -{ - for (size_t i = 0; i <= HIGHEST_ENTITY; i++) + float LeastCostEstimate(void *start, void *end) override { - CachedEntity *ent = ENTITY(i); - if (CE_INVALID(ent)) - continue; - if (ent->m_iClassID() == CL_CLASS(CObjectSentrygun)) + return reinterpret_cast(start)->m_center.DistTo(reinterpret_cast(end)->m_center); + } + void AdjacentCost(void *main, std::vector *adjacent) override + { + CNavArea &area = *reinterpret_cast(main); + for (NavConnect &connection : area.m_connections) { - if (!ent->m_bEnemy()) - continue; - if (HasCondition(LOCAL_E)) - continue; - Vector loc = GetBuildingPosition(ent); - if (RAW_ENT(ent)->IsDormant()) - { - auto vec = ent->m_vecDormantOrigin(); - if (vec) - { - loc -= RAW_ENT(ent)->GetCollideable()->GetCollisionOrigin(); - loc += *vec; - } - else + // An area being entered twice means it is blacklisted from entry entirely + auto connection_key = std::pair(connection.area, connection.area); + auto cached_connection = vischeck_cache.find(connection_key); + + // Entered and marked bad? + if (cached_connection != vischeck_cache.end()) + if (!cached_connection->second.vischeck_state) continue; - } - // It's still building, ignore - else if (CE_BYTE(ent, netvar.m_bBuilding) || CE_BYTE(ent, netvar.m_bPlacing)) - continue; - // Keep track of good spots - std::vector spot_list{}; - - // Don't blacklist if local player is standing in it - bool local_player_in_range = false; - - // Local player's nav area - auto local_area = findClosestNavSquare(LOCAL_E->m_vecOrigin()); - - // Actual building check - for (auto &i : navfile->m_areas) - { - Vector area = i.m_center; - area.z += 41.5f; - if (loc.DistTo(area) > 1100) - continue; - // Check if sentry can see us - if (!IsVectorVisible(loc, area, true)) - continue; - // local player's nav area? - if (local_area == &i) + // If the extern blacklist is running, ensure we don't try to use a bad area + bool is_blacklisted = false; + if (!free_blacklist_blocked) + for (auto &entry : free_blacklist) { - local_player_in_range = true; - break; + if (entry.first == connection.area) + { + is_blacklisted = true; + break; + } } - spot_list.push_back(&i); - } - - // Local player is in the sentry range, let him nav - if (local_player_in_range) + if (is_blacklisted) continue; - // Ignore these - for (auto &i : spot_list) + auto points = determinePoints(&area, connection.area); + + // Apply dropdown + points.center = handleDropdown(points.center, points.next); + + float height_diff = points.center_next.z - points.center.z; + + // Too high for us to jump! + if (height_diff > PLAYER_JUMP_HEIGHT) + continue; + + points.current.z += PLAYER_JUMP_HEIGHT; + points.center.z += PLAYER_JUMP_HEIGHT; + points.next.z += PLAYER_JUMP_HEIGHT; + + auto key = std::pair(&area, connection.area); + auto cached = vischeck_cache.find(key); + if (cached != vischeck_cache.end()) { - ignoredata &data = ignores[{ i, nullptr }]; - data.status = danger_found; - data.ignoreTimeout.update(); - data.ignoreTimeout.last -= std::chrono::seconds(17); - } - } - else if (ent->m_iClassID() == CL_CLASS(CTFGrenadePipebombProjectile)) - { - if (!ent->m_bEnemy()) - continue; - if (CE_INT(ent, netvar.iPipeType) == 1) - continue; - Vector loc = ent->m_vecOrigin(); - - // Keep track of good spots - std::vector spot_list{}; - - // Don't blacklist if local player is standing in it - bool local_player_in_range = false; - - // Local player's nav area - auto local_area = findClosestNavSquare(LOCAL_E->m_vecOrigin()); - - // Actual Sticky check - for (auto &i : navfile->m_areas) - { - Vector area = i.m_center; - area.z += 41.5f; - if (loc.DistTo(area) > 130) - continue; - // Check if in Sticky vis range - if (!IsVectorVisible(loc, area, true)) - continue; - // local player's nav area? - if (local_area == &i) + if (cached->second.vischeck_state) { - local_player_in_range = true; - break; + float cost = connection.area->m_center.DistTo(area.m_center); + adjacent->push_back(micropather::StateCost{ reinterpret_cast(connection.area), cost }); } - spot_list.push_back(&i); } - - // Local player is in the sentry range, let him nav - if (local_player_in_range) - continue; - - // Ignore these - for (auto &i : spot_list) - { - ignoredata &data = ignores[{ i, nullptr }]; - data.status = danger_found; - data.ignoreTimeout.update(); - data.ignoreTimeout.last -= std::chrono::seconds(17); - } - } - } -} - -static void checkPath() -{ - bool perform_repath = false; - // Vischecks - for (size_t i = 0; i < crumbs.size() - 1; i++) - { - CNavArea *begin = crumbs[i]; - CNavArea *end = crumbs[i + 1]; - if (!begin || !end) - continue; - ignoredata &data = ignores[{ begin, end }]; - if (data.status == vischeck_failed) - return; - if (data.status == vischeck_blockedentity && vischeckBlock) - return; - auto vis_status = vischeck(begin, end); - if (vis_status == vischeck_failed) - { - data.status = vischeck_failed; - data.ignoreTimeout.update(); - perform_repath = true; - } - else if (vis_status == vischeck_blockedentity && vischeckBlock) - { - data.status = vischeck_blockedentity; - data.ignoreTimeout.update(); - perform_repath = true; - } - else if (ignores[{ end, nullptr }].status == danger_found) - { - perform_repath = true; - } - } - if (perform_repath) - repath(); -} -// 0 = Not ignored, 1 = low priority, 2 = ignored -static int isIgnored(CNavArea *begin, CNavArea *end) -{ - if (ignores[{ end, nullptr }].status == danger_found) - return 2; - ignore_status status = ignores[{ begin, end }].status; - if (status == unknown) - status = runIgnoreChecks(begin, end); - if (status == vischeck_success) - return 0; - else if (status == vischeck_blockedentity && !vischeckBlock) - return 1; - else - return 2; -} -static bool addTime(ignoredata &connection, ignore_status status) -{ - connection.status = status; - connection.ignoreTimeout.update(); - - return true; -} -static bool addTime(CNavArea *begin, CNavArea *end, ignore_status status) -{ - logging::Info("Ignored Connection %i-%i", begin->m_id, end->m_id); - return addTime(ignores[{ begin, end }], status); -} -static bool addTime(CNavArea *begin, CNavArea *end, Timer &time) -{ - if (!begin || !end) - { - // We can't reach the destination vector. Destination vector might - // be out of bounds/reach. - clearInstructions(); - return true; - } - using namespace std::chrono; - // Check if connection is already known - if (ignores.find({ begin, end }) == ignores.end()) - { - ignores[{ begin, end }] = {}; - } - ignoredata &connection = ignores[{ begin, end }]; - connection.stucktime += duration_cast(system_clock::now() - time.last).count(); - if (connection.stucktime >= *stuck_time) - { - logging::Info("Ignored Connection %i-%i", begin->m_id, end->m_id); - return addTime(connection, explicit_ignored); - } - return false; -} -static void reset() -{ - ignores.clear(); - ResetPather(); -} -static void updateIgnores() -{ - static Timer update{}; - static Timer last_pather_reset{}; - static bool reset_pather = false; - if (!update.test_and_set(500)) - return; - updateDanger(); - if (crumbs.empty()) - { - for (auto &i : ignores) - { - switch (i.second.status) - { - case explicit_ignored: - if (i.second.ignoreTimeout.check(60000)) - { - i.second.status = unknown; - i.second.stucktime = 0; - reset_pather = true; - } - break; - case unknown: - break; - case danger_found: - if (i.second.ignoreTimeout.check(20000)) - { - i.second.status = unknown; - reset_pather = true; - } - break; - case vischeck_failed: - case vischeck_blockedentity: - case vischeck_success: - default: - if (i.second.ignoreTimeout.check(30000)) - { - i.second.status = unknown; - i.second.stucktime = 0; - reset_pather = true; - } - break; - } - } - } - else - checkPath(); - if (reset_pather && last_pather_reset.test_and_set(10000)) - { - reset_pather = false; - ResetPather(); - } -} -static bool isSafe(CNavArea *area) -{ - return !(ignores[{ area, nullptr }].status == danger_found); -} -}; // namespace ignoremanager - -struct Graph : public micropather::Graph -{ - std::unique_ptr pather; - - Graph() - { - pather = std::make_unique(this, 3000, 6, true); - } - ~Graph() override - { - } - void AdjacentCost(void *state, MP_VECTOR *adjacent) override - { - CNavArea *center = static_cast(state); - for (auto &i : center->m_connections) - { - CNavArea *neighbour = i.area; - int isIgnored = ignoremanager::isIgnored(center, neighbour); - if (isIgnored == 2) - continue; - float distance = center->m_center.DistTo(i.area->m_center); - if (isIgnored == 1) - distance += 2000; - // Check priority based on usage else { - float score = area_score[neighbour->m_id]; - // Formula to calculate by how much % to reduce the distance by (https://xaktly.com/LogisticFunctions.html) - float multiplier = 2.0f * ((0.9f) / (1.0f + exp(-0.8f * score)) - 0.45f); - distance *= 1.0f - multiplier; - } + // Check if there is direct line of sight + if (IsPlayerPassableNavigation(points.current, points.center) && IsPlayerPassableNavigation(points.center, points.next)) + { + vischeck_cache[key] = { TICKCOUNT_TIMESTAMP(60), true }; - adjacent->emplace_back(micropather::StateCost{ reinterpret_cast(neighbour), distance }); + float cost = points.next.DistTo(points.current); + adjacent->push_back(micropather::StateCost{ reinterpret_cast(connection.area), cost }); + } + else + { + vischeck_cache[key] = { TICKCOUNT_TIMESTAMP(60), false }; + } + } } } - float LeastCostEstimate(void *stateStart, void *stateEnd) override + + // Function for getting closest Area to player, aka "LocalNav" + CNavArea *findClosestNavSquare(const Vector &vec) { - CNavArea *start = reinterpret_cast(stateStart); - CNavArea *end = reinterpret_cast(stateEnd); - return start->m_center.DistTo(end->m_center); + auto vec_corrected = vec; + vec_corrected.z += PLAYER_JUMP_HEIGHT; + float ovBestDist = FLT_MAX, bestDist = FLT_MAX; + // If multiple candidates for LocalNav have been found, pick the closest + CNavArea *ovBestSquare = nullptr, *bestSquare = nullptr; + for (auto &i : navfile.m_areas) + { + // Marked bad, do not use if local origin + if (g_pLocalPlayer->v_Origin == vec) + { + auto key = std::pair(&i, &i); + if (vischeck_cache.find(key) != vischeck_cache.end()) + if (!vischeck_cache[key].vischeck_state) + continue; + } + + float dist = i.m_center.DistTo(vec); + if (dist < bestDist) + { + bestDist = dist; + bestSquare = &i; + } + auto center_corrected = i.m_center; + center_corrected.z += PLAYER_JUMP_HEIGHT; + // Check if we are within x and y bounds of an area + if (ovBestDist < dist || !i.IsOverlapping(vec) || !IsVectorVisibleNavigation(vec_corrected, center_corrected)) + { + continue; + } + ovBestDist = dist; + ovBestSquare = &i; + } + if (!ovBestSquare) + ovBestSquare = bestSquare; + + return ovBestSquare; } + std::vector findPath(CNavArea *local, CNavArea *dest) + { + using namespace std::chrono; + + if (state != NavState::Active) + return {}; + + if (log_pathing) + { + logging::Info("Start: (%f,%f,%f)", local->m_center.x, local->m_center.y, local->m_center.z); + logging::Info("End: (%f,%f,%f)", dest->m_center.x, dest->m_center.y, dest->m_center.z); + } + + std::vector pathNodes; + float cost; + + time_point begin_pathing = high_resolution_clock::now(); + int result = pather.Solve(reinterpret_cast(local), reinterpret_cast(dest), &pathNodes, &cost); + long long timetaken = duration_cast(high_resolution_clock::now() - begin_pathing).count(); + if (log_pathing) + logging::Info("Pathing: Pather result: %i. Time taken (NS): %lld", result, timetaken); + // Start and end are the same, return start node + if (result == micropather::MicroPather::START_END_SAME) + return { reinterpret_cast(local) }; + + return pathNodes; + } + + void updateIgnores() + { + static Timer update_time; + if (!update_time.test_and_set(1000)) + return; + + // Sentries make sounds, so we can just rely on soundcache here and always clear sentries + NavEngine::clearFreeBlacklist(SENTRY); + // Find sentries and stickies + for (int i = g_IEngine->GetMaxClients() + 1; i < MAX_ENTITIES; i++) + { + CachedEntity *ent = ENTITY(i); + if (CE_INVALID(ent) || !ent->m_bAlivePlayer() || ent->m_iTeam() == g_pLocalPlayer->team) + continue; + bool is_sentry = ent->m_iClassID() == CL_CLASS(CObjectSentrygun); + bool is_sticky = ent->m_iClassID() == CL_CLASS(CTFGrenadePipebombProjectile) && CE_INT(ent, netvar.iPipeType) == 1 && CE_VECTOR(ent, netvar.vVelocity).IsZero(1.0f); + // Not sticky/sentry, ignore. + // (Or dormant sticky) + if (!is_sentry && (!is_sticky || CE_BAD(ent))) + continue; + if (is_sentry) + { + // Should we even ignore the sentry? + // Soldier/Heavy do not care about Level 1 or mini sentries + bool is_strong_class = g_pLocalPlayer->clazz == tf_soldier || g_pLocalPlayer->clazz == tf_heavy; + if (is_strong_class && (CE_BYTE(ent, netvar.m_bMiniBuilding) || CE_INT(ent, netvar.iUpgradeLevel) == 1)) + continue; + + // It's still building/being sapped, ignore. + // Unless it just was deployed from a carry, then it's dangerous + if ((!CE_BYTE(ent, netvar.m_bCarryDeploy) && CE_BYTE(ent, netvar.m_bBuilding)) || CE_BYTE(ent, netvar.m_bPlacing) || CE_BYTE(ent, netvar.m_bHasSapper)) + continue; + + // Get origin of the sentry + auto building_origin = GetBuildingPosition(ent); + // For dormant sentries we need to add the jump height to the z + if (CE_BAD(ent)) + building_origin.z += PLAYER_JUMP_HEIGHT; + // Actual building check + for (auto &i : navfile.m_areas) + { + Vector area = i.m_center; + area.z += PLAYER_JUMP_HEIGHT; + // Out of range + if (building_origin.DistToSqr(area) > (1100 + HALF_PLAYER_WIDTH) * (1100 + HALF_PLAYER_WIDTH)) + continue; + // Check if sentry can see us + if (!IsVectorVisibleNavigation(building_origin, area)) + continue; + // Blacklist because it's in view range of the sentry + free_blacklist[&i] = SENTRY; + } + } + else + { + auto sticky_origin = ent->m_vecOrigin(); + // Make sure the sticky doesn't vischeck from inside the floor + sticky_origin.z += PLAYER_JUMP_HEIGHT / 2.0f; + for (auto &i : navfile.m_areas) + { + Vector area = i.m_center; + area.z += PLAYER_JUMP_HEIGHT; + // Out of range + if (sticky_origin.DistToSqr(area) > (130 + HALF_PLAYER_WIDTH) * (130 + HALF_PLAYER_WIDTH)) + continue; + // Check if Sticky can see the reason + if (!IsVectorVisibleNavigation(sticky_origin, area)) + continue; + // Blacklist because it's in range of the sticky, but stickies make no noise, so blacklist it for a specific timeframe + free_blacklist[&i] = { STICKY, TICKCOUNT_TIMESTAMP(*sticky_ignore_time) }; + } + } + } + + static int previous_blacklist_size = 0; + + bool erased = false; + if (previous_blacklist_size != free_blacklist.size()) + erased = true; + previous_blacklist_size = free_blacklist.size(); + // When we switch to c++20, we can use std::erase_if + for (auto it = begin(free_blacklist); it != end(free_blacklist);) + { + // Clear entries from the free blacklist when expired and if it has a set time + if (it->second.time && it->second.time < g_GlobalVars->tickcount) + { + it = free_blacklist.erase(it); // previously this was something like m_map.erase(it++); + erased = true; + } + else + ++it; + } + + for (auto it = begin(vischeck_cache); it != end(vischeck_cache);) + { + if (it->second.expire_tick < g_GlobalVars->tickcount) + { + it = vischeck_cache.erase(it); // previously this was something like m_map.erase(it++); + erased = true; + } + else + ++it; + } + for (auto it = begin(connection_stuck_time); it != end(connection_stuck_time);) + { + if (it->second.expire_tick < g_GlobalVars->tickcount) + { + it = connection_stuck_time.erase(it); // previously this was something like m_map.erase(it++); + erased = true; + } + else + ++it; + } + if (erased) + pather.Reset(); + } + + void Reset() + { + vischeck_cache.clear(); + connection_stuck_time.clear(); + free_blacklist.clear(); + pather.Reset(); + } + + // Uncesseray thing that is sadly necessary void PrintStateInfo(void *) override { } }; -// Navfile containing areas -std::unique_ptr navfile; -// Status -std::atomic status; - -// See "Graph", does pathing and stuff I guess -static Graph Map; - -void initThread() +namespace NavEngine { - char *p, cwd[PATH_MAX + 1], nav_path[PATH_MAX + 1], lvl_name[256]; +std::unique_ptr map; +Crumb last_crumb; +std::vector crumbs; - std::strncpy(lvl_name, g_IEngine->GetLevelName(), 255); - lvl_name[255] = 0; - p = std::strrchr(lvl_name, '.'); - if (!p) - { - logging::Info("Failed to find dot in level name"); - return; - } - *p = 0; - p = getcwd(cwd, sizeof(cwd)); - if (!p) - { - logging::Info("Failed to get current working directory: %s", strerror(errno)); - return; - } - std::snprintf(nav_path, sizeof(nav_path), "%s/tf/%s.nav", cwd, lvl_name); - logging::Info("Pathing: Nav File location: %s", nav_path); - navfile = std::make_unique(nav_path); - if (!navfile->m_isOK) - { - navfile.reset(); - status = unavailable; - return; - } - logging::Info("Pather: Initing with %i Areas", navfile->m_areas.size()); - status = on; +int current_priority = 0; +bool current_navtolocal = false; +bool repath_on_fail = false; +Vector last_destination; + +bool isReady() +{ + return enabled && map && map->state == NavState::Active && (g_pTeamRoundTimer->GetRoundState() != RT_STATE_SETUP || g_pLocalPlayer->team != TEAM_BLU); } -void init() +bool isPathing() { - area_score.clear(); - endPoint.Invalidate(); - ignoremanager::reset(); - status = initing; - std::thread thread; - thread = std::thread(initThread); - thread.detach(); + return !crumbs.empty(); } -bool prepare() +CNavFile *getNavFile() { - if (!enabled) - return false; - init_status fast_status = status; - if (fast_status == on) - return true; - if (fast_status == off) - { - init(); - } - return false; + return &map->navfile; } -// This prevents the bot from gettings completely stuck in some cases -static std::vector findClosestNavSquare_localAreas(6); - -// Function for getting closest Area to player, aka "LocalNav" -CNavArea *findClosestNavSquare(const Vector &vec) +CNavArea *findClosestNavSquare(const Vector origin) { - bool isLocal = vec == g_pLocalPlayer->v_Origin; - if (isLocal && findClosestNavSquare_localAreas.size() > 5) - findClosestNavSquare_localAreas.erase(findClosestNavSquare_localAreas.begin()); - - float ovBestDist = FLT_MAX, bestDist = FLT_MAX; - // If multiple candidates for LocalNav have been found, pick the closest - CNavArea *ovBestSquare = nullptr, *bestSquare = nullptr; - for (auto &i : navfile->m_areas) - { - // Make sure we're not stuck on the same area for too long - if (isLocal && std::count(findClosestNavSquare_localAreas.begin(), findClosestNavSquare_localAreas.end(), &i) >= 3) - { - continue; - } - float dist = i.m_center.DistTo(vec); - if (dist < bestDist) - { - bestDist = dist; - bestSquare = &i; - } - // Check if we are within x and y bounds of an area - if (ovBestDist >= dist || !i.IsOverlapping(vec) || !IsVectorVisibleNavigation(vec, i.m_center, MASK_PLAYERSOLID)) - { - continue; - } - ovBestDist = dist; - ovBestSquare = &i; - } - if (!ovBestSquare) - ovBestSquare = bestSquare; - - if (isLocal) - findClosestNavSquare_localAreas.push_back(ovBestSquare); - - return ovBestSquare; + return map->findClosestNavSquare(origin); } -std::vector findPath(const Vector &start, const Vector &end) +std::vector *getCrumbs() { - using namespace std::chrono; - - if (status != on) - return {}; - - CNavArea *local, *dest; - if (!(local = findClosestNavSquare(start)) || !(dest = findClosestNavSquare(end))) - return {}; - - if (log_pathing) - { - logging::Info("Start: (%f,%f,%f)", local->m_center.x, local->m_center.y, local->m_center.z); - logging::Info("End: (%f,%f,%f)", dest->m_center.x, dest->m_center.y, dest->m_center.z); - } - float cost; - std::vector pathNodes; - - time_point begin_pathing = high_resolution_clock::now(); - int result = Map.pather->Solve(reinterpret_cast(local), reinterpret_cast(dest), reinterpret_cast *>(&pathNodes), &cost); - long long timetaken = duration_cast(high_resolution_clock::now() - begin_pathing).count(); - if (log_pathing) - logging::Info("Pathing: Pather result: %i. Time taken (NS): %lld", result, timetaken); - // If no result found, return empty Vector - if (result == micropather::MicroPather::NO_SOLUTION) - return {}; - - return pathNodes; + return &crumbs; } -static Vector loc(0.0f, 0.0f, 0.0f); -static CNavArea *last_area = nullptr; -bool ReadyForCommands = true; static Timer inactivity{}; -int curr_priority = 0; -static bool ensureArrival = false; +static Timer time_spent_on_crumb{}; bool navTo(const Vector &destination, int priority, bool should_repath, bool nav_to_local, bool is_repath) { - if (!prepare() || priority < curr_priority) + if (!isReady()) + return false; + // Don't path, priority is too low + if (priority < current_priority) return false; - auto path = findPath(g_pLocalPlayer->v_Origin, destination); + CNavArea *start_area = map->findClosestNavSquare(g_pLocalPlayer->v_Origin); + CNavArea *dest_area = map->findClosestNavSquare(destination); + + if (!start_area || !dest_area) + return false; + auto path = map->findPath(start_area, dest_area); if (path.empty()) - { - clearInstructions(); return false; - } - auto crumb = crumbs.begin(); - if (crumb != crumbs.end() && ignoremanager::addTime(last_area, *crumb, inactivity)) - ResetPather(); - auto path_it = path.begin(); - last_area = *path_it; if (!nav_to_local) - { - path.erase(path_it); - if (path.empty()) - return false; - } - inactivity.update(); - if (!is_repath) - findClosestNavSquare_localAreas.clear(); + path.erase(path.begin()); + crumbs.clear(); + + for (size_t i = 0; i < path.size(); i++) + { + CNavArea *area = reinterpret_cast(path.at(i)); + + // All entries besides the last need an extra crumb + if (i != path.size() - 1) + { + CNavArea *next_area = (CNavArea *) path.at(i + 1); + + auto points = determinePoints(area, next_area); + + points.center = handleDropdown(points.center, points.next); + + crumbs.push_back({ area, std::move(points.current) }); + crumbs.push_back({ area, std::move(points.center) }); + } + else + crumbs.push_back({ area, area->m_center }); + } + + crumbs.push_back({ nullptr, destination }); + inactivity.update(); + + current_priority = priority; + current_navtolocal = nav_to_local; + repath_on_fail = should_repath; + // Ensure we know where to go + if (repath_on_fail) + last_destination = destination; - ensureArrival = should_repath; - ReadyForCommands = false; - curr_priority = priority; - crumbs = std::move(path); - endPoint = destination; return true; } -void repath() +// Use when something unexpected happens, e.g. vischeck fails +void abandonPath() { - if (!ensureArrival) + if (!map) return; - - Vector last; - if (!crumbs.empty()) - last = crumbs.back()->m_center; - else if (endPoint.IsValid()) - last = endPoint; + map->pather.Reset(); + crumbs.clear(); + last_crumb.navarea = nullptr; + // We want to repath on failure + if (repath_on_fail) + navTo(last_destination, current_priority, true, current_navtolocal, false); else - return; - - clearInstructions(); - ResetPather(); - navTo(last, curr_priority, true, true, true); + current_priority = 0; } -// Track pather resets -static Timer reset_pather_timer{}; -// Update area score to prefer paths used by actual players a bit more -void updateAreaScore() +// Use to cancel pathing completely +void cancelPath() { - for (int i = 1; i <= g_IEngine->GetMaxClients(); i++) - { - CachedEntity *ent = ENTITY(i); - if (i == g_pLocalPlayer->entity_idx || CE_INVALID(ent) || !g_pPlayerResource->isAlive(i)) - continue; - - // Get area - CNavArea *closest_area = nullptr; - if (ent->m_vecDormantOrigin()) - findClosestNavSquare(*ent->m_vecDormantOrigin()); - - // Add usage to area if valid - if (closest_area) - area_score[closest_area->m_id] += g_GlobalVars->interval_per_tick; - } - if (reset_pather_timer.test_and_set(10000)) - ResetPather(); + crumbs.clear(); + last_crumb.navarea = nullptr; + current_priority = 0; } static Timer last_jump{}; -// Main movement function, gets path from NavTo -static void cm() +// Used to determine if we want to jump or if we want to crouch +static bool crouch = false; +static int ticks_since_jump = 0; +static Crumb current_crumb; + +static void followCrumbs() { - if (!enabled || status != on) - return; - // Run the logic for Nav area score - updateAreaScore(); + size_t crumbs_amount = crumbs.size(); - if (CE_BAD(LOCAL_E) || CE_BAD(LOCAL_W)) - return; - if (!LOCAL_E->m_bAlivePlayer()) + // No more crumbs, reset status + if (!crumbs_amount) { - // Clear path if player dead - clearInstructions(); + // Invalidate last crumb + last_crumb.navarea = nullptr; + + repath_on_fail = false; + current_priority = 0; return; } - ignoremanager::updateIgnores(); - auto crumb = crumbs.begin(); - const Vector *crumb_vec; - // Crumbs empty, prepare for next instruction - if (crumb == crumbs.end()) + if (current_crumb.navarea != crumbs[0].navarea) + time_spent_on_crumb.update(); + current_crumb = crumbs[0]; + + // We are close enough to the crumb to have reached it + if (crumbs[0].vec.DistTo(g_pLocalPlayer->v_Origin) < 50) { - if (endPoint.IsValid()) - crumb_vec = &endPoint; - else - { - curr_priority = 0; - ReadyForCommands = true; - ensureArrival = false; + last_crumb = crumbs[0]; + crumbs.erase(crumbs.begin()); + time_spent_on_crumb.update(); + if (!--crumbs_amount) return; - } - } - else - crumb_vec = &(*crumb)->m_center; - - ReadyForCommands = false; - // Remove old crumbs - if (g_pLocalPlayer->v_Origin.DistTo(*crumb_vec) < 50.0f) - { inactivity.update(); - if (crumb_vec == &endPoint) - { - endPoint.Invalidate(); + } + // We are close enough to the second crumb, Skip both (This is espcially helpful with drop downs) + if (crumbs.size() > 1 && crumbs[1].vec.DistTo(g_pLocalPlayer->v_Origin) < 50) + { + last_crumb = crumbs[1]; + crumbs.erase(crumbs.begin(), std::next(crumbs.begin())); + --crumbs_amount; + if (!--crumbs_amount) return; - } - last_area = *crumb; - crumbs.erase(crumb); - crumb = crumbs.begin(); - if (crumb == crumbs.end()) + inactivity.update(); + } + + // If we make any progress at all, reset this + else + { + // If we spend way too long on this crumb, ignore the logic below + if (!time_spent_on_crumb.check(*stuck_detect_time * 1000)) { - if (!endPoint.IsValid()) - { - logging::Info("navparser.cpp cm -> endPoint.IsValid() == false"); - return; - } - crumb_vec = &endPoint; + Vector vel; + velocity::EstimateAbsVelocity(RAW_ENT(LOCAL_E), vel); + // 44.0f -> Revved brass beast, do not use z axis as jumping counts towards that. Yes this will mean long falls will trigger it, but that is not really bad. + if (!vel.AsVector2D().IsZero(40.0f)) + inactivity.update(); } } - if (look && !hacks::shared::aimbot::CurrentTarget()) + + // Detect when jumping is necessary. + // 1. No jumping if zoomed (or revved) + // 2. Jump if its necessary to do so based on z values + // 3. Jump if stuck (not getting closer) for more than stuck_time/2 (500ms) + if ((!(g_pLocalPlayer->holding_sniper_rifle && g_pLocalPlayer->bZoomed) && !(g_pLocalPlayer->bRevved || g_pLocalPlayer->bRevving) && (crouch || crumbs[0].vec.z - g_pLocalPlayer->v_Origin.z > 18) && last_jump.check(200)) || (last_jump.check(200) && inactivity.check(*stuck_time / 2))) { - Vector next{ crumb_vec->x, crumb_vec->y, g_pLocalPlayer->v_Eye.z }; - next = GetAimAtAngles(g_pLocalPlayer->v_Eye, next); - hacks::tf2::misc_aimbot::DoSlowAim(next); - current_user_cmd->viewangles = next; - } - // Detect when jumping is necessary - if ((!(g_pLocalPlayer->holding_sniper_rifle && g_pLocalPlayer->bZoomed) && crumb_vec->z - g_pLocalPlayer->v_Origin.z > 18 && last_jump.check(200)) || (last_jump.check(200) && inactivity.check(*stuck_time / 2))) - { - auto local = findClosestNavSquare(g_pLocalPlayer->v_Origin); + auto local = map->findClosestNavSquare(g_pLocalPlayer->v_Origin); // Check if current area allows jumping if (!local || !(local->m_attributeFlags & (NAV_MESH_NO_JUMP | NAV_MESH_STAIRS))) { - static bool flip_action = false; - // Make it crouch the second tick - current_user_cmd->buttons |= flip_action ? IN_DUCK : IN_JUMP; + // Make it crouch until we land, but jump the first tick + current_user_cmd->buttons |= crouch ? IN_DUCK : IN_JUMP; - // Update jump timer now - if (flip_action) + // Only flip to crouch state, not to jump state + if (!crouch) + { + crouch = true; + ticks_since_jump = 0; + } + ticks_since_jump++; + + // Update jump timer now since we are back on ground + if (crouch && CE_INT(LOCAL_E, netvar.iFlags) & FL_ONGROUND && ticks_since_jump > 3) + { + // Reset + crouch = false; last_jump.update(); - flip_action = !flip_action; + } } } - // Walk to next crumb - WalkTo(*crumb_vec); - /* If can't go through for some time (doors aren't instantly opening) - * ignore that connection - * Or if inactive for too long - */ - if (inactivity.check(*stuck_time) || (inactivity.check(*unreachable_time) && !IsVectorVisible(g_pLocalPlayer->v_Origin, *crumb_vec + Vector(.0f, .0f, 41.5f), false, LOCAL_E, MASK_PLAYERSOLID))) + + /*if (inactivity.check(*stuck_time) || (inactivity.check(*unreachable_time) && !IsVectorVisible(g_pLocalPlayer->v_Origin, *crumb_vec + Vector(.0f, .0f, 41.5f), false, LOCAL_E, MASK_PLAYERSOLID))) { - /* crumb is invalid if endPoint is used */ - if (crumb_vec != &endPoint) + if (crumbs[0].navarea) ignoremanager::addTime(last_area, *crumb, inactivity); repath(); return; + }*/ + + // Look at path + if (look && !hacks::shared::aimbot::isAiming()) + { + Vector next{ crumbs[0].vec.x, crumbs[0].vec.y, g_pLocalPlayer->v_Eye.z }; + next = GetAimAtAngles(g_pLocalPlayer->v_Eye, next); + + // Slow aim to smoothen + hacks::tf2::misc_aimbot::DoSlowAim(next); + current_user_cmd->viewangles = next; + } + + WalkTo(crumbs[0].vec); +} + +static Timer vischeck_timer{}; +void vischeckPath() +{ + // No crumbs to check, or vischeck timer should not run yet, bail. + if (crumbs.size() < 2 || !vischeck_timer.test_and_set(*vischeck_time)) + return; + + // Iterate all the crumbs + for (int i = 0; i < (int) crumbs.size() - 1; i++) + { + auto current_crumb = crumbs[i]; + auto next_crumb = crumbs[i + 1]; + auto current_center = current_crumb.vec; + auto next_center = next_crumb.vec; + + current_center.z += PLAYER_JUMP_HEIGHT; + next_center.z += PLAYER_JUMP_HEIGHT; + auto key = std::pair(current_crumb.navarea, next_crumb.navarea); + // Check if we can pass, if not, abort pathing and mark as bad + if (!IsPlayerPassableNavigation(current_center, next_center)) + { + // Mark as invalid for a while + map->vischeck_cache[key] = { TICKCOUNT_TIMESTAMP(*vischeck_cache_time), false }; + abandonPath(); + } + // Else we can update the cache (if not marked bad before this) + else if (map->vischeck_cache.find(key) == map->vischeck_cache.end() || map->vischeck_cache[key].vischeck_state) + { + map->vischeck_cache[key] = { TICKCOUNT_TIMESTAMP(*vischeck_cache_time), true }; + } + } +} + +static Timer blacklist_check_timer{}; +// Check if one of the crumbs is suddenly blacklisted +void checkBlacklist() +{ + // Only check every 500ms + if (!blacklist_check_timer.test_and_set(500)) + return; + + // Local player is ubered and does not care about the blacklist + // TODO: Only for damage type things + if (IsPlayerInvulnerable(LOCAL_E)) + { + map->free_blacklist_blocked = true; + map->pather.Reset(); + return; + } + CNavArea *local_area = map->findClosestNavSquare(g_pLocalPlayer->v_Origin); + for (auto &entry : map->free_blacklist) + { + // Local player is in a blocked area, so temporarily remove the blacklist as else we would be stuck + if (entry.first == local_area) + { + map->free_blacklist_blocked = true; + map->pather.Reset(); + return; + } + } + + // Local player is not blocking the nav area, so blacklist should not be marked as blocked + map->free_blacklist_blocked = false; + + bool should_abandon = false; + for (auto &crumb : crumbs) + { + if (should_abandon) + break; + // A path Node is blacklisted, abandon pathing + for (auto &entry : map->free_blacklist) + { + if (entry.first == crumb.navarea) + should_abandon = true; + } + } + if (should_abandon) + abandonPath(); +} + +void updateStuckTime() +{ + // No crumbs + if (!crumbs.size()) + return; + // We're stuck, add time to connection + if (inactivity.check(*stuck_time / 2)) + { + std::pair key; + // last crumb is invalid + if (!last_crumb.navarea) + key = std::pair(crumbs[0].navarea, crumbs[0].navarea); + else + key = std::pair(last_crumb.navarea, crumbs[0].navarea); + + // Expires in 10 seconds + map->connection_stuck_time[key].expire_tick = TICKCOUNT_TIMESTAMP(*stuck_expire_time); + // Stuck for one tick + map->connection_stuck_time[key].time_stuck += 1; + + // We are stuck for too long, blastlist node for a while and repath + if (map->connection_stuck_time[key].time_stuck > TIME_TO_TICKS(*stuck_detect_time)) + { + map->vischeck_cache[key].expire_tick = TICKCOUNT_TIMESTAMP(*stuck_blacklist_time); + map->vischeck_cache[key].vischeck_state = false; + if (log_pathing) + logging::Info("Blackisted connection %d->%d", key.first->m_id, key.second->m_id); + abandonPath(); + } + } +} + +void CreateMove() +{ + if (!isReady()) + return; + if (CE_BAD(LOCAL_E) || !LOCAL_E->m_bAlivePlayer()) + { + cancelPath(); + return; + } + round_states round_state = g_pTeamRoundTimer->GetRoundState(); + // Still in setuptime, if on fitting team, then do not path yet + if (round_state == RT_STATE_SETUP && g_pLocalPlayer->team == TEAM_BLU) + { + if (navparser::NavEngine::isPathing()) + navparser::NavEngine::cancelPath(); + return; + } + + if (vischeck_runtime) + vischeckPath(); + checkBlacklist(); + + followCrumbs(); + updateStuckTime(); + map->updateIgnores(); +} + +void LevelInit() +{ + auto level_name = g_IEngine->GetLevelName(); + if (!map || map->mapname != level_name) + { + char *p, cwd[PATH_MAX + 1], nav_path[PATH_MAX + 1], lvl_name[256]; + + std::strncpy(lvl_name, level_name, 255); + lvl_name[255] = 0; + p = std::strrchr(lvl_name, '.'); + if (!p) + { + logging::Info("Failed to find dot in level name"); + return; + } + *p = 0; + p = getcwd(cwd, sizeof(cwd)); + if (!p) + { + logging::Info("Failed to get current working directory: %s", strerror(errno)); + return; + } + std::snprintf(nav_path, sizeof(nav_path), "%s/tf/%s.nav", cwd, lvl_name); + logging::Info("Pathing: Nav File location: %s", nav_path); + map = std::make_unique(nav_path); + } + else + { + map->Reset(); + } +} + +// Return the whole thing +std::unordered_map *getFreeBlacklist() +{ + return &map->free_blacklist; +} + +// Return a specific category, we keep the same indexes to provide single element erasing +std::unordered_map getFreeBlacklist(BlacklistReason reason) +{ + std::unordered_map return_map; + for (auto &entry : map->free_blacklist) + { + // Category matches + if (entry.second.value == reason.value) + return_map[entry.first] = entry.second; + } + return return_map; +} + +// Clear whole blacklist +void clearFreeBlacklist() +{ + map->free_blacklist.clear(); +} + +// Clear by category +void clearFreeBlacklist(BlacklistReason reason) +{ + for (auto it = begin(map->free_blacklist); it != end(map->free_blacklist);) + { + if (it->second.value == reason.value) + it = map->free_blacklist.erase(it); // previously this was something like m_map.erase(it++); + else + ++it; } } #if ENABLE_VISUALS -static void drawcrumbs() +void drawNavArea(CNavArea *area) { - if (!enabled || !draw) + Vector nw, ne, sw, se; + bool nw_screen = draw::WorldToScreen(area->m_nwCorner, nw); + bool ne_screen = draw::WorldToScreen(area->getNeCorner(), ne); + bool sw_screen = draw::WorldToScreen(area->getSwCorner(), sw); + bool se_screen = draw::WorldToScreen(area->m_seCorner, se); + + // Nw -> Ne + if (nw_screen && ne_screen) + draw::Line(nw.x, nw.y, ne.x - nw.x, ne.y - nw.y, colors::green, 1.0f); + // Nw -> Sw + if (nw_screen && sw_screen) + draw::Line(nw.x, nw.y, sw.x - nw.x, sw.y - nw.y, colors::green, 1.0f); + // Ne -> Se + if (ne_screen && se_screen) + draw::Line(ne.x, ne.y, se.x - ne.x, se.y - ne.y, colors::green, 1.0f); + // Sw -> Se + if (sw_screen && se_screen) + draw::Line(sw.x, sw.y, se.x - sw.x, se.y - sw.y, colors::green, 1.0f); +} + +void Draw() +{ + if (!isReady() || !draw) return; - if (CE_BAD(LOCAL_E) || CE_BAD(LOCAL_W)) - return; - if (!LOCAL_E->m_bAlivePlayer()) - return; - if (crumbs.size() < 2) + if (draw_debug_areas && CE_GOOD(LOCAL_E) && LOCAL_E->m_bAlivePlayer()) + { + auto area = map->findClosestNavSquare(g_pLocalPlayer->v_Origin); + auto edge = area->getNearestPoint(g_pLocalPlayer->v_Origin.AsVector2D()); + Vector scrEdge; + edge.z += PLAYER_JUMP_HEIGHT; + if (draw::WorldToScreen(edge, scrEdge)) + draw::Rectangle(scrEdge.x - 2.0f, scrEdge.y - 2.0f, 4.0f, 4.0f, colors::red); + drawNavArea(area); + } + + if (crumbs.empty()) return; + for (size_t i = 0; i < crumbs.size(); i++) { - Vector wts1, wts2, *o1, *o2; - if (crumbs.size() - 1 == i) - { - if (!endPoint.IsValid()) - break; + Vector start_pos = crumbs[i].vec; - o2 = &endPoint; - } - else - o2 = &crumbs[i + 1]->m_center; - - o1 = &crumbs[i]->m_center; - if (draw::WorldToScreen(*o1, wts1) && draw::WorldToScreen(*o2, wts2)) + Vector start_screen, end_screen; + if (draw::WorldToScreen(start_pos, start_screen)) { - draw::Line(wts1.x, wts1.y, wts2.x - wts1.x, wts2.y - wts1.y, colors::white, 0.3f); + draw::Rectangle(start_screen.x - 5.0f, start_screen.y - 5.0f, 10.0f, 10.0f, colors::white); + + if (i < crumbs.size() - 1) + { + Vector end_pos = crumbs[i + 1].vec; + if (draw::WorldToScreen(end_pos, end_screen)) + draw::Line(start_screen.x, start_screen.y, end_screen.x - start_screen.x, end_screen.y - start_screen.y, colors::white, 2.0f); + } } } - Vector wts; - if (!draw::WorldToScreen(crumbs[0]->m_center, wts)) - return; - draw::Rectangle(wts.x - 4, wts.y - 4, 8, 8, colors::white); - draw::RectangleOutlined(wts.x - 4, wts.y - 4, 7, 7, colors::white, 1.0f); } #endif +}; // namespace NavEngine -static InitRoutine runinit([]() { - EC::Register(EC::CreateMove, cm, "cm_navparser", EC::average); -#if ENABLE_VISUALS - EC::Register(EC::Draw, drawcrumbs, "draw_navparser", EC::average); -#endif -}); - -void ResetPather() -{ - Map.pather->Reset(); -} - -bool isSafe(CNavArea *area) -{ - return ignoremanager::isSafe(area); -} - -static CatCommand nav_find("nav_find", "Debug nav find", []() { - auto path = findPath(g_pLocalPlayer->v_Origin, loc); - if (path.empty()) - { - logging::Info("Pathing: No path found"); - return; - } - std::string output = "Pathing: Path found! Path: "; - for (int i = 0; i < path.size(); i++) - { - output.append(format(path[i]->m_center.x, ",", format(path[i]->m_center.y), " ")); - } - logging::Info(output.c_str()); -}); +Vector loc; static CatCommand nav_set("nav_set", "Debug nav find", []() { loc = g_pLocalPlayer->v_Origin; }); -static CatCommand nav_init("nav_init", "Debug nav init", []() { - status = off; - prepare(); +static CatCommand nav_path("nav_path", "Debug nav path", []() { NavEngine::navTo(loc, 20, true, true, false); }); + +static CatCommand nav_path_noreapth("nav_path_norepath", "Debug nav path", []() { NavEngine::navTo(loc, 20, false, true, false); }); + +static CatCommand nav_init("nav_init", "Reload nav mesh", []() { + NavEngine::map.reset(); + NavEngine::LevelInit(); }); -static CatCommand nav_path("nav_path", "Debug nav path", []() { navTo(loc); }); +static CatCommand nav_debug_check("nav_debug_check", "Perform nav checks between two areas. First area: cat_nav_set Second area: Your location while running this command.", []() { + if (!NavEngine::isReady()) + return; + auto next = NavEngine::map->findClosestNavSquare(g_pLocalPlayer->v_Origin); + auto current = NavEngine::map->findClosestNavSquare(loc); -static CatCommand nav_path_no_local("nav_path_no_local", "Debug nav path", []() { navTo(loc, 5, false, false); }); + auto points = determinePoints(current, next); -static CatCommand nav_reset_ignores("nav_reset_ignores", "Reset all ignores.", []() { ignoremanager::reset(); }); + points.center = handleDropdown(points.center, points.next); -void clearInstructions() -{ - crumbs.clear(); - endPoint.Invalidate(); - curr_priority = 0; -} -static CatCommand nav_stop("nav_cancel", "Cancel Navigation", []() { clearInstructions(); }); -} // namespace nav + // Too high for us to jump! + if (points.center_next.z - points.center.z > PLAYER_JUMP_HEIGHT) + { + return logging::Info("Nav: Area too high!"); + } + + points.current.z += PLAYER_JUMP_HEIGHT; + points.center.z += PLAYER_JUMP_HEIGHT; + points.next.z += PLAYER_JUMP_HEIGHT; + + if (IsPlayerPassableNavigation(points.current, points.center) && IsPlayerPassableNavigation(points.center, points.next)) + { + logging::Info("Nav: Area is player passable!"); + } + else + { + logging::Info("Nav: Area is NOT player passable! %.2f,%.2f,%.2f %.2f,%.2f,%.2f %.2f,%.2f,%.2f", points.current.x, points.current.y, points.current.z, points.center.x, points.center.y, points.center.z, points.next.x, points.next.y, points.next.z); + } +}); + +static CatCommand nav_debug_blacklist("nav_debug_blacklist", "Blacklist connection between two areas for 30s. First area: cat_nav_set Second area: Your location while running this command.", []() { + if (!NavEngine::isReady()) + return; + auto next = NavEngine::map->findClosestNavSquare(g_pLocalPlayer->v_Origin); + auto current = NavEngine::map->findClosestNavSquare(loc); + + std::pair key(current, next); + NavEngine::map->vischeck_cache[key].expire_tick = TICKCOUNT_TIMESTAMP(30); + NavEngine::map->vischeck_cache[key].vischeck_state = false; + NavEngine::map->pather.Reset(); + logging::Info("Nav: Connection %d->%d Blacklisted.", current->m_id, next->m_id); +}); + +static InitRoutine init([]() { + EC::Register(EC::CreateMove, NavEngine::CreateMove, "navengine_cm"); + EC::Register(EC::LevelInit, NavEngine::LevelInit, "navengine_levelinit"); +#if ENABLE_VISUALS + EC::Register(EC::Draw, NavEngine::Draw, "navengine_draw"); +#endif + enabled.installChangeCallback([](settings::VariableBase &, bool after) { + if (after && g_IEngine->IsInGame()) + NavEngine::LevelInit(); + }); +}); + +} // namespace navparser diff --git a/src/payloadcontroller.cpp b/src/payloadcontroller.cpp new file mode 100644 index 00000000..95dbabb0 --- /dev/null +++ b/src/payloadcontroller.cpp @@ -0,0 +1,69 @@ +#include "payloadcontroller.hpp" +#include "common.hpp" + +namespace plcontroller +{ + +// Array that controls all the payloads for each team. Red team is first, then comes blue team. +static std::array, 2> payloads; +static Timer update_payloads{}; + +void Update() +{ + // We should update the payload list + if (update_payloads.test_and_set(3000)) + { + // Reset entries + for (auto &entry : payloads) + entry.clear(); + + for (int i = g_IEngine->GetMaxClients() + 1; i < MAX_ENTITIES; i++) + { + CachedEntity *ent = ENTITY(i); + // Not the object we need or invalid (team) + if (CE_BAD(ent) || ent->m_iClassID() != CL_CLASS(CObjectCartDispenser) || ent->m_iTeam() < TEAM_RED || ent->m_iTeam() > TEAM_BLU) + continue; + int team = ent->m_iTeam(); + + // Add new entry for the team + payloads.at(team - TEAM_RED).push_back(ent); + } + } +} +std::optional getClosestPayload(Vector source, int team) +{ + // Invalid team + if (team < TEAM_RED || team > TEAM_BLU) + return std::nullopt; + // Convert to index + int index = team - TEAM_RED; + auto entry = payloads[index]; + + float best_distance = FLT_MAX; + std::optional best_pos; + + // Find best payload + for (auto payload : entry) + { + if (CE_BAD(payload) || payload->m_iClassID() != CL_CLASS(CObjectCartDispenser)) + continue; + if (payload->m_vecOrigin().DistTo(source) < best_distance) + { + best_pos = payload->m_vecOrigin(); + best_distance = payload->m_vecOrigin().DistTo(source); + } + } + return best_pos; +} + +void LevelInit() +{ + for (auto &entry : payloads) + entry.clear(); +} + +static InitRoutine init([]() { + EC::Register(EC::CreateMove, Update, "plcreatemove"); + EC::Register(EC::LevelInit, LevelInit, "levelinit_plcontroller"); +}); +} // namespace plcontroller diff --git a/src/settings/SettingsIO.cpp b/src/settings/SettingsIO.cpp index 3d9fb25f..53d470f6 100644 --- a/src/settings/SettingsIO.cpp +++ b/src/settings/SettingsIO.cpp @@ -226,11 +226,15 @@ struct migration_struct }; /* clang-format off */ // Use one per line, from -> to -static std::array migrations({ +static std::array migrations({ migration_struct{ "misc.semi-auto", "misc.full-auto" }, migration_struct{ "cat-bot.abandon-if.bots-gte", "cat-bot.abandon-if.ipc-bots-gte" }, migration_struct{ "votelogger.partysay-casts", "votelogger.chat.casts" }, - migration_struct{ "votelogger.partysay-casts.f1-only", "votelogger.chat.casts.f1-only" } + migration_struct{ "votelogger.partysay-casts.f1-only", "votelogger.chat.casts.f1-only" }, + migration_struct{ "misc.pathing", "nav.enabled" }, + migration_struct{ "misc.pathing.draw", "nav.draw" }, + migration_struct{ "misc.pathing.log", "nav.log"}, + migration_struct{ "misc.pathing.look-at-path", "nav.look-at-path"} }); /* clang-format on */ void settings::SettingsReader::finishString(bool complete) diff --git a/src/targethelper.cpp b/src/targethelper.cpp index 646cbd32..6e1b358c 100644 --- a/src/targethelper.cpp +++ b/src/targethelper.cpp @@ -22,11 +22,22 @@ int GetScoreForEntity(CachedEntity *entity) { if (!entity) return 0; - // TODO + if (entity->m_Type() == ENTITY_BUILDING) { if (entity->m_iClassID() == CL_CLASS(CObjectSentrygun)) { + bool is_strong_class = g_pLocalPlayer->clazz == tf_heavy || g_pLocalPlayer->clazz == tf_soldier; + + if (is_strong_class) + { + float distance = (g_pLocalPlayer->v_Origin - entity->m_vecOrigin()).Length(); + if (distance < 400.0f) + return 120; + else if (distance < 1100.0f) + return 60; + return 30; + } return 1; } return 0; @@ -94,7 +105,7 @@ int GetScoreForEntity(CachedEntity *entity) total = 999; if (IsSentryBuster(entity)) total = 0; - if (clazz == tf_medic) - total = 999; // TODO only for mvm + if (clazz == tf_medic && g_pGameRules->isPVEMode) + total = 999; return total; }