From a616f25b1a2d6fbb83e40e361b0e4340d0b7a507 Mon Sep 17 00:00:00 2001 From: jeefo Date: Sat, 6 May 2023 20:14:03 +0300 Subject: [PATCH] bot: implemented asynchronous pathfinding nav: floyd-warshall matrices and practice updates are done asynchronously by now add: yb_threadpool_workers cvar, that controls number of worker threads bot will use add: cv_autovacate_keep_slots, the amount of slots to keep by auto vacate aim: enemy prediction is now done asynchronously by now bot: minor fixes and refactoring, including analyze suspend mistake (ref #441) note: the master builds are now NOT production ready, please test before installing on real servers! --- ext/crlib | 2 +- ext/hlsdk/const.h | 10 +- ext/hlsdk/eiface.h | 4 +- ext/hlsdk/extdll.h | 6 +- ext/hlsdk/meta_api.h | 2 +- ext/hlsdk/metamod.h | 2 +- ext/hlsdk/model.h | 6 +- ext/hlsdk/physint.h | 195 +++++++++++++++++----------------- inc/engine.h | 6 +- inc/graph.h | 9 -- inc/manager.h | 40 +++++-- inc/planner.h | 30 +++--- inc/practice.h | 15 ++- inc/support.h | 6 ++ inc/yapb.h | 28 ++++- meson.build | 2 +- src/analyze.cpp | 5 +- src/botlib.cpp | 219 ++++++++++++++++++++++++++------------ src/combat.cpp | 21 ++-- src/control.cpp | 6 +- src/engine.cpp | 50 +++++++-- src/graph.cpp | 1 + src/linkage.cpp | 57 ++++------ src/manager.cpp | 73 ++++++++++--- src/navigate.cpp | 225 +++++++++++++++++++++++++--------------- src/planner.cpp | 91 ++++++++++------ src/practice.cpp | 30 +++++- src/support.cpp | 9 +- vc/yapb.vcxproj | 2 + vc/yapb.vcxproj.filters | 12 ++- 30 files changed, 743 insertions(+), 421 deletions(-) diff --git a/ext/crlib b/ext/crlib index 4d370ec..95405e9 160000 --- a/ext/crlib +++ b/ext/crlib @@ -1 +1 @@ -Subproject commit 4d370ec500675ad23e9507831bbbbd6bb166ce3a +Subproject commit 95405e93fcdad85586103c843a20f8cb5985ae94 diff --git a/ext/hlsdk/const.h b/ext/hlsdk/const.h index 1fd1710..cee93c9 100644 --- a/ext/hlsdk/const.h +++ b/ext/hlsdk/const.h @@ -15,10 +15,10 @@ #ifndef CONST_H #define CONST_H -// -// Constants shared by the engine and dlls -// This header file included by engine files and DLL files. -// Most came from server.h + // + // Constants shared by the engine and dlls + // This header file included by engine files and DLL files. + // Most came from server.h #define INTERFACE_VERSION 140 @@ -842,7 +842,7 @@ typedef enum { missile = 2 } IGNORE_MONSTERS; -typedef enum { +typedef enum { ignore_glass = 1, dont_ignore_glass = 0 } IGNORE_GLASS; diff --git a/ext/hlsdk/eiface.h b/ext/hlsdk/eiface.h index 7f6c878..68d286b 100644 --- a/ext/hlsdk/eiface.h +++ b/ext/hlsdk/eiface.h @@ -54,7 +54,7 @@ typedef struct usercmd_s { byte msec; // Duration in ms of command vec3_t viewangles; // Command view angles. -// intended velocities + // intended velocities float forwardmove; // Forward velocity. float sidemove; // Sideways velocity. float upmove; // Upward velocity. @@ -63,7 +63,7 @@ typedef struct usercmd_s { byte impulse; // Impulse command issued. byte weaponselect; // Current weapon id -// Experimental player impact stuff. + // Experimental player impact stuff. int impact_index; vec3_t impact_position; } usercmd_t; diff --git a/ext/hlsdk/extdll.h b/ext/hlsdk/extdll.h index d8b8da5..2701578 100644 --- a/ext/hlsdk/extdll.h +++ b/ext/hlsdk/extdll.h @@ -39,11 +39,9 @@ private: estring_t offset; public: - explicit string_t () : offset (0) - { } + explicit string_t () : offset (0) {} - string_t (int offset) : offset (offset) - { } + string_t (int offset) : offset (offset) {} ~string_t () = default; diff --git a/ext/hlsdk/meta_api.h b/ext/hlsdk/meta_api.h index d5e0b3d..f853fbf 100644 --- a/ext/hlsdk/meta_api.h +++ b/ext/hlsdk/meta_api.h @@ -29,7 +29,7 @@ * */ -// Simplified version by Wei Mingzhi + // Simplified version by Wei Mingzhi #ifndef META_API_H #define META_API_H diff --git a/ext/hlsdk/metamod.h b/ext/hlsdk/metamod.h index 6452fb8..a0a3381 100644 --- a/ext/hlsdk/metamod.h +++ b/ext/hlsdk/metamod.h @@ -29,7 +29,7 @@ * */ -// Simplified version by Wei Mingzhi + // Simplified version by Wei Mingzhi #ifndef METAMOD_H #define METAMOD_H diff --git a/ext/hlsdk/model.h b/ext/hlsdk/model.h index aecf6c6..790294a 100644 --- a/ext/hlsdk/model.h +++ b/ext/hlsdk/model.h @@ -13,7 +13,7 @@ * ****/ -// stripped down version of com_model.h & pm_info.h + // stripped down version of com_model.h & pm_info.h #ifndef MODEL_H #define MODEL_H @@ -103,7 +103,7 @@ typedef struct msurface_s { } msurface_t; typedef struct cache_user_s { - void* data; // extradata + void *data; // extradata } cache_user_t; typedef struct hull_s { @@ -166,7 +166,7 @@ typedef struct physent_s { char name[32]; // name of model, or "player" or "world". int player; vec3_t origin; // model's origin in world coordinates. - struct model_s* model; // only for bsp models + struct model_s *model; // only for bsp models } physent_t; typedef struct playermove_s { diff --git a/ext/hlsdk/physint.h b/ext/hlsdk/physint.h index 1427163..b4c4203 100644 --- a/ext/hlsdk/physint.h +++ b/ext/hlsdk/physint.h @@ -48,117 +48,114 @@ GNU General Public License for more details. #define LUMP_SAVE_NO_DATA 7 #define LUMP_SAVE_CORRUPTED 8 -typedef struct areanode_s -{ - int axis; // -1 = leaf node - float dist; - struct areanode_s *children[2]; - link_t trigger_edicts; - link_t solid_edicts; - link_t portal_edicts; +typedef struct areanode_s { + int axis; // -1 = leaf node + float dist; + struct areanode_s *children[2]; + link_t trigger_edicts; + link_t solid_edicts; + link_t portal_edicts; } areanode_t; -typedef struct server_physics_api_s -{ - // unlink edict from old position and link onto new - void ( *pfnLinkEdict) ( edict_t *ent, qboolean touch_triggers ); - double ( *pfnGetServerTime )( void ); // unclamped - double ( *pfnGetFrameTime )( void ); // unclamped - void* ( *pfnGetModel )( int modelindex ); - areanode_t* ( *pfnGetHeadnode )( void ); // AABB tree for all physic entities - int ( *pfnServerState )( void ); - void ( *pfnHost_Error )( const char *error, ... ); // cause Host Error -// ONLY ADD NEW FUNCTIONS TO THE END OF THIS STRUCT. INTERFACE VERSION IS FROZEN AT 6 - struct triangleapi_s *pTriAPI; // draw coliisions etc. Only for local system +typedef struct server_physics_api_s { + // unlink edict from old position and link onto new + void (*pfnLinkEdict) (edict_t *ent, qboolean touch_triggers); + double (*pfnGetServerTime)(void); // unclamped + double (*pfnGetFrameTime)(void); // unclamped + void *(*pfnGetModel)(int modelindex); + areanode_t *(*pfnGetHeadnode)(void); // AABB tree for all physic entities + int (*pfnServerState)(void); + void (*pfnHost_Error)(const char *error, ...); // cause Host Error + // ONLY ADD NEW FUNCTIONS TO THE END OF THIS STRUCT. INTERFACE VERSION IS FROZEN AT 6 + struct triangleapi_s *pTriAPI; // draw coliisions etc. Only for local system - // draw debug messages (must be called from DrawOrthoTriangles). Only for local system - int ( *pfnDrawConsoleString )( int x, int y, char *string ); - void ( *pfnDrawSetTextColor )( float r, float g, float b ); - void ( *pfnDrawConsoleStringLen )( const char *string, int *length, int *height ); - void ( *Con_NPrintf )( int pos, const char *fmt, ... ); - void ( *Con_NXPrintf )( struct con_nprint_s *info, const char *fmt, ... ); - const char *( *pfnGetLightStyle )( int style ); // read custom appreance for selected lightstyle - void ( *pfnUpdateFogSettings )( unsigned int packed_fog ); - char **(*pfnGetFilesList)( const char *pattern, int *numFiles, int gamedironly ); - struct msurface_s *(*pfnTraceSurface)( edict_t *pTextureEntity, const float *v1, const float *v2 ); - const byte *(*pfnGetTextureData)( unsigned int texnum ); + // draw debug messages (must be called from DrawOrthoTriangles). Only for local system + int (*pfnDrawConsoleString)(int x, int y, char *string); + void (*pfnDrawSetTextColor)(float r, float g, float b); + void (*pfnDrawConsoleStringLen)(const char *string, int *length, int *height); + void (*Con_NPrintf)(int pos, const char *fmt, ...); + void (*Con_NXPrintf)(struct con_nprint_s *info, const char *fmt, ...); + const char *(*pfnGetLightStyle)(int style); // read custom appreance for selected lightstyle + void (*pfnUpdateFogSettings)(unsigned int packed_fog); + char **(*pfnGetFilesList)(const char *pattern, int *numFiles, int gamedironly); + struct msurface_s *(*pfnTraceSurface)(edict_t *pTextureEntity, const float *v1, const float *v2); + const byte *(*pfnGetTextureData)(unsigned int texnum); - // static allocations - void *(*pfnMemAlloc)( size_t cb, const char *filename, const int fileline ); - void (*pfnMemFree)( void *mem, const char *filename, const int fileline ); + // static allocations + void *(*pfnMemAlloc)(size_t cb, const char *filename, const int fileline); + void (*pfnMemFree)(void *mem, const char *filename, const int fileline); - // trace & contents - int (*pfnMaskPointContents)( const float *pos, int groupmask ); - struct trace_t (*pfnTrace)( const float *p0, float *mins, float *maxs, const float *p1, int type, edict_t *e ); - struct trace_t (*pfnTraceNoEnts)( const float *p0, float *mins, float *maxs, const float *p1, int type, edict_t *e ); - int (*pfnBoxInPVS)( const float *org, const float *boxmins, const float *boxmaxs ); + // trace & contents + int (*pfnMaskPointContents)(const float *pos, int groupmask); + struct trace_t (*pfnTrace)(const float *p0, float *mins, float *maxs, const float *p1, int type, edict_t *e); + struct trace_t (*pfnTraceNoEnts)(const float *p0, float *mins, float *maxs, const float *p1, int type, edict_t *e); + int (*pfnBoxInPVS)(const float *org, const float *boxmins, const float *boxmaxs); - // message handler (missed function to write raw bytes) - void (*pfnWriteBytes)( const byte *bytes, int count ); + // message handler (missed function to write raw bytes) + void (*pfnWriteBytes)(const byte *bytes, int count); - // BSP lump management - int (*pfnCheckLump)( const char *filename, const int lump, int *lumpsize ); - int (*pfnReadLump)( const char *filename, const int lump, void **lumpdata, int *lumpsize ); - int (*pfnSaveLump)( const char *filename, const int lump, void *lumpdata, int lumpsize ); + // BSP lump management + int (*pfnCheckLump)(const char *filename, const int lump, int *lumpsize); + int (*pfnReadLump)(const char *filename, const int lump, void **lumpdata, int *lumpsize); + int (*pfnSaveLump)(const char *filename, const int lump, void *lumpdata, int lumpsize); - // FS tools - int (*pfnSaveFile)( const char *filename, const void *data, int len ); - const byte *(*pfnLoadImagePixels)( const char *filename, int *width, int *height ); + // FS tools + int (*pfnSaveFile)(const char *filename, const void *data, int len); + const byte *(*pfnLoadImagePixels)(const char *filename, int *width, int *height); - const char* (*pfnGetModelName)( int modelindex ); + const char *(*pfnGetModelName)(int modelindex); } server_physics_api_t; // physic callbacks -typedef struct physics_interface_s -{ - int version; - // passed through pfnCreate (0 is attempt to create, -1 is reject) - int ( *SV_CreateEntity )( edict_t *pent, const char *szName ); - // run custom physics for each entity (return 0 to use built-in engine physic) - int ( *SV_PhysicsEntity )( edict_t *pEntity ); - // spawn entities with internal mod function e.g. for re-arrange spawn order (0 - use engine parser, 1 - use mod parser) - int ( *SV_LoadEntities )( const char *mapname, char *entities ); - // update conveyor belt for clients - void ( *SV_UpdatePlayerBaseVelocity )( edict_t *ent ); - // The game .dll should return 1 if save game should be allowed - int ( *SV_AllowSaveGame )( void ); -// ONLY ADD NEW FUNCTIONS TO THE END OF THIS STRUCT. INTERFACE VERSION IS FROZEN AT 6 - // override trigger area checking and touching - int ( *SV_TriggerTouch )( edict_t *pent, edict_t *trigger ); - // some engine features can be enabled only through this function - unsigned int ( *SV_CheckFeatures )( void ); - // used for draw debug collisions for custom physic engine etc - void ( *DrawDebugTriangles )( void ); - // used for draw debug overlay (textured) - void ( *DrawNormalTriangles )( void ); - // used for draw debug messages (2d mode) - void ( *DrawOrthoTriangles )( void ); - // tracing entities with SOLID_CUSTOM mode on a server (not used by pmove code) - void ( *ClipMoveToEntity)( edict_t *ent, const float *start, float *mins, float *maxs, const float *end, trace_t *trace ); - // tracing entities with SOLID_CUSTOM mode on a server (only used by pmove code) - void ( *ClipPMoveToEntity)( struct physent_s *pe, const float *start, float *mins, float *maxs, const float *end, struct pmtrace_s *tr ); - // called at end the frame of SV_Physics call - void ( *SV_EndFrame )( void ); - // obsolete - void (*pfnPrepWorldFrame)( void ); - // called through save\restore process - void (*pfnCreateEntitiesInRestoreList)( SAVERESTOREDATA *pSaveData, int levelMask, qboolean create_world ); - // allocate custom string (e.g. using user implementation of stringtable, not engine strings) - string_t (*pfnAllocString)( const char *szValue ); - // make custom string (e.g. using user implementation of stringtable, not engine strings) - string_t (*pfnMakeString)( const char *szValue ); - // read custom string (e.g. using user implementation of stringtable, not engine strings) - const char* (*pfnGetString)( string_t iString ); - // helper for restore custom decals that have custom message (e.g. Paranoia) - int (*pfnRestoreDecal)( struct decallist_s *entry, edict_t *pEdict, qboolean adjacent ); - // handle custom trigger touching for player - void (*PM_PlayerTouch)( struct playermove_s *ppmove, edict_t *client ); - // alloc or destroy model custom data (called only for dedicated servers, otherwise using an client version) - void (*Mod_ProcessUserData)( struct model_s *mod, qboolean create, const byte *buffer ); - // select BSP-hull for trace with specified mins\maxs - void *(*SV_HullForBsp)( edict_t *ent, const float *mins, const float *maxs, float *offset ); - // handle player custom think function - int (*SV_PlayerThink)( edict_t *ent, float frametime, double time ); +typedef struct physics_interface_s { + int version; + // passed through pfnCreate (0 is attempt to create, -1 is reject) + int (*SV_CreateEntity)(edict_t *pent, const char *szName); + // run custom physics for each entity (return 0 to use built-in engine physic) + int (*SV_PhysicsEntity)(edict_t *pEntity); + // spawn entities with internal mod function e.g. for re-arrange spawn order (0 - use engine parser, 1 - use mod parser) + int (*SV_LoadEntities)(const char *mapname, char *entities); + // update conveyor belt for clients + void (*SV_UpdatePlayerBaseVelocity)(edict_t *ent); + // The game .dll should return 1 if save game should be allowed + int (*SV_AllowSaveGame)(void); + // ONLY ADD NEW FUNCTIONS TO THE END OF THIS STRUCT. INTERFACE VERSION IS FROZEN AT 6 + // override trigger area checking and touching + int (*SV_TriggerTouch)(edict_t *pent, edict_t *trigger); + // some engine features can be enabled only through this function + unsigned int (*SV_CheckFeatures)(void); + // used for draw debug collisions for custom physic engine etc + void (*DrawDebugTriangles)(void); + // used for draw debug overlay (textured) + void (*DrawNormalTriangles)(void); + // used for draw debug messages (2d mode) + void (*DrawOrthoTriangles)(void); + // tracing entities with SOLID_CUSTOM mode on a server (not used by pmove code) + void (*ClipMoveToEntity)(edict_t *ent, const float *start, float *mins, float *maxs, const float *end, trace_t *trace); + // tracing entities with SOLID_CUSTOM mode on a server (only used by pmove code) + void (*ClipPMoveToEntity)(struct physent_s *pe, const float *start, float *mins, float *maxs, const float *end, struct pmtrace_s *tr); + // called at end the frame of SV_Physics call + void (*SV_EndFrame)(void); + // obsolete + void (*pfnPrepWorldFrame)(void); + // called through save\restore process + void (*pfnCreateEntitiesInRestoreList)(SAVERESTOREDATA *pSaveData, int levelMask, qboolean create_world); + // allocate custom string (e.g. using user implementation of stringtable, not engine strings) + string_t (*pfnAllocString)(const char *szValue); + // make custom string (e.g. using user implementation of stringtable, not engine strings) + string_t (*pfnMakeString)(const char *szValue); + // read custom string (e.g. using user implementation of stringtable, not engine strings) + const char *(*pfnGetString)(string_t iString); + // helper for restore custom decals that have custom message (e.g. Paranoia) + int (*pfnRestoreDecal)(struct decallist_s *entry, edict_t *pEdict, qboolean adjacent); + // handle custom trigger touching for player + void (*PM_PlayerTouch)(struct playermove_s *ppmove, edict_t *client); + // alloc or destroy model custom data (called only for dedicated servers, otherwise using an client version) + void (*Mod_ProcessUserData)(struct model_s *mod, qboolean create, const byte *buffer); + // select BSP-hull for trace with specified mins\maxs + void *(*SV_HullForBsp)(edict_t *ent, const float *mins, const float *maxs, float *offset); + // handle player custom think function + int (*SV_PlayerThink)(edict_t *ent, float frametime, double time); } physics_interface_t; #endif//PHYSINT_H diff --git a/inc/engine.h b/inc/engine.h index f8f7594..a174514 100644 --- a/inc/engine.h +++ b/inc/engine.h @@ -146,6 +146,9 @@ public: // initialize levels void levelInitialize (edict_t *entities, int max); + // shutdown levels + void levelShutdown (); + // display world line void drawLine (edict_t *ent, const Vector &start, const Vector &end, int width, int noise, const Color &color, int brightness, int speed, int life, DrawLine type = DrawLine::Simple); @@ -688,8 +691,7 @@ public: int close (DLSYM_HANDLE module) { if (m_self.handle () == module) { disable (); - - return m_dlclose (module); + return m_dlclose (module); } return m_dlclose (module); } diff --git a/inc/graph.h b/inc/graph.h index 52d1005..ddc0fe6 100644 --- a/inc/graph.h +++ b/inc/graph.h @@ -153,7 +153,6 @@ private: int m_lastJumpNode {}; int m_findWPIndex {}; int m_facingAtIndex {}; - int m_highestDamage[kGameTeamNum] {}; int m_autoSaveCount {}; float m_timeJumpStarted {}; @@ -265,14 +264,6 @@ public: return m_paths.length () / 2; } - int getHighestDamageForTeam (int team) const { - return cr::max (1, m_highestDamage[team]); - } - - void setHighestDamageForTeam (int team, int value) { - m_highestDamage[team] = value; - } - StringRef getAuthor () const { return m_graphAuthor; } diff --git a/inc/manager.h b/inc/manager.h index fe793f0..78f41f6 100644 --- a/inc/manager.h +++ b/inc/manager.h @@ -63,7 +63,7 @@ private: float m_maintainTime {}; // time to maintain bot creation float m_quotaMaintainTime {}; // time to maintain bot quota float m_grenadeUpdateTime {}; // time to update active grenades - float m_entityUpdateTime {}; // time to update intresting entities + float m_entityUpdateTime {}; // time to update interesting entities float m_plantSearchUpdateTime {}; // time to update for searching planted bomb float m_lastChatTime {}; // global chat time timestamp float m_timeBombPlanted {}; // time the bomb were planted @@ -81,7 +81,7 @@ private: bool m_roundOver {}; Array m_activeGrenades {}; // holds currently active grenades on the map - Array m_intrestingEntities {}; // holds currently intresting entities on the map + Array m_interestingEntities {}; // holds currently interesting entities on the map Deque m_saveBotNames {}; // bots names that persist upon changelevel Deque m_addRequests {}; // bot creation tab @@ -143,7 +143,7 @@ public: void initFilters (); void resetFilters (); void updateActiveGrenade (); - void updateIntrestingEntities (); + void updateInterestingEntities (); void captureChatRadio (const char *cmd, const char *arg, edict_t *ent); void notifyBombDefuse (); void execGameEntity (edict_t *ent); @@ -160,16 +160,16 @@ public: return m_activeGrenades; } - const Array &getIntrestingEntities () { - return m_intrestingEntities; + const Array &getInterestingEntities () { + return m_interestingEntities; } bool hasActiveGrenades () const { return !m_activeGrenades.empty (); } - bool hasIntrestingEntities () const { - return !m_intrestingEntities.empty (); + bool hasInterestingEntities () const { + return !m_interestingEntities.empty (); } bool checkTeamEco (int team) const { @@ -302,5 +302,31 @@ public: } }; +// bot async worker wrapper +class BotThreadWorker final : public Singleton { +private: + ThreadPool m_botWorker; + +public: + BotThreadWorker () = default; + ~BotThreadWorker () = default; + +public: + void shutdown (); + void startup (int workers); + +public: + template void enqueue (F &&fn) { + if (m_botWorker.threadCount () == 0) { + fn (); // no threads, no fun, just run task in current thread + return; + } + m_botWorker.enqueue (cr::forward (fn)); + } +}; + // explose global CR_EXPOSE_GLOBAL_SINGLETON (BotManager, bots); + +// expose async worker +CR_EXPOSE_GLOBAL_SINGLETON (BotThreadWorker, worker); diff --git a/inc/planner.h b/inc/planner.h index 0d7a0f4..eaab1bc 100644 --- a/inc/planner.h +++ b/inc/planner.h @@ -84,7 +84,7 @@ public: }; // A* algorithm for bots -class AStarAlgo final { +class AStarAlgo final : public DenyCopying { public: using HeuristicFn = Heuristic::Func; @@ -108,7 +108,7 @@ private: Array m_smoothedPath; private: - // cleares the currently built route + // clears the currently built route void clearRoute (); // can the node can be skipped? @@ -118,6 +118,10 @@ private: void postSmooth (NodeAdderFn onAddedNode); public: + explicit AStarAlgo (const int length) { + init (length); + } + AStarAlgo () = default; ~AStarAlgo () = default; @@ -180,6 +184,9 @@ public: private: // create floyd matrics + void syncRebuild (); + + // async rebuild void rebuild (); public: @@ -201,6 +208,9 @@ public: // dijkstra shortest path algorithm class DijkstraAlgo final { +private: + mutable Mutex m_cs {}; + private: using Route = Twin ; @@ -215,11 +225,6 @@ public: DijkstraAlgo () = default; ~DijkstraAlgo () = default; - -private: - // reset pathfinder state to defaults - void resetState (); - public: // initialize dijkstra with valid path length void init (const int length); @@ -236,7 +241,6 @@ class PathPlanner : public Singleton { private: UniquePtr m_dijkstra; UniquePtr m_floyd; - UniquePtr m_astar; bool m_memoryLimitHit {}; public: @@ -261,17 +265,15 @@ public: return m_floyd.get (); } - // get the floyd algo - decltype (auto) getAStar () { - return m_astar.get (); - } - public: // do the pathfinding bool find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance = nullptr); // distance between two nodes with pathfinder - int dist (int srcIndex, int destIndex); + float dist (int srcIndex, int destIndex); + + // get the precise distanace regardless of cvar + float preciseDistance (int srcIndex, int destIndex); }; CR_EXPOSE_GLOBAL_SINGLETON (PathPlanner, planner); diff --git a/inc/practice.h b/inc/practice.h index a06c9ef..c037ad3 100644 --- a/inc/practice.h +++ b/inc/practice.h @@ -80,17 +80,22 @@ public: DangerSaveRestore (const DangerStorage &ds, const PracticeData &pd) : danger (ds), data (pd) {} }; +private: HashMap m_data {}; int32_t m_teamHighestDamage[kGameTeamNum] {}; + // avoid concurrent access to practice + mutable Mutex m_damageUpdateLock {}; + public: BotPractice () = default; ~BotPractice () = default; private: - inline bool exists (int32_t team, int32_t start, int32_t goal) const { + bool exists (int32_t team, int32_t start, int32_t goal) const { return m_data.exists ({ start, goal, team }); } + void syncUpdate (); public: int32_t getIndex (int32_t team, int32_t start, int32_t goal); @@ -102,17 +107,21 @@ public: int32_t getDamage (int32_t team, int32_t start, int32_t goal); void setDamage (int32_t team, int32_t start, int32_t goal, int32_t value); + // interlocked get damage + float plannerGetDamage (int32_t team, int32_t start, int32_t goal, bool addTeamHighestDamage); + public: void update (); void load (); void save (); public: - int32_t getHighestDamageForTeam (int32_t team) const { - return cr::max (1, m_teamHighestDamage[team]); + template U getHighestDamageForTeam (int32_t team) const { + return static_cast (cr::max (1, m_teamHighestDamage[team])); } void setHighestDamageForTeam (int32_t team, int32_t value) { + MutexScopedLock lock (m_damageUpdateLock); m_teamHighestDamage[team] = value; } }; diff --git a/inc/support.h b/inc/support.h index e6ede2f..ee59ee2 100644 --- a/inc/support.h +++ b/inc/support.h @@ -8,6 +8,9 @@ #pragma once class BotSupport final : public Singleton { +private: + mutable Mutex m_cs {}; + private: bool m_needToSendWelcome {}; float m_welcomeReceiveTime {}; @@ -78,6 +81,9 @@ public: // generates ping bitmask for SVC_PINGS message int getPingBitmask (edict_t *ent, int loss, int ping); + // calculate our own pings for all the players + void syncCalculatePings (); + // calculate our own pings for all the players void calculatePings (); diff --git a/inc/yapb.h b/inc/yapb.h index 3fcf2f4..38c03b1 100644 --- a/inc/yapb.h +++ b/inc/yapb.h @@ -663,6 +663,10 @@ class Bot final { public: friend class BotManager; +private: + mutable Mutex m_pathFindLock {}; + mutable Mutex m_predictLock {}; + private: uint32_t m_states {}; // sensing bitstates uint32_t m_collideMoves[kMaxCollideMoves] {}; // sorted array of movements @@ -689,8 +693,10 @@ private: int m_tryOpenDoor {}; // attempt's to open the door int m_liftState {}; // state of lift handling int m_radioSelect {}; // radio entry - int m_lastPredictIndex {}; // last predicted index - + + int m_lastPredictIndex { kInvalidNodeIndex }; // last predicted path index + int m_lastPredictLength {}; // last predicted path length + float m_headedTime {}; float m_prevTime {}; // time previously checked movement speed float m_heavyTimestamp {}; // is it time to execute heavy-weight functions @@ -747,6 +753,7 @@ private: bool m_moveToGoal {}; // bot currently moving to goal?? bool m_isStuck {}; // bot is stuck + bool m_isStale {}; // bot is leaving server bool m_isReloading {}; // bot is reloading a gun bool m_forceRadio {}; // should bot use radio anyway? bool m_defendedBomb {}; // defend action issued @@ -773,6 +780,7 @@ private: FindPath m_pathType {}; // which pathfinder to use uint8_t m_enemyParts {}; // visibility flags TraceResult m_lastTrace[TraceChannel::Num] {}; // last trace result + UniquePtr m_planner; edict_t *m_pickupItem {}; // pointer to entity of item to use/pickup edict_t *m_itemIgnore {}; // pointer to entity to ignore for pickup @@ -888,7 +896,6 @@ private: void doPlayerAvoidance (const Vector &normal); void selectCampButtons (int index); - void markStale (); void instantChatter (int type); void update (); void runMovement (); @@ -920,6 +927,7 @@ private: void findShortestPath (int srcIndex, int destIndex); void calculateFrustum (); void findPath (int srcIndex, int destIndex, FindPath pathType = FindPath::Fast); + void syncFindPath (int srcIndex, int destIndex, FindPath pathType); void debugMsgInternal (const char *str); void frame (); void resetCollision (); @@ -933,6 +941,7 @@ private: void decideFollowUser (); void attackMovement (); void findValidNode (); + void setPathOrigin (); void fireWeapons (); void selectWeapons (float distance, int index, int id, int choosen); void focusEnemy (); @@ -940,6 +949,9 @@ private: void selectSecondary (); void selectWeaponById (int id); void selectWeaponByIndex (int index); + void refreshEnemyPredict (); + void syncUpdatePredictedIndex (); + void updatePredictedIndex (); void completeTask (); void executeTasks (); @@ -1094,11 +1106,13 @@ public: bool m_hasNVG {}; // does bot has nightvision goggles bool m_usesNVG {}; // does nightvision goggles turned on bool m_hasC4 {}; // does bot has c4 bomb + bool m_hasHostage {}; // does bot owns some hostages bool m_hasProgressBar {}; // has progress bar on a HUD bool m_jumpReady {}; // is double jump ready bool m_canChooseAimDirection {}; // can choose aiming direction bool m_isEnemyReachable {}; // direct line to enemy bool m_kickedByRotation {}; // is bot kicked due to rotation ? + bool m_kickMeFromServer {}; // kick the bot off the server? edict_t *m_doubleJumpEntity {}; // pointer to entity that request double jump edict_t *m_radioEntity {}; // pointer to entity issuing a radio command @@ -1121,7 +1135,11 @@ public: public: Bot (edict_t *bot, int difficulty, int personality, int team, int skin); - ~Bot () = default; + + // need to wait until all threads will finish it's work before terminating bot object + ~Bot () { + MutexScopedLock lock (m_pathFindLock); + } public: void logic (); /// the things that can be executed while skipping frames @@ -1154,7 +1172,7 @@ public: void resetDoubleJump (); void startDoubleJump (edict_t *ent); void sendBotToOrigin (const Vector &origin); - + void markStale (); bool hasHostage (); bool usesRifle (); bool usesPistol (); diff --git a/meson.build b/meson.build index 88d62e1..b6a71f0 100644 --- a/meson.build +++ b/meson.build @@ -171,7 +171,7 @@ if cxx == 'clang' or cxx == 'gcc' # link needed libraries if os == 'linux' ldflags += [ - '-lm','-ldl' + '-lm','-ldl', '-lpthread' ] elif os == 'windows' if cxx == 'gcc' or (cross and cxx == 'clang') diff --git a/src/analyze.cpp b/src/analyze.cpp index 04e5584..c4b54e0 100644 --- a/src/analyze.cpp +++ b/src/analyze.cpp @@ -126,6 +126,7 @@ void GraphAnalyze::suspend () { m_updateInterval = 0.0f; m_isAnalyzing = false; m_isAnalyzed = false; + m_basicsCreated = false; } void GraphAnalyze::finish () { @@ -144,7 +145,7 @@ void GraphAnalyze::finish () { ctrl.msg ("Complete map analyzation."); - // autosave bots graph + // auto save bots graph if (cv_graph_analyze_auto_save.bool_ ()) { if (!graph.saveGraphData ()) { ctrl.msg ("Can't save analyzed graph. Internal error."); @@ -170,7 +171,7 @@ void GraphAnalyze::optimize () { } cleanup (); - // clear the uselss connections + // clear the useless connections if (cv_graph_analyze_clean_paths_on_finish.bool_ ()) { for (auto i = 0; i < graph.length (); ++i) { graph.clearConnections (i); diff --git a/src/botlib.cpp b/src/botlib.cpp index 2d1579d..b0b970a 100644 --- a/src/botlib.cpp +++ b/src/botlib.cpp @@ -13,7 +13,7 @@ ConVar cv_user_follow_percent ("yb_user_follow_percent", "20", "Specifies the pe ConVar cv_user_max_followers ("yb_user_max_followers", "1", "Specifies how many bots can follow a single user.", true, 0.0f, static_cast (kGameMaxPlayers / 4)); ConVar cv_jasonmode ("yb_jasonmode", "0", "If enabled, all bots will be forced only the knife, skipping weapon buying routines."); -ConVar cv_radio_mode ("yb_radio_mode", "2", "Allows bots to use radio or chattter.\nAllowed values: '0', '1', '2'.\nIf '0', radio and chatter is disabled.\nIf '1', only radio allowed.\nIf '2' chatter and radio allowed.", true, 0.0f, 2.0f); +ConVar cv_radio_mode ("yb_radio_mode", "2", "Allows bots to use radio or chatter.\nAllowed values: '0', '1', '2'.\nIf '0', radio and chatter is disabled.\nIf '1', only radio allowed.\nIf '2' chatter and radio allowed.", true, 0.0f, 2.0f); ConVar cv_economics_rounds ("yb_economics_rounds", "1", "Specifies whether bots able to use team economics, like do not buy any weapons for whole team to keep money for better guns."); ConVar cv_economics_disrespect_percent ("yb_economics_disrespect_percent", "25", "Allows bots to ignore economics and buy weapons with disrespect of economics.", true, 0.0f, 100.0f); @@ -44,7 +44,7 @@ ConVar cv_pickup_best ("yb_pickup_best", "1", "Allows or disallows bots to picku ConVar cv_ignore_objectives ("yb_ignore_objectives", "0", "Allows or disallows bots to do map objectives, i.e. plant/defuse bombs, and saves hostages."); ConVar cv_random_knife_attacks ("yb_random_knife_attacks", "1", "Allows or disallows the ability for random knife attacks when bot is rushing and no enemy is nearby."); -ConVar cv_max_nodes_for_predict ("yb_max_nodes_for_predict", "30", "Maximum number for path length, to predict the enemy.", true, 15.0f, 256.0f); +ConVar cv_max_nodes_for_predict ("yb_max_nodes_for_predict", "20", "Maximum number for path length, to predict the enemy.", true, 15.0f, 256.0f); // game console variables ConVar mp_c4timer ("mp_c4timer", nullptr, Var::GameRef); @@ -103,7 +103,7 @@ bool Bot::seesItem (const Vector &destination, const char *classname) { game.testLine (getEyesPos (), destination, TraceIgnore::None, ent (), &tr); // check if line of sight to object is not blocked (i.e. visible) - if (tr.flFraction < 1.0f && tr.pHit) { + if (tr.flFraction < 1.0f && tr.pHit && !tr.fStartSolid) { return strcmp (tr.pHit->v.classname.chars (), classname) == 0; } return true; @@ -217,7 +217,7 @@ void Bot::checkBreakablesAround () { } auto radius = cv_object_destroy_radius.float_ (); - // check if we're have some breakbles in 400 units range + // check if we're have some breakables in 400 units range for (const auto &breakable : game.getBreakables ()) { bool ignoreBreakable = false; @@ -325,23 +325,23 @@ void Bot::updatePickups () { // this function finds Items to collect or use in the near of a bot // don't try to pickup anything while on ladder or trying to escape from bomb... - if ((m_states & Sense::SeeingEnemy) || isOnLadder () || getCurrentTaskId () == Task::EscapeFromBomb || !cv_pickup_best.bool_ () || cv_jasonmode.bool_ () || !bots.hasIntrestingEntities ()) { + if ((m_states & Sense::SeeingEnemy) || isOnLadder () || getCurrentTaskId () == Task::EscapeFromBomb || !cv_pickup_best.bool_ () || cv_jasonmode.bool_ () || !bots.hasInterestingEntities ()) { m_pickupItem = nullptr; m_pickupType = Pickup::None; return; } - const auto &intresting = bots.getIntrestingEntities (); + const auto &interesting = bots.getInterestingEntities (); const float radius = cr::sqrf (cv_object_pickup_radius.float_ ()); if (!game.isNullEntity (m_pickupItem)) { bool itemExists = false; auto pickupItem = m_pickupItem; - for (auto &ent : intresting) { + for (auto &ent : interesting) { - // in the periods of updating intresting entities we can get fake ones, that already were picked up, so double check if drawn + // in the periods of updating interesting entities we can get fake ones, that already were picked up, so double check if drawn if (ent->v.effects & EF_NODRAW) { continue; } @@ -376,14 +376,14 @@ void Bot::updatePickups () { m_pickupItem = nullptr; m_pickupType = Pickup::None; - for (const auto &ent : intresting) { + for (const auto &ent : interesting) { bool allowPickup = false; // assume can't use it until known otherwise // get the entity origin const auto &origin = game.getEntityOrigin (ent); if ((ent->v.effects & EF_NODRAW) || ent == m_itemIgnore || cr::abs (origin.z - pev->origin.z) > 96.0f) { - continue; // someone owns this weapon or it hasn't respawned yet + continue; // someone owns this weapon or it hasn't re-spawned yet } // too far from us ? @@ -692,7 +692,7 @@ void Bot::ensureEntitiesClear () { if (currentTask == Task::PickupItem || (m_states & Sense::PickupItem)) { if (!game.isNullEntity (m_pickupItem) && !m_hasProgressBar) { - m_itemIgnore = m_pickupItem; // clear these pointers, bot mingh be stuck getting to them + m_itemIgnore = m_pickupItem; // clear these pointers, bot might be stuck getting to them } m_itemCheckTime = game.time () + 5.0f; @@ -720,7 +720,7 @@ void Bot::ensureEntitiesClear () { Vector Bot::getCampDirection (const Vector &dest) { // this function check if view on last enemy position is blocked - replace with better vector then - // mostly used for getting a good camping direction vector if not camping on a camp waypoint + // mostly used for getting a good camping direction vector if not camping on a camp node TraceResult tr {}; const Vector &src = getEyesPos (); @@ -750,7 +750,7 @@ Vector Bot::getCampDirection (const Vector &dest) { if (link.index == kInvalidNodeIndex) { continue; } - auto distance = static_cast (planner.dist (link.index, enemyIndex)); + auto distance = planner.dist (link.index, enemyIndex); if (distance < minDistance) { minDistance = distance; @@ -771,7 +771,7 @@ Vector Bot::getCampDirection (const Vector &dest) { } void Bot::showChatterIcon (bool show, bool disconnect) { - // this function depending on show boolen, shows/remove chatter, icon, on the head of bot. + // this function depending on show boolean, shows/remove chatter, icon, on the head of bot. if (!game.is (GameFlags::HasBotVoice) || cv_radio_mode.int_ () != 2) { return; @@ -1588,6 +1588,78 @@ void Bot::overrideConditions () { } } +void Bot::syncUpdatePredictedIndex () { + auto wipePredict = [this] () { + m_lastPredictIndex = kInvalidNodeIndex; + m_lastPredictLength = 0; + }; + + if (!m_predictLock.tryLock ()) { + return; // allow only single instance of search per-bot + } + ScopedUnlock unlock (m_predictLock); + + auto lastEnemyOrigin = m_lastEnemyOrigin; + auto currentNodeIndex = m_currentNodeIndex; + auto botOrigin = pev->origin; + + if (lastEnemyOrigin.empty () || !vistab.isReady ()) { + wipePredict (); + return; + } + + int destIndex = graph.getNearest (lastEnemyOrigin); + int bestIndex = kInvalidNodeIndex; + + if (destIndex == kInvalidNodeIndex) { + return; + } + int pathLength = 0; + + planner.find (destIndex, currentNodeIndex, [&] (int index) { + ++pathLength; + + if (vistab.visible (currentNodeIndex, index) && botOrigin.distanceSq (graph[index].origin) > cr::sqrf (256.0f)) { + bestIndex = index; + return false; + } + return true; + }); + + if (bestIndex != kInvalidNodeIndex) { + m_lastPredictIndex = bestIndex; + m_lastPredictLength = pathLength; + + return; + } + wipePredict (); +} + +void Bot::updatePredictedIndex () { + worker.enqueue ([this] () { + syncUpdatePredictedIndex (); + }); +} + +void Bot::refreshEnemyPredict () { + if (game.isNullEntity (m_enemy) && !game.isNullEntity (m_lastEnemy) && !m_lastEnemyOrigin.empty ()) { + const auto distanceToLastEnemySq = m_lastEnemyOrigin.distanceSq (pev->origin); + + if (distanceToLastEnemySq > cr::sqrf (384.0f) && (distanceToLastEnemySq < cr::sqrf (1600.0f) || usesSniper ())) { + m_aimFlags |= AimFlags::PredictPath; + } + const bool denyLastEnemy = pev->velocity.lengthSq2d () > 0.0f && distanceToLastEnemySq < cr::sqrf (256.0f); + + if (!denyLastEnemy && seesEntity (m_lastEnemyOrigin, true)) { + m_aimFlags |= AimFlags::LastEnemy; + } + } + + if (m_aimFlags & AimFlags::PredictPath) { + updatePredictedIndex (); + } +} + void Bot::setConditions () { // this function carried out each frame. does all of the sensing, calculates emotions and finally sets the desired // action after applying all of the Filters @@ -1648,7 +1720,7 @@ void Bot::setConditions () { } } - // if no more enemies found AND bomb planted, switch to knife to get to bombplace faster + // if no more enemies found AND bomb planted, switch to knife to get to bomb place faster if (m_team == Team::CT && !usesKnife () && m_numEnemiesLeft == 0 && bots.isBombPlanted ()) { selectWeaponById (Weapon::Knife); m_plantedBombNodeIndex = getNearestToPlantedBomb (); @@ -1665,6 +1737,9 @@ void Bot::setConditions () { m_lastVictim = nullptr; } + m_numFriendsLeft = numFriendsNear (pev->origin, kInfiniteDistance); + m_numEnemiesLeft = numEnemiesNear (pev->origin, kInfiniteDistance); + auto clearLastEnemy = [&] () { m_lastEnemyOrigin = nullptr; m_lastEnemy = nullptr; @@ -1688,26 +1763,7 @@ void Bot::setConditions () { else if (m_heardSoundTime < game.time ()) { m_states &= ~Sense::HearingEnemy; } - - if (game.isNullEntity (m_enemy) && !game.isNullEntity (m_lastEnemy) && !m_lastEnemyOrigin.empty () && m_seeEnemyTime + 0.5f < game.time ()) { - m_lastPredictIndex = kInvalidNodeIndex; - - auto distanceToLastEnemySq = m_lastEnemyOrigin.distanceSq (pev->origin); - - if (distanceToLastEnemySq < cr::sqrf (1600.0f)) { - auto pathLength = 0; - auto nodeIndex = findAimingNode (m_lastEnemyOrigin, pathLength); - - if (graph.exists (nodeIndex) && pathLength < cv_max_nodes_for_predict.int_ () && pev->origin.distanceSq (graph[nodeIndex].origin) > cr::sqrf (384.0f)) { - m_aimFlags |= AimFlags::PredictPath; - m_lastPredictIndex = nodeIndex; - } - } - - if (seesEntity (m_lastEnemyOrigin, true)) { - m_aimFlags |= AimFlags::LastEnemy; - } - } + refreshEnemyPredict (); // check for grenades depending on difficulty if (rg.chance (cr::max (25, m_difficulty * 25))) { @@ -1818,7 +1874,7 @@ void Bot::filterTasks () { } // if half of the round is over, allow hunting - if (getCurrentTaskId () != Task::EscapeFromBomb && game.isNullEntity (m_enemy) && !m_isVIP && bots.getRoundMidTime () < game.time () && !hasHostage () && !m_isUsingGrenade && m_currentNodeIndex != graph.getNearest (m_lastEnemyOrigin) && m_personality != Personality::Careful && !cv_ignore_enemies.bool_ ()) { + if (getCurrentTaskId () != Task::EscapeFromBomb && game.isNullEntity (m_enemy) && !m_isVIP && bots.getRoundMidTime () < game.time () && !m_hasHostage && !m_isUsingGrenade && m_currentNodeIndex != graph.getNearest (m_lastEnemyOrigin) && m_personality != Personality::Careful && !cv_ignore_enemies.bool_ ()) { float desireLevel = 4096.0f - ((1.0f - tempAgression) * m_lastEnemyOrigin.distance (pev->origin)); desireLevel = (100.0f * desireLevel) / 4096.0f; @@ -2056,13 +2112,13 @@ bool Bot::reactOnEnemy () { } int enemyIndex = graph.getNearest (m_enemy->v.origin); - auto lineDist = m_enemy->v.origin.distance2d (pev->origin); - auto pathDist = static_cast (planner.dist (ownIndex, enemyIndex)); + auto lineDist = m_enemy->v.origin.distance (pev->origin); + auto pathDist = planner.preciseDistance (ownIndex, enemyIndex); - if (pathDist - lineDist > (planner.hasRealPathDistance () ? 112.0f : 32.0f) || isOnLadder ()) { + if (pathDist - lineDist > 112.0f || isOnLadder ()) { m_isEnemyReachable = false; } - else if (vistab.visible (ownIndex, enemyIndex)) { + else { m_isEnemyReachable = true; } m_enemyReachableTimer = game.time () + 1.0f; @@ -2088,7 +2144,7 @@ void Bot::checkRadioQueue () { // don't allow bot listen you if bot is busy - if (getCurrentTaskId () == Task::DefuseBomb || getCurrentTaskId () == Task::PlantBomb || hasHostage () || m_hasC4) { + if (getCurrentTaskId () == Task::DefuseBomb || getCurrentTaskId () == Task::PlantBomb || m_hasHostage || m_hasC4) { m_radioOrder = 0; return; } @@ -2631,30 +2687,54 @@ void Bot::updateAimDir () { } else if (flags & AimFlags::PredictPath) { bool changePredictedEnemy = true; + bool predictFailed = false; if (m_timeNextTracking < game.time () && m_trackingEdict == m_lastEnemy) { changePredictedEnemy = false; } - if (changePredictedEnemy) { - auto pathLength = 0; - auto aimNode = graph.exists (m_lastPredictIndex) ? m_lastPredictIndex : findAimingNode (m_lastEnemyOrigin, pathLength); + auto doFailPredict = [this] () { + m_aimFlags &= ~AimFlags::PredictPath; + m_trackingEdict = nullptr; + }; - if (graph.exists (aimNode) && pathLength < cv_max_nodes_for_predict.int_ ()) { - m_lookAt = graph[aimNode].origin; + if (changePredictedEnemy) { + int pathLength = m_lastPredictLength; + int predictNode = m_lastPredictIndex; + + if (predictNode != kInvalidNodeIndex) { + TraceResult tr; + game.testLine (getEyesPos (), graph[predictNode].origin, TraceIgnore::Everything, ent (), &tr); + + if (tr.flFraction < 0.2f) { + pathLength = kInfiniteDistanceLong; + } + } + + if (pathLength < cv_max_nodes_for_predict.int_ ()) { + m_lookAt = graph[predictNode].origin; m_lookAtSafe = m_lookAt; - m_timeNextTracking = game.time () + 0.75f; + m_timeNextTracking = game.time () + 0.25f; m_trackingEdict = m_lastEnemy; + + // feel free to fire if shootable + if (!usesSniper () && lastEnemyShootable ()) { + m_wantsToFire = true; + } } else { - if (!m_lookAtSafe.empty ()) { - m_lookAt = m_lookAtSafe; - } + doFailPredict (); + predictFailed = true; } } else { - m_lookAt = m_lookAtSafe; + if (predictFailed) { + doFailPredict (); + } + else { + m_lookAt = m_lookAtSafe; + } } } else if (flags & AimFlags::Camp) { @@ -2777,8 +2857,6 @@ void Bot::frame () { if (m_slowFrameTimestamp > game.time ()) { return; } - m_numFriendsLeft = numFriendsNear (pev->origin, kInfiniteDistance); - m_numEnemiesLeft = numEnemiesNear (pev->origin, kInfiniteDistance); if (bots.isBombPlanted () && m_team == Team::CT && m_notKilled) { const Vector &bombPosition = graph.getBombOrigin (); @@ -2801,7 +2879,7 @@ void Bot::frame () { // kick the bot if stay time is over, the quota maintain will add new bot for us later if (cv_rotate_bots.bool_ () && m_stayTime < game.time ()) { - m_kickedByRotation = true; // kicked by roration, so not save bot name if save bot names is active + m_kickedByRotation = true; // kicked by rotation, so not save bot name if save bot names is active kick (); return; @@ -2816,6 +2894,12 @@ void Bot::frame () { } void Bot::update () { + + // kick bot from server if requested + if (m_kickMeFromServer) { + kick (); + return; + } pev->button = 0; m_moveSpeed = 0.0f; @@ -2834,6 +2918,9 @@ void Bot::update () { m_hasC4 = false; } } + else if (m_team == Team::CT && game.mapIs (MapFlags::HostageRescue)) { + m_hasHostage = hasHostage (); + } // is bot movement enabled bool botMovement = false; @@ -2869,7 +2956,7 @@ void Bot::update () { } checkMsgQueue (); - if (botMovement) { + if (!m_isStale && botMovement) { logic (); // execute main code } else if (pev->maxspeed < 10.0f) { @@ -2928,7 +3015,7 @@ void Bot::normal_ () { } // bots rushing with knife, when have no enemy (thanks for idea to nicebot project) - if (cv_random_knife_attacks.bool_ () && usesKnife () && (game.isNullEntity (m_lastEnemy) || !util.isAlive (m_lastEnemy)) && game.isNullEntity (m_enemy) && m_knifeAttackTime < game.time () && !hasHostage () && !hasShield () && numFriendsNear (pev->origin, 96.0f) == 0) { + if (cv_random_knife_attacks.bool_ () && usesKnife () && (game.isNullEntity (m_lastEnemy) || !util.isAlive (m_lastEnemy)) && game.isNullEntity (m_enemy) && m_knifeAttackTime < game.time () && !m_hasHostage && !hasShield () && numFriendsNear (pev->origin, 96.0f) == 0) { if (rg.chance (40)) { pev->button |= IN_ATTACK; } @@ -2970,7 +3057,7 @@ void Bot::normal_ () { if ((m_pathFlags & NodeFlag::Camp) && !game.is (GameFlags::CSDM) && cv_camping_allowed.bool_ () && !isKnifeMode ()) { // check if bot has got a primary weapon and hasn't camped before - if (hasPrimaryWeapon () && m_timeCamping + 10.0f < game.time () && !hasHostage ()) { + if (hasPrimaryWeapon () && m_timeCamping + 10.0f < game.time () && !m_hasHostage) { bool campingAllowed = true; // Check if it's not allowed for this team to camp here @@ -3016,7 +3103,7 @@ void Bot::normal_ () { m_campDirection = 0; // tell the world we're camping - if (rg.chance (40)) { + if (rg.chance (25)) { pushRadioMessage (Radio::ImInPosition); } m_moveToGoal = false; @@ -3031,7 +3118,7 @@ void Bot::normal_ () { // some goal waypoints are map dependant so check it out... if (game.mapIs (MapFlags::HostageRescue)) { // CT Bot has some hostages following? - if (m_team == Team::CT && hasHostage ()) { + if (m_team == Team::CT && m_hasHostage) { // and reached a rescue point? if (m_pathFlags & NodeFlag::Rescue) { m_hostages.clear (); @@ -3127,7 +3214,7 @@ void Bot::normal_ () { } float shiftSpeed = getShiftSpeed (); - if ((!cr::fzero (m_moveSpeed) && m_moveSpeed > shiftSpeed) && (cv_walking_allowed.bool_ () && mp_footsteps.bool_ ()) && m_difficulty >= Difficulty::Normal && !(m_aimFlags & AimFlags::Enemy) && (m_heardSoundTime + 6.0f >= game.time () || (m_states & Sense::SuspectEnemy)) && numEnemiesNear (pev->origin, 768.0f) >= 1 && !isKnifeMode () && !bots.isBombPlanted ()) { + if (!m_isStuck && (!cr::fzero (m_moveSpeed) && m_moveSpeed > shiftSpeed) && (cv_walking_allowed.bool_ () && mp_footsteps.bool_ ()) && m_difficulty >= Difficulty::Normal && !(m_aimFlags & AimFlags::Enemy) && (m_heardSoundTime + 6.0f >= game.time () || (m_states & Sense::SuspectEnemy)) && numEnemiesNear (pev->origin, 768.0f) >= 1 && !isKnifeMode () && !bots.isBombPlanted ()) { m_moveSpeed = shiftSpeed; } @@ -5099,7 +5186,7 @@ void Bot::takeBlind (int alpha) { void Bot::updatePracticeValue (int damage) { // gets called each time a bot gets damaged by some enemy. tries to achieve a statistic about most/less dangerous - // waypoints for a destination goal used for pathfinding + // nodes for a destination goal used for pathfinding if (graph.length () < 1 || graph.hasChanged () || m_chosenGoalIndex < 0 || m_prevGoalIndex < 0) { return; @@ -5109,7 +5196,7 @@ void Bot::updatePracticeValue (int damage) { // max goal value constexpr int maxGoalValue = PracticeLimit::Goal; - // only rate goal waypoint if bot died because of the damage + // only rate goal node if bot died because of the damage // FIXME: could be done a lot better, however this cares most about damage done by sniping or really deadly weapons if (health - damage <= 0) { practice.setValue (m_team, m_chosenGoalIndex, m_prevGoalIndex, cr::clamp (practice.getValue (m_team, m_chosenGoalIndex, m_prevGoalIndex) - health / 20, -maxGoalValue, maxGoalValue)); @@ -5117,7 +5204,7 @@ void Bot::updatePracticeValue (int damage) { } void Bot::updatePracticeDamage (edict_t *attacker, int damage) { - // this function gets called each time a bot gets damaged by some enemy. sotores the damage (teamspecific) done by victim. + // this function gets called each time a bot gets damaged by some enemy. stores the damage (team-specific) done by victim. if (!util.isPlayer (attacker)) { return; @@ -5148,7 +5235,7 @@ void Bot::updatePracticeDamage (edict_t *attacker, int damage) { if (victimIndex == kInvalidNodeIndex) { victimIndex = findNearestNode (); } - + if (m_healthValue > 20.0f) { if (victimTeam == Team::Terrorist || victimTeam == Team::CT) { practice.setDamage (victimIndex, victimIndex, victimIndex, cr::clamp (practice.getDamage (victimTeam, victimIndex, victimIndex), 0, maxDamageValue)); @@ -5159,8 +5246,8 @@ void Bot::updatePracticeDamage (edict_t *attacker, int damage) { // store away the damage done int damageValue = cr::clamp (practice.getDamage (m_team, victimIndex, attackerIndex) + damage / updateDamage, 0, maxDamageValue); - if (damageValue > graph.getHighestDamageForTeam (m_team)) { - graph.setHighestDamageForTeam (m_team, damageValue); + if (damageValue > practice.getHighestDamageForTeam (m_team)) { + practice.setHighestDamageForTeam (m_team, damageValue); } practice.setDamage (m_team, victimIndex, attackerIndex, damageValue); } diff --git a/src/combat.cpp b/src/combat.cpp index 988de02..54c40c1 100644 --- a/src/combat.cpp +++ b/src/combat.cpp @@ -262,25 +262,25 @@ bool Bot::lookupEnemies () { if (cv_attack_monsters.bool_ ()) { // search the world for monsters... - for (const auto &intresting : bots.getIntrestingEntities ()) { - if (!util.isMonster (intresting)) { + for (const auto &interesting : bots.getInterestingEntities ()) { + if (!util.isMonster (interesting)) { continue; } // check the engine PVS - if (!isEnemyInFrustum (intresting) || !game.checkVisibility (intresting, set)) { + if (!isEnemyInFrustum (interesting) || !game.checkVisibility (interesting, set)) { continue; } // see if bot can see the monster... - if (seesEnemy (intresting)) { + if (seesEnemy (interesting)) { // higher priority for big monsters - float scaleFactor = (1.0f / calculateScaleFactor (intresting)); - float distance = intresting->v.origin.distanceSq (pev->origin) * scaleFactor; + float scaleFactor = (1.0f / calculateScaleFactor (interesting)); + float distance = interesting->v.origin.distanceSq (pev->origin) * scaleFactor; if (distance < nearestDistance) { nearestDistance = distance; - newEnemy = intresting; + newEnemy = interesting; } } } @@ -347,7 +347,7 @@ bool Bot::lookupEnemies () { return true; } else { - if (m_seeEnemyTime + 3.0f < game.time () && (m_hasC4 || hasHostage () || !game.isNullEntity (m_targetEntity))) { + if (m_seeEnemyTime + 3.0f < game.time () && (m_hasC4 || m_hasHostage || !game.isNullEntity (m_targetEntity))) { pushRadioMessage (Radio::EnemySpotted); } m_targetEntity = nullptr; // stop following when we see an enemy... @@ -516,6 +516,7 @@ Vector Bot::getBodyOffsetError (float distance) { } else if (m_enemyParts & Visibility::Body) { spot = m_enemy->v.origin; + spot.z += 3.0f; } else if (m_enemyParts & Visibility::Other) { spot = m_enemyOrigin; @@ -752,7 +753,7 @@ bool Bot::isPenetrableObstacle3 (const Vector &dest) { bool Bot::needToPauseFiring (float distance) { // returns true if bot needs to pause between firing to compensate for punchangle & weapon spread - if (usesSniper () || m_isUsingGrenade) { + if (usesSniper () || m_isUsingGrenade || ((m_states & Sense::SuspectEnemy) && distance < 400.0f)) { return false; } @@ -1949,7 +1950,7 @@ void Bot::checkGrenadesThrow () { break; } - for (const auto predict : predicted) { + for (const auto &predict : predicted) { allowThrowing = true; if (!graph.exists (predict)) { diff --git a/src/control.cpp b/src/control.cpp index e70c2da..bf98544 100644 --- a/src/control.cpp +++ b/src/control.cpp @@ -1192,7 +1192,7 @@ int BotControl::menuCommands (int item) { switch (item) { case 1: case 2: - if (util.findNearestPlayer (reinterpret_cast (&m_djump), m_ent, 600.0f, true, true, true, true, false) && !m_djump->m_hasC4 && !m_djump->hasHostage ()) { + if (util.findNearestPlayer (reinterpret_cast (&m_djump), m_ent, 600.0f, true, true, true, true, false) && !m_djump->m_hasC4 && !m_djump->m_hasHostage) { if (item == 1) { m_djump->startDoubleJump (m_ent); } @@ -1765,7 +1765,7 @@ bool BotControl::executeCommands () { for (auto &item : m_cmds) { if (!hasSecondArg) { - cmd = item.name.split ("/")[0]; + cmd = item.name.split ("/").first (); } if (!hasSecondArg || aliasMatch (item.name, strValue (2), cmd)) { @@ -1804,7 +1804,7 @@ bool BotControl::executeCommands () { msg ("valid commands are: "); for (auto &item : m_cmds) { - msg (" %s - %s", item.name.split ("/")[0], conf.translate (item.help)); + msg (" %-14.11s - %s", item.name.split ("/").first (), String (conf.translate (item.help)).lowercase ()); } return true; } diff --git a/src/engine.cpp b/src/engine.cpp index b8307ee..f98d5c3 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -9,6 +9,7 @@ ConVar cv_csdm_mode ("yb_csdm_mode", "0", "Enables or disables CSDM / FFA mode for bots.\nAllowed values: '0', '1', '2', '3'.\nIf '0', CSDM / FFA mode is auto-detected.\nIf '1', CSDM mode is enabled, but FFA is disabled.\nIf '2', CSDM and FFA mode is enabled.\nIf '3', CSDM and FFA mode is disabled.", true, 0.0f, 3.0f); ConVar cv_breakable_health_limit ("yb_breakable_health_limit", "500.0", "Specifies the maximum health of breakable object, that bot will consider to destroy.", true, 1.0f, 3000.0); +ConVar cv_threadpool_workers ("yb_threadpool_workers", "-1", "Maximum number of threads bot will run to process some tasks. -1 means half of CPU cores used.", true, -1.0f, static_cast (plat.hardwareConcurrency ())); ConVar sv_skycolor_r ("sv_skycolor_r", nullptr, Var::GameRef); ConVar sv_skycolor_g ("sv_skycolor_g", nullptr, Var::GameRef); @@ -53,6 +54,12 @@ void Game::precache () { void Game::levelInitialize (edict_t *entities, int max) { // this function precaches needed models and initialize class variables + // re-initialize bot's array + bots.destroy (); + + // startup threaded worker + worker.startup (cv_threadpool_workers.int_ ()); + m_spawnCount[Team::CT] = 0; m_spawnCount[Team::Terrorist] = 0; @@ -83,9 +90,6 @@ void Game::levelInitialize (edict_t *entities, int max) { // flush any print queue ctrl.resetFlushTimestamp (); - // suspend any analyzer tasks - analyzer.suspend (); - // go thru the all entities on map, and do whatever we're want for (int i = 0; i < max; ++i) { auto ent = entities + i; @@ -162,6 +166,40 @@ void Game::levelInitialize (edict_t *entities, int max) { m_halfSecondFrame = 0.0f; } +void Game::levelShutdown () { + // stop thread pool + worker.shutdown (); + + // save collected practice on shutdown + practice.save (); + + // destroy global killer entity + bots.destroyKillerEntity (); + + // ensure players are off on xash3d + if (game.is (GameFlags::Xash3D)) { + bots.kickEveryone (true, false); + } + + // set state to unprecached + game.setUnprecached (); + + // enable lightstyle animations on level change + illum.enableAnimation (true); + + // send message on new map + util.setNeedForWelcome (false); + + // clear local entity + game.setLocalEntity (nullptr); + + // reset graph state + graph.reset (); + + // suspend any analyzer tasks + analyzer.suspend (); +} + void Game::drawLine (edict_t *ent, const Vector &start, const Vector &end, int width, int noise, const Color &color, int brightness, int speed, int life, DrawLine type) { // this function draws a arrow visible from the client side of the player whose player entity // is pointed to by ent, from the vector location start to the vector location end, @@ -963,6 +1001,9 @@ void Game::slowFrame () { // refresh bomb origin in case some plugin moved it out graph.setBombOrigin (); + // update client pings + util.calculatePings (); + // update next update time m_halfSecondFrame = nextUpdate * 0.25f + time (); } @@ -978,9 +1019,6 @@ void Game::slowFrame () { // check if we're need to autokill bots bots.maintainAutoKill (); - // update client pings - util.calculatePings (); - // maintain leaders selection upon round start bots.maintainLeaders (); diff --git a/src/graph.cpp b/src/graph.cpp index c16ffda..f16e94c 100644 --- a/src/graph.cpp +++ b/src/graph.cpp @@ -1196,6 +1196,7 @@ void BotGraph::showFileInfo () { msg (" compressed_size: %dkB", m_graphHeader.compressed / 1024); msg (" uncompressed_size: %dkB", m_graphHeader.uncompressed / 1024); msg (" options: %d", m_graphHeader.options); // display as string ? + msg (" analyzed: %s", (m_graphHeader.options & StorageOption::Analyzed) ? "yes" : "no"); // display as string ? msg (""); diff --git a/src/linkage.cpp b/src/linkage.cpp index 75cac27..d44d632 100644 --- a/src/linkage.cpp +++ b/src/linkage.cpp @@ -94,11 +94,11 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int) { plat.bzero (table, sizeof (gamefuncs_t)); if (!(game.is (GameFlags::Metamod))) { - auto api_GetEntityAPI = game.lib ().resolve (__FUNCTION__); + auto api_GetEntityAPI = game.lib ().resolve (__func__); // pass other DLLs engine callbacks to function table... if (!api_GetEntityAPI || api_GetEntityAPI (&dllapi, INTERFACE_VERSION) == 0) { - logger.fatal ("Could not resolve symbol \"%s\" in the game dll.", __FUNCTION__); + logger.fatal ("Could not resolve symbol \"%s\" in the game dll.", __func__); } dllfuncs.dllapi_table = &dllapi; gpGamedllFuncs = &dllfuncs; @@ -332,29 +332,8 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int) { // any case, when the new map will be booting, ServerActivate() will be called, so we'll do // the loading of new bots and the new BSP data parsing there. - // save collected practice on shutdown - practice.save (); - - // destroy global killer entity - bots.destroyKillerEntity (); - - // set state to unprecached - game.setUnprecached (); - - // enable lightstyle animations on level change - illum.enableAnimation (true); - - // send message on new map - util.setNeedForWelcome (false); - - // clear local entity - game.setLocalEntity (nullptr); - - // reset graph state - graph.reset (); - - // clear all the bots - bots.destroy (); + // notify level shutdown + game.levelShutdown (); if (game.is (GameFlags::Metamod)) { RETURN_META (MRES_IGNORED); @@ -397,8 +376,8 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int) { // keep track of grenades on map bots.updateActiveGrenade (); - // keep track of intresting entities - bots.updateIntrestingEntities (); + // keep track of interesting entities + bots.updateInterestingEntities (); } // keep bot number up to date @@ -527,7 +506,7 @@ CR_LINKAGE_C int GetEngineFunctions (enginefuncs_t *table, int *) { } table->pfnLightStyle = [] (int style, char *val) { - // ths function update lightstyle for the bots + // this function update lightstyle for the bots illum.updateLight (style, val); @@ -572,7 +551,6 @@ CR_LINKAGE_C int GetEngineFunctions (enginefuncs_t *table, int *) { table->pfnMessageBegin = [] (int msgDest, int msgType, const float *origin, edict_t *ed) { // this function called each time a message is about to sent. - msgs.start (ed, msgType); if (game.is (GameFlags::Metamod)) { @@ -801,11 +779,11 @@ CR_EXPORT int GetNewDLLFunctions (newgamefuncs_t *table, int *interfaceVersion) plat.bzero (table, sizeof (newgamefuncs_t)); if (!(game.is (GameFlags::Metamod))) { - auto api_GetNewDLLFunctions = game.lib ().resolve (__FUNCTION__); + auto api_GetNewDLLFunctions = game.lib ().resolve (__func__); // pass other DLLs engine callbacks to function table... if (!api_GetNewDLLFunctions || api_GetNewDLLFunctions (&newapi, interfaceVersion) == 0) { - logger.error ("Could not resolve symbol \"%s\" in the game dll.", __FUNCTION__); + logger.error ("Could not resolve symbol \"%s\" in the game dll.", __func__); } dllfuncs.newapi_table = &newapi; memcpy (table, &newapi, sizeof (newgamefuncs_t)); @@ -817,6 +795,7 @@ CR_EXPORT int GetNewDLLFunctions (newgamefuncs_t *table, int *interfaceVersion) if (bot->m_enemy == ent) { bot->m_enemy = nullptr; bot->m_lastEnemy = nullptr; + bot->markStale (); } } @@ -854,7 +833,6 @@ CR_LINKAGE_C int GetEngineFunctions_Post (enginefuncs_t *table, int *) { RETURN_META_VALUE (MRES_IGNORED, 0); }; - return HLTrue; } @@ -907,11 +885,16 @@ CR_EXPORT int Meta_Detach (PLUG_LOADTIME now, PL_UNLOAD_REASON reason) { logger.error ("%s: plugin NOT detaching (can't unload plugin right now)", Plugin_info.name); return HLFalse; // returning FALSE prevents metamod from unloading this plugin } - bots.kickEveryone (true); // kick all bots off this server + // stop the worker + worker.shutdown (); + + // kick all bots off this server + bots.kickEveryone (true); // save collected practice on shutdown practice.save (); + // disable hooks util.disableSendTo (); // make sure all stuff cleared @@ -963,10 +946,10 @@ DLL_GIVEFNPTRSTODLL GiveFnptrsToDll (enginefuncs_t *table, globalvars_t *glob) { if (game.postload ()) { return; } - auto api_GiveFnptrsToDll = game.lib ().resolve (__FUNCTION__); + auto api_GiveFnptrsToDll = game.lib ().resolve (__func__); if (!api_GiveFnptrsToDll) { - logger.fatal ("Could not resolve symbol \"%s\" in the game dll.", __FUNCTION__); + logger.fatal ("Could not resolve symbol \"%s\" in the game dll.", __func__); } GetEngineFunctions (table, nullptr); @@ -984,10 +967,10 @@ CR_EXPORT int Server_GetBlendingInterface (int version, struct sv_blending_inter // of the body move, which bones, which hitboxes and how) between the server and the game DLL. // some MODs can be using a different hitbox scheme than the standard one. - auto api_GetBlendingInterface = game.lib ().resolve (__FUNCTION__); + auto api_GetBlendingInterface = game.lib ().resolve (__func__); if (!api_GetBlendingInterface) { - logger.error ("Could not resolve symbol \"%s\" in the game dll. Continuing...", __FUNCTION__); + logger.error ("Could not resolve symbol \"%s\" in the game dll. Continuing...", __func__); return HLFalse; } return api_GetBlendingInterface (version, ppinterface, pstudio, rotationmatrix, bonetransform); diff --git a/src/manager.cpp b/src/manager.cpp index d75b1cd..85ad940 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -8,6 +8,7 @@ #include ConVar cv_autovacate ("yb_autovacate", "1", "Kick bots to automatically make room for human players."); +ConVar cv_autovacate_keep_slots ("yb_autovacate_keep_slots", "1", "How many slots autovacate feature should keep for human players", true, 1.0f, 8.0f); ConVar cv_kick_after_player_connect ("yb_kick_after_player_connect", "1", "Kick the bot immediately when a human player joins the server (yb_autovacate must be enabled)."); ConVar cv_quota ("yb_quota", "9", "Specifies the number bots to be added to the game.", true, 0.0f, static_cast (kGameMaxPlayers)); @@ -405,10 +406,10 @@ void BotManager::maintainQuota () { if (cv_autovacate.bool_ ()) { if (cv_kick_after_player_connect.bool_ ()) { - desiredBotCount = cr::min (desiredBotCount, maxClients - (totalHumansInGame + 1)); + desiredBotCount = cr::min (desiredBotCount, maxClients - (totalHumansInGame + cv_autovacate_keep_slots.int_ ())); } else { - desiredBotCount = cr::min (desiredBotCount, maxClients - (humanPlayersInGame + 1)); + desiredBotCount = cr::min (desiredBotCount, maxClients - (humanPlayersInGame + cv_autovacate_keep_slots.int_ ())); } } else { @@ -515,7 +516,7 @@ void BotManager::reset () { m_timeBombPlanted = 0.0f; m_bombSayStatus = BombPlantedSay::ChatSay | BombPlantedSay::Chatter; - m_intrestingEntities.clear (); + m_interestingEntities.clear (); m_activeGrenades.clear (); } @@ -569,7 +570,7 @@ void BotManager::serverFill (int selection, int personality, int difficulty, int // this function fill server with bots, with specified team & personality // always keep one slot - int maxClients = cv_autovacate.bool_ () ? game.maxClients () - 1 - (game.isDedicated () ? 0 : getHumansCount ()) : game.maxClients (); + int maxClients = cv_autovacate.bool_ () ? game.maxClients () - cv_autovacate_keep_slots.int_ () - (game.isDedicated () ? 0 : getHumansCount ()) : game.maxClients (); if (getBotCount () >= maxClients - getHumansCount ()) { return; @@ -942,6 +943,9 @@ void BotManager::balanceBotDifficulties () { void BotManager::destroy () { // this function free all bots slots (used on server shutdown) + for (auto &bot : m_bots) { + bot->markStale (); + } m_bots.clear (); } @@ -1078,7 +1082,10 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int skin) { // init path walker m_pathWalk.init (graph.getMaxRouteLength ()); - // bot is not kicked by rorataion + // init async planner + m_planner = cr::makeUnique (graph.length ()); + + // bot is not kicked by rotation m_kickedByRotation = false; // assign team and class @@ -1141,7 +1148,7 @@ int BotManager::getPlayerPriority (edict_t *ent) { } // give bots some priority - if (bot->m_hasC4 || bot->m_isVIP || bot->hasHostage () || bot->m_healthValue < ent->v.health) { + if (bot->m_hasC4 || bot->m_isVIP || bot->m_hasHostage || bot->m_healthValue < ent->v.health) { return bot->entindex () + highPrio; } return bot->entindex (); @@ -1183,7 +1190,6 @@ void BotManager::erase (Bot *bot) { m_bots.erase (index, 1); // remove from bots array break; } - } void BotManager::handleDeath (edict_t *killer, edict_t *victim) { @@ -1246,6 +1252,7 @@ void BotManager::handleDeath (edict_t *killer, edict_t *victim) { } } + void Bot::newRound () { // this function initializes a bot after creation & at the start of each round @@ -1276,7 +1283,6 @@ void Bot::newRound () { for (auto &node : m_previousNodes) { node = kInvalidNodeIndex; } - m_navTimeset = game.time (); m_team = game.getTeam (ent ()); @@ -1407,6 +1413,7 @@ void Bot::newRound () { m_inBombZone = false; m_ignoreBuyDelay = false; m_hasC4 = false; + m_hasHostage = false; m_fallDownTime = 0.0f; m_shieldCheckTime = 0.0f; @@ -1505,8 +1512,12 @@ void Bot::kick () { } void Bot::markStale () { + // switch chatter icon off showChatterIcon (false, true); + // mark bot as leaving + m_isStale = true; + // clear the bot name conf.clearUsedName (this); @@ -1723,13 +1734,13 @@ void BotManager::updateActiveGrenade () { m_grenadeUpdateTime = game.time () + 0.25f; } -void BotManager::updateIntrestingEntities () { +void BotManager::updateInterestingEntities () { if (m_entityUpdateTime > game.time ()) { return; } // clear previously stored entities - m_intrestingEntities.clear (); + m_interestingEntities.clear (); // search the map for any type of grenade game.searchEntities (nullptr, kInfiniteDistance, [&] (edict_t *e) { @@ -1737,26 +1748,26 @@ void BotManager::updateIntrestingEntities () { // search for grenades, weaponboxes, weapons, items and armoury entities if (strncmp ("weaponbox", classname, 9) == 0 || strncmp ("grenade", classname, 7) == 0 || util.isItem (e) || strncmp ("armoury", classname, 7) == 0) { - m_intrestingEntities.push (e); + m_interestingEntities.push (e); } // pickup some csdm stuff if we're running csdm if (game.mapIs (MapFlags::HostageRescue) && strncmp ("hostage", classname, 7) == 0) { - m_intrestingEntities.push (e); + m_interestingEntities.push (e); } // add buttons if (game.mapIs (MapFlags::HasButtons) && strncmp ("func_button", classname, 11) == 0) { - m_intrestingEntities.push (e); + m_interestingEntities.push (e); } // pickup some csdm stuff if we're running csdm if (game.is (GameFlags::CSDM) && strncmp ("csdm", classname, 4) == 0) { - m_intrestingEntities.push (e); + m_interestingEntities.push (e); } if (cv_attack_monsters.bool_ () && util.isMonster (e)) { - m_intrestingEntities.push (e); + m_interestingEntities.push (e); } // continue iteration @@ -1914,3 +1925,35 @@ void BotManager::setBombPlanted (bool isPlanted) { } m_bombPlanted = isPlanted; } + +void BotThreadWorker::shutdown () { + game.print ("Shutting down bot thread worker."); + m_botWorker.shutdown (); +} + +void BotThreadWorker::startup (int workers) { + size_t count = m_botWorker.threadCount (); + + if (count > 0) { + logger.error ("Tried to start thread pool with existing %d threads in pool.", count); + return; + } + + int requetedThreadCount = workers; + int hardwareConcurrency = plat.hardwareConcurrency (); + + if (requetedThreadCount == -1) { + requetedThreadCount = hardwareConcurrency / 4; + + if (requetedThreadCount == 0) { + requetedThreadCount = 1; + } + } + requetedThreadCount = cr::clamp (requetedThreadCount, 1, hardwareConcurrency); + + // notify user + game.print ("Starting up bot thread worker with %d threads.", requetedThreadCount); + + // start up the worker + m_botWorker.startup (static_cast (requetedThreadCount)); +} diff --git a/src/navigate.cpp b/src/navigate.cpp index 07c7cd0..a92051f 100644 --- a/src/navigate.cpp +++ b/src/navigate.cpp @@ -54,7 +54,7 @@ int Bot::findBestGoal () { tactic = 3; return findGoalPost (tactic, defensiveNodes, offensiveNodes); } - else if (m_team == Team::CT && hasHostage ()) { + else if (m_team == Team::CT && m_hasHostage) { tactic = 4; return findGoalPost (tactic, defensiveNodes, offensiveNodes); } @@ -406,7 +406,7 @@ void Bot::doPlayerAvoidance (const Vector &normal) { if (game.isNullEntity (m_hindrance)) { return; } - const float interval = m_frameInterval * 7.25f; + const float interval = m_frameInterval * (pev->velocity.lengthSq2d () > 0.0f ? 9.0f : 2.0f); // use our movement angles, try to predict where we should be next frame Vector right, forward; @@ -538,7 +538,7 @@ void Bot::checkTerrain (float movedDistance, const Vector &dirNormal) { dirLeft = true; } const auto &testDir = m_moveSpeed > 0.0f ? forward : -forward; - constexpr float blockDistance = 56.0f; + constexpr float blockDistance = 40.0f; // now check which side is blocked src = pev->origin + right * blockDistance; @@ -813,11 +813,8 @@ bool Bot::updateNavigation () { // check if we need to find a node... if (m_currentNodeIndex == kInvalidNodeIndex) { findValidNode (); + setPathOrigin (); - // if graph node radius non zero vary origin a bit depending on the body angles - if (m_path->radius > 0.0f) { - m_pathOrigin += Vector (pev->angles.x, cr::wrapAngle (pev->angles.y + rg.get (-90.0f, 90.0f)), 0.0f).forward () * rg.get (0.0f, m_path->radius); - } m_navTimeset = game.time (); } m_destOrigin = m_pathOrigin + pev->view_ofs; @@ -841,16 +838,16 @@ bool Bot::updateNavigation () { m_desiredVelocity = nullptr; } } - else if (!isKnifeMode () && m_switchedToKnifeDuringJump && isOnFloor ()) { - selectBestWeapon (); + } + else if (m_jumpDistance > 0.0f && !isKnifeMode () && m_switchedToKnifeDuringJump && isOnFloor ()) { + selectBestWeapon (); - // if jump distance was big enough, cooldown a little - if (m_jumpDistance > 180.0f) { - startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + 0.45f, false); - } - m_jumpDistance = 0.0f; - m_switchedToKnifeDuringJump = false; + // if jump distance was big enough, cooldown a little + if (m_jumpDistance > 180.0f) { + startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + 0.45f, false); } + m_jumpDistance = 0.0f; + m_switchedToKnifeDuringJump = false; } if (m_pathFlags & NodeFlag::Ladder) { @@ -1495,7 +1492,7 @@ bool Bot::findNextBestNode () { } // cts with hostages should not pick nodes with no hostage flag - if (game.mapIs (MapFlags::HostageRescue) && m_team == Team::CT && (graph[path.number].flags & NodeFlag::NoHostage) && hasHostage ()) { + if (game.mapIs (MapFlags::HostageRescue) && m_team == Team::CT && (graph[path.number].flags & NodeFlag::NoHostage) && m_hasHostage) { continue; } @@ -1566,9 +1563,7 @@ bool Bot::findNextBestNode () { if (selected == kInvalidNodeIndex) { selected = findNearestNode (); } - m_nodeHistory.push (selected); changeNodeIndex (selected); - return true; } @@ -1647,7 +1642,7 @@ int Bot::changeNodeIndex (int index) { m_path = &graph[m_currentNodeIndex]; m_pathFlags = m_path->flags; m_pathOrigin = m_path->origin; - + return m_currentNodeIndex; // to satisfy static-code analyzers } @@ -1665,14 +1660,14 @@ int Bot::findNearestNode () { for (const auto &i : bucket) { const auto &path = graph[i]; - if (!graph.exists (path.number) || !graph.exists (m_currentNodeIndex) || path.number == m_currentNodeIndex) { + if (!graph.exists (path.number)) { continue; } const float distance = path.origin.distanceSq (pev->origin); if (distance < minimumDistance) { // if bot doing navigation, make sure node really visible and reacheable - if (vistab.visible (m_currentNodeIndex, path.number) && isReachableNode (path.number)) { + if (isReachableNode (path.number)) { index = path.number; minimumDistance = distance; } @@ -1680,7 +1675,7 @@ int Bot::findNearestNode () { } // try to search ANYTHING that can be reachaed - if (index == kInvalidNodeIndex) { + if (!graph.exists (index)) { minimumDistance = cr::sqrf (kMaxDistance); const auto &nearestNodes = graph.getNarestInRadius (kMaxDistance, pev->origin); @@ -1776,18 +1771,18 @@ int Bot::findDefendNode (const Vector &origin) { TraceResult tr {}; int nodeIndex[kMaxNodeLinks] {}; - int minDistance[kMaxNodeLinks] {}; + float minDistance[kMaxNodeLinks] {}; for (int i = 0; i < kMaxNodeLinks; ++i) { nodeIndex[i] = kInvalidNodeIndex; - minDistance[i] = 128; + minDistance[i] = 128.0f; } int posIndex = graph.getNearest (origin); int srcIndex = m_currentNodeIndex; // max search distance - const int kMaxDistance = cr::clamp (148 * bots.getBotCount (), 256, 1024); + const auto kMaxDistance = cr::clamp (148.0f * bots.getBotCount (), 256.0f, 1024.0f); // some of points not found, return random one if (srcIndex == kInvalidNodeIndex || posIndex == kInvalidNodeIndex) { @@ -1802,7 +1797,7 @@ int Bot::findDefendNode (const Vector &origin) { } // use the 'real' pathfinding distances - int distance = planner.dist (srcIndex, path.number); + auto distance = planner.dist (srcIndex, path.number); // skip wayponts too far if (distance > kMaxDistance) { @@ -1860,13 +1855,13 @@ int Bot::findDefendNode (const Vector &origin) { int practiceDamage = practice.getDamage (m_team, nodeIndex[i], nodeIndex[i]); practiceDamage = (practiceDamage * 100) / practice.getHighestDamageForTeam (m_team); - minDistance[i] = (practiceDamage * 100) / 8192; - minDistance[i] += practiceDamage; + minDistance[i] = static_cast ((practiceDamage * 100) / 8192); + minDistance[i] += static_cast (practiceDamage); } } bool sorting = false; - // sort results nodes for farest distance + // sort resulting nodes for farest distance do { sorting = false; @@ -1925,11 +1920,11 @@ int Bot::findCoverNode (float maxDistance) { IntArray enemies; int nodeIndex[kMaxNodeLinks] {}; - int minDistance[kMaxNodeLinks] {}; + float minDistance[kMaxNodeLinks] {}; for (int i = 0; i < kMaxNodeLinks; ++i) { nodeIndex[i] = kInvalidNodeIndex; - minDistance[i] = static_cast (maxDistance); + minDistance[i] = maxDistance; } if (enemyIndex == kInvalidNodeIndex) { @@ -1967,8 +1962,8 @@ int Bot::findCoverNode (float maxDistance) { } // use the 'real' pathfinding distances - int distance = planner.dist (srcIndex, path.number); - int enemyDistance = planner.dist (enemyIndex, path.number); + float distance = planner.dist (srcIndex, path.number); + float enemyDistance = planner.dist (enemyIndex, path.number); if (distance >= enemyDistance) { continue; @@ -2014,8 +2009,8 @@ int Bot::findCoverNode (float maxDistance) { int practiceDamage = practice.getDamage (m_team, nodeIndex[i], nodeIndex[i]); practiceDamage = (practiceDamage * 100) / practice.getHighestDamageForTeam (m_team); - minDistance[i] = (practiceDamage * 100) / 8192; - minDistance[i] += practiceDamage; + minDistance[i] = static_cast ((practiceDamage * 100) / 8192); + minDistance[i] += static_cast (practiceDamage); } } bool sorting = false; @@ -2105,7 +2100,6 @@ bool Bot::advanceMovement () { if (m_pathWalk.empty ()) { return false; } - TraceResult tr {}; m_pathWalk.shift (); // advance in list m_currentTravelFlags = 0; // reset travel flags (jumping etc) @@ -2136,7 +2130,7 @@ bool Bot::advanceMovement () { Task taskID = getCurrentTaskId (); // only if we in normal task and bomb is not planted - if (taskID == Task::Normal && bots.getRoundMidTime () + 5.0f < game.time () && m_timeCamping + 5.0f < game.time () && !bots.isBombPlanted () && m_personality != Personality::Rusher && !m_hasC4 && !m_isVIP && m_loosedBombNodeIndex == kInvalidNodeIndex && !hasHostage ()) { + if (taskID == Task::Normal && bots.getRoundMidTime () + 5.0f < game.time () && m_timeCamping + 5.0f < game.time () && !bots.isBombPlanted () && m_personality != Personality::Rusher && !m_hasC4 && !m_isVIP && m_loosedBombNodeIndex == kInvalidNodeIndex && !m_hasHostage) { m_campButtons = 0; const int nextIndex = m_pathWalk.next (); @@ -2241,22 +2235,55 @@ bool Bot::advanceMovement () { changeNodeIndex (destIndex); } } + setPathOrigin (); + m_navTimeset = game.time (); - // if wayzone radius non zero vary origin a bit depending on the body angles + return true; +} + +void Bot::setPathOrigin () { + constexpr int kMaxAlternatives = 5; + + // if node radius non zero vary origin a bit depending on the body angles if (m_path->radius > 0.0f) { - m_pathOrigin += Vector (pev->angles.x, cr::wrapAngle (pev->angles.y + rg.get (-90.0f, 90.0f)), 0.0f).forward () * rg.get (0.0f, m_path->radius); + int nearestIndex = kInvalidNodeIndex; + + if (!m_pathWalk.empty () && m_pathWalk.hasNext ()) { + Vector orgs[kMaxAlternatives] {}; + + for (int i = 0; i < kMaxAlternatives; ++i) { + orgs[i] = m_pathOrigin + Vector (rg.get (-m_path->radius, m_path->radius), rg.get (-m_path->radius, m_path->radius), 0.0f); + } + float closest = kInfiniteDistance; + + for (int i = 0; i < kMaxAlternatives; ++i) { + float distance = pev->origin.distanceSq (orgs[i]); + + if (distance < closest) { + nearestIndex = i; + closest = distance; + } + } + + // set the origin if found alternative + if (nearestIndex != kInvalidNodeIndex) { + m_pathOrigin = orgs[nearestIndex]; + } + } + + if (nearestIndex == kInvalidNodeIndex) { + m_pathOrigin += Vector (pev->angles.x, cr::wrapAngle (pev->angles.y + rg.get (-90.0f, 90.0f)), 0.0f).forward () * rg.get (0.0f, m_path->radius); + } } if (isOnLadder ()) { + TraceResult tr {}; game.testLine (Vector (pev->origin.x, pev->origin.y, pev->absmin.z), m_pathOrigin, TraceIgnore::Everything, ent (), &tr); if (tr.flFraction < 1.0f) { m_pathOrigin = m_pathOrigin + (pev->origin - m_pathOrigin) * 0.5f + Vector (0.0f, 0.0f, 32.0f); } } - m_navTimeset = game.time (); - - return true; } bool Bot::cantMoveForward (const Vector &normal, TraceResult *tr) { @@ -3158,67 +3185,89 @@ void Bot::findShortestPath (int srcIndex, int destIndex) { m_chosenGoalIndex = srcIndex; m_goalValue = 0.0f; - bool success = planner.find (srcIndex, destIndex, [&] (int index) { + bool success = planner.find (srcIndex, destIndex, [this] (int index) { m_pathWalk.add (index); return true; }); - if (success) { + if (!success) { m_prevGoalIndex = kInvalidNodeIndex; getTask ()->data = kInvalidNodeIndex; } } -void Bot::findPath (int srcIndex, int destIndex, FindPath pathType /*= FindPath::Fast */) { - // this function finds a path from srcIndex to destIndex +void Bot::syncFindPath (int srcIndex, int destIndex, FindPath pathType) { + // this function finds a path from srcIndex to destIndex; + + if (!m_pathFindLock.tryLock ()) { + return; // allow only single instance of syncFindPath per-bot + } + ScopedUnlock unlock (m_pathFindLock); 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 (); + srcIndex = changeNodeIndex (graph.getNearestNoBuckets (pev->origin, 256.0f)); - // 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); + if (!graph.exists (srcIndex)) { + printf ("%s source path index not valid (%d).", __func__, srcIndex); + return; } } - 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 (!graph.exists (destIndex) || destIndex == srcIndex) { + destIndex = graph.getNearestNoBuckets (pev->origin, kInfiniteDistance, NodeFlag::Goal); + + if (!graph.exists (destIndex) || srcIndex == destIndex) { + destIndex = rg.get (0, graph.length () - 1); + + if (!graph.exists (destIndex)) { + printf ("%s dest path index not valid (%d).", __func__, destIndex); + return; + } } } - else { - if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) { - pathPlanner->setH (Heuristic::hfunctionPathDistWithHostage); - pathPlanner->setG (Heuristic::gfunctionPathDistWithHostage); - } - else { - pathPlanner->setH (Heuristic::hfunctionPathDist); - pathPlanner->setG (Heuristic::gfunctionPathDist); - } + + // do not process if src points to dst + if (srcIndex == destIndex) { + printf ("%s source path is same as dest (%d).", __func__, destIndex); + return; } clearSearchNodes (); + // get correct calculation for heuristic + if (pathType == FindPath::Optimal) { + if (game.mapIs (MapFlags::HostageRescue) && m_hasHostage) { + m_planner->setH (Heuristic::hfunctionPathDistWithHostage); + m_planner->setG (Heuristic::gfunctionKillsDistCTWithHostage); + } + else { + m_planner->setH (Heuristic::hfunctionPathDist); + m_planner->setG (Heuristic::gfunctionKillsDist); + } + } + else if (pathType == FindPath::Safe) { + if (game.mapIs (MapFlags::HostageRescue) && m_hasHostage) { + m_planner->setH (Heuristic::hfunctionNone); + m_planner->setG (Heuristic::gfunctionKillsCTWithHostage); + } + else { + m_planner->setH (Heuristic::hfunctionNone); + m_planner->setG (Heuristic::gfunctionKills); + } + } + else { + if (game.mapIs (MapFlags::HostageRescue) && m_hasHostage) { + m_planner->setH (Heuristic::hfunctionPathDistWithHostage); + m_planner->setG (Heuristic::gfunctionPathDistWithHostage); + } + else { + m_planner->setH (Heuristic::hfunctionPathDist); + m_planner->setG (Heuristic::gfunctionPathDist); + } + } + m_chosenGoalIndex = srcIndex; m_goalValue = 0.0f; - auto result = pathPlanner->find (m_team, srcIndex, destIndex, [&] (int index) { + auto result = m_planner->find (m_team, srcIndex, destIndex, [this] (int index) { m_pathWalk.add (index); return true; }); @@ -3230,10 +3279,10 @@ void Bot::findPath (int srcIndex, int destIndex, FindPath pathType /*= FindPath: break; case AStarResult::InternalError: - // bot should not roam when this occurs - kick (); // kick the bot off... + m_kickMeFromServer = true; // bot should be kicked within main thread, not here - logger.error ("A* Search for bot \"%s\" failed with internal pathfinder error. Seems to be graph is broken. Bot removed (re-added).", pev->netname.chars ()); + // bot should not roam when this occurs + printf ("A* Search for bot \"%s\" failed with internal pathfinder error. Seems to be graph is broken. Bot removed (re-added).", pev->netname.chars ()); break; case AStarResult::Failed: @@ -3241,8 +3290,14 @@ void Bot::findPath (int srcIndex, int destIndex, FindPath pathType /*= FindPath: findShortestPath (srcIndex, destIndex); // A* found no path, try floyd pathfinder instead if (cv_debug.bool_ ()) { - logger.error ("A* Search for bot \"%s\" has failed. Falling back to shortest-path algorithm. Seems to be graph is broken.", pev->netname.chars ()); + printf ("A* Search for bot \"%s\" has failed. Falling back to shortest-path algorithm. Seems to be graph is broken.", pev->netname.chars ()); } break; } } + +void Bot::findPath (int srcIndex, int destIndex, FindPath pathType /*= FindPath::Fast */) { + worker.enqueue ([this, srcIndex, destIndex, pathType] () { + syncFindPath (srcIndex, destIndex, pathType); + }); +} diff --git a/src/planner.cpp b/src/planner.cpp index c014bb2..4565347 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -8,20 +8,20 @@ #include ConVar cv_path_heuristic_mode ("yb_path_heuristic_mode", "3", "Selects the heuristic function mode. For debug purposes only.", true, 0.0f, 4.0f); -ConVar cv_path_floyd_memory_limit ("yb_path_floyd_memory_limit", "3", "Limit maximum floyd-warshall memory (megabytes). Use Dijkstra if memory exceeds.", true, 0.0, 12.0f); +ConVar cv_path_floyd_memory_limit ("yb_path_floyd_memory_limit", "6", "Limit maximum floyd-warshall memory (megabytes). Use Dijkstra if memory exceeds.", true, 0.0, 32.0f); ConVar cv_path_dijkstra_simple_distance ("yb_path_dijkstra_simple_distance", "1", "Use simple distance path calculation instead of running full Dijkstra path cycle. Used only when Floyd matrices unavailable due to memory limit."); -ConVar cv_path_astar_post_smooth ("yb_path_astar_post_smooth", "1", "Enables post-smoothing for A*. Reduces zig-zags on paths at cost of some CPU cycles."); +ConVar cv_path_astar_post_smooth ("yb_path_astar_post_smooth", "0", "Enables post-smoothing for A*. Reduces zig-zags on paths at cost of some CPU cycles."); float Heuristic::gfunctionKillsDist (int team, int currentIndex, int parentIndex) { if (parentIndex == kInvalidNodeIndex) { return 0.0f; } - auto cost = static_cast (practice.getDamage (team, currentIndex, currentIndex) + practice.getHighestDamageForTeam (team)); + auto cost = practice.plannerGetDamage (team, currentIndex, currentIndex, true); const auto ¤t = graph[currentIndex]; for (const auto &neighbour : current.links) { if (neighbour.index != kInvalidNodeIndex) { - cost += static_cast (practice.getDamage (team, neighbour.index, neighbour.index)); + cost += practice.plannerGetDamage (team, neighbour.index, neighbour.index, false); } } @@ -44,12 +44,12 @@ float Heuristic::gfunctionKillsDistCTWithHostage (int team, int currentIndex, in } float Heuristic::gfunctionKills (int team, int currentIndex, int) { - auto cost = static_cast (practice.getDamage (team, currentIndex, currentIndex)); + auto cost = practice.plannerGetDamage (team, currentIndex, currentIndex, false); const auto ¤t = graph[currentIndex]; for (const auto &neighbour : current.links) { if (neighbour.index != kInvalidNodeIndex) { - cost += static_cast (practice.getDamage (team, neighbour.index, neighbour.index)); + cost += practice.plannerGetDamage (team, neighbour.index, neighbour.index, false); } } @@ -176,11 +176,17 @@ bool AStarAlgo::cantSkipNode (const int a, const int b) { const auto &ag = graph[a]; const auto &bg = graph[b]; - const bool notVisible = !vistab.visible (ag.number, bg.number); + const bool hasZeroRadius = cr::fzero (ag.radius) || cr::fzero (ag.radius); + + if (hasZeroRadius) { + return true; + } + const bool notVisible = !vistab.visible (ag.number, bg.number) || !vistab.visible (bg.number, ag.number); if (notVisible) { return true; } + const bool tooHigh = cr::abs (ag.origin.z - bg.origin.z) > 17.0f; if (tooHigh) { @@ -191,9 +197,12 @@ bool AStarAlgo::cantSkipNode (const int a, const int b) { if (tooNarrow) { return true; } - const bool tooFar = ag.origin.distanceSq (bg.origin) > cr::sqrf (300.0f); + const float distance = ag.origin.distanceSq (bg.origin); - if (tooFar) { + const bool tooFar = distance > cr::sqrf (400.0f); + const bool tooClose = distance < cr::sqrtf (40.0f); + + if (tooFar || tooClose) { return true; } bool hasJumps = false; @@ -208,22 +217,23 @@ bool AStarAlgo::cantSkipNode (const int a, const int b) { } void AStarAlgo::postSmooth (NodeAdderFn onAddedNode) { + m_smoothedPath.clear (); + int index = 0; - m_smoothedPath.emplace (m_constructedPath.first ()); + m_smoothedPath.push (m_constructedPath.first ()); for (size_t i = 1; i < m_constructedPath.length () - 1; ++i) { if (cantSkipNode (m_smoothedPath[index], m_constructedPath[i + 1])) { ++index; - m_smoothedPath.emplace (m_constructedPath[i]); + m_smoothedPath.push (m_constructedPath[i]); } } - m_smoothedPath.emplace (m_constructedPath.last ()); + m_smoothedPath.push (m_constructedPath.last ()); + // give nodes back to bot for (const auto &spn : m_smoothedPath) { onAddedNode (spn); } - m_constructedPath.clear (); - m_smoothedPath.clear (); } AStarResult AStarAlgo::find (int botTeam, int srcIndex, int destIndex, NodeAdderFn onAddedNode) { @@ -242,7 +252,10 @@ AStarResult AStarAlgo::find (int botTeam, int srcIndex, int destIndex, NodeAdder m_routeQue.clear (); m_routeQue.emplace (srcIndex, srcRoute->g); - bool postSmoothPath = cv_path_astar_post_smooth.bool_ (); + bool postSmoothPath = cv_path_astar_post_smooth.bool_ () && vistab.isReady (); + + // always clear constructed path + m_constructedPath.clear (); while (!m_routeQue.empty ()) { // remove the first node from the open list @@ -258,7 +271,7 @@ AStarResult AStarAlgo::find (int botTeam, int srcIndex, int destIndex, NodeAdder // build the complete path do { if (postSmoothPath) { - m_constructedPath.emplace (currentIndex); + m_constructedPath.push (currentIndex); } else { onAddedNode (currentIndex); @@ -312,6 +325,12 @@ void FloydWarshallAlgo::rebuild () { m_length = graph.length (); m_matrix.resize (cr::sqrf (m_length)); + worker.enqueue ([this] () { + syncRebuild (); + }); +} + +void FloydWarshallAlgo::syncRebuild () { auto matrix = m_matrix.data (); // re-initialize matrix every load @@ -379,18 +398,19 @@ bool FloydWarshallAlgo::find (int srcIndex, int destIndex, NodeAdderFn onAddedNo srcIndex = (m_matrix.data () + (srcIndex * m_length) + destIndex)->index; if (srcIndex < 0) { - // only fill path distance on full path - if (pathDistance != nullptr) { - *pathDistance = dist (srcIndex, destIndex); - } - return true; + return false; } if (!onAddedNode (srcIndex)) { return true; } } - return false; + + // only fill path distance on full path + if (pathDistance != nullptr) { + *pathDistance = dist (srcIndex, destIndex); + } + return true; } void DijkstraAlgo::init (const int length) { @@ -400,15 +420,13 @@ void DijkstraAlgo::init (const int length) { m_parent.resize (length); } -void DijkstraAlgo::resetState () { +bool DijkstraAlgo::find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance) { + MutexScopedLock lock (m_cs); + m_queue.clear (); m_parent.fill (kInvalidNodeIndex); m_distance.fill (kInfiniteDistanceLong); -} - -bool DijkstraAlgo::find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance) { - resetState (); m_queue.emplace (0, srcIndex); m_distance[srcIndex] = 0; @@ -473,7 +491,6 @@ int DijkstraAlgo::dist (int srcIndex, int destIndex) { PathPlanner::PathPlanner () { m_dijkstra = cr::makeUnique (); m_floyd = cr::makeUnique (); - m_astar = cr::makeUnique (); } void PathPlanner::init () { @@ -487,7 +504,6 @@ void PathPlanner::init () { m_memoryLimitHit = true; } m_dijkstra->init (length); - m_astar->init (length); // load (re-create) floyds, if we're not hitting memory limits if (!m_memoryLimitHit) { @@ -503,7 +519,6 @@ bool PathPlanner::find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, in if (!graph.exists (srcIndex) || !graph.exists (destIndex)) { return false; } - // limit hit, use dijkstra if (m_memoryLimitHit) { return m_dijkstra->find (srcIndex, destIndex, onAddedNode, pathDistance); @@ -511,7 +526,7 @@ bool PathPlanner::find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, in return m_floyd->find (srcIndex, destIndex, onAddedNode, pathDistance);; } -int PathPlanner::dist (int srcIndex, int destIndex) { +float PathPlanner::dist (int srcIndex, int destIndex) { if (!graph.exists (srcIndex) || !graph.exists (destIndex)) { return kInfiniteDistanceLong; } @@ -523,9 +538,17 @@ int PathPlanner::dist (int srcIndex, int destIndex) { // limit hit, use dijkstra if (m_memoryLimitHit) { if (cv_path_dijkstra_simple_distance.bool_ ()) { - return static_cast (graph[srcIndex].origin.distance2d (graph[destIndex].origin)); + return graph[srcIndex].origin.distance2d (graph[destIndex].origin); } - return m_dijkstra->dist (srcIndex, destIndex); + return static_cast (m_dijkstra->dist (srcIndex, destIndex)); } - return m_floyd->dist (srcIndex, destIndex); + return static_cast (m_floyd->dist (srcIndex, destIndex)); +} + +float PathPlanner::preciseDistance (int srcIndex, int destIndex) { + // limit hit, use dijkstra + if (m_memoryLimitHit) { + return static_cast (m_dijkstra->dist (srcIndex, destIndex)); + } + return static_cast (m_floyd->dist (srcIndex, destIndex)); } diff --git a/src/practice.cpp b/src/practice.cpp index 82593cc..afc30a7 100644 --- a/src/practice.cpp +++ b/src/practice.cpp @@ -18,6 +18,7 @@ void BotPractice::setIndex (int32_t team, int32_t start, int32_t goal, int32_t v if (team != Team::Terrorist && team != Team::CT) { return; } + MutexScopedLock lock (m_damageUpdateLock); // reliability check if (!graph.exists (start) || !graph.exists (goal) || !graph.exists (value)) { @@ -37,6 +38,7 @@ void BotPractice::setValue (int32_t team, int32_t start, int32_t goal, int32_t v if (team != Team::Terrorist && team != Team::CT) { return; } + MutexScopedLock lock (m_damageUpdateLock); // reliability check if (!graph.exists (start) || !graph.exists (goal)) { @@ -46,7 +48,7 @@ void BotPractice::setValue (int32_t team, int32_t start, int32_t goal, int32_t v } int32_t BotPractice::getDamage (int32_t team, int32_t start, int32_t goal) { - if (!exists (team, start, goal)) { + if (!vistab.isReady () || !exists (team, start, goal)) { return 0; } return m_data[{start, goal, team}].damage; @@ -56,6 +58,7 @@ void BotPractice::setDamage (int32_t team, int32_t start, int32_t goal, int32_t if (team != Team::Terrorist && team != Team::CT) { return; } + MutexScopedLock lock (m_damageUpdateLock); // reliability check if (!graph.exists (start) || !graph.exists (goal)) { @@ -64,7 +67,26 @@ void BotPractice::setDamage (int32_t team, int32_t start, int32_t goal, int32_t m_data[{start, goal, team}].damage = static_cast (value); } +float BotPractice::plannerGetDamage (int32_t team, int32_t start, int32_t goal, bool addTeamHighestDamage) { + if (!m_damageUpdateLock.tryLock ()) { + return 1.0f; + } + ScopedUnlock unlock (m_damageUpdateLock); + auto damage = static_cast (getDamage (team, start, goal)); + + if (addTeamHighestDamage) { + damage += getHighestDamageForTeam (team); + } + return damage; +} + void BotPractice::update () { + worker.enqueue ([this] () { + syncUpdate (); + }); +} + +void BotPractice::syncUpdate () { // this function called after each end of the round to update knowledge about most dangerous nodes for each team. auto graphLength = graph.length (); @@ -98,7 +120,10 @@ void BotPractice::update () { if (maxDamage > PracticeLimit::Damage) { adjustValues = true; } - setIndex (team, i, i, bestIndex); + + if (graph.exists (bestIndex)) { + setIndex (team, i, i, bestIndex); + } } } constexpr auto kFullDamageVal = static_cast (PracticeLimit::Damage); @@ -117,6 +142,7 @@ void BotPractice::update () { } } } + MutexScopedLock lock (m_damageUpdateLock); for (int team = Team::Terrorist; team < kGameTeamNum; ++team) { m_teamHighestDamage[team] = cr::clamp (m_teamHighestDamage[team] - kHalfDamageVal, 1, kFullDamageVal); diff --git a/src/support.cpp b/src/support.cpp index 132702a..eca2698 100644 --- a/src/support.cpp +++ b/src/support.cpp @@ -405,9 +405,16 @@ int BotSupport::getPingBitmask (edict_t *ent, int loss, int ping) { } void BotSupport::calculatePings () { + worker.enqueue ([this] () { + syncCalculatePings (); + }); +} + +void BotSupport::syncCalculatePings () { if (!game.is (GameFlags::HasFakePings) || cv_show_latency.int_ () != 2) { return; } + MutexScopedLock lock (m_cs); Twin average { 0, 0 }; int numHumans = 0; @@ -444,7 +451,7 @@ void BotSupport::calculatePings () { } auto bot = bots[client.ent]; - // we're only intrested in bots here + // we're only interested in bots here if (!bot) { continue; } diff --git a/vc/yapb.vcxproj b/vc/yapb.vcxproj index 2e0e29e..9c9664a 100644 --- a/vc/yapb.vcxproj +++ b/vc/yapb.vcxproj @@ -32,6 +32,7 @@ + @@ -42,6 +43,7 @@ + diff --git a/vc/yapb.vcxproj.filters b/vc/yapb.vcxproj.filters index be29e7f..2d44645 100644 --- a/vc/yapb.vcxproj.filters +++ b/vc/yapb.vcxproj.filters @@ -171,6 +171,12 @@ inc + + inc\ext\crlib + + + inc\ext\hlsdk + @@ -221,15 +227,15 @@ src + + src + src src - - src - src