bot: implemented asynchronous pathfinding

nav: floyd-warshall matrices and practice updates are done asynchronously by now
add: yb_threadpool_workers cvar, that controls number of worker threads bot will use
add: cv_autovacate_keep_slots, the amount of slots to keep by auto vacate
aim: enemy prediction is now done asynchronously by now
bot: minor fixes and refactoring, including analyze suspend mistake (ref #441)

note: the master builds are now NOT production ready, please test before installing on real servers!
This commit is contained in:
jeefo 2023-05-06 20:14:03 +03:00
commit a616f25b1a
No known key found for this signature in database
GPG key ID: 927BCA0779BEA8ED
30 changed files with 743 additions and 421 deletions

@ -1 +1 @@
Subproject commit 4d370ec500675ad23e9507831bbbbd6bb166ce3a
Subproject commit 95405e93fcdad85586103c843a20f8cb5985ae94

View file

@ -39,11 +39,9 @@ private:
estring_t offset;
public:
explicit string_t () : offset (0)
{ }
explicit string_t () : offset (0) {}
string_t (int offset) : offset (offset)
{ }
string_t (int offset) : offset (offset) {}
~string_t () = default;

View file

@ -48,8 +48,7 @@ GNU General Public License for more details.
#define LUMP_SAVE_NO_DATA 7
#define LUMP_SAVE_CORRUPTED 8
typedef struct areanode_s
{
typedef struct areanode_s {
int axis; // -1 = leaf node
float dist;
struct areanode_s *children[2];
@ -58,8 +57,7 @@ typedef struct areanode_s
link_t portal_edicts;
} areanode_t;
typedef struct server_physics_api_s
{
typedef struct server_physics_api_s {
// unlink edict from old position and link onto new
void (*pfnLinkEdict) (edict_t *ent, qboolean touch_triggers);
double (*pfnGetServerTime)(void); // unclamped
@ -109,8 +107,7 @@ typedef struct server_physics_api_s
} server_physics_api_t;
// physic callbacks
typedef struct physics_interface_s
{
typedef struct physics_interface_s {
int version;
// passed through pfnCreate (0 is attempt to create, -1 is reject)
int (*SV_CreateEntity)(edict_t *pent, const char *szName);

View file

@ -146,6 +146,9 @@ public:
// initialize levels
void levelInitialize (edict_t *entities, int max);
// shutdown levels
void levelShutdown ();
// display world line
void drawLine (edict_t *ent, const Vector &start, const Vector &end, int width, int noise, const Color &color, int brightness, int speed, int life, DrawLine type = DrawLine::Simple);
@ -688,7 +691,6 @@ public:
int close (DLSYM_HANDLE module) {
if (m_self.handle () == module) {
disable ();
return m_dlclose (module);
}
return m_dlclose (module);

View file

@ -153,7 +153,6 @@ private:
int m_lastJumpNode {};
int m_findWPIndex {};
int m_facingAtIndex {};
int m_highestDamage[kGameTeamNum] {};
int m_autoSaveCount {};
float m_timeJumpStarted {};
@ -265,14 +264,6 @@ public:
return m_paths.length () / 2;
}
int getHighestDamageForTeam (int team) const {
return cr::max (1, m_highestDamage[team]);
}
void setHighestDamageForTeam (int team, int value) {
m_highestDamage[team] = value;
}
StringRef getAuthor () const {
return m_graphAuthor;
}

View file

@ -63,7 +63,7 @@ private:
float m_maintainTime {}; // time to maintain bot creation
float m_quotaMaintainTime {}; // time to maintain bot quota
float m_grenadeUpdateTime {}; // time to update active grenades
float m_entityUpdateTime {}; // time to update intresting entities
float m_entityUpdateTime {}; // time to update interesting entities
float m_plantSearchUpdateTime {}; // time to update for searching planted bomb
float m_lastChatTime {}; // global chat time timestamp
float m_timeBombPlanted {}; // time the bomb were planted
@ -81,7 +81,7 @@ private:
bool m_roundOver {};
Array <edict_t *> m_activeGrenades {}; // holds currently active grenades on the map
Array <edict_t *> m_intrestingEntities {}; // holds currently intresting entities on the map
Array <edict_t *> m_interestingEntities {}; // holds currently interesting entities on the map
Deque <String> m_saveBotNames {}; // bots names that persist upon changelevel
Deque <BotRequest> m_addRequests {}; // bot creation tab
@ -143,7 +143,7 @@ public:
void initFilters ();
void resetFilters ();
void updateActiveGrenade ();
void updateIntrestingEntities ();
void updateInterestingEntities ();
void captureChatRadio (const char *cmd, const char *arg, edict_t *ent);
void notifyBombDefuse ();
void execGameEntity (edict_t *ent);
@ -160,16 +160,16 @@ public:
return m_activeGrenades;
}
const Array <edict_t *> &getIntrestingEntities () {
return m_intrestingEntities;
const Array <edict_t *> &getInterestingEntities () {
return m_interestingEntities;
}
bool hasActiveGrenades () const {
return !m_activeGrenades.empty ();
}
bool hasIntrestingEntities () const {
return !m_intrestingEntities.empty ();
bool hasInterestingEntities () const {
return !m_interestingEntities.empty ();
}
bool checkTeamEco (int team) const {
@ -302,5 +302,31 @@ public:
}
};
// bot async worker wrapper
class BotThreadWorker final : public Singleton <BotThreadWorker> {
private:
ThreadPool m_botWorker;
public:
BotThreadWorker () = default;
~BotThreadWorker () = default;
public:
void shutdown ();
void startup (int workers);
public:
template <typename F> void enqueue (F &&fn) {
if (m_botWorker.threadCount () == 0) {
fn (); // no threads, no fun, just run task in current thread
return;
}
m_botWorker.enqueue (cr::forward <F> (fn));
}
};
// explose global
CR_EXPOSE_GLOBAL_SINGLETON (BotManager, bots);
// expose async worker
CR_EXPOSE_GLOBAL_SINGLETON (BotThreadWorker, worker);

View file

@ -84,7 +84,7 @@ public:
};
// A* algorithm for bots
class AStarAlgo final {
class AStarAlgo final : public DenyCopying {
public:
using HeuristicFn = Heuristic::Func;
@ -108,7 +108,7 @@ private:
Array <int> m_smoothedPath;
private:
// cleares the currently built route
// clears the currently built route
void clearRoute ();
// can the node can be skipped?
@ -118,6 +118,10 @@ private:
void postSmooth (NodeAdderFn onAddedNode);
public:
explicit AStarAlgo (const int length) {
init (length);
}
AStarAlgo () = default;
~AStarAlgo () = default;
@ -180,6 +184,9 @@ public:
private:
// create floyd matrics
void syncRebuild ();
// async rebuild
void rebuild ();
public:
@ -201,6 +208,9 @@ public:
// dijkstra shortest path algorithm
class DijkstraAlgo final {
private:
mutable Mutex m_cs {};
private:
using Route = Twin <int, int>;
@ -215,11 +225,6 @@ public:
DijkstraAlgo () = default;
~DijkstraAlgo () = default;
private:
// reset pathfinder state to defaults
void resetState ();
public:
// initialize dijkstra with valid path length
void init (const int length);
@ -236,7 +241,6 @@ class PathPlanner : public Singleton <PathPlanner> {
private:
UniquePtr <DijkstraAlgo> m_dijkstra;
UniquePtr <FloydWarshallAlgo> m_floyd;
UniquePtr <AStarAlgo > m_astar;
bool m_memoryLimitHit {};
public:
@ -261,17 +265,15 @@ public:
return m_floyd.get ();
}
// get the floyd algo
decltype (auto) getAStar () {
return m_astar.get ();
}
public:
// do the pathfinding
bool find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance = nullptr);
// distance between two nodes with pathfinder
int dist (int srcIndex, int destIndex);
float dist (int srcIndex, int destIndex);
// get the precise distanace regardless of cvar
float preciseDistance (int srcIndex, int destIndex);
};
CR_EXPOSE_GLOBAL_SINGLETON (PathPlanner, planner);

View file

@ -80,17 +80,22 @@ public:
DangerSaveRestore (const DangerStorage &ds, const PracticeData &pd) : danger (ds), data (pd) {}
};
private:
HashMap <DangerStorage, PracticeData> m_data {};
int32_t m_teamHighestDamage[kGameTeamNum] {};
// avoid concurrent access to practice
mutable Mutex m_damageUpdateLock {};
public:
BotPractice () = default;
~BotPractice () = default;
private:
inline bool exists (int32_t team, int32_t start, int32_t goal) const {
bool exists (int32_t team, int32_t start, int32_t goal) const {
return m_data.exists ({ start, goal, team });
}
void syncUpdate ();
public:
int32_t getIndex (int32_t team, int32_t start, int32_t goal);
@ -102,17 +107,21 @@ public:
int32_t getDamage (int32_t team, int32_t start, int32_t goal);
void setDamage (int32_t team, int32_t start, int32_t goal, int32_t value);
// interlocked get damage
float plannerGetDamage (int32_t team, int32_t start, int32_t goal, bool addTeamHighestDamage);
public:
void update ();
void load ();
void save ();
public:
int32_t getHighestDamageForTeam (int32_t team) const {
return cr::max (1, m_teamHighestDamage[team]);
template <typename U = int32_t> U getHighestDamageForTeam (int32_t team) const {
return static_cast <U> (cr::max (1, m_teamHighestDamage[team]));
}
void setHighestDamageForTeam (int32_t team, int32_t value) {
MutexScopedLock lock (m_damageUpdateLock);
m_teamHighestDamage[team] = value;
}
};

View file

@ -8,6 +8,9 @@
#pragma once
class BotSupport final : public Singleton <BotSupport> {
private:
mutable Mutex m_cs {};
private:
bool m_needToSendWelcome {};
float m_welcomeReceiveTime {};
@ -78,6 +81,9 @@ public:
// generates ping bitmask for SVC_PINGS message
int getPingBitmask (edict_t *ent, int loss, int ping);
// calculate our own pings for all the players
void syncCalculatePings ();
// calculate our own pings for all the players
void calculatePings ();

View file

@ -663,6 +663,10 @@ class Bot final {
public:
friend class BotManager;
private:
mutable Mutex m_pathFindLock {};
mutable Mutex m_predictLock {};
private:
uint32_t m_states {}; // sensing bitstates
uint32_t m_collideMoves[kMaxCollideMoves] {}; // sorted array of movements
@ -689,7 +693,9 @@ private:
int m_tryOpenDoor {}; // attempt's to open the door
int m_liftState {}; // state of lift handling
int m_radioSelect {}; // radio entry
int m_lastPredictIndex {}; // last predicted index
int m_lastPredictIndex { kInvalidNodeIndex }; // last predicted path index
int m_lastPredictLength {}; // last predicted path length
float m_headedTime {};
float m_prevTime {}; // time previously checked movement speed
@ -747,6 +753,7 @@ private:
bool m_moveToGoal {}; // bot currently moving to goal??
bool m_isStuck {}; // bot is stuck
bool m_isStale {}; // bot is leaving server
bool m_isReloading {}; // bot is reloading a gun
bool m_forceRadio {}; // should bot use radio anyway?
bool m_defendedBomb {}; // defend action issued
@ -773,6 +780,7 @@ private:
FindPath m_pathType {}; // which pathfinder to use
uint8_t m_enemyParts {}; // visibility flags
TraceResult m_lastTrace[TraceChannel::Num] {}; // last trace result
UniquePtr <class AStarAlgo> m_planner;
edict_t *m_pickupItem {}; // pointer to entity of item to use/pickup
edict_t *m_itemIgnore {}; // pointer to entity to ignore for pickup
@ -888,7 +896,6 @@ private:
void doPlayerAvoidance (const Vector &normal);
void selectCampButtons (int index);
void markStale ();
void instantChatter (int type);
void update ();
void runMovement ();
@ -920,6 +927,7 @@ private:
void findShortestPath (int srcIndex, int destIndex);
void calculateFrustum ();
void findPath (int srcIndex, int destIndex, FindPath pathType = FindPath::Fast);
void syncFindPath (int srcIndex, int destIndex, FindPath pathType);
void debugMsgInternal (const char *str);
void frame ();
void resetCollision ();
@ -933,6 +941,7 @@ private:
void decideFollowUser ();
void attackMovement ();
void findValidNode ();
void setPathOrigin ();
void fireWeapons ();
void selectWeapons (float distance, int index, int id, int choosen);
void focusEnemy ();
@ -940,6 +949,9 @@ private:
void selectSecondary ();
void selectWeaponById (int id);
void selectWeaponByIndex (int index);
void refreshEnemyPredict ();
void syncUpdatePredictedIndex ();
void updatePredictedIndex ();
void completeTask ();
void executeTasks ();
@ -1094,11 +1106,13 @@ public:
bool m_hasNVG {}; // does bot has nightvision goggles
bool m_usesNVG {}; // does nightvision goggles turned on
bool m_hasC4 {}; // does bot has c4 bomb
bool m_hasHostage {}; // does bot owns some hostages
bool m_hasProgressBar {}; // has progress bar on a HUD
bool m_jumpReady {}; // is double jump ready
bool m_canChooseAimDirection {}; // can choose aiming direction
bool m_isEnemyReachable {}; // direct line to enemy
bool m_kickedByRotation {}; // is bot kicked due to rotation ?
bool m_kickMeFromServer {}; // kick the bot off the server?
edict_t *m_doubleJumpEntity {}; // pointer to entity that request double jump
edict_t *m_radioEntity {}; // pointer to entity issuing a radio command
@ -1121,7 +1135,11 @@ public:
public:
Bot (edict_t *bot, int difficulty, int personality, int team, int skin);
~Bot () = default;
// need to wait until all threads will finish it's work before terminating bot object
~Bot () {
MutexScopedLock lock (m_pathFindLock);
}
public:
void logic (); /// the things that can be executed while skipping frames
@ -1154,7 +1172,7 @@ public:
void resetDoubleJump ();
void startDoubleJump (edict_t *ent);
void sendBotToOrigin (const Vector &origin);
void markStale ();
bool hasHostage ();
bool usesRifle ();
bool usesPistol ();

View file

@ -171,7 +171,7 @@ if cxx == 'clang' or cxx == 'gcc'
# link needed libraries
if os == 'linux'
ldflags += [
'-lm','-ldl'
'-lm','-ldl', '-lpthread'
]
elif os == 'windows'
if cxx == 'gcc' or (cross and cxx == 'clang')

View file

@ -126,6 +126,7 @@ void GraphAnalyze::suspend () {
m_updateInterval = 0.0f;
m_isAnalyzing = false;
m_isAnalyzed = false;
m_basicsCreated = false;
}
void GraphAnalyze::finish () {
@ -170,7 +171,7 @@ void GraphAnalyze::optimize () {
}
cleanup ();
// clear the uselss connections
// clear the useless connections
if (cv_graph_analyze_clean_paths_on_finish.bool_ ()) {
for (auto i = 0; i < graph.length (); ++i) {
graph.clearConnections (i);

View file

@ -13,7 +13,7 @@ ConVar cv_user_follow_percent ("yb_user_follow_percent", "20", "Specifies the pe
ConVar cv_user_max_followers ("yb_user_max_followers", "1", "Specifies how many bots can follow a single user.", true, 0.0f, static_cast <float> (kGameMaxPlayers / 4));
ConVar cv_jasonmode ("yb_jasonmode", "0", "If enabled, all bots will be forced only the knife, skipping weapon buying routines.");
ConVar cv_radio_mode ("yb_radio_mode", "2", "Allows bots to use radio or chattter.\nAllowed values: '0', '1', '2'.\nIf '0', radio and chatter is disabled.\nIf '1', only radio allowed.\nIf '2' chatter and radio allowed.", true, 0.0f, 2.0f);
ConVar cv_radio_mode ("yb_radio_mode", "2", "Allows bots to use radio or chatter.\nAllowed values: '0', '1', '2'.\nIf '0', radio and chatter is disabled.\nIf '1', only radio allowed.\nIf '2' chatter and radio allowed.", true, 0.0f, 2.0f);
ConVar cv_economics_rounds ("yb_economics_rounds", "1", "Specifies whether bots able to use team economics, like do not buy any weapons for whole team to keep money for better guns.");
ConVar cv_economics_disrespect_percent ("yb_economics_disrespect_percent", "25", "Allows bots to ignore economics and buy weapons with disrespect of economics.", true, 0.0f, 100.0f);
@ -44,7 +44,7 @@ ConVar cv_pickup_best ("yb_pickup_best", "1", "Allows or disallows bots to picku
ConVar cv_ignore_objectives ("yb_ignore_objectives", "0", "Allows or disallows bots to do map objectives, i.e. plant/defuse bombs, and saves hostages.");
ConVar cv_random_knife_attacks ("yb_random_knife_attacks", "1", "Allows or disallows the ability for random knife attacks when bot is rushing and no enemy is nearby.");
ConVar cv_max_nodes_for_predict ("yb_max_nodes_for_predict", "30", "Maximum number for path length, to predict the enemy.", true, 15.0f, 256.0f);
ConVar cv_max_nodes_for_predict ("yb_max_nodes_for_predict", "20", "Maximum number for path length, to predict the enemy.", true, 15.0f, 256.0f);
// game console variables
ConVar mp_c4timer ("mp_c4timer", nullptr, Var::GameRef);
@ -103,7 +103,7 @@ bool Bot::seesItem (const Vector &destination, const char *classname) {
game.testLine (getEyesPos (), destination, TraceIgnore::None, ent (), &tr);
// check if line of sight to object is not blocked (i.e. visible)
if (tr.flFraction < 1.0f && tr.pHit) {
if (tr.flFraction < 1.0f && tr.pHit && !tr.fStartSolid) {
return strcmp (tr.pHit->v.classname.chars (), classname) == 0;
}
return true;
@ -217,7 +217,7 @@ void Bot::checkBreakablesAround () {
}
auto radius = cv_object_destroy_radius.float_ ();
// check if we're have some breakbles in 400 units range
// check if we're have some breakables in 400 units range
for (const auto &breakable : game.getBreakables ()) {
bool ignoreBreakable = false;
@ -325,23 +325,23 @@ void Bot::updatePickups () {
// this function finds Items to collect or use in the near of a bot
// don't try to pickup anything while on ladder or trying to escape from bomb...
if ((m_states & Sense::SeeingEnemy) || isOnLadder () || getCurrentTaskId () == Task::EscapeFromBomb || !cv_pickup_best.bool_ () || cv_jasonmode.bool_ () || !bots.hasIntrestingEntities ()) {
if ((m_states & Sense::SeeingEnemy) || isOnLadder () || getCurrentTaskId () == Task::EscapeFromBomb || !cv_pickup_best.bool_ () || cv_jasonmode.bool_ () || !bots.hasInterestingEntities ()) {
m_pickupItem = nullptr;
m_pickupType = Pickup::None;
return;
}
const auto &intresting = bots.getIntrestingEntities ();
const auto &interesting = bots.getInterestingEntities ();
const float radius = cr::sqrf (cv_object_pickup_radius.float_ ());
if (!game.isNullEntity (m_pickupItem)) {
bool itemExists = false;
auto pickupItem = m_pickupItem;
for (auto &ent : intresting) {
for (auto &ent : interesting) {
// in the periods of updating intresting entities we can get fake ones, that already were picked up, so double check if drawn
// in the periods of updating interesting entities we can get fake ones, that already were picked up, so double check if drawn
if (ent->v.effects & EF_NODRAW) {
continue;
}
@ -376,14 +376,14 @@ void Bot::updatePickups () {
m_pickupItem = nullptr;
m_pickupType = Pickup::None;
for (const auto &ent : intresting) {
for (const auto &ent : interesting) {
bool allowPickup = false; // assume can't use it until known otherwise
// get the entity origin
const auto &origin = game.getEntityOrigin (ent);
if ((ent->v.effects & EF_NODRAW) || ent == m_itemIgnore || cr::abs (origin.z - pev->origin.z) > 96.0f) {
continue; // someone owns this weapon or it hasn't respawned yet
continue; // someone owns this weapon or it hasn't re-spawned yet
}
// too far from us ?
@ -692,7 +692,7 @@ void Bot::ensureEntitiesClear () {
if (currentTask == Task::PickupItem || (m_states & Sense::PickupItem)) {
if (!game.isNullEntity (m_pickupItem) && !m_hasProgressBar) {
m_itemIgnore = m_pickupItem; // clear these pointers, bot mingh be stuck getting to them
m_itemIgnore = m_pickupItem; // clear these pointers, bot might be stuck getting to them
}
m_itemCheckTime = game.time () + 5.0f;
@ -720,7 +720,7 @@ void Bot::ensureEntitiesClear () {
Vector Bot::getCampDirection (const Vector &dest) {
// this function check if view on last enemy position is blocked - replace with better vector then
// mostly used for getting a good camping direction vector if not camping on a camp waypoint
// mostly used for getting a good camping direction vector if not camping on a camp node
TraceResult tr {};
const Vector &src = getEyesPos ();
@ -750,7 +750,7 @@ Vector Bot::getCampDirection (const Vector &dest) {
if (link.index == kInvalidNodeIndex) {
continue;
}
auto distance = static_cast <float> (planner.dist (link.index, enemyIndex));
auto distance = planner.dist (link.index, enemyIndex);
if (distance < minDistance) {
minDistance = distance;
@ -771,7 +771,7 @@ Vector Bot::getCampDirection (const Vector &dest) {
}
void Bot::showChatterIcon (bool show, bool disconnect) {
// this function depending on show boolen, shows/remove chatter, icon, on the head of bot.
// this function depending on show boolean, shows/remove chatter, icon, on the head of bot.
if (!game.is (GameFlags::HasBotVoice) || cv_radio_mode.int_ () != 2) {
return;
@ -1588,6 +1588,78 @@ void Bot::overrideConditions () {
}
}
void Bot::syncUpdatePredictedIndex () {
auto wipePredict = [this] () {
m_lastPredictIndex = kInvalidNodeIndex;
m_lastPredictLength = 0;
};
if (!m_predictLock.tryLock ()) {
return; // allow only single instance of search per-bot
}
ScopedUnlock <Mutex> unlock (m_predictLock);
auto lastEnemyOrigin = m_lastEnemyOrigin;
auto currentNodeIndex = m_currentNodeIndex;
auto botOrigin = pev->origin;
if (lastEnemyOrigin.empty () || !vistab.isReady ()) {
wipePredict ();
return;
}
int destIndex = graph.getNearest (lastEnemyOrigin);
int bestIndex = kInvalidNodeIndex;
if (destIndex == kInvalidNodeIndex) {
return;
}
int pathLength = 0;
planner.find (destIndex, currentNodeIndex, [&] (int index) {
++pathLength;
if (vistab.visible (currentNodeIndex, index) && botOrigin.distanceSq (graph[index].origin) > cr::sqrf (256.0f)) {
bestIndex = index;
return false;
}
return true;
});
if (bestIndex != kInvalidNodeIndex) {
m_lastPredictIndex = bestIndex;
m_lastPredictLength = pathLength;
return;
}
wipePredict ();
}
void Bot::updatePredictedIndex () {
worker.enqueue ([this] () {
syncUpdatePredictedIndex ();
});
}
void Bot::refreshEnemyPredict () {
if (game.isNullEntity (m_enemy) && !game.isNullEntity (m_lastEnemy) && !m_lastEnemyOrigin.empty ()) {
const auto distanceToLastEnemySq = m_lastEnemyOrigin.distanceSq (pev->origin);
if (distanceToLastEnemySq > cr::sqrf (384.0f) && (distanceToLastEnemySq < cr::sqrf (1600.0f) || usesSniper ())) {
m_aimFlags |= AimFlags::PredictPath;
}
const bool denyLastEnemy = pev->velocity.lengthSq2d () > 0.0f && distanceToLastEnemySq < cr::sqrf (256.0f);
if (!denyLastEnemy && seesEntity (m_lastEnemyOrigin, true)) {
m_aimFlags |= AimFlags::LastEnemy;
}
}
if (m_aimFlags & AimFlags::PredictPath) {
updatePredictedIndex ();
}
}
void Bot::setConditions () {
// this function carried out each frame. does all of the sensing, calculates emotions and finally sets the desired
// action after applying all of the Filters
@ -1665,6 +1737,9 @@ void Bot::setConditions () {
m_lastVictim = nullptr;
}
m_numFriendsLeft = numFriendsNear (pev->origin, kInfiniteDistance);
m_numEnemiesLeft = numEnemiesNear (pev->origin, kInfiniteDistance);
auto clearLastEnemy = [&] () {
m_lastEnemyOrigin = nullptr;
m_lastEnemy = nullptr;
@ -1688,26 +1763,7 @@ void Bot::setConditions () {
else if (m_heardSoundTime < game.time ()) {
m_states &= ~Sense::HearingEnemy;
}
if (game.isNullEntity (m_enemy) && !game.isNullEntity (m_lastEnemy) && !m_lastEnemyOrigin.empty () && m_seeEnemyTime + 0.5f < game.time ()) {
m_lastPredictIndex = kInvalidNodeIndex;
auto distanceToLastEnemySq = m_lastEnemyOrigin.distanceSq (pev->origin);
if (distanceToLastEnemySq < cr::sqrf (1600.0f)) {
auto pathLength = 0;
auto nodeIndex = findAimingNode (m_lastEnemyOrigin, pathLength);
if (graph.exists (nodeIndex) && pathLength < cv_max_nodes_for_predict.int_ () && pev->origin.distanceSq (graph[nodeIndex].origin) > cr::sqrf (384.0f)) {
m_aimFlags |= AimFlags::PredictPath;
m_lastPredictIndex = nodeIndex;
}
}
if (seesEntity (m_lastEnemyOrigin, true)) {
m_aimFlags |= AimFlags::LastEnemy;
}
}
refreshEnemyPredict ();
// check for grenades depending on difficulty
if (rg.chance (cr::max (25, m_difficulty * 25))) {
@ -1818,7 +1874,7 @@ void Bot::filterTasks () {
}
// if half of the round is over, allow hunting
if (getCurrentTaskId () != Task::EscapeFromBomb && game.isNullEntity (m_enemy) && !m_isVIP && bots.getRoundMidTime () < game.time () && !hasHostage () && !m_isUsingGrenade && m_currentNodeIndex != graph.getNearest (m_lastEnemyOrigin) && m_personality != Personality::Careful && !cv_ignore_enemies.bool_ ()) {
if (getCurrentTaskId () != Task::EscapeFromBomb && game.isNullEntity (m_enemy) && !m_isVIP && bots.getRoundMidTime () < game.time () && !m_hasHostage && !m_isUsingGrenade && m_currentNodeIndex != graph.getNearest (m_lastEnemyOrigin) && m_personality != Personality::Careful && !cv_ignore_enemies.bool_ ()) {
float desireLevel = 4096.0f - ((1.0f - tempAgression) * m_lastEnemyOrigin.distance (pev->origin));
desireLevel = (100.0f * desireLevel) / 4096.0f;
@ -2056,13 +2112,13 @@ bool Bot::reactOnEnemy () {
}
int enemyIndex = graph.getNearest (m_enemy->v.origin);
auto lineDist = m_enemy->v.origin.distance2d (pev->origin);
auto pathDist = static_cast <float> (planner.dist (ownIndex, enemyIndex));
auto lineDist = m_enemy->v.origin.distance (pev->origin);
auto pathDist = planner.preciseDistance (ownIndex, enemyIndex);
if (pathDist - lineDist > (planner.hasRealPathDistance () ? 112.0f : 32.0f) || isOnLadder ()) {
if (pathDist - lineDist > 112.0f || isOnLadder ()) {
m_isEnemyReachable = false;
}
else if (vistab.visible (ownIndex, enemyIndex)) {
else {
m_isEnemyReachable = true;
}
m_enemyReachableTimer = game.time () + 1.0f;
@ -2088,7 +2144,7 @@ void Bot::checkRadioQueue () {
// don't allow bot listen you if bot is busy
if (getCurrentTaskId () == Task::DefuseBomb || getCurrentTaskId () == Task::PlantBomb || hasHostage () || m_hasC4) {
if (getCurrentTaskId () == Task::DefuseBomb || getCurrentTaskId () == Task::PlantBomb || m_hasHostage || m_hasC4) {
m_radioOrder = 0;
return;
}
@ -2631,32 +2687,56 @@ void Bot::updateAimDir () {
}
else if (flags & AimFlags::PredictPath) {
bool changePredictedEnemy = true;
bool predictFailed = false;
if (m_timeNextTracking < game.time () && m_trackingEdict == m_lastEnemy) {
changePredictedEnemy = false;
}
if (changePredictedEnemy) {
auto pathLength = 0;
auto aimNode = graph.exists (m_lastPredictIndex) ? m_lastPredictIndex : findAimingNode (m_lastEnemyOrigin, pathLength);
auto doFailPredict = [this] () {
m_aimFlags &= ~AimFlags::PredictPath;
m_trackingEdict = nullptr;
};
if (graph.exists (aimNode) && pathLength < cv_max_nodes_for_predict.int_ ()) {
m_lookAt = graph[aimNode].origin;
if (changePredictedEnemy) {
int pathLength = m_lastPredictLength;
int predictNode = m_lastPredictIndex;
if (predictNode != kInvalidNodeIndex) {
TraceResult tr;
game.testLine (getEyesPos (), graph[predictNode].origin, TraceIgnore::Everything, ent (), &tr);
if (tr.flFraction < 0.2f) {
pathLength = kInfiniteDistanceLong;
}
}
if (pathLength < cv_max_nodes_for_predict.int_ ()) {
m_lookAt = graph[predictNode].origin;
m_lookAtSafe = m_lookAt;
m_timeNextTracking = game.time () + 0.75f;
m_timeNextTracking = game.time () + 0.25f;
m_trackingEdict = m_lastEnemy;
}
else {
if (!m_lookAtSafe.empty ()) {
m_lookAt = m_lookAtSafe;
}
// feel free to fire if shootable
if (!usesSniper () && lastEnemyShootable ()) {
m_wantsToFire = true;
}
}
else {
doFailPredict ();
predictFailed = true;
}
}
else {
if (predictFailed) {
doFailPredict ();
}
else {
m_lookAt = m_lookAtSafe;
}
}
}
else if (flags & AimFlags::Camp) {
m_lookAt = m_lookAtSafe;
}
@ -2777,8 +2857,6 @@ void Bot::frame () {
if (m_slowFrameTimestamp > game.time ()) {
return;
}
m_numFriendsLeft = numFriendsNear (pev->origin, kInfiniteDistance);
m_numEnemiesLeft = numEnemiesNear (pev->origin, kInfiniteDistance);
if (bots.isBombPlanted () && m_team == Team::CT && m_notKilled) {
const Vector &bombPosition = graph.getBombOrigin ();
@ -2801,7 +2879,7 @@ void Bot::frame () {
// kick the bot if stay time is over, the quota maintain will add new bot for us later
if (cv_rotate_bots.bool_ () && m_stayTime < game.time ()) {
m_kickedByRotation = true; // kicked by roration, so not save bot name if save bot names is active
m_kickedByRotation = true; // kicked by rotation, so not save bot name if save bot names is active
kick ();
return;
@ -2816,6 +2894,12 @@ void Bot::frame () {
}
void Bot::update () {
// kick bot from server if requested
if (m_kickMeFromServer) {
kick ();
return;
}
pev->button = 0;
m_moveSpeed = 0.0f;
@ -2834,6 +2918,9 @@ void Bot::update () {
m_hasC4 = false;
}
}
else if (m_team == Team::CT && game.mapIs (MapFlags::HostageRescue)) {
m_hasHostage = hasHostage ();
}
// is bot movement enabled
bool botMovement = false;
@ -2869,7 +2956,7 @@ void Bot::update () {
}
checkMsgQueue ();
if (botMovement) {
if (!m_isStale && botMovement) {
logic (); // execute main code
}
else if (pev->maxspeed < 10.0f) {
@ -2928,7 +3015,7 @@ void Bot::normal_ () {
}
// bots rushing with knife, when have no enemy (thanks for idea to nicebot project)
if (cv_random_knife_attacks.bool_ () && usesKnife () && (game.isNullEntity (m_lastEnemy) || !util.isAlive (m_lastEnemy)) && game.isNullEntity (m_enemy) && m_knifeAttackTime < game.time () && !hasHostage () && !hasShield () && numFriendsNear (pev->origin, 96.0f) == 0) {
if (cv_random_knife_attacks.bool_ () && usesKnife () && (game.isNullEntity (m_lastEnemy) || !util.isAlive (m_lastEnemy)) && game.isNullEntity (m_enemy) && m_knifeAttackTime < game.time () && !m_hasHostage && !hasShield () && numFriendsNear (pev->origin, 96.0f) == 0) {
if (rg.chance (40)) {
pev->button |= IN_ATTACK;
}
@ -2970,7 +3057,7 @@ void Bot::normal_ () {
if ((m_pathFlags & NodeFlag::Camp) && !game.is (GameFlags::CSDM) && cv_camping_allowed.bool_ () && !isKnifeMode ()) {
// check if bot has got a primary weapon and hasn't camped before
if (hasPrimaryWeapon () && m_timeCamping + 10.0f < game.time () && !hasHostage ()) {
if (hasPrimaryWeapon () && m_timeCamping + 10.0f < game.time () && !m_hasHostage) {
bool campingAllowed = true;
// Check if it's not allowed for this team to camp here
@ -3016,7 +3103,7 @@ void Bot::normal_ () {
m_campDirection = 0;
// tell the world we're camping
if (rg.chance (40)) {
if (rg.chance (25)) {
pushRadioMessage (Radio::ImInPosition);
}
m_moveToGoal = false;
@ -3031,7 +3118,7 @@ void Bot::normal_ () {
// some goal waypoints are map dependant so check it out...
if (game.mapIs (MapFlags::HostageRescue)) {
// CT Bot has some hostages following?
if (m_team == Team::CT && hasHostage ()) {
if (m_team == Team::CT && m_hasHostage) {
// and reached a rescue point?
if (m_pathFlags & NodeFlag::Rescue) {
m_hostages.clear ();
@ -3127,7 +3214,7 @@ void Bot::normal_ () {
}
float shiftSpeed = getShiftSpeed ();
if ((!cr::fzero (m_moveSpeed) && m_moveSpeed > shiftSpeed) && (cv_walking_allowed.bool_ () && mp_footsteps.bool_ ()) && m_difficulty >= Difficulty::Normal && !(m_aimFlags & AimFlags::Enemy) && (m_heardSoundTime + 6.0f >= game.time () || (m_states & Sense::SuspectEnemy)) && numEnemiesNear (pev->origin, 768.0f) >= 1 && !isKnifeMode () && !bots.isBombPlanted ()) {
if (!m_isStuck && (!cr::fzero (m_moveSpeed) && m_moveSpeed > shiftSpeed) && (cv_walking_allowed.bool_ () && mp_footsteps.bool_ ()) && m_difficulty >= Difficulty::Normal && !(m_aimFlags & AimFlags::Enemy) && (m_heardSoundTime + 6.0f >= game.time () || (m_states & Sense::SuspectEnemy)) && numEnemiesNear (pev->origin, 768.0f) >= 1 && !isKnifeMode () && !bots.isBombPlanted ()) {
m_moveSpeed = shiftSpeed;
}
@ -5099,7 +5186,7 @@ void Bot::takeBlind (int alpha) {
void Bot::updatePracticeValue (int damage) {
// gets called each time a bot gets damaged by some enemy. tries to achieve a statistic about most/less dangerous
// waypoints for a destination goal used for pathfinding
// nodes for a destination goal used for pathfinding
if (graph.length () < 1 || graph.hasChanged () || m_chosenGoalIndex < 0 || m_prevGoalIndex < 0) {
return;
@ -5109,7 +5196,7 @@ void Bot::updatePracticeValue (int damage) {
// max goal value
constexpr int maxGoalValue = PracticeLimit::Goal;
// only rate goal waypoint if bot died because of the damage
// only rate goal node if bot died because of the damage
// FIXME: could be done a lot better, however this cares most about damage done by sniping or really deadly weapons
if (health - damage <= 0) {
practice.setValue (m_team, m_chosenGoalIndex, m_prevGoalIndex, cr::clamp (practice.getValue (m_team, m_chosenGoalIndex, m_prevGoalIndex) - health / 20, -maxGoalValue, maxGoalValue));
@ -5117,7 +5204,7 @@ void Bot::updatePracticeValue (int damage) {
}
void Bot::updatePracticeDamage (edict_t *attacker, int damage) {
// this function gets called each time a bot gets damaged by some enemy. sotores the damage (teamspecific) done by victim.
// this function gets called each time a bot gets damaged by some enemy. stores the damage (team-specific) done by victim.
if (!util.isPlayer (attacker)) {
return;
@ -5159,8 +5246,8 @@ void Bot::updatePracticeDamage (edict_t *attacker, int damage) {
// store away the damage done
int damageValue = cr::clamp (practice.getDamage (m_team, victimIndex, attackerIndex) + damage / updateDamage, 0, maxDamageValue);
if (damageValue > graph.getHighestDamageForTeam (m_team)) {
graph.setHighestDamageForTeam (m_team, damageValue);
if (damageValue > practice.getHighestDamageForTeam (m_team)) {
practice.setHighestDamageForTeam (m_team, damageValue);
}
practice.setDamage (m_team, victimIndex, attackerIndex, damageValue);
}

View file

@ -262,25 +262,25 @@ bool Bot::lookupEnemies () {
if (cv_attack_monsters.bool_ ()) {
// search the world for monsters...
for (const auto &intresting : bots.getIntrestingEntities ()) {
if (!util.isMonster (intresting)) {
for (const auto &interesting : bots.getInterestingEntities ()) {
if (!util.isMonster (interesting)) {
continue;
}
// check the engine PVS
if (!isEnemyInFrustum (intresting) || !game.checkVisibility (intresting, set)) {
if (!isEnemyInFrustum (interesting) || !game.checkVisibility (interesting, set)) {
continue;
}
// see if bot can see the monster...
if (seesEnemy (intresting)) {
if (seesEnemy (interesting)) {
// higher priority for big monsters
float scaleFactor = (1.0f / calculateScaleFactor (intresting));
float distance = intresting->v.origin.distanceSq (pev->origin) * scaleFactor;
float scaleFactor = (1.0f / calculateScaleFactor (interesting));
float distance = interesting->v.origin.distanceSq (pev->origin) * scaleFactor;
if (distance < nearestDistance) {
nearestDistance = distance;
newEnemy = intresting;
newEnemy = interesting;
}
}
}
@ -347,7 +347,7 @@ bool Bot::lookupEnemies () {
return true;
}
else {
if (m_seeEnemyTime + 3.0f < game.time () && (m_hasC4 || hasHostage () || !game.isNullEntity (m_targetEntity))) {
if (m_seeEnemyTime + 3.0f < game.time () && (m_hasC4 || m_hasHostage || !game.isNullEntity (m_targetEntity))) {
pushRadioMessage (Radio::EnemySpotted);
}
m_targetEntity = nullptr; // stop following when we see an enemy...
@ -516,6 +516,7 @@ Vector Bot::getBodyOffsetError (float distance) {
}
else if (m_enemyParts & Visibility::Body) {
spot = m_enemy->v.origin;
spot.z += 3.0f;
}
else if (m_enemyParts & Visibility::Other) {
spot = m_enemyOrigin;
@ -752,7 +753,7 @@ bool Bot::isPenetrableObstacle3 (const Vector &dest) {
bool Bot::needToPauseFiring (float distance) {
// returns true if bot needs to pause between firing to compensate for punchangle & weapon spread
if (usesSniper () || m_isUsingGrenade) {
if (usesSniper () || m_isUsingGrenade || ((m_states & Sense::SuspectEnemy) && distance < 400.0f)) {
return false;
}
@ -1949,7 +1950,7 @@ void Bot::checkGrenadesThrow () {
break;
}
for (const auto predict : predicted) {
for (const auto &predict : predicted) {
allowThrowing = true;
if (!graph.exists (predict)) {

View file

@ -1192,7 +1192,7 @@ int BotControl::menuCommands (int item) {
switch (item) {
case 1:
case 2:
if (util.findNearestPlayer (reinterpret_cast <void **> (&m_djump), m_ent, 600.0f, true, true, true, true, false) && !m_djump->m_hasC4 && !m_djump->hasHostage ()) {
if (util.findNearestPlayer (reinterpret_cast <void **> (&m_djump), m_ent, 600.0f, true, true, true, true, false) && !m_djump->m_hasC4 && !m_djump->m_hasHostage) {
if (item == 1) {
m_djump->startDoubleJump (m_ent);
}
@ -1765,7 +1765,7 @@ bool BotControl::executeCommands () {
for (auto &item : m_cmds) {
if (!hasSecondArg) {
cmd = item.name.split ("/")[0];
cmd = item.name.split ("/").first ();
}
if (!hasSecondArg || aliasMatch (item.name, strValue (2), cmd)) {
@ -1804,7 +1804,7 @@ bool BotControl::executeCommands () {
msg ("valid commands are: ");
for (auto &item : m_cmds) {
msg (" %s - %s", item.name.split ("/")[0], conf.translate (item.help));
msg (" %-14.11s - %s", item.name.split ("/").first (), String (conf.translate (item.help)).lowercase ());
}
return true;
}

View file

@ -9,6 +9,7 @@
ConVar cv_csdm_mode ("yb_csdm_mode", "0", "Enables or disables CSDM / FFA mode for bots.\nAllowed values: '0', '1', '2', '3'.\nIf '0', CSDM / FFA mode is auto-detected.\nIf '1', CSDM mode is enabled, but FFA is disabled.\nIf '2', CSDM and FFA mode is enabled.\nIf '3', CSDM and FFA mode is disabled.", true, 0.0f, 3.0f);
ConVar cv_breakable_health_limit ("yb_breakable_health_limit", "500.0", "Specifies the maximum health of breakable object, that bot will consider to destroy.", true, 1.0f, 3000.0);
ConVar cv_threadpool_workers ("yb_threadpool_workers", "-1", "Maximum number of threads bot will run to process some tasks. -1 means half of CPU cores used.", true, -1.0f, static_cast <float> (plat.hardwareConcurrency ()));
ConVar sv_skycolor_r ("sv_skycolor_r", nullptr, Var::GameRef);
ConVar sv_skycolor_g ("sv_skycolor_g", nullptr, Var::GameRef);
@ -53,6 +54,12 @@ void Game::precache () {
void Game::levelInitialize (edict_t *entities, int max) {
// this function precaches needed models and initialize class variables
// re-initialize bot's array
bots.destroy ();
// startup threaded worker
worker.startup (cv_threadpool_workers.int_ ());
m_spawnCount[Team::CT] = 0;
m_spawnCount[Team::Terrorist] = 0;
@ -83,9 +90,6 @@ void Game::levelInitialize (edict_t *entities, int max) {
// flush any print queue
ctrl.resetFlushTimestamp ();
// suspend any analyzer tasks
analyzer.suspend ();
// go thru the all entities on map, and do whatever we're want
for (int i = 0; i < max; ++i) {
auto ent = entities + i;
@ -162,6 +166,40 @@ void Game::levelInitialize (edict_t *entities, int max) {
m_halfSecondFrame = 0.0f;
}
void Game::levelShutdown () {
// stop thread pool
worker.shutdown ();
// save collected practice on shutdown
practice.save ();
// destroy global killer entity
bots.destroyKillerEntity ();
// ensure players are off on xash3d
if (game.is (GameFlags::Xash3D)) {
bots.kickEveryone (true, false);
}
// set state to unprecached
game.setUnprecached ();
// enable lightstyle animations on level change
illum.enableAnimation (true);
// send message on new map
util.setNeedForWelcome (false);
// clear local entity
game.setLocalEntity (nullptr);
// reset graph state
graph.reset ();
// suspend any analyzer tasks
analyzer.suspend ();
}
void Game::drawLine (edict_t *ent, const Vector &start, const Vector &end, int width, int noise, const Color &color, int brightness, int speed, int life, DrawLine type) {
// this function draws a arrow visible from the client side of the player whose player entity
// is pointed to by ent, from the vector location start to the vector location end,
@ -963,6 +1001,9 @@ void Game::slowFrame () {
// refresh bomb origin in case some plugin moved it out
graph.setBombOrigin ();
// update client pings
util.calculatePings ();
// update next update time
m_halfSecondFrame = nextUpdate * 0.25f + time ();
}
@ -978,9 +1019,6 @@ void Game::slowFrame () {
// check if we're need to autokill bots
bots.maintainAutoKill ();
// update client pings
util.calculatePings ();
// maintain leaders selection upon round start
bots.maintainLeaders ();

View file

@ -1196,6 +1196,7 @@ void BotGraph::showFileInfo () {
msg (" compressed_size: %dkB", m_graphHeader.compressed / 1024);
msg (" uncompressed_size: %dkB", m_graphHeader.uncompressed / 1024);
msg (" options: %d", m_graphHeader.options); // display as string ?
msg (" analyzed: %s", (m_graphHeader.options & StorageOption::Analyzed) ? "yes" : "no"); // display as string ?
msg ("");

View file

@ -94,11 +94,11 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int) {
plat.bzero (table, sizeof (gamefuncs_t));
if (!(game.is (GameFlags::Metamod))) {
auto api_GetEntityAPI = game.lib ().resolve <decltype (&GetEntityAPI)> (__FUNCTION__);
auto api_GetEntityAPI = game.lib ().resolve <decltype (&GetEntityAPI)> (__func__);
// pass other DLLs engine callbacks to function table...
if (!api_GetEntityAPI || api_GetEntityAPI (&dllapi, INTERFACE_VERSION) == 0) {
logger.fatal ("Could not resolve symbol \"%s\" in the game dll.", __FUNCTION__);
logger.fatal ("Could not resolve symbol \"%s\" in the game dll.", __func__);
}
dllfuncs.dllapi_table = &dllapi;
gpGamedllFuncs = &dllfuncs;
@ -332,29 +332,8 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int) {
// any case, when the new map will be booting, ServerActivate() will be called, so we'll do
// the loading of new bots and the new BSP data parsing there.
// save collected practice on shutdown
practice.save ();
// destroy global killer entity
bots.destroyKillerEntity ();
// set state to unprecached
game.setUnprecached ();
// enable lightstyle animations on level change
illum.enableAnimation (true);
// send message on new map
util.setNeedForWelcome (false);
// clear local entity
game.setLocalEntity (nullptr);
// reset graph state
graph.reset ();
// clear all the bots
bots.destroy ();
// notify level shutdown
game.levelShutdown ();
if (game.is (GameFlags::Metamod)) {
RETURN_META (MRES_IGNORED);
@ -397,8 +376,8 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int) {
// keep track of grenades on map
bots.updateActiveGrenade ();
// keep track of intresting entities
bots.updateIntrestingEntities ();
// keep track of interesting entities
bots.updateInterestingEntities ();
}
// keep bot number up to date
@ -527,7 +506,7 @@ CR_LINKAGE_C int GetEngineFunctions (enginefuncs_t *table, int *) {
}
table->pfnLightStyle = [] (int style, char *val) {
// ths function update lightstyle for the bots
// this function update lightstyle for the bots
illum.updateLight (style, val);
@ -572,7 +551,6 @@ CR_LINKAGE_C int GetEngineFunctions (enginefuncs_t *table, int *) {
table->pfnMessageBegin = [] (int msgDest, int msgType, const float *origin, edict_t *ed) {
// this function called each time a message is about to sent.
msgs.start (ed, msgType);
if (game.is (GameFlags::Metamod)) {
@ -801,11 +779,11 @@ CR_EXPORT int GetNewDLLFunctions (newgamefuncs_t *table, int *interfaceVersion)
plat.bzero (table, sizeof (newgamefuncs_t));
if (!(game.is (GameFlags::Metamod))) {
auto api_GetNewDLLFunctions = game.lib ().resolve <decltype (&GetNewDLLFunctions)> (__FUNCTION__);
auto api_GetNewDLLFunctions = game.lib ().resolve <decltype (&GetNewDLLFunctions)> (__func__);
// pass other DLLs engine callbacks to function table...
if (!api_GetNewDLLFunctions || api_GetNewDLLFunctions (&newapi, interfaceVersion) == 0) {
logger.error ("Could not resolve symbol \"%s\" in the game dll.", __FUNCTION__);
logger.error ("Could not resolve symbol \"%s\" in the game dll.", __func__);
}
dllfuncs.newapi_table = &newapi;
memcpy (table, &newapi, sizeof (newgamefuncs_t));
@ -817,6 +795,7 @@ CR_EXPORT int GetNewDLLFunctions (newgamefuncs_t *table, int *interfaceVersion)
if (bot->m_enemy == ent) {
bot->m_enemy = nullptr;
bot->m_lastEnemy = nullptr;
bot->markStale ();
}
}
@ -854,7 +833,6 @@ CR_LINKAGE_C int GetEngineFunctions_Post (enginefuncs_t *table, int *) {
RETURN_META_VALUE (MRES_IGNORED, 0);
};
return HLTrue;
}
@ -907,11 +885,16 @@ CR_EXPORT int Meta_Detach (PLUG_LOADTIME now, PL_UNLOAD_REASON reason) {
logger.error ("%s: plugin NOT detaching (can't unload plugin right now)", Plugin_info.name);
return HLFalse; // returning FALSE prevents metamod from unloading this plugin
}
bots.kickEveryone (true); // kick all bots off this server
// stop the worker
worker.shutdown ();
// kick all bots off this server
bots.kickEveryone (true);
// save collected practice on shutdown
practice.save ();
// disable hooks
util.disableSendTo ();
// make sure all stuff cleared
@ -963,10 +946,10 @@ DLL_GIVEFNPTRSTODLL GiveFnptrsToDll (enginefuncs_t *table, globalvars_t *glob) {
if (game.postload ()) {
return;
}
auto api_GiveFnptrsToDll = game.lib ().resolve <decltype (&GiveFnptrsToDll)> (__FUNCTION__);
auto api_GiveFnptrsToDll = game.lib ().resolve <decltype (&GiveFnptrsToDll)> (__func__);
if (!api_GiveFnptrsToDll) {
logger.fatal ("Could not resolve symbol \"%s\" in the game dll.", __FUNCTION__);
logger.fatal ("Could not resolve symbol \"%s\" in the game dll.", __func__);
}
GetEngineFunctions (table, nullptr);
@ -984,10 +967,10 @@ CR_EXPORT int Server_GetBlendingInterface (int version, struct sv_blending_inter
// of the body move, which bones, which hitboxes and how) between the server and the game DLL.
// some MODs can be using a different hitbox scheme than the standard one.
auto api_GetBlendingInterface = game.lib ().resolve <decltype (&Server_GetBlendingInterface)> (__FUNCTION__);
auto api_GetBlendingInterface = game.lib ().resolve <decltype (&Server_GetBlendingInterface)> (__func__);
if (!api_GetBlendingInterface) {
logger.error ("Could not resolve symbol \"%s\" in the game dll. Continuing...", __FUNCTION__);
logger.error ("Could not resolve symbol \"%s\" in the game dll. Continuing...", __func__);
return HLFalse;
}
return api_GetBlendingInterface (version, ppinterface, pstudio, rotationmatrix, bonetransform);

View file

@ -8,6 +8,7 @@
#include <yapb.h>
ConVar cv_autovacate ("yb_autovacate", "1", "Kick bots to automatically make room for human players.");
ConVar cv_autovacate_keep_slots ("yb_autovacate_keep_slots", "1", "How many slots autovacate feature should keep for human players", true, 1.0f, 8.0f);
ConVar cv_kick_after_player_connect ("yb_kick_after_player_connect", "1", "Kick the bot immediately when a human player joins the server (yb_autovacate must be enabled).");
ConVar cv_quota ("yb_quota", "9", "Specifies the number bots to be added to the game.", true, 0.0f, static_cast <float> (kGameMaxPlayers));
@ -405,10 +406,10 @@ void BotManager::maintainQuota () {
if (cv_autovacate.bool_ ()) {
if (cv_kick_after_player_connect.bool_ ()) {
desiredBotCount = cr::min <int> (desiredBotCount, maxClients - (totalHumansInGame + 1));
desiredBotCount = cr::min <int> (desiredBotCount, maxClients - (totalHumansInGame + cv_autovacate_keep_slots.int_ ()));
}
else {
desiredBotCount = cr::min <int> (desiredBotCount, maxClients - (humanPlayersInGame + 1));
desiredBotCount = cr::min <int> (desiredBotCount, maxClients - (humanPlayersInGame + cv_autovacate_keep_slots.int_ ()));
}
}
else {
@ -515,7 +516,7 @@ void BotManager::reset () {
m_timeBombPlanted = 0.0f;
m_bombSayStatus = BombPlantedSay::ChatSay | BombPlantedSay::Chatter;
m_intrestingEntities.clear ();
m_interestingEntities.clear ();
m_activeGrenades.clear ();
}
@ -569,7 +570,7 @@ void BotManager::serverFill (int selection, int personality, int difficulty, int
// this function fill server with bots, with specified team & personality
// always keep one slot
int maxClients = cv_autovacate.bool_ () ? game.maxClients () - 1 - (game.isDedicated () ? 0 : getHumansCount ()) : game.maxClients ();
int maxClients = cv_autovacate.bool_ () ? game.maxClients () - cv_autovacate_keep_slots.int_ () - (game.isDedicated () ? 0 : getHumansCount ()) : game.maxClients ();
if (getBotCount () >= maxClients - getHumansCount ()) {
return;
@ -942,6 +943,9 @@ void BotManager::balanceBotDifficulties () {
void BotManager::destroy () {
// this function free all bots slots (used on server shutdown)
for (auto &bot : m_bots) {
bot->markStale ();
}
m_bots.clear ();
}
@ -1078,7 +1082,10 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int skin) {
// init path walker
m_pathWalk.init (graph.getMaxRouteLength ());
// bot is not kicked by rorataion
// init async planner
m_planner = cr::makeUnique <AStarAlgo> (graph.length ());
// bot is not kicked by rotation
m_kickedByRotation = false;
// assign team and class
@ -1141,7 +1148,7 @@ int BotManager::getPlayerPriority (edict_t *ent) {
}
// give bots some priority
if (bot->m_hasC4 || bot->m_isVIP || bot->hasHostage () || bot->m_healthValue < ent->v.health) {
if (bot->m_hasC4 || bot->m_isVIP || bot->m_hasHostage || bot->m_healthValue < ent->v.health) {
return bot->entindex () + highPrio;
}
return bot->entindex ();
@ -1183,7 +1190,6 @@ void BotManager::erase (Bot *bot) {
m_bots.erase (index, 1); // remove from bots array
break;
}
}
void BotManager::handleDeath (edict_t *killer, edict_t *victim) {
@ -1246,6 +1252,7 @@ void BotManager::handleDeath (edict_t *killer, edict_t *victim) {
}
}
void Bot::newRound () {
// this function initializes a bot after creation & at the start of each round
@ -1276,7 +1283,6 @@ void Bot::newRound () {
for (auto &node : m_previousNodes) {
node = kInvalidNodeIndex;
}
m_navTimeset = game.time ();
m_team = game.getTeam (ent ());
@ -1407,6 +1413,7 @@ void Bot::newRound () {
m_inBombZone = false;
m_ignoreBuyDelay = false;
m_hasC4 = false;
m_hasHostage = false;
m_fallDownTime = 0.0f;
m_shieldCheckTime = 0.0f;
@ -1505,8 +1512,12 @@ void Bot::kick () {
}
void Bot::markStale () {
// switch chatter icon off
showChatterIcon (false, true);
// mark bot as leaving
m_isStale = true;
// clear the bot name
conf.clearUsedName (this);
@ -1723,13 +1734,13 @@ void BotManager::updateActiveGrenade () {
m_grenadeUpdateTime = game.time () + 0.25f;
}
void BotManager::updateIntrestingEntities () {
void BotManager::updateInterestingEntities () {
if (m_entityUpdateTime > game.time ()) {
return;
}
// clear previously stored entities
m_intrestingEntities.clear ();
m_interestingEntities.clear ();
// search the map for any type of grenade
game.searchEntities (nullptr, kInfiniteDistance, [&] (edict_t *e) {
@ -1737,26 +1748,26 @@ void BotManager::updateIntrestingEntities () {
// search for grenades, weaponboxes, weapons, items and armoury entities
if (strncmp ("weaponbox", classname, 9) == 0 || strncmp ("grenade", classname, 7) == 0 || util.isItem (e) || strncmp ("armoury", classname, 7) == 0) {
m_intrestingEntities.push (e);
m_interestingEntities.push (e);
}
// pickup some csdm stuff if we're running csdm
if (game.mapIs (MapFlags::HostageRescue) && strncmp ("hostage", classname, 7) == 0) {
m_intrestingEntities.push (e);
m_interestingEntities.push (e);
}
// add buttons
if (game.mapIs (MapFlags::HasButtons) && strncmp ("func_button", classname, 11) == 0) {
m_intrestingEntities.push (e);
m_interestingEntities.push (e);
}
// pickup some csdm stuff if we're running csdm
if (game.is (GameFlags::CSDM) && strncmp ("csdm", classname, 4) == 0) {
m_intrestingEntities.push (e);
m_interestingEntities.push (e);
}
if (cv_attack_monsters.bool_ () && util.isMonster (e)) {
m_intrestingEntities.push (e);
m_interestingEntities.push (e);
}
// continue iteration
@ -1914,3 +1925,35 @@ void BotManager::setBombPlanted (bool isPlanted) {
}
m_bombPlanted = isPlanted;
}
void BotThreadWorker::shutdown () {
game.print ("Shutting down bot thread worker.");
m_botWorker.shutdown ();
}
void BotThreadWorker::startup (int workers) {
size_t count = m_botWorker.threadCount ();
if (count > 0) {
logger.error ("Tried to start thread pool with existing %d threads in pool.", count);
return;
}
int requetedThreadCount = workers;
int hardwareConcurrency = plat.hardwareConcurrency ();
if (requetedThreadCount == -1) {
requetedThreadCount = hardwareConcurrency / 4;
if (requetedThreadCount == 0) {
requetedThreadCount = 1;
}
}
requetedThreadCount = cr::clamp (requetedThreadCount, 1, hardwareConcurrency);
// notify user
game.print ("Starting up bot thread worker with %d threads.", requetedThreadCount);
// start up the worker
m_botWorker.startup (static_cast <int> (requetedThreadCount));
}

View file

@ -54,7 +54,7 @@ int Bot::findBestGoal () {
tactic = 3;
return findGoalPost (tactic, defensiveNodes, offensiveNodes);
}
else if (m_team == Team::CT && hasHostage ()) {
else if (m_team == Team::CT && m_hasHostage) {
tactic = 4;
return findGoalPost (tactic, defensiveNodes, offensiveNodes);
}
@ -406,7 +406,7 @@ void Bot::doPlayerAvoidance (const Vector &normal) {
if (game.isNullEntity (m_hindrance)) {
return;
}
const float interval = m_frameInterval * 7.25f;
const float interval = m_frameInterval * (pev->velocity.lengthSq2d () > 0.0f ? 9.0f : 2.0f);
// use our movement angles, try to predict where we should be next frame
Vector right, forward;
@ -538,7 +538,7 @@ void Bot::checkTerrain (float movedDistance, const Vector &dirNormal) {
dirLeft = true;
}
const auto &testDir = m_moveSpeed > 0.0f ? forward : -forward;
constexpr float blockDistance = 56.0f;
constexpr float blockDistance = 40.0f;
// now check which side is blocked
src = pev->origin + right * blockDistance;
@ -813,11 +813,8 @@ bool Bot::updateNavigation () {
// check if we need to find a node...
if (m_currentNodeIndex == kInvalidNodeIndex) {
findValidNode ();
setPathOrigin ();
// if graph node radius non zero vary origin a bit depending on the body angles
if (m_path->radius > 0.0f) {
m_pathOrigin += Vector (pev->angles.x, cr::wrapAngle (pev->angles.y + rg.get (-90.0f, 90.0f)), 0.0f).forward () * rg.get (0.0f, m_path->radius);
}
m_navTimeset = game.time ();
}
m_destOrigin = m_pathOrigin + pev->view_ofs;
@ -841,7 +838,8 @@ bool Bot::updateNavigation () {
m_desiredVelocity = nullptr;
}
}
else if (!isKnifeMode () && m_switchedToKnifeDuringJump && isOnFloor ()) {
}
else if (m_jumpDistance > 0.0f && !isKnifeMode () && m_switchedToKnifeDuringJump && isOnFloor ()) {
selectBestWeapon ();
// if jump distance was big enough, cooldown a little
@ -851,7 +849,6 @@ bool Bot::updateNavigation () {
m_jumpDistance = 0.0f;
m_switchedToKnifeDuringJump = false;
}
}
if (m_pathFlags & NodeFlag::Ladder) {
if (!m_pathWalk.empty ()) {
@ -1495,7 +1492,7 @@ bool Bot::findNextBestNode () {
}
// cts with hostages should not pick nodes with no hostage flag
if (game.mapIs (MapFlags::HostageRescue) && m_team == Team::CT && (graph[path.number].flags & NodeFlag::NoHostage) && hasHostage ()) {
if (game.mapIs (MapFlags::HostageRescue) && m_team == Team::CT && (graph[path.number].flags & NodeFlag::NoHostage) && m_hasHostage) {
continue;
}
@ -1566,9 +1563,7 @@ bool Bot::findNextBestNode () {
if (selected == kInvalidNodeIndex) {
selected = findNearestNode ();
}
m_nodeHistory.push (selected);
changeNodeIndex (selected);
return true;
}
@ -1665,14 +1660,14 @@ int Bot::findNearestNode () {
for (const auto &i : bucket) {
const auto &path = graph[i];
if (!graph.exists (path.number) || !graph.exists (m_currentNodeIndex) || path.number == m_currentNodeIndex) {
if (!graph.exists (path.number)) {
continue;
}
const float distance = path.origin.distanceSq (pev->origin);
if (distance < minimumDistance) {
// if bot doing navigation, make sure node really visible and reacheable
if (vistab.visible (m_currentNodeIndex, path.number) && isReachableNode (path.number)) {
if (isReachableNode (path.number)) {
index = path.number;
minimumDistance = distance;
}
@ -1680,7 +1675,7 @@ int Bot::findNearestNode () {
}
// try to search ANYTHING that can be reachaed
if (index == kInvalidNodeIndex) {
if (!graph.exists (index)) {
minimumDistance = cr::sqrf (kMaxDistance);
const auto &nearestNodes = graph.getNarestInRadius (kMaxDistance, pev->origin);
@ -1776,18 +1771,18 @@ int Bot::findDefendNode (const Vector &origin) {
TraceResult tr {};
int nodeIndex[kMaxNodeLinks] {};
int minDistance[kMaxNodeLinks] {};
float minDistance[kMaxNodeLinks] {};
for (int i = 0; i < kMaxNodeLinks; ++i) {
nodeIndex[i] = kInvalidNodeIndex;
minDistance[i] = 128;
minDistance[i] = 128.0f;
}
int posIndex = graph.getNearest (origin);
int srcIndex = m_currentNodeIndex;
// max search distance
const int kMaxDistance = cr::clamp (148 * bots.getBotCount (), 256, 1024);
const auto kMaxDistance = cr::clamp (148.0f * bots.getBotCount (), 256.0f, 1024.0f);
// some of points not found, return random one
if (srcIndex == kInvalidNodeIndex || posIndex == kInvalidNodeIndex) {
@ -1802,7 +1797,7 @@ int Bot::findDefendNode (const Vector &origin) {
}
// use the 'real' pathfinding distances
int distance = planner.dist (srcIndex, path.number);
auto distance = planner.dist (srcIndex, path.number);
// skip wayponts too far
if (distance > kMaxDistance) {
@ -1860,13 +1855,13 @@ int Bot::findDefendNode (const Vector &origin) {
int practiceDamage = practice.getDamage (m_team, nodeIndex[i], nodeIndex[i]);
practiceDamage = (practiceDamage * 100) / practice.getHighestDamageForTeam (m_team);
minDistance[i] = (practiceDamage * 100) / 8192;
minDistance[i] += practiceDamage;
minDistance[i] = static_cast <float> ((practiceDamage * 100) / 8192);
minDistance[i] += static_cast <float> (practiceDamage);
}
}
bool sorting = false;
// sort results nodes for farest distance
// sort resulting nodes for farest distance
do {
sorting = false;
@ -1925,11 +1920,11 @@ int Bot::findCoverNode (float maxDistance) {
IntArray enemies;
int nodeIndex[kMaxNodeLinks] {};
int minDistance[kMaxNodeLinks] {};
float minDistance[kMaxNodeLinks] {};
for (int i = 0; i < kMaxNodeLinks; ++i) {
nodeIndex[i] = kInvalidNodeIndex;
minDistance[i] = static_cast <int> (maxDistance);
minDistance[i] = maxDistance;
}
if (enemyIndex == kInvalidNodeIndex) {
@ -1967,8 +1962,8 @@ int Bot::findCoverNode (float maxDistance) {
}
// use the 'real' pathfinding distances
int distance = planner.dist (srcIndex, path.number);
int enemyDistance = planner.dist (enemyIndex, path.number);
float distance = planner.dist (srcIndex, path.number);
float enemyDistance = planner.dist (enemyIndex, path.number);
if (distance >= enemyDistance) {
continue;
@ -2014,8 +2009,8 @@ int Bot::findCoverNode (float maxDistance) {
int practiceDamage = practice.getDamage (m_team, nodeIndex[i], nodeIndex[i]);
practiceDamage = (practiceDamage * 100) / practice.getHighestDamageForTeam (m_team);
minDistance[i] = (practiceDamage * 100) / 8192;
minDistance[i] += practiceDamage;
minDistance[i] = static_cast <float> ((practiceDamage * 100) / 8192);
minDistance[i] += static_cast <float> (practiceDamage);
}
}
bool sorting = false;
@ -2105,7 +2100,6 @@ bool Bot::advanceMovement () {
if (m_pathWalk.empty ()) {
return false;
}
TraceResult tr {};
m_pathWalk.shift (); // advance in list
m_currentTravelFlags = 0; // reset travel flags (jumping etc)
@ -2136,7 +2130,7 @@ bool Bot::advanceMovement () {
Task taskID = getCurrentTaskId ();
// only if we in normal task and bomb is not planted
if (taskID == Task::Normal && bots.getRoundMidTime () + 5.0f < game.time () && m_timeCamping + 5.0f < game.time () && !bots.isBombPlanted () && m_personality != Personality::Rusher && !m_hasC4 && !m_isVIP && m_loosedBombNodeIndex == kInvalidNodeIndex && !hasHostage ()) {
if (taskID == Task::Normal && bots.getRoundMidTime () + 5.0f < game.time () && m_timeCamping + 5.0f < game.time () && !bots.isBombPlanted () && m_personality != Personality::Rusher && !m_hasC4 && !m_isVIP && m_loosedBombNodeIndex == kInvalidNodeIndex && !m_hasHostage) {
m_campButtons = 0;
const int nextIndex = m_pathWalk.next ();
@ -2241,22 +2235,55 @@ bool Bot::advanceMovement () {
changeNodeIndex (destIndex);
}
}
setPathOrigin ();
m_navTimeset = game.time ();
// if wayzone radius non zero vary origin a bit depending on the body angles
return true;
}
void Bot::setPathOrigin () {
constexpr int kMaxAlternatives = 5;
// if node radius non zero vary origin a bit depending on the body angles
if (m_path->radius > 0.0f) {
int nearestIndex = kInvalidNodeIndex;
if (!m_pathWalk.empty () && m_pathWalk.hasNext ()) {
Vector orgs[kMaxAlternatives] {};
for (int i = 0; i < kMaxAlternatives; ++i) {
orgs[i] = m_pathOrigin + Vector (rg.get (-m_path->radius, m_path->radius), rg.get (-m_path->radius, m_path->radius), 0.0f);
}
float closest = kInfiniteDistance;
for (int i = 0; i < kMaxAlternatives; ++i) {
float distance = pev->origin.distanceSq (orgs[i]);
if (distance < closest) {
nearestIndex = i;
closest = distance;
}
}
// set the origin if found alternative
if (nearestIndex != kInvalidNodeIndex) {
m_pathOrigin = orgs[nearestIndex];
}
}
if (nearestIndex == kInvalidNodeIndex) {
m_pathOrigin += Vector (pev->angles.x, cr::wrapAngle (pev->angles.y + rg.get (-90.0f, 90.0f)), 0.0f).forward () * rg.get (0.0f, m_path->radius);
}
}
if (isOnLadder ()) {
TraceResult tr {};
game.testLine (Vector (pev->origin.x, pev->origin.y, pev->absmin.z), m_pathOrigin, TraceIgnore::Everything, ent (), &tr);
if (tr.flFraction < 1.0f) {
m_pathOrigin = m_pathOrigin + (pev->origin - m_pathOrigin) * 0.5f + Vector (0.0f, 0.0f, 32.0f);
}
}
m_navTimeset = game.time ();
return true;
}
bool Bot::cantMoveForward (const Vector &normal, TraceResult *tr) {
@ -3158,67 +3185,89 @@ void Bot::findShortestPath (int srcIndex, int destIndex) {
m_chosenGoalIndex = srcIndex;
m_goalValue = 0.0f;
bool success = planner.find (srcIndex, destIndex, [&] (int index) {
bool success = planner.find (srcIndex, destIndex, [this] (int index) {
m_pathWalk.add (index);
return true;
});
if (success) {
if (!success) {
m_prevGoalIndex = kInvalidNodeIndex;
getTask ()->data = kInvalidNodeIndex;
}
}
void Bot::findPath (int srcIndex, int destIndex, FindPath pathType /*= FindPath::Fast */) {
// this function finds a path from srcIndex to destIndex
void Bot::syncFindPath (int srcIndex, int destIndex, FindPath pathType) {
// this function finds a path from srcIndex to destIndex;
if (!m_pathFindLock.tryLock ()) {
return; // allow only single instance of syncFindPath per-bot
}
ScopedUnlock <Mutex> unlock (m_pathFindLock);
if (!graph.exists (srcIndex)) {
logger.error ("%s source path index not valid (%d).", __FUNCTION__, srcIndex);
srcIndex = changeNodeIndex (graph.getNearestNoBuckets (pev->origin, 256.0f));
if (!graph.exists (srcIndex)) {
printf ("%s source path index not valid (%d).", __func__, srcIndex);
return;
}
else if (!graph.exists (destIndex)) {
logger.error ("%s destination path index not valid (%d).", __FUNCTION__, destIndex);
}
else if (!graph.exists (destIndex) || destIndex == srcIndex) {
destIndex = graph.getNearestNoBuckets (pev->origin, kInfiniteDistance, NodeFlag::Goal);
if (!graph.exists (destIndex) || srcIndex == destIndex) {
destIndex = rg.get (0, graph.length () - 1);
if (!graph.exists (destIndex)) {
printf ("%s dest path index not valid (%d).", __func__, destIndex);
return;
}
auto pathPlanner = planner.getAStar ();
}
}
// do not process if src points to dst
if (srcIndex == destIndex) {
printf ("%s source path is same as dest (%d).", __func__, destIndex);
return;
}
clearSearchNodes ();
// get correct calculation for heuristic
if (pathType == FindPath::Optimal) {
if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) {
pathPlanner->setH (Heuristic::hfunctionPathDistWithHostage);
pathPlanner->setG (Heuristic::gfunctionKillsDistCTWithHostage);
if (game.mapIs (MapFlags::HostageRescue) && m_hasHostage) {
m_planner->setH (Heuristic::hfunctionPathDistWithHostage);
m_planner->setG (Heuristic::gfunctionKillsDistCTWithHostage);
}
else {
pathPlanner->setH (Heuristic::hfunctionPathDist);
pathPlanner->setG (Heuristic::gfunctionKillsDist);
m_planner->setH (Heuristic::hfunctionPathDist);
m_planner->setG (Heuristic::gfunctionKillsDist);
}
}
else if (pathType == FindPath::Safe) {
if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) {
pathPlanner->setH (Heuristic::hfunctionNone);
pathPlanner->setG (Heuristic::gfunctionKillsCTWithHostage);
if (game.mapIs (MapFlags::HostageRescue) && m_hasHostage) {
m_planner->setH (Heuristic::hfunctionNone);
m_planner->setG (Heuristic::gfunctionKillsCTWithHostage);
}
else {
pathPlanner->setH (Heuristic::hfunctionNone);
pathPlanner->setG (Heuristic::gfunctionKills);
m_planner->setH (Heuristic::hfunctionNone);
m_planner->setG (Heuristic::gfunctionKills);
}
}
else {
if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) {
pathPlanner->setH (Heuristic::hfunctionPathDistWithHostage);
pathPlanner->setG (Heuristic::gfunctionPathDistWithHostage);
if (game.mapIs (MapFlags::HostageRescue) && m_hasHostage) {
m_planner->setH (Heuristic::hfunctionPathDistWithHostage);
m_planner->setG (Heuristic::gfunctionPathDistWithHostage);
}
else {
pathPlanner->setH (Heuristic::hfunctionPathDist);
pathPlanner->setG (Heuristic::gfunctionPathDist);
m_planner->setH (Heuristic::hfunctionPathDist);
m_planner->setG (Heuristic::gfunctionPathDist);
}
}
clearSearchNodes ();
m_chosenGoalIndex = srcIndex;
m_goalValue = 0.0f;
auto result = pathPlanner->find (m_team, srcIndex, destIndex, [&] (int index) {
auto result = m_planner->find (m_team, srcIndex, destIndex, [this] (int index) {
m_pathWalk.add (index);
return true;
});
@ -3230,10 +3279,10 @@ void Bot::findPath (int srcIndex, int destIndex, FindPath pathType /*= FindPath:
break;
case AStarResult::InternalError:
// bot should not roam when this occurs
kick (); // kick the bot off...
m_kickMeFromServer = true; // bot should be kicked within main thread, not here
logger.error ("A* Search for bot \"%s\" failed with internal pathfinder error. Seems to be graph is broken. Bot removed (re-added).", pev->netname.chars ());
// bot should not roam when this occurs
printf ("A* Search for bot \"%s\" failed with internal pathfinder error. Seems to be graph is broken. Bot removed (re-added).", pev->netname.chars ());
break;
case AStarResult::Failed:
@ -3241,8 +3290,14 @@ void Bot::findPath (int srcIndex, int destIndex, FindPath pathType /*= FindPath:
findShortestPath (srcIndex, destIndex); // A* found no path, try floyd pathfinder instead
if (cv_debug.bool_ ()) {
logger.error ("A* Search for bot \"%s\" has failed. Falling back to shortest-path algorithm. Seems to be graph is broken.", pev->netname.chars ());
printf ("A* Search for bot \"%s\" has failed. Falling back to shortest-path algorithm. Seems to be graph is broken.", pev->netname.chars ());
}
break;
}
}
void Bot::findPath (int srcIndex, int destIndex, FindPath pathType /*= FindPath::Fast */) {
worker.enqueue ([this, srcIndex, destIndex, pathType] () {
syncFindPath (srcIndex, destIndex, pathType);
});
}

View file

@ -8,20 +8,20 @@
#include <yapb.h>
ConVar cv_path_heuristic_mode ("yb_path_heuristic_mode", "3", "Selects the heuristic function mode. For debug purposes only.", true, 0.0f, 4.0f);
ConVar cv_path_floyd_memory_limit ("yb_path_floyd_memory_limit", "3", "Limit maximum floyd-warshall memory (megabytes). Use Dijkstra if memory exceeds.", true, 0.0, 12.0f);
ConVar cv_path_floyd_memory_limit ("yb_path_floyd_memory_limit", "6", "Limit maximum floyd-warshall memory (megabytes). Use Dijkstra if memory exceeds.", true, 0.0, 32.0f);
ConVar cv_path_dijkstra_simple_distance ("yb_path_dijkstra_simple_distance", "1", "Use simple distance path calculation instead of running full Dijkstra path cycle. Used only when Floyd matrices unavailable due to memory limit.");
ConVar cv_path_astar_post_smooth ("yb_path_astar_post_smooth", "1", "Enables post-smoothing for A*. Reduces zig-zags on paths at cost of some CPU cycles.");
ConVar cv_path_astar_post_smooth ("yb_path_astar_post_smooth", "0", "Enables post-smoothing for A*. Reduces zig-zags on paths at cost of some CPU cycles.");
float Heuristic::gfunctionKillsDist (int team, int currentIndex, int parentIndex) {
if (parentIndex == kInvalidNodeIndex) {
return 0.0f;
}
auto cost = static_cast <float> (practice.getDamage (team, currentIndex, currentIndex) + practice.getHighestDamageForTeam (team));
auto cost = practice.plannerGetDamage (team, currentIndex, currentIndex, true);
const auto &current = graph[currentIndex];
for (const auto &neighbour : current.links) {
if (neighbour.index != kInvalidNodeIndex) {
cost += static_cast <float> (practice.getDamage (team, neighbour.index, neighbour.index));
cost += practice.plannerGetDamage (team, neighbour.index, neighbour.index, false);
}
}
@ -44,12 +44,12 @@ float Heuristic::gfunctionKillsDistCTWithHostage (int team, int currentIndex, in
}
float Heuristic::gfunctionKills (int team, int currentIndex, int) {
auto cost = static_cast <float> (practice.getDamage (team, currentIndex, currentIndex));
auto cost = practice.plannerGetDamage (team, currentIndex, currentIndex, false);
const auto &current = graph[currentIndex];
for (const auto &neighbour : current.links) {
if (neighbour.index != kInvalidNodeIndex) {
cost += static_cast <float> (practice.getDamage (team, neighbour.index, neighbour.index));
cost += practice.plannerGetDamage (team, neighbour.index, neighbour.index, false);
}
}
@ -176,11 +176,17 @@ bool AStarAlgo::cantSkipNode (const int a, const int b) {
const auto &ag = graph[a];
const auto &bg = graph[b];
const bool notVisible = !vistab.visible (ag.number, bg.number);
const bool hasZeroRadius = cr::fzero (ag.radius) || cr::fzero (ag.radius);
if (hasZeroRadius) {
return true;
}
const bool notVisible = !vistab.visible (ag.number, bg.number) || !vistab.visible (bg.number, ag.number);
if (notVisible) {
return true;
}
const bool tooHigh = cr::abs (ag.origin.z - bg.origin.z) > 17.0f;
if (tooHigh) {
@ -191,9 +197,12 @@ bool AStarAlgo::cantSkipNode (const int a, const int b) {
if (tooNarrow) {
return true;
}
const bool tooFar = ag.origin.distanceSq (bg.origin) > cr::sqrf (300.0f);
const float distance = ag.origin.distanceSq (bg.origin);
if (tooFar) {
const bool tooFar = distance > cr::sqrf (400.0f);
const bool tooClose = distance < cr::sqrtf (40.0f);
if (tooFar || tooClose) {
return true;
}
bool hasJumps = false;
@ -208,22 +217,23 @@ bool AStarAlgo::cantSkipNode (const int a, const int b) {
}
void AStarAlgo::postSmooth (NodeAdderFn onAddedNode) {
m_smoothedPath.clear ();
int index = 0;
m_smoothedPath.emplace (m_constructedPath.first ());
m_smoothedPath.push (m_constructedPath.first ());
for (size_t i = 1; i < m_constructedPath.length () - 1; ++i) {
if (cantSkipNode (m_smoothedPath[index], m_constructedPath[i + 1])) {
++index;
m_smoothedPath.emplace (m_constructedPath[i]);
m_smoothedPath.push (m_constructedPath[i]);
}
}
m_smoothedPath.emplace (m_constructedPath.last ());
m_smoothedPath.push (m_constructedPath.last ());
// give nodes back to bot
for (const auto &spn : m_smoothedPath) {
onAddedNode (spn);
}
m_constructedPath.clear ();
m_smoothedPath.clear ();
}
AStarResult AStarAlgo::find (int botTeam, int srcIndex, int destIndex, NodeAdderFn onAddedNode) {
@ -242,7 +252,10 @@ AStarResult AStarAlgo::find (int botTeam, int srcIndex, int destIndex, NodeAdder
m_routeQue.clear ();
m_routeQue.emplace (srcIndex, srcRoute->g);
bool postSmoothPath = cv_path_astar_post_smooth.bool_ ();
bool postSmoothPath = cv_path_astar_post_smooth.bool_ () && vistab.isReady ();
// always clear constructed path
m_constructedPath.clear ();
while (!m_routeQue.empty ()) {
// remove the first node from the open list
@ -258,7 +271,7 @@ AStarResult AStarAlgo::find (int botTeam, int srcIndex, int destIndex, NodeAdder
// build the complete path
do {
if (postSmoothPath) {
m_constructedPath.emplace (currentIndex);
m_constructedPath.push (currentIndex);
}
else {
onAddedNode (currentIndex);
@ -312,6 +325,12 @@ void FloydWarshallAlgo::rebuild () {
m_length = graph.length ();
m_matrix.resize (cr::sqrf (m_length));
worker.enqueue ([this] () {
syncRebuild ();
});
}
void FloydWarshallAlgo::syncRebuild () {
auto matrix = m_matrix.data ();
// re-initialize matrix every load
@ -379,18 +398,19 @@ bool FloydWarshallAlgo::find (int srcIndex, int destIndex, NodeAdderFn onAddedNo
srcIndex = (m_matrix.data () + (srcIndex * m_length) + destIndex)->index;
if (srcIndex < 0) {
// only fill path distance on full path
if (pathDistance != nullptr) {
*pathDistance = dist (srcIndex, destIndex);
}
return true;
return false;
}
if (!onAddedNode (srcIndex)) {
return true;
}
}
return false;
// only fill path distance on full path
if (pathDistance != nullptr) {
*pathDistance = dist (srcIndex, destIndex);
}
return true;
}
void DijkstraAlgo::init (const int length) {
@ -400,15 +420,13 @@ void DijkstraAlgo::init (const int length) {
m_parent.resize (length);
}
void DijkstraAlgo::resetState () {
bool DijkstraAlgo::find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance) {
MutexScopedLock lock (m_cs);
m_queue.clear ();
m_parent.fill (kInvalidNodeIndex);
m_distance.fill (kInfiniteDistanceLong);
}
bool DijkstraAlgo::find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance) {
resetState ();
m_queue.emplace (0, srcIndex);
m_distance[srcIndex] = 0;
@ -473,7 +491,6 @@ int DijkstraAlgo::dist (int srcIndex, int destIndex) {
PathPlanner::PathPlanner () {
m_dijkstra = cr::makeUnique <DijkstraAlgo> ();
m_floyd = cr::makeUnique <FloydWarshallAlgo> ();
m_astar = cr::makeUnique <AStarAlgo> ();
}
void PathPlanner::init () {
@ -487,7 +504,6 @@ void PathPlanner::init () {
m_memoryLimitHit = true;
}
m_dijkstra->init (length);
m_astar->init (length);
// load (re-create) floyds, if we're not hitting memory limits
if (!m_memoryLimitHit) {
@ -503,7 +519,6 @@ bool PathPlanner::find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, in
if (!graph.exists (srcIndex) || !graph.exists (destIndex)) {
return false;
}
// limit hit, use dijkstra
if (m_memoryLimitHit) {
return m_dijkstra->find (srcIndex, destIndex, onAddedNode, pathDistance);
@ -511,7 +526,7 @@ bool PathPlanner::find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, in
return m_floyd->find (srcIndex, destIndex, onAddedNode, pathDistance);;
}
int PathPlanner::dist (int srcIndex, int destIndex) {
float PathPlanner::dist (int srcIndex, int destIndex) {
if (!graph.exists (srcIndex) || !graph.exists (destIndex)) {
return kInfiniteDistanceLong;
}
@ -523,9 +538,17 @@ int PathPlanner::dist (int srcIndex, int destIndex) {
// limit hit, use dijkstra
if (m_memoryLimitHit) {
if (cv_path_dijkstra_simple_distance.bool_ ()) {
return static_cast <int> (graph[srcIndex].origin.distance2d (graph[destIndex].origin));
return graph[srcIndex].origin.distance2d (graph[destIndex].origin);
}
return m_dijkstra->dist (srcIndex, destIndex);
return static_cast <float> (m_dijkstra->dist (srcIndex, destIndex));
}
return m_floyd->dist (srcIndex, destIndex);
return static_cast <float> (m_floyd->dist (srcIndex, destIndex));
}
float PathPlanner::preciseDistance (int srcIndex, int destIndex) {
// limit hit, use dijkstra
if (m_memoryLimitHit) {
return static_cast <float> (m_dijkstra->dist (srcIndex, destIndex));
}
return static_cast <float> (m_floyd->dist (srcIndex, destIndex));
}

View file

@ -18,6 +18,7 @@ void BotPractice::setIndex (int32_t team, int32_t start, int32_t goal, int32_t v
if (team != Team::Terrorist && team != Team::CT) {
return;
}
MutexScopedLock lock (m_damageUpdateLock);
// reliability check
if (!graph.exists (start) || !graph.exists (goal) || !graph.exists (value)) {
@ -37,6 +38,7 @@ void BotPractice::setValue (int32_t team, int32_t start, int32_t goal, int32_t v
if (team != Team::Terrorist && team != Team::CT) {
return;
}
MutexScopedLock lock (m_damageUpdateLock);
// reliability check
if (!graph.exists (start) || !graph.exists (goal)) {
@ -46,7 +48,7 @@ void BotPractice::setValue (int32_t team, int32_t start, int32_t goal, int32_t v
}
int32_t BotPractice::getDamage (int32_t team, int32_t start, int32_t goal) {
if (!exists (team, start, goal)) {
if (!vistab.isReady () || !exists (team, start, goal)) {
return 0;
}
return m_data[{start, goal, team}].damage;
@ -56,6 +58,7 @@ void BotPractice::setDamage (int32_t team, int32_t start, int32_t goal, int32_t
if (team != Team::Terrorist && team != Team::CT) {
return;
}
MutexScopedLock lock (m_damageUpdateLock);
// reliability check
if (!graph.exists (start) || !graph.exists (goal)) {
@ -64,7 +67,26 @@ void BotPractice::setDamage (int32_t team, int32_t start, int32_t goal, int32_t
m_data[{start, goal, team}].damage = static_cast <int16_t> (value);
}
float BotPractice::plannerGetDamage (int32_t team, int32_t start, int32_t goal, bool addTeamHighestDamage) {
if (!m_damageUpdateLock.tryLock ()) {
return 1.0f;
}
ScopedUnlock <Mutex> unlock (m_damageUpdateLock);
auto damage = static_cast <float> (getDamage (team, start, goal));
if (addTeamHighestDamage) {
damage += getHighestDamageForTeam <float> (team);
}
return damage;
}
void BotPractice::update () {
worker.enqueue ([this] () {
syncUpdate ();
});
}
void BotPractice::syncUpdate () {
// this function called after each end of the round to update knowledge about most dangerous nodes for each team.
auto graphLength = graph.length ();
@ -98,9 +120,12 @@ void BotPractice::update () {
if (maxDamage > PracticeLimit::Damage) {
adjustValues = true;
}
if (graph.exists (bestIndex)) {
setIndex (team, i, i, bestIndex);
}
}
}
constexpr auto kFullDamageVal = static_cast <int32_t> (PracticeLimit::Damage);
constexpr auto kHalfDamageVal = static_cast <int32_t> (PracticeLimit::Damage / 2);
@ -117,6 +142,7 @@ void BotPractice::update () {
}
}
}
MutexScopedLock lock (m_damageUpdateLock);
for (int team = Team::Terrorist; team < kGameTeamNum; ++team) {
m_teamHighestDamage[team] = cr::clamp (m_teamHighestDamage[team] - kHalfDamageVal, 1, kFullDamageVal);

View file

@ -405,9 +405,16 @@ int BotSupport::getPingBitmask (edict_t *ent, int loss, int ping) {
}
void BotSupport::calculatePings () {
worker.enqueue ([this] () {
syncCalculatePings ();
});
}
void BotSupport::syncCalculatePings () {
if (!game.is (GameFlags::HasFakePings) || cv_show_latency.int_ () != 2) {
return;
}
MutexScopedLock lock (m_cs);
Twin <int, int> average { 0, 0 };
int numHumans = 0;
@ -444,7 +451,7 @@ void BotSupport::calculatePings () {
}
auto bot = bots[client.ent];
// we're only intrested in bots here
// we're only interested in bots here
if (!bot) {
continue;
}

View file

@ -32,6 +32,7 @@
<ClInclude Include="..\ext\crlib\crlib\random.h" />
<ClInclude Include="..\ext\crlib\crlib\simd.h" />
<ClInclude Include="..\ext\crlib\crlib\string.h" />
<ClInclude Include="..\ext\crlib\crlib\thread.h" />
<ClInclude Include="..\ext\crlib\crlib\twin.h" />
<ClInclude Include="..\ext\crlib\crlib\ulz.h" />
<ClInclude Include="..\ext\crlib\crlib\uniqueptr.h" />
@ -42,6 +43,7 @@
<ClInclude Include="..\ext\hlsdk\metamod.h" />
<ClInclude Include="..\ext\hlsdk\meta_api.h" />
<ClInclude Include="..\ext\hlsdk\model.h" />
<ClInclude Include="..\ext\hlsdk\physint.h" />
<ClInclude Include="..\ext\hlsdk\progdefs.h" />
<ClInclude Include="..\ext\hlsdk\util.h" />
<ClInclude Include="..\inc\analyze.h" />

View file

@ -171,6 +171,12 @@
<ClInclude Include="..\inc\vistable.h">
<Filter>inc</Filter>
</ClInclude>
<ClInclude Include="..\ext\crlib\crlib\thread.h">
<Filter>inc\ext\crlib</Filter>
</ClInclude>
<ClInclude Include="..\ext\hlsdk\physint.h">
<Filter>inc\ext\hlsdk</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\src\botlib.cpp">
@ -221,15 +227,15 @@
<ClCompile Include="..\src\analyze.cpp">
<Filter>src</Filter>
</ClCompile>
<ClCompile Include="..\src\planner.cpp">
<Filter>src</Filter>
</ClCompile>
<ClCompile Include="..\src\practice.cpp">
<Filter>src</Filter>
</ClCompile>
<ClCompile Include="..\src\storage.cpp">
<Filter>src</Filter>
</ClCompile>
<ClCompile Include="..\src\planner.cpp">
<Filter>src</Filter>
</ClCompile>
<ClCompile Include="..\src\vistable.cpp">
<Filter>src</Filter>
</ClCompile>