backport: nodes flooder (analyzer) from cs-ebot

analyze: allow to disable goal marking
analyze: add cvars descriptions and bounds
nav: added optional post path smoothing for astar algorithm
nav: now bots will use Dijkstra algo instead of floyd-warshall if memory usage too high (controlled via yb_path_floyd_memory_limit cvar) (fixes #434)
nav: vistable are now calculated every frame to prevent game-freeze during loading the game (fixes #434)
graph: pracrice reworked to hash table so memory footprint is as low as possible (at cost 5-10% performance loss on practice) (fixes #434)
control: bots commands now is case-insensitive
bot: major refactoring of bot's code
nav: issue warnings about path fail only with debug
practice: check for visibility when updating danger index
analyzer: suspend any analyzing on change level
control: add kickall_ct/kickall_t
nav: increase blocked distance in stuck check
This commit is contained in:
jeefo 2023-05-02 09:42:43 +03:00 committed by GitHub
commit e7712a551a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 3114 additions and 1722 deletions

@ -1 +1 @@
Subproject commit 6d716494737f740ebea58cfc6e6b6b30039cf7e9 Subproject commit 4d370ec500675ad23e9507831bbbbd6bb166ce3a

View file

@ -27,10 +27,16 @@ typedef unsigned char byte;
#include <crlib/vector.h> #include <crlib/vector.h>
#include <crlib/string.h> #include <crlib/string.h>
#if defined (CR_ARCH_X64)
using estring_t = int32_t;
#else
using estring_t = uint32_t;
#endif
// idea from regamedll // idea from regamedll
class string_t final : public cr::DenyCopying { class string_t final : public cr::DenyCopying {
private: private:
int offset; estring_t offset;
public: public:
explicit string_t () : offset (0) explicit string_t () : offset (0)
@ -42,10 +48,10 @@ public:
~string_t () = default; ~string_t () = default;
public: public:
const char *chars (size_t shift = 0) const; const char *chars (estring_t shift = 0) const;
public: public:
operator int () const { operator estring_t () const {
return offset; return offset;
} }
@ -90,7 +96,9 @@ extern enginefuncs_t engfuncs;
extern gamefuncs_t dllapi; extern gamefuncs_t dllapi;
// Use this instead of ALLOC_STRING on constant strings // Use this instead of ALLOC_STRING on constant strings
#define STRING(offset) (const char *)(globals->pStringBase + (int)offset) static inline const char *STRING (estring_t offset) {
return globals->pStringBase + offset;
}
// form fwgs-hlsdk // form fwgs-hlsdk
#if defined (CR_ARCH_X64) #if defined (CR_ARCH_X64)
@ -103,12 +111,13 @@ static inline int MAKE_STRING (const char *val) {
return static_cast <int> (ptrdiff); return static_cast <int> (ptrdiff);
} }
#else #else
#define MAKE_STRING(str) ((uint64_t)(str) - (uint64_t)(STRING(0))) static inline estring_t MAKE_STRING (const char *str) {
return static_cast <estring_t> (reinterpret_cast <uint64_t> (str) - reinterpret_cast <uint64_t> (globals->pStringBase));
}
#endif #endif
inline const char *string_t::chars (size_t shift) const { inline const char *string_t::chars (estring_t shift) const {
auto result = STRING (offset); auto result = STRING (offset);
return cr::strings.isEmpty (result) ? &cr::kNullChar : (result + shift); return cr::strings.isEmpty (result) ? &cr::kNullChar : (result + shift);
} }

View file

@ -20,25 +20,6 @@ extern globalvars_t *globals;
extern enginefuncs_t engfuncs; extern enginefuncs_t engfuncs;
extern gamefuncs_t dllapi; extern gamefuncs_t dllapi;
// Use this instead of ALLOC_STRING on constant strings
#define STRING(offset) (const char *)(globals->pStringBase + (int)offset)
// form fwgs-hlsdk
#if defined (CR_ARCH_X64)
static inline int MAKE_STRING (const char *val) {
long long ptrdiff = val - STRING (0);
if (ptrdiff > INT_MAX || ptrdiff < INT_MIN) {
return engfuncs.pfnAllocString (val);
}
return static_cast <int> (ptrdiff);
}
#else
#define MAKE_STRING(str) ((uint64_t)(str) - (uint64_t)(STRING(0)))
#endif
#define ENGINE_STR(str) (const_cast <char *> (STRING (engfuncs.pfnAllocString (str))))
// Dot products for view cone checking // Dot products for view cone checking
#define VIEW_FIELD_FULL (float)-1.0 // +-180 degrees #define VIEW_FIELD_FULL (float)-1.0 // +-180 degrees
#define VIEW_FIELD_WIDE (float)-0.7 // +-135 degrees 0.1 // +-85 degrees, used for full FOV checks #define VIEW_FIELD_WIDE (float)-0.7 // +-135 degrees 0.1 // +-85 degrees, used for full FOV checks

79
inc/analyze.h Normal file
View file

@ -0,0 +1,79 @@
//
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
// Copyright © 2004-2023 YaPB Project <yapb@jeefo.net>.
//
// SPDX-License-Identifier: MIT
//
#pragma once
// next code is based on cs-ebot implemntation, devised by EfeDursun125
class GraphAnalyze : public Singleton <GraphAnalyze> {
public:
GraphAnalyze () = default;
~GraphAnalyze () = default;
private:
float m_updateInterval {}; // time to update analyzer
bool m_basicsCreated {}; // basics waypoints were created?
bool m_isCrouch {}; // is node to be created as crouch ?
bool m_isAnalyzing {}; // we're in analyzing ?
bool m_isAnalyzed {}; // current waypoint is analyzed
bool m_expandedNodes[kMaxNodes] {}; // all nodes expanded ?
bool m_optimizedNodes[kMaxNodes] {}; // all nodes expanded ?
public:
// start analyzation process
void start ();
// update analyzation process
void update ();
// suspend aanalyzing
void suspend ();
private:
// flood with nodes
void flood (const Vector &pos, const Vector &next, float range);
// set update interval (keeps game from freezing)
void setUpdateInterval ();
// mark waypoints as goals
void markGoals ();
// terminate analyzation and save data
void finish ();
// optimize nodes a little
void optimize ();
// cleanup bad nodes
void cleanup ();
public:
// node should be created as crouch
bool isCrouch () const {
return m_isCrouch;
}
// is currently anaylyzing ?
bool isAnalyzing () const {
return m_isAnalyzing;
}
// current graph is analyzed graph ?
bool isAnalyzed () const {
return m_isAnalyzed;
}
// mark as optimized
void markOptimized (const int index) {
m_optimizedNodes[index] = true;
}
};
// explose global
CR_EXPOSE_GLOBAL_SINGLETON (GraphAnalyze, analyzer);

View file

@ -216,7 +216,7 @@ public:
m_args.clear (); m_args.clear ();
for (int i = 0; i < engfuncs.pfnCmd_Argc (); ++i) { for (int i = 0; i < engfuncs.pfnCmd_Argc (); ++i) {
m_args.emplace (engfuncs.pfnCmd_Argv (i)); m_args.emplace (String (engfuncs.pfnCmd_Argv (i)).lowercase ());
} }
} }
@ -274,5 +274,14 @@ template <typename ...Args> inline void BotControl::msg (const char *fmt, Args &
} }
} }
// graph heloer for sending message to correct channel
template <typename ...Args> inline void BotGraph::msg (const char *fmt, Args &&...args) {
if (m_silenceMessages) {
return; // no messages while analyzing (too much spam)
}
BotControl::instance ().msg (strings.format (conf.translate (fmt), cr::forward <Args> (args)...));
}
// explose global // explose global
CR_EXPOSE_GLOBAL_SINGLETON (BotControl, ctrl); CR_EXPOSE_GLOBAL_SINGLETON (BotControl, ctrl);

View file

@ -71,6 +71,7 @@ struct ConVarReg {
String info; String info;
String init; String init;
String regval; String regval;
String name;
class ConVar *self; class ConVar *self;
float initial, min, max; float initial, min, max;
bool missing; bool missing;
@ -420,6 +421,19 @@ public:
Game::instance ().addNewCvar (name, initval, info, bounded, min, max, type, regMissing, regVal, this); Game::instance ().addNewCvar (name, initval, info, bounded, min, max, type, regMissing, regVal, this);
} }
template <typename U> constexpr U get () const {
if constexpr (cr::is_same <U, float>::value) {
return ptr->value;
}
else if constexpr (cr::is_same <U, bool>::value) {
return ptr->value > 0.0f;
}
else if constexpr (cr::is_same <U, int>::value) {
return static_cast <int> (ptr->value);
}
assert ("!Inavlid type requeted.");
}
bool bool_ () const { bool bool_ () const {
return ptr->value > 0.0f; return ptr->value > 0.0f;
} }
@ -447,6 +461,9 @@ public:
void set (const char *val) { void set (const char *val) {
engfuncs.pfnCvar_DirectSet (ptr, val); engfuncs.pfnCvar_DirectSet (ptr, val);
} }
// revet cvar to default value
void revert ();
}; };
class MessageWriter final { class MessageWriter final {

View file

@ -7,6 +7,9 @@
#pragma once #pragma once
constexpr int kMaxNodes = 4096; // max nodes per graph
constexpr int kMaxNodeLinks = 8; // max links for single node
// defines for nodes flags field (32 bits are available) // defines for nodes flags field (32 bits are available)
CR_DECLARE_SCOPED_ENUM (NodeFlag, CR_DECLARE_SCOPED_ENUM (NodeFlag,
Lift = cr::bit (1), // wait for lift to be down before approaching this node Lift = cr::bit (1), // wait for lift to be down before approaching this node
@ -43,13 +46,6 @@ CR_DECLARE_SCOPED_ENUM (PathConnection,
Bidirectional Bidirectional
) )
// a* route state
CR_DECLARE_SCOPED_ENUM (RouteState,
Open = 0,
Closed,
New
)
// node edit states // node edit states
CR_DECLARE_SCOPED_ENUM (GraphEdit, CR_DECLARE_SCOPED_ENUM (GraphEdit,
On = cr::bit (1), On = cr::bit (1),
@ -57,26 +53,6 @@ CR_DECLARE_SCOPED_ENUM (GraphEdit,
Auto = cr::bit (3) Auto = cr::bit (3)
) )
// storage header options
CR_DECLARE_SCOPED_ENUM (StorageOption,
Practice = cr::bit (0), // this is practice (experience) file
Matrix = cr::bit (1), // this is floyd warshal path & distance matrix
Vistable = cr::bit (2), // this is vistable data
Graph = cr::bit (3), // this is a node graph data
Official = cr::bit (4), // this is additional flag for graph indicates graph are official
Recovered = cr::bit (5), // this is additional flag indicates graph converted from podbot and was bad
Exten = cr::bit (6) // this is additional flag indicates that there's extension info
)
// storage header versions
CR_DECLARE_SCOPED_ENUM (StorageVersion,
Graph = 2,
Practice = 1,
Vistable = 2,
Matrix = 1,
Podbot = 7
)
// lift usage states // lift usage states
CR_DECLARE_SCOPED_ENUM (LiftState, CR_DECLARE_SCOPED_ENUM (LiftState,
None = 0, None = 0,
@ -103,29 +79,13 @@ CR_DECLARE_SCOPED_ENUM (NodeAddFlag,
Goal = 100 Goal = 100
) )
// a* route CR_DECLARE_SCOPED_ENUM (NotifySound,
struct Route { Done = 0,
float g, f; Change = 1,
int parent; Added = 2
RouteState state; )
};
// general stprage header information structure #include <vistable.h>
struct StorageHeader {
int32_t magic;
int32_t version;
int32_t options;
int32_t length;
int32_t compressed;
int32_t uncompressed;
};
// extension header for graph information
struct ExtenHeader {
char author[32]; // original author of graph
int32_t mapSize; // bsp size for checksumming map consistency
char modified[32]; // by whom modified
};
// general waypoint header information structure // general waypoint header information structure
struct PODGraphHeader { struct PODGraphHeader {
@ -136,19 +96,6 @@ struct PODGraphHeader {
char author[32]; char author[32];
}; };
// floyd-warshall matrices
struct Matrix {
int16_t dist;
int16_t index;
};
// experience data hold in memory while playing
struct Practice {
int16_t damage[kGameTeamNum];
int16_t index[kGameTeamNum];
int16_t value[kGameTeamNum];
};
// defines linked waypoints // defines linked waypoints
struct PathLink { struct PathLink {
Vector velocity; Vector velocity;
@ -157,11 +104,6 @@ struct PathLink {
int16_t index; int16_t index;
}; };
// defines visibility count
struct PathVis {
uint16_t stand, crouch;
};
// define graph path structure for yapb // define graph path structure for yapb
struct Path { struct Path {
int32_t number, flags; int32_t number, flags;
@ -183,74 +125,21 @@ struct PODPath {
PathVis vis; PathVis vis;
}; };
// this structure links nodes returned from pathfinder // general stprage header information structure
class PathWalk final : public DenyCopying { struct StorageHeader {
private: int32_t magic;
size_t m_cursor {}; int32_t version;
size_t m_length {}; int32_t options;
int32_t length;
int32_t compressed;
int32_t uncompressed;
};
UniquePtr <int32_t[]> m_path {}; // extension header for graph information
struct ExtenHeader {
public: char author[32]; // original author of graph
explicit PathWalk () = default; int32_t mapSize; // bsp size for checksumming map consistency
~PathWalk () = default; char modified[32]; // by whom modified
public:
int32_t &next () {
return at (1);
}
int32_t &first () {
return at (0);
}
int32_t &last () {
return at (length () - 1);
}
int32_t &at (size_t index) {
return m_path[m_cursor + index];
}
void shift () {
++m_cursor;
}
void reverse () {
for (size_t i = 0; i < m_length / 2; ++i) {
cr::swap (m_path[i], m_path[m_length - 1 - i]);
}
}
size_t length () const {
if (m_cursor >= m_length) {
return 0;
}
return m_length - m_cursor;
}
bool hasNext () const {
return length () > m_cursor;
}
bool empty () const {
return !length ();
}
void add (int32_t node) {
m_path[m_length++] = node;
}
void clear () {
m_cursor = 0;
m_length = 0;
m_path[0] = 0;
}
void init (size_t length) {
m_path = cr::makeUnique <int32_t []> (length);
}
}; };
// graph operation class // graph operation class
@ -260,7 +149,6 @@ public:
private: private:
int m_editFlags {}; int m_editFlags {};
int m_loadAttempts {};
int m_cacheNodeIndex {}; int m_cacheNodeIndex {};
int m_lastJumpNode {}; int m_lastJumpNode {};
int m_findWPIndex {}; int m_findWPIndex {};
@ -277,8 +165,8 @@ private:
bool m_endJumpPoint {}; bool m_endJumpPoint {};
bool m_jumpLearnNode {}; bool m_jumpLearnNode {};
bool m_hasChanged {}; bool m_hasChanged {};
bool m_needsVisRebuild {};
bool m_narrowChecked {}; bool m_narrowChecked {};
bool m_silenceMessages {};
Vector m_learnVelocity {}; Vector m_learnVelocity {};
Vector m_learnPosition {}; Vector m_learnPosition {};
@ -293,11 +181,8 @@ private:
IntArray m_rescuePoints {}; IntArray m_rescuePoints {};
IntArray m_visitedGoals {}; IntArray m_visitedGoals {};
SmallArray <Matrix> m_matrix {}; public:
SmallArray <Practice> m_practice {};
SmallArray <Path> m_paths {}; SmallArray <Path> m_paths {};
SmallArray <uint8_t> m_vistable {};
HashMap <int32_t, Array <int32_t>, EmptyHash <int32_t>> m_hashTable; HashMap <int32_t, Array <int32_t>, EmptyHash <int32_t>> m_hashTable;
String m_graphAuthor {}; String m_graphAuthor {};
@ -315,13 +200,10 @@ public:
public: public:
int getFacingIndex (); int getFacingIndex ();
int getFarest (const Vector &origin, float maxDistance = 32.0); int getFarest (const Vector &origin, float maxDistance = 32.0);
int getForAnalyzer (const Vector &origin, float maxDistance);
int getNearest (const Vector &origin, float minDistance = kInfiniteDistance, int flags = -1); int getNearest (const Vector &origin, float minDistance = kInfiniteDistance, int flags = -1);
int getNearestNoBuckets (const Vector &origin, float minDistance = kInfiniteDistance, int flags = -1); int getNearestNoBuckets (const Vector &origin, float minDistance = kInfiniteDistance, int flags = -1);
int getEditorNearest (); int getEditorNearest ();
int getDangerIndex (int team, int start, int goal);
int getDangerValue (int team, int start, int goal);
int getDangerDamage (int team, int start, int goal);
int getPathDist (int srcIndex, int destIndex);
int clearConnections (int index); int clearConnections (int index);
int getBspSize (); int getBspSize ();
int locateBucket (const Vector &pos); int locateBucket (const Vector &pos);
@ -329,30 +211,23 @@ public:
float calculateTravelTime (float maxSpeed, const Vector &src, const Vector &origin); float calculateTravelTime (float maxSpeed, const Vector &src, const Vector &origin);
bool convertOldFormat (); bool convertOldFormat ();
bool isVisible (int srcIndex, int destIndex);
bool isStandVisible (int srcIndex, int destIndex);
bool isDuckVisible (int srcIndex, int destIndex);
bool isConnected (int a, int b); bool isConnected (int a, int b);
bool isConnected (int index); bool isConnected (int index);
bool isNodeReacheableEx (const Vector &src, const Vector &destination, const float maxHeight);
bool isNodeReacheable (const Vector &src, const Vector &destination); bool isNodeReacheable (const Vector &src, const Vector &destination);
bool isNodeReacheableWithJump (const Vector &src, const Vector &destination);
bool checkNodes (bool teleportPlayer); bool checkNodes (bool teleportPlayer);
bool loadPathMatrix ();
bool isVisited (int index); bool isVisited (int index);
bool isAnalyzed () const;
bool saveGraphData (); bool saveGraphData ();
bool loadGraphData (); bool loadGraphData ();
bool canDownload (); bool canDownload ();
template <typename U> bool saveStorage (StringRef name, StorageOption options, StorageVersion version, const SmallArray <U> &data, ExtenHeader *exten);
template <typename U> bool loadStorage (StringRef name, StorageOption options, StorageVersion version, SmallArray <U> &data, ExtenHeader *exten, int32_t *outOptions);
template <typename ...Args> bool raiseLoadingError (bool isGraph, bool isDebug, MemFile &file, const char *fmt, Args &&...args);
void saveOldFormat (); void saveOldFormat ();
void reset (); void reset ();
void frame (); void frame ();
void loadPractice (); void populateNodes ();
void loadVisibility ();
void initNodesTypes ();
void initLightLevels (); void initLightLevels ();
void initNarrowPlaces (); void initNarrowPlaces ();
void addPath (int addIndex, int pathIndex, float distance); void addPath (int addIndex, int pathIndex, float distance);
@ -360,16 +235,11 @@ public:
void erase (int target); void erase (int target);
void toggleFlags (int toggleFlag); void toggleFlags (int toggleFlag);
void setRadius (int index, float radius); void setRadius (int index, float radius);
void rebuildVisibility ();
void pathCreate (char dir); void pathCreate (char dir);
void erasePath (); void erasePath ();
void cachePoint (int index); void cachePoint (int index);
void calculatePathRadius (int index); void calculatePathRadius (int index);
void savePractice ();
void saveVisibility ();
void addBasic (); void addBasic ();
void eraseFromDisk ();
void savePathMatrix ();
void setSearchIndex (int index); void setSearchIndex (int index);
void startLearnJump (); void startLearnJump ();
void setVisited (int index); void setVisited (int index);
@ -378,16 +248,14 @@ public:
void addToBucket (const Vector &pos, int index); void addToBucket (const Vector &pos, int index);
void eraseFromBucket (const Vector &pos, int index); void eraseFromBucket (const Vector &pos, int index);
void setBombOrigin (bool reset = false, const Vector &pos = nullptr); void setBombOrigin (bool reset = false, const Vector &pos = nullptr);
void updateGlobalPractice ();
void unassignPath (int from, int to); void unassignPath (int from, int to);
void setDangerValue (int team, int start, int goal, int value);
void setDangerDamage (int team, int start, int goal, int value);
void convertFromPOD (Path &path, const PODPath &pod); void convertFromPOD (Path &path, const PODPath &pod);
void convertToPOD (const Path &path, PODPath &pod); void convertToPOD (const Path &path, PODPath &pod);
void convertCampDirection (Path &path); void convertCampDirection (Path &path);
void setAutoPathDistance (const float distance); void setAutoPathDistance (const float distance);
void showStats (); void showStats ();
void showFileInfo (); void showFileInfo ();
void emitNotify (int32_t sound);
IntArray getNarestInRadius (float radius, const Vector &origin, int maxCount = -1); IntArray getNarestInRadius (float radius, const Vector &origin, int maxCount = -1);
const IntArray &getNodesInBucket (const Vector &pos); const IntArray &getNodesInBucket (const Vector &pos);
@ -463,6 +331,25 @@ public:
return m_editor; return m_editor;
} }
// slicence all graph messages or not
void setMessageSilence (bool enable) {
m_silenceMessages = enable;
}
// set exten header from binary storage
void setExtenHeader (ExtenHeader *hdr) {
memcpy (&m_extenHeader, hdr, sizeof (ExtenHeader));
}
// set graph header from binary storage
void setGraphHeader (StorageHeader *hdr) {
memcpy (&m_graphHeader, hdr, sizeof (StorageHeader));
}
public:
// graph heloer for sending message to correct channel
template <typename ...Args> void msg (const char *fmt, Args &&...args);
public: public:
Path *begin () { Path *begin () {
return m_paths.begin (); return m_paths.begin ();
@ -481,29 +368,8 @@ public:
} }
}; };
// we're need `bots`
#include <manager.h> #include <manager.h>
#include <practice.h>
// helper for reporting load errors
template <typename ...Args> bool BotGraph::raiseLoadingError (bool isGraph, bool isDebug, MemFile &file, const char *fmt, Args &&...args) {
auto result = strings.format (fmt, cr::forward <Args> (args)...);
// display error only for graph file
if (isGraph || isDebug) {
logger.error (result);
}
// if graph reset paths
if (isGraph) {
bots.kickEveryone (true);
m_graphAuthor = result;
m_paths.clear ();
}
file.close ();
return false;
}
// explose global // explose global
CR_EXPOSE_GLOBAL_SINGLETON (BotGraph, graph); CR_EXPOSE_GLOBAL_SINGLETON (BotGraph, graph);

277
inc/planner.h Normal file
View file

@ -0,0 +1,277 @@
//
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
// Copyright © 2004-2023 YaPB Project <yapb@jeefo.net>.
//
// SPDX-License-Identifier: MIT
//
#pragma once
const float kInfiniteHeuristic = 65535.0f; // max out heuristic value
// a* route state
CR_DECLARE_SCOPED_ENUM (RouteState,
Open = 0,
Closed,
New
)
// a * find path result
CR_DECLARE_SCOPED_ENUM (AStarResult,
Success = 0,
Failed,
InternalError,
)
// node added
using NodeAdderFn = Lambda <bool (int)>;
// route twin node
template <typename HT> struct RouteTwin final {
public:
int32_t index {};
HT heuristic {};
constexpr RouteTwin () = default;
~RouteTwin () = default;
public:
constexpr RouteTwin (const int32_t &ri, const HT &rh) : index (ri), heuristic (rh) {}
public:
constexpr bool operator < (const RouteTwin &rhs) const {
return heuristic < rhs.heuristic;
}
constexpr bool operator > (const RouteTwin &rhs) const {
return heuristic > rhs.heuristic;
}
};
// bot heuristic functions for astar planner
class Heuristic final {
public:
using Func = Lambda <float (int, int, int)>;
public:
// least kills and number of nodes to goal for a team
static float gfunctionKillsDist (int team, int currentIndex, int parentIndex);;
// least kills and number of nodes to goal for a team (when with hostage)
static float gfunctionKillsDistCTWithHostage (int team, int currentIndex, int parentIndex);;
// least kills to goal for a team
static float gfunctionKills (int team, int currentIndex, int);;
// least kills to goal for a team (when with hostage)
static auto gfunctionKillsCTWithHostage (int team, int currentIndex, int parentIndex) -> float;;
// least distance for a team
static float gfunctionPathDist (int, int currentIndex, int parentIndex);;
// least distance for a team (when with hostage)
static float gfunctionPathDistWithHostage (int, int currentIndex, int parentIndex);;
public:
// square distance heuristic
static float hfunctionPathDist (int index, int, int goalIndex);;
// square distance heuristic with hostages
static float hfunctionPathDistWithHostage (int index, int, int goalIndex);;
// none heuristic
static float hfunctionNone (int index, int, int goalIndex);;
};
// A* algorithm for bots
class AStarAlgo final {
public:
using HeuristicFn = Heuristic::Func;
public:
struct Route {
float g {}, f {};
int parent { kInvalidNodeIndex };
RouteState state { RouteState::New };
};
private:
BinaryHeap <RouteTwin <float>> m_routeQue {};
Array <Route> m_routes {};
HeuristicFn m_hcalc;
HeuristicFn m_gcalc;
int m_length {};
Array <int> m_constructedPath;
Array <int> m_smoothedPath;
private:
// cleares the currently built route
void clearRoute ();
// can the node can be skipped?
bool cantSkipNode (const int a, const int b);
// do a post-smoothing after a* finished constructing path
void postSmooth (NodeAdderFn onAddedNode);
public:
AStarAlgo () = default;
~AStarAlgo () = default;
public:
// do the pathfinding
AStarResult find (int botTeam, int srcIndex, int destIndex, NodeAdderFn onAddedNode);
public:
// initialize astar with valid path length
void init (const int length) {
m_length = length;
clearRoute ();
m_constructedPath.reserve (getMaxLength ());
m_smoothedPath.reserve (getMaxLength ());
}
// set the g heuristic
void setG (HeuristicFn fn) {
m_gcalc = fn;
}
// set the h heuristic
void setH (HeuristicFn fn) {
m_hcalc = fn;
}
// get route max length, route length should not be larger than half of map nodes
size_t getMaxLength () const {
return m_length / 2;
}
};
// floyd-warshall shortest path algorithm
class FloydWarshallAlgo final {
private:
int m_length {};
public:
// floyd-warshall matrices
struct Matrix {
int16_t index { kInvalidNodeIndex };
int16_t dist { SHRT_MAX };
public:
Matrix () = default;
~Matrix () = default;
public:
Matrix (const int index, const int dist) : index (static_cast <int16_t> (index)), dist (static_cast <int16_t> (dist)) {}
};
private:
SmallArray <Matrix> m_matrix {};
public:
FloydWarshallAlgo () = default;
~FloydWarshallAlgo () = default;
private:
// create floyd matrics
void rebuild ();
public:
// load matrices from disk
bool load ();
// flush matrices to disk, so we will not rebuild them on load same map
void save ();
// do the pathfinding
bool find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance = nullptr);
public:
// distance between two nodes with pathfinder
int dist (int srcIndex, int destIndex) {
return static_cast <int> ((m_matrix.data () + (srcIndex * m_length) + destIndex)->dist);
}
};
// dijkstra shortest path algorithm
class DijkstraAlgo final {
private:
using Route = Twin <int, int>;
private:
Array <int> m_distance {};
Array <int> m_parent {};
BinaryHeap <Route> m_queue {};
int m_length {};
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);
// 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);
};
// the bot path planner
class PathPlanner : public Singleton <PathPlanner> {
private:
UniquePtr <DijkstraAlgo> m_dijkstra;
UniquePtr <FloydWarshallAlgo> m_floyd;
UniquePtr <AStarAlgo > m_astar;
bool m_memoryLimitHit {};
public:
PathPlanner ();
~PathPlanner () = default;
public:
// initialize all planners
void init ();
// has real path distance (instead of distance2d) ?
bool hasRealPathDistance () const;
public:
// get the dijkstra algo
decltype (auto) getDijkstra () {
return m_dijkstra.get ();
}
// get the floyd algo
decltype (auto) getFloydWarshall () {
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);
};
CR_EXPOSE_GLOBAL_SINGLETON (PathPlanner, planner);

121
inc/practice.h Normal file
View file

@ -0,0 +1,121 @@
//
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
// Copyright © 2004-2023 YaPB Project <yapb@jeefo.net>.
//
// SPDX-License-Identifier: MIT
//
#pragma once
// limits for storing practice data
CR_DECLARE_SCOPED_ENUM_TYPE (PracticeLimit, int32_t,
Goal = 2040,
Damage = 2040
);
// storage for from, to, team
class DangerStorage final {
protected:
uint16_t data[3] {};
public:
constexpr DangerStorage () = default;
public:
constexpr DangerStorage (const int32_t &a, const int32_t &b, const int32_t &c) :
data { static_cast <uint16_t> (a), static_cast <uint16_t> (b), static_cast <uint16_t> (c) } {}
public:
constexpr bool operator == (const DangerStorage &rhs) const {
return rhs.data[2] == data[2] && rhs.data[1] == data[1] && rhs.data[0] == data[0];
}
constexpr bool operator != (const DangerStorage &rhs) const {
return !operator == (rhs);
}
public:
// fnv1a for 3d vector hash
constexpr uint32_t hash () const {
constexpr uint32_t prime = 16777619u;
constexpr uint32_t seed = 2166136261u;
uint32_t hash = seed;
for (const auto &key : data) {
hash = (hash * prime) ^ key;
}
return hash;
}
};
// define hash function for hash map
CR_NAMESPACE_BEGIN
template <> struct Hash <DangerStorage> {
uint32_t operator () (const DangerStorage &key) const noexcept {
return key.hash ();
}
};
CR_NAMESPACE_END
class BotPractice final : public Singleton <BotPractice> {
public:
// collected data
struct PracticeData {
int16_t damage {}, value {};
int16_t index { kInvalidNodeIndex };
};
// used to save-restore practice data
struct DangerSaveRestore {
DangerStorage danger {};
PracticeData data {};
public:
DangerSaveRestore () = default;
public:
DangerSaveRestore (const DangerStorage &ds, const PracticeData &pd) : danger (ds), data (pd) {}
};
HashMap <DangerStorage, PracticeData> m_data {};
int32_t m_teamHighestDamage[kGameTeamNum] {};
public:
BotPractice () = default;
~BotPractice () = default;
private:
inline bool exists (int32_t team, int32_t start, int32_t goal) const {
return m_data.exists ({ start, goal, team });
}
public:
int32_t getIndex (int32_t team, int32_t start, int32_t goal);
void setIndex (int32_t team, int32_t start, int32_t goal, int32_t value);
int32_t getValue (int32_t team, int32_t start, int32_t goal);
void setValue (int32_t team, int32_t start, int32_t goal, int32_t value);
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);
public:
void update ();
void load ();
void save ();
public:
int32_t getHighestDamageForTeam (int32_t team) const {
return cr::max (1, m_teamHighestDamage[team]);
}
void setHighestDamageForTeam (int32_t team, int32_t value) {
m_teamHighestDamage[team] = value;
}
};
// explose global
CR_EXPOSE_GLOBAL_SINGLETON (BotPractice, practice);

103
inc/storage.h Normal file
View file

@ -0,0 +1,103 @@
//
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
// Copyright © 2004-2023 YaPB Project <yapb@jeefo.net>.
//
// SPDX-License-Identifier: MIT
//
#pragma once
// storage file magic (podbot)
constexpr char kPodbotMagic[8] = "PODWAY!";
constexpr int32_t kStorageMagic = 0x59415042; // storage magic for yapb-data files
constexpr int32_t kStorageMagicUB = 0x544f4255; //support also the fork format (merged back into yapb)
// storage header options
CR_DECLARE_SCOPED_ENUM (StorageOption,
Practice = cr::bit (0), // this is practice (experience) file
Matrix = cr::bit (1), // this is floyd warshal path & distance matrix
Vistable = cr::bit (2), // this is vistable data
Graph = cr::bit (3), // this is a node graph data
Official = cr::bit (4), // this is additional flag for graph indicates graph are official
Recovered = cr::bit (5), // this is additional flag indicates graph converted from podbot and was bad
Exten = cr::bit (6), // this is additional flag indicates that there's extension info
Analyzed = cr::bit (7) // this graph has been analyzed
)
// storage header versions
CR_DECLARE_SCOPED_ENUM (StorageVersion,
Graph = 2,
Practice = 2,
Vistable = 3,
Matrix = 2,
Podbot = 7
)
CR_DECLARE_SCOPED_ENUM_TYPE (BotFile, uint32_t,
Vistable = 0,
LogFile = 1,
Practice = 2,
Graph = 3,
Pathmatrix = 4,
PodbotPWF = 5,
EbotEWP = 6
)
class BotStorage final : public Singleton <BotStorage> {
private:
struct SaveLoadData {
String name {};
int32_t option {};
int32_t version {};
public:
SaveLoadData (StringRef name, int32_t option, int32_t version) : name (name), option (option), version (version) {}
};
private:
int m_retries {};
public:
BotStorage () = default;
~BotStorage () = default;
public:
// converts type to save/load options
template <typename U> SaveLoadData guessType ();
// loads the data and decompress ulz
template <typename U> bool load (SmallArray <U> &data, ExtenHeader *exten = nullptr, int32_t *outOptions = nullptr);
// saves the data and compress with ulz
template <typename U> bool save (const SmallArray <U> &data, ExtenHeader *exten = nullptr, int32_t passOptions = 0);
// report fatail error loading stuff
template <typename ...Args> bool error (bool isGraph, bool isDebug, MemFile &file, const char *fmt, Args &&...args);
// builds the filename to requested filename
String buildPath (int32_t type, bool isMemoryLoad = false);
// converts storage option to stroage filename
int32_t storageToBotFile (int32_t options);
// remove all bot related files frorm disk
void unlinkFromDisk ();
public:
// loading the graph may attemp to recurse loading, with converting or download, reset retry counter
void resetRetries () {
m_retries = 0;
}
};
#if !defined (BOT_STORAGE_EXPLICIT_INSTANTIATIONS)
# define BOT_STORAGE_EXPLICIT_INSTANTIATIONS
# include "../src/storage.cpp"
#endif
#undef BOT_STORAGE_EXPLICIT_INSTANTIATIONS
// explose global
CR_EXPOSE_GLOBAL_SINGLETON (BotStorage, bstor);

View file

@ -7,16 +7,6 @@
#pragma once #pragma once
CR_DECLARE_SCOPED_ENUM_TYPE (BotFile, uint32_t,
Vistable = 0,
LogFile = 1,
Practice = 2,
Graph = 3,
Pathmatrix = 4,
PodbotPWF = 5,
EbotEWP = 6
)
class BotSupport final : public Singleton <BotSupport> { class BotSupport final : public Singleton <BotSupport> {
private: private:
bool m_needToSendWelcome {}; bool m_needToSendWelcome {};
@ -106,12 +96,6 @@ public:
// get the current date and time as string // get the current date and time as string
String getCurrentDateTime (); String getCurrentDateTime ();
// builds the filename to requested filename
String buildPath (int32_t type, bool isMemoryLoad = false);
// converts storage option to stroage filename
int32_t storageToBotFile (StorageOption options);
public: public:
// re-show welcome after changelevel ? // re-show welcome after changelevel ?

61
inc/vistable.h Normal file
View file

@ -0,0 +1,61 @@
//
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
// Copyright © 2004-2023 YaPB Project <yapb@jeefo.net>.
//
// SPDX-License-Identifier: MIT
//
#pragma once
// limits for storing practice data
CR_DECLARE_SCOPED_ENUM_TYPE (VisIndex, int32_t,
None = 0,
Stand = 1,
Crouch = 2,
Any = Stand | Crouch
)
// defines visibility count
struct PathVis {
uint16_t stand, crouch;
};
class GraphVistable final : public Singleton <GraphVistable> {
public:
using VisStorage = uint8_t;
private:
SmallArray <VisStorage> m_vistable {};
bool m_rebuild {};
int m_length {};
int m_curIndex {};
int m_sliceIndex {};
float m_notifyMsgTimestamp {};
public:
GraphVistable () = default;
~GraphVistable () = default;
public:
bool visible (int srcIndex, int destIndex, VisIndex vis = VisIndex::Any);
void load ();
void save ();
void rebuild ();
public:
// triggers re-check for all the nodes
void startRebuild ();
// ready to use ?
bool isReady () const {
return !m_rebuild;
}
};
// explose global
CR_EXPOSE_GLOBAL_SINGLETON (GraphVistable, vistab);

View file

@ -443,12 +443,6 @@ namespace TaskPri {
static constexpr float EscapeFromBomb { 100.0f }; static constexpr float EscapeFromBomb { 100.0f };
} }
// storage file magic
constexpr char kPodbotMagic[8] = "PODWAY!";
constexpr int32_t kStorageMagic = 0x59415042; // storage magic for yapb-data files
constexpr int32_t kStorageMagicUB = 0x544f4255; //support also the fork format (merged back into yapb)
constexpr float kInfiniteDistance = 9999999.0f; constexpr float kInfiniteDistance = 9999999.0f;
constexpr float kGrenadeCheckTime = 0.6f; constexpr float kGrenadeCheckTime = 0.6f;
constexpr float kSprayDistance = 260.0f; constexpr float kSprayDistance = 260.0f;
@ -456,10 +450,6 @@ constexpr float kDoubleSprayDistance = kSprayDistance * 2;
constexpr float kMaxChatterRepeatInterval = 99.0f; constexpr float kMaxChatterRepeatInterval = 99.0f;
constexpr int kInfiniteDistanceLong = static_cast <int> (kInfiniteDistance); constexpr int kInfiniteDistanceLong = static_cast <int> (kInfiniteDistance);
constexpr int kMaxNodeLinks = 8;
constexpr int kMaxPracticeDamageValue = 2040;
constexpr int kMaxPracticeGoalValue = 2040;
constexpr int kMaxNodes = 2048;
constexpr int kMaxWeapons = 32; constexpr int kMaxWeapons = 32;
constexpr int kNumWeapons = 26; constexpr int kNumWeapons = 26;
constexpr int kMaxCollideMoves = 3; constexpr int kMaxCollideMoves = 3;
@ -598,11 +588,78 @@ struct ChatCollection {
// include bot graph stuff // include bot graph stuff
#include <graph.h> #include <graph.h>
// this structure links nodes returned from pathfinder
class PathWalk final : public DenyCopying {
private:
size_t m_cursor {};
size_t m_length {};
UniquePtr <int32_t[]> m_path {};
public:
explicit PathWalk () = default;
~PathWalk () = default;
public:
int32_t &next () {
return at (1);
}
int32_t &first () {
return at (0);
}
int32_t &last () {
return at (length () - 1);
}
int32_t &at (size_t index) {
return m_path[m_cursor + index];
}
void shift () {
++m_cursor;
}
void reverse () {
for (size_t i = 0; i < m_length / 2; ++i) {
cr::swap (m_path[i], m_path[m_length - 1 - i]);
}
}
size_t length () const {
if (m_cursor >= m_length) {
return 0;
}
return m_length - m_cursor;
}
bool hasNext () const {
return length () > m_cursor;
}
bool empty () const {
return !length ();
}
void add (int32_t node) {
m_path[m_length++] = node;
}
void clear () {
m_cursor = 0;
m_length = 0;
m_path[0] = 0;
}
void init (size_t length) {
m_path = cr::makeUnique <int32_t[]> (length);
}
};
// main bot class // main bot class
class Bot final { class Bot final {
private:
using RouteTwin = Twin <int, float>;
public: public:
friend class BotManager; friend class BotManager;
@ -632,6 +689,7 @@ private:
int m_tryOpenDoor {}; // attempt's to open the door int m_tryOpenDoor {}; // attempt's to open the door
int m_liftState {}; // state of lift handling int m_liftState {}; // state of lift handling
int m_radioSelect {}; // radio entry int m_radioSelect {}; // radio entry
int m_lastPredictIndex {}; // last predicted index
float m_headedTime {}; float m_headedTime {};
float m_prevTime {}; // time previously checked movement speed float m_prevTime {}; // time previously checked movement speed
@ -744,16 +802,14 @@ private:
Array <edict_t *> m_ignoredBreakable {}; // list of ignored breakables Array <edict_t *> m_ignoredBreakable {}; // list of ignored breakables
Array <edict_t *> m_hostages {}; // pointer to used hostage entities Array <edict_t *> m_hostages {}; // pointer to used hostage entities
Array <Route> m_routes {}; // pointer
Array <int32_t> m_nodeHistory {}; // history of selected goals Array <int32_t> m_nodeHistory {}; // history of selected goals
BinaryHeap <RouteTwin> m_routeQue {};
Path *m_path {}; // pointer to the current path node Path *m_path {}; // pointer to the current path node
String m_chatBuffer {}; // space for strings (say text...) String m_chatBuffer {}; // space for strings (say text...)
FrustumPlane m_frustum[FrustumSide::Num] {}; FrustumPlane m_frustum[FrustumSide::Num] {};
private: private:
int pickBestWeapon (int *vec, int count, int moneySave); int pickBestWeapon (Array <int> &vec, int moneySave);
int getRandomCampDir (); int getRandomCampDir ();
int findAimingNode (const Vector &to, int &pathLength); int findAimingNode (const Vector &to, int &pathLength);
int findNearestNode (); int findNearestNode ();
@ -863,9 +919,7 @@ private:
void updatePracticeDamage (edict_t *attacker, int damage); void updatePracticeDamage (edict_t *attacker, int damage);
void findShortestPath (int srcIndex, int destIndex); void findShortestPath (int srcIndex, int destIndex);
void calculateFrustum (); void calculateFrustum ();
void findPath (int srcIndex, int destIndex, FindPath pathType = FindPath::Fast); void findPath (int srcIndex, int destIndex, FindPath pathType = FindPath::Fast);
void clearRoute ();
void debugMsgInternal (const char *str); void debugMsgInternal (const char *str);
void frame (); void frame ();
void resetCollision (); void resetCollision ();
@ -950,6 +1004,13 @@ private:
issueCommand ("drop"); issueCommand ("drop");
} }
// ensures current node is ok
void ensureCurrentNodeIndex () {
if (m_currentNodeIndex == kInvalidNodeIndex) {
changeNodeIndex (findNearestNode ());
}
}
public: public:
entvars_t *pev {}; entvars_t *pev {};
@ -1189,6 +1250,9 @@ public:
#include "engine.h" #include "engine.h"
#include "manager.h" #include "manager.h"
#include "control.h" #include "control.h"
#include "planner.h"
#include "storage.h"
#include "analyze.h"
// very global convars // very global convars
extern ConVar cv_jasonmode; extern ConVar cv_jasonmode;
@ -1211,7 +1275,10 @@ extern ConVar cv_debug_goal;
extern ConVar cv_save_bots_names; extern ConVar cv_save_bots_names;
extern ConVar cv_random_knife_attacks; extern ConVar cv_random_knife_attacks;
extern ConVar cv_rotate_bots; extern ConVar cv_rotate_bots;
extern ConVar cv_graph_url;
extern ConVar cv_graph_url_upload; extern ConVar cv_graph_url_upload;
extern ConVar cv_graph_auto_save_count;
extern ConVar cv_graph_analyze_max_jump_height;
extern ConVar mp_freezetime; extern ConVar mp_freezetime;
extern ConVar mp_roundtime; extern ConVar mp_roundtime;

View file

@ -14,7 +14,7 @@ project (
default_options: [ default_options: [
'buildtype=release', 'buildtype=release',
'b_ndebug=if-release', 'b_ndebug=if-release',
'cpp_std=c++14', 'cpp_std=c++17',
'warning_level=3', 'warning_level=3',
'werror=true', 'werror=true',
'backend=ninja', 'backend=ninja',
@ -222,6 +222,7 @@ add_global_link_arguments (ldflags, language: 'cpp')
# add the sources # add the sources
sources = files( sources = files(
'src/analyze.cpp',
'src/botlib.cpp', 'src/botlib.cpp',
'src/chatlib.cpp', 'src/chatlib.cpp',
'src/combat.cpp', 'src/combat.cpp',
@ -234,8 +235,12 @@ sources = files (
'src/module.cpp', 'src/module.cpp',
'src/message.cpp', 'src/message.cpp',
'src/navigate.cpp', 'src/navigate.cpp',
'src/planner.cpp',
'src/practice.cpp',
'src/sounds.cpp', 'src/sounds.cpp',
'src/support.cpp' 'src/storage.cpp',
'src/support.cpp',
'src/vistable.cpp'
) )
# add the include directories # add the include directories

359
src/analyze.cpp Normal file
View file

@ -0,0 +1,359 @@
//
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
// Copyright © 2004-2023 YaPB Project <yapb@jeefo.net>.
//
// SPDX-License-Identifier: MIT
//
#include <yapb.h>
ConVar cv_graph_analyze_auto_start ("yb_graph_analyze_auto_start", "1", "Autostart analyzer if all other cases are failed.");
ConVar cv_graph_analyze_auto_save ("yb_graph_analyze_auto_save", "1", "Auto save results of analyzation to graph file. And re-add bots.");
ConVar cv_graph_analyze_distance ("yb_graph_analyze_distance", "64", "The minimum distance to keep nodes from each other.", true, 42.0f, 128.0f);
ConVar cv_graph_analyze_max_jump_height ("yb_graph_analyze_max_jump_height", "44", "Max jump height to test if next node will be unreachable.", true, 44.0f, 64.0f);
ConVar cv_graph_analyze_fps ("yb_graph_analyze_fps", "30.0", "The FPS at which analyzer process is running. This keeps game from freezing during analyzing.", false, 25.0f, 99.0f);
ConVar cv_graph_analyze_clean_paths_on_finish ("yb_graph_analyze_clean_paths_on_finish", "1", "Specifies if analyzer should clean the unnecessary paths upon finishing.");
ConVar cv_graph_analyze_optimize_nodes_on_finish ("yb_graph_analyze_optimize_nodes_on_finish", "1", "Specifies if analyzer should merge some near-placed nodes with much of connections together.");
ConVar cv_graph_analyze_mark_goals_on_finish ("yb_graph_analyze_mark_goals_on_finish", "1", "Specifies if analyzer should mark nodes as map goals automatically upon finish.");
void GraphAnalyze::start () {
// start analyzer in few seconds after level initialized
if (cv_graph_analyze_auto_start.bool_ ()) {
m_updateInterval = game.time () + 3.0f;
m_basicsCreated = false;
// set as we're analyzing
m_isAnalyzing = true;
// silence all graph messages
graph.setMessageSilence (true);
// set all nodes as not expanded
for (auto &expanded : m_expandedNodes) {
expanded = false;
}
// set all nodes as not optimized
for (auto &optimized : m_optimizedNodes) {
optimized = false;
}
ctrl.msg ("Starting map analyzation.");
}
else {
m_updateInterval = 0.0f;
}
}
void GraphAnalyze::update () {
if (cr::fzero (m_updateInterval) || !m_isAnalyzing) {
return;
}
if (m_updateInterval >= game.time ()) {
return;
}
// add basic nodes
if (!m_basicsCreated) {
graph.addBasic ();
m_basicsCreated = true;
}
for (int i = 0; i < graph.length (); ++i) {
if (m_updateInterval >= game.time ()) {
return;
}
if (!graph.exists (i)) {
return;
}
if (m_expandedNodes[i]) {
continue;
}
m_expandedNodes[i] = true;
setUpdateInterval ();
auto pos = graph[i].origin;
auto range = cv_graph_analyze_distance.float_ ();
for (int dir = 1; dir < kMaxNodeLinks; ++dir) {
switch (dir) {
case 1:
flood (pos, { pos.x + range, pos.y, pos.z }, range);
break;
case 2:
flood (pos, { pos.x - range, pos.y, pos.z }, range);
break;
case 3:
flood (pos, { pos.x, pos.y + range, pos.z }, range);
break;
case 4:
flood (pos, { pos.x, pos.y - range, pos.z }, range);
break;
case 5:
flood (pos, { pos.x + range, pos.y, pos.z + 128.0f }, range);
break;
case 6:
flood (pos, { pos.x - range, pos.y, pos.z + 128.0f }, range);
break;
case 7:
flood (pos, { pos.x, pos.y + range, pos.z + 128.0f }, range);
break;
case 8:
flood (pos, { pos.x, pos.y - range, pos.z + 128.0f }, range);
break;
}
}
}
// finish generation if no updates occurred recently
if (m_updateInterval + 2.0f < game.time ()) {
finish ();
return;
}
}
void GraphAnalyze::suspend () {
m_updateInterval = 0.0f;
m_isAnalyzing = false;
m_isAnalyzed = false;
}
void GraphAnalyze::finish () {
// run optimization on finish
optimize ();
// mark goal nodes
markGoals ();
m_isAnalyzed = true;
m_isAnalyzing = false;
m_updateInterval = 0.0f;
// un-silence all graph messages
graph.setMessageSilence (false);
ctrl.msg ("Complete map analyzation.");
// autosave bots graph
if (cv_graph_analyze_auto_save.bool_ ()) {
if (!graph.saveGraphData ()) {
ctrl.msg ("Can't save analyzed graph. Internal error.");
return;
}
if (!graph.loadGraphData ()) {
ctrl.msg ("Can't load analyzed graph. Internal error.");
return;
}
vistab.startRebuild ();
cv_quota.revert ();
}
}
void GraphAnalyze::optimize () {
if (graph.length () == 0) {
return;
}
if (!cv_graph_analyze_optimize_nodes_on_finish.bool_ ()) {
return;
}
cleanup ();
// clear the uselss connections
if (cv_graph_analyze_clean_paths_on_finish.bool_ ()) {
for (auto i = 0; i < graph.length (); ++i) {
graph.clearConnections (i);
}
}
auto smooth = [] (const Array <int> &nodes) {
Vector result;
for (const auto &node : nodes) {
result += graph[node].origin;
}
result /= kMaxNodeLinks;
result.z = graph[nodes.first ()].origin.z;
return result;
};
// set all nodes as not optimized
for (auto &optimized : m_optimizedNodes) {
optimized = false;
}
for (int i = 0; i < graph.length (); ++i) {
if (m_optimizedNodes[i]) {
continue;
}
const auto &path = graph[i];
Array <int> indexes;
for (const auto &link : path.links) {
if (graph.exists (link.index) && !m_optimizedNodes[link.index] && cr::fequal (path.origin.z, graph[link.index].origin.z)) {
indexes.emplace (link.index);
}
}
// we're have max out node links
if (indexes.length () >= kMaxNodeLinks) {
const Vector &pos = smooth (indexes);
for (const auto &index : indexes) {
graph.erase (index);
}
graph.add (NodeAddFlag::Normal, pos);
}
}
}
void GraphAnalyze::cleanup () {
int connections = 0; // clean bad paths
for (auto i = 0; i < graph.length (); ++i) {
connections = 0;
for (const auto &link : graph[i].links) {
if (link.index != kInvalidNodeIndex) {
if (link.index > graph.length ()) {
graph.erase (i);
}
++connections;
}
}
// no connections
if (!connections) {
graph.erase (i);
}
// path number differs
if (graph[i].number != i) {
graph.erase (i);
}
for (const auto &link : graph[i].links) {
if (link.index != kInvalidNodeIndex) {
if (link.index >= graph.length () || link.index < -kInvalidNodeIndex) {
graph.erase (i);
}
else if (link.index == i) {
graph.erase (i);
}
}
}
if (!graph.isConnected (i)) {
graph.erase (i);
}
}
}
void GraphAnalyze::flood (const Vector &pos, const Vector &next, float range) {
range *= 0.75f;
TraceResult tr;
game.testHull (pos, { next.x, next.y, next.z + 19.0f }, TraceIgnore::Monsters, head_hull, nullptr, &tr);
// we're can't reach next point
if (!cr::fequal (tr.flFraction, 1.0f) && !game.isShootableBreakable (tr.pHit)) {
return;
}
// we're have something in around, skip
if (graph.exists (graph.getForAnalyzer (tr.vecEndPos, range))) {
return;
}
game.testHull (tr.vecEndPos, { tr.vecEndPos.x, tr.vecEndPos.y, tr.vecEndPos.z - 999.0f }, TraceIgnore::Monsters, head_hull, nullptr, &tr);
// ground is away for a break
if (cr::fequal (tr.flFraction, 1.0f)) {
return;
}
Vector nextPos = { tr.vecEndPos.x, tr.vecEndPos.y, tr.vecEndPos.z + 19.0f };
const int endIndex = graph.getForAnalyzer (nextPos, range);
const int targetIndex = graph.getNearestNoBuckets (nextPos, 250.0f);
if (graph.exists (endIndex) || !graph.exists (targetIndex)) {
return;
}
auto targetPos = graph[targetIndex].origin;
// re-check there's nothing nearby, and add something we're want
if (!graph.exists (graph.getNearestNoBuckets (nextPos, range))) {
m_isCrouch = false;
game.testLine (nextPos, { nextPos.x, nextPos.y, nextPos.z + 36.0f }, TraceIgnore::Monsters, nullptr, &tr);
if (!cr::fequal (tr.flFraction, 1.0f)) {
m_isCrouch = true;
}
auto testPos = m_isCrouch ? Vector { nextPos.x, nextPos.y, nextPos.z - 18.0f } : nextPos;
if ((graph.isNodeReacheable (targetPos, testPos) && graph.isNodeReacheable (testPos, targetPos)) || (graph.isNodeReacheableWithJump (testPos, targetPos) && graph.isNodeReacheableWithJump (targetPos, testPos))) {
graph.add (NodeAddFlag::Normal, m_isCrouch ? Vector { nextPos.x, nextPos.y, nextPos.z - 9.0f } : nextPos);
}
}
}
void GraphAnalyze::setUpdateInterval () {
const auto frametime (globals->frametime);
if ((cv_graph_analyze_fps.float_ () + frametime) <= 1.0f / frametime) {
m_updateInterval = game.time () + frametime * 0.06f;
}
}
void GraphAnalyze::markGoals () {
if (!cv_graph_analyze_mark_goals_on_finish.bool_ ()) {
return;
}
auto updateNodeFlags = [] (int type, const char *entity) {
game.searchEntities ("classname", entity, [&] (edict_t *ent) {
for (auto &path : graph) {
const auto &absOrigin = path.origin + Vector (1.0f, 1.0f, 1.0f);
if (ent->v.absmin.x > absOrigin.x || ent->v.absmin.y > absOrigin.y) {
continue;
}
if (ent->v.absmax.x < absOrigin.x || ent->v.absmax.y < absOrigin.y) {
continue;
}
path.flags |= type;
}
return EntitySearchResult::Continue;
});
};
if (game.mapIs (MapFlags::Demolition)) {
updateNodeFlags (NodeFlag::Goal, "func_bomb_target"); // bombspot zone
updateNodeFlags (NodeFlag::Goal, "info_bomb_target"); // bombspot zone (same as above)
}
else if (game.mapIs (MapFlags::HostageRescue)) {
updateNodeFlags (NodeFlag::Rescue, "func_hostage_rescue"); // hostage rescue zone
updateNodeFlags (NodeFlag::Rescue, "info_hostage_rescue"); // hostage rescue zone (same as above)
updateNodeFlags (NodeFlag::Rescue, "info_player_start"); // then add ct spawnpoints
updateNodeFlags (NodeFlag::Goal, "hostage_entity"); // hostage entities
updateNodeFlags (NodeFlag::Goal, "monster_scientist"); // hostage entities (same as above)
}
else if (game.mapIs (MapFlags::Assassination)) {
updateNodeFlags (NodeFlag::Goal, "func_vip_safetyzone"); // vip rescue (safety) zone
updateNodeFlags (NodeFlag::Goal, "func_escapezone"); // terrorist escape zone
}
}

View file

@ -167,7 +167,7 @@ void Bot::avoidGrenades () {
float distance = pent->v.origin.distanceSq (pev->origin); float distance = pent->v.origin.distanceSq (pev->origin);
float distanceMoved = pev->origin.distance (pent->v.origin + pent->v.velocity * m_frameInterval); float distanceMoved = pev->origin.distance (pent->v.origin + pent->v.velocity * m_frameInterval);
if (distanceMoved < distance && distance < cr::square (500.0f)) { if (distanceMoved < distance && distance < cr::sqrf (500.0f)) {
const auto &dirToPoint = (pev->origin - pent->v.origin).normalize2d (); const auto &dirToPoint = (pev->origin - pent->v.origin).normalize2d ();
const auto &rightSide = pev->v_angle.right ().normalize2d (); const auto &rightSide = pev->v_angle.right ().normalize2d ();
@ -242,12 +242,12 @@ void Bot::checkBreakablesAround () {
const auto lengthToObstacle = origin.distanceSq (pev->origin); const auto lengthToObstacle = origin.distanceSq (pev->origin);
// too far, skip it // too far, skip it
if (lengthToObstacle > cr::square (radius)) { if (lengthToObstacle > cr::sqrf (radius)) {
continue; continue;
} }
// too close, skip it // too close, skip it
if (lengthToObstacle < cr::square (100.0f)) { if (lengthToObstacle < cr::sqrf (100.0f)) {
continue; continue;
} }
@ -333,7 +333,7 @@ void Bot::updatePickups () {
} }
const auto &intresting = bots.getIntrestingEntities (); const auto &intresting = bots.getIntrestingEntities ();
const float radius = cr::square (cv_object_pickup_radius.float_ ()); const float radius = cr::sqrf (cv_object_pickup_radius.float_ ());
if (!game.isNullEntity (m_pickupItem)) { if (!game.isNullEntity (m_pickupItem)) {
bool itemExists = false; bool itemExists = false;
@ -573,7 +573,7 @@ void Bot::updatePickups () {
if (allowPickup) { if (allowPickup) {
for (auto &client : util.getClients ()) { for (auto &client : util.getClients ()) {
if ((client.flags & ClientFlags::Used) && !(client.ent->v.flags & FL_FAKECLIENT) && (client.flags & ClientFlags::Alive) && if ((client.flags & ClientFlags::Used) && !(client.ent->v.flags & FL_FAKECLIENT) && (client.flags & ClientFlags::Alive) &&
client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::square (240.0f)) { client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::sqrf (240.0f)) {
allowPickup = false; allowPickup = false;
break; break;
} }
@ -620,7 +620,7 @@ void Bot::updatePickups () {
} }
} }
if (pev->origin.distanceSq (origin) > cr::square (60.0f)) { if (pev->origin.distanceSq (origin) > cr::sqrf (60.0f)) {
if (!graph.isNodeReacheable (pev->origin, origin)) { if (!graph.isNodeReacheable (pev->origin, origin)) {
allowPickup = false; allowPickup = false;
@ -731,7 +731,7 @@ Vector Bot::getCampDirection (const Vector &dest) {
if (tr.flFraction < 1.0f) { if (tr.flFraction < 1.0f) {
float length = tr.vecEndPos.distanceSq (src); float length = tr.vecEndPos.distanceSq (src);
if (length > cr::square (1024.0f)) { if (length > cr::sqrf (1024.0f)) {
return nullptr; return nullptr;
} }
@ -750,7 +750,7 @@ Vector Bot::getCampDirection (const Vector &dest) {
if (link.index == kInvalidNodeIndex) { if (link.index == kInvalidNodeIndex) {
continue; continue;
} }
auto distance = static_cast <float> (graph.getPathDist (link.index, enemyIndex)); auto distance = static_cast <float> (planner.dist (link.index, enemyIndex));
if (distance < minDistance) { if (distance < minDistance) {
minDistance = distance; minDistance = distance;
@ -762,7 +762,7 @@ Vector Bot::getCampDirection (const Vector &dest) {
return graph[lookAtWaypoint].origin; return graph[lookAtWaypoint].origin;
} }
} }
auto dangerIndex = graph.getDangerIndex (m_team, m_currentNodeIndex, m_currentNodeIndex); auto dangerIndex = practice.getIndex (m_team, m_currentNodeIndex, m_currentNodeIndex);
if (graph.exists (dangerIndex)) { if (graph.exists (dangerIndex)) {
return graph[dangerIndex].origin; return graph[dangerIndex].origin;
@ -1110,9 +1110,12 @@ bool Bot::canReplaceWeapon () {
return isWeaponRestricted (m_currentWeapon); return isWeaponRestricted (m_currentWeapon);
} }
int Bot::pickBestWeapon (int *vec, int count, int moneySave) { int Bot::pickBestWeapon (Array <int> &vec, int moneySave) {
// this function picks best available weapon from random choice with money save // this function picks best available weapon from random choice with money save
if (vec.length () < 2) {
return vec.first ();
}
bool needMoreRandomWeapon = (m_personality == Personality::Careful) || (rg.chance (25) && m_personality == Personality::Normal); bool needMoreRandomWeapon = (m_personality == Personality::Careful) || (rg.chance (25) && m_personality == Personality::Normal);
if (needMoreRandomWeapon) { if (needMoreRandomWeapon) {
@ -1121,14 +1124,11 @@ int Bot::pickBestWeapon (int *vec, int count, int moneySave) {
if (buyFactor < 1.0f) { if (buyFactor < 1.0f) {
buyFactor = 1.0f; buyFactor = 1.0f;
} }
// swap array values // swap array values
for (int *begin = vec, *end = vec + count - 1; begin < end; ++begin, --end) { vec.reverse ();
cr::swap (*end, *begin);
}
return vec[static_cast <int> (static_cast <float> (count - 1) * cr::log10 (rg.get (1.0f, cr::powf (10.0f, buyFactor))) / buyFactor + 0.5f)];
}
return vec[static_cast <int> (static_cast <float> (vec.length <int32_t> () - 1) * cr::log10 (rg.get (1.0f, cr::powf (10.0f, buyFactor))) / buyFactor + 0.5f)];
}
int chance = 95; int chance = 95;
// high skilled bots almost always prefer best weapon // high skilled bots almost always prefer best weapon
@ -1140,17 +1140,17 @@ int Bot::pickBestWeapon (int *vec, int count, int moneySave) {
chance = 75; chance = 75;
} }
} }
auto &info = conf.getWeapons (); const auto &tab = conf.getWeapons ();
for (int i = 0; i < count; ++i) { for (const auto &w : vec) {
auto &weapon = info[vec[i]]; const auto &weapon = tab[w];
// if wea have enough money for weapon buy it // if wea have enough money for weapon buy it
if (weapon.price + moneySave < m_moneyAmount + rg.get (50, 200) && rg.chance (chance)) { if (weapon.price + moneySave < m_moneyAmount + rg.get (50, 200) && rg.chance (chance)) {
return vec[i]; return w;
} }
} }
return vec[rg.get (0, count - 1)]; return vec.random ();
} }
void Bot::buyStuff () { void Bot::buyStuff () {
@ -1163,8 +1163,8 @@ void Bot::buyStuff () {
m_nextBuyTime += rg.get (0.3f, 0.5f); m_nextBuyTime += rg.get (0.3f, 0.5f);
} }
int count = 0, weaponCount = 0; int count = 0;
int choices[kNumWeapons] {}; Array <int32_t> choices;
// select the priority tab for this personality // select the priority tab for this personality
const int *pref = conf.getWeaponPrefs (m_personality) + kNumWeapons; const int *pref = conf.getWeaponPrefs (m_personality) + kNumWeapons;
@ -1312,23 +1312,14 @@ void Bot::buyStuff () {
} }
if (selectedWeapon->price <= (m_moneyAmount - moneySave)) { if (selectedWeapon->price <= (m_moneyAmount - moneySave)) {
choices[weaponCount++] = *pref; choices.emplace (*pref);
} }
} while (count < kNumWeapons && weaponCount < 4); } while (count < kNumWeapons && choices.length () < 4);
// found a desired weapon? // found a desired weapon?
if (weaponCount > 0) { if (!choices.empty ()) {
int chosenWeapon; selectedWeapon = &tab[pickBestWeapon (choices, moneySave)];
// choose randomly from the best ones...
if (weaponCount > 1) {
chosenWeapon = pickBestWeapon (choices, weaponCount, moneySave);
}
else {
chosenWeapon = choices[weaponCount - 1];
}
selectedWeapon = &tab[chosenWeapon];
} }
else { else {
selectedWeapon = nullptr; selectedWeapon = nullptr;
@ -1406,23 +1397,14 @@ void Bot::buyStuff () {
} }
if (selectedWeapon->price <= (m_moneyAmount - rg.get (100, 200))) { if (selectedWeapon->price <= (m_moneyAmount - rg.get (100, 200))) {
choices[weaponCount++] = *pref; choices.emplace (*pref);
} }
} while (count < kNumWeapons && weaponCount < 4); } while (count < kNumWeapons && choices.length () < 4);
// found a desired weapon? // found a desired weapon?
if (weaponCount > 0) { if (!choices.empty ()) {
int chosenWeapon; selectedWeapon = &tab[pickBestWeapon (choices, rg.get (100, 200))];
// choose randomly from the best ones...
if (weaponCount > 1) {
chosenWeapon = pickBestWeapon (choices, weaponCount, rg.get (100, 200));
}
else {
chosenWeapon = choices[weaponCount - 1];
}
selectedWeapon = &tab[chosenWeapon];
} }
else { else {
selectedWeapon = nullptr; selectedWeapon = nullptr;
@ -1708,14 +1690,17 @@ void Bot::setConditions () {
} }
if (game.isNullEntity (m_enemy) && !game.isNullEntity (m_lastEnemy) && !m_lastEnemyOrigin.empty () && m_seeEnemyTime + 0.5f < game.time ()) { 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); auto distanceToLastEnemySq = m_lastEnemyOrigin.distanceSq (pev->origin);
if (distanceToLastEnemySq < cr::square (1600.0f)) { if (distanceToLastEnemySq < cr::sqrf (1600.0f)) {
auto pathLength = 0; auto pathLength = 0;
auto nodeIndex = findAimingNode (m_lastEnemyOrigin, pathLength); auto nodeIndex = findAimingNode (m_lastEnemyOrigin, pathLength);
if (graph.exists (nodeIndex) && pathLength < cv_max_nodes_for_predict.int_ () && pev->origin.distanceSq (graph[nodeIndex].origin) > cr::square (384.0f)) { 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_aimFlags |= AimFlags::PredictPath;
m_lastPredictIndex = nodeIndex;
} }
} }
@ -2050,7 +2035,7 @@ bool Bot::isEnemyThreat () {
} }
// if enemy is near or facing us directly // if enemy is near or facing us directly
if (m_enemy->v.origin.distanceSq (pev->origin) < cr::square (256.0f) || (!usesKnife () && isInViewCone (m_enemy->v.origin))) { if (m_enemy->v.origin.distanceSq (pev->origin) < cr::sqrf (256.0f) || (!usesKnife () && isInViewCone (m_enemy->v.origin))) {
return true; return true;
} }
return false; return false;
@ -2071,13 +2056,13 @@ bool Bot::reactOnEnemy () {
} }
int enemyIndex = graph.getNearest (m_enemy->v.origin); int enemyIndex = graph.getNearest (m_enemy->v.origin);
auto lineDist = m_enemy->v.origin.distance (pev->origin); auto lineDist = m_enemy->v.origin.distance2d (pev->origin);
auto pathDist = static_cast <float> (graph.getPathDist (ownIndex, enemyIndex)); auto pathDist = static_cast <float> (planner.dist (ownIndex, enemyIndex));
if (pathDist - lineDist > 112.0f || isOnLadder ()) { if (pathDist - lineDist > (planner.hasRealPathDistance () ? 112.0f : 32.0f) || isOnLadder ()) {
m_isEnemyReachable = false; m_isEnemyReachable = false;
} }
else { else if (vistab.visible (ownIndex, enemyIndex)) {
m_isEnemyReachable = true; m_isEnemyReachable = true;
} }
m_enemyReachableTimer = game.time () + 1.0f; m_enemyReachableTimer = game.time () + 1.0f;
@ -2647,19 +2632,19 @@ void Bot::updateAimDir () {
else if (flags & AimFlags::PredictPath) { else if (flags & AimFlags::PredictPath) {
bool changePredictedEnemy = true; bool changePredictedEnemy = true;
if (m_timeNextTracking > game.time () && m_trackingEdict == m_lastEnemy) { if (m_timeNextTracking < game.time () && m_trackingEdict == m_lastEnemy) {
changePredictedEnemy = false; changePredictedEnemy = false;
} }
if (changePredictedEnemy) { if (changePredictedEnemy) {
auto pathLength = 0; auto pathLength = 0;
auto aimNode = findAimingNode (m_lastEnemyOrigin, pathLength); auto aimNode = graph.exists (m_lastPredictIndex) ? m_lastPredictIndex : findAimingNode (m_lastEnemyOrigin, pathLength);
if (graph.exists (aimNode) && pathLength < cv_max_nodes_for_predict.int_ ()) { if (graph.exists (aimNode) && pathLength < cv_max_nodes_for_predict.int_ ()) {
m_lookAt = graph[aimNode].origin; m_lookAt = graph[aimNode].origin;
m_lookAtSafe = m_lookAt; m_lookAtSafe = m_lookAt;
m_timeNextTracking = game.time () + 0.5f; m_timeNextTracking = game.time () + 0.75f;
m_trackingEdict = m_lastEnemy; m_trackingEdict = m_lastEnemy;
} }
else { else {
@ -2678,10 +2663,10 @@ void Bot::updateAimDir () {
else if (flags & AimFlags::Nav) { else if (flags & AimFlags::Nav) {
m_lookAt = m_destOrigin; m_lookAt = m_destOrigin;
if (m_moveToGoal && m_seeEnemyTime + 4.0f < game.time () && !m_isStuck && m_moveSpeed > getShiftSpeed () && !(pev->button & IN_DUCK) && m_currentNodeIndex != kInvalidNodeIndex && !(m_pathFlags & (NodeFlag::Ladder | NodeFlag::Crouch)) && m_pathWalk.hasNext () && pev->origin.distanceSq (m_destOrigin) < cr::square (512.0f)) { if (m_moveToGoal && m_seeEnemyTime + 4.0f < game.time () && !m_isStuck && m_moveSpeed > getShiftSpeed () && !(pev->button & IN_DUCK) && m_currentNodeIndex != kInvalidNodeIndex && !(m_pathFlags & (NodeFlag::Ladder | NodeFlag::Crouch)) && m_pathWalk.hasNext () && pev->origin.distanceSq (m_destOrigin) < cr::sqrf (512.0f)) {
auto nextPathIndex = m_pathWalk.next (); auto nextPathIndex = m_pathWalk.next ();
if (graph.isVisible (m_currentNodeIndex, nextPathIndex)) { if (vistab.visible (m_currentNodeIndex, nextPathIndex)) {
m_lookAt = graph[nextPathIndex].origin + pev->view_ofs; m_lookAt = graph[nextPathIndex].origin + pev->view_ofs;
} }
else { else {
@ -2696,10 +2681,10 @@ void Bot::updateAimDir () {
} }
if (m_canChooseAimDirection && m_seeEnemyTime + 4.0f < game.time () && m_currentNodeIndex != kInvalidNodeIndex && !(m_pathFlags & NodeFlag::Ladder)) { if (m_canChooseAimDirection && m_seeEnemyTime + 4.0f < game.time () && m_currentNodeIndex != kInvalidNodeIndex && !(m_pathFlags & NodeFlag::Ladder)) {
auto dangerIndex = graph.getDangerIndex (m_team, m_currentNodeIndex, m_currentNodeIndex); auto dangerIndex = practice.getIndex (m_team, m_currentNodeIndex, m_currentNodeIndex);
if (graph.exists (dangerIndex) && graph.isVisible (m_currentNodeIndex, dangerIndex) && !(graph[dangerIndex].flags & NodeFlag::Crouch)) { if (graph.exists (dangerIndex) && vistab.visible (m_currentNodeIndex, dangerIndex) && !(graph[dangerIndex].flags & NodeFlag::Crouch)) {
if (pev->origin.distanceSq (graph[dangerIndex].origin) < cr::square (160.0f)) { if (pev->origin.distanceSq (graph[dangerIndex].origin) < cr::sqrf (160.0f)) {
m_lookAt = m_destOrigin; m_lookAt = m_destOrigin;
} }
else { else {
@ -2730,7 +2715,7 @@ void Bot::checkDarkness () {
} }
// do not check every frame // do not check every frame
if (m_checkDarkTime + 5.0f > game.time ()) { if (m_checkDarkTime + 5.0f > game.time () || cr::fzero (m_path->light)) {
return; return;
} }
auto skyColor = illum.getSkyColor (); auto skyColor = illum.getSkyColor ();
@ -2798,7 +2783,7 @@ void Bot::frame () {
if (bots.isBombPlanted () && m_team == Team::CT && m_notKilled) { if (bots.isBombPlanted () && m_team == Team::CT && m_notKilled) {
const Vector &bombPosition = graph.getBombOrigin (); const Vector &bombPosition = graph.getBombOrigin ();
if (!m_hasProgressBar && getCurrentTaskId () != Task::EscapeFromBomb && pev->origin.distanceSq (bombPosition) < cr::square (1540.0f) && !isBombDefusing (bombPosition)) { if (!m_hasProgressBar && getCurrentTaskId () != Task::EscapeFromBomb && pev->origin.distanceSq (bombPosition) < cr::sqrf (1540.0f) && !isBombDefusing (bombPosition)) {
m_itemIgnore = nullptr; m_itemIgnore = nullptr;
m_itemCheckTime = game.time (); m_itemCheckTime = game.time ();
@ -2823,7 +2808,7 @@ void Bot::frame () {
} }
// clear enemy far away // clear enemy far away
if (!m_lastEnemyOrigin.empty () && !game.isNullEntity (m_lastEnemy) && pev->origin.distanceSq (m_lastEnemyOrigin) >= cr::square (2048.0)) { if (!m_lastEnemyOrigin.empty () && !game.isNullEntity (m_lastEnemy) && pev->origin.distanceSq (m_lastEnemyOrigin) >= cr::sqrf (2048.0)) {
m_lastEnemy = nullptr; m_lastEnemy = nullptr;
m_lastEnemyOrigin = nullptr; m_lastEnemyOrigin = nullptr;
} }
@ -3128,6 +3113,7 @@ void Bot::normal_ () {
if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted ()) { if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted ()) {
pathSearchType = rg.chance (60) ? FindPath::Fast : FindPath::Optimal; pathSearchType = rg.chance (60) ? FindPath::Fast : FindPath::Optimal;
} }
ensureCurrentNodeIndex ();
// do pathfinding if it's not the current waypoint // do pathfinding if it's not the current waypoint
if (destIndex != m_currentNodeIndex) { if (destIndex != m_currentNodeIndex) {
@ -3347,6 +3333,8 @@ void Bot::seekCover_ () {
m_prevGoalIndex = destIndex; m_prevGoalIndex = destIndex;
getTask ()->data = destIndex; getTask ()->data = destIndex;
ensureCurrentNodeIndex ();
if (destIndex != m_currentNodeIndex) { if (destIndex != m_currentNodeIndex) {
findPath (m_currentNodeIndex, destIndex, FindPath::Fast); findPath (m_currentNodeIndex, destIndex, FindPath::Fast);
} }
@ -3432,6 +3420,7 @@ void Bot::blind_ () {
} }
else if (!hasActiveGoal ()) { else if (!hasActiveGoal ()) {
clearSearchNodes (); clearSearchNodes ();
ensureCurrentNodeIndex ();
m_prevGoalIndex = m_blindNodeIndex; m_prevGoalIndex = m_blindNodeIndex;
getTask ()->data = m_blindNodeIndex; getTask ()->data = m_blindNodeIndex;
@ -3652,6 +3641,7 @@ void Bot::moveToPos_ () {
m_prevGoalIndex = destIndex; m_prevGoalIndex = destIndex;
getTask ()->data = destIndex; getTask ()->data = destIndex;
ensureCurrentNodeIndex ();
findPath (m_currentNodeIndex, destIndex, m_pathType); findPath (m_currentNodeIndex, destIndex, m_pathType);
} }
else { else {
@ -3918,7 +3908,7 @@ void Bot::followUser_ () {
m_reloadState = Reload::Primary; m_reloadState = Reload::Primary;
} }
if (m_targetEntity->v.origin.distanceSq (pev->origin) > cr::square (130.0f)) { if (m_targetEntity->v.origin.distanceSq (pev->origin) > cr::sqrf (130.0f)) {
m_followWaitTime = 0.0f; m_followWaitTime = 0.0f;
} }
else { else {
@ -3998,7 +3988,7 @@ void Bot::throwExplosive_ () {
ignoreCollision (); ignoreCollision ();
if (pev->origin.distanceSq (dest) < cr::square (450.0f)) { if (pev->origin.distanceSq (dest) < cr::sqrf (450.0f)) {
// heck, I don't wanna blow up myself // heck, I don't wanna blow up myself
m_grenadeCheckTime = game.time () + kGrenadeCheckTime * 2.0f; m_grenadeCheckTime = game.time () + kGrenadeCheckTime * 2.0f;
@ -4065,7 +4055,7 @@ void Bot::throwFlashbang_ () {
ignoreCollision (); ignoreCollision ();
if (pev->origin.distanceSq (dest) < cr::square (450.0f)) { if (pev->origin.distanceSq (dest) < cr::sqrf (450.0f)) {
m_grenadeCheckTime = game.time () + kGrenadeCheckTime * 2.0f; // heck, I don't wanna blow up myself m_grenadeCheckTime = game.time () + kGrenadeCheckTime * 2.0f; // heck, I don't wanna blow up myself
selectBestWeapon (); selectBestWeapon ();
@ -4255,7 +4245,7 @@ void Bot::escapeFromBomb_ () {
completeTask (); // we're done completeTask (); // we're done
// press duck button if we still have some enemies // press duck button if we still have some enemies
if (numEnemiesNear (pev->origin, 2048.0f)) { if (m_numEnemiesLeft > 0) {
m_campButtons = IN_DUCK; m_campButtons = IN_DUCK;
} }
@ -4267,37 +4257,39 @@ void Bot::escapeFromBomb_ () {
else if (!hasActiveGoal ()) { else if (!hasActiveGoal ()) {
clearSearchNodes (); clearSearchNodes ();
int lastSelectedGoal = kInvalidNodeIndex, minPathDistance = kInfiniteDistanceLong; int bestIndex = kInvalidNodeIndex;
float safeRadius = rg.get (1513.0f, 2048.0f); float safeRadius = rg.get (1513.0f, 2048.0f);
float closestDistance = kInfiniteDistance;
for (const auto &path : graph) { for (const auto &path : graph) {
if (path.origin.distance (graph.getBombOrigin ()) < safeRadius || isOccupiedNode (path.number)) { if (path.origin.distance (graph.getBombOrigin ()) < safeRadius || isOccupiedNode (path.number)) {
continue; continue;
} }
int pathDistance = graph.getPathDist (m_currentNodeIndex, path.number); float distance = pev->origin.distance (path.origin);
if (minPathDistance > pathDistance) { if (closestDistance > distance) {
minPathDistance = pathDistance; closestDistance = distance;
lastSelectedGoal = path.number; bestIndex = path.number;
} }
} }
if (lastSelectedGoal < 0) { if (bestIndex < 0) {
lastSelectedGoal = graph.getFarest (pev->origin, safeRadius); bestIndex = graph.getFarest (pev->origin, safeRadius);
} }
// still no luck? // still no luck?
if (lastSelectedGoal < 0) { if (bestIndex < 0) {
completeTask (); // we're done completeTask (); // we're done
// we have no destination point, so just sit down and camp // we have no destination point, so just sit down and camp
startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + 10.0f, true); startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + 10.0f, true);
return; return;
} }
m_prevGoalIndex = lastSelectedGoal; m_prevGoalIndex = bestIndex;
getTask ()->data = lastSelectedGoal; getTask ()->data = bestIndex;
findPath (m_currentNodeIndex, lastSelectedGoal, FindPath::Fast); findPath (m_currentNodeIndex, bestIndex, FindPath::Fast);
} }
} }
@ -4511,7 +4503,7 @@ void Bot::pickupItem_ () {
// check if hostage is with a human teammate (hack) // check if hostage is with a human teammate (hack)
for (auto &client : util.getClients ()) { for (auto &client : util.getClients ()) {
if ((client.flags & ClientFlags::Used) && !(client.ent->v.flags & FL_FAKECLIENT) && (client.flags & ClientFlags::Alive) && if ((client.flags & ClientFlags::Used) && !(client.ent->v.flags & FL_FAKECLIENT) && (client.flags & ClientFlags::Alive) &&
client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::square (240.0f)) { client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::sqrf (240.0f)) {
return EntitySearchResult::Continue; return EntitySearchResult::Continue;
} }
} }
@ -4760,7 +4752,7 @@ void Bot::logic () {
// calculate 2 direction vectors, 1 without the up/down component // calculate 2 direction vectors, 1 without the up/down component
const Vector &dirOld = m_destOrigin - (pev->origin + pev->velocity * m_frameInterval); const Vector &dirOld = m_destOrigin - (pev->origin + pev->velocity * m_frameInterval);
const Vector &dirNormal = dirOld.normalize2d (); const Vector &dirNormal = dirOld.normalize2d_apx ();
m_moveAngles = dirOld.angles (); m_moveAngles = dirOld.angles ();
m_moveAngles.clampAngles (); m_moveAngles.clampAngles ();
@ -4968,7 +4960,7 @@ bool Bot::hasHostage () {
if (!game.isNullEntity (hostage)) { if (!game.isNullEntity (hostage)) {
// don't care about dead hostages // don't care about dead hostages
if (hostage->v.health <= 0.0f || pev->origin.distanceSq (hostage->v.origin) > cr::square (600.0f)) { if (hostage->v.health <= 0.0f || pev->origin.distanceSq (hostage->v.origin) > cr::sqrf (600.0f)) {
hostage = nullptr; hostage = nullptr;
continue; continue;
} }
@ -5114,10 +5106,13 @@ void Bot::updatePracticeValue (int damage) {
} }
auto health = static_cast <int> (m_healthValue); auto health = static_cast <int> (m_healthValue);
// max goal value
constexpr int maxGoalValue = PracticeLimit::Goal;
// only rate goal waypoint if bot died because of the damage // only rate goal waypoint 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 // FIXME: could be done a lot better, however this cares most about damage done by sniping or really deadly weapons
if (health - damage <= 0) { if (health - damage <= 0) {
graph.setDangerValue (m_team, m_chosenGoalIndex, m_prevGoalIndex, cr::clamp (graph.getDangerValue (m_team, m_chosenGoalIndex, m_prevGoalIndex) - health / 20, -kMaxPracticeGoalValue, kMaxPracticeGoalValue)); practice.setValue (m_team, m_chosenGoalIndex, m_prevGoalIndex, cr::clamp (practice.getValue (m_team, m_chosenGoalIndex, m_prevGoalIndex) - health / 20, -maxGoalValue, maxGoalValue));
} }
} }
@ -5134,6 +5129,7 @@ void Bot::updatePracticeDamage (edict_t *attacker, int damage) {
if (attackerTeam == victimTeam) { if (attackerTeam == victimTeam) {
return; return;
} }
constexpr int maxDamageValue = PracticeLimit::Damage;
// if these are bots also remember damage to rank destination of the bot // if these are bots also remember damage to rank destination of the bot
m_goalValue -= static_cast <float> (damage); m_goalValue -= static_cast <float> (damage);
@ -5155,18 +5151,18 @@ void Bot::updatePracticeDamage (edict_t *attacker, int damage) {
if (m_healthValue > 20.0f) { if (m_healthValue > 20.0f) {
if (victimTeam == Team::Terrorist || victimTeam == Team::CT) { if (victimTeam == Team::Terrorist || victimTeam == Team::CT) {
graph.setDangerDamage (victimIndex, victimIndex, victimIndex, cr::clamp (graph.getDangerDamage (victimTeam, victimIndex, victimIndex), 0, kMaxPracticeDamageValue)); practice.setDamage (victimIndex, victimIndex, victimIndex, cr::clamp (practice.getDamage (victimTeam, victimIndex, victimIndex), 0, maxDamageValue));
} }
} }
auto updateDamage = util.isFakeClient (attacker) ? 10 : 7; auto updateDamage = util.isFakeClient (attacker) ? 10 : 7;
// store away the damage done // store away the damage done
int damageValue = cr::clamp (graph.getDangerDamage (m_team, victimIndex, attackerIndex) + damage / updateDamage, 0, kMaxPracticeDamageValue); int damageValue = cr::clamp (practice.getDamage (m_team, victimIndex, attackerIndex) + damage / updateDamage, 0, maxDamageValue);
if (damageValue > graph.getHighestDamageForTeam (m_team)) { if (damageValue > graph.getHighestDamageForTeam (m_team)) {
graph.setHighestDamageForTeam (m_team, damageValue); graph.setHighestDamageForTeam (m_team, damageValue);
} }
graph.setDangerDamage (m_team, victimIndex, attackerIndex, damageValue); practice.setDamage (m_team, victimIndex, attackerIndex, damageValue);
} }
void Bot::pushChatMessage (int type, bool isTeamSay) { void Bot::pushChatMessage (int type, bool isTeamSay) {
@ -5434,10 +5430,10 @@ bool Bot::isOutOfBombTimer () {
if (timeLeft > 13.0f) { if (timeLeft > 13.0f) {
return false; return false;
} }
const Vector &bombOrigin = graph.getBombOrigin (); const auto &bombOrigin = graph.getBombOrigin ();
// for terrorist, if timer is lower than 13 seconds, return true // for terrorist, if timer is lower than 13 seconds, return true
if (timeLeft < 13.0f && m_team == Team::Terrorist && bombOrigin.distanceSq (pev->origin) < cr::square (964.0f)) { if (timeLeft < 13.0f && m_team == Team::Terrorist && bombOrigin.distanceSq (pev->origin) < cr::sqrf (964.0f)) {
return true; return true;
} }
bool hasTeammatesWithDefuserKit = false; bool hasTeammatesWithDefuserKit = false;
@ -5445,7 +5441,7 @@ bool Bot::isOutOfBombTimer () {
// check if our teammates has defusal kit // check if our teammates has defusal kit
for (const auto &bot : bots) { for (const auto &bot : bots) {
// search players with defuse kit // search players with defuse kit
if (bot.get () != this && bot->m_team == Team::CT && bot->m_hasDefuser && bombOrigin.distanceSq (bot->pev->origin) < cr::square (512.0f)) { if (bot.get () != this && bot->m_team == Team::CT && bot->m_hasDefuser && bombOrigin.distanceSq (bot->pev->origin) < cr::sqrf (512.0f)) {
hasTeammatesWithDefuserKit = true; hasTeammatesWithDefuserKit = true;
break; break;
} }
@ -5474,7 +5470,7 @@ void Bot::updateHearing () {
// loop through all enemy clients to check for hearable stuff // loop through all enemy clients to check for hearable stuff
for (int i = 0; i < game.maxClients (); ++i) { for (int i = 0; i < game.maxClients (); ++i) {
const Client &client = util.getClient (i); const auto &client = util.getClient (i);
if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive) || client.ent == ent () || client.team == m_team || client.noise.last < game.time ()) { if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive) || client.ent == ent () || client.team == m_team || client.noise.last < game.time ()) {
continue; continue;
@ -5615,7 +5611,7 @@ bool Bot::isBombDefusing (const Vector &bombOrigin) {
return false; return false;
} }
bool defusingInProgress = false; bool defusingInProgress = false;
constexpr auto distanceToBomb = cr::square (165.0f); constexpr auto distanceToBomb = cr::sqrf (165.0f);
for (const auto &client : util.getClients ()) { for (const auto &client : util.getClients ()) {
if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive)) { if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive)) {

View file

@ -24,7 +24,7 @@ int Bot::numFriendsNear (const Vector &origin, float radius) {
continue; continue;
} }
if (client.origin.distanceSq (origin) < cr::square (radius)) { if (client.origin.distanceSq (origin) < cr::sqrf (radius)) {
count++; count++;
} }
} }
@ -39,7 +39,7 @@ int Bot::numEnemiesNear (const Vector &origin, float radius) {
continue; continue;
} }
if (client.origin.distanceSq (origin) < cr::square (radius)) { if (client.origin.distanceSq (origin) < cr::sqrf (radius)) {
count++; count++;
} }
} }
@ -231,7 +231,7 @@ bool Bot::lookupEnemies () {
return false; return false;
} }
edict_t *player, *newEnemy = nullptr; edict_t *player, *newEnemy = nullptr;
float nearestDistance = cr::square (m_viewDistance); float nearestDistance = cr::sqrf (m_viewDistance);
// clear suspected flag // clear suspected flag
if (!game.isNullEntity (m_enemy) && (m_states & Sense::SeeingEnemy)) { if (!game.isNullEntity (m_enemy) && (m_states & Sense::SeeingEnemy)) {
@ -300,7 +300,7 @@ bool Bot::lookupEnemies () {
// extra skill player can see thru smoke... if beeing attacked // extra skill player can see thru smoke... if beeing attacked
if ((player->v.button & (IN_ATTACK | IN_ATTACK2)) && m_viewDistance < m_maxViewDistance && m_difficulty == Difficulty::Expert) { if ((player->v.button & (IN_ATTACK | IN_ATTACK2)) && m_viewDistance < m_maxViewDistance && m_difficulty == Difficulty::Expert) {
nearestDistance = cr::square (m_maxViewDistance); nearestDistance = cr::sqrf (m_maxViewDistance);
} }
// see if bot can see the player... // see if bot can see the player...
@ -645,7 +645,7 @@ bool Bot::isPenetrableObstacle (const Vector &dest) {
game.testLine (dest, source, TraceIgnore::Monsters, ent (), &tr); game.testLine (dest, source, TraceIgnore::Monsters, ent (), &tr);
if (!cr::fequal (tr.flFraction, 1.0f)) { if (!cr::fequal (tr.flFraction, 1.0f)) {
if (tr.vecEndPos.distanceSq (dest) > cr::square (800.0f)) { if (tr.vecEndPos.distanceSq (dest) > cr::sqrf (800.0f)) {
return false; return false;
} }
@ -655,7 +655,7 @@ bool Bot::isPenetrableObstacle (const Vector &dest) {
obstacleDistance = tr.vecEndPos.distanceSq (source); obstacleDistance = tr.vecEndPos.distanceSq (source);
} }
} }
const float distance = cr::square (75.0f); const float distance = cr::sqrf (75.0f);
if (obstacleDistance > 0.0f) { if (obstacleDistance > 0.0f) {
while (power > 0) { while (power > 0) {
@ -679,7 +679,7 @@ bool Bot::isPenetrableObstacle2 (const Vector &dest) {
} }
const Vector &source = getEyesPos (); const Vector &source = getEyesPos ();
const Vector &direction = (dest - source).normalize (); // 1 unit long const Vector &direction = (dest - source).normalize_apx (); // 1 unit long
int thikness = 0; int thikness = 0;
int numHits = 0; int numHits = 0;
@ -703,7 +703,7 @@ bool Bot::isPenetrableObstacle2 (const Vector &dest) {
} }
if (numHits < 3 && thikness < 98) { if (numHits < 3 && thikness < 98) {
if (dest.distanceSq (point) < cr::square (112.0f)) { if (dest.distanceSq (point) < cr::sqrf (112.0f)) {
return true; return true;
} }
} }
@ -724,7 +724,7 @@ bool Bot::isPenetrableObstacle3 (const Vector &dest) {
TraceResult tr {}; TraceResult tr {};
Vector source = getEyesPos (); Vector source = getEyesPos ();
const auto &dir = (dest - source).normalize () * 8.0f; const auto &dir = (dest - source).normalize_apx () * 8.0f;
for (;;) { for (;;) {
game.testLine (source, dest, TraceIgnore::Monsters, ent (), &tr); game.testLine (source, dest, TraceIgnore::Monsters, ent (), &tr);
@ -773,8 +773,8 @@ bool Bot::needToPauseFiring (float distance) {
else if (distance < kDoubleSprayDistance) { else if (distance < kDoubleSprayDistance) {
offset = 2.75f; offset = 2.75f;
} }
const float xPunch = cr::square (cr::deg2rad (pev->punchangle.x)); const float xPunch = cr::sqrf (cr::deg2rad (pev->punchangle.x));
const float yPunch = cr::square (cr::deg2rad (pev->punchangle.y)); const float yPunch = cr::sqrf (cr::deg2rad (pev->punchangle.y));
const float interval = m_frameInterval; const float interval = m_frameInterval;
const float tolerance = (100.0f - static_cast <float> (m_difficulty) * 25.0f) / 99.0f; const float tolerance = (100.0f - static_cast <float> (m_difficulty) * 25.0f) / 99.0f;
@ -1277,7 +1277,7 @@ void Bot::attackMovement () {
else if ((distance > kDoubleSprayDistance && hasPrimaryWeapon ()) && (m_enemyParts & (Visibility::Head | Visibility::Body)) && getCurrentTaskId () != Task::SeekCover && getCurrentTaskId () != Task::Hunt) { else if ((distance > kDoubleSprayDistance && hasPrimaryWeapon ()) && (m_enemyParts & (Visibility::Head | Visibility::Body)) && getCurrentTaskId () != Task::SeekCover && getCurrentTaskId () != Task::Hunt) {
int enemyNearestIndex = graph.getNearest (m_enemy->v.origin); int enemyNearestIndex = graph.getNearest (m_enemy->v.origin);
if (graph.isVisible (m_currentNodeIndex, enemyNearestIndex) && graph.isDuckVisible (m_currentNodeIndex, enemyNearestIndex) && graph.isDuckVisible (enemyNearestIndex, m_currentNodeIndex)) { if (vistab.visible (m_currentNodeIndex, enemyNearestIndex, VisIndex::Crouch) && vistab.visible (enemyNearestIndex, m_currentNodeIndex, VisIndex::Crouch)) {
m_duckTime = game.time () + m_frameInterval * 2.0f; m_duckTime = game.time () + m_frameInterval * 2.0f;
} }
} }
@ -1657,7 +1657,7 @@ bool Bot::isGroupOfEnemies (const Vector &location, int numEnemies, float radius
continue; continue;
} }
if (client.ent->v.origin.distanceSq (location) < cr::square (radius)) { if (client.ent->v.origin.distanceSq (location) < cr::sqrf (radius)) {
// don't target our teammates... // don't target our teammates...
if (client.team == m_team) { if (client.team == m_team) {
return false; return false;

View file

@ -58,14 +58,21 @@ int BotControl::cmdKickBot () {
} }
int BotControl::cmdKickBots () { int BotControl::cmdKickBots () {
enum args { alias = 1, instant }; enum args { alias = 1, instant, team };
// check if we're need to remove bots instantly // check if we're need to remove bots instantly
auto kickInstant = strValue (instant) == "instant"; auto kickInstant = strValue (instant) == "instant";
// kick the bots // if team is specified, kick from specified tram
if (strValue (alias).endsWith ("_ct") || intValue (team) == 2 || strValue (team) == "ct") {
bots.kickFromTeam (Team::CT, true);
}
else if (strValue (alias).endsWith ("_t") || intValue (team) == 1 || strValue (team) == "t") {
bots.kickFromTeam (Team::Terrorist, true);
}
else {
bots.kickEveryone (kickInstant); bots.kickEveryone (kickInstant);
}
return BotCommandResult::Handled; return BotCommandResult::Handled;
} }
@ -529,7 +536,7 @@ int BotControl::cmdNodeErase () {
// prevent accidents when graph are deleted unintentionally // prevent accidents when graph are deleted unintentionally
if (strValue (iamsure) == "iamsure") { if (strValue (iamsure) == "iamsure") {
graph.eraseFromDisk (); bstor.unlinkFromDisk ();
} }
else { else {
msg ("Please, append \"iamsure\" as parameter to get graph erased from the disk."); msg ("Please, append \"iamsure\" as parameter to get graph erased from the disk.");
@ -761,10 +768,16 @@ int BotControl::cmdNodeReleaseEditor () {
int BotControl::cmdNodeUpload () { int BotControl::cmdNodeUpload () {
enum args { graph_cmd = 1, cmd }; enum args { graph_cmd = 1, cmd };
// do not allow to upload analyzed graphs
if (graph.isAnalyzed ()) {
msg ("Sorry, unable to upload graph that was generated automatically.");
return BotCommandResult::Handled;
}
// do not allow to upload bad graph // do not allow to upload bad graph
if (!graph.checkNodes (false)) { if (!graph.checkNodes (false)) {
msg ("Sorry, unable to upload graph file that contains errors. Please type \"wp check\" to verify graph consistency."); msg ("Sorry, unable to upload graph file that contains errors. Please type \"wp check\" to verify graph consistency.");
return BotCommandResult::BadFormat; return BotCommandResult::Handled;
} }
msg ("\n"); msg ("\n");
@ -776,7 +789,7 @@ int BotControl::cmdNodeUpload () {
String uploadUrl = cv_graph_url_upload.str (); String uploadUrl = cv_graph_url_upload.str ();
// try to upload the file // try to upload the file
if (http.uploadFile (uploadUrl, util.buildPath (BotFile::Graph))) { if (http.uploadFile (uploadUrl, bstor.buildPath (BotFile::Graph))) {
msg ("Graph file was successfully validated and uploaded to the YaPB Graph DB (%s).", product.download); msg ("Graph file was successfully validated and uploaded to the YaPB Graph DB (%s).", product.download);
msg ("It will be available for download for all YaPB users in a few minutes."); msg ("It will be available for download for all YaPB users in a few minutes.");
msg ("\n"); msg ("\n");
@ -1717,7 +1730,7 @@ bool BotControl::executeCommands () {
if (m_args.empty ()) { if (m_args.empty ()) {
return false; return false;
} }
const auto &prefix = m_args[0]; const auto &prefix = m_args.first ();
// no handling if not for us // no handling if not for us
if (prefix != product.cmdPri && prefix != product.cmdSec) { if (prefix != product.cmdPri && prefix != product.cmdSec) {
@ -2055,7 +2068,7 @@ BotControl::BotControl () {
m_cmds.emplace ("add/addbot/add_ct/addbot_ct/add_t/addbot_t/addhs/addhs_t/addhs_ct", "add [difficulty] [personality] [team] [model] [name]", "Adding specific bot into the game.", &BotControl::cmdAddBot); m_cmds.emplace ("add/addbot/add_ct/addbot_ct/add_t/addbot_t/addhs/addhs_t/addhs_ct", "add [difficulty] [personality] [team] [model] [name]", "Adding specific bot into the game.", &BotControl::cmdAddBot);
m_cmds.emplace ("kick/kickone/kick_ct/kick_t/kickbot_ct/kickbot_t", "kick [team]", "Kicks off the random bot from the game.", &BotControl::cmdKickBot); m_cmds.emplace ("kick/kickone/kick_ct/kick_t/kickbot_ct/kickbot_t", "kick [team]", "Kicks off the random bot from the game.", &BotControl::cmdKickBot);
m_cmds.emplace ("removebots/kickbots/kickall", "removebots [instant]", "Kicks all the bots from the game.", &BotControl::cmdKickBots); m_cmds.emplace ("removebots/kickbots/kickall/kickall_ct/kickall_t", "removebots [instant] [team]", "Kicks all the bots from the game.", &BotControl::cmdKickBots);
m_cmds.emplace ("kill/killbots/killall/kill_ct/kill_t", "kill [team]", "Kills the specified team / all the bots.", &BotControl::cmdKillBots); m_cmds.emplace ("kill/killbots/killall/kill_ct/kill_t", "kill [team]", "Kills the specified team / all the bots.", &BotControl::cmdKillBots);
m_cmds.emplace ("fill/fillserver", "fill [team] [count] [difficulty] [personality]", "Fill the server (add bots) with specified parameters.", &BotControl::cmdFill); m_cmds.emplace ("fill/fillserver", "fill [team] [count] [difficulty] [personality]", "Fill the server (add bots) with specified parameters.", &BotControl::cmdFill);
m_cmds.emplace ("vote/votemap", "vote [map_id]", "Forces all the bot to vote to specified map.", &BotControl::cmdVote); m_cmds.emplace ("vote/votemap", "vote [map_id]", "Forces all the bot to vote to specified map.", &BotControl::cmdVote);

View file

@ -39,10 +39,7 @@ void Game::precache () {
m_engineWrap.precacheSound ("weapons/xbow_hit1.wav"); // waypoint add m_engineWrap.precacheSound ("weapons/xbow_hit1.wav"); // waypoint add
m_engineWrap.precacheSound ("weapons/mine_activate.wav"); // waypoint delete m_engineWrap.precacheSound ("weapons/mine_activate.wav"); // waypoint delete
m_engineWrap.precacheSound ("common/wpn_hudoff.wav"); // path add/delete start
m_engineWrap.precacheSound ("common/wpn_hudon.wav"); // path add/delete done m_engineWrap.precacheSound ("common/wpn_hudon.wav"); // path add/delete done
m_engineWrap.precacheSound ("common/wpn_moveselect.wav"); // path add/delete cancel
m_engineWrap.precacheSound ("common/wpn_denyselect.wav"); // path add/delete error
m_mapFlags = 0; // reset map type as worldspawn is the first entity spawned m_mapFlags = 0; // reset map type as worldspawn is the first entity spawned
@ -68,27 +65,27 @@ void Game::levelInitialize (edict_t *entities, int max) {
// update worldmodel // update worldmodel
illum.resetWorldModel (); illum.resetWorldModel ();
// do level initialization stuff here...
graph.loadGraphData ();
// execute main config // execute main config
conf.loadMainConfig (); conf.loadMainConfig ();
// load map-specific config // load map-specific config
conf.loadMapSpecificConfig (); conf.loadMapSpecificConfig ();
// do level initialization stuff here...
graph.loadGraphData ();
// initialize quota management // initialize quota management
bots.initQuota (); bots.initQuota ();
// rebuild vistable if needed
graph.rebuildVisibility ();
// install the sendto hook to fake queries // install the sendto hook to fake queries
util.installSendTo (); util.installSendTo ();
// flush any print queue // flush any print queue
ctrl.resetFlushTimestamp (); ctrl.resetFlushTimestamp ();
// suspend any analyzer tasks
analyzer.suspend ();
// go thru the all entities on map, and do whatever we're want // go thru the all entities on map, and do whatever we're want
for (int i = 0; i < max; ++i) { for (int i = 0; i < max; ++i) {
auto ent = entities + i; auto ent = entities + i;
@ -413,11 +410,11 @@ void Game::playSound (edict_t *ent, const char *sound) {
} }
void Game::setPlayerStartDrawModels () { void Game::setPlayerStartDrawModels () {
HashMap <String, String> models; static HashMap <String, String> models {
{"info_player_start", "models/player/urban/urban.mdl"},
models["info_player_start"] = "models/player/urban/urban.mdl"; {"info_player_deathmatch", "models/player/terror/terror.mdl"},
models["info_player_deathmatch"] = "models/player/terror/terror.mdl"; {"info_vip_start", "models/player/vip/vip.mdl"}
models["info_vip_start"] = "models/player/vip/vip.mdl"; };
models.foreach ([&] (const String &key, const String &val) { models.foreach ([&] (const String &key, const String &val) {
game.searchEntities ("classname", key, [&] (edict_t *ent) { game.searchEntities ("classname", key, [&] (edict_t *ent) {
@ -620,6 +617,7 @@ void Game::addNewCvar (const char *name, const char *value, const char *info, bo
reg.reg.name = const_cast <char *> (name); reg.reg.name = const_cast <char *> (name);
reg.reg.string = const_cast <char *> (value); reg.reg.string = const_cast <char *> (value);
reg.name = name;
reg.missing = missingAction; reg.missing = missingAction;
reg.init = value; reg.init = value;
reg.info = info; reg.info = info;
@ -654,6 +652,20 @@ void Game::addNewCvar (const char *name, const char *value, const char *info, bo
m_cvars.push (cr::move (reg)); m_cvars.push (cr::move (reg));
} }
void ConVar::revert () {
if (!ptr) {
return;
}
const auto &cvars = game.getCvars ();
for (const auto &var : cvars) {
if (var.name == ptr->name) {
set (var.initial);
break;
}
}
}
void Game::checkCvarsBounds () { void Game::checkCvarsBounds () {
for (const auto &var : m_cvars) { for (const auto &var : m_cvars) {
@ -669,14 +681,14 @@ void Game::checkCvarsBounds () {
continue; continue;
} }
auto value = var.self->float_ (); auto value = var.self->float_ ();
auto str = var.self->str (); auto str = String (var.self->str ());
// check the bounds and set default if out of bounds // check the bounds and set default if out of bounds
if (value > var.max || value < var.min || (!strings.isEmpty (str) && isalpha (*str))) { if (value > var.max || value < var.min || (!str.empty () && isalpha (str[0]))) {
var.self->set (var.initial); var.self->set (var.initial);
// notify about that // notify about that
ctrl.msg ("Bogus value for cvar '%s', min is '%.1f' and max is '%.1f', and we're got '%s', value reverted to default '%.1f'.", var.reg.name, var.min, var.max, str, var.initial); ctrl.msg ("Bogus value for cvar '%s', min is '%.1f' and max is '%.1f', and we're got '%s', value reverted to default '%.1f'.", var.name, var.min, var.max, str, var.initial);
} }
} }
@ -741,19 +753,7 @@ bool Game::loadCSBinary () {
if (!modname) { if (!modname) {
return false; return false;
} }
StringArray libs; StringArray libs { "mp", "cs", "cs_i386" };
if (plat.win) {
libs.push ("mp.dll");
libs.push ("cs.dll");
}
else if (plat.nix) {
libs.push ("cs.so");
libs.push ("cs_i386.so");
}
else if (plat.osx) {
libs.push ("cs.dylib");
}
auto libCheck = [&] (StringRef mod, StringRef dll) { auto libCheck = [&] (StringRef mod, StringRef dll) {
// try to load gamedll // try to load gamedll
@ -771,7 +771,7 @@ bool Game::loadCSBinary () {
// search the libraries inside game dlls directory // search the libraries inside game dlls directory
for (const auto &lib : libs) { for (const auto &lib : libs) {
auto path = strings.format ("%s/dlls/%s", modname, lib); auto path = strings.format ("%s/dlls/%s.%s", modname, lib, DLL_SUFFIX);
// if we can't read file, skip it // if we can't read file, skip it
if (!File::exists (path)) { if (!File::exists (path)) {
@ -834,7 +834,7 @@ bool Game::loadCSBinary () {
bool Game::postload () { bool Game::postload () {
// register logger // register logger
logger.initialize (util.buildPath (BotFile::LogFile), [] (const char *msg) { logger.initialize (bstor.buildPath (BotFile::LogFile), [] (const char *msg) {
game.print (msg); game.print (msg);
}); });

File diff suppressed because it is too large Load diff

View file

@ -332,8 +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 // 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. // the loading of new bots and the new BSP data parsing there.
// save collected experience on shutdown // save collected practice on shutdown
graph.savePractice (); practice.save ();
// destroy global killer entity // destroy global killer entity
bots.destroyKillerEntity (); bots.destroyKillerEntity ();
@ -384,9 +384,15 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int) {
graph.frame (); graph.frame ();
} }
// update analyzer if needed
analyzer.update ();
// run stuff periodically // run stuff periodically
game.slowFrame (); game.slowFrame ();
// rebuild vistable if needed
vistab.rebuild ();
if (bots.hasBotsOnline ()) { if (bots.hasBotsOnline ()) {
// keep track of grenades on map // keep track of grenades on map
bots.updateActiveGrenade (); bots.updateActiveGrenade ();
@ -903,8 +909,9 @@ CR_EXPORT int Meta_Detach (PLUG_LOADTIME now, PL_UNLOAD_REASON reason) {
} }
bots.kickEveryone (true); // kick all bots off this server bots.kickEveryone (true); // kick all bots off this server
// save collected experience on shutdown // save collected practice on shutdown
graph.savePractice (); practice.save ();
util.disableSendTo (); util.disableSendTo ();
// make sure all stuff cleared // make sure all stuff cleared

View file

@ -1251,7 +1251,6 @@ void Bot::newRound () {
// delete all allocated path nodes // delete all allocated path nodes
clearSearchNodes (); clearSearchNodes ();
clearRoute ();
m_pathOrigin = nullptr; m_pathOrigin = nullptr;
m_destOrigin = nullptr; m_destOrigin = nullptr;
@ -1688,16 +1687,21 @@ void BotManager::notifyBombDefuse () {
if (!isBombPlanted ()) { if (!isBombPlanted ()) {
return; return;
} }
auto bombPos = graph.getBombOrigin ();
for (const auto &bot : bots) { for (const auto &bot : bots) {
if (bot->m_team == Team::Terrorist && bot->m_notKilled && bot->getCurrentTaskId () != Task::MoveToPosition) { auto task = bot->getCurrentTaskId ();
if (bot->m_notKilled && task != Task::MoveToPosition && task != Task::DefuseBomb && task != Task::EscapeFromBomb) {
if (bot->m_team == Team::CT || bot->pev->origin.distanceSq (bombPos) < cr::sqrf (384.0f)) {
bot->clearSearchNodes (); bot->clearSearchNodes ();
bot->m_position = graph.getBombOrigin (); bot->m_position = bombPos;
bot->startTask (Task::MoveToPosition, TaskPri::MoveToPosition, kInvalidNodeIndex, 0.0f, true); bot->startTask (Task::MoveToPosition, TaskPri::MoveToPosition, kInvalidNodeIndex, 0.0f, true);
} }
} }
} }
}
void BotManager::updateActiveGrenade () { void BotManager::updateActiveGrenade () {
if (m_grenadeUpdateTime > game.time ()) { if (m_grenadeUpdateTime > game.time ()) {
@ -1891,7 +1895,7 @@ void BotManager::initRound () {
m_botsCanPause = false; m_botsCanPause = false;
resetFilters (); resetFilters ();
graph.updateGlobalPractice (); // update experience data on round start practice.update (); // update practice data on round start
// calculate the round mid/end in world time // calculate the round mid/end in world time
m_timeRoundStart = game.time () + mp_freezetime.float_ (); m_timeRoundStart = game.time () + mp_freezetime.float_ ();

View file

@ -371,7 +371,7 @@ void MessageDispatcher::netMsgBarTime () {
m_bot->m_hasProgressBar = true; // the progress bar on a hud m_bot->m_hasProgressBar = true; // the progress bar on a hud
// notify bots about defusing has started // notify bots about defusing has started
if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted () && m_bot->m_team == Team::CT) { if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted ()) {
bots.notifyBombDefuse (); bots.notifyBombDefuse ();
} }
} }

View file

@ -7,10 +7,6 @@
#include <yapb.h> #include <yapb.h>
ConVar cv_path_heuristic_mode ("yb_path_heuristic_mode", "4", "Selects the heuristic function mode. For debug purposes only.", true, 0.0f, 4.0f);
ConVar cv_path_danger_factor_min ("yb_path_danger_factor_min", "200", "Lower bound of danger factor that used to add additional danger to path based on practice.", true, 100.0f, 2400.0f);
ConVar cv_path_danger_factor_max ("yb_path_danger_factor_max", "400", "Upper bound of danger factor that used to add additional danger to path based on practice.", true, 200.0f, 4800.0f);
int Bot::findBestGoal () { int Bot::findBestGoal () {
auto pushToHistroy = [&] (int32_t goal) -> int32_t { auto pushToHistroy = [&] (int32_t goal) -> int32_t {
m_nodeHistory.push (goal); m_nodeHistory.push (goal);
@ -233,7 +229,7 @@ int Bot::findGoalPost (int tactic, IntArray *defensive, IntArray *offsensive) {
for (auto &point : graph.m_goalPoints) { for (auto &point : graph.m_goalPoints) {
float distance = graph[point].origin.distanceSq (pev->origin); float distance = graph[point].origin.distanceSq (pev->origin);
if (distance > cr::square (1024.0f)) { if (distance > cr::sqrf (1024.0f)) {
continue; continue;
} }
if (distance < minDist) { if (distance < minDist) {
@ -299,7 +295,7 @@ int Bot::findGoalPost (int tactic, IntArray *defensive, IntArray *offsensive) {
break; break;
} }
if (graph.getDangerValue (m_team, m_currentNodeIndex, goalChoices[i]) < graph.getDangerValue (m_team, m_currentNodeIndex, goalChoices[i + 1])) { if (practice.getValue (m_team, m_currentNodeIndex, goalChoices[i]) < practice.getValue (m_team, m_currentNodeIndex, goalChoices[i + 1])) {
cr::swap (goalChoices[i + 1], goalChoices[i]); cr::swap (goalChoices[i + 1], goalChoices[i]);
sorting = true; sorting = true;
} }
@ -368,7 +364,7 @@ void Bot::ignoreCollision () {
void Bot::doPlayerAvoidance (const Vector &normal) { void Bot::doPlayerAvoidance (const Vector &normal) {
m_hindrance = nullptr; m_hindrance = nullptr;
float distance = cr::square (348.0f); float distance = cr::sqrf (348.0f);
if (getCurrentTaskId () == Task::Attack || isOnLadder ()) { if (getCurrentTaskId () == Task::Attack || isOnLadder ()) {
return; return;
@ -400,7 +396,7 @@ void Bot::doPlayerAvoidance (const Vector &normal) {
} }
auto nearest = client.ent->v.origin.distanceSq (pev->origin); auto nearest = client.ent->v.origin.distanceSq (pev->origin);
if (nearest < cr::square (pev->maxspeed) && nearest < distance) { if (nearest < cr::sqrf (pev->maxspeed) && nearest < distance) {
m_hindrance = client.ent; m_hindrance = client.ent;
distance = nearest; distance = nearest;
} }
@ -425,19 +421,19 @@ void Bot::doPlayerAvoidance (const Vector &normal) {
auto nextFrameDistance = pev->origin.distanceSq (m_hindrance->v.origin + m_hindrance->v.velocity * interval); auto nextFrameDistance = pev->origin.distanceSq (m_hindrance->v.origin + m_hindrance->v.velocity * interval);
// is player that near now or in future that we need to steer away? // is player that near now or in future that we need to steer away?
if (movedDistance <= cr::square (48.0f) || (distance <= cr::square (56.0f) && nextFrameDistance < distance)) { if (movedDistance <= cr::sqrf (48.0f) || (distance <= cr::sqrf (56.0f) && nextFrameDistance < distance)) {
auto dir = (pev->origin - m_hindrance->v.origin).normalize2d (); auto dir = (pev->origin - m_hindrance->v.origin).normalize2d_apx ();
// to start strafing, we have to first figure out if the target is on the left side or right side // to start strafing, we have to first figure out if the target is on the left side or right side
if ((dir | right.normalize2d ()) > 0.0f) { if ((dir | right.normalize2d_apx ()) > 0.0f) {
setStrafeSpeed (normal, pev->maxspeed); setStrafeSpeed (normal, pev->maxspeed);
} }
else { else {
setStrafeSpeed (normal, -pev->maxspeed); setStrafeSpeed (normal, -pev->maxspeed);
} }
if (distance < cr::square (76.0f)) { if (distance < cr::sqrf (76.0f)) {
if ((dir | forward.normalize2d ()) < 0.0f) { if ((dir | forward.normalize2d_apx ()) < 0.0f) {
m_moveSpeed = -pev->maxspeed; m_moveSpeed = -pev->maxspeed;
} }
} }
@ -495,7 +491,7 @@ void Bot::checkTerrain (float movedDistance, const Vector &dirNormal) {
} }
// bot is stuc, but not yet decided what to do? // bot is stuck, but not yet decided what to do?
if (m_collisionState == CollisionState::Undecided) { if (m_collisionState == CollisionState::Undecided) {
uint32_t bits = 0; uint32_t bits = 0;
@ -541,19 +537,20 @@ void Bot::checkTerrain (float movedDistance, const Vector &dirNormal) {
else { else {
dirLeft = true; dirLeft = true;
} }
const Vector &testDir = m_moveSpeed > 0.0f ? forward : -forward; const auto &testDir = m_moveSpeed > 0.0f ? forward : -forward;
constexpr float blockDistance = 56.0f;
// now check which side is blocked // now check which side is blocked
src = pev->origin + right * 32.0f; src = pev->origin + right * blockDistance;
dst = src + testDir * 32.0f; dst = src + testDir * blockDistance;
game.testHull (src, dst, TraceIgnore::Monsters, head_hull, ent (), &tr); game.testHull (src, dst, TraceIgnore::Monsters, head_hull, ent (), &tr);
if (!cr::fequal (tr.flFraction, 1.0f)) { if (!cr::fequal (tr.flFraction, 1.0f)) {
blockedRight = true; blockedRight = true;
} }
src = pev->origin - right * 32.0f; src = pev->origin - right * blockDistance;
dst = src + testDir * 32.0f; dst = src + testDir * blockDistance;
game.testHull (src, dst, TraceIgnore::Monsters, head_hull, ent (), &tr); game.testHull (src, dst, TraceIgnore::Monsters, head_hull, ent (), &tr);
@ -1057,7 +1054,7 @@ bool Bot::updateNavigation () {
} }
// needs precise placement - check if we get past the point // needs precise placement - check if we get past the point
if (desiredDistance < 22.0f && nodeDistance < 30.0f && m_pathOrigin.distanceSq (pev->origin + pev->velocity * m_frameInterval) >= cr::square (nodeDistance)) { if (desiredDistance < 22.0f && nodeDistance < 30.0f && m_pathOrigin.distanceSq (pev->origin + pev->velocity * m_frameInterval) >= cr::sqrf (nodeDistance)) {
desiredDistance = nodeDistance + 1.0f; desiredDistance = nodeDistance + 1.0f;
} }
@ -1078,15 +1075,16 @@ bool Bot::updateNavigation () {
// did we reach a destination node? // did we reach a destination node?
if (getTask ()->data == m_currentNodeIndex) { if (getTask ()->data == m_currentNodeIndex) {
if (m_chosenGoalIndex != kInvalidNodeIndex) { if (m_chosenGoalIndex != kInvalidNodeIndex) {
constexpr int maxGoalValue = PracticeLimit::Goal;
// add goal values // add goal values
int goalValue = graph.getDangerValue (m_team, m_chosenGoalIndex, m_currentNodeIndex); int goalValue = practice.getValue (m_team, m_chosenGoalIndex, m_currentNodeIndex);
int addedValue = static_cast <int> (m_healthValue * 0.5f + m_goalValue * 0.5f); int addedValue = static_cast <int> (m_healthValue * 0.5f + m_goalValue * 0.5f);
goalValue = cr::clamp (goalValue + addedValue, -kMaxPracticeGoalValue, kMaxPracticeGoalValue); goalValue = cr::clamp (goalValue + addedValue, -maxGoalValue, maxGoalValue);
// update the practice for team // update the practice for team
graph.setDangerValue (m_team, m_chosenGoalIndex, m_currentNodeIndex, goalValue); practice.setValue (m_team, m_chosenGoalIndex, m_currentNodeIndex, goalValue);
} }
return true; return true;
} }
@ -1193,7 +1191,7 @@ bool Bot::updateLiftHandling () {
m_destOrigin = m_liftTravelPos; m_destOrigin = m_liftTravelPos;
// check if we enough to destination // check if we enough to destination
if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) { if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) {
wait (); wait ();
// need to wait our following teammate ? // need to wait our following teammate ?
@ -1247,7 +1245,7 @@ bool Bot::updateLiftHandling () {
if (needWaitForTeammate) { if (needWaitForTeammate) {
m_destOrigin = m_liftTravelPos; m_destOrigin = m_liftTravelPos;
if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) { if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) {
wait (); wait ();
} }
} }
@ -1278,7 +1276,7 @@ bool Bot::updateLiftHandling () {
m_liftState = LiftState::TravelingBy; m_liftState = LiftState::TravelingBy;
m_liftUsageTime = game.time () + 14.0f; m_liftUsageTime = game.time () + 14.0f;
if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) { if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) {
wait (); wait ();
} }
} }
@ -1288,7 +1286,7 @@ bool Bot::updateLiftHandling () {
if (m_liftState == LiftState::TravelingBy) { if (m_liftState == LiftState::TravelingBy) {
m_destOrigin = Vector (m_liftTravelPos.x, m_liftTravelPos.y, pev->origin.z); m_destOrigin = Vector (m_liftTravelPos.x, m_liftTravelPos.y, pev->origin.z);
if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) { if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) {
wait (); wait ();
} }
} }
@ -1305,7 +1303,7 @@ bool Bot::updateLiftHandling () {
m_destOrigin = pev->origin; m_destOrigin = pev->origin;
} }
if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) { if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) {
wait (); wait ();
} }
} }
@ -1338,7 +1336,7 @@ bool Bot::updateLiftHandling () {
m_destOrigin = button->v.origin; m_destOrigin = button->v.origin;
} }
if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) { if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) {
wait (); wait ();
} }
} }
@ -1369,7 +1367,7 @@ bool Bot::updateLiftHandling () {
} }
} }
if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) { if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) {
wait (); wait ();
} }
} }
@ -1433,344 +1431,15 @@ bool Bot::updateLiftStates () {
return true; return true;
} }
void Bot::findShortestPath (int srcIndex, int destIndex) {
// this function finds the shortest path from source index to destination index
if (!graph.exists (srcIndex)) {
logger.error ("%s source path index not valid (%d).", __FUNCTION__, srcIndex);
return;
}
else if (!graph.exists (destIndex)) {
logger.error ("%s destination path index not valid (%d).", __FUNCTION__, destIndex);
return;
}
clearSearchNodes ();
m_chosenGoalIndex = srcIndex;
m_goalValue = 0.0f;
m_pathWalk.add (srcIndex);
while (srcIndex != destIndex) {
srcIndex = (graph.m_matrix.data () + (srcIndex * graph.length ()) + destIndex)->index;
if (srcIndex < 0) {
m_prevGoalIndex = kInvalidNodeIndex;
getTask ()->data = kInvalidNodeIndex;
return;
}
m_pathWalk.add (srcIndex);
}
}
void Bot::findPath (int srcIndex, int destIndex, FindPath pathType /*= FindPath::Fast */) {
// this function finds a path from srcIndex to destIndex
auto dangerFactor = [&] () -> float {
return rg.get (cv_path_danger_factor_min.float_ (), cv_path_danger_factor_max.float_ ()) * 2.0f / cr::clamp (static_cast <float> (m_difficulty), 1.0f, 4.0f);
};
// least kills and number of nodes to goal for a team
auto gfunctionKillsDist = [&dangerFactor] (int team, int currentIndex, int parentIndex) -> float {
if (parentIndex == kInvalidNodeIndex) {
return 0.0f;
}
auto cost = static_cast <float> (graph.getDangerDamage (team, currentIndex, currentIndex) + graph.getHighestDamageForTeam (team));
const auto &current = graph[currentIndex];
for (auto &neighbour : current.links) {
if (neighbour.index != kInvalidNodeIndex) {
cost += static_cast <float> (graph.getDangerDamage (team, neighbour.index, neighbour.index));
}
}
if (current.flags & NodeFlag::Crouch) {
cost *= 1.5f;
}
return cost + dangerFactor ();
};
// least kills and number of nodes to goal for a team
auto gfunctionKillsDistCTWithHostage = [&gfunctionKillsDist] (int team, int currentIndex, int parentIndex) -> float {
const auto &current = graph[currentIndex];
if (current.flags & NodeFlag::NoHostage) {
return 65355.0f;
}
else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) {
return gfunctionKillsDist (team, currentIndex, parentIndex) * 500.0f;
}
return gfunctionKillsDist (team, currentIndex, parentIndex);
};
// least kills to goal for a team
auto gfunctionKills = [&dangerFactor] (int team, int currentIndex, int) -> float {
auto cost = static_cast <float> (graph.getDangerDamage (team, currentIndex, currentIndex));
const auto &current = graph[currentIndex];
for (auto &neighbour : current.links) {
if (neighbour.index != kInvalidNodeIndex) {
cost += static_cast <float> (graph.getDangerDamage (team, neighbour.index, neighbour.index));
}
}
if (current.flags & NodeFlag::Crouch) {
cost *= 1.5f;
}
return cost + dangerFactor () + 1.0f;
};
// least kills to goal for a team
auto gfunctionKillsCTWithHostage = [&gfunctionKills] (int team, int currentIndex, int parentIndex) -> float {
if (parentIndex == kInvalidNodeIndex) {
return 0.0f;
}
const auto &current = graph[currentIndex];
if (current.flags & NodeFlag::NoHostage) {
return 65355.0f;
}
else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) {
return gfunctionKills (team, currentIndex, parentIndex) * 500.0f;
}
return gfunctionKills (team, currentIndex, parentIndex);
};
auto gfunctionPathDist = [] (int, int currentIndex, int parentIndex) -> float {
if (parentIndex == kInvalidNodeIndex) {
return 0.0f;
}
const auto &parent = graph[parentIndex];
const auto &current = graph[currentIndex];
for (const auto &link : parent.links) {
if (link.index == currentIndex) {
const auto distance = static_cast <float> (link.distance);
// we don't like ladder or crouch point
if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) {
return distance * 1.5f;
}
return distance;
}
}
return 65355.0f;
};
auto gfunctionPathDistWithHostage = [&gfunctionPathDist] (int, int currentIndex, int parentIndex) -> float {
const auto &current = graph[currentIndex];
if (current.flags & NodeFlag::NoHostage) {
return 65355.0f;
}
else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) {
return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex) * 500.0f;
}
return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex);
};
// square distance heuristic
auto hfunctionPathDist = [] (int index, int, int goalIndex) -> float {
const auto &start = graph[index];
const auto &goal = graph[goalIndex];
float x = cr::abs (start.origin.x - goal.origin.x);
float y = cr::abs (start.origin.y - goal.origin.y);
float z = cr::abs (start.origin.z - goal.origin.z);
switch (cv_path_heuristic_mode.int_ ()) {
case 0:
default:
return cr::max (cr::max (x, y), z); // chebyshev distance
case 1:
return x + y + z; // manhattan distance
case 2:
return 0.0f; // no heuristic
case 3:
case 4:
// euclidean based distance
float euclidean = cr::powf (cr::powf (x, 2.0f) + cr::powf (y, 2.0f) + cr::powf (z, 2.0f), 0.5f);
if (cv_path_heuristic_mode.int_ () == 4) {
return 1000.0f * (cr::ceilf (euclidean) - euclidean);
}
return euclidean;
}
};
// square distance heuristic with hostages
auto hfunctionPathDistWithHostage = [&hfunctionPathDist] (int index, int, int goalIndex) -> float {
if (graph[index].flags & NodeFlag::NoHostage) {
return 65355.0f;
}
return hfunctionPathDist (index, kInvalidNodeIndex, goalIndex);
};
// none heuristic
auto hfunctionNone = [&hfunctionPathDist] (int index, int, int goalIndex) -> float {
return hfunctionPathDist (index, kInvalidNodeIndex, goalIndex) / (128.0f * 10.0f);
};
if (!graph.exists (srcIndex)) {
logger.error ("%s source path index not valid (%d).", __FUNCTION__, srcIndex);
return;
}
else if (!graph.exists (destIndex)) {
logger.error ("%s destination path index not valid (%d).", __FUNCTION__, destIndex);
return;
}
// astar needs some nodes to work with
if (graph.length () < kMaxNodeLinks) {
logger.error ("A* Search for bot \"%s\" has failed due to too small number of nodes (%d). Seems to be graph is broken.", pev->netname.chars (), graph.length ());
return;
}
// holders for heuristic functions
static Lambda <float (int, int, int)> gcalc, hcalc;
// get correct calculation for heuristic
if (pathType == FindPath::Optimal) {
if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) {
hcalc = hfunctionPathDistWithHostage;
gcalc = gfunctionKillsDistCTWithHostage;
}
else {
hcalc = hfunctionPathDist;
gcalc = gfunctionKillsDist;
}
}
else if (pathType == FindPath::Safe) {
if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) {
hcalc = hfunctionNone;
gcalc = gfunctionKillsCTWithHostage;
}
else {
hcalc = hfunctionNone;
gcalc = gfunctionKills;
}
}
else {
if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) {
hcalc = hfunctionPathDistWithHostage;
gcalc = gfunctionPathDistWithHostage;
}
else {
hcalc = hfunctionPathDist;
gcalc = gfunctionPathDist;
}
}
clearSearchNodes ();
m_chosenGoalIndex = srcIndex;
m_goalValue = 0.0f;
clearRoute ();
auto srcRoute = &m_routes[srcIndex];
// put start node into open list
srcRoute->g = gcalc (m_team, srcIndex, kInvalidNodeIndex);
srcRoute->f = srcRoute->g + hcalc (srcIndex, kInvalidNodeIndex, destIndex);
srcRoute->state = RouteState::Open;
m_routeQue.clear ();
m_routeQue.emplace (srcIndex, srcRoute->g);
while (!m_routeQue.empty ()) {
// remove the first node from the open list
int currentIndex = m_routeQue.pop ().first;
// safes us from bad graph...
if (m_routeQue.length () >= graph.getMaxRouteLength () - 1) {
logger.error ("A* Search for bot \"%s\" has tried to build path with at least %d nodes. Seems to be graph is broken.", pev->netname.chars (), m_routeQue.length ());
kick (); // kick the bot off...
return;
}
// is the current node the goal node?
if (currentIndex == destIndex) {
// build the complete path
do {
m_pathWalk.add (currentIndex);
currentIndex = m_routes[currentIndex].parent;
} while (currentIndex != kInvalidNodeIndex);
// reverse path for path follower
m_pathWalk.reverse ();
return;
}
auto curRoute = &m_routes[currentIndex];
if (curRoute->state != RouteState::Open) {
continue;
}
// put current node into CLOSED list
curRoute->state = RouteState::Closed;
// now expand the current node
for (const auto &child : graph[currentIndex].links) {
if (child.index == kInvalidNodeIndex) {
continue;
}
auto childRoute = &m_routes[child.index];
// calculate the F value as F = G + H
const float g = curRoute->g + gcalc (m_team, child.index, currentIndex);
const float h = hcalc (child.index, kInvalidNodeIndex, destIndex);
const float f = g + h;
if (childRoute->state == RouteState::New || childRoute->f > f) {
// put the current child into open list
childRoute->parent = currentIndex;
childRoute->state = RouteState::Open;
childRoute->g = g;
childRoute->f = f;
m_routeQue.emplace (child.index, g);
}
}
}
logger.error ("A* Search for bot \"%s\" has failed. Falling back to shortest-path algorithm. Seems to be graph is broken.", pev->netname.chars ());
// fallback to shortest path
findShortestPath (srcIndex, destIndex); // A* found no path, try floyd pathfinder instead
}
void Bot::clearSearchNodes () { void Bot::clearSearchNodes () {
m_pathWalk.clear (); m_pathWalk.clear ();
m_chosenGoalIndex = kInvalidNodeIndex; m_chosenGoalIndex = kInvalidNodeIndex;
} }
void Bot::clearRoute () {
m_routes.resize (static_cast <size_t> (graph.length ()));
for (const auto &path : graph) {
auto route = &m_routes[path.number];
route->g = route->f = 0.0f;
route->parent = kInvalidNodeIndex;
route->state = RouteState::New;
}
m_routes.clear ();
}
int Bot::findAimingNode (const Vector &to, int &pathLength) { int Bot::findAimingNode (const Vector &to, int &pathLength) {
// return the most distant node which is seen from the bot to the target and is within count // return the most distant node which is seen from the bot to the target and is within count
if (m_currentNodeIndex == kInvalidNodeIndex) { ensureCurrentNodeIndex ();
changeNodeIndex (findNearestNode ());
}
int destIndex = graph.getNearest (to); int destIndex = graph.getNearest (to);
int bestIndex = m_currentNodeIndex; int bestIndex = m_currentNodeIndex;
@ -1779,19 +1448,15 @@ int Bot::findAimingNode (const Vector &to, int &pathLength) {
return kInvalidNodeIndex; return kInvalidNodeIndex;
} }
while (destIndex != m_currentNodeIndex) { planner.find (destIndex, m_currentNodeIndex, [&] (int index) {
destIndex = (graph.m_matrix.data () + (destIndex * graph.length ()) + m_currentNodeIndex)->index;
if (destIndex < 0) {
break;
}
++pathLength; ++pathLength;
if (graph.isVisible (m_currentNodeIndex, destIndex)) { if (vistab.visible (m_currentNodeIndex, index)) {
bestIndex = destIndex; bestIndex = index;
break; return false;
}
} }
return true;
});
if (bestIndex == m_currentNodeIndex) { if (bestIndex == m_currentNodeIndex) {
return kInvalidNodeIndex; return kInvalidNodeIndex;
@ -1920,7 +1585,7 @@ float Bot::getEstimatedNodeReachTime () {
float distance = graph[m_previousNodes[0]].origin.distanceSq (graph[m_currentNodeIndex].origin); float distance = graph[m_previousNodes[0]].origin.distanceSq (graph[m_currentNodeIndex].origin);
// caclulate estimated time // caclulate estimated time
estimatedTime = 5.0f * (distance / cr::square (m_moveSpeed + 1.0f)); estimatedTime = 5.0f * (distance / cr::sqrf (m_moveSpeed + 1.0f));
bool longTermReachability = (m_pathFlags & NodeFlag::Crouch) || (m_pathFlags & NodeFlag::Ladder) || (pev->button & IN_DUCK) || (m_oldButtons & IN_DUCK); bool longTermReachability = (m_pathFlags & NodeFlag::Crouch) || (m_pathFlags & NodeFlag::Ladder) || (pev->button & IN_DUCK) || (m_oldButtons & IN_DUCK);
@ -1942,21 +1607,23 @@ void Bot::findValidNode () {
findNextBestNode (); findNextBestNode ();
} }
else if (m_navTimeset + getEstimatedNodeReachTime () < game.time ()) { else if (m_navTimeset + getEstimatedNodeReachTime () < game.time ()) {
constexpr int maxDamageValue = PracticeLimit::Damage;
// increase danager for both teams // increase danager for both teams
for (int team = Team::Terrorist; team < kGameTeamNum; ++team) { for (int team = Team::Terrorist; team < kGameTeamNum; ++team) {
int damageValue = graph.getDangerDamage (team, m_currentNodeIndex, m_currentNodeIndex); int damageValue = practice.getDamage (team, m_currentNodeIndex, m_currentNodeIndex);
damageValue = cr::clamp (damageValue + 100, 0, kMaxPracticeDamageValue); damageValue = cr::clamp (damageValue + 100, 0, maxDamageValue);
// affect nearby connected with victim nodes // affect nearby connected with victim nodes
for (auto &neighbour : m_path->links) { for (auto &neighbour : m_path->links) {
if (graph.exists (neighbour.index)) { if (graph.exists (neighbour.index)) {
int neighbourValue = graph.getDangerDamage (team, neighbour.index, neighbour.index); int neighbourValue = practice.getDamage (team, neighbour.index, neighbour.index);
neighbourValue = cr::clamp (neighbourValue + 100, 0, kMaxPracticeDamageValue); neighbourValue = cr::clamp (neighbourValue + 100, 0, maxDamageValue);
graph.setDangerDamage (m_team, neighbour.index, neighbour.index, neighbourValue); practice.setDamage (m_team, neighbour.index, neighbour.index, neighbourValue);
} }
} }
graph.setDangerDamage (m_team, m_currentNodeIndex, m_currentNodeIndex, damageValue); practice.setDamage (m_team, m_currentNodeIndex, m_currentNodeIndex, damageValue);
} }
clearSearchNodes (); clearSearchNodes ();
findNextBestNode (); findNextBestNode ();
@ -1990,7 +1657,7 @@ int Bot::findNearestNode () {
constexpr float kMaxDistance = 1024.0f; constexpr float kMaxDistance = 1024.0f;
int index = kInvalidNodeIndex; int index = kInvalidNodeIndex;
float minimumDistance = cr::square (kMaxDistance); float minimumDistance = cr::sqrf (kMaxDistance);
const auto &origin = pev->origin + pev->velocity * m_frameInterval; const auto &origin = pev->origin + pev->velocity * m_frameInterval;
const auto &bucket = graph.getNodesInBucket (origin); const auto &bucket = graph.getNodesInBucket (origin);
@ -2005,7 +1672,7 @@ int Bot::findNearestNode () {
if (distance < minimumDistance) { if (distance < minimumDistance) {
// if bot doing navigation, make sure node really visible and reacheable // if bot doing navigation, make sure node really visible and reacheable
if (graph.isVisible (m_currentNodeIndex, path.number) && isReachableNode (path.number)) { if (vistab.visible (m_currentNodeIndex, path.number) && isReachableNode (path.number)) {
index = path.number; index = path.number;
minimumDistance = distance; minimumDistance = distance;
} }
@ -2014,7 +1681,7 @@ int Bot::findNearestNode () {
// try to search ANYTHING that can be reachaed // try to search ANYTHING that can be reachaed
if (index == kInvalidNodeIndex) { if (index == kInvalidNodeIndex) {
minimumDistance = cr::square (kMaxDistance); minimumDistance = cr::sqrf (kMaxDistance);
const auto &nearestNodes = graph.getNarestInRadius (kMaxDistance, pev->origin); const auto &nearestNodes = graph.getNarestInRadius (kMaxDistance, pev->origin);
for (const auto &i : nearestNodes) { for (const auto &i : nearestNodes) {
@ -2059,7 +1726,7 @@ int Bot::findBombNode () {
const auto &audible = isBombAudible (); const auto &audible = isBombAudible ();
// take the nearest to bomb nodes instead of goal if close enough // take the nearest to bomb nodes instead of goal if close enough
if (pev->origin.distanceSq (bomb) < cr::square (96.0f)) { if (pev->origin.distanceSq (bomb) < cr::sqrf (96.0f)) {
int node = graph.getNearest (bomb, 420.0f); int node = graph.getNearest (bomb, 420.0f);
m_bombSearchOverridden = true; m_bombSearchOverridden = true;
@ -2105,9 +1772,7 @@ int Bot::findDefendNode (const Vector &origin) {
// this function tries to find a good position which has a line of sight to a position, // this function tries to find a good position which has a line of sight to a position,
// provides enough cover point, and is far away from the defending position // provides enough cover point, and is far away from the defending position
if (m_currentNodeIndex == kInvalidNodeIndex) { ensureCurrentNodeIndex ();
changeNodeIndex (findNearestNode ());
}
TraceResult tr {}; TraceResult tr {};
int nodeIndex[kMaxNodeLinks] {}; int nodeIndex[kMaxNodeLinks] {};
@ -2132,12 +1797,12 @@ int Bot::findDefendNode (const Vector &origin) {
// find the best node now // find the best node now
for (const auto &path : graph) { for (const auto &path : graph) {
// exclude ladder & current nodes // exclude ladder & current nodes
if ((path.flags & NodeFlag::Ladder) || path.number == srcIndex || !graph.isVisible (path.number, posIndex)) { if ((path.flags & NodeFlag::Ladder) || path.number == srcIndex || !vistab.visible (path.number, posIndex)) {
continue; continue;
} }
// use the 'real' pathfinding distances // use the 'real' pathfinding distances
int distance = graph.getPathDist (srcIndex, path.number); int distance = planner.dist (srcIndex, path.number);
// skip wayponts too far // skip wayponts too far
if (distance > kMaxDistance) { if (distance > kMaxDistance) {
@ -2192,11 +1857,11 @@ int Bot::findDefendNode (const Vector &origin) {
// use statistic if we have them // use statistic if we have them
for (int i = 0; i < kMaxNodeLinks; ++i) { for (int i = 0; i < kMaxNodeLinks; ++i) {
if (nodeIndex[i] != kInvalidNodeIndex) { if (nodeIndex[i] != kInvalidNodeIndex) {
int practice = graph.getDangerDamage (m_team, nodeIndex[i], nodeIndex[i]); int practiceDamage = practice.getDamage (m_team, nodeIndex[i], nodeIndex[i]);
practice = (practice * 100) / graph.getHighestDamageForTeam (m_team); practiceDamage = (practiceDamage * 100) / practice.getHighestDamageForTeam (m_team);
minDistance[i] = (practice * 100) / 8192; minDistance[i] = (practiceDamage * 100) / 8192;
minDistance[i] += practice; minDistance[i] += practiceDamage;
} }
} }
bool sorting = false; bool sorting = false;
@ -2220,7 +1885,7 @@ int Bot::findDefendNode (const Vector &origin) {
IntArray found; IntArray found;
for (const auto &path : graph) { for (const auto &path : graph) {
if (origin.distanceSq (path.origin) < cr::square (static_cast <float> (kMaxDistance)) && graph.isVisible (path.number, posIndex) && !isOccupiedNode (path.number)) { if (origin.distanceSq (path.origin) < cr::sqrf (static_cast <float> (kMaxDistance)) && vistab.visible (path.number, posIndex) && !isOccupiedNode (path.number)) {
found.push (path.number); found.push (path.number);
} }
} }
@ -2284,13 +1949,13 @@ int Bot::findCoverNode (float maxDistance) {
// find the best node now // find the best node now
for (const auto &path : graph) { for (const auto &path : graph) {
// exclude ladder, current node and nodes seen by the enemy // exclude ladder, current node and nodes seen by the enemy
if ((path.flags & NodeFlag::Ladder) || path.number == srcIndex || graph.isVisible (enemyIndex, path.number)) { if ((path.flags & NodeFlag::Ladder) || path.number == srcIndex || vistab.visible (enemyIndex, path.number)) {
continue; continue;
} }
bool neighbourVisible = false; // now check neighbour nodes for visibility bool neighbourVisible = false; // now check neighbour nodes for visibility
for (auto &enemy : enemies) { for (auto &enemy : enemies) {
if (graph.isVisible (enemy, path.number)) { if (vistab.visible (enemy, path.number)) {
neighbourVisible = true; neighbourVisible = true;
break; break;
} }
@ -2302,8 +1967,8 @@ int Bot::findCoverNode (float maxDistance) {
} }
// use the 'real' pathfinding distances // use the 'real' pathfinding distances
int distance = graph.getPathDist (srcIndex, path.number); int distance = planner.dist (srcIndex, path.number);
int enemyDistance = graph.getPathDist (enemyIndex, path.number); int enemyDistance = planner.dist (enemyIndex, path.number);
if (distance >= enemyDistance) { if (distance >= enemyDistance) {
continue; continue;
@ -2346,11 +2011,11 @@ int Bot::findCoverNode (float maxDistance) {
// use statistic if we have them // use statistic if we have them
for (int i = 0; i < kMaxNodeLinks; ++i) { for (int i = 0; i < kMaxNodeLinks; ++i) {
if (nodeIndex[i] != kInvalidNodeIndex) { if (nodeIndex[i] != kInvalidNodeIndex) {
int practice = graph.getDangerDamage (m_team, nodeIndex[i], nodeIndex[i]); int practiceDamage = practice.getDamage (m_team, nodeIndex[i], nodeIndex[i]);
practice = (practice * 100) / graph.getHighestDamageForTeam (m_team); practiceDamage = (practiceDamage * 100) / practice.getHighestDamageForTeam (m_team);
minDistance[i] = (practice * 100) / 8192; minDistance[i] = (practiceDamage * 100) / 8192;
minDistance[i] += practice; minDistance[i] += practiceDamage;
} }
} }
bool sorting = false; bool sorting = false;
@ -2396,23 +2061,36 @@ bool Bot::selectBestNextNode () {
assert (!m_pathWalk.empty ()); assert (!m_pathWalk.empty ());
assert (m_pathWalk.hasNext ()); assert (m_pathWalk.hasNext ());
if (!isOccupiedNode (m_pathWalk.first ())) { auto nextNodeIndex = m_pathWalk.next ();
auto currentNodeIndex = m_pathWalk.first ();
auto prevNodeIndex = m_currentNodeIndex;
if (!isOccupiedNode (currentNodeIndex)) {
return false; return false;
} }
for (auto &link : m_path->links) { // check the links
if (graph.exists (link.index) && m_pathWalk.first () != link.index && graph.isConnected (link.index, m_pathWalk.next ()) && graph.isConnected (m_currentNodeIndex, link.index)) { for (const auto &link : graph[prevNodeIndex].links) {
// don't use ladder nodes as alternative // skip invalid links, or links that points to itself
if (graph[link.index].flags & (NodeFlag::Ladder | PathFlag::Jump)) { if (!graph.exists (link.index) || currentNodeIndex == link.index) {
continue; continue;
} }
if (graph[link.index].origin.z <= graph[m_currentNodeIndex].origin.z + 10.0f && !isOccupiedNode (link.index)) { // skip itn't connected links
m_pathWalk.first () = link.index; if (!graph.isConnected (link.index, nextNodeIndex) || !graph.isConnected (link.index, prevNodeIndex)) {
continue;
return true;
} }
// don't use ladder nodes as alternative
if (graph[link.index].flags & (NodeFlag::Ladder | NodeFlag::Camp | PathFlag::Jump)) {
continue;
}
// if not occupied, just set advance
if (!isOccupiedNode (link.index)) {
m_pathWalk.first () = link.index;
return true;
} }
} }
return false; return false;
@ -2462,7 +2140,7 @@ bool Bot::advanceMovement () {
m_campButtons = 0; m_campButtons = 0;
const int nextIndex = m_pathWalk.next (); const int nextIndex = m_pathWalk.next ();
auto kills = static_cast <float> (graph.getDangerDamage (m_team, nextIndex, nextIndex)); auto kills = static_cast <float> (practice.getDamage (m_team, nextIndex, nextIndex));
// if damage done higher than one // if damage done higher than one
if (kills > 1.0f && bots.getRoundMidTime () > game.time ()) { if (kills > 1.0f && bots.getRoundMidTime () > game.time ()) {
@ -3010,7 +2688,7 @@ bool Bot::isDeadlyMove (const Vector &to) {
// this function eturns if given location would hurt Bot with falling damage // this function eturns if given location would hurt Bot with falling damage
TraceResult tr {}; TraceResult tr {};
const auto &direction = (to - pev->origin).normalize (); // 1 unit long const auto &direction = (to - pev->origin).normalize_apx (); // 1 unit long
Vector check = to, down = to; Vector check = to, down = to;
down.z -= 1000.0f; // straight down 1000 units down.z -= 1000.0f; // straight down 1000 units
@ -3022,13 +2700,13 @@ bool Bot::isDeadlyMove (const Vector &to) {
} }
float lastHeight = tr.flFraction * 1000.0f; // height from ground float lastHeight = tr.flFraction * 1000.0f; // height from ground
float distance = to.distance (check); // distance from goal float distance = to.distanceSq (check); // distance from goal
if (distance <= 30.0f && lastHeight > 150.0f) { if (distance <= cr::sqrf (30.0f) && lastHeight > 150.0f) {
return true; return true;
} }
while (distance > 30.0f) { while (distance > cr::sqrf (30.0f)) {
check = check - direction * 30.0f; // move 10 units closer to the goal... check = check - direction * 30.0f; // move 10 units closer to the goal...
down = check; down = check;
@ -3047,7 +2725,7 @@ bool Bot::isDeadlyMove (const Vector &to) {
return true; return true;
} }
lastHeight = height; lastHeight = height;
distance = to.distance (check); // distance from goal distance = to.distanceSq (check); // distance from goal
} }
return false; return false;
} }
@ -3116,9 +2794,7 @@ void Bot::changeYaw (float speed) {
int Bot::getRandomCampDir () { int Bot::getRandomCampDir () {
// find a good node to look at when camping // find a good node to look at when camping
if (m_currentNodeIndex == kInvalidNodeIndex) { ensureCurrentNodeIndex ();
changeNodeIndex (findNearestNode ());
}
constexpr int kMaxNodesToSearch = 5; constexpr int kMaxNodesToSearch = 5;
int count = 0, indices[kMaxNodesToSearch] {}; int count = 0, indices[kMaxNodesToSearch] {};
@ -3126,7 +2802,7 @@ int Bot::getRandomCampDir () {
uint16_t visibility[kMaxNodesToSearch] {}; uint16_t visibility[kMaxNodesToSearch] {};
for (const auto &path : graph) { for (const auto &path : graph) {
if (m_currentNodeIndex == path.number || !graph.isVisible (m_currentNodeIndex, path.number)) { if (m_currentNodeIndex == path.number || !vistab.visible (m_currentNodeIndex, path.number)) {
continue; continue;
} }
@ -3196,7 +2872,7 @@ void Bot::updateLookAngles () {
float stiffness = 200.0f; float stiffness = 200.0f;
float damping = 25.0f; float damping = 25.0f;
if (((m_aimFlags & (AimFlags::Enemy | AimFlags::Entity | AimFlags::Grenade)) || m_isEnemyReachable) && m_difficulty > Difficulty::Normal) { if (((m_aimFlags & (AimFlags::Enemy | AimFlags::Entity | AimFlags::Grenade)) || m_wantsToFire) && m_difficulty > Difficulty::Normal) {
if (m_difficulty == Difficulty::Expert) { if (m_difficulty == Difficulty::Expert) {
accelerate += 600.0f; accelerate += 600.0f;
} }
@ -3355,7 +3031,7 @@ bool Bot::isOccupiedNode (int index, bool needZeroVelocity) {
} }
// do not check clients far away from us // do not check clients far away from us
if (pev->origin.distanceSq (client.origin) > cr::square (320.0f)) { if (pev->origin.distanceSq (client.origin) > cr::sqrf (320.0f)) {
continue; continue;
} }
@ -3364,7 +3040,7 @@ bool Bot::isOccupiedNode (int index, bool needZeroVelocity) {
} }
auto length = client.origin.distanceSq (graph[index].origin); auto length = client.origin.distanceSq (graph[index].origin);
if (length < cr::clamp (cr::square (graph[index].radius) * 2.0f, cr::square (40.0f), cr::square (90.0f))) { if (length < cr::clamp (cr::sqrf (graph[index].radius) * 2.0f, cr::sqrf (40.0f), cr::sqrf (90.0f))) {
return true; return true;
} }
auto bot = bots[client.ent]; auto bot = bots[client.ent];
@ -3417,7 +3093,7 @@ bool Bot::isReachableNode (int index) {
const Vector &dst = graph[index].origin; const Vector &dst = graph[index].origin;
// is the destination close enough? // is the destination close enough?
if (dst.distanceSq (src) >= cr::square (600.0f)) { if (dst.distanceSq (src) >= cr::sqrf (600.0f)) {
return false; return false;
} }
@ -3473,3 +3149,100 @@ bool Bot::isBannedNode (int index) {
} }
return false; return false;
} }
void Bot::findShortestPath (int srcIndex, int destIndex) {
// this function finds the shortest path from source index to destination index
clearSearchNodes ();
m_chosenGoalIndex = srcIndex;
m_goalValue = 0.0f;
bool success = planner.find (srcIndex, destIndex, [&] (int index) {
m_pathWalk.add (index);
return true;
});
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
if (!graph.exists (srcIndex)) {
logger.error ("%s source path index not valid (%d).", __FUNCTION__, srcIndex);
return;
}
else if (!graph.exists (destIndex)) {
logger.error ("%s destination path index not valid (%d).", __FUNCTION__, destIndex);
return;
}
auto pathPlanner = planner.getAStar ();
// get correct calculation for heuristic
if (pathType == FindPath::Optimal) {
if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) {
pathPlanner->setH (Heuristic::hfunctionPathDistWithHostage);
pathPlanner->setG (Heuristic::gfunctionKillsDistCTWithHostage);
}
else {
pathPlanner->setH (Heuristic::hfunctionPathDist);
pathPlanner->setG (Heuristic::gfunctionKillsDist);
}
}
else if (pathType == FindPath::Safe) {
if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) {
pathPlanner->setH (Heuristic::hfunctionNone);
pathPlanner->setG (Heuristic::gfunctionKillsCTWithHostage);
}
else {
pathPlanner->setH (Heuristic::hfunctionNone);
pathPlanner->setG (Heuristic::gfunctionKills);
}
}
else {
if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) {
pathPlanner->setH (Heuristic::hfunctionPathDistWithHostage);
pathPlanner->setG (Heuristic::gfunctionPathDistWithHostage);
}
else {
pathPlanner->setH (Heuristic::hfunctionPathDist);
pathPlanner->setG (Heuristic::gfunctionPathDist);
}
}
clearSearchNodes ();
m_chosenGoalIndex = srcIndex;
m_goalValue = 0.0f;
auto result = pathPlanner->find (m_team, srcIndex, destIndex, [&] (int index) {
m_pathWalk.add (index);
return true;
});
// view the results
switch (result) {
case AStarResult::Success:
m_pathWalk.reverse (); // reverse path for path follower
break;
case AStarResult::InternalError:
// bot should not roam when this occurs
kick (); // kick the bot off...
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 ());
break;
case AStarResult::Failed:
// fallback to shortest path
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 ());
}
break;
}
}

531
src/planner.cpp Normal file
View file

@ -0,0 +1,531 @@
//
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
// Copyright © 2004-2023 YaPB Project <yapb@jeefo.net>.
//
// SPDX-License-Identifier: MIT
//
#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_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.");
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));
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));
}
}
if (current.flags & NodeFlag::Crouch) {
cost *= 1.5f;
}
return cost;
}
float Heuristic::gfunctionKillsDistCTWithHostage (int team, int currentIndex, int parentIndex) {
const auto &current = graph[currentIndex];
if (current.flags & NodeFlag::NoHostage) {
return kInfiniteHeuristic;
}
else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) {
return gfunctionKillsDist (team, currentIndex, parentIndex) * 500.0f;
}
return gfunctionKillsDist (team, currentIndex, parentIndex);
}
float Heuristic::gfunctionKills (int team, int currentIndex, int) {
auto cost = static_cast <float> (practice.getDamage (team, currentIndex, currentIndex));
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));
}
}
if (current.flags & NodeFlag::Crouch) {
cost *= 1.5f;
}
return cost;
}
auto Heuristic::gfunctionKillsCTWithHostage (int team, int currentIndex, int parentIndex) -> float {
if (parentIndex == kInvalidNodeIndex) {
return 0.0f;
}
const auto &current = graph[currentIndex];
if (current.flags & NodeFlag::NoHostage) {
return kInfiniteHeuristic;
}
else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) {
return gfunctionKills (team, currentIndex, parentIndex) * 500.0f;
}
return gfunctionKills (team, currentIndex, parentIndex);
}
float Heuristic::gfunctionPathDist (int, int currentIndex, int parentIndex) {
if (parentIndex == kInvalidNodeIndex) {
return 0.0f;
}
const auto &parent = graph[parentIndex];
const auto &current = graph[currentIndex];
for (const auto &link : parent.links) {
if (link.index == currentIndex) {
const auto distance = static_cast <float> (link.distance);
// we don't like ladder or crouch point
if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) {
return distance * 1.5f;
}
return distance;
}
}
return kInfiniteHeuristic;
}
float Heuristic::gfunctionPathDistWithHostage (int, int currentIndex, int parentIndex) {
const auto &current = graph[currentIndex];
if (current.flags & NodeFlag::NoHostage) {
return kInfiniteHeuristic;
}
else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) {
return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex) * 500.0f;
}
return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex);
}
float Heuristic::hfunctionPathDist (int index, int, int goalIndex) {
const auto &start = graph[index];
const auto &goal = graph[goalIndex];
const float x = start.origin.x - goal.origin.x;
const float y = start.origin.y - goal.origin.y;
const float z = start.origin.z - goal.origin.z;
switch (cv_path_heuristic_mode.int_ ()) {
case 0:
return cr::max (cr::max (cr::abs (x), cr::abs (y)), cr::abs (z)); // chebyshev distance
case 1:
return cr::abs (x) + cr::abs (y) + cr::abs (z); // manhattan distance
case 2:
return 0.0f; // no heuristic
case 3: {
const float dx = cr::abs (x);
const float dy = cr::abs (y);
const float dz = cr::abs (z);
const float dmin = cr::min (cr::min (dx, dy), dz);
const float dmax = cr::max (cr::max (dx, dy), dz);
const float dmid = dx + dy + dz - dmin - dmax;
const float d1 = 1.0f;
const float d2 = cr::sqrtf (2.0f);
const float d3 = cr::sqrtf (3.0f);
return (d3 - d2) * dmin + (d2 - d1) * dmid + d1 * dmax; // diagonal distance
}
default:
case 4:
return 10.0f * cr::sqrtf (cr::sqrf (x) + cr::sqrf (y) + cr::sqrf (z)); // euclidean distance
}
}
float Heuristic::hfunctionPathDistWithHostage (int index, int, int goalIndex) {
if (graph[index].flags & NodeFlag::NoHostage) {
return kInfiniteHeuristic;
}
return hfunctionPathDist (index, kInvalidNodeIndex, goalIndex);
}
float Heuristic::hfunctionNone (int index, int, int goalIndex) {
return hfunctionPathDist (index, kInvalidNodeIndex, goalIndex) / (128.0f * 10.0f);
}
void AStarAlgo::clearRoute () {
m_routes.resize (static_cast <size_t> (m_length));
for (const auto &path : graph) {
auto route = &m_routes[path.number];
route->g = route->f = 0.0f;
route->parent = kInvalidNodeIndex;
route->state = RouteState::New;
}
m_routes.clear ();
}
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);
if (notVisible) {
return true;
}
const bool tooHigh = cr::abs (ag.origin.z - bg.origin.z) > 17.0f;
if (tooHigh) {
return true;
}
const bool tooNarrow = (ag.flags | bg.flags) & NodeFlag::Narrow;
if (tooNarrow) {
return true;
}
const bool tooFar = ag.origin.distanceSq (bg.origin) > cr::sqrf (300.0f);
if (tooFar) {
return true;
}
bool hasJumps = false;
for (int i = 0; i < kMaxNodeLinks; ++i) {
if ((ag.links[i].flags | bg.links[i].flags) & PathFlag::Jump) {
hasJumps = true;
break;
}
}
return hasJumps;
}
void AStarAlgo::postSmooth (NodeAdderFn onAddedNode) {
int index = 0;
m_smoothedPath.emplace (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.emplace (m_constructedPath.last ());
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) {
if (m_length < kMaxNodeLinks) {
return AStarResult::InternalError; // astar needs some nodes to work with
}
clearRoute ();
auto srcRoute = &m_routes[srcIndex];
// put start node into open list
srcRoute->g = m_gcalc (botTeam, srcIndex, kInvalidNodeIndex);
srcRoute->f = srcRoute->g + m_hcalc (srcIndex, kInvalidNodeIndex, destIndex);
srcRoute->state = RouteState::Open;
m_routeQue.clear ();
m_routeQue.emplace (srcIndex, srcRoute->g);
bool postSmoothPath = cv_path_astar_post_smooth.bool_ ();
while (!m_routeQue.empty ()) {
// remove the first node from the open list
int currentIndex = m_routeQue.pop ().index;
// safes us from bad graph...
if (m_routeQue.length () >= getMaxLength () - 1) {
return AStarResult::InternalError;
}
// is the current node the goal node?
if (currentIndex == destIndex) {
// build the complete path
do {
if (postSmoothPath) {
m_constructedPath.emplace (currentIndex);
}
else {
onAddedNode (currentIndex);
}
currentIndex = m_routes[currentIndex].parent;
} while (currentIndex != kInvalidNodeIndex);
// do a post-smooth if requested
if (postSmoothPath) {
postSmooth (onAddedNode);
}
return AStarResult::Success;
}
auto curRoute = &m_routes[currentIndex];
if (curRoute->state != RouteState::Open) {
continue;
}
// put current node into CLOSED list
curRoute->state = RouteState::Closed;
// now expand the current node
for (const auto &child : graph[currentIndex].links) {
if (child.index == kInvalidNodeIndex) {
continue;
}
auto childRoute = &m_routes[child.index];
// calculate the F value as F = G + H
const float g = curRoute->g + m_gcalc (botTeam, child.index, currentIndex);
const float h = m_hcalc (child.index, kInvalidNodeIndex, destIndex);
const float f = g + h;
if (childRoute->state == RouteState::New || childRoute->f > f) {
// put the current child into open list
childRoute->parent = currentIndex;
childRoute->state = RouteState::Open;
childRoute->g = g;
childRoute->f = f;
m_routeQue.emplace (child.index, g);
}
}
}
return AStarResult::Failed;
}
void FloydWarshallAlgo::rebuild () {
m_length = graph.length ();
m_matrix.resize (cr::sqrf (m_length));
auto matrix = m_matrix.data ();
// re-initialize matrix every load
for (int i = 0; i < m_length; ++i) {
for (int j = 0; j < m_length; ++j) {
*(matrix + (i * m_length) + j) = { kInvalidNodeIndex, SHRT_MAX };
}
}
for (int i = 0; i < m_length; ++i) {
for (const auto &link : graph[i].links) {
if (!graph.exists (link.index)) {
continue;
}
*(matrix + (i * m_length) + link.index) = { link.index, link.distance };
}
}
for (int i = 0; i < m_length; ++i) {
(matrix + (i * m_length) + i)->dist = 0;
}
for (int k = 0; k < m_length; ++k) {
for (int i = 0; i < m_length; ++i) {
for (int j = 0; j < m_length; ++j) {
int distance = (matrix + (i * m_length) + k)->dist + (matrix + (k * m_length) + j)->dist;
if (distance < (matrix + (i * m_length) + j)->dist) {
*(matrix + (i * m_length) + j) = { (matrix + (i * m_length) + k)->index, distance };
}
}
}
}
save (); // save path matrix to file for faster access
}
bool FloydWarshallAlgo::load () {
m_length = graph.length ();
if (!m_length) {
return false;
}
bool dataLoaded = bstor.load <Matrix> (m_matrix);
// do not rebuild if loaded
if (dataLoaded) {
return true;
}
rebuild (); // rebuilds matrix
return true;
}
void FloydWarshallAlgo::save () {
if (!m_length) {
return;
}
bstor.save <Matrix> (m_matrix);
}
bool FloydWarshallAlgo::find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance) {
onAddedNode (srcIndex);
while (srcIndex != destIndex) {
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;
}
if (!onAddedNode (srcIndex)) {
return true;
}
}
return false;
}
void DijkstraAlgo::init (const int length) {
m_length = length;
m_distance.resize (length);
m_parent.resize (length);
}
void DijkstraAlgo::resetState () {
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;
while (!m_queue.empty ()) {
auto p = cr::move (m_queue.pop ());
auto current = p.second;
// finished search
if (current == destIndex) {
break;
}
if (m_distance[current] != p.first) {
continue;
}
for (const auto &link : graph[current].links) {
if (link.index != kInvalidNodeIndex) {
auto dlink = m_distance[current] + link.distance;
if (dlink < m_distance[link.index]) {
m_distance[link.index] = dlink;
m_parent[link.index] = current;
m_queue.emplace (m_distance[link.index], link.index);
}
}
}
}
static SmallArray <int> pathInReverse;
pathInReverse.clear ();
for (int i = destIndex; i != kInvalidNodeIndex; i = m_parent[i]) {
pathInReverse.emplace (i);
}
pathInReverse.reverse ();
for (const auto &node : pathInReverse) {
if (!onAddedNode (node)) {
break;
}
}
// always fill path distance if we're need to
if (pathDistance != nullptr) {
*pathDistance = m_distance[destIndex];
}
return m_distance[destIndex] < kInfiniteDistanceLong;
}
int DijkstraAlgo::dist (int srcIndex, int destIndex) {
int pathDistance = 0;
find (srcIndex, destIndex, [&] (int) {
return true;
}, &pathDistance);
return pathDistance;
}
PathPlanner::PathPlanner () {
m_dijkstra = cr::makeUnique <DijkstraAlgo> ();
m_floyd = cr::makeUnique <FloydWarshallAlgo> ();
m_astar = cr::makeUnique <AStarAlgo> ();
}
void PathPlanner::init () {
const int length = graph.length ();
const float limitInMb = cv_path_floyd_memory_limit.float_ ();
const float memoryUse = static_cast <float> (sizeof (FloydWarshallAlgo::Matrix) * cr::sqrf (length) / 1024 / 1024);
// if we're have too much memory for floyd matrices, planner will use dijkstra or uniform planner for other than pathfinding needs
if (memoryUse > limitInMb) {
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) {
m_floyd->load ();
}
}
bool PathPlanner::hasRealPathDistance () const {
return !m_memoryLimitHit || !cv_path_dijkstra_simple_distance.bool_ ();
}
bool PathPlanner::find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance) {
if (!graph.exists (srcIndex) || !graph.exists (destIndex)) {
return false;
}
// limit hit, use dijkstra
if (m_memoryLimitHit) {
return m_dijkstra->find (srcIndex, destIndex, onAddedNode, pathDistance);
}
return m_floyd->find (srcIndex, destIndex, onAddedNode, pathDistance);;
}
int PathPlanner::dist (int srcIndex, int destIndex) {
if (!graph.exists (srcIndex) || !graph.exists (destIndex)) {
return kInfiniteDistanceLong;
}
if (srcIndex == destIndex) {
return 1;
}
// 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 m_dijkstra->dist (srcIndex, destIndex);
}
return m_floyd->dist (srcIndex, destIndex);
}

157
src/practice.cpp Normal file
View file

@ -0,0 +1,157 @@
//
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
// Copyright © 2004-2023 YaPB Project <yapb@jeefo.net>.
//
// SPDX-License-Identifier: MIT
//
#include <yapb.h>
int32_t BotPractice::getIndex (int32_t team, int32_t start, int32_t goal) {
if (!exists (team, start, goal)) {
return kInvalidNodeIndex;
}
return m_data[{start, goal, team}].index;
}
void BotPractice::setIndex (int32_t team, int32_t start, int32_t goal, int32_t value) {
if (team != Team::Terrorist && team != Team::CT) {
return;
}
// reliability check
if (!graph.exists (start) || !graph.exists (goal) || !graph.exists (value)) {
return;
}
m_data[{start, goal, team}].index = static_cast <int16_t> (value);
}
int32_t BotPractice::getValue (int32_t team, int32_t start, int32_t goal) {
if (!exists (team, start, goal)) {
return 0;
}
return m_data[{start, goal, team}].value;
}
void BotPractice::setValue (int32_t team, int32_t start, int32_t goal, int32_t value) {
if (team != Team::Terrorist && team != Team::CT) {
return;
}
// reliability check
if (!graph.exists (start) || !graph.exists (goal)) {
return;
}
m_data[{start, goal, team}].value = static_cast <int16_t> (value);
}
int32_t BotPractice::getDamage (int32_t team, int32_t start, int32_t goal) {
if (!exists (team, start, goal)) {
return 0;
}
return m_data[{start, goal, team}].damage;
}
void BotPractice::setDamage (int32_t team, int32_t start, int32_t goal, int32_t value) {
if (team != Team::Terrorist && team != Team::CT) {
return;
}
// reliability check
if (!graph.exists (start) || !graph.exists (goal)) {
return;
}
m_data[{start, goal, team}].damage = static_cast <int16_t> (value);
}
void BotPractice::update () {
// this function called after each end of the round to update knowledge about most dangerous nodes for each team.
auto graphLength = graph.length ();
// no nodes, no practice used or nodes edited or being edited?
if (!graphLength || graph.hasChanged () || !vistab.isReady ()) {
return; // no action
}
auto adjustValues = false;
// get the most dangerous node for this position for both teams
for (int team = Team::Terrorist; team < kGameTeamNum; ++team) {
auto bestIndex = kInvalidNodeIndex; // best index to store
for (int i = 0; i < graphLength; ++i) {
auto maxDamage = 0;
bestIndex = kInvalidNodeIndex;
for (int j = 0; j < graphLength; ++j) {
if (i == j || !vistab.visible (i, j) || !exists (team, i, j)) {
continue;
}
auto actDamage = getDamage (team, i, j);
if (actDamage > maxDamage) {
maxDamage = actDamage;
bestIndex = j;
}
}
if (maxDamage > PracticeLimit::Damage) {
adjustValues = true;
}
setIndex (team, i, i, bestIndex);
}
}
constexpr auto kFullDamageVal = static_cast <int32_t> (PracticeLimit::Damage);
constexpr auto kHalfDamageVal = static_cast <int32_t> (PracticeLimit::Damage / 2);
// adjust values if overflow is about to happen
if (adjustValues) {
for (int team = Team::Terrorist; team < kGameTeamNum; ++team) {
for (int i = 0; i < graphLength; ++i) {
for (int j = 0; j < graphLength; ++j) {
if (i == j || !exists (team, i, j)) {
continue;
}
setDamage (team, i, j, cr::clamp (getDamage (team, i, j) - kHalfDamageVal, 0, kFullDamageVal));
}
}
}
}
for (int team = Team::Terrorist; team < kGameTeamNum; ++team) {
m_teamHighestDamage[team] = cr::clamp (m_teamHighestDamage[team] - kHalfDamageVal, 1, kFullDamageVal);
}
}
void BotPractice::save () {
if (!graph.length () || graph.hasChanged ()) {
return; // no action
}
SmallArray <DangerSaveRestore> data;
// copy hash-map data to our vector
m_data.foreach ([&data] (const DangerStorage &ds, const PracticeData &pd) {
data.emplace (ds, pd);
});
bstor.save <DangerSaveRestore> (data);
}
void BotPractice::load () {
if (!graph.length ()) {
return; // no action
}
SmallArray <DangerSaveRestore> data;
m_data.clear ();
bool dataLoaded = bstor.load <DangerSaveRestore> (data);
// copy back to hash table
if (dataLoaded) {
for (const auto &dsr : data) {
if (dsr.data.damage > 0 || dsr.data.index != kInvalidNodeIndex || dsr.data.value > 0) {
m_data.insert (dsr.danger, dsr.data);
}
}
}
}

394
src/storage.cpp Normal file
View file

@ -0,0 +1,394 @@
//
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
// Copyright © 2004-2023 YaPB Project <yapb@jeefo.net>.
//
// SPDX-License-Identifier: MIT
//
#include <yapb.h>
#if defined (BOT_STORAGE_EXPLICIT_INSTANTIATIONS)
template <typename U> bool BotStorage::load (SmallArray <U> &data, ExtenHeader *exten, int32_t *outOptions) {
auto type = guessType <U> ();
String filename = buildPath (storageToBotFile (type.option), true);
extern ConVar cv_debug, cv_graph_url;
// graphs can be downloaded...
auto isGraph = !!(type.option & StorageOption::Graph);
auto isDebug = cv_debug.bool_ ();
MemFile file (filename); // open the file
data.clear ();
// resize data to fit the stuff
auto resizeData = [&] (const size_t length) {
data.resize (length); // for non-graph data the graph should be already loaded
data.shrink (); // free up memory to minimum
// ensure we're have enough memory to decompress the data
data.ensure (length + ULZ::Excess);
};
// if graph & attempted to load multiple times, bail out, we're failed
if (isGraph && ++m_retries > 2) {
resetRetries ();
return error (isGraph, isDebug, file, "Unable to load %s (filename: '%s'). Download process has failed as well. No nodes has been found.", type.name, filename);
}
// downloader for graph
auto download = [&] () -> bool {
if (!graph.canDownload ()) {
return false;
}
auto downloadAddress = cv_graph_url.str ();
auto toDownload = buildPath (storageToBotFile (type.option), false);
auto fromDownload = strings.format ("http://%s/graph/%s.graph", downloadAddress, game.getMapName ());
// try to download
if (http.downloadFile (fromDownload, toDownload)) {
ctrl.msg ("%s file '%s' successfully downloaded. Processing...", type.name, filename);
return true;
}
else {
ctrl.msg ("Can't download '%s' from '%s' to '%s'... (%d).", filename, fromDownload, toDownload, http.getLastStatusCode ());
}
return false;
};
// tries to reload or open pwf file
auto tryReload = [&] () -> bool {
file.close ();
if (!isGraph) {
return false;
}
if (download ()) {
return load (data, exten, outOptions);
}
if (graph.convertOldFormat ()) {
return load (data, exten, outOptions);
}
return false;
};
// no open no fun
if (!file) {
if (tryReload ()) {
return true;
}
return error (isGraph, isDebug, file, "Unable to open %s file for reading (filename: '%s').", type.name, filename);
}
// read the header
StorageHeader hdr {};
file.read (&hdr, sizeof (StorageHeader));
// check the magic
if (hdr.magic != kStorageMagic && hdr.magic != kStorageMagicUB) {
if (tryReload ()) {
return true;
}
return error (isGraph, isDebug, file, "Unable to read magic of %s (filename: '%s').", type.name, filename);
}
// check the path-numbers
if (!isGraph && hdr.length != graph.length ()) {
return error (isGraph, isDebug, file, "Damaged %s (filename: '%s'). Mismatch number of nodes (got: '%d', need: '%d').", type.name, filename, hdr.length, graph.length ());
}
// check the count
if (hdr.length == 0 || hdr.length > kMaxNodes || hdr.length < kMaxNodeLinks) {
if (tryReload ()) {
return true;
}
return error (isGraph, isDebug, file, "Damaged %s (filename: '%s'). Paths length is overflowed (got: '%d').", type.name, filename, hdr.length);
}
// check the version
if (hdr.version > type.version && isGraph) {
ctrl.msg ("Graph version mismatch %s (filename: '%s'). Version number differs (got: '%d', need: '%d') Please, upgrade %s.", type.name, filename, hdr.version, type.version, product.name);
}
else if (hdr.version != type.version && !isGraph) {
return error (isGraph, isDebug, file, "Damaged %s (filename: '%s'). Version number differs (got: '%d', need: '%d').", type.name, filename, hdr.version, type.version);
}
// save graph version
if (isGraph) {
graph.setGraphHeader (&hdr);
}
// check the storage type
if ((hdr.options & type.option) != type.option) {
return error (isGraph, isDebug, file, "Incorrect storage format for %s (filename: '%s').", type.name, filename);
}
auto compressedSize = static_cast <size_t> (hdr.compressed);
auto numberNodes = static_cast <size_t> (hdr.length);
SmallArray <uint8_t> compressed (compressedSize + sizeof (uint8_t) * ULZ::Excess);
// graph is not resized upon load
if (isGraph) {
resizeData (numberNodes);
}
else {
resizeData (hdr.uncompressed / sizeof (U));
}
// read compressed data
if (file.read (compressed.data (), sizeof (uint8_t), compressedSize) == compressedSize) {
// try to uncompress
if (ulz.uncompress (compressed.data (), hdr.compressed, reinterpret_cast <uint8_t *> (data.data ()), hdr.uncompressed) == ULZ::UncompressFailure) {
return error (isGraph, isDebug, file, "Unable to decompress ULZ data for %s (filename: '%s').", type.name, filename);
}
else {
if (outOptions) {
outOptions = &hdr.options;
}
// author of graph.. save
if ((hdr.options & StorageOption::Exten) && exten != nullptr) {
auto extenSize = sizeof (ExtenHeader);
auto actuallyRead = file.read (exten, extenSize) * extenSize;
if (isGraph) {
resetRetries ();
ExtenHeader extenHeader;
strings.copy (extenHeader.author, exten->author, cr::bufsize (exten->author));
if (extenSize <= actuallyRead) {
// write modified by, only if the name is different
if (!strings.isEmpty (extenHeader.author) && strncmp (extenHeader.author, exten->modified, cr::bufsize (extenHeader.author)) != 0) {
strings.copy (extenHeader.modified, exten->modified, cr::bufsize (exten->modified));
}
}
else {
strings.copy (extenHeader.modified, "(none)", cr::bufsize (exten->modified));
}
extenHeader.mapSize = exten->mapSize;
// tell graph about exten header
graph.setExtenHeader (&extenHeader);
}
}
ctrl.msg ("Loaded Bots %s data v%d (Memory: %.2fMB).", type.name, hdr.version, static_cast <float> (data.capacity () * sizeof (U)) / 1024.0f / 1024.0f);
file.close ();
return true;
}
}
else {
return error (isGraph, isDebug, file, "Unable to read ULZ data for %s (filename: '%s').", type.name, filename);
}
return false;
}
template <typename U> bool BotStorage::save (const SmallArray <U> &data, ExtenHeader *exten, int32_t passOptions) {
auto type = guessType <U> ();
// append additional options
if (passOptions != 0) {
type.option |= passOptions;
}
auto isGraph = !!(type.option & StorageOption::Graph);
// do not allow to save graph with less than 8 nodes
if (isGraph && graph.length () < kMaxNodeLinks) {
ctrl.msg ("Can't save graph data with less than %d nodes. Please add some more before saving.", kMaxNodeLinks);
return false;
}
String filename = buildPath (storageToBotFile (type.option));
if (data.empty ()) {
logger.error ("Unable to save %s file. Empty data. (filename: '%s').", type.name, filename);
return false;
}
else if (isGraph) {
for (auto &path : graph) {
path.display = 0.0f;
path.light = illum.getLightLevel (path.origin);
}
}
// open the file
File file (filename, "wb");
// no open no fun
if (!file) {
logger.error ("Unable to open %s file for writing (filename: '%s').", type.name, filename);
return false;
}
auto rawLength = data.length () * sizeof (U);
SmallArray <uint8_t> compressed (rawLength + sizeof (uint8_t) * ULZ::Excess);
// try to compress
auto compressedLength = static_cast <size_t> (ulz.compress (reinterpret_cast <uint8_t *> (data.data ()), static_cast <int32_t> (rawLength), reinterpret_cast <uint8_t *> (compressed.data ())));
if (compressedLength > 0) {
StorageHeader hdr {};
hdr.magic = kStorageMagic;
hdr.version = type.version;
hdr.options = type.option;
hdr.length = graph.length ();
hdr.compressed = static_cast <int32_t> (compressedLength);
hdr.uncompressed = static_cast <int32_t> (rawLength);
file.write (&hdr, sizeof (StorageHeader));
file.write (compressed.data (), sizeof (uint8_t), compressedLength);
// add extension
if ((type.option & StorageOption::Exten) && exten != nullptr) {
file.write (exten, sizeof (ExtenHeader));
}
extern ConVar cv_debug;
// notify only about graph
if (isGraph || cv_debug.bool_ ()) {
ctrl.msg ("Successfully saved Bots %s data.", type.name);
}
}
else {
logger.error ("Unable to compress %s data (filename: '%s').", type.name, filename);
return false;
}
return true;
}
template <typename ...Args> bool BotStorage::error (bool isGraph, bool isDebug, MemFile &file, const char *fmt, Args &&...args) {
auto result = strings.format (fmt, cr::forward <Args> (args)...);
// display error only for graph file
if (isGraph || isDebug) {
logger.error (result);
}
// if graph reset paths
if (isGraph) {
bots.kickEveryone (true);
graph.reset ();
}
file.close ();
return false;
}
template <typename U> BotStorage::SaveLoadData BotStorage::guessType () {
if constexpr (cr::is_same <U, FloydWarshallAlgo::Matrix>::value) {
return { "Pathmatrix", StorageOption::Matrix, StorageVersion::Matrix };
}
else if constexpr (cr::is_same <U, BotPractice::DangerSaveRestore>::value) {
return { "Practice", StorageOption::Practice, StorageVersion::Practice };
}
else if constexpr (cr::is_same <U, GraphVistable::VisStorage>::value) {
return { "Vistable", StorageOption::Vistable, StorageVersion::Vistable };
}
else if constexpr (cr::is_same <U, Path>::value) {
return { "Graph", StorageOption::Graph, StorageVersion::Graph };
}
}
#else
String BotStorage::buildPath (int32_t file, bool isMemoryLoad) {
using FilePath = Twin <String, String>;
static HashMap <int32_t, FilePath> paths = {
{ BotFile::Vistable, FilePath ("train", "vis")},
{ BotFile::Practice, FilePath ("train", "prc")},
{ BotFile::Pathmatrix, FilePath ("train", "pmx")},
{ BotFile::LogFile, FilePath ("logs", "txt")},
{ BotFile::Graph, FilePath ("graph", "graph")},
{ BotFile::PodbotPWF, FilePath ("pwf", "pwf")},
{ BotFile::EbotEWP, FilePath ("ewp", "ewp")},
};
static StringArray path;
path.clear ();
// if not memory file we're don't need game dir
if (!isMemoryLoad) {
path.emplace (game.getRunningModName ());
}
// allways append addons/product
path.emplace ("addons");
path.emplace (product.folder);
// the datadir
path.emplace ("data");
// append real filepath
path.emplace (paths[file].first);
// if file is logfile use correct logfile name with date
if (file == BotFile::LogFile) {
time_t ticks = time (&ticks);
tm timeinfo {};
plat.loctime (&timeinfo, &ticks);
auto timebuf = strings.chars ();
strftime (timebuf, StringBuffer::StaticBufferSize, "L%d%m%Y", &timeinfo);
path.emplace (strings.format ("%s_%s.%s", product.folder, timebuf, paths[file].second));
}
else {
String mapName = game.getMapName ();
path.emplace (strings.format ("%s.%s", mapName.lowercase (), paths[file].second));
}
// finally use correct path separarators for us
return String::join (path, PATH_SEP);
}
int32_t BotStorage::storageToBotFile (int32_t options) {
// converts storage option to stroage filename
if (options & StorageOption::Graph) {
return BotFile::Graph;
}
else if (options & StorageOption::Matrix) {
return BotFile::Pathmatrix;
}
else if (options & StorageOption::Vistable) {
return BotFile::Vistable;
}
else if (options & StorageOption::Practice) {
return BotFile::Practice;
}
return BotFile::Graph;
}
void BotStorage::unlinkFromDisk () {
// this function removes graph file from the hard disk
StringArray unlinkable;
bots.kickEveryone (true);
// if we're delete graph, delete all corresponding to it files
unlinkable.emplace (buildPath (BotFile::Graph)); // graph itself
unlinkable.emplace (buildPath (BotFile::Practice)); // corresponding to practice
unlinkable.emplace (buildPath (BotFile::Vistable)); // corresponding to vistable
unlinkable.emplace (buildPath (BotFile::Pathmatrix)); // corresponding to matrix
for (const auto &item : unlinkable) {
if (File::exists (item)) {
plat.removeFile (item.chars ());
ctrl.msg ("File %s, has been deleted from the hard disk", item);
}
else {
logger.error ("Unable to open %s", item);
}
}
graph.reset (); // re-intialize points
}
#endif // BOT_STORAGE_EXPLICIT_INSTANTIATIONS

View file

@ -538,75 +538,6 @@ String BotSupport::getCurrentDateTime () {
return String (timebuf); return String (timebuf);
} }
String BotSupport::buildPath (int32_t file, bool isMemoryLoad) {
using FilePath = Twin <String, String>;
static HashMap <int32_t, FilePath> paths = {
{ BotFile::Vistable, FilePath ("train", "vis")},
{ BotFile::Practice, FilePath ("train", "prc")},
{ BotFile::Pathmatrix, FilePath ("train", "pmx")},
{ BotFile::LogFile, FilePath ("logs", "txt")},
{ BotFile::Graph, FilePath ("graph", "graph")},
{ BotFile::PodbotPWF, FilePath ("pwf", "pwf")},
{ BotFile::EbotEWP, FilePath ("ewp", "ewp")},
};
static StringArray path;
path.clear ();
// if not memory file we're don't need game dir
if (!isMemoryLoad) {
path.emplace (game.getRunningModName ());
}
// allways append addons/product
path.emplace ("addons");
path.emplace (product.folder);
// the datadir
path.emplace ("data");
// append real filepath
path.emplace (paths[file].first);
// if file is logfile use correct logfile name with date
if (file == BotFile::LogFile) {
time_t ticks = time (&ticks);
tm timeinfo {};
plat.loctime (&timeinfo, &ticks);
auto timebuf = strings.chars ();
strftime (timebuf, StringBuffer::StaticBufferSize, "L%d%m%Y", &timeinfo);
path.emplace (strings.format ("%s_%s.%s", product.folder, timebuf, paths[file].second));
}
else {
String mapName = game.getMapName ();
path.emplace (strings.format ("%s.%s", mapName.lowercase (), paths[file].second));
}
// finally use correct path separarators for us
return String::join (path, plat.win ? "\\" : "/");
}
// converts storage option to stroage filename
int32_t BotSupport::storageToBotFile (StorageOption options) {
if (options & StorageOption::Graph) {
return BotFile::Graph;
}
else if (options & StorageOption::Matrix) {
return BotFile::Pathmatrix;
}
else if (options & StorageOption::Vistable) {
return BotFile::Vistable;
}
else if (options & StorageOption::Practice) {
return BotFile::Practice;
}
return BotFile::Graph;
}
int32_t BotSupport::sendTo (int socket, const void *message, size_t length, int flags, const sockaddr *dest, int destLength) { int32_t BotSupport::sendTo (int socket, const void *message, size_t length, int flags, const sockaddr *dest, int destLength) {
const auto send = [&] (const Twin <const uint8_t *, size_t> &msg) -> int32_t { const auto send = [&] (const Twin <const uint8_t *, size_t> &msg) -> int32_t {
return Socket::sendto (socket, msg.first, msg.second, flags, dest, destLength); return Socket::sendto (socket, msg.first, msg.second, flags, dest, destLength);

178
src/vistable.cpp Normal file
View file

@ -0,0 +1,178 @@
//
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
// Copyright © 2004-2023 YaPB Project <yapb@jeefo.net>.
//
// SPDX-License-Identifier: MIT
//
#include <yapb.h>
void GraphVistable::rebuild () {
if (!m_rebuild) {
return;
}
m_length = graph.length ();
TraceResult tr {};
uint8_t res, shift;
if (!graph.exists (m_sliceIndex)) {
m_sliceIndex = 0;
}
if (!graph.exists (m_curIndex)) {
m_curIndex = 0;
}
const auto &vis = graph[m_curIndex];
auto sourceCrouch = vis.origin;
auto sourceStand = vis.origin;
if (vis.flags & NodeFlag::Crouch) {
sourceCrouch.z += 12.0f;
sourceStand.z += 18.0f + 28.0f;
}
else {
sourceCrouch.z += -18.0f + 12.0f;
sourceStand.z += 28.0f;
}
auto end = m_sliceIndex + rg.get (250, 400);
if (end > m_length) {
end = m_length;
}
uint16_t standCount = 0, crouchCount = 0;
for (int i = m_sliceIndex; i < end; ++i) {
const auto &path = graph[i];
// first check ducked visibility
Vector dest = path.origin;
game.testLine (sourceCrouch, dest, TraceIgnore::Monsters, nullptr, &tr);
// check if line of sight to object is not blocked (i.e. visible)
if (!cr::fequal (tr.flFraction, 1.0f)) {
res = VisIndex::Stand;
}
else {
res = VisIndex::None;
}
res <<= 1;
game.testLine (sourceStand, dest, TraceIgnore::Monsters, nullptr, &tr);
// check if line of sight to object is not blocked (i.e. visible)
if (!cr::fequal (tr.flFraction, 1.0f)) {
res |= VisIndex::Stand;
}
if (res != VisIndex::None) {
dest = path.origin;
// first check ducked visibility
if (path.flags & NodeFlag::Crouch) {
dest.z += 18.0f + 28.0f;
}
else {
dest.z += 28.0f;
}
game.testLine (sourceCrouch, dest, TraceIgnore::Monsters, nullptr, &tr);
// check if line of sight to object is not blocked (i.e. visible)
if (!cr::fequal (tr.flFraction, 1.0f)) {
res |= VisIndex::Crouch;;
}
else {
res &= VisIndex::Stand;;
}
game.testLine (sourceStand, dest, TraceIgnore::Monsters, nullptr, &tr);
// check if line of sight to object is not blocked (i.e. visible)
if (!cr::fequal (tr.flFraction, 1.0f)) {
res |= VisIndex::Stand;
}
else {
res &= VisIndex::Crouch;;
}
}
shift = static_cast <uint8_t> ((path.number % 4) << 1);
m_vistable[vis.number * m_length + path.number] &= ~static_cast <uint8_t> (VisIndex::Any << shift);
m_vistable[vis.number * m_length + path.number] |= res << shift;
if (!(res & VisIndex::Crouch)) {
++crouchCount;
}
if (!(res & VisIndex::Stand)) {
++standCount;
}
}
graph[vis.number].vis.crouch = crouchCount;
graph[vis.number].vis.stand = standCount;
if (end == m_length) {
m_sliceIndex = 0;
m_curIndex++;
}
else {
m_sliceIndex += rg.get (250, 400);
}
// notify host about rebuilding
if (m_notifyMsgTimestamp > 0.0f && m_notifyMsgTimestamp < game.time () && end == m_length) {
game.print ("Rebuilding vistable... %d%% done.", m_curIndex * 100 / m_length);
m_notifyMsgTimestamp = game.time () + 1.0f;
}
if (m_curIndex == m_length && end == m_length) {
m_rebuild = false;
m_notifyMsgTimestamp = 0.0f;
save ();
}
}
void GraphVistable::startRebuild () {
m_rebuild = true;
m_notifyMsgTimestamp = game.time ();
}
bool GraphVistable::visible (int srcIndex, int destIndex, VisIndex vis) {
if (!graph.exists (srcIndex) || !graph.exists (destIndex)) {
return false;
}
return !(((m_vistable[srcIndex * m_length + destIndex] >> ((destIndex % 4) << 1)) & vis) == vis);
}
void GraphVistable::load () {
m_rebuild = true;
m_length = graph.length ();
m_sliceIndex = 0;
m_curIndex = 0;
m_notifyMsgTimestamp = 0.0f;
if (!m_length) {
return;
}
bool dataLoaded = bstor.load <VisStorage> (m_vistable);
// if loaded, do not recalculate visibility
if (dataLoaded) {
m_rebuild = false;
}
else {
m_vistable.resize (cr::sqrf (m_length));
m_notifyMsgTimestamp = game.time ();
}
}
void GraphVistable::save () {
if (!graph.length () || graph.hasChanged () || m_rebuild) {
return;
}
bstor.save <VisStorage> (m_vistable);
}

View file

@ -44,6 +44,7 @@
<ClInclude Include="..\ext\hlsdk\model.h" /> <ClInclude Include="..\ext\hlsdk\model.h" />
<ClInclude Include="..\ext\hlsdk\progdefs.h" /> <ClInclude Include="..\ext\hlsdk\progdefs.h" />
<ClInclude Include="..\ext\hlsdk\util.h" /> <ClInclude Include="..\ext\hlsdk\util.h" />
<ClInclude Include="..\inc\analyze.h" />
<ClInclude Include="..\inc\config.h" /> <ClInclude Include="..\inc\config.h" />
<ClInclude Include="..\inc\control.h" /> <ClInclude Include="..\inc\control.h" />
<ClInclude Include="..\inc\engine.h" /> <ClInclude Include="..\inc\engine.h" />
@ -51,13 +52,18 @@
<ClInclude Include="..\inc\manager.h" /> <ClInclude Include="..\inc\manager.h" />
<ClInclude Include="..\inc\message.h" /> <ClInclude Include="..\inc\message.h" />
<ClInclude Include="..\inc\module.h" /> <ClInclude Include="..\inc\module.h" />
<ClInclude Include="..\inc\planner.h" />
<ClInclude Include="..\inc\practice.h" />
<ClInclude Include="..\inc\product.h" /> <ClInclude Include="..\inc\product.h" />
<ClInclude Include="..\inc\sounds.h" /> <ClInclude Include="..\inc\sounds.h" />
<ClInclude Include="..\inc\storage.h" />
<ClInclude Include="..\inc\support.h" /> <ClInclude Include="..\inc\support.h" />
<ClInclude Include="..\inc\vistable.h" />
<ClInclude Include="..\inc\yapb.h" /> <ClInclude Include="..\inc\yapb.h" />
<ClInclude Include="..\inc\version.h" /> <ClInclude Include="..\inc\version.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="..\src\analyze.cpp" />
<ClCompile Include="..\src\entities.cpp"> <ClCompile Include="..\src\entities.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild> <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild> <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
@ -74,8 +80,12 @@
<ClCompile Include="..\src\message.cpp" /> <ClCompile Include="..\src\message.cpp" />
<ClCompile Include="..\src\module.cpp" /> <ClCompile Include="..\src\module.cpp" />
<ClCompile Include="..\src\navigate.cpp" /> <ClCompile Include="..\src\navigate.cpp" />
<ClCompile Include="..\src\planner.cpp" />
<ClCompile Include="..\src\practice.cpp" />
<ClCompile Include="..\src\sounds.cpp" /> <ClCompile Include="..\src\sounds.cpp" />
<ClCompile Include="..\src\storage.cpp" />
<ClCompile Include="..\src\support.cpp" /> <ClCompile Include="..\src\support.cpp" />
<ClCompile Include="..\src\vistable.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ResourceCompile Include="yapb.rc"> <ResourceCompile Include="yapb.rc">
@ -175,7 +185,7 @@
<InlineFunctionExpansion>Default</InlineFunctionExpansion> <InlineFunctionExpansion>Default</InlineFunctionExpansion>
<MultiProcessorCompilation>true</MultiProcessorCompilation> <MultiProcessorCompilation>true</MultiProcessorCompilation>
<DisableLanguageExtensions>false</DisableLanguageExtensions> <DisableLanguageExtensions>false</DisableLanguageExtensions>
<LanguageStandard>Default</LanguageStandard> <LanguageStandard>stdcpp17</LanguageStandard>
<DebugInformationFormat>EditAndContinue</DebugInformationFormat> <DebugInformationFormat>EditAndContinue</DebugInformationFormat>
</ClCompile> </ClCompile>
<ResourceCompile> <ResourceCompile>
@ -256,7 +266,7 @@
<StringPooling>true</StringPooling> <StringPooling>true</StringPooling>
<BufferSecurityCheck>false</BufferSecurityCheck> <BufferSecurityCheck>false</BufferSecurityCheck>
<FunctionLevelLinking>true</FunctionLevelLinking> <FunctionLevelLinking>true</FunctionLevelLinking>
<LanguageStandard>Default</LanguageStandard> <LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile> </ClCompile>
<ResourceCompile> <ResourceCompile>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>

View file

@ -156,6 +156,21 @@
<ClInclude Include="..\inc\sounds.h"> <ClInclude Include="..\inc\sounds.h">
<Filter>inc</Filter> <Filter>inc</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="..\inc\analyze.h">
<Filter>inc</Filter>
</ClInclude>
<ClInclude Include="..\inc\practice.h">
<Filter>inc</Filter>
</ClInclude>
<ClInclude Include="..\inc\storage.h">
<Filter>inc</Filter>
</ClInclude>
<ClInclude Include="..\inc\planner.h">
<Filter>inc</Filter>
</ClInclude>
<ClInclude Include="..\inc\vistable.h">
<Filter>inc</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="..\src\botlib.cpp"> <ClCompile Include="..\src\botlib.cpp">
@ -203,6 +218,21 @@
<ClCompile Include="..\src\sounds.cpp"> <ClCompile Include="..\src\sounds.cpp">
<Filter>src</Filter> <Filter>src</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="..\src\analyze.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>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ResourceCompile Include="yapb.rc"> <ResourceCompile Include="yapb.rc">