diff --git a/data/menu/nullifiedcat/movement.xml b/data/menu/nullifiedcat/movement.xml index aa6b8fb0..01b3c227 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 9f72fdf6..b2e479c4 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] @@ -204,6 +205,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; offset_t m_flResetTime; 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 99474f0b..c17ae4c3 100644 --- a/include/helpers.hpp +++ b/include/helpers.hpp @@ -203,7 +203,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 cde34813..669ec649 100644 --- a/include/localplayer.hpp +++ b/include/localplayer.hpp @@ -36,6 +36,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 27dc2d87..c3ba4756 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 27b12d79..ee4aea58 100644 --- a/src/hacks/Aimbot.cpp +++ b/src/hacks/Aimbot.cpp @@ -217,6 +217,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; @@ -237,6 +238,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; @@ -982,6 +985,7 @@ void Aim(CachedEntity *entity) if (data) hacks::tf2::backtrack::SetBacktrackData(entity, *data); } + aimed_this_tick = true; // Finish function return; } @@ -1515,6 +1519,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 c6764258..289c97dd 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 5bb5defc..9ffb7fd8 100644 --- a/src/hacks/Misc.cpp +++ b/src/hacks/Misc.cpp @@ -638,7 +638,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 c1d99f14..0976048a 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,42 +1018,114 @@ 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::CreateMoveWarp, CreateMove, "navbot_w", 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::CreateMoveWarp, 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 5bf7a776..14668dd6 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) @@ -1462,7 +1471,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 d9336441..7d67dd1e 100644 --- a/src/hooks/CreateMove.cpp +++ b/src/hooks/CreateMove.cpp @@ -252,10 +252,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 05d9ba27..699f71bc 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 31496ffb..652eaf8b 100644 --- a/src/navparser.cpp +++ b/src/navparser.cpp @@ -1,861 +1,1044 @@ +/* + 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))) - { - /* crumb is invalid if endPoint is used */ - if (crumb_vec != &endPoint) - ignoremanager::addTime(last_area, *crumb, inactivity); + /*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 (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_NoEnginePred, 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_NoEnginePred, 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; }