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