From e7712a551a925b9bdf14c83915619457727fe8e4 Mon Sep 17 00:00:00 2001 From: jeefo Date: Tue, 2 May 2023 09:42:43 +0300 Subject: [PATCH] backport: nodes flooder (analyzer) from cs-ebot analyze: allow to disable goal marking analyze: add cvars descriptions and bounds nav: added optional post path smoothing for astar algorithm nav: now bots will use Dijkstra algo instead of floyd-warshall if memory usage too high (controlled via yb_path_floyd_memory_limit cvar) (fixes #434) nav: vistable are now calculated every frame to prevent game-freeze during loading the game (fixes #434) graph: pracrice reworked to hash table so memory footprint is as low as possible (at cost 5-10% performance loss on practice) (fixes #434) control: bots commands now is case-insensitive bot: major refactoring of bot's code nav: issue warnings about path fail only with debug practice: check for visibility when updating danger index analyzer: suspend any analyzing on change level control: add kickall_ct/kickall_t nav: increase blocked distance in stuck check --- ext/crlib | 2 +- ext/hlsdk/extdll.h | 23 +- ext/hlsdk/util.h | 19 - inc/analyze.h | 79 +++ inc/control.h | 11 +- inc/engine.h | 17 + inc/graph.h | 236 ++------- inc/planner.h | 277 ++++++++++ inc/practice.h | 121 +++++ inc/storage.h | 103 ++++ inc/support.h | 16 - inc/vistable.h | 61 +++ inc/yapb.h | 103 +++- meson.build | 19 +- src/analyze.cpp | 359 +++++++++++++ src/botlib.cpp | 184 ++++--- src/combat.cpp | 26 +- src/control.cpp | 31 +- src/engine.cpp | 64 +-- src/graph.cpp | 1056 +++++++++------------------------------ src/linkage.cpp | 15 +- src/manager.cpp | 16 +- src/message.cpp | 2 +- src/navigate.cpp | 623 ++++++++--------------- src/planner.cpp | 531 ++++++++++++++++++++ src/practice.cpp | 157 ++++++ src/storage.cpp | 394 +++++++++++++++ src/support.cpp | 69 --- src/vistable.cpp | 178 +++++++ vc/yapb.vcxproj | 14 +- vc/yapb.vcxproj.filters | 30 ++ 31 files changed, 3114 insertions(+), 1722 deletions(-) create mode 100644 inc/analyze.h create mode 100644 inc/planner.h create mode 100644 inc/practice.h create mode 100644 inc/storage.h create mode 100644 inc/vistable.h create mode 100644 src/analyze.cpp create mode 100644 src/planner.cpp create mode 100644 src/practice.cpp create mode 100644 src/storage.cpp create mode 100644 src/vistable.cpp diff --git a/ext/crlib b/ext/crlib index 6d71649..4d370ec 160000 --- a/ext/crlib +++ b/ext/crlib @@ -1 +1 @@ -Subproject commit 6d716494737f740ebea58cfc6e6b6b30039cf7e9 +Subproject commit 4d370ec500675ad23e9507831bbbbd6bb166ce3a diff --git a/ext/hlsdk/extdll.h b/ext/hlsdk/extdll.h index 7477de8..d8b8da5 100644 --- a/ext/hlsdk/extdll.h +++ b/ext/hlsdk/extdll.h @@ -27,10 +27,16 @@ typedef unsigned char byte; #include #include +#if defined (CR_ARCH_X64) +using estring_t = int32_t; +#else +using estring_t = uint32_t; +#endif + // idea from regamedll class string_t final : public cr::DenyCopying { private: - int offset; + estring_t offset; public: explicit string_t () : offset (0) @@ -42,10 +48,10 @@ public: ~string_t () = default; public: - const char *chars (size_t shift = 0) const; + const char *chars (estring_t shift = 0) const; public: - operator int () const { + operator estring_t () const { return offset; } @@ -90,7 +96,9 @@ extern enginefuncs_t engfuncs; extern gamefuncs_t dllapi; // Use this instead of ALLOC_STRING on constant strings -#define STRING(offset) (const char *)(globals->pStringBase + (int)offset) +static inline const char *STRING (estring_t offset) { + return globals->pStringBase + offset; +} // form fwgs-hlsdk #if defined (CR_ARCH_X64) @@ -103,12 +111,13 @@ static inline int MAKE_STRING (const char *val) { return static_cast (ptrdiff); } #else -#define MAKE_STRING(str) ((uint64_t)(str) - (uint64_t)(STRING(0))) +static inline estring_t MAKE_STRING (const char *str) { + return static_cast (reinterpret_cast (str) - reinterpret_cast (globals->pStringBase)); +} #endif -inline const char *string_t::chars (size_t shift) const { +inline const char *string_t::chars (estring_t shift) const { auto result = STRING (offset); - return cr::strings.isEmpty (result) ? &cr::kNullChar : (result + shift); } diff --git a/ext/hlsdk/util.h b/ext/hlsdk/util.h index 5424ecb..95b73e7 100644 --- a/ext/hlsdk/util.h +++ b/ext/hlsdk/util.h @@ -20,25 +20,6 @@ extern globalvars_t *globals; extern enginefuncs_t engfuncs; extern gamefuncs_t dllapi; -// Use this instead of ALLOC_STRING on constant strings -#define STRING(offset) (const char *)(globals->pStringBase + (int)offset) - -// form fwgs-hlsdk -#if defined (CR_ARCH_X64) -static inline int MAKE_STRING (const char *val) { - long long ptrdiff = val - STRING (0); - - if (ptrdiff > INT_MAX || ptrdiff < INT_MIN) { - return engfuncs.pfnAllocString (val); - } - return static_cast (ptrdiff); -} -#else -#define MAKE_STRING(str) ((uint64_t)(str) - (uint64_t)(STRING(0))) -#endif - -#define ENGINE_STR(str) (const_cast (STRING (engfuncs.pfnAllocString (str)))) - // Dot products for view cone checking #define VIEW_FIELD_FULL (float)-1.0 // +-180 degrees #define VIEW_FIELD_WIDE (float)-0.7 // +-135 degrees 0.1 // +-85 degrees, used for full FOV checks diff --git a/inc/analyze.h b/inc/analyze.h new file mode 100644 index 0000000..627e1e9 --- /dev/null +++ b/inc/analyze.h @@ -0,0 +1,79 @@ +// +// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge. +// Copyright © 2004-2023 YaPB Project . +// +// SPDX-License-Identifier: MIT +// + +#pragma once + +// next code is based on cs-ebot implemntation, devised by EfeDursun125 +class GraphAnalyze : public Singleton { +public: + GraphAnalyze () = default; + ~GraphAnalyze () = default; + +private: + float m_updateInterval {}; // time to update analyzer + + bool m_basicsCreated {}; // basics waypoints were created? + bool m_isCrouch {}; // is node to be created as crouch ? + bool m_isAnalyzing {}; // we're in analyzing ? + bool m_isAnalyzed {}; // current waypoint is analyzed + bool m_expandedNodes[kMaxNodes] {}; // all nodes expanded ? + bool m_optimizedNodes[kMaxNodes] {}; // all nodes expanded ? + +public: + // start analyzation process + void start (); + + // update analyzation process + void update (); + + // suspend aanalyzing + void suspend (); + +private: + // flood with nodes + void flood (const Vector &pos, const Vector &next, float range); + + // set update interval (keeps game from freezing) + void setUpdateInterval (); + + // mark waypoints as goals + void markGoals (); + + // terminate analyzation and save data + void finish (); + + // optimize nodes a little + void optimize (); + + // cleanup bad nodes + void cleanup (); + +public: + + // node should be created as crouch + bool isCrouch () const { + return m_isCrouch; + } + + // is currently anaylyzing ? + bool isAnalyzing () const { + return m_isAnalyzing; + } + + // current graph is analyzed graph ? + bool isAnalyzed () const { + return m_isAnalyzed; + } + + // mark as optimized + void markOptimized (const int index) { + m_optimizedNodes[index] = true; + } +}; + +// explose global +CR_EXPOSE_GLOBAL_SINGLETON (GraphAnalyze, analyzer); diff --git a/inc/control.h b/inc/control.h index aa705e3..c1ec69c 100644 --- a/inc/control.h +++ b/inc/control.h @@ -216,7 +216,7 @@ public: m_args.clear (); for (int i = 0; i < engfuncs.pfnCmd_Argc (); ++i) { - m_args.emplace (engfuncs.pfnCmd_Argv (i)); + m_args.emplace (String (engfuncs.pfnCmd_Argv (i)).lowercase ()); } } @@ -274,5 +274,14 @@ template inline void BotControl::msg (const char *fmt, Args & } } +// graph heloer for sending message to correct channel +template inline void BotGraph::msg (const char *fmt, Args &&...args) { + if (m_silenceMessages) { + return; // no messages while analyzing (too much spam) + } + BotControl::instance ().msg (strings.format (conf.translate (fmt), cr::forward (args)...)); +} + + // explose global CR_EXPOSE_GLOBAL_SINGLETON (BotControl, ctrl); diff --git a/inc/engine.h b/inc/engine.h index 60d9cd8..f8f7594 100644 --- a/inc/engine.h +++ b/inc/engine.h @@ -71,6 +71,7 @@ struct ConVarReg { String info; String init; String regval; + String name; class ConVar *self; float initial, min, max; bool missing; @@ -420,6 +421,19 @@ public: Game::instance ().addNewCvar (name, initval, info, bounded, min, max, type, regMissing, regVal, this); } + template constexpr U get () const { + if constexpr (cr::is_same ::value) { + return ptr->value; + } + else if constexpr (cr::is_same ::value) { + return ptr->value > 0.0f; + } + else if constexpr (cr::is_same ::value) { + return static_cast (ptr->value); + } + assert ("!Inavlid type requeted."); + } + bool bool_ () const { return ptr->value > 0.0f; } @@ -447,6 +461,9 @@ public: void set (const char *val) { engfuncs.pfnCvar_DirectSet (ptr, val); } + + // revet cvar to default value + void revert (); }; class MessageWriter final { diff --git a/inc/graph.h b/inc/graph.h index 0b0f593..52d1005 100644 --- a/inc/graph.h +++ b/inc/graph.h @@ -7,6 +7,9 @@ #pragma once +constexpr int kMaxNodes = 4096; // max nodes per graph +constexpr int kMaxNodeLinks = 8; // max links for single node + // defines for nodes flags field (32 bits are available) CR_DECLARE_SCOPED_ENUM (NodeFlag, Lift = cr::bit (1), // wait for lift to be down before approaching this node @@ -43,13 +46,6 @@ CR_DECLARE_SCOPED_ENUM (PathConnection, Bidirectional ) -// a* route state -CR_DECLARE_SCOPED_ENUM (RouteState, - Open = 0, - Closed, - New -) - // node edit states CR_DECLARE_SCOPED_ENUM (GraphEdit, On = cr::bit (1), @@ -57,26 +53,6 @@ CR_DECLARE_SCOPED_ENUM (GraphEdit, Auto = cr::bit (3) ) -// storage header options -CR_DECLARE_SCOPED_ENUM (StorageOption, - Practice = cr::bit (0), // this is practice (experience) file - Matrix = cr::bit (1), // this is floyd warshal path & distance matrix - Vistable = cr::bit (2), // this is vistable data - Graph = cr::bit (3), // this is a node graph data - Official = cr::bit (4), // this is additional flag for graph indicates graph are official - Recovered = cr::bit (5), // this is additional flag indicates graph converted from podbot and was bad - Exten = cr::bit (6) // this is additional flag indicates that there's extension info -) - -// storage header versions -CR_DECLARE_SCOPED_ENUM (StorageVersion, - Graph = 2, - Practice = 1, - Vistable = 2, - Matrix = 1, - Podbot = 7 -) - // lift usage states CR_DECLARE_SCOPED_ENUM (LiftState, None = 0, @@ -103,29 +79,13 @@ CR_DECLARE_SCOPED_ENUM (NodeAddFlag, Goal = 100 ) -// a* route -struct Route { - float g, f; - int parent; - RouteState state; -}; +CR_DECLARE_SCOPED_ENUM (NotifySound, + Done = 0, + Change = 1, + Added = 2 +) -// general stprage header information structure -struct StorageHeader { - int32_t magic; - int32_t version; - int32_t options; - int32_t length; - int32_t compressed; - int32_t uncompressed; -}; - -// extension header for graph information -struct ExtenHeader { - char author[32]; // original author of graph - int32_t mapSize; // bsp size for checksumming map consistency - char modified[32]; // by whom modified -}; +#include // general waypoint header information structure struct PODGraphHeader { @@ -136,19 +96,6 @@ struct PODGraphHeader { char author[32]; }; -// floyd-warshall matrices -struct Matrix { - int16_t dist; - int16_t index; -}; - -// experience data hold in memory while playing -struct Practice { - int16_t damage[kGameTeamNum]; - int16_t index[kGameTeamNum]; - int16_t value[kGameTeamNum]; -}; - // defines linked waypoints struct PathLink { Vector velocity; @@ -157,11 +104,6 @@ struct PathLink { int16_t index; }; -// defines visibility count -struct PathVis { - uint16_t stand, crouch; -}; - // define graph path structure for yapb struct Path { int32_t number, flags; @@ -183,74 +125,21 @@ struct PODPath { PathVis vis; }; -// this structure links nodes returned from pathfinder -class PathWalk final : public DenyCopying { -private: - size_t m_cursor {}; - size_t m_length {}; +// general stprage header information structure +struct StorageHeader { + int32_t magic; + int32_t version; + int32_t options; + int32_t length; + int32_t compressed; + int32_t uncompressed; +}; - UniquePtr m_path {}; - -public: - explicit PathWalk () = default; - ~PathWalk () = default; - -public: - int32_t &next () { - return at (1); - } - - int32_t &first () { - return at (0); - } - - int32_t &last () { - return at (length () - 1); - } - - int32_t &at (size_t index) { - return m_path[m_cursor + index]; - } - - void shift () { - ++m_cursor; - } - - void reverse () { - for (size_t i = 0; i < m_length / 2; ++i) { - cr::swap (m_path[i], m_path[m_length - 1 - i]); - } - } - - size_t length () const { - if (m_cursor >= m_length) { - return 0; - } - return m_length - m_cursor; - } - - bool hasNext () const { - return length () > m_cursor; - } - - bool empty () const { - return !length (); - } - - void add (int32_t node) { - m_path[m_length++] = node; - } - - void clear () { - m_cursor = 0; - m_length = 0; - - m_path[0] = 0; - } - - void init (size_t length) { - m_path = cr::makeUnique (length); - } +// extension header for graph information +struct ExtenHeader { + char author[32]; // original author of graph + int32_t mapSize; // bsp size for checksumming map consistency + char modified[32]; // by whom modified }; // graph operation class @@ -260,7 +149,6 @@ public: private: int m_editFlags {}; - int m_loadAttempts {}; int m_cacheNodeIndex {}; int m_lastJumpNode {}; int m_findWPIndex {}; @@ -277,8 +165,8 @@ private: bool m_endJumpPoint {}; bool m_jumpLearnNode {}; bool m_hasChanged {}; - bool m_needsVisRebuild {}; bool m_narrowChecked {}; + bool m_silenceMessages {}; Vector m_learnVelocity {}; Vector m_learnPosition {}; @@ -293,11 +181,8 @@ private: IntArray m_rescuePoints {}; IntArray m_visitedGoals {}; - SmallArray m_matrix {}; - SmallArray m_practice {}; +public: SmallArray m_paths {}; - SmallArray m_vistable {}; - HashMap , EmptyHash > m_hashTable; String m_graphAuthor {}; @@ -315,13 +200,10 @@ public: public: int getFacingIndex (); int getFarest (const Vector &origin, float maxDistance = 32.0); + int getForAnalyzer (const Vector &origin, float maxDistance); int getNearest (const Vector &origin, float minDistance = kInfiniteDistance, int flags = -1); int getNearestNoBuckets (const Vector &origin, float minDistance = kInfiniteDistance, int flags = -1); int getEditorNearest (); - int getDangerIndex (int team, int start, int goal); - int getDangerValue (int team, int start, int goal); - int getDangerDamage (int team, int start, int goal); - int getPathDist (int srcIndex, int destIndex); int clearConnections (int index); int getBspSize (); int locateBucket (const Vector &pos); @@ -329,30 +211,23 @@ public: float calculateTravelTime (float maxSpeed, const Vector &src, const Vector &origin); bool convertOldFormat (); - bool isVisible (int srcIndex, int destIndex); - bool isStandVisible (int srcIndex, int destIndex); - bool isDuckVisible (int srcIndex, int destIndex); bool isConnected (int a, int b); bool isConnected (int index); + bool isNodeReacheableEx (const Vector &src, const Vector &destination, const float maxHeight); bool isNodeReacheable (const Vector &src, const Vector &destination); + bool isNodeReacheableWithJump (const Vector &src, const Vector &destination); bool checkNodes (bool teleportPlayer); - bool loadPathMatrix (); bool isVisited (int index); + bool isAnalyzed () const; bool saveGraphData (); bool loadGraphData (); bool canDownload (); - template bool saveStorage (StringRef name, StorageOption options, StorageVersion version, const SmallArray &data, ExtenHeader *exten); - template bool loadStorage (StringRef name, StorageOption options, StorageVersion version, SmallArray &data, ExtenHeader *exten, int32_t *outOptions); - template bool raiseLoadingError (bool isGraph, bool isDebug, MemFile &file, const char *fmt, Args &&...args); - void saveOldFormat (); void reset (); void frame (); - void loadPractice (); - void loadVisibility (); - void initNodesTypes (); + void populateNodes (); void initLightLevels (); void initNarrowPlaces (); void addPath (int addIndex, int pathIndex, float distance); @@ -360,16 +235,11 @@ public: void erase (int target); void toggleFlags (int toggleFlag); void setRadius (int index, float radius); - void rebuildVisibility (); void pathCreate (char dir); void erasePath (); void cachePoint (int index); void calculatePathRadius (int index); - void savePractice (); - void saveVisibility (); void addBasic (); - void eraseFromDisk (); - void savePathMatrix (); void setSearchIndex (int index); void startLearnJump (); void setVisited (int index); @@ -378,16 +248,14 @@ public: void addToBucket (const Vector &pos, int index); void eraseFromBucket (const Vector &pos, int index); void setBombOrigin (bool reset = false, const Vector &pos = nullptr); - void updateGlobalPractice (); void unassignPath (int from, int to); - void setDangerValue (int team, int start, int goal, int value); - void setDangerDamage (int team, int start, int goal, int value); void convertFromPOD (Path &path, const PODPath &pod); void convertToPOD (const Path &path, PODPath &pod); void convertCampDirection (Path &path); void setAutoPathDistance (const float distance); void showStats (); void showFileInfo (); + void emitNotify (int32_t sound); IntArray getNarestInRadius (float radius, const Vector &origin, int maxCount = -1); const IntArray &getNodesInBucket (const Vector &pos); @@ -463,6 +331,25 @@ public: return m_editor; } + // slicence all graph messages or not + void setMessageSilence (bool enable) { + m_silenceMessages = enable; + } + + // set exten header from binary storage + void setExtenHeader (ExtenHeader *hdr) { + memcpy (&m_extenHeader, hdr, sizeof (ExtenHeader)); + } + + // set graph header from binary storage + void setGraphHeader (StorageHeader *hdr) { + memcpy (&m_graphHeader, hdr, sizeof (StorageHeader)); + } + +public: + // graph heloer for sending message to correct channel + template void msg (const char *fmt, Args &&...args); + public: Path *begin () { return m_paths.begin (); @@ -481,29 +368,8 @@ public: } }; -// we're need `bots` #include - -// helper for reporting load errors -template bool BotGraph::raiseLoadingError (bool isGraph, bool isDebug, MemFile &file, const char *fmt, Args &&...args) { - auto result = strings.format (fmt, cr::forward (args)...); - - // display error only for graph file - if (isGraph || isDebug) { - logger.error (result); - } - - // if graph reset paths - if (isGraph) { - bots.kickEveryone (true); - - m_graphAuthor = result; - m_paths.clear (); - } - file.close (); - - return false; -} +#include // explose global CR_EXPOSE_GLOBAL_SINGLETON (BotGraph, graph); diff --git a/inc/planner.h b/inc/planner.h new file mode 100644 index 0000000..0d7a0f4 --- /dev/null +++ b/inc/planner.h @@ -0,0 +1,277 @@ +// +// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge. +// Copyright © 2004-2023 YaPB Project . +// +// SPDX-License-Identifier: MIT +// + +#pragma once + +const float kInfiniteHeuristic = 65535.0f; // max out heuristic value + +// a* route state +CR_DECLARE_SCOPED_ENUM (RouteState, + Open = 0, + Closed, + New +) + +// a * find path result +CR_DECLARE_SCOPED_ENUM (AStarResult, + Success = 0, + Failed, + InternalError, +) + +// node added +using NodeAdderFn = Lambda ; + +// route twin node +template struct RouteTwin final { +public: + int32_t index {}; + HT heuristic {}; + + constexpr RouteTwin () = default; + ~RouteTwin () = default; + +public: + constexpr RouteTwin (const int32_t &ri, const HT &rh) : index (ri), heuristic (rh) {} + +public: + constexpr bool operator < (const RouteTwin &rhs) const { + return heuristic < rhs.heuristic; + } + + constexpr bool operator > (const RouteTwin &rhs) const { + return heuristic > rhs.heuristic; + } +}; + +// bot heuristic functions for astar planner +class Heuristic final { +public: + using Func = Lambda ; + +public: + // least kills and number of nodes to goal for a team + static float gfunctionKillsDist (int team, int currentIndex, int parentIndex);; + + // least kills and number of nodes to goal for a team (when with hostage) + static float gfunctionKillsDistCTWithHostage (int team, int currentIndex, int parentIndex);; + + // least kills to goal for a team + static float gfunctionKills (int team, int currentIndex, int);; + + // least kills to goal for a team (when with hostage) + static auto gfunctionKillsCTWithHostage (int team, int currentIndex, int parentIndex) -> float;; + + // least distance for a team + static float gfunctionPathDist (int, int currentIndex, int parentIndex);; + + // least distance for a team (when with hostage) + static float gfunctionPathDistWithHostage (int, int currentIndex, int parentIndex);; + +public: + // square distance heuristic + static float hfunctionPathDist (int index, int, int goalIndex);; + + // square distance heuristic with hostages + static float hfunctionPathDistWithHostage (int index, int, int goalIndex);; + + // none heuristic + static float hfunctionNone (int index, int, int goalIndex);; +}; + +// A* algorithm for bots +class AStarAlgo final { +public: + using HeuristicFn = Heuristic::Func; + +public: + struct Route { + float g {}, f {}; + int parent { kInvalidNodeIndex }; + RouteState state { RouteState::New }; + }; + +private: + BinaryHeap > m_routeQue {}; + Array m_routes {}; + + HeuristicFn m_hcalc; + HeuristicFn m_gcalc; + + int m_length {}; + + Array m_constructedPath; + Array m_smoothedPath; + +private: + // cleares the currently built route + void clearRoute (); + + // can the node can be skipped? + bool cantSkipNode (const int a, const int b); + + // do a post-smoothing after a* finished constructing path + void postSmooth (NodeAdderFn onAddedNode); + +public: + AStarAlgo () = default; + ~AStarAlgo () = default; + +public: + // do the pathfinding + AStarResult find (int botTeam, int srcIndex, int destIndex, NodeAdderFn onAddedNode); + +public: + // initialize astar with valid path length + void init (const int length) { + m_length = length; + clearRoute (); + + m_constructedPath.reserve (getMaxLength ()); + m_smoothedPath.reserve (getMaxLength ()); + } + + // set the g heuristic + void setG (HeuristicFn fn) { + m_gcalc = fn; + } + + // set the h heuristic + void setH (HeuristicFn fn) { + m_hcalc = fn; + } + + // get route max length, route length should not be larger than half of map nodes + size_t getMaxLength () const { + return m_length / 2; + } +}; + +// floyd-warshall shortest path algorithm +class FloydWarshallAlgo final { +private: + int m_length {}; + +public: + + // floyd-warshall matrices + struct Matrix { + int16_t index { kInvalidNodeIndex }; + int16_t dist { SHRT_MAX }; + + public: + Matrix () = default; + ~Matrix () = default; + + public: + Matrix (const int index, const int dist) : index (static_cast (index)), dist (static_cast (dist)) {} + }; + +private: + SmallArray m_matrix {}; + +public: + FloydWarshallAlgo () = default; + ~FloydWarshallAlgo () = default; + +private: + // create floyd matrics + void rebuild (); + +public: + // load matrices from disk + bool load (); + + // flush matrices to disk, so we will not rebuild them on load same map + void save (); + + // do the pathfinding + bool find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance = nullptr); + +public: + // distance between two nodes with pathfinder + int dist (int srcIndex, int destIndex) { + return static_cast ((m_matrix.data () + (srcIndex * m_length) + destIndex)->dist); + } +}; + +// dijkstra shortest path algorithm +class DijkstraAlgo final { +private: + using Route = Twin ; + +private: + Array m_distance {}; + Array m_parent {}; + + BinaryHeap m_queue {}; + int m_length {}; + +public: + DijkstraAlgo () = default; + ~DijkstraAlgo () = default; + + +private: + // reset pathfinder state to defaults + void resetState (); + +public: + // initialize dijkstra with valid path length + void init (const int length); + + // do the pathfinding + bool find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance = nullptr); + + // distance between two nodes with pathfinder + int dist (int srcIndex, int destIndex); +}; + +// the bot path planner +class PathPlanner : public Singleton { +private: + UniquePtr m_dijkstra; + UniquePtr m_floyd; + UniquePtr m_astar; + bool m_memoryLimitHit {}; + +public: + PathPlanner (); + ~PathPlanner () = default; + +public: + // initialize all planners + void init (); + + // has real path distance (instead of distance2d) ? + bool hasRealPathDistance () const; + +public: + // get the dijkstra algo + decltype (auto) getDijkstra () { + return m_dijkstra.get (); + } + + // get the floyd algo + decltype (auto) getFloydWarshall () { + return m_floyd.get (); + } + + // get the floyd algo + decltype (auto) getAStar () { + return m_astar.get (); + } + +public: + // do the pathfinding + bool find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance = nullptr); + + // distance between two nodes with pathfinder + int dist (int srcIndex, int destIndex); +}; + +CR_EXPOSE_GLOBAL_SINGLETON (PathPlanner, planner); diff --git a/inc/practice.h b/inc/practice.h new file mode 100644 index 0000000..a06c9ef --- /dev/null +++ b/inc/practice.h @@ -0,0 +1,121 @@ +// +// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge. +// Copyright © 2004-2023 YaPB Project . +// +// SPDX-License-Identifier: MIT +// + +#pragma once + +// limits for storing practice data +CR_DECLARE_SCOPED_ENUM_TYPE (PracticeLimit, int32_t, + Goal = 2040, + Damage = 2040 +); + +// storage for from, to, team +class DangerStorage final { +protected: + uint16_t data[3] {}; + +public: + constexpr DangerStorage () = default; + +public: + constexpr DangerStorage (const int32_t &a, const int32_t &b, const int32_t &c) : + data { static_cast (a), static_cast (b), static_cast (c) } {} + +public: + constexpr bool operator == (const DangerStorage &rhs) const { + return rhs.data[2] == data[2] && rhs.data[1] == data[1] && rhs.data[0] == data[0]; + } + + constexpr bool operator != (const DangerStorage &rhs) const { + return !operator == (rhs); + } + +public: + // fnv1a for 3d vector hash + constexpr uint32_t hash () const { + constexpr uint32_t prime = 16777619u; + constexpr uint32_t seed = 2166136261u; + + uint32_t hash = seed; + + for (const auto &key : data) { + hash = (hash * prime) ^ key; + } + return hash; + } +}; + +// define hash function for hash map +CR_NAMESPACE_BEGIN + +template <> struct Hash { + uint32_t operator () (const DangerStorage &key) const noexcept { + return key.hash (); + } +}; + +CR_NAMESPACE_END + +class BotPractice final : public Singleton { +public: + // collected data + struct PracticeData { + int16_t damage {}, value {}; + int16_t index { kInvalidNodeIndex }; + }; + + // used to save-restore practice data + struct DangerSaveRestore { + DangerStorage danger {}; + PracticeData data {}; + + public: + DangerSaveRestore () = default; + + public: + DangerSaveRestore (const DangerStorage &ds, const PracticeData &pd) : danger (ds), data (pd) {} + }; + + HashMap m_data {}; + int32_t m_teamHighestDamage[kGameTeamNum] {}; + +public: + BotPractice () = default; + ~BotPractice () = default; + +private: + inline bool exists (int32_t team, int32_t start, int32_t goal) const { + return m_data.exists ({ start, goal, team }); + } + +public: + int32_t getIndex (int32_t team, int32_t start, int32_t goal); + void setIndex (int32_t team, int32_t start, int32_t goal, int32_t value); + + int32_t getValue (int32_t team, int32_t start, int32_t goal); + void setValue (int32_t team, int32_t start, int32_t goal, int32_t value); + + int32_t getDamage (int32_t team, int32_t start, int32_t goal); + void setDamage (int32_t team, int32_t start, int32_t goal, int32_t value); + +public: + void update (); + void load (); + void save (); + +public: + int32_t getHighestDamageForTeam (int32_t team) const { + return cr::max (1, m_teamHighestDamage[team]); + } + + void setHighestDamageForTeam (int32_t team, int32_t value) { + m_teamHighestDamage[team] = value; + } +}; + +// explose global +CR_EXPOSE_GLOBAL_SINGLETON (BotPractice, practice); diff --git a/inc/storage.h b/inc/storage.h new file mode 100644 index 0000000..306c5d3 --- /dev/null +++ b/inc/storage.h @@ -0,0 +1,103 @@ +// +// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge. +// Copyright © 2004-2023 YaPB Project . +// +// SPDX-License-Identifier: MIT +// + +#pragma once + +// storage file magic (podbot) +constexpr char kPodbotMagic[8] = "PODWAY!"; + +constexpr int32_t kStorageMagic = 0x59415042; // storage magic for yapb-data files +constexpr int32_t kStorageMagicUB = 0x544f4255; //support also the fork format (merged back into yapb) + +// storage header options +CR_DECLARE_SCOPED_ENUM (StorageOption, + Practice = cr::bit (0), // this is practice (experience) file + Matrix = cr::bit (1), // this is floyd warshal path & distance matrix + Vistable = cr::bit (2), // this is vistable data + Graph = cr::bit (3), // this is a node graph data + Official = cr::bit (4), // this is additional flag for graph indicates graph are official + Recovered = cr::bit (5), // this is additional flag indicates graph converted from podbot and was bad + Exten = cr::bit (6), // this is additional flag indicates that there's extension info + Analyzed = cr::bit (7) // this graph has been analyzed +) + +// storage header versions +CR_DECLARE_SCOPED_ENUM (StorageVersion, + Graph = 2, + Practice = 2, + Vistable = 3, + Matrix = 2, + Podbot = 7 +) + +CR_DECLARE_SCOPED_ENUM_TYPE (BotFile, uint32_t, + Vistable = 0, + LogFile = 1, + Practice = 2, + Graph = 3, + Pathmatrix = 4, + PodbotPWF = 5, + EbotEWP = 6 +) + +class BotStorage final : public Singleton { +private: + struct SaveLoadData { + String name {}; + int32_t option {}; + int32_t version {}; + + public: + SaveLoadData (StringRef name, int32_t option, int32_t version) : name (name), option (option), version (version) {} + }; + +private: + int m_retries {}; + +public: + BotStorage () = default; + ~BotStorage () = default; + +public: + // converts type to save/load options + template SaveLoadData guessType (); + + // loads the data and decompress ulz + template bool load (SmallArray &data, ExtenHeader *exten = nullptr, int32_t *outOptions = nullptr); + + // saves the data and compress with ulz + template bool save (const SmallArray &data, ExtenHeader *exten = nullptr, int32_t passOptions = 0); + + // report fatail error loading stuff + template bool error (bool isGraph, bool isDebug, MemFile &file, const char *fmt, Args &&...args); + + // builds the filename to requested filename + String buildPath (int32_t type, bool isMemoryLoad = false); + + // converts storage option to stroage filename + int32_t storageToBotFile (int32_t options); + + // remove all bot related files frorm disk + void unlinkFromDisk (); + +public: + // loading the graph may attemp to recurse loading, with converting or download, reset retry counter + void resetRetries () { + m_retries = 0; + } +}; + +#if !defined (BOT_STORAGE_EXPLICIT_INSTANTIATIONS) +# define BOT_STORAGE_EXPLICIT_INSTANTIATIONS +# include "../src/storage.cpp" +#endif + +#undef BOT_STORAGE_EXPLICIT_INSTANTIATIONS + +// explose global +CR_EXPOSE_GLOBAL_SINGLETON (BotStorage, bstor); + diff --git a/inc/support.h b/inc/support.h index 4c0d825..e6ede2f 100644 --- a/inc/support.h +++ b/inc/support.h @@ -7,16 +7,6 @@ #pragma once -CR_DECLARE_SCOPED_ENUM_TYPE (BotFile, uint32_t, - Vistable = 0, - LogFile = 1, - Practice = 2, - Graph = 3, - Pathmatrix = 4, - PodbotPWF = 5, - EbotEWP = 6 -) - class BotSupport final : public Singleton { private: bool m_needToSendWelcome {}; @@ -106,12 +96,6 @@ public: // get the current date and time as string String getCurrentDateTime (); - // builds the filename to requested filename - String buildPath (int32_t type, bool isMemoryLoad = false); - - // converts storage option to stroage filename - int32_t storageToBotFile (StorageOption options); - public: // re-show welcome after changelevel ? diff --git a/inc/vistable.h b/inc/vistable.h new file mode 100644 index 0000000..e443149 --- /dev/null +++ b/inc/vistable.h @@ -0,0 +1,61 @@ +// +// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge. +// Copyright © 2004-2023 YaPB Project . +// +// SPDX-License-Identifier: MIT +// + +#pragma once + +// limits for storing practice data +CR_DECLARE_SCOPED_ENUM_TYPE (VisIndex, int32_t, + None = 0, + Stand = 1, + Crouch = 2, + Any = Stand | Crouch +) + +// defines visibility count +struct PathVis { + uint16_t stand, crouch; +}; + +class GraphVistable final : public Singleton { +public: + using VisStorage = uint8_t; + +private: + SmallArray m_vistable {}; + bool m_rebuild {}; + int m_length {}; + + int m_curIndex {}; + int m_sliceIndex {}; + + float m_notifyMsgTimestamp {}; + +public: + GraphVistable () = default; + ~GraphVistable () = default; + +public: + bool visible (int srcIndex, int destIndex, VisIndex vis = VisIndex::Any); + + void load (); + void save (); + void rebuild (); + +public: + + // triggers re-check for all the nodes + void startRebuild (); + + // ready to use ? + bool isReady () const { + return !m_rebuild; + } +}; + +// explose global +CR_EXPOSE_GLOBAL_SINGLETON (GraphVistable, vistab); + diff --git a/inc/yapb.h b/inc/yapb.h index eaf1885..3fcf2f4 100644 --- a/inc/yapb.h +++ b/inc/yapb.h @@ -443,12 +443,6 @@ namespace TaskPri { static constexpr float EscapeFromBomb { 100.0f }; } -// storage file magic -constexpr char kPodbotMagic[8] = "PODWAY!"; - -constexpr int32_t kStorageMagic = 0x59415042; // storage magic for yapb-data files -constexpr int32_t kStorageMagicUB = 0x544f4255; //support also the fork format (merged back into yapb) - constexpr float kInfiniteDistance = 9999999.0f; constexpr float kGrenadeCheckTime = 0.6f; constexpr float kSprayDistance = 260.0f; @@ -456,10 +450,6 @@ constexpr float kDoubleSprayDistance = kSprayDistance * 2; constexpr float kMaxChatterRepeatInterval = 99.0f; constexpr int kInfiniteDistanceLong = static_cast (kInfiniteDistance); -constexpr int kMaxNodeLinks = 8; -constexpr int kMaxPracticeDamageValue = 2040; -constexpr int kMaxPracticeGoalValue = 2040; -constexpr int kMaxNodes = 2048; constexpr int kMaxWeapons = 32; constexpr int kNumWeapons = 26; constexpr int kMaxCollideMoves = 3; @@ -598,11 +588,78 @@ struct ChatCollection { // include bot graph stuff #include +// this structure links nodes returned from pathfinder +class PathWalk final : public DenyCopying { +private: + size_t m_cursor {}; + size_t m_length {}; + + UniquePtr m_path {}; + +public: + explicit PathWalk () = default; + ~PathWalk () = default; + +public: + int32_t &next () { + return at (1); + } + + int32_t &first () { + return at (0); + } + + int32_t &last () { + return at (length () - 1); + } + + int32_t &at (size_t index) { + return m_path[m_cursor + index]; + } + + void shift () { + ++m_cursor; + } + + void reverse () { + for (size_t i = 0; i < m_length / 2; ++i) { + cr::swap (m_path[i], m_path[m_length - 1 - i]); + } + } + + size_t length () const { + if (m_cursor >= m_length) { + return 0; + } + return m_length - m_cursor; + } + + bool hasNext () const { + return length () > m_cursor; + } + + bool empty () const { + return !length (); + } + + void add (int32_t node) { + m_path[m_length++] = node; + } + + void clear () { + m_cursor = 0; + m_length = 0; + + m_path[0] = 0; + } + + void init (size_t length) { + m_path = cr::makeUnique (length); + } +}; + // main bot class class Bot final { -private: - using RouteTwin = Twin ; - public: friend class BotManager; @@ -632,6 +689,7 @@ 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 float m_headedTime {}; float m_prevTime {}; // time previously checked movement speed @@ -744,16 +802,14 @@ private: Array m_ignoredBreakable {}; // list of ignored breakables Array m_hostages {}; // pointer to used hostage entities - Array m_routes {}; // pointer Array m_nodeHistory {}; // history of selected goals - BinaryHeap m_routeQue {}; Path *m_path {}; // pointer to the current path node String m_chatBuffer {}; // space for strings (say text...) FrustumPlane m_frustum[FrustumSide::Num] {}; private: - int pickBestWeapon (int *vec, int count, int moneySave); + int pickBestWeapon (Array &vec, int moneySave); int getRandomCampDir (); int findAimingNode (const Vector &to, int &pathLength); int findNearestNode (); @@ -863,9 +919,7 @@ private: void updatePracticeDamage (edict_t *attacker, int damage); void findShortestPath (int srcIndex, int destIndex); void calculateFrustum (); - void findPath (int srcIndex, int destIndex, FindPath pathType = FindPath::Fast); - void clearRoute (); void debugMsgInternal (const char *str); void frame (); void resetCollision (); @@ -950,6 +1004,13 @@ private: issueCommand ("drop"); } + // ensures current node is ok + void ensureCurrentNodeIndex () { + if (m_currentNodeIndex == kInvalidNodeIndex) { + changeNodeIndex (findNearestNode ()); + } + } + public: entvars_t *pev {}; @@ -1189,6 +1250,9 @@ public: #include "engine.h" #include "manager.h" #include "control.h" +#include "planner.h" +#include "storage.h" +#include "analyze.h" // very global convars extern ConVar cv_jasonmode; @@ -1211,7 +1275,10 @@ extern ConVar cv_debug_goal; extern ConVar cv_save_bots_names; extern ConVar cv_random_knife_attacks; extern ConVar cv_rotate_bots; +extern ConVar cv_graph_url; extern ConVar cv_graph_url_upload; +extern ConVar cv_graph_auto_save_count; +extern ConVar cv_graph_analyze_max_jump_height; extern ConVar mp_freezetime; extern ConVar mp_roundtime; diff --git a/meson.build b/meson.build index 05c038c..88d62e1 100644 --- a/meson.build +++ b/meson.build @@ -14,7 +14,7 @@ project ( default_options: [ 'buildtype=release', 'b_ndebug=if-release', - 'cpp_std=c++14', + 'cpp_std=c++17', 'warning_level=3', 'werror=true', 'backend=ninja', @@ -217,11 +217,12 @@ elif os == 'windows' and (cxx =='msvc' or cxx == 'clang-cl') endif # pass our hell to meson -add_global_arguments (cxxflags, language: 'cpp') -add_global_link_arguments (ldflags, language: 'cpp') +add_global_arguments(cxxflags, language: 'cpp') +add_global_link_arguments(ldflags, language: 'cpp') # add the sources -sources = files ( +sources = files( + 'src/analyze.cpp', 'src/botlib.cpp', 'src/chatlib.cpp', 'src/combat.cpp', @@ -234,18 +235,22 @@ sources = files ( 'src/module.cpp', 'src/message.cpp', 'src/navigate.cpp', + 'src/planner.cpp', + 'src/practice.cpp', 'src/sounds.cpp', - 'src/support.cpp' + 'src/storage.cpp', + 'src/support.cpp', + 'src/vistable.cpp' ) # add the include directories -includes = include_directories ([ +includes = include_directories([ '.', 'inc', 'ext', 'ext/crlib' ], is_system: true) # if have git and on windows add windows-specific version info if os == 'windows' and git.found() - sources += import('windows').compile_resources ( + sources += import('windows').compile_resources( 'vc/yapb.rc', include_directories: includes, args: flags_versioned diff --git a/src/analyze.cpp b/src/analyze.cpp new file mode 100644 index 0000000..04e5584 --- /dev/null +++ b/src/analyze.cpp @@ -0,0 +1,359 @@ +// +// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge. +// Copyright © 2004-2023 YaPB Project . +// +// SPDX-License-Identifier: MIT +// + +#include + +ConVar cv_graph_analyze_auto_start ("yb_graph_analyze_auto_start", "1", "Autostart analyzer if all other cases are failed."); +ConVar cv_graph_analyze_auto_save ("yb_graph_analyze_auto_save", "1", "Auto save results of analyzation to graph file. And re-add bots."); +ConVar cv_graph_analyze_distance ("yb_graph_analyze_distance", "64", "The minimum distance to keep nodes from each other.", true, 42.0f, 128.0f); +ConVar cv_graph_analyze_max_jump_height ("yb_graph_analyze_max_jump_height", "44", "Max jump height to test if next node will be unreachable.", true, 44.0f, 64.0f); +ConVar cv_graph_analyze_fps ("yb_graph_analyze_fps", "30.0", "The FPS at which analyzer process is running. This keeps game from freezing during analyzing.", false, 25.0f, 99.0f); +ConVar cv_graph_analyze_clean_paths_on_finish ("yb_graph_analyze_clean_paths_on_finish", "1", "Specifies if analyzer should clean the unnecessary paths upon finishing."); +ConVar cv_graph_analyze_optimize_nodes_on_finish ("yb_graph_analyze_optimize_nodes_on_finish", "1", "Specifies if analyzer should merge some near-placed nodes with much of connections together."); +ConVar cv_graph_analyze_mark_goals_on_finish ("yb_graph_analyze_mark_goals_on_finish", "1", "Specifies if analyzer should mark nodes as map goals automatically upon finish."); + +void GraphAnalyze::start () { + // start analyzer in few seconds after level initialized + if (cv_graph_analyze_auto_start.bool_ ()) { + m_updateInterval = game.time () + 3.0f; + m_basicsCreated = false; + + // set as we're analyzing + m_isAnalyzing = true; + + // silence all graph messages + graph.setMessageSilence (true); + + // set all nodes as not expanded + for (auto &expanded : m_expandedNodes) { + expanded = false; + } + + // set all nodes as not optimized + for (auto &optimized : m_optimizedNodes) { + optimized = false; + } + ctrl.msg ("Starting map analyzation."); + } + else { + m_updateInterval = 0.0f; + } +} + +void GraphAnalyze::update () { + if (cr::fzero (m_updateInterval) || !m_isAnalyzing) { + return; + } + + if (m_updateInterval >= game.time ()) { + return; + } + + // add basic nodes + if (!m_basicsCreated) { + graph.addBasic (); + m_basicsCreated = true; + } + + for (int i = 0; i < graph.length (); ++i) { + if (m_updateInterval >= game.time ()) { + return; + } + + if (!graph.exists (i)) { + return; + } + + if (m_expandedNodes[i]) { + continue; + } + + m_expandedNodes[i] = true; + setUpdateInterval (); + + auto pos = graph[i].origin; + auto range = cv_graph_analyze_distance.float_ (); + + for (int dir = 1; dir < kMaxNodeLinks; ++dir) { + switch (dir) { + case 1: + flood (pos, { pos.x + range, pos.y, pos.z }, range); + break; + + case 2: + flood (pos, { pos.x - range, pos.y, pos.z }, range); + break; + + case 3: + flood (pos, { pos.x, pos.y + range, pos.z }, range); + break; + + case 4: + flood (pos, { pos.x, pos.y - range, pos.z }, range); + break; + + case 5: + flood (pos, { pos.x + range, pos.y, pos.z + 128.0f }, range); + break; + + case 6: + flood (pos, { pos.x - range, pos.y, pos.z + 128.0f }, range); + break; + + case 7: + flood (pos, { pos.x, pos.y + range, pos.z + 128.0f }, range); + break; + + case 8: + flood (pos, { pos.x, pos.y - range, pos.z + 128.0f }, range); + break; + } + } + } + + // finish generation if no updates occurred recently + if (m_updateInterval + 2.0f < game.time ()) { + finish (); + return; + } +} + +void GraphAnalyze::suspend () { + m_updateInterval = 0.0f; + m_isAnalyzing = false; + m_isAnalyzed = false; +} + +void GraphAnalyze::finish () { + // run optimization on finish + optimize (); + + // mark goal nodes + markGoals (); + + m_isAnalyzed = true; + m_isAnalyzing = false; + m_updateInterval = 0.0f; + + // un-silence all graph messages + graph.setMessageSilence (false); + + ctrl.msg ("Complete map analyzation."); + + // autosave bots graph + if (cv_graph_analyze_auto_save.bool_ ()) { + if (!graph.saveGraphData ()) { + ctrl.msg ("Can't save analyzed graph. Internal error."); + return; + } + + if (!graph.loadGraphData ()) { + ctrl.msg ("Can't load analyzed graph. Internal error."); + return; + } + vistab.startRebuild (); + cv_quota.revert (); + } +} + +void GraphAnalyze::optimize () { + if (graph.length () == 0) { + return; + } + + if (!cv_graph_analyze_optimize_nodes_on_finish.bool_ ()) { + return; + } + cleanup (); + + // clear the uselss connections + if (cv_graph_analyze_clean_paths_on_finish.bool_ ()) { + for (auto i = 0; i < graph.length (); ++i) { + graph.clearConnections (i); + } + } + + auto smooth = [] (const Array &nodes) { + Vector result; + + for (const auto &node : nodes) { + result += graph[node].origin; + } + result /= kMaxNodeLinks; + result.z = graph[nodes.first ()].origin.z; + + return result; + }; + + // set all nodes as not optimized + for (auto &optimized : m_optimizedNodes) { + optimized = false; + } + + for (int i = 0; i < graph.length (); ++i) { + if (m_optimizedNodes[i]) { + continue; + } + const auto &path = graph[i]; + Array indexes; + + for (const auto &link : path.links) { + if (graph.exists (link.index) && !m_optimizedNodes[link.index] && cr::fequal (path.origin.z, graph[link.index].origin.z)) { + indexes.emplace (link.index); + } + } + + // we're have max out node links + if (indexes.length () >= kMaxNodeLinks) { + const Vector &pos = smooth (indexes); + + for (const auto &index : indexes) { + graph.erase (index); + } + graph.add (NodeAddFlag::Normal, pos); + } + } +} + +void GraphAnalyze::cleanup () { + int connections = 0; // clean bad paths + + for (auto i = 0; i < graph.length (); ++i) { + connections = 0; + + for (const auto &link : graph[i].links) { + if (link.index != kInvalidNodeIndex) { + if (link.index > graph.length ()) { + graph.erase (i); + } + ++connections; + } + } + + // no connections + if (!connections) { + graph.erase (i); + } + + // path number differs + if (graph[i].number != i) { + graph.erase (i); + } + + for (const auto &link : graph[i].links) { + if (link.index != kInvalidNodeIndex) { + if (link.index >= graph.length () || link.index < -kInvalidNodeIndex) { + graph.erase (i); + } + else if (link.index == i) { + graph.erase (i); + } + } + } + + if (!graph.isConnected (i)) { + graph.erase (i); + } + } +} + +void GraphAnalyze::flood (const Vector &pos, const Vector &next, float range) { + range *= 0.75f; + + TraceResult tr; + game.testHull (pos, { next.x, next.y, next.z + 19.0f }, TraceIgnore::Monsters, head_hull, nullptr, &tr); + + // we're can't reach next point + if (!cr::fequal (tr.flFraction, 1.0f) && !game.isShootableBreakable (tr.pHit)) { + return; + } + + // we're have something in around, skip + if (graph.exists (graph.getForAnalyzer (tr.vecEndPos, range))) { + return; + } + game.testHull (tr.vecEndPos, { tr.vecEndPos.x, tr.vecEndPos.y, tr.vecEndPos.z - 999.0f }, TraceIgnore::Monsters, head_hull, nullptr, &tr); + + // ground is away for a break + if (cr::fequal (tr.flFraction, 1.0f)) { + return; + } + Vector nextPos = { tr.vecEndPos.x, tr.vecEndPos.y, tr.vecEndPos.z + 19.0f }; + + const int endIndex = graph.getForAnalyzer (nextPos, range); + const int targetIndex = graph.getNearestNoBuckets (nextPos, 250.0f); + + if (graph.exists (endIndex) || !graph.exists (targetIndex)) { + return; + } + auto targetPos = graph[targetIndex].origin; + + // re-check there's nothing nearby, and add something we're want + if (!graph.exists (graph.getNearestNoBuckets (nextPos, range))) { + m_isCrouch = false; + game.testLine (nextPos, { nextPos.x, nextPos.y, nextPos.z + 36.0f }, TraceIgnore::Monsters, nullptr, &tr); + + if (!cr::fequal (tr.flFraction, 1.0f)) { + m_isCrouch = true; + } + auto testPos = m_isCrouch ? Vector { nextPos.x, nextPos.y, nextPos.z - 18.0f } : nextPos; + + if ((graph.isNodeReacheable (targetPos, testPos) && graph.isNodeReacheable (testPos, targetPos)) || (graph.isNodeReacheableWithJump (testPos, targetPos) && graph.isNodeReacheableWithJump (targetPos, testPos))) { + graph.add (NodeAddFlag::Normal, m_isCrouch ? Vector { nextPos.x, nextPos.y, nextPos.z - 9.0f } : nextPos); + } + } +} + +void GraphAnalyze::setUpdateInterval () { + const auto frametime (globals->frametime); + + if ((cv_graph_analyze_fps.float_ () + frametime) <= 1.0f / frametime) { + m_updateInterval = game.time () + frametime * 0.06f; + } +} + +void GraphAnalyze::markGoals () { + if (!cv_graph_analyze_mark_goals_on_finish.bool_ ()) { + return; + } + + auto updateNodeFlags = [] (int type, const char *entity) { + game.searchEntities ("classname", entity, [&] (edict_t *ent) { + for (auto &path : graph) { + const auto &absOrigin = path.origin + Vector (1.0f, 1.0f, 1.0f); + + if (ent->v.absmin.x > absOrigin.x || ent->v.absmin.y > absOrigin.y) { + continue; + } + + if (ent->v.absmax.x < absOrigin.x || ent->v.absmax.y < absOrigin.y) { + continue; + } + path.flags |= type; + } + return EntitySearchResult::Continue; + }); + }; + + + if (game.mapIs (MapFlags::Demolition)) { + updateNodeFlags (NodeFlag::Goal, "func_bomb_target"); // bombspot zone + updateNodeFlags (NodeFlag::Goal, "info_bomb_target"); // bombspot zone (same as above) + } + else if (game.mapIs (MapFlags::HostageRescue)) { + updateNodeFlags (NodeFlag::Rescue, "func_hostage_rescue"); // hostage rescue zone + updateNodeFlags (NodeFlag::Rescue, "info_hostage_rescue"); // hostage rescue zone (same as above) + updateNodeFlags (NodeFlag::Rescue, "info_player_start"); // then add ct spawnpoints + + updateNodeFlags (NodeFlag::Goal, "hostage_entity"); // hostage entities + updateNodeFlags (NodeFlag::Goal, "monster_scientist"); // hostage entities (same as above) + } + else if (game.mapIs (MapFlags::Assassination)) { + updateNodeFlags (NodeFlag::Goal, "func_vip_safetyzone"); // vip rescue (safety) zone + updateNodeFlags (NodeFlag::Goal, "func_escapezone"); // terrorist escape zone + } +} diff --git a/src/botlib.cpp b/src/botlib.cpp index e0886ca..2d1579d 100644 --- a/src/botlib.cpp +++ b/src/botlib.cpp @@ -167,7 +167,7 @@ void Bot::avoidGrenades () { float distance = pent->v.origin.distanceSq (pev->origin); float distanceMoved = pev->origin.distance (pent->v.origin + pent->v.velocity * m_frameInterval); - if (distanceMoved < distance && distance < cr::square (500.0f)) { + if (distanceMoved < distance && distance < cr::sqrf (500.0f)) { const auto &dirToPoint = (pev->origin - pent->v.origin).normalize2d (); const auto &rightSide = pev->v_angle.right ().normalize2d (); @@ -242,12 +242,12 @@ void Bot::checkBreakablesAround () { const auto lengthToObstacle = origin.distanceSq (pev->origin); // too far, skip it - if (lengthToObstacle > cr::square (radius)) { + if (lengthToObstacle > cr::sqrf (radius)) { continue; } // too close, skip it - if (lengthToObstacle < cr::square (100.0f)) { + if (lengthToObstacle < cr::sqrf (100.0f)) { continue; } @@ -333,7 +333,7 @@ void Bot::updatePickups () { } const auto &intresting = bots.getIntrestingEntities (); - const float radius = cr::square (cv_object_pickup_radius.float_ ()); + const float radius = cr::sqrf (cv_object_pickup_radius.float_ ()); if (!game.isNullEntity (m_pickupItem)) { bool itemExists = false; @@ -573,7 +573,7 @@ void Bot::updatePickups () { if (allowPickup) { for (auto &client : util.getClients ()) { if ((client.flags & ClientFlags::Used) && !(client.ent->v.flags & FL_FAKECLIENT) && (client.flags & ClientFlags::Alive) && - client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::square (240.0f)) { + client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::sqrf (240.0f)) { allowPickup = false; break; } @@ -620,7 +620,7 @@ void Bot::updatePickups () { } } - if (pev->origin.distanceSq (origin) > cr::square (60.0f)) { + if (pev->origin.distanceSq (origin) > cr::sqrf (60.0f)) { if (!graph.isNodeReacheable (pev->origin, origin)) { allowPickup = false; @@ -731,7 +731,7 @@ Vector Bot::getCampDirection (const Vector &dest) { if (tr.flFraction < 1.0f) { float length = tr.vecEndPos.distanceSq (src); - if (length > cr::square (1024.0f)) { + if (length > cr::sqrf (1024.0f)) { return nullptr; } @@ -750,7 +750,7 @@ Vector Bot::getCampDirection (const Vector &dest) { if (link.index == kInvalidNodeIndex) { continue; } - auto distance = static_cast (graph.getPathDist (link.index, enemyIndex)); + auto distance = static_cast (planner.dist (link.index, enemyIndex)); if (distance < minDistance) { minDistance = distance; @@ -762,7 +762,7 @@ Vector Bot::getCampDirection (const Vector &dest) { return graph[lookAtWaypoint].origin; } } - auto dangerIndex = graph.getDangerIndex (m_team, m_currentNodeIndex, m_currentNodeIndex); + auto dangerIndex = practice.getIndex (m_team, m_currentNodeIndex, m_currentNodeIndex); if (graph.exists (dangerIndex)) { return graph[dangerIndex].origin; @@ -1110,9 +1110,12 @@ bool Bot::canReplaceWeapon () { return isWeaponRestricted (m_currentWeapon); } -int Bot::pickBestWeapon (int *vec, int count, int moneySave) { +int Bot::pickBestWeapon (Array &vec, int moneySave) { // this function picks best available weapon from random choice with money save + if (vec.length () < 2) { + return vec.first (); + } bool needMoreRandomWeapon = (m_personality == Personality::Careful) || (rg.chance (25) && m_personality == Personality::Normal); if (needMoreRandomWeapon) { @@ -1121,14 +1124,11 @@ int Bot::pickBestWeapon (int *vec, int count, int moneySave) { if (buyFactor < 1.0f) { buyFactor = 1.0f; } - // swap array values - for (int *begin = vec, *end = vec + count - 1; begin < end; ++begin, --end) { - cr::swap (*end, *begin); - } - return vec[static_cast (static_cast (count - 1) * cr::log10 (rg.get (1.0f, cr::powf (10.0f, buyFactor))) / buyFactor + 0.5f)]; - } + vec.reverse (); + return vec[static_cast (static_cast (vec.length () - 1) * cr::log10 (rg.get (1.0f, cr::powf (10.0f, buyFactor))) / buyFactor + 0.5f)]; + } int chance = 95; // high skilled bots almost always prefer best weapon @@ -1140,17 +1140,17 @@ int Bot::pickBestWeapon (int *vec, int count, int moneySave) { chance = 75; } } - auto &info = conf.getWeapons (); + const auto &tab = conf.getWeapons (); - for (int i = 0; i < count; ++i) { - auto &weapon = info[vec[i]]; + for (const auto &w : vec) { + const auto &weapon = tab[w]; // if wea have enough money for weapon buy it if (weapon.price + moneySave < m_moneyAmount + rg.get (50, 200) && rg.chance (chance)) { - return vec[i]; + return w; } } - return vec[rg.get (0, count - 1)]; + return vec.random (); } void Bot::buyStuff () { @@ -1163,8 +1163,8 @@ void Bot::buyStuff () { m_nextBuyTime += rg.get (0.3f, 0.5f); } - int count = 0, weaponCount = 0; - int choices[kNumWeapons] {}; + int count = 0; + Array choices; // select the priority tab for this personality const int *pref = conf.getWeaponPrefs (m_personality) + kNumWeapons; @@ -1312,23 +1312,14 @@ void Bot::buyStuff () { } if (selectedWeapon->price <= (m_moneyAmount - moneySave)) { - choices[weaponCount++] = *pref; + choices.emplace (*pref); } - } while (count < kNumWeapons && weaponCount < 4); + } while (count < kNumWeapons && choices.length () < 4); // found a desired weapon? - if (weaponCount > 0) { - int chosenWeapon; - - // choose randomly from the best ones... - if (weaponCount > 1) { - chosenWeapon = pickBestWeapon (choices, weaponCount, moneySave); - } - else { - chosenWeapon = choices[weaponCount - 1]; - } - selectedWeapon = &tab[chosenWeapon]; + if (!choices.empty ()) { + selectedWeapon = &tab[pickBestWeapon (choices, moneySave)]; } else { selectedWeapon = nullptr; @@ -1406,23 +1397,14 @@ void Bot::buyStuff () { } if (selectedWeapon->price <= (m_moneyAmount - rg.get (100, 200))) { - choices[weaponCount++] = *pref; + choices.emplace (*pref); } - } while (count < kNumWeapons && weaponCount < 4); + } while (count < kNumWeapons && choices.length () < 4); // found a desired weapon? - if (weaponCount > 0) { - int chosenWeapon; - - // choose randomly from the best ones... - if (weaponCount > 1) { - chosenWeapon = pickBestWeapon (choices, weaponCount, rg.get (100, 200)); - } - else { - chosenWeapon = choices[weaponCount - 1]; - } - selectedWeapon = &tab[chosenWeapon]; + if (!choices.empty ()) { + selectedWeapon = &tab[pickBestWeapon (choices, rg.get (100, 200))]; } else { selectedWeapon = nullptr; @@ -1708,14 +1690,17 @@ void Bot::setConditions () { } if (game.isNullEntity (m_enemy) && !game.isNullEntity (m_lastEnemy) && !m_lastEnemyOrigin.empty () && m_seeEnemyTime + 0.5f < game.time ()) { + m_lastPredictIndex = kInvalidNodeIndex; + auto distanceToLastEnemySq = m_lastEnemyOrigin.distanceSq (pev->origin); - if (distanceToLastEnemySq < cr::square (1600.0f)) { + 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::square (384.0f)) { + if (graph.exists (nodeIndex) && pathLength < cv_max_nodes_for_predict.int_ () && pev->origin.distanceSq (graph[nodeIndex].origin) > cr::sqrf (384.0f)) { m_aimFlags |= AimFlags::PredictPath; + m_lastPredictIndex = nodeIndex; } } @@ -2050,7 +2035,7 @@ bool Bot::isEnemyThreat () { } // if enemy is near or facing us directly - if (m_enemy->v.origin.distanceSq (pev->origin) < cr::square (256.0f) || (!usesKnife () && isInViewCone (m_enemy->v.origin))) { + if (m_enemy->v.origin.distanceSq (pev->origin) < cr::sqrf (256.0f) || (!usesKnife () && isInViewCone (m_enemy->v.origin))) { return true; } return false; @@ -2071,13 +2056,13 @@ bool Bot::reactOnEnemy () { } int enemyIndex = graph.getNearest (m_enemy->v.origin); - auto lineDist = m_enemy->v.origin.distance (pev->origin); - auto pathDist = static_cast (graph.getPathDist (ownIndex, enemyIndex)); + auto lineDist = m_enemy->v.origin.distance2d (pev->origin); + auto pathDist = static_cast (planner.dist (ownIndex, enemyIndex)); - if (pathDist - lineDist > 112.0f || isOnLadder ()) { + if (pathDist - lineDist > (planner.hasRealPathDistance () ? 112.0f : 32.0f) || isOnLadder ()) { m_isEnemyReachable = false; } - else { + else if (vistab.visible (ownIndex, enemyIndex)) { m_isEnemyReachable = true; } m_enemyReachableTimer = game.time () + 1.0f; @@ -2647,19 +2632,19 @@ void Bot::updateAimDir () { else if (flags & AimFlags::PredictPath) { bool changePredictedEnemy = true; - if (m_timeNextTracking > game.time () && m_trackingEdict == m_lastEnemy) { + if (m_timeNextTracking < game.time () && m_trackingEdict == m_lastEnemy) { changePredictedEnemy = false; } if (changePredictedEnemy) { auto pathLength = 0; - auto aimNode = findAimingNode (m_lastEnemyOrigin, pathLength); + auto aimNode = graph.exists (m_lastPredictIndex) ? m_lastPredictIndex : findAimingNode (m_lastEnemyOrigin, pathLength); if (graph.exists (aimNode) && pathLength < cv_max_nodes_for_predict.int_ ()) { m_lookAt = graph[aimNode].origin; m_lookAtSafe = m_lookAt; - m_timeNextTracking = game.time () + 0.5f; + m_timeNextTracking = game.time () + 0.75f; m_trackingEdict = m_lastEnemy; } else { @@ -2678,10 +2663,10 @@ void Bot::updateAimDir () { else if (flags & AimFlags::Nav) { m_lookAt = m_destOrigin; - if (m_moveToGoal && m_seeEnemyTime + 4.0f < game.time () && !m_isStuck && m_moveSpeed > getShiftSpeed () && !(pev->button & IN_DUCK) && m_currentNodeIndex != kInvalidNodeIndex && !(m_pathFlags & (NodeFlag::Ladder | NodeFlag::Crouch)) && m_pathWalk.hasNext () && pev->origin.distanceSq (m_destOrigin) < cr::square (512.0f)) { + if (m_moveToGoal && m_seeEnemyTime + 4.0f < game.time () && !m_isStuck && m_moveSpeed > getShiftSpeed () && !(pev->button & IN_DUCK) && m_currentNodeIndex != kInvalidNodeIndex && !(m_pathFlags & (NodeFlag::Ladder | NodeFlag::Crouch)) && m_pathWalk.hasNext () && pev->origin.distanceSq (m_destOrigin) < cr::sqrf (512.0f)) { auto nextPathIndex = m_pathWalk.next (); - if (graph.isVisible (m_currentNodeIndex, nextPathIndex)) { + if (vistab.visible (m_currentNodeIndex, nextPathIndex)) { m_lookAt = graph[nextPathIndex].origin + pev->view_ofs; } else { @@ -2696,10 +2681,10 @@ void Bot::updateAimDir () { } if (m_canChooseAimDirection && m_seeEnemyTime + 4.0f < game.time () && m_currentNodeIndex != kInvalidNodeIndex && !(m_pathFlags & NodeFlag::Ladder)) { - auto dangerIndex = graph.getDangerIndex (m_team, m_currentNodeIndex, m_currentNodeIndex); + auto dangerIndex = practice.getIndex (m_team, m_currentNodeIndex, m_currentNodeIndex); - if (graph.exists (dangerIndex) && graph.isVisible (m_currentNodeIndex, dangerIndex) && !(graph[dangerIndex].flags & NodeFlag::Crouch)) { - if (pev->origin.distanceSq (graph[dangerIndex].origin) < cr::square (160.0f)) { + if (graph.exists (dangerIndex) && vistab.visible (m_currentNodeIndex, dangerIndex) && !(graph[dangerIndex].flags & NodeFlag::Crouch)) { + if (pev->origin.distanceSq (graph[dangerIndex].origin) < cr::sqrf (160.0f)) { m_lookAt = m_destOrigin; } else { @@ -2730,7 +2715,7 @@ void Bot::checkDarkness () { } // do not check every frame - if (m_checkDarkTime + 5.0f > game.time ()) { + if (m_checkDarkTime + 5.0f > game.time () || cr::fzero (m_path->light)) { return; } auto skyColor = illum.getSkyColor (); @@ -2798,7 +2783,7 @@ void Bot::frame () { if (bots.isBombPlanted () && m_team == Team::CT && m_notKilled) { const Vector &bombPosition = graph.getBombOrigin (); - if (!m_hasProgressBar && getCurrentTaskId () != Task::EscapeFromBomb && pev->origin.distanceSq (bombPosition) < cr::square (1540.0f) && !isBombDefusing (bombPosition)) { + if (!m_hasProgressBar && getCurrentTaskId () != Task::EscapeFromBomb && pev->origin.distanceSq (bombPosition) < cr::sqrf (1540.0f) && !isBombDefusing (bombPosition)) { m_itemIgnore = nullptr; m_itemCheckTime = game.time (); @@ -2823,7 +2808,7 @@ void Bot::frame () { } // clear enemy far away - if (!m_lastEnemyOrigin.empty () && !game.isNullEntity (m_lastEnemy) && pev->origin.distanceSq (m_lastEnemyOrigin) >= cr::square (2048.0)) { + if (!m_lastEnemyOrigin.empty () && !game.isNullEntity (m_lastEnemy) && pev->origin.distanceSq (m_lastEnemyOrigin) >= cr::sqrf (2048.0)) { m_lastEnemy = nullptr; m_lastEnemyOrigin = nullptr; } @@ -3128,6 +3113,7 @@ void Bot::normal_ () { if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted ()) { pathSearchType = rg.chance (60) ? FindPath::Fast : FindPath::Optimal; } + ensureCurrentNodeIndex (); // do pathfinding if it's not the current waypoint if (destIndex != m_currentNodeIndex) { @@ -3347,6 +3333,8 @@ void Bot::seekCover_ () { m_prevGoalIndex = destIndex; getTask ()->data = destIndex; + ensureCurrentNodeIndex (); + if (destIndex != m_currentNodeIndex) { findPath (m_currentNodeIndex, destIndex, FindPath::Fast); } @@ -3432,6 +3420,7 @@ void Bot::blind_ () { } else if (!hasActiveGoal ()) { clearSearchNodes (); + ensureCurrentNodeIndex (); m_prevGoalIndex = m_blindNodeIndex; getTask ()->data = m_blindNodeIndex; @@ -3652,6 +3641,7 @@ void Bot::moveToPos_ () { m_prevGoalIndex = destIndex; getTask ()->data = destIndex; + ensureCurrentNodeIndex (); findPath (m_currentNodeIndex, destIndex, m_pathType); } else { @@ -3918,7 +3908,7 @@ void Bot::followUser_ () { m_reloadState = Reload::Primary; } - if (m_targetEntity->v.origin.distanceSq (pev->origin) > cr::square (130.0f)) { + if (m_targetEntity->v.origin.distanceSq (pev->origin) > cr::sqrf (130.0f)) { m_followWaitTime = 0.0f; } else { @@ -3998,7 +3988,7 @@ void Bot::throwExplosive_ () { ignoreCollision (); - if (pev->origin.distanceSq (dest) < cr::square (450.0f)) { + if (pev->origin.distanceSq (dest) < cr::sqrf (450.0f)) { // heck, I don't wanna blow up myself m_grenadeCheckTime = game.time () + kGrenadeCheckTime * 2.0f; @@ -4065,7 +4055,7 @@ void Bot::throwFlashbang_ () { ignoreCollision (); - if (pev->origin.distanceSq (dest) < cr::square (450.0f)) { + if (pev->origin.distanceSq (dest) < cr::sqrf (450.0f)) { m_grenadeCheckTime = game.time () + kGrenadeCheckTime * 2.0f; // heck, I don't wanna blow up myself selectBestWeapon (); @@ -4255,7 +4245,7 @@ void Bot::escapeFromBomb_ () { completeTask (); // we're done // press duck button if we still have some enemies - if (numEnemiesNear (pev->origin, 2048.0f)) { + if (m_numEnemiesLeft > 0) { m_campButtons = IN_DUCK; } @@ -4267,37 +4257,39 @@ void Bot::escapeFromBomb_ () { else if (!hasActiveGoal ()) { clearSearchNodes (); - int lastSelectedGoal = kInvalidNodeIndex, minPathDistance = kInfiniteDistanceLong; + int bestIndex = kInvalidNodeIndex; + float safeRadius = rg.get (1513.0f, 2048.0f); + float closestDistance = kInfiniteDistance; for (const auto &path : graph) { if (path.origin.distance (graph.getBombOrigin ()) < safeRadius || isOccupiedNode (path.number)) { continue; } - int pathDistance = graph.getPathDist (m_currentNodeIndex, path.number); + float distance = pev->origin.distance (path.origin); - if (minPathDistance > pathDistance) { - minPathDistance = pathDistance; - lastSelectedGoal = path.number; + if (closestDistance > distance) { + closestDistance = distance; + bestIndex = path.number; } } - if (lastSelectedGoal < 0) { - lastSelectedGoal = graph.getFarest (pev->origin, safeRadius); + if (bestIndex < 0) { + bestIndex = graph.getFarest (pev->origin, safeRadius); } // still no luck? - if (lastSelectedGoal < 0) { + if (bestIndex < 0) { completeTask (); // we're done // we have no destination point, so just sit down and camp startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + 10.0f, true); return; } - m_prevGoalIndex = lastSelectedGoal; - getTask ()->data = lastSelectedGoal; + m_prevGoalIndex = bestIndex; + getTask ()->data = bestIndex; - findPath (m_currentNodeIndex, lastSelectedGoal, FindPath::Fast); + findPath (m_currentNodeIndex, bestIndex, FindPath::Fast); } } @@ -4511,7 +4503,7 @@ void Bot::pickupItem_ () { // check if hostage is with a human teammate (hack) for (auto &client : util.getClients ()) { if ((client.flags & ClientFlags::Used) && !(client.ent->v.flags & FL_FAKECLIENT) && (client.flags & ClientFlags::Alive) && - client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::square (240.0f)) { + client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::sqrf (240.0f)) { return EntitySearchResult::Continue; } } @@ -4760,7 +4752,7 @@ void Bot::logic () { // calculate 2 direction vectors, 1 without the up/down component const Vector &dirOld = m_destOrigin - (pev->origin + pev->velocity * m_frameInterval); - const Vector &dirNormal = dirOld.normalize2d (); + const Vector &dirNormal = dirOld.normalize2d_apx (); m_moveAngles = dirOld.angles (); m_moveAngles.clampAngles (); @@ -4968,7 +4960,7 @@ bool Bot::hasHostage () { if (!game.isNullEntity (hostage)) { // don't care about dead hostages - if (hostage->v.health <= 0.0f || pev->origin.distanceSq (hostage->v.origin) > cr::square (600.0f)) { + if (hostage->v.health <= 0.0f || pev->origin.distanceSq (hostage->v.origin) > cr::sqrf (600.0f)) { hostage = nullptr; continue; } @@ -5114,10 +5106,13 @@ void Bot::updatePracticeValue (int damage) { } auto health = static_cast (m_healthValue); + // max goal value + constexpr int maxGoalValue = PracticeLimit::Goal; + // only rate goal waypoint if bot died because of the damage // FIXME: could be done a lot better, however this cares most about damage done by sniping or really deadly weapons if (health - damage <= 0) { - graph.setDangerValue (m_team, m_chosenGoalIndex, m_prevGoalIndex, cr::clamp (graph.getDangerValue (m_team, m_chosenGoalIndex, m_prevGoalIndex) - health / 20, -kMaxPracticeGoalValue, kMaxPracticeGoalValue)); + practice.setValue (m_team, m_chosenGoalIndex, m_prevGoalIndex, cr::clamp (practice.getValue (m_team, m_chosenGoalIndex, m_prevGoalIndex) - health / 20, -maxGoalValue, maxGoalValue)); } } @@ -5134,6 +5129,7 @@ void Bot::updatePracticeDamage (edict_t *attacker, int damage) { if (attackerTeam == victimTeam) { return; } + constexpr int maxDamageValue = PracticeLimit::Damage; // if these are bots also remember damage to rank destination of the bot m_goalValue -= static_cast (damage); @@ -5155,18 +5151,18 @@ void Bot::updatePracticeDamage (edict_t *attacker, int damage) { if (m_healthValue > 20.0f) { if (victimTeam == Team::Terrorist || victimTeam == Team::CT) { - graph.setDangerDamage (victimIndex, victimIndex, victimIndex, cr::clamp (graph.getDangerDamage (victimTeam, victimIndex, victimIndex), 0, kMaxPracticeDamageValue)); + practice.setDamage (victimIndex, victimIndex, victimIndex, cr::clamp (practice.getDamage (victimTeam, victimIndex, victimIndex), 0, maxDamageValue)); } } auto updateDamage = util.isFakeClient (attacker) ? 10 : 7; // store away the damage done - int damageValue = cr::clamp (graph.getDangerDamage (m_team, victimIndex, attackerIndex) + damage / updateDamage, 0, kMaxPracticeDamageValue); + int damageValue = cr::clamp (practice.getDamage (m_team, victimIndex, attackerIndex) + damage / updateDamage, 0, maxDamageValue); if (damageValue > graph.getHighestDamageForTeam (m_team)) { graph.setHighestDamageForTeam (m_team, damageValue); } - graph.setDangerDamage (m_team, victimIndex, attackerIndex, damageValue); + practice.setDamage (m_team, victimIndex, attackerIndex, damageValue); } void Bot::pushChatMessage (int type, bool isTeamSay) { @@ -5434,10 +5430,10 @@ bool Bot::isOutOfBombTimer () { if (timeLeft > 13.0f) { return false; } - const Vector &bombOrigin = graph.getBombOrigin (); + const auto &bombOrigin = graph.getBombOrigin (); // for terrorist, if timer is lower than 13 seconds, return true - if (timeLeft < 13.0f && m_team == Team::Terrorist && bombOrigin.distanceSq (pev->origin) < cr::square (964.0f)) { + if (timeLeft < 13.0f && m_team == Team::Terrorist && bombOrigin.distanceSq (pev->origin) < cr::sqrf (964.0f)) { return true; } bool hasTeammatesWithDefuserKit = false; @@ -5445,7 +5441,7 @@ bool Bot::isOutOfBombTimer () { // check if our teammates has defusal kit for (const auto &bot : bots) { // search players with defuse kit - if (bot.get () != this && bot->m_team == Team::CT && bot->m_hasDefuser && bombOrigin.distanceSq (bot->pev->origin) < cr::square (512.0f)) { + if (bot.get () != this && bot->m_team == Team::CT && bot->m_hasDefuser && bombOrigin.distanceSq (bot->pev->origin) < cr::sqrf (512.0f)) { hasTeammatesWithDefuserKit = true; break; } @@ -5474,7 +5470,7 @@ void Bot::updateHearing () { // loop through all enemy clients to check for hearable stuff for (int i = 0; i < game.maxClients (); ++i) { - const Client &client = util.getClient (i); + const auto &client = util.getClient (i); if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive) || client.ent == ent () || client.team == m_team || client.noise.last < game.time ()) { continue; @@ -5615,7 +5611,7 @@ bool Bot::isBombDefusing (const Vector &bombOrigin) { return false; } bool defusingInProgress = false; - constexpr auto distanceToBomb = cr::square (165.0f); + constexpr auto distanceToBomb = cr::sqrf (165.0f); for (const auto &client : util.getClients ()) { if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive)) { diff --git a/src/combat.cpp b/src/combat.cpp index d8186c7..988de02 100644 --- a/src/combat.cpp +++ b/src/combat.cpp @@ -24,7 +24,7 @@ int Bot::numFriendsNear (const Vector &origin, float radius) { continue; } - if (client.origin.distanceSq (origin) < cr::square (radius)) { + if (client.origin.distanceSq (origin) < cr::sqrf (radius)) { count++; } } @@ -39,7 +39,7 @@ int Bot::numEnemiesNear (const Vector &origin, float radius) { continue; } - if (client.origin.distanceSq (origin) < cr::square (radius)) { + if (client.origin.distanceSq (origin) < cr::sqrf (radius)) { count++; } } @@ -231,7 +231,7 @@ bool Bot::lookupEnemies () { return false; } edict_t *player, *newEnemy = nullptr; - float nearestDistance = cr::square (m_viewDistance); + float nearestDistance = cr::sqrf (m_viewDistance); // clear suspected flag if (!game.isNullEntity (m_enemy) && (m_states & Sense::SeeingEnemy)) { @@ -300,7 +300,7 @@ bool Bot::lookupEnemies () { // extra skill player can see thru smoke... if beeing attacked if ((player->v.button & (IN_ATTACK | IN_ATTACK2)) && m_viewDistance < m_maxViewDistance && m_difficulty == Difficulty::Expert) { - nearestDistance = cr::square (m_maxViewDistance); + nearestDistance = cr::sqrf (m_maxViewDistance); } // see if bot can see the player... @@ -645,7 +645,7 @@ bool Bot::isPenetrableObstacle (const Vector &dest) { game.testLine (dest, source, TraceIgnore::Monsters, ent (), &tr); if (!cr::fequal (tr.flFraction, 1.0f)) { - if (tr.vecEndPos.distanceSq (dest) > cr::square (800.0f)) { + if (tr.vecEndPos.distanceSq (dest) > cr::sqrf (800.0f)) { return false; } @@ -655,7 +655,7 @@ bool Bot::isPenetrableObstacle (const Vector &dest) { obstacleDistance = tr.vecEndPos.distanceSq (source); } } - const float distance = cr::square (75.0f); + const float distance = cr::sqrf (75.0f); if (obstacleDistance > 0.0f) { while (power > 0) { @@ -679,7 +679,7 @@ bool Bot::isPenetrableObstacle2 (const Vector &dest) { } const Vector &source = getEyesPos (); - const Vector &direction = (dest - source).normalize (); // 1 unit long + const Vector &direction = (dest - source).normalize_apx (); // 1 unit long int thikness = 0; int numHits = 0; @@ -703,7 +703,7 @@ bool Bot::isPenetrableObstacle2 (const Vector &dest) { } if (numHits < 3 && thikness < 98) { - if (dest.distanceSq (point) < cr::square (112.0f)) { + if (dest.distanceSq (point) < cr::sqrf (112.0f)) { return true; } } @@ -724,7 +724,7 @@ bool Bot::isPenetrableObstacle3 (const Vector &dest) { TraceResult tr {}; Vector source = getEyesPos (); - const auto &dir = (dest - source).normalize () * 8.0f; + const auto &dir = (dest - source).normalize_apx () * 8.0f; for (;;) { game.testLine (source, dest, TraceIgnore::Monsters, ent (), &tr); @@ -773,8 +773,8 @@ bool Bot::needToPauseFiring (float distance) { else if (distance < kDoubleSprayDistance) { offset = 2.75f; } - const float xPunch = cr::square (cr::deg2rad (pev->punchangle.x)); - const float yPunch = cr::square (cr::deg2rad (pev->punchangle.y)); + const float xPunch = cr::sqrf (cr::deg2rad (pev->punchangle.x)); + const float yPunch = cr::sqrf (cr::deg2rad (pev->punchangle.y)); const float interval = m_frameInterval; const float tolerance = (100.0f - static_cast (m_difficulty) * 25.0f) / 99.0f; @@ -1277,7 +1277,7 @@ void Bot::attackMovement () { else if ((distance > kDoubleSprayDistance && hasPrimaryWeapon ()) && (m_enemyParts & (Visibility::Head | Visibility::Body)) && getCurrentTaskId () != Task::SeekCover && getCurrentTaskId () != Task::Hunt) { int enemyNearestIndex = graph.getNearest (m_enemy->v.origin); - if (graph.isVisible (m_currentNodeIndex, enemyNearestIndex) && graph.isDuckVisible (m_currentNodeIndex, enemyNearestIndex) && graph.isDuckVisible (enemyNearestIndex, m_currentNodeIndex)) { + if (vistab.visible (m_currentNodeIndex, enemyNearestIndex, VisIndex::Crouch) && vistab.visible (enemyNearestIndex, m_currentNodeIndex, VisIndex::Crouch)) { m_duckTime = game.time () + m_frameInterval * 2.0f; } } @@ -1657,7 +1657,7 @@ bool Bot::isGroupOfEnemies (const Vector &location, int numEnemies, float radius continue; } - if (client.ent->v.origin.distanceSq (location) < cr::square (radius)) { + if (client.ent->v.origin.distanceSq (location) < cr::sqrf (radius)) { // don't target our teammates... if (client.team == m_team) { return false; diff --git a/src/control.cpp b/src/control.cpp index 2296e7e..e70c2da 100644 --- a/src/control.cpp +++ b/src/control.cpp @@ -58,14 +58,21 @@ int BotControl::cmdKickBot () { } int BotControl::cmdKickBots () { - enum args { alias = 1, instant }; + enum args { alias = 1, instant, team }; // check if we're need to remove bots instantly auto kickInstant = strValue (instant) == "instant"; - // kick the bots - bots.kickEveryone (kickInstant); - + // if team is specified, kick from specified tram + if (strValue (alias).endsWith ("_ct") || intValue (team) == 2 || strValue (team) == "ct") { + bots.kickFromTeam (Team::CT, true); + } + else if (strValue (alias).endsWith ("_t") || intValue (team) == 1 || strValue (team) == "t") { + bots.kickFromTeam (Team::Terrorist, true); + } + else { + bots.kickEveryone (kickInstant); + } return BotCommandResult::Handled; } @@ -529,7 +536,7 @@ int BotControl::cmdNodeErase () { // prevent accidents when graph are deleted unintentionally if (strValue (iamsure) == "iamsure") { - graph.eraseFromDisk (); + bstor.unlinkFromDisk (); } else { msg ("Please, append \"iamsure\" as parameter to get graph erased from the disk."); @@ -761,10 +768,16 @@ int BotControl::cmdNodeReleaseEditor () { int BotControl::cmdNodeUpload () { enum args { graph_cmd = 1, cmd }; + // do not allow to upload analyzed graphs + if (graph.isAnalyzed ()) { + msg ("Sorry, unable to upload graph that was generated automatically."); + return BotCommandResult::Handled; + } + // do not allow to upload bad graph if (!graph.checkNodes (false)) { msg ("Sorry, unable to upload graph file that contains errors. Please type \"wp check\" to verify graph consistency."); - return BotCommandResult::BadFormat; + return BotCommandResult::Handled; } msg ("\n"); @@ -776,7 +789,7 @@ int BotControl::cmdNodeUpload () { String uploadUrl = cv_graph_url_upload.str (); // try to upload the file - if (http.uploadFile (uploadUrl, util.buildPath (BotFile::Graph))) { + if (http.uploadFile (uploadUrl, bstor.buildPath (BotFile::Graph))) { msg ("Graph file was successfully validated and uploaded to the YaPB Graph DB (%s).", product.download); msg ("It will be available for download for all YaPB users in a few minutes."); msg ("\n"); @@ -1717,7 +1730,7 @@ bool BotControl::executeCommands () { if (m_args.empty ()) { return false; } - const auto &prefix = m_args[0]; + const auto &prefix = m_args.first (); // no handling if not for us if (prefix != product.cmdPri && prefix != product.cmdSec) { @@ -2055,7 +2068,7 @@ BotControl::BotControl () { m_cmds.emplace ("add/addbot/add_ct/addbot_ct/add_t/addbot_t/addhs/addhs_t/addhs_ct", "add [difficulty] [personality] [team] [model] [name]", "Adding specific bot into the game.", &BotControl::cmdAddBot); m_cmds.emplace ("kick/kickone/kick_ct/kick_t/kickbot_ct/kickbot_t", "kick [team]", "Kicks off the random bot from the game.", &BotControl::cmdKickBot); - m_cmds.emplace ("removebots/kickbots/kickall", "removebots [instant]", "Kicks all the bots from the game.", &BotControl::cmdKickBots); + m_cmds.emplace ("removebots/kickbots/kickall/kickall_ct/kickall_t", "removebots [instant] [team]", "Kicks all the bots from the game.", &BotControl::cmdKickBots); m_cmds.emplace ("kill/killbots/killall/kill_ct/kill_t", "kill [team]", "Kills the specified team / all the bots.", &BotControl::cmdKillBots); m_cmds.emplace ("fill/fillserver", "fill [team] [count] [difficulty] [personality]", "Fill the server (add bots) with specified parameters.", &BotControl::cmdFill); m_cmds.emplace ("vote/votemap", "vote [map_id]", "Forces all the bot to vote to specified map.", &BotControl::cmdVote); diff --git a/src/engine.cpp b/src/engine.cpp index db439db..b8307ee 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -39,10 +39,7 @@ void Game::precache () { m_engineWrap.precacheSound ("weapons/xbow_hit1.wav"); // waypoint add m_engineWrap.precacheSound ("weapons/mine_activate.wav"); // waypoint delete - m_engineWrap.precacheSound ("common/wpn_hudoff.wav"); // path add/delete start m_engineWrap.precacheSound ("common/wpn_hudon.wav"); // path add/delete done - m_engineWrap.precacheSound ("common/wpn_moveselect.wav"); // path add/delete cancel - m_engineWrap.precacheSound ("common/wpn_denyselect.wav"); // path add/delete error m_mapFlags = 0; // reset map type as worldspawn is the first entity spawned @@ -68,27 +65,27 @@ void Game::levelInitialize (edict_t *entities, int max) { // update worldmodel illum.resetWorldModel (); - // do level initialization stuff here... - graph.loadGraphData (); - // execute main config conf.loadMainConfig (); // load map-specific config conf.loadMapSpecificConfig (); + // do level initialization stuff here... + graph.loadGraphData (); + // initialize quota management bots.initQuota (); - // rebuild vistable if needed - graph.rebuildVisibility (); - // install the sendto hook to fake queries util.installSendTo (); // 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; @@ -413,11 +410,11 @@ void Game::playSound (edict_t *ent, const char *sound) { } void Game::setPlayerStartDrawModels () { - HashMap models; - - models["info_player_start"] = "models/player/urban/urban.mdl"; - models["info_player_deathmatch"] = "models/player/terror/terror.mdl"; - models["info_vip_start"] = "models/player/vip/vip.mdl"; + static HashMap models { + {"info_player_start", "models/player/urban/urban.mdl"}, + {"info_player_deathmatch", "models/player/terror/terror.mdl"}, + {"info_vip_start", "models/player/vip/vip.mdl"} + }; models.foreach ([&] (const String &key, const String &val) { game.searchEntities ("classname", key, [&] (edict_t *ent) { @@ -620,6 +617,7 @@ void Game::addNewCvar (const char *name, const char *value, const char *info, bo reg.reg.name = const_cast (name); reg.reg.string = const_cast (value); + reg.name = name; reg.missing = missingAction; reg.init = value; reg.info = info; @@ -654,6 +652,20 @@ void Game::addNewCvar (const char *name, const char *value, const char *info, bo m_cvars.push (cr::move (reg)); } +void ConVar::revert () { + if (!ptr) { + return; + } + const auto &cvars = game.getCvars (); + + for (const auto &var : cvars) { + if (var.name == ptr->name) { + set (var.initial); + break; + } + } +} + void Game::checkCvarsBounds () { for (const auto &var : m_cvars) { @@ -669,14 +681,14 @@ void Game::checkCvarsBounds () { continue; } auto value = var.self->float_ (); - auto str = var.self->str (); + auto str = String (var.self->str ()); // check the bounds and set default if out of bounds - if (value > var.max || value < var.min || (!strings.isEmpty (str) && isalpha (*str))) { + if (value > var.max || value < var.min || (!str.empty () && isalpha (str[0]))) { var.self->set (var.initial); // notify about that - ctrl.msg ("Bogus value for cvar '%s', min is '%.1f' and max is '%.1f', and we're got '%s', value reverted to default '%.1f'.", var.reg.name, var.min, var.max, str, var.initial); + ctrl.msg ("Bogus value for cvar '%s', min is '%.1f' and max is '%.1f', and we're got '%s', value reverted to default '%.1f'.", var.name, var.min, var.max, str, var.initial); } } @@ -741,19 +753,7 @@ bool Game::loadCSBinary () { if (!modname) { return false; } - StringArray libs; - - if (plat.win) { - libs.push ("mp.dll"); - libs.push ("cs.dll"); - } - else if (plat.nix) { - libs.push ("cs.so"); - libs.push ("cs_i386.so"); - } - else if (plat.osx) { - libs.push ("cs.dylib"); - } + StringArray libs { "mp", "cs", "cs_i386" }; auto libCheck = [&] (StringRef mod, StringRef dll) { // try to load gamedll @@ -771,7 +771,7 @@ bool Game::loadCSBinary () { // search the libraries inside game dlls directory for (const auto &lib : libs) { - auto path = strings.format ("%s/dlls/%s", modname, lib); + auto path = strings.format ("%s/dlls/%s.%s", modname, lib, DLL_SUFFIX); // if we can't read file, skip it if (!File::exists (path)) { @@ -834,7 +834,7 @@ bool Game::loadCSBinary () { bool Game::postload () { // register logger - logger.initialize (util.buildPath (BotFile::LogFile), [] (const char *msg) { + logger.initialize (bstor.buildPath (BotFile::LogFile), [] (const char *msg) { game.print (msg); }); diff --git a/src/graph.cpp b/src/graph.cpp index 886ead8..c16ffda 100644 --- a/src/graph.cpp +++ b/src/graph.cpp @@ -16,7 +16,6 @@ ConVar cv_graph_draw_distance ("yb_graph_draw_distance", "400", "Maximum distanc void BotGraph::reset () { // this function initialize the graph structures.. - m_loadAttempts = 0; m_editFlags = 0; m_autoSaveCount = 0; @@ -32,6 +31,8 @@ void BotGraph::reset () { m_graphAuthor.clear (); m_graphModified.clear (); + + m_paths.clear (); } int BotGraph::clearConnections (int index) { @@ -96,7 +97,7 @@ int BotGraph::clearConnections (int index) { } if (top.number == kInvalidNodeIndex) { - ctrl.msg ("Cannot find path to the closest connected node to node number %d.", index); + msg ("Cannot find path to the closest connected node to node number %d.", index); return numFixedLinks; } bool sorting = false; @@ -171,14 +172,14 @@ int BotGraph::clearConnections (int index) { if ((cur.distance + prev2.distance) * 1.1f / 2.0f < prev.distance) { if (path.links[prev.number].index == prev.index) { - ctrl.msg ("Removing a useless (P.0.1) connection from index = %d to %d.", index, prev.index); + msg ("Removing a useless (P.0.1) connection from index = %d to %d.", index, prev.index); // unassign this path clearPath (index, prev.number); for (int j = 0; j < kMaxNodeLinks; ++j) { if (m_paths[prev.index].links[j].index == index && !(m_paths[prev.index].links[j].flags & PathFlag::Jump)) { - ctrl.msg ("Removing a useless (P.0.2) connection from index = %d to %d.", prev.index, index); + msg ("Removing a useless (P.0.2) connection from index = %d to %d.", prev.index, index); // unassign this path clearPath (prev.index, j); @@ -195,7 +196,7 @@ int BotGraph::clearConnections (int index) { return true; } else { - ctrl.msg ("Failed to remove a useless (P.0) connection from index = %d to %d.", index, prev.index); + msg ("Failed to remove a useless (P.0) connection from index = %d to %d.", index, prev.index); return false; } } @@ -213,14 +214,14 @@ int BotGraph::clearConnections (int index) { if ((sorted[1].angles - top.angles < 80.0f || 360.0f - (sorted[1].angles - top.angles) < 80.0f) && (!(m_paths[sorted[0].index].flags & NodeFlag::Ladder) || !(path.flags & NodeFlag::Ladder)) && !(path.links[sorted[0].number].flags & PathFlag::Jump)) { if ((sorted[1].distance + top.distance) * 1.1f / 2.0f < sorted[0].distance) { if (path.links[sorted[0].number].index == sorted[0].index) { - ctrl.msg ("Removing a useless (P.1.1) connection from index = %d to %d.", index, sorted[0].index); + msg ("Removing a useless (P.1.1) connection from index = %d to %d.", index, sorted[0].index); // unassign this path clearPath (index, sorted[0].number); for (int j = 0; j < kMaxNodeLinks; ++j) { if (m_paths[sorted[0].index].links[j].index == index && !(m_paths[sorted[0].index].links[j].flags & PathFlag::Jump)) { - ctrl.msg ("Removing a useless (P.1.2) connection from index = %d to %d.", sorted[0].index, index); + msg ("Removing a useless (P.1.2) connection from index = %d to %d.", sorted[0].index, index); // unassign this path clearPath (sorted[0].index, j); @@ -234,7 +235,7 @@ int BotGraph::clearConnections (int index) { sorted[kMaxNodeLinks - 1].index = kInvalidNodeIndex; } else { - ctrl.msg ("Failed to remove a useless (P.1) connection from index = %d to %d.", sorted[0].index, index); + msg ("Failed to remove a useless (P.1) connection from index = %d to %d.", sorted[0].index, index); } } } @@ -261,14 +262,14 @@ int BotGraph::clearConnections (int index) { } if (path.links[cur.number].index == cur.index) { - ctrl.msg ("Removing a useless (P.2.1) connection from index = %d to %d.", index, cur.index); + msg ("Removing a useless (P.2.1) connection from index = %d to %d.", index, cur.index); // unassign this path clearPath (index, cur.number); for (int j = 0; j < kMaxNodeLinks; ++j) { if (m_paths[cur.index].links[j].index == index && !(m_paths[cur.index].links[j].flags & PathFlag::Jump)) { - ctrl.msg ("Removing a useless (P.2.2) connection from index = %d to %d.", cur.index, index); + msg ("Removing a useless (P.2.2) connection from index = %d to %d.", cur.index, index); // unassign this path clearPath (cur.index, j); @@ -283,7 +284,7 @@ int BotGraph::clearConnections (int index) { return true; } else { - ctrl.msg ("Failed to remove a useless (P.2) connection from index = %d to %d.", index, cur.index); + msg ("Failed to remove a useless (P.2) connection from index = %d to %d.", index, cur.index); } } else if (cur.distance < prev.distance * 1.1f) { @@ -293,14 +294,14 @@ int BotGraph::clearConnections (int index) { } if (path.links[prev.number].index == prev.index) { - ctrl.msg ("Removing a useless (P.2.3) connection from index = %d to %d.", index, prev.index); + msg ("Removing a useless (P.2.3) connection from index = %d to %d.", index, prev.index); // unassign this path clearPath (index, prev.number); for (int j = 0; j < kMaxNodeLinks; ++j) { if (m_paths[prev.index].links[j].index == index && !(m_paths[prev.index].links[j].flags & PathFlag::Jump)) { - ctrl.msg ("Removing a useless (P.2.4) connection from index = %d to %d.", prev.index, index); + msg ("Removing a useless (P.2.4) connection from index = %d to %d.", prev.index, index); // unassign this path clearPath (prev.index, j); @@ -317,7 +318,7 @@ int BotGraph::clearConnections (int index) { return true; } else { - ctrl.msg ("Failed to remove a useless (P.2) connection from index = %d to %d.", index, prev.index); + msg ("Failed to remove a useless (P.2) connection from index = %d to %d.", index, prev.index); } } } @@ -336,14 +337,14 @@ int BotGraph::clearConnections (int index) { if ((top.angles - sorted[0].angles < 40.0f || (360.0f - top.angles - sorted[0].angles) < 40.0f) && (!(m_paths[sorted[0].index].flags & NodeFlag::Ladder) || !(path.flags & NodeFlag::Ladder)) && !(path.links[sorted[0].number].flags & PathFlag::Jump)) { if (top.distance * 1.1f < sorted[0].distance) { if (path.links[sorted[0].number].index == sorted[0].index) { - ctrl.msg ("Removing a useless (P.3.1) connection from index = %d to %d.", index, sorted[0].index); + msg ("Removing a useless (P.3.1) connection from index = %d to %d.", index, sorted[0].index); // unassign this path clearPath (index, sorted[0].number); for (int j = 0; j < kMaxNodeLinks; ++j) { if (m_paths[sorted[0].index].links[j].index == index && !(m_paths[sorted[0].index].links[j].flags & PathFlag::Jump)) { - ctrl.msg ("Removing a useless (P.3.2) connection from index = %d to %d.", sorted[0].index, index); + msg ("Removing a useless (P.3.2) connection from index = %d to %d.", sorted[0].index, index); // unassign this path clearPath (sorted[0].index, j); @@ -357,19 +358,19 @@ int BotGraph::clearConnections (int index) { sorted[kMaxNodeLinks - 1].index = kInvalidNodeIndex; } else { - ctrl.msg ("Failed to remove a useless (P.3) connection from index = %d to %d.", sorted[0].index, index); + msg ("Failed to remove a useless (P.3) connection from index = %d to %d.", sorted[0].index, index); } } else if (sorted[0].distance * 1.1f < top.distance && !(path.links[top.number].flags & PathFlag::Jump)) { if (path.links[top.number].index == top.index) { - ctrl.msg ("Removing a useless (P.3.3) connection from index = %d to %d.", index, sorted[0].index); + msg ("Removing a useless (P.3.3) connection from index = %d to %d.", index, sorted[0].index); // unassign this path clearPath (index, top.number); for (int j = 0; j < kMaxNodeLinks; ++j) { if (m_paths[top.index].links[j].index == index && !(m_paths[top.index].links[j].flags & PathFlag::Jump)) { - ctrl.msg ("Removing a useless (P.3.4) connection from index = %d to %d.", sorted[0].index, index); + msg ("Removing a useless (P.3.4) connection from index = %d to %d.", sorted[0].index, index); // unassign this path clearPath (top.index, j); @@ -378,7 +379,7 @@ int BotGraph::clearConnections (int index) { sorted[0].index = kInvalidNodeIndex; } else { - ctrl.msg ("Failed to remove a useless (P.3) connection from index = %d to %d.", sorted[0].index, index); + msg ("Failed to remove a useless (P.3) connection from index = %d to %d.", sorted[0].index, index); } } } @@ -398,7 +399,7 @@ int BotGraph::getBspSize () { } void BotGraph::addPath (int addIndex, int pathIndex, float distance) { - if (!exists (addIndex) || !exists (pathIndex)) { + if (!exists (addIndex) || !exists (pathIndex) || pathIndex == addIndex) { return; } auto &path = m_paths[addIndex]; @@ -406,7 +407,7 @@ void BotGraph::addPath (int addIndex, int pathIndex, float distance) { // don't allow paths get connected twice for (const auto &link : path.links) { if (link.index == pathIndex) { - ctrl.msg ("Denied path creation from %d to %d (path already exists).", addIndex, pathIndex); + msg ("Denied path creation from %d to %d (path already exists).", addIndex, pathIndex); return; } } @@ -417,7 +418,7 @@ void BotGraph::addPath (int addIndex, int pathIndex, float distance) { link.index = static_cast (pathIndex); link.distance = cr::abs (static_cast (distance)); - ctrl.msg ("Path added from %d to %d.", addIndex, pathIndex); + msg ("Path added from %d to %d.", addIndex, pathIndex); return; } } @@ -434,7 +435,7 @@ void BotGraph::addPath (int addIndex, int pathIndex, float distance) { } if (slot != kInvalidNodeIndex) { - ctrl.msg ("Path added from %d to %d.", addIndex, pathIndex); + msg ("Path added from %d to %d.", addIndex, pathIndex); path.links[slot].index = static_cast (pathIndex); path.links[slot].distance = cr::abs (static_cast (distance)); @@ -445,7 +446,7 @@ int BotGraph::getFarest (const Vector &origin, float maxDistance) { // find the farest node to that origin, and return the index to this node int index = kInvalidNodeIndex; - maxDistance = cr::square (maxDistance); + maxDistance = cr::sqrf (maxDistance); for (const auto &path : m_paths) { const float distance = path.origin.distanceSq (origin); @@ -458,12 +459,28 @@ int BotGraph::getFarest (const Vector &origin, float maxDistance) { return index; } +int BotGraph::getForAnalyzer (const Vector &origin, float maxDistance) { + // find the farest node to that origin, and return the index to this node + + int index = kInvalidNodeIndex; + + for (const auto &path : m_paths) { + const float distance = path.origin.distance (origin); + + if (distance < maxDistance) { + index = path.number; + maxDistance = distance; + } + } + return index; +} + int BotGraph::getNearestNoBuckets (const Vector &origin, float minDistance, int flags) { // find the nearest node to that origin and return the index // fallback and go thru wall the nodes... int index = kInvalidNodeIndex; - minDistance = cr::square (minDistance); + minDistance = cr::sqrf (minDistance); for (const auto &path : m_paths) { if (flags != -1 && !(path.flags & flags)) { @@ -499,7 +516,7 @@ int BotGraph::getNearest (const Vector &origin, float minDistance, int flags) { } int index = kInvalidNodeIndex; - auto minDistanceSq = cr::square (minDistance); + auto minDistanceSq = cr::sqrf (minDistance); for (const auto &at : bucket) { if (flags != -1 && !(m_paths[at].flags & flags)) { @@ -523,12 +540,12 @@ int BotGraph::getNearest (const Vector &origin, float minDistance, int flags) { IntArray BotGraph::getNarestInRadius (float radius, const Vector &origin, int maxCount) { // returns all nodes within radius from position - radius = cr::square (radius); + radius = cr::sqrf (radius); IntArray result; const auto &bucket = getNodesInBucket (origin); - if (bucket.length () < kMaxNodeLinks || radius > cr::square (256.0f)) { + if (bucket.length () < kMaxNodeLinks || radius > cr::sqrf (256.0f)) { for (const auto &path : m_paths) { if (maxCount != -1 && static_cast (result.length ()) > maxCount) { break; @@ -554,16 +571,21 @@ IntArray BotGraph::getNarestInRadius (float radius, const Vector &origin, int ma } void BotGraph::add (int type, const Vector &pos) { - // @todo: remove type magic numbers - - if (game.isNullEntity (m_editor)) { + if (game.isNullEntity (m_editor) && !analyzer.isAnalyzing ()) { return; } int index = kInvalidNodeIndex; Path *path = nullptr; bool addNewNode = true; - Vector newOrigin = pos.empty () ? m_editor->v.origin : pos; + Vector newOrigin = pos; + + if (newOrigin.empty ()) { + if (game.isNullEntity (m_editor)) { + return; + } + newOrigin = m_editor->v.origin; + } if (bots.hasBotsOnline ()) { bots.kickEveryone (true); @@ -579,9 +601,7 @@ void BotGraph::add (int type, const Vector &pos) { if (path->flags & NodeFlag::Camp) { path->start = m_editor->v.v_angle.get2d (); - - // play "done" sound... - game.playSound (m_editor, "common/wpn_hudon.wav"); + emitNotify (NotifySound::Done); // play "done" sound... return; } } @@ -594,13 +614,11 @@ void BotGraph::add (int type, const Vector &pos) { path = &m_paths[index]; if (!(path->flags & NodeFlag::Camp)) { - ctrl.msg ("This is not camping node."); + msg ("This is not camping node."); return; } path->end = m_editor->v.v_angle.get2d (); - - // play "done" sound... - game.playSound (m_editor, "common/wpn_hudon.wav"); + emitNotify (NotifySound::Done); // play "done" sound... } return; @@ -647,12 +665,21 @@ void BotGraph::add (int type, const Vector &pos) { } if (addNewNode) { - auto nearest = getEditorNearest (); + if (analyzer.isAnalyzing ()) { + for (const auto &cp : m_paths) { + if (newOrigin.distanceSq (cp.origin) < cr::sqrf (24.0f)) { + return; + } + } + } + else { + auto nearest = getEditorNearest (); - // do not allow to place waypoints "inside" waypoints, make at leat 10 units range - if (exists (nearest) && newOrigin.distanceSq (m_paths[nearest].origin) < cr::square (10.0f)) { - ctrl.msg ("Can't add node. It's way to near to %d node. Please move some units anywhere.", nearest); - return; + // do not allow to place waypoints "inside" waypoints, make at leat 10 units range + if (exists (nearest) && newOrigin.distanceSq (m_paths[nearest].origin) < cr::sqrf (10.0f)) { + msg ("Can't add node. It's way to near to %d node. Please move some units anywhere.", nearest); + return; + } } // need to remove limit? @@ -683,18 +710,20 @@ void BotGraph::add (int type, const Vector &pos) { } // autosave nodes here and there - if (cv_graph_auto_save_count.bool_ () && ++m_autoSaveCount >= cv_graph_auto_save_count.int_ ()) { + if (!analyzer.isAnalyzing () && cv_graph_auto_save_count.bool_ () && ++m_autoSaveCount >= cv_graph_auto_save_count.int_ ()) { if (saveGraphData ()) { - ctrl.msg ("Nodes has been autosaved..."); + msg ("Nodes has been autosaved..."); } else { - ctrl.msg ("Can't autosave graph data..."); + msg ("Can't autosave graph data..."); } m_autoSaveCount = 0; } // store the last used node for the auto node code... - m_lastNode = m_editor->v.origin; + if (!analyzer.isAnalyzing ()) { + m_lastNode = m_editor->v.origin; + } } if (type == NodeAddFlag::JumpStart) { @@ -720,11 +749,11 @@ void BotGraph::add (int type, const Vector &pos) { return; } - if (m_editor->v.flags & FL_DUCKING) { + if (analyzer.isCrouch () || (!analyzer.isAnalyzing () && (m_editor->v.flags & FL_DUCKING))) { path->flags |= NodeFlag::Crouch; // set a crouch node } - if (m_editor->v.movetype == MOVETYPE_FLY) { + if (!analyzer.isAnalyzing () && m_editor->v.movetype == MOVETYPE_FLY) { path->flags |= NodeFlag::Ladder; } else if (m_isOnLadder) { @@ -754,7 +783,10 @@ void BotGraph::add (int type, const Vector &pos) { path->flags |= NodeFlag::Crossing; path->flags |= NodeFlag::Camp; - path->start = m_editor->v.v_angle; + if (!analyzer.isAnalyzing ()) { + path->start = m_editor->v.v_angle; + path->end = m_editor->v.v_angle; + } break; case NodeAddFlag::Goal: @@ -762,7 +794,7 @@ void BotGraph::add (int type, const Vector &pos) { break; } - // Ladder nodes need careful connections + // ladder nodes need careful connections if (path->flags & NodeFlag::Ladder) { float minDistance = kInfiniteDistance; int destIndex = kInvalidNodeIndex; @@ -781,34 +813,44 @@ void BotGraph::add (int type, const Vector &pos) { game.testLine (newOrigin, calc.origin, TraceIgnore::Monsters, m_editor, &tr); if (cr::fequal (tr.flFraction, 1.0f) && cr::abs (newOrigin.x - calc.origin.x) < 64.0f && cr::abs (newOrigin.y - calc.origin.y) < 64.0f && cr::abs (newOrigin.z - calc.origin.z) < m_autoPathDistance) { - float distance = newOrigin.distance (calc.origin); + float distance = newOrigin.distance2d (calc.origin); addPath (index, calc.number, distance); addPath (calc.number, index, distance); } } else { - // check if the node is reachable from the new one - if (isNodeReacheable (newOrigin, calc.origin) || isNodeReacheable (calc.origin, newOrigin)) { - float distance = newOrigin.distance (calc.origin); + float distance = newOrigin.distance2d (calc.origin); - if (distance < minDistance) { - destIndex = calc.number; - minDistance = distance; - } + if (distance < minDistance) { + destIndex = calc.number; + minDistance = distance; + } + + // check if the node is reachable from the new one + if (isNodeReacheable (newOrigin, calc.origin)) { + addPath (index, calc.number, distance); } } } if (exists (destIndex)) { - // check if the node is reachable from the new one (one-way) - if (isNodeReacheable (newOrigin, m_paths[destIndex].origin)) { - addPath (index, destIndex, newOrigin.distance (m_paths[destIndex].origin)); - } + const float distance = newOrigin.distance2d (m_paths[destIndex].origin); - // check if the new one is reachable from the node (other way) - if (isNodeReacheable (m_paths[destIndex].origin, newOrigin)) { - addPath (destIndex, index, newOrigin.distance (m_paths[destIndex].origin)); + if (analyzer.isAnalyzing ()) { + addPath (index, destIndex, distance); + addPath (destIndex, index, distance); + } + else { + // check if the node is reachable from the new one (one-way) + if (isNodeReacheable (newOrigin, m_paths[destIndex].origin)) { + addPath (index, destIndex, newOrigin.distance (m_paths[destIndex].origin)); + } + + // check if the new one is reachable from the node (other way) + if (isNodeReacheable (m_paths[destIndex].origin, newOrigin)) { + addPath (destIndex, index, newOrigin.distance (m_paths[destIndex].origin)); + } } } } @@ -818,21 +860,29 @@ void BotGraph::add (int type, const Vector &pos) { if (calc.number == index) { continue; // skip the node that was just added } + const float distance = calc.origin.distance2d (newOrigin); // check if the node is reachable from the new one (one-way) if (isNodeReacheable (newOrigin, calc.origin)) { - addPath (index, calc.number, calc.origin.distance (newOrigin)); + addPath (index, calc.number, distance); } // check if the new one is reachable from the node (other way) if (isNodeReacheable (calc.origin, newOrigin)) { - addPath (calc.number, index, calc.origin.distance (newOrigin)); + addPath (calc.number, index, distance); } } - clearConnections (index); + + if (!analyzer.isAnalyzing ()) { + clearConnections (index); + } } - game.playSound (m_editor, "weapons/xbow_hit1.wav"); + emitNotify (NotifySound::Added); calculatePathRadius (index); // calculate the wayzone of this node + + if (analyzer.isAnalyzing ()) { + analyzer.markOptimized (index); + } } void BotGraph::erase (int target) { @@ -879,8 +929,7 @@ void BotGraph::erase (int target) { } } m_paths.remove (path); - - game.playSound (m_editor, "weapons/mine_activate.wav"); + emitNotify (NotifySound::Change); } void BotGraph::toggleFlags (int toggleFlag) { @@ -894,14 +943,12 @@ void BotGraph::toggleFlags (int toggleFlag) { } else { if (toggleFlag == NodeFlag::Sniper && !(m_paths[index].flags & NodeFlag::Camp)) { - ctrl.msg ("Cannot assign sniper flag to node %d. This is not camp node.", index); + msg ("Cannot assign sniper flag to node %d. This is not camp node.", index); return; } m_paths[index].flags |= toggleFlag; } - - // play "done" sound... - game.playSound (m_editor, "common/wpn_hudon.wav"); + emitNotify (NotifySound::Done); // play "done" sound... } } @@ -912,10 +959,9 @@ void BotGraph::setRadius (int index, float radius) { if (node != kInvalidNodeIndex) { m_paths[node].radius = radius; + emitNotify (NotifySound::Done); // play "done" sound... - // play "done" sound... - game.playSound (m_editor, "common/wpn_hudon.wav"); - ctrl.msg ("Node %d has been set to radius %.2f.", node, radius); + msg ("Node %d has been set to radius %.2f.", node, radius); } } @@ -954,7 +1000,7 @@ int BotGraph::getFacingIndex () { auto angles = (to.angles () - m_editor->v.v_angle).clampAngles (); // skip the waypoints that are too far away from us, and we're not looking at them directly - if (to.lengthSq () > cr::square (500.0f) || cr::abs (angles.y) > result.second) { + if (to.lengthSq () > cr::sqrf (500.0f) || cr::abs (angles.y) > result.second) { continue; } @@ -985,7 +1031,7 @@ void BotGraph::pathCreate (char dir) { int nodeFrom = getEditorNearest (); if (nodeFrom == kInvalidNodeIndex) { - ctrl.msg ("Unable to find nearest node in 50 units."); + msg ("Unable to find nearest node in 50 units."); return; } int nodeTo = m_facingAtIndex; @@ -995,13 +1041,13 @@ void BotGraph::pathCreate (char dir) { nodeTo = m_cacheNodeIndex; } else { - ctrl.msg ("Unable to find destination node."); + msg ("Unable to find destination node."); return; } } if (nodeTo == nodeFrom) { - ctrl.msg ("Unable to connect node with itself."); + msg ("Unable to connect node with itself."); return; } @@ -1017,8 +1063,7 @@ void BotGraph::pathCreate (char dir) { addPath (nodeFrom, nodeTo, distance); addPath (nodeTo, nodeFrom, distance); } - - game.playSound (m_editor, "common/wpn_hudon.wav"); + emitNotify (NotifySound::Done); // play "done" sound... m_hasChanged = true; } @@ -1028,7 +1073,7 @@ void BotGraph::erasePath () { int nodeFrom = getEditorNearest (); if (nodeFrom == kInvalidNodeIndex) { - ctrl.msg ("Unable to find nearest node in 50 units."); + msg ("Unable to find nearest node in 50 units."); return; } int nodeTo = m_facingAtIndex; @@ -1038,7 +1083,7 @@ void BotGraph::erasePath () { nodeTo = m_cacheNodeIndex; } else { - ctrl.msg ("Unable to find destination node."); + msg ("Unable to find destination node."); return; } } @@ -1054,7 +1099,7 @@ void BotGraph::erasePath () { for (auto &link : m_paths[nodeFrom].links) { if (link.index == nodeTo) { destroy (link); - game.playSound (m_editor, "weapons/mine_activate.wav"); + emitNotify (NotifySound::Change); return; } @@ -1066,12 +1111,12 @@ void BotGraph::erasePath () { for (auto &link : m_paths[nodeFrom].links) { if (link.index == nodeTo) { destroy (link); - game.playSound (m_editor, "weapons/mine_activate.wav"); + emitNotify (NotifySound::Change); return; } } - ctrl.msg ("There is already no path on this node."); + msg ("There is already no path on this node."); } void BotGraph::cachePoint (int index) { @@ -1079,22 +1124,22 @@ void BotGraph::cachePoint (int index) { if (node == kInvalidNodeIndex) { m_cacheNodeIndex = kInvalidNodeIndex; - ctrl.msg ("Cached node cleared (nearby point not found in 50 units range)."); + msg ("Cached node cleared (nearby point not found in 50 units range)."); return; } m_cacheNodeIndex = node; - ctrl.msg ("Node %d has been put into memory.", m_cacheNodeIndex); + msg ("Node %d has been put into memory.", m_cacheNodeIndex); } void BotGraph::setAutoPathDistance (const float distance) { m_autoPathDistance = distance; if (cr::fzero (distance)) { - ctrl.msg ("Autopathing is now disabled."); + msg ("Autopathing is now disabled."); } else { - ctrl.msg ("Autopath distance is set to %.2f.", distance); + msg ("Autopath distance is set to %.2f.", distance); } } @@ -1137,27 +1182,40 @@ void BotGraph::showStats () { } } - ctrl.msg ("Nodes: %d - T Points: %d", m_paths.length (), terrPoints); - ctrl.msg ("CT Points: %d - Goal Points: %d", ctPoints, goalPoints); - ctrl.msg ("Rescue Points: %d - Camp Points: %d", rescuePoints, campPoints); - ctrl.msg ("Block Hostage Points: %d - Sniper Points: %d", noHostagePoints, sniperPoints); + msg ("Nodes: %d - T Points: %d", m_paths.length (), terrPoints); + msg ("CT Points: %d - Goal Points: %d", ctPoints, goalPoints); + msg ("Rescue Points: %d - Camp Points: %d", rescuePoints, campPoints); + msg ("Block Hostage Points: %d - Sniper Points: %d", noHostagePoints, sniperPoints); } void BotGraph::showFileInfo () { - ctrl.msg ("header:"); - ctrl.msg (" magic: %d", m_graphHeader.magic); - ctrl.msg (" version: %d", m_graphHeader.version); - ctrl.msg (" node_count: %d", m_graphHeader.length); - ctrl.msg (" compressed_size: %dkB", m_graphHeader.compressed / 1024); - ctrl.msg (" uncompressed_size: %dkB", m_graphHeader.uncompressed / 1024); - ctrl.msg (" options: %d", m_graphHeader.options); // display as string ? + msg ("header:"); + msg (" magic: %d", m_graphHeader.magic); + msg (" version: %d", m_graphHeader.version); + msg (" node_count: %d", m_graphHeader.length); + 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 ? - ctrl.msg (""); + msg (""); - ctrl.msg ("extensions:"); - ctrl.msg (" author: %s", m_extenHeader.author); - ctrl.msg (" modified_by: %s", m_extenHeader.modified); - ctrl.msg (" bsp_size: %d", m_extenHeader.mapSize); + msg ("extensions:"); + msg (" author: %s", m_extenHeader.author); + msg (" modified_by: %s", m_extenHeader.modified); + msg (" bsp_size: %d", m_extenHeader.mapSize); +} + +void BotGraph::emitNotify (int32_t sound) { + static HashMap notifySounds = { + { NotifySound::Added, "weapons/xbow_hit1.wav" }, + { NotifySound::Change, "weapons/mine_activate.wav" }, + { NotifySound::Done, "common/wpn_hudon.wav" } + }; + + // notify editor + if (util.isPlayer (m_editor) && !m_silenceMessages) { + game.playSound (m_editor, notifySounds[sound].chars ()); + } } void BotGraph::calculatePathRadius (int index) { @@ -1257,131 +1315,6 @@ void BotGraph::calculatePathRadius (int index) { } } -void BotGraph::loadPractice () { - if (m_paths.empty ()) { - return; - } - - bool dataLoaded = loadStorage ("Practice", StorageOption::Practice, StorageVersion::Practice, m_practice, nullptr, nullptr); - int count = length (); - - // set's the highest damage if loaded ok - if (dataLoaded) { - auto practice = m_practice.data (); - - for (int team = Team::Terrorist; team < kGameTeamNum; ++team) { - for (int i = 0; i < count; ++i) { - for (int j = 0; j < count; ++j) { - if (i == j) { - if ((practice + (i * count) + j)->damage[team] > m_highestDamage[team]) { - m_highestDamage[team] = (practice + (i * count) + j)->damage[team]; - } - } - } - } - } - return; - } - auto practice = m_practice.data (); - - // initialize table by hand to correct values, and NOT zero it out - for (int team = Team::Terrorist; team < kGameTeamNum; ++team) { - for (int i = 0; i < count; ++i) { - for (int j = 0; j < count; ++j) { - (practice + (i * count) + j)->index[team] = kInvalidNodeIndex; - (practice + (i * count) + j)->damage[team] = 0; - (practice + (i * count) + j)->value[team] = 0; - } - } - } -} - -void BotGraph::savePractice () { - if (m_paths.empty () || m_hasChanged) { - return; - } - saveStorage ("Practice", StorageOption::Practice, StorageVersion::Practice, m_practice, nullptr); -} - -void BotGraph::loadVisibility () { - m_needsVisRebuild = true; - - if (m_paths.empty ()) { - return; - } - bool dataLoaded = loadStorage ("Visibility", StorageOption::Vistable, StorageVersion::Vistable, m_vistable, nullptr, nullptr); - - // if loaded, do not recalculate visibility - if (dataLoaded) { - m_needsVisRebuild = false; - } -} - -void BotGraph::saveVisibility () { - if (m_paths.empty () || m_hasChanged || m_needsVisRebuild) { - return; - } - saveStorage ("Visibility", StorageOption::Vistable, StorageVersion::Vistable, m_vistable, nullptr); -} - -bool BotGraph::loadPathMatrix () { - if (m_paths.empty ()) { - return false; - } - bool dataLoaded = loadStorage ("Pathmatrix", StorageOption::Matrix, StorageVersion::Matrix, m_matrix, nullptr, nullptr); - - // do not rebuild if loaded - if (dataLoaded) { - return true; - } - auto count = length (); - auto matrix = m_matrix.data (); - - for (int i = 0; i < count; ++i) { - for (int j = 0; j < count; ++j) { - (matrix + i * count + j)->dist = SHRT_MAX; - (matrix + i * count + j)->index = kInvalidNodeIndex; - } - } - - for (int i = 0; i < count; ++i) { - for (const auto &link : m_paths[i].links) { - if (!exists (link.index)) { - continue; - } - (matrix + (i * count) + link.index)->dist = static_cast (link.distance); - (matrix + (i * count) + link.index)->index = static_cast (link.index); - } - } - - for (int i = 0; i < count; ++i) { - (matrix + (i * count) + i)->dist = 0; - } - - for (int k = 0; k < count; ++k) { - for (int i = 0; i < count; ++i) { - for (int j = 0; j < count; ++j) { - int distance = (matrix + (i * count) + k)->dist + (matrix + (k * count) + j)->dist; - - if (distance < (matrix + (i * count) + j)->dist) { - (matrix + (i * count) + j)->dist = static_cast (distance); - (matrix + (i * count) + j)->index = (matrix + (i * count) + k)->index; - } - } - } - } - savePathMatrix (); // save path matrix to file for faster access - - return true; -} - -void BotGraph::savePathMatrix () { - if (m_paths.empty ()) { - return; - } - saveStorage ("Pathmatrix", StorageOption::Matrix, StorageVersion::Matrix, m_matrix, nullptr); -} - void BotGraph::initLightLevels () { // this function get's the light level for each waypoin on the map @@ -1396,7 +1329,6 @@ void BotGraph::initLightLevels () { } // disable lightstyle animations on finish (will be auto-enabled on mapchange) illum.enableAnimation (false); - } void BotGraph::initNarrowPlaces () { @@ -1493,7 +1425,7 @@ void BotGraph::initNarrowPlaces () { m_narrowChecked = true; } -void BotGraph::initNodesTypes () { +void BotGraph::populateNodes () { m_terrorPoints.clear (); m_ctPoints.clear (); m_goalPoints.clear (); @@ -1525,10 +1457,10 @@ void BotGraph::initNodesTypes () { } bool BotGraph::convertOldFormat () { - MemFile fp (util.buildPath (BotFile::PodbotPWF, true)); + MemFile fp (bstor.buildPath (BotFile::PodbotPWF, true)); if (!fp) { - if (!fp.open (util.buildPath (BotFile::EbotEWP, true))) { + if (!fp.open (bstor.buildPath (BotFile::EbotEWP, true))) { return false; } } @@ -1557,7 +1489,6 @@ bool BotGraph::convertOldFormat () { return false; } reset (); - m_paths.clear (); for (int i = 0; i < header.pointNumber; ++i) { Path path {}; @@ -1579,7 +1510,7 @@ bool BotGraph::convertOldFormat () { // save new format in case loaded older one if (!m_paths.empty ()) { - ctrl.msg ("Converting old PWF to new format Graph."); + msg ("Converting old PWF to new format Graph."); m_graphAuthor = header.author; @@ -1604,257 +1535,6 @@ bool BotGraph::convertOldFormat () { return false; } -template bool BotGraph::saveStorage (StringRef name, StorageOption options, StorageVersion version, const SmallArray &data, ExtenHeader *exten) { - bool isGraph = !!(options & StorageOption::Graph); - - // do not allow to save graph with less than 8 nodes - if (isGraph && length () < kMaxNodeLinks) { - ctrl.msg ("Can't save graph data with less than %d nodes. Please add some more before saving.", kMaxNodeLinks); - return false; - } - String filename = util.buildPath (util.storageToBotFile (options)); - - if (data.empty ()) { - logger.error ("Unable to save %s file. Empty data. (filename: '%s').", name, filename); - return false; - } - else if (isGraph) { - for (auto &path : m_paths) { - path.display = 0.0f; - path.light = illum.getLightLevel (path.origin); - } - } - - // open the file - File file (filename, "wb"); - - // no open no fun - if (!file) { - logger.error ("Unable to open %s file for writing (filename: '%s').", name, filename); - return false; - } - auto rawLength = data.length () * sizeof (U); - SmallArray compressed (rawLength + sizeof (uint8_t) * ULZ::Excess); - - // try to compress - auto compressedLength = static_cast (ulz.compress (reinterpret_cast (data.data ()), static_cast (rawLength), reinterpret_cast (compressed.data ()))); - - if (compressedLength > 0) { - StorageHeader hdr {}; - - hdr.magic = kStorageMagic; - hdr.version = version; - hdr.options = options; - hdr.length = length (); - hdr.compressed = static_cast (compressedLength); - hdr.uncompressed = static_cast (rawLength); - - file.write (&hdr, sizeof (StorageHeader)); - file.write (compressed.data (), sizeof (uint8_t), compressedLength); - - // add extension - if ((options & StorageOption::Exten) && exten != nullptr) { - file.write (exten, sizeof (ExtenHeader)); - } - - // notify only about graph - if (isGraph || cv_debug.bool_ ()) { - ctrl.msg ("Successfully saved Bots %s data.", name); - } - } - else { - logger.error ("Unable to compress %s data (filename: '%s').", name, filename); - return false; - } - return true; -} - -template bool BotGraph::loadStorage (StringRef name, StorageOption options, StorageVersion version, SmallArray &data, ExtenHeader *exten, int32_t *outOptions) { - String filename = util.buildPath (util.storageToBotFile (options), true); - - // graphs can be downloaded... - bool isGraph = !!(options & StorageOption::Graph); - bool isDebug = cv_debug.bool_ (); - - MemFile file (filename); // open the file - - data.clear (); - data.shrink (); - - // resize data to fit the stuff - auto resizeData = [&] (const size_t length) { - data.resize (length); // for non-graph data the graph should be already loaded - data.shrink (); // free up memory to minimum - - // ensure we're have enough memory to decompress the data - data.ensure (length + ULZ::Excess); - }; - - // allocate non-graph data, so we're will be ok in case if loading will fail - if (!isGraph) { - resizeData (m_paths.length () * m_paths.length ()); - } - - // if graph & attempted to load multiple times, bail out, we're failed - if (isGraph && ++m_loadAttempts > 2) { - m_loadAttempts = 0; - return raiseLoadingError (isGraph, isDebug, file, "Unable to load %s (filename: '%s'). Download process has failed as well. No nodes has been found.", name, filename); - } - - // downloader for graph - auto download = [&] () -> bool { - if (!graph.canDownload ()) { - return false; - } - auto downloadAddress = cv_graph_url.str (); - - auto toDownload = util.buildPath (util.storageToBotFile (options), false); - auto fromDownload = strings.format ("http://%s/graph/%s.graph", downloadAddress, game.getMapName ()); - - // try to download - if (http.downloadFile (fromDownload, toDownload)) { - ctrl.msg ("%s file '%s' successfully downloaded. Processing...", name, filename); - return true; - } - else { - ctrl.msg ("Can't download '%s' from '%s' to '%s'... (%d).", filename, fromDownload, toDownload, http.getLastStatusCode ()); - } - return false; - }; - - // tries to reload or open pwf file - auto tryReload = [&] () -> bool { - file.close (); - - if (!isGraph) { - return false; - } - - if (download ()) { - return loadStorage (name, options, version, data, exten, outOptions); - } - - if (convertOldFormat ()) { - return loadStorage (name, options, version, data, exten, outOptions); - } - return false; - }; - - // no open no fun - if (!file) { - if (tryReload ()) { - return true; - } - return raiseLoadingError (isGraph, isDebug, file, "Unable to open %s file for reading (filename: '%s').", name, filename); - } - - // read the header - StorageHeader hdr {}; - file.read (&hdr, sizeof (StorageHeader)); - - // check the magic - if (hdr.magic != kStorageMagic && hdr.magic != kStorageMagicUB) { - if (tryReload ()) { - return true; - } - return raiseLoadingError (isGraph, isDebug, file, "Unable to read magic of %s (filename: '%s').", name, filename); - } - - // check the path-numbers - if (!isGraph && hdr.length != length ()) { - return raiseLoadingError (isGraph, isDebug, file, "Damaged %s (filename: '%s'). Mismatch number of nodes (got: '%d', need: '%d').", name, filename, hdr.length, m_paths.length ()); - } - - // check the count - if (hdr.length == 0 || hdr.length > kMaxNodes || hdr.length < kMaxNodeLinks) { - if (tryReload ()) { - return true; - } - return raiseLoadingError (isGraph, isDebug, file, "Damaged %s (filename: '%s'). Paths length is overflowed (got: '%d').", name, filename, hdr.length); - } - - // check the version - if (hdr.version > version && isGraph) { - ctrl.msg ("Graph version mismatch %s (filename: '%s'). Version number differs (got: '%d', need: '%d') Please, upgrade %s.", name, filename, hdr.version, version, product.name); - } - else if (hdr.version > version && !isGraph) { - return raiseLoadingError (isGraph, isDebug, file, "Damaged %s (filename: '%s'). Version number differs (got: '%d', need: '%d').", name, filename, hdr.version, version); - } - - // temporary solution to kill version 1 vistables, which has a bugs - if ((options & StorageOption::Vistable) && hdr.version == 1) { - auto vistablePath = util.buildPath (BotFile::Vistable); - - if (File::exists (vistablePath)) { - plat.removeFile (vistablePath.chars ()); - } - return raiseLoadingError (isGraph, isDebug, file, "Bugged vistable %s (filename: '%s'). Version 1 has a bugs, vistable will be recreated.", name, filename); - } - - // save graph version - if (isGraph) { - memcpy (&m_graphHeader, &hdr, sizeof (StorageHeader)); - } - - // check the storage type - if ((hdr.options & options) != options) { - return raiseLoadingError (isGraph, isDebug, file, "Incorrect storage format for %s (filename: '%s').", name, filename); - } - auto compressedSize = static_cast (hdr.compressed); - auto numberNodes = static_cast (hdr.length); - - SmallArray compressed (compressedSize + sizeof (uint8_t) * ULZ::Excess); - - // graph is not resized upon load - if (isGraph) { - resizeData (numberNodes); - } - - // read compressed data - if (file.read (compressed.data (), sizeof (uint8_t), compressedSize) == compressedSize) { - - // try to uncompress - if (ulz.uncompress (compressed.data (), hdr.compressed, reinterpret_cast (data.data ()), hdr.uncompressed) == ULZ::UncompressFailure) { - return raiseLoadingError (isGraph, isDebug, file, "Unable to decompress ULZ data for %s (filename: '%s').", name, filename); - } - else { - - if (outOptions) { - outOptions = &hdr.options; - } - - // author of graph.. save - if ((hdr.options & StorageOption::Exten) && exten != nullptr) { - auto extenSize = sizeof (ExtenHeader); - auto actuallyRead = file.read (exten, extenSize) * extenSize; - - if (isGraph) { - strings.copy (m_extenHeader.author, exten->author, cr::bufsize (exten->author)); - - if (extenSize <= actuallyRead) { - // write modified by, only if the name is different - if (!strings.isEmpty (m_extenHeader.author) && strncmp (m_extenHeader.author, exten->modified, cr::bufsize (m_extenHeader.author)) != 0) { - strings.copy (m_extenHeader.modified, exten->modified, cr::bufsize (exten->modified)); - } - } - else { - strings.copy (m_extenHeader.modified, "(none)", cr::bufsize (exten->modified)); - } - m_extenHeader.mapSize = exten->mapSize; - } - } - ctrl.msg ("Loaded Bots %s data v%d (%d/%.2fMB).", name, hdr.version, m_paths.length (), static_cast (data.capacity () * sizeof (U)) / 1024.0f / 1024.0f); - file.close (); - - return true; - } - } - else { - return raiseLoadingError (isGraph, isDebug, file, "Unable to read ULZ data for %s (filename: '%s').", name, filename); - } - return false; -} - bool BotGraph::loadGraphData () { ExtenHeader exten {}; int32_t outOptions = 0; @@ -1862,11 +1542,13 @@ bool BotGraph::loadGraphData () { m_graphHeader = {}; m_extenHeader = {}; + // re-initialize paths + reset (); + // check if loaded - bool dataLoaded = loadStorage ("Graph", StorageOption::Graph, StorageVersion::Graph, m_paths, &exten, &outOptions); + bool dataLoaded = bstor.load (m_paths, &exten, &outOptions); if (dataLoaded) { - reset (); initBuckets (); // add data to buckets @@ -1885,23 +1567,26 @@ bool BotGraph::loadGraphData () { if (!modified.empty () && !modified.contains ("(none)")) { m_graphModified.assign (exten.modified); } + planner.init (); // initialize our little path planner + practice.load (); // load bots practice + vistab.load (); // load/initialize visibility - initNodesTypes (); - loadPathMatrix (); - loadVisibility (); - loadPractice (); - + populateNodes (); + if (exten.mapSize > 0) { int mapSize = getBspSize (); if (mapSize != exten.mapSize) { - ctrl.msg ("Warning: Graph data is probably not for this map. Please check bots behaviour."); + msg ("Warning: Graph data is probably not for this map. Please check bots behaviour."); } } cv_debug_goal.set (kInvalidNodeIndex); return true; } + else { + analyzer.start (); + } return false; } @@ -1927,6 +1612,11 @@ bool BotGraph::saveGraphData () { editorName = product.name; } + // mark as analyzed + if (analyzer.isAnalyzed ()) { + options |= StorageOption::Analyzed; + } + // mark as official if (editorName.startsWith (product.name)) { options |= StorageOption::Official; @@ -1951,7 +1641,7 @@ bool BotGraph::saveGraphData () { m_narrowChecked = false; initNarrowPlaces (); - return saveStorage ("Graph", static_cast (options), StorageVersion::Graph, m_paths, &exten); + return bstor.save (m_paths, &exten, options); } void BotGraph::saveOldFormat () { @@ -1968,7 +1658,7 @@ void BotGraph::saveOldFormat () { File fp; // file was opened - if (fp.open (util.buildPath (BotFile::PodbotPWF), "wb")) { + if (fp.open (bstor.buildPath (BotFile::PodbotPWF), "wb")) { // write the node header to the file... fp.write (&header, sizeof (header)); @@ -1992,11 +1682,15 @@ float BotGraph::calculateTravelTime (float maxSpeed, const Vector &src, const Ve return origin.distance2d (src) / maxSpeed; } -bool BotGraph::isNodeReacheable (const Vector &src, const Vector &destination) { +bool BotGraph::isNodeReacheableEx (const Vector &src, const Vector &destination, const float maxHeight) { TraceResult tr {}; float distance = destination.distance (src); + if ((destination.z - src.z) >= 45.0f) { + return false; + } + // is the destination not close enough? if (distance > m_autoPathDistance) { return false; @@ -2065,7 +1759,7 @@ bool BotGraph::isNodeReacheable (const Vector &src, const Vector &destination) { float height = tr.flFraction * 1000.0f; // height from ground // is the current height greater than the step height? - if (height < lastHeight - 18.0f) { + if (height < lastHeight - maxHeight) { return false; // can't get there without jumping... } lastHeight = height; @@ -2076,130 +1770,13 @@ bool BotGraph::isNodeReacheable (const Vector &src, const Vector &destination) { return false; } -void BotGraph::rebuildVisibility () { - if (!m_needsVisRebuild) { - return; - } - TraceResult tr {}; - uint8_t res, shift; - - for (const auto &vis : m_paths) { - Vector sourceDuck = vis.origin; - Vector sourceStand = vis.origin; - - if (vis.flags & NodeFlag::Crouch) { - sourceDuck.z += 12.0f; - sourceStand.z += 18.0f + 28.0f; - } - else { - sourceDuck.z += -18.0f + 12.0f; - sourceStand.z += 28.0f; - } - uint16_t standCount = 0, crouchCount = 0; - - for (const auto &path : m_paths) { - // first check ducked visibility - Vector dest = path.origin; - - game.testLine (sourceDuck, dest, TraceIgnore::Monsters, nullptr, &tr); - - // check if line of sight to object is not blocked (i.e. visible) - if (!cr::fequal (tr.flFraction, 1.0f) || tr.fStartSolid) { - res = 1; - } - else { - res = 0; - } - res <<= 1; - - game.testLine (sourceStand, dest, TraceIgnore::Monsters, nullptr, &tr); - - // check if line of sight to object is not blocked (i.e. visible) - if (!cr::fequal (tr.flFraction, 1.0f) || tr.fStartSolid) { - res |= 1; - } - - if (res != 0) { - dest = path.origin; - - // first check ducked visibility - if (path.flags & NodeFlag::Crouch) { - dest.z += 18.0f + 28.0f; - } - else { - dest.z += 28.0f; - } - game.testLine (sourceDuck, dest, TraceIgnore::Monsters, nullptr, &tr); - - // check if line of sight to object is not blocked (i.e. visible) - if (!cr::fequal (tr.flFraction, 1.0f) || tr.fStartSolid) { - res |= 2; - } - else { - res &= 1; - } - game.testLine (sourceStand, dest, TraceIgnore::Monsters, nullptr, &tr); - - // check if line of sight to object is not blocked (i.e. visible) - if (!cr::fequal (tr.flFraction, 1.0f) || tr.fStartSolid) { - res |= 1; - } - else { - res &= 2; - } - } - shift = static_cast ((path.number % 4) << 1); - - m_vistable[vis.number * length () + path.number] &= ~static_cast (3 << shift); - m_vistable[vis.number * length () + path.number] |= res << shift; - - if (!(res & 2)) { - ++crouchCount; - } - - if (!(res & 1)) { - ++standCount; - } - } - m_paths[vis.number].vis.crouch = crouchCount; - m_paths[vis.number].vis.stand = standCount; - } - m_needsVisRebuild = false; - saveVisibility (); +bool BotGraph::isNodeReacheable (const Vector &src, const Vector &destination) { + return isNodeReacheableEx (src, destination, 45.0f); } -bool BotGraph::isVisible (int srcIndex, int destIndex) { - if (!exists (srcIndex) || !exists (destIndex)) { - return false; - } - - uint8_t res = m_vistable[srcIndex * length () + destIndex]; - res >>= (destIndex % 4) << 1; - - return !((res & 3) == 3); -} - -bool BotGraph::isDuckVisible (int srcIndex, int destIndex) { - if (!exists (srcIndex) || !exists (destIndex)) { - return false; - } - - uint8_t res = m_vistable[srcIndex * length () + destIndex]; - res >>= (destIndex % 4) << 1; - - return !((res & 2) == 2); -} - -bool BotGraph::isStandVisible (int srcIndex, int destIndex) { - if (!exists (srcIndex) || !exists (destIndex)) { - return false; - } - - uint8_t res = m_vistable[srcIndex * length () + destIndex]; - res >>= (destIndex % 4) << 1; - - return !((res & 1) == 1); +bool BotGraph::isNodeReacheableWithJump (const Vector &src, const Vector &destination) { + return isNodeReacheableEx (src, destination, cv_graph_analyze_max_jump_height.float_ ()); } void BotGraph::frame () { @@ -2244,7 +1821,7 @@ void BotGraph::frame () { // find the distance from the last used node float distance = m_lastNode.distanceSq (m_editor->v.origin); - if (distance > cr::square (128.0f)) { + if (distance > cr::sqrf (128.0f)) { // check that no other reachable nodes are nearby... for (const auto &path : m_paths) { if (isNodeReacheable (m_editor->v.origin, path.origin)) { @@ -2257,7 +1834,7 @@ void BotGraph::frame () { } // make sure nearest node is far enough away... - if (nearestDistance >= cr::square (128.0f)) { + if (nearestDistance >= cr::sqrf (128.0f)) { add (NodeAddFlag::Normal); // place a node here } } @@ -2440,7 +2017,7 @@ void BotGraph::frame () { // if radius is nonzero, draw a full circle if (path.radius > 0.0f) { - float sqr = cr::sqrtf (cr::square (path.radius) * 0.5f); + float sqr = cr::sqrtf (cr::sqrf (path.radius) * 0.5f); game.drawLine (m_editor, origin + Vector (path.radius, 0.0f, 0.0f), origin + Vector (sqr, -sqr, 0.0f), 5, 0, radiusColor, 200, 0, 10); game.drawLine (m_editor, origin + Vector (sqr, -sqr, 0.0f), origin + Vector (0.0f, -path.radius, 0.0f), 5, 0, radiusColor, 200, 0, 10); @@ -2463,8 +2040,8 @@ void BotGraph::frame () { // draw the danger directions if (!m_hasChanged) { - int dangerIndexT = getDangerIndex (Team::Terrorist, nearestIndex, nearestIndex); - int dangerIndexCT = getDangerIndex (Team::CT, nearestIndex, nearestIndex); + int dangerIndexT = practice.getIndex (Team::Terrorist, nearestIndex, nearestIndex); + int dangerIndexCT = practice.getIndex (Team::CT, nearestIndex, nearestIndex); if (exists (dangerIndexT)) { game.drawLine (m_editor, path.origin, m_paths[dangerIndexT].origin, 15, 0, { 255, 0, 0 }, 200, 0, 10, DrawLine::Arrow); // draw a red arrow to this index's danger point @@ -2556,15 +2133,15 @@ void BotGraph::frame () { // if node is not changed display experience also if (!m_hasChanged) { - int dangerIndexCT = getDangerIndex (Team::CT, nearestIndex, nearestIndex); - int dangerIndexT = getDangerIndex (Team::Terrorist, nearestIndex, nearestIndex); + int dangerIndexCT = practice.getIndex (Team::CT, nearestIndex, nearestIndex); + int dangerIndexT = practice.getIndex (Team::Terrorist, nearestIndex, nearestIndex); - String practice; - practice.assignf (" Node practice data (index / damage):\n" - " CT: %d / %d\n" - " T: %d / %d\n\n", dangerIndexCT, dangerIndexCT != kInvalidNodeIndex ? getDangerDamage (Team::CT, nearestIndex, dangerIndexCT) : 0, dangerIndexT, dangerIndexT != kInvalidNodeIndex ? getDangerDamage (Team::Terrorist, nearestIndex, dangerIndexT) : 0); + String practiceText; + practiceText.assignf (" Node practice data (index / damage):\n" + " CT: %d / %d\n" + " T: %d / %d\n\n", dangerIndexCT, dangerIndexCT != kInvalidNodeIndex ? practice.getDamage (Team::CT, nearestIndex, dangerIndexCT) : 0, dangerIndexT, dangerIndexT != kInvalidNodeIndex ? practice.getDamage (Team::Terrorist, nearestIndex, dangerIndexT) : 0); - sendHudMessage ({ 255, 255, 255 }, 0.0f, 0.16f, m_editor, practice + timeMessage); + sendHudMessage ({ 255, 255, 255 }, 0.0f, 0.16f, m_editor, practiceText + timeMessage); } else { sendHudMessage ({ 255, 255, 255 }, 0.0f, 0.16f, m_editor, timeMessage); @@ -2605,14 +2182,14 @@ bool BotGraph::checkNodes (bool teleportPlayer) { int connections = 0; if (path.number != static_cast (m_paths.index (path))) { - ctrl.msg ("Node %d path differs from index %d.", path.number, m_paths.index (path)); + msg ("Node %d path differs from index %d.", path.number, m_paths.index (path)); break; } for (const auto &test : path.links) { if (test.index != kInvalidNodeIndex) { if (test.index > length ()) { - ctrl.msg ("Node %d connected with invalid node %d.", path.number, test.index); + msg ("Node %d connected with invalid node %d.", path.number, test.index); return false; } ++connections; @@ -2622,14 +2199,14 @@ bool BotGraph::checkNodes (bool teleportPlayer) { if (connections == 0) { if (!isConnected (path.number)) { - ctrl.msg ("Node %d isn't connected with any other node.", path.number); + msg ("Node %d isn't connected with any other node.", path.number); return false; } } if (path.flags & NodeFlag::Camp) { if (path.end.empty ()) { - ctrl.msg ("Node %d camp-endposition not set.", path.number); + msg ("Node %d camp-endposition not set.", path.number); return false; } } @@ -2649,13 +2226,13 @@ bool BotGraph::checkNodes (bool teleportPlayer) { for (const auto &test : path.links) { if (test.index != kInvalidNodeIndex) { if (!exists (test.index)) { - ctrl.msg ("Node %d path index %d out of range.", path.number, test.index); + msg ("Node %d path index %d out of range.", path.number, test.index); teleport (path); return false; } else if (test.index == path.number) { - ctrl.msg ("Node %d path index %d points to itself.", path.number, test.index); + msg ("Node %d path index %d points to itself.", path.number, test.index); teleport (path); return false; @@ -2666,20 +2243,20 @@ bool BotGraph::checkNodes (bool teleportPlayer) { if (game.mapIs (MapFlags::HostageRescue)) { if (rescuePoints == 0) { - ctrl.msg ("You didn't set a rescue point."); + msg ("You didn't set a rescue point."); return false; } } if (terrPoints == 0) { - ctrl.msg ("You didn't set any terrorist important point."); + msg ("You didn't set any terrorist important point."); return false; } else if (ctPoints == 0) { - ctrl.msg ("You didn't set any CT important point."); + msg ("You didn't set any CT important point."); return false; } else if (goalPoints == 0) { - ctrl.msg ("You didn't set any goal point."); + msg ("You didn't set any goal point."); return false; } @@ -2721,7 +2298,7 @@ bool BotGraph::checkNodes (bool teleportPlayer) { for (const auto &path : m_paths) { if (!visited[path.number]) { - ctrl.msg ("Path broken from node 0 to node %d.", path.number); + msg ("Path broken from node 0 to node %d.", path.number); teleport (path); return false; @@ -2764,7 +2341,7 @@ bool BotGraph::checkNodes (bool teleportPlayer) { for (const auto &path : m_paths) { if (!visited[path.number]) { - ctrl.msg ("Path broken from node %d to node 0.", path.number); + msg ("Path broken from node %d to node 0.", path.number); teleport (path); return false; @@ -2773,13 +2350,6 @@ bool BotGraph::checkNodes (bool teleportPlayer) { return true; } -int BotGraph::getPathDist (int srcIndex, int destIndex) { - if (!exists (srcIndex) || !exists (destIndex)) { - return 1; - } - return (m_matrix.data () + (srcIndex * length ()) + destIndex)->dist; -} - void BotGraph::setVisited (int index) { if (!exists (index)) { return; @@ -2814,7 +2384,7 @@ void BotGraph::addBasic () { TraceResult tr {}; Vector up, down, front, back; - const Vector &diff = ((ladderLeft - ladderRight) ^ Vector (0.0f, 0.0f, 0.0f)).normalize () * 15.0f; + Vector diff = ((ladderLeft - ladderRight) ^ nullptr) * 15.0f; front = back = game.getEntityOrigin (ent); front = front + diff; // front @@ -2840,7 +2410,7 @@ void BotGraph::addBasic () { if (getNearestNoBuckets (point, 50.0f) == kInvalidNodeIndex) { add (NodeAddFlag::NoHostage, point); } - point.z += 160; + point.z += 160.0f; } while (point.z < down.z - 40.0f); point = down + Vector (0.0f, 0.0f, 38.0f); @@ -2855,10 +2425,14 @@ void BotGraph::addBasic () { auto autoCreateForEntity = [] (int type, const char *entity) { game.searchEntities ("classname", entity, [&] (edict_t *ent) { - const Vector &pos = game.getEntityOrigin (ent); + Vector pos = game.getEntityOrigin (ent); - if (graph.getNearestNoBuckets (pos, 50.0f) == kInvalidNodeIndex) { - graph.add (type, pos); + TraceResult tr; + game.testLine (pos, pos - Vector (0.0f, 0.0f, 999.0f), TraceIgnore::Monsters, nullptr, &tr); + tr.vecEndPos.z += 36.0f; + + if (graph.getNearestNoBuckets (tr.vecEndPos, 50.0f) == kInvalidNodeIndex) { + graph.add (type, tr.vecEndPos); } return EntitySearchResult::Continue; }); @@ -2880,31 +2454,6 @@ void BotGraph::addBasic () { autoCreateForEntity (NodeAddFlag::Goal, "func_escapezone"); // terrorist escape zone } -void BotGraph::eraseFromDisk () { - // this function removes graph file from the hard disk - - StringArray forErase; - bots.kickEveryone (true); - - // if we're delete graph, delete all corresponding to it files - forErase.emplace (util.buildPath (BotFile::Graph)); // graph itself - forErase.emplace (util.buildPath (BotFile::Practice)); // corresponding to practice - forErase.emplace (util.buildPath (BotFile::Vistable)); // corresponding to vistable - forErase.emplace (util.buildPath (BotFile::Pathmatrix)); // corresponding to matrix - - for (const auto &item : forErase) { - if (File::exists (item)) { - plat.removeFile (item.chars ()); - ctrl.msg ("File %s, has been deleted from the hard disk", item); - } - else { - logger.error ("Unable to open %s", item); - } - } - reset (); // reintialize points - m_paths.clear (); -} - void BotGraph::setBombOrigin (bool reset, const Vector &pos) { // this function stores the bomb position as a vector @@ -2950,7 +2499,7 @@ void BotGraph::setSearchIndex (int index) { m_findWPIndex = index; if (exists (m_findWPIndex)) { - ctrl.msg ("Showing direction to node %d.", m_findWPIndex); + msg ("Showing direction to node %d.", m_findWPIndex); } else { m_findWPIndex = kInvalidNodeIndex; @@ -2959,7 +2508,6 @@ void BotGraph::setSearchIndex (int index) { BotGraph::BotGraph () { m_endJumpPoint = false; - m_needsVisRebuild = false; m_jumpLearnNode = false; m_hasChanged = false; m_narrowChecked = false; @@ -2969,7 +2517,6 @@ BotGraph::BotGraph () { m_cacheNodeIndex = kInvalidNodeIndex; m_findWPIndex = kInvalidNodeIndex; m_facingAtIndex = kInvalidNodeIndex; - m_loadAttempts = 0; m_isOnLadder = false; m_terrorPoints.clear (); @@ -2979,16 +2526,12 @@ BotGraph::BotGraph () { m_rescuePoints.clear (); m_sniperPoints.clear (); - m_loadAttempts = 0; m_editFlags = 0; m_pathDisplayTime = 0.0f; m_arrowDisplayTime = 0.0f; m_autoPathDistance = 250.0f; - m_matrix.clear (); - m_practice.clear (); - m_editor = nullptr; } @@ -3004,6 +2547,10 @@ const Array &BotGraph::getNodesInBucket (const Vector &pos) { return m_hashTable[locateBucket (pos)]; } +bool BotGraph::isAnalyzed () const { + return (m_graphHeader.options & StorageOption::Analyzed); +} + void BotGraph::eraseFromBucket (const Vector &pos, int index) { auto &data = m_hashTable[locateBucket (pos)]; @@ -3016,7 +2563,7 @@ void BotGraph::eraseFromBucket (const Vector &pos, int index) { } int BotGraph::locateBucket (const Vector &pos) { - constexpr auto width = cr::square (kMaxNodes); + constexpr auto width = 8192; auto hash = [&] (float axis, int32_t shift) { return ((static_cast (axis) + width) & 0x007f80) >> shift; @@ -3024,63 +2571,6 @@ int BotGraph::locateBucket (const Vector &pos) { return hash (pos.x, 15) + hash (pos.y, 7); } -void BotGraph::updateGlobalPractice () { - // this function called after each end of the round to update knowledge about most dangerous nodes for each team. - - // no nodes, no experience used or nodes edited or being edited? - if (m_paths.empty () || m_hasChanged) { - return; // no action - } - bool adjustValues = false; - auto practice = m_practice.data (); - - // get the most dangerous node for this position for both teams - for (int team = Team::Terrorist; team < kGameTeamNum; ++team) { - int bestIndex = kInvalidNodeIndex; // best index to store - - for (int i = 0; i < length (); ++i) { - int maxDamage = 0; - bestIndex = kInvalidNodeIndex; - - for (int j = 0; j < length (); ++j) { - if (i == j) { - continue; - } - int actDamage = getDangerDamage (team, i, j); - - if (actDamage > maxDamage) { - maxDamage = actDamage; - bestIndex = j; - } - } - - if (maxDamage > kMaxPracticeDamageValue) { - adjustValues = true; - } - (practice + (i * length ()) + i)->index[team] = static_cast (bestIndex); - } - } - constexpr auto kHalfDamageVal = static_cast (kMaxPracticeDamageValue * 0.5); - - // adjust values if overflow is about to happen - if (adjustValues) { - for (int team = Team::Terrorist; team < kGameTeamNum; ++team) { - for (int i = 0; i < length (); ++i) { - for (int j = 0; j < length (); ++j) { - if (i == j) { - continue; - } - (practice + (i * length ()) + j)->damage[team] = static_cast (cr::clamp (getDangerDamage (team, i, j) - kHalfDamageVal, 0, kMaxPracticeDamageValue)); - } - } - } - } - - for (int team = Team::Terrorist; team < kGameTeamNum; ++team) { - m_highestDamage[team] = cr::clamp (m_highestDamage[team] - kHalfDamageVal, 1, kMaxPracticeDamageValue); - } -} - void BotGraph::unassignPath (int from, int to) { auto &link = m_paths[from].links[to]; @@ -3159,63 +2649,3 @@ void BotGraph::convertCampDirection (Path &path) { path.start.clampAngles (); path.end.clampAngles (); } - -int BotGraph::getDangerIndex (int team, int start, int goal) { - if (team != Team::Terrorist && team != Team::CT) { - return kInvalidNodeIndex; - } - - // realiablity check - if (!exists (start) || !exists (goal)) { - return kInvalidNodeIndex; - } - return (m_practice.data () + (start * length ()) + goal)->index[team]; -} - -int BotGraph::getDangerValue (int team, int start, int goal) { - if (team != Team::Terrorist && team != Team::CT) { - return 0; - } - - // reliability check - if (!exists (start) || !exists (goal)) { - return 0; - } - return (m_practice.data () + (start * length ()) + goal)->value[team]; -} - -int BotGraph::getDangerDamage (int team, int start, int goal) { - if (team != Team::Terrorist && team != Team::CT) { - return 0; - } - - // reliability check - if (!exists (start) || !exists (goal)) { - return 0; - } - return (m_practice.data () + (start * length ()) + goal)->damage[team]; -} - -void BotGraph::setDangerValue (int team, int start, int goal, int value) { - if (team != Team::Terrorist && team != Team::CT) { - return; - } - - // reliability check - if (!exists (start) || !exists (goal)) { - return; - } - (m_practice.data () + (start * length ()) + goal)->value[team] = static_cast (value); -} - -void BotGraph::setDangerDamage (int team, int start, int goal, int value) { - if (team != Team::Terrorist && team != Team::CT) { - return; - } - - // reliability check - if (!exists (start) || !exists (goal)) { - return; - } - (m_practice.data () + (start * length ()) + goal)->damage[team] = static_cast (value); -} diff --git a/src/linkage.cpp b/src/linkage.cpp index 1ee0d50..75cac27 100644 --- a/src/linkage.cpp +++ b/src/linkage.cpp @@ -332,8 +332,8 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int) { // any case, when the new map will be booting, ServerActivate() will be called, so we'll do // the loading of new bots and the new BSP data parsing there. - // save collected experience on shutdown - graph.savePractice (); + // save collected practice on shutdown + practice.save (); // destroy global killer entity bots.destroyKillerEntity (); @@ -384,9 +384,15 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int) { graph.frame (); } + // update analyzer if needed + analyzer.update (); + // run stuff periodically game.slowFrame (); + // rebuild vistable if needed + vistab.rebuild (); + if (bots.hasBotsOnline ()) { // keep track of grenades on map bots.updateActiveGrenade (); @@ -903,8 +909,9 @@ CR_EXPORT int Meta_Detach (PLUG_LOADTIME now, PL_UNLOAD_REASON reason) { } bots.kickEveryone (true); // kick all bots off this server - // save collected experience on shutdown - graph.savePractice (); + // save collected practice on shutdown + practice.save (); + util.disableSendTo (); // make sure all stuff cleared diff --git a/src/manager.cpp b/src/manager.cpp index 38ac546..d75b1cd 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -1251,7 +1251,6 @@ void Bot::newRound () { // delete all allocated path nodes clearSearchNodes (); - clearRoute (); m_pathOrigin = nullptr; m_destOrigin = nullptr; @@ -1688,13 +1687,18 @@ void BotManager::notifyBombDefuse () { if (!isBombPlanted ()) { return; } + auto bombPos = graph.getBombOrigin (); for (const auto &bot : bots) { - if (bot->m_team == Team::Terrorist && bot->m_notKilled && bot->getCurrentTaskId () != Task::MoveToPosition) { - bot->clearSearchNodes (); + auto task = bot->getCurrentTaskId (); - bot->m_position = graph.getBombOrigin (); - bot->startTask (Task::MoveToPosition, TaskPri::MoveToPosition, kInvalidNodeIndex, 0.0f, true); + if (bot->m_notKilled && task != Task::MoveToPosition && task != Task::DefuseBomb && task != Task::EscapeFromBomb) { + if (bot->m_team == Team::CT || bot->pev->origin.distanceSq (bombPos) < cr::sqrf (384.0f)) { + bot->clearSearchNodes (); + + bot->m_position = bombPos; + bot->startTask (Task::MoveToPosition, TaskPri::MoveToPosition, kInvalidNodeIndex, 0.0f, true); + } } } } @@ -1891,7 +1895,7 @@ void BotManager::initRound () { m_botsCanPause = false; resetFilters (); - graph.updateGlobalPractice (); // update experience data on round start + practice.update (); // update practice data on round start // calculate the round mid/end in world time m_timeRoundStart = game.time () + mp_freezetime.float_ (); diff --git a/src/message.cpp b/src/message.cpp index f00b375..d2f01d2 100644 --- a/src/message.cpp +++ b/src/message.cpp @@ -371,7 +371,7 @@ void MessageDispatcher::netMsgBarTime () { m_bot->m_hasProgressBar = true; // the progress bar on a hud // notify bots about defusing has started - if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted () && m_bot->m_team == Team::CT) { + if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted ()) { bots.notifyBombDefuse (); } } diff --git a/src/navigate.cpp b/src/navigate.cpp index 7556baf..07c7cd0 100644 --- a/src/navigate.cpp +++ b/src/navigate.cpp @@ -7,10 +7,6 @@ #include -ConVar cv_path_heuristic_mode ("yb_path_heuristic_mode", "4", "Selects the heuristic function mode. For debug purposes only.", true, 0.0f, 4.0f); -ConVar cv_path_danger_factor_min ("yb_path_danger_factor_min", "200", "Lower bound of danger factor that used to add additional danger to path based on practice.", true, 100.0f, 2400.0f); -ConVar cv_path_danger_factor_max ("yb_path_danger_factor_max", "400", "Upper bound of danger factor that used to add additional danger to path based on practice.", true, 200.0f, 4800.0f); - int Bot::findBestGoal () { auto pushToHistroy = [&] (int32_t goal) -> int32_t { m_nodeHistory.push (goal); @@ -233,7 +229,7 @@ int Bot::findGoalPost (int tactic, IntArray *defensive, IntArray *offsensive) { for (auto &point : graph.m_goalPoints) { float distance = graph[point].origin.distanceSq (pev->origin); - if (distance > cr::square (1024.0f)) { + if (distance > cr::sqrf (1024.0f)) { continue; } if (distance < minDist) { @@ -299,7 +295,7 @@ int Bot::findGoalPost (int tactic, IntArray *defensive, IntArray *offsensive) { break; } - if (graph.getDangerValue (m_team, m_currentNodeIndex, goalChoices[i]) < graph.getDangerValue (m_team, m_currentNodeIndex, goalChoices[i + 1])) { + if (practice.getValue (m_team, m_currentNodeIndex, goalChoices[i]) < practice.getValue (m_team, m_currentNodeIndex, goalChoices[i + 1])) { cr::swap (goalChoices[i + 1], goalChoices[i]); sorting = true; } @@ -368,7 +364,7 @@ void Bot::ignoreCollision () { void Bot::doPlayerAvoidance (const Vector &normal) { m_hindrance = nullptr; - float distance = cr::square (348.0f); + float distance = cr::sqrf (348.0f); if (getCurrentTaskId () == Task::Attack || isOnLadder ()) { return; @@ -400,7 +396,7 @@ void Bot::doPlayerAvoidance (const Vector &normal) { } auto nearest = client.ent->v.origin.distanceSq (pev->origin); - if (nearest < cr::square (pev->maxspeed) && nearest < distance) { + if (nearest < cr::sqrf (pev->maxspeed) && nearest < distance) { m_hindrance = client.ent; distance = nearest; } @@ -425,19 +421,19 @@ void Bot::doPlayerAvoidance (const Vector &normal) { auto nextFrameDistance = pev->origin.distanceSq (m_hindrance->v.origin + m_hindrance->v.velocity * interval); // is player that near now or in future that we need to steer away? - if (movedDistance <= cr::square (48.0f) || (distance <= cr::square (56.0f) && nextFrameDistance < distance)) { - auto dir = (pev->origin - m_hindrance->v.origin).normalize2d (); + if (movedDistance <= cr::sqrf (48.0f) || (distance <= cr::sqrf (56.0f) && nextFrameDistance < distance)) { + auto dir = (pev->origin - m_hindrance->v.origin).normalize2d_apx (); // to start strafing, we have to first figure out if the target is on the left side or right side - if ((dir | right.normalize2d ()) > 0.0f) { + if ((dir | right.normalize2d_apx ()) > 0.0f) { setStrafeSpeed (normal, pev->maxspeed); } else { setStrafeSpeed (normal, -pev->maxspeed); } - if (distance < cr::square (76.0f)) { - if ((dir | forward.normalize2d ()) < 0.0f) { + if (distance < cr::sqrf (76.0f)) { + if ((dir | forward.normalize2d_apx ()) < 0.0f) { m_moveSpeed = -pev->maxspeed; } } @@ -495,7 +491,7 @@ void Bot::checkTerrain (float movedDistance, const Vector &dirNormal) { } - // bot is stuc, but not yet decided what to do? + // bot is stuck, but not yet decided what to do? if (m_collisionState == CollisionState::Undecided) { uint32_t bits = 0; @@ -541,19 +537,20 @@ void Bot::checkTerrain (float movedDistance, const Vector &dirNormal) { else { dirLeft = true; } - const Vector &testDir = m_moveSpeed > 0.0f ? forward : -forward; + const auto &testDir = m_moveSpeed > 0.0f ? forward : -forward; + constexpr float blockDistance = 56.0f; // now check which side is blocked - src = pev->origin + right * 32.0f; - dst = src + testDir * 32.0f; + src = pev->origin + right * blockDistance; + dst = src + testDir * blockDistance; game.testHull (src, dst, TraceIgnore::Monsters, head_hull, ent (), &tr); if (!cr::fequal (tr.flFraction, 1.0f)) { blockedRight = true; } - src = pev->origin - right * 32.0f; - dst = src + testDir * 32.0f; + src = pev->origin - right * blockDistance; + dst = src + testDir * blockDistance; game.testHull (src, dst, TraceIgnore::Monsters, head_hull, ent (), &tr); @@ -1057,7 +1054,7 @@ bool Bot::updateNavigation () { } // needs precise placement - check if we get past the point - if (desiredDistance < 22.0f && nodeDistance < 30.0f && m_pathOrigin.distanceSq (pev->origin + pev->velocity * m_frameInterval) >= cr::square (nodeDistance)) { + if (desiredDistance < 22.0f && nodeDistance < 30.0f && m_pathOrigin.distanceSq (pev->origin + pev->velocity * m_frameInterval) >= cr::sqrf (nodeDistance)) { desiredDistance = nodeDistance + 1.0f; } @@ -1078,15 +1075,16 @@ bool Bot::updateNavigation () { // did we reach a destination node? if (getTask ()->data == m_currentNodeIndex) { if (m_chosenGoalIndex != kInvalidNodeIndex) { + constexpr int maxGoalValue = PracticeLimit::Goal; // add goal values - int goalValue = graph.getDangerValue (m_team, m_chosenGoalIndex, m_currentNodeIndex); + int goalValue = practice.getValue (m_team, m_chosenGoalIndex, m_currentNodeIndex); int addedValue = static_cast (m_healthValue * 0.5f + m_goalValue * 0.5f); - goalValue = cr::clamp (goalValue + addedValue, -kMaxPracticeGoalValue, kMaxPracticeGoalValue); + goalValue = cr::clamp (goalValue + addedValue, -maxGoalValue, maxGoalValue); // update the practice for team - graph.setDangerValue (m_team, m_chosenGoalIndex, m_currentNodeIndex, goalValue); + practice.setValue (m_team, m_chosenGoalIndex, m_currentNodeIndex, goalValue); } return true; } @@ -1193,7 +1191,7 @@ bool Bot::updateLiftHandling () { m_destOrigin = m_liftTravelPos; // check if we enough to destination - if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) { + if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) { wait (); // need to wait our following teammate ? @@ -1247,7 +1245,7 @@ bool Bot::updateLiftHandling () { if (needWaitForTeammate) { m_destOrigin = m_liftTravelPos; - if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) { + if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) { wait (); } } @@ -1278,7 +1276,7 @@ bool Bot::updateLiftHandling () { m_liftState = LiftState::TravelingBy; m_liftUsageTime = game.time () + 14.0f; - if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) { + if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) { wait (); } } @@ -1288,7 +1286,7 @@ bool Bot::updateLiftHandling () { if (m_liftState == LiftState::TravelingBy) { m_destOrigin = Vector (m_liftTravelPos.x, m_liftTravelPos.y, pev->origin.z); - if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) { + if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) { wait (); } } @@ -1305,7 +1303,7 @@ bool Bot::updateLiftHandling () { m_destOrigin = pev->origin; } - if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) { + if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) { wait (); } } @@ -1338,7 +1336,7 @@ bool Bot::updateLiftHandling () { m_destOrigin = button->v.origin; } - if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) { + if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) { wait (); } } @@ -1369,7 +1367,7 @@ bool Bot::updateLiftHandling () { } } - if (pev->origin.distanceSq (m_destOrigin) < cr::square (20.0f)) { + if (pev->origin.distanceSq (m_destOrigin) < cr::sqrf (20.0f)) { wait (); } } @@ -1433,344 +1431,15 @@ bool Bot::updateLiftStates () { return true; } -void Bot::findShortestPath (int srcIndex, int destIndex) { - // this function finds the shortest path from source index to destination index - - if (!graph.exists (srcIndex)) { - logger.error ("%s source path index not valid (%d).", __FUNCTION__, srcIndex); - return; - } - else if (!graph.exists (destIndex)) { - logger.error ("%s destination path index not valid (%d).", __FUNCTION__, destIndex); - return; - } - clearSearchNodes (); - - m_chosenGoalIndex = srcIndex; - m_goalValue = 0.0f; - - m_pathWalk.add (srcIndex); - - while (srcIndex != destIndex) { - srcIndex = (graph.m_matrix.data () + (srcIndex * graph.length ()) + destIndex)->index; - - if (srcIndex < 0) { - m_prevGoalIndex = kInvalidNodeIndex; - getTask ()->data = kInvalidNodeIndex; - - return; - } - m_pathWalk.add (srcIndex); - } -} - -void Bot::findPath (int srcIndex, int destIndex, FindPath pathType /*= FindPath::Fast */) { - // this function finds a path from srcIndex to destIndex - - auto dangerFactor = [&] () -> float { - return rg.get (cv_path_danger_factor_min.float_ (), cv_path_danger_factor_max.float_ ()) * 2.0f / cr::clamp (static_cast (m_difficulty), 1.0f, 4.0f); - }; - - // least kills and number of nodes to goal for a team - auto gfunctionKillsDist = [&dangerFactor] (int team, int currentIndex, int parentIndex) -> float { - if (parentIndex == kInvalidNodeIndex) { - return 0.0f; - } - auto cost = static_cast (graph.getDangerDamage (team, currentIndex, currentIndex) + graph.getHighestDamageForTeam (team)); - const auto ¤t = graph[currentIndex]; - - for (auto &neighbour : current.links) { - if (neighbour.index != kInvalidNodeIndex) { - cost += static_cast (graph.getDangerDamage (team, neighbour.index, neighbour.index)); - } - } - - if (current.flags & NodeFlag::Crouch) { - cost *= 1.5f; - } - return cost + dangerFactor (); - }; - - // least kills and number of nodes to goal for a team - auto gfunctionKillsDistCTWithHostage = [&gfunctionKillsDist] (int team, int currentIndex, int parentIndex) -> float { - const auto ¤t = graph[currentIndex]; - - if (current.flags & NodeFlag::NoHostage) { - return 65355.0f; - } - else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) { - return gfunctionKillsDist (team, currentIndex, parentIndex) * 500.0f; - } - return gfunctionKillsDist (team, currentIndex, parentIndex); - }; - - // least kills to goal for a team - auto gfunctionKills = [&dangerFactor] (int team, int currentIndex, int) -> float { - auto cost = static_cast (graph.getDangerDamage (team, currentIndex, currentIndex)); - const auto ¤t = graph[currentIndex]; - - for (auto &neighbour : current.links) { - if (neighbour.index != kInvalidNodeIndex) { - cost += static_cast (graph.getDangerDamage (team, neighbour.index, neighbour.index)); - } - } - - if (current.flags & NodeFlag::Crouch) { - cost *= 1.5f; - } - return cost + dangerFactor () + 1.0f; - }; - - // least kills to goal for a team - auto gfunctionKillsCTWithHostage = [&gfunctionKills] (int team, int currentIndex, int parentIndex) -> float { - if (parentIndex == kInvalidNodeIndex) { - return 0.0f; - } - const auto ¤t = graph[currentIndex]; - - if (current.flags & NodeFlag::NoHostage) { - return 65355.0f; - } - else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) { - return gfunctionKills (team, currentIndex, parentIndex) * 500.0f; - } - return gfunctionKills (team, currentIndex, parentIndex); - }; - - auto gfunctionPathDist = [] (int, int currentIndex, int parentIndex) -> float { - if (parentIndex == kInvalidNodeIndex) { - return 0.0f; - } - const auto &parent = graph[parentIndex]; - const auto ¤t = graph[currentIndex]; - - for (const auto &link : parent.links) { - if (link.index == currentIndex) { - const auto distance = static_cast (link.distance); - - // we don't like ladder or crouch point - if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) { - return distance * 1.5f; - } - return distance; - } - } - return 65355.0f; - }; - - auto gfunctionPathDistWithHostage = [&gfunctionPathDist] (int, int currentIndex, int parentIndex) -> float { - const auto ¤t = graph[currentIndex]; - - if (current.flags & NodeFlag::NoHostage) { - return 65355.0f; - } - else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) { - return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex) * 500.0f; - } - return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex); - }; - - // square distance heuristic - auto hfunctionPathDist = [] (int index, int, int goalIndex) -> float { - const auto &start = graph[index]; - const auto &goal = graph[goalIndex]; - - float x = cr::abs (start.origin.x - goal.origin.x); - float y = cr::abs (start.origin.y - goal.origin.y); - float z = cr::abs (start.origin.z - goal.origin.z); - - switch (cv_path_heuristic_mode.int_ ()) { - case 0: - default: - return cr::max (cr::max (x, y), z); // chebyshev distance - - case 1: - return x + y + z; // manhattan distance - - case 2: - return 0.0f; // no heuristic - - case 3: - case 4: - // euclidean based distance - float euclidean = cr::powf (cr::powf (x, 2.0f) + cr::powf (y, 2.0f) + cr::powf (z, 2.0f), 0.5f); - - if (cv_path_heuristic_mode.int_ () == 4) { - return 1000.0f * (cr::ceilf (euclidean) - euclidean); - } - return euclidean; - } - }; - - // square distance heuristic with hostages - auto hfunctionPathDistWithHostage = [&hfunctionPathDist] (int index, int, int goalIndex) -> float { - if (graph[index].flags & NodeFlag::NoHostage) { - return 65355.0f; - } - return hfunctionPathDist (index, kInvalidNodeIndex, goalIndex); - }; - - // none heuristic - auto hfunctionNone = [&hfunctionPathDist] (int index, int, int goalIndex) -> float { - return hfunctionPathDist (index, kInvalidNodeIndex, goalIndex) / (128.0f * 10.0f); - }; - - if (!graph.exists (srcIndex)) { - logger.error ("%s source path index not valid (%d).", __FUNCTION__, srcIndex); - return; - } - else if (!graph.exists (destIndex)) { - - logger.error ("%s destination path index not valid (%d).", __FUNCTION__, destIndex); - return; - } - - // astar needs some nodes to work with - if (graph.length () < kMaxNodeLinks) { - logger.error ("A* Search for bot \"%s\" has failed due to too small number of nodes (%d). Seems to be graph is broken.", pev->netname.chars (), graph.length ()); - return; - } - - // holders for heuristic functions - static Lambda gcalc, hcalc; - - // get correct calculation for heuristic - if (pathType == FindPath::Optimal) { - if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) { - hcalc = hfunctionPathDistWithHostage; - gcalc = gfunctionKillsDistCTWithHostage; - } - else { - hcalc = hfunctionPathDist; - gcalc = gfunctionKillsDist; - } - } - else if (pathType == FindPath::Safe) { - if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) { - hcalc = hfunctionNone; - gcalc = gfunctionKillsCTWithHostage; - } - else { - hcalc = hfunctionNone; - gcalc = gfunctionKills; - } - } - else { - if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) { - hcalc = hfunctionPathDistWithHostage; - gcalc = gfunctionPathDistWithHostage; - } - else { - hcalc = hfunctionPathDist; - gcalc = gfunctionPathDist; - } - } - clearSearchNodes (); - - m_chosenGoalIndex = srcIndex; - m_goalValue = 0.0f; - - clearRoute (); - auto srcRoute = &m_routes[srcIndex]; - - // put start node into open list - srcRoute->g = gcalc (m_team, srcIndex, kInvalidNodeIndex); - srcRoute->f = srcRoute->g + hcalc (srcIndex, kInvalidNodeIndex, destIndex); - srcRoute->state = RouteState::Open; - - m_routeQue.clear (); - m_routeQue.emplace (srcIndex, srcRoute->g); - - while (!m_routeQue.empty ()) { - // remove the first node from the open list - int currentIndex = m_routeQue.pop ().first; - - // safes us from bad graph... - if (m_routeQue.length () >= graph.getMaxRouteLength () - 1) { - logger.error ("A* Search for bot \"%s\" has tried to build path with at least %d nodes. Seems to be graph is broken.", pev->netname.chars (), m_routeQue.length ()); - - kick (); // kick the bot off... - return; - } - - // is the current node the goal node? - if (currentIndex == destIndex) { - - // build the complete path - do { - m_pathWalk.add (currentIndex); - currentIndex = m_routes[currentIndex].parent; - - } while (currentIndex != kInvalidNodeIndex); - - // reverse path for path follower - m_pathWalk.reverse (); - - return; - } - auto curRoute = &m_routes[currentIndex]; - - if (curRoute->state != RouteState::Open) { - continue; - } - - // put current node into CLOSED list - curRoute->state = RouteState::Closed; - - // now expand the current node - for (const auto &child : graph[currentIndex].links) { - if (child.index == kInvalidNodeIndex) { - continue; - } - auto childRoute = &m_routes[child.index]; - - // calculate the F value as F = G + H - const float g = curRoute->g + gcalc (m_team, child.index, currentIndex); - const float h = hcalc (child.index, kInvalidNodeIndex, destIndex); - const float f = g + h; - - if (childRoute->state == RouteState::New || childRoute->f > f) { - // put the current child into open list - childRoute->parent = currentIndex; - childRoute->state = RouteState::Open; - - childRoute->g = g; - childRoute->f = f; - - m_routeQue.emplace (child.index, g); - } - } - } - logger.error ("A* Search for bot \"%s\" has failed. Falling back to shortest-path algorithm. Seems to be graph is broken.", pev->netname.chars ()); - - // fallback to shortest path - findShortestPath (srcIndex, destIndex); // A* found no path, try floyd pathfinder instead -} - void Bot::clearSearchNodes () { m_pathWalk.clear (); m_chosenGoalIndex = kInvalidNodeIndex; } -void Bot::clearRoute () { - m_routes.resize (static_cast (graph.length ())); - - for (const auto &path : graph) { - auto route = &m_routes[path.number]; - - route->g = route->f = 0.0f; - route->parent = kInvalidNodeIndex; - route->state = RouteState::New; - } - m_routes.clear (); -} - int Bot::findAimingNode (const Vector &to, int &pathLength) { // return the most distant node which is seen from the bot to the target and is within count - if (m_currentNodeIndex == kInvalidNodeIndex) { - changeNodeIndex (findNearestNode ()); - } + ensureCurrentNodeIndex (); int destIndex = graph.getNearest (to); int bestIndex = m_currentNodeIndex; @@ -1779,19 +1448,15 @@ int Bot::findAimingNode (const Vector &to, int &pathLength) { return kInvalidNodeIndex; } - while (destIndex != m_currentNodeIndex) { - destIndex = (graph.m_matrix.data () + (destIndex * graph.length ()) + m_currentNodeIndex)->index; - - if (destIndex < 0) { - break; - } + planner.find (destIndex, m_currentNodeIndex, [&] (int index) { ++pathLength; - if (graph.isVisible (m_currentNodeIndex, destIndex)) { - bestIndex = destIndex; - break; + if (vistab.visible (m_currentNodeIndex, index)) { + bestIndex = index; + return false; } - } + return true; + }); if (bestIndex == m_currentNodeIndex) { return kInvalidNodeIndex; @@ -1920,7 +1585,7 @@ float Bot::getEstimatedNodeReachTime () { float distance = graph[m_previousNodes[0]].origin.distanceSq (graph[m_currentNodeIndex].origin); // caclulate estimated time - estimatedTime = 5.0f * (distance / cr::square (m_moveSpeed + 1.0f)); + estimatedTime = 5.0f * (distance / cr::sqrf (m_moveSpeed + 1.0f)); bool longTermReachability = (m_pathFlags & NodeFlag::Crouch) || (m_pathFlags & NodeFlag::Ladder) || (pev->button & IN_DUCK) || (m_oldButtons & IN_DUCK); @@ -1942,21 +1607,23 @@ void Bot::findValidNode () { findNextBestNode (); } else if (m_navTimeset + getEstimatedNodeReachTime () < game.time ()) { + constexpr int maxDamageValue = PracticeLimit::Damage; + // increase danager for both teams for (int team = Team::Terrorist; team < kGameTeamNum; ++team) { - int damageValue = graph.getDangerDamage (team, m_currentNodeIndex, m_currentNodeIndex); - damageValue = cr::clamp (damageValue + 100, 0, kMaxPracticeDamageValue); + int damageValue = practice.getDamage (team, m_currentNodeIndex, m_currentNodeIndex); + damageValue = cr::clamp (damageValue + 100, 0, maxDamageValue); // affect nearby connected with victim nodes for (auto &neighbour : m_path->links) { if (graph.exists (neighbour.index)) { - int neighbourValue = graph.getDangerDamage (team, neighbour.index, neighbour.index); - neighbourValue = cr::clamp (neighbourValue + 100, 0, kMaxPracticeDamageValue); + int neighbourValue = practice.getDamage (team, neighbour.index, neighbour.index); + neighbourValue = cr::clamp (neighbourValue + 100, 0, maxDamageValue); - graph.setDangerDamage (m_team, neighbour.index, neighbour.index, neighbourValue); + practice.setDamage (m_team, neighbour.index, neighbour.index, neighbourValue); } } - graph.setDangerDamage (m_team, m_currentNodeIndex, m_currentNodeIndex, damageValue); + practice.setDamage (m_team, m_currentNodeIndex, m_currentNodeIndex, damageValue); } clearSearchNodes (); findNextBestNode (); @@ -1990,7 +1657,7 @@ int Bot::findNearestNode () { constexpr float kMaxDistance = 1024.0f; int index = kInvalidNodeIndex; - float minimumDistance = cr::square (kMaxDistance); + float minimumDistance = cr::sqrf (kMaxDistance); const auto &origin = pev->origin + pev->velocity * m_frameInterval; const auto &bucket = graph.getNodesInBucket (origin); @@ -2005,7 +1672,7 @@ int Bot::findNearestNode () { if (distance < minimumDistance) { // if bot doing navigation, make sure node really visible and reacheable - if (graph.isVisible (m_currentNodeIndex, path.number) && isReachableNode (path.number)) { + if (vistab.visible (m_currentNodeIndex, path.number) && isReachableNode (path.number)) { index = path.number; minimumDistance = distance; } @@ -2014,7 +1681,7 @@ int Bot::findNearestNode () { // try to search ANYTHING that can be reachaed if (index == kInvalidNodeIndex) { - minimumDistance = cr::square (kMaxDistance); + minimumDistance = cr::sqrf (kMaxDistance); const auto &nearestNodes = graph.getNarestInRadius (kMaxDistance, pev->origin); for (const auto &i : nearestNodes) { @@ -2059,7 +1726,7 @@ int Bot::findBombNode () { const auto &audible = isBombAudible (); // take the nearest to bomb nodes instead of goal if close enough - if (pev->origin.distanceSq (bomb) < cr::square (96.0f)) { + if (pev->origin.distanceSq (bomb) < cr::sqrf (96.0f)) { int node = graph.getNearest (bomb, 420.0f); m_bombSearchOverridden = true; @@ -2105,9 +1772,7 @@ int Bot::findDefendNode (const Vector &origin) { // this function tries to find a good position which has a line of sight to a position, // provides enough cover point, and is far away from the defending position - if (m_currentNodeIndex == kInvalidNodeIndex) { - changeNodeIndex (findNearestNode ()); - } + ensureCurrentNodeIndex (); TraceResult tr {}; int nodeIndex[kMaxNodeLinks] {}; @@ -2132,12 +1797,12 @@ int Bot::findDefendNode (const Vector &origin) { // find the best node now for (const auto &path : graph) { // exclude ladder & current nodes - if ((path.flags & NodeFlag::Ladder) || path.number == srcIndex || !graph.isVisible (path.number, posIndex)) { + if ((path.flags & NodeFlag::Ladder) || path.number == srcIndex || !vistab.visible (path.number, posIndex)) { continue; } // use the 'real' pathfinding distances - int distance = graph.getPathDist (srcIndex, path.number); + int distance = planner.dist (srcIndex, path.number); // skip wayponts too far if (distance > kMaxDistance) { @@ -2192,11 +1857,11 @@ int Bot::findDefendNode (const Vector &origin) { // use statistic if we have them for (int i = 0; i < kMaxNodeLinks; ++i) { if (nodeIndex[i] != kInvalidNodeIndex) { - int practice = graph.getDangerDamage (m_team, nodeIndex[i], nodeIndex[i]); - practice = (practice * 100) / graph.getHighestDamageForTeam (m_team); + int practiceDamage = practice.getDamage (m_team, nodeIndex[i], nodeIndex[i]); + practiceDamage = (practiceDamage * 100) / practice.getHighestDamageForTeam (m_team); - minDistance[i] = (practice * 100) / 8192; - minDistance[i] += practice; + minDistance[i] = (practiceDamage * 100) / 8192; + minDistance[i] += practiceDamage; } } bool sorting = false; @@ -2220,7 +1885,7 @@ int Bot::findDefendNode (const Vector &origin) { IntArray found; for (const auto &path : graph) { - if (origin.distanceSq (path.origin) < cr::square (static_cast (kMaxDistance)) && graph.isVisible (path.number, posIndex) && !isOccupiedNode (path.number)) { + if (origin.distanceSq (path.origin) < cr::sqrf (static_cast (kMaxDistance)) && vistab.visible (path.number, posIndex) && !isOccupiedNode (path.number)) { found.push (path.number); } } @@ -2284,13 +1949,13 @@ int Bot::findCoverNode (float maxDistance) { // find the best node now for (const auto &path : graph) { // exclude ladder, current node and nodes seen by the enemy - if ((path.flags & NodeFlag::Ladder) || path.number == srcIndex || graph.isVisible (enemyIndex, path.number)) { + if ((path.flags & NodeFlag::Ladder) || path.number == srcIndex || vistab.visible (enemyIndex, path.number)) { continue; } bool neighbourVisible = false; // now check neighbour nodes for visibility for (auto &enemy : enemies) { - if (graph.isVisible (enemy, path.number)) { + if (vistab.visible (enemy, path.number)) { neighbourVisible = true; break; } @@ -2302,8 +1967,8 @@ int Bot::findCoverNode (float maxDistance) { } // use the 'real' pathfinding distances - int distance = graph.getPathDist (srcIndex, path.number); - int enemyDistance = graph.getPathDist (enemyIndex, path.number); + int distance = planner.dist (srcIndex, path.number); + int enemyDistance = planner.dist (enemyIndex, path.number); if (distance >= enemyDistance) { continue; @@ -2346,11 +2011,11 @@ int Bot::findCoverNode (float maxDistance) { // use statistic if we have them for (int i = 0; i < kMaxNodeLinks; ++i) { if (nodeIndex[i] != kInvalidNodeIndex) { - int practice = graph.getDangerDamage (m_team, nodeIndex[i], nodeIndex[i]); - practice = (practice * 100) / graph.getHighestDamageForTeam (m_team); + int practiceDamage = practice.getDamage (m_team, nodeIndex[i], nodeIndex[i]); + practiceDamage = (practiceDamage * 100) / practice.getHighestDamageForTeam (m_team); - minDistance[i] = (practice * 100) / 8192; - minDistance[i] += practice; + minDistance[i] = (practiceDamage * 100) / 8192; + minDistance[i] += practiceDamage; } } bool sorting = false; @@ -2396,23 +2061,36 @@ bool Bot::selectBestNextNode () { assert (!m_pathWalk.empty ()); assert (m_pathWalk.hasNext ()); - if (!isOccupiedNode (m_pathWalk.first ())) { + auto nextNodeIndex = m_pathWalk.next (); + auto currentNodeIndex = m_pathWalk.first (); + auto prevNodeIndex = m_currentNodeIndex; + + if (!isOccupiedNode (currentNodeIndex)) { return false; } - for (auto &link : m_path->links) { - if (graph.exists (link.index) && m_pathWalk.first () != link.index && graph.isConnected (link.index, m_pathWalk.next ()) && graph.isConnected (m_currentNodeIndex, link.index)) { + // check the links + for (const auto &link : graph[prevNodeIndex].links) { - // don't use ladder nodes as alternative - if (graph[link.index].flags & (NodeFlag::Ladder | PathFlag::Jump)) { - continue; - } + // skip invalid links, or links that points to itself + if (!graph.exists (link.index) || currentNodeIndex == link.index) { + continue; + } - if (graph[link.index].origin.z <= graph[m_currentNodeIndex].origin.z + 10.0f && !isOccupiedNode (link.index)) { - m_pathWalk.first () = link.index; - - return true; - } + // skip itn't connected links + if (!graph.isConnected (link.index, nextNodeIndex) || !graph.isConnected (link.index, prevNodeIndex)) { + continue; + } + + // don't use ladder nodes as alternative + if (graph[link.index].flags & (NodeFlag::Ladder | NodeFlag::Camp | PathFlag::Jump)) { + continue; + } + + // if not occupied, just set advance + if (!isOccupiedNode (link.index)) { + m_pathWalk.first () = link.index; + return true; } } return false; @@ -2462,7 +2140,7 @@ bool Bot::advanceMovement () { m_campButtons = 0; const int nextIndex = m_pathWalk.next (); - auto kills = static_cast (graph.getDangerDamage (m_team, nextIndex, nextIndex)); + auto kills = static_cast (practice.getDamage (m_team, nextIndex, nextIndex)); // if damage done higher than one if (kills > 1.0f && bots.getRoundMidTime () > game.time ()) { @@ -3010,7 +2688,7 @@ bool Bot::isDeadlyMove (const Vector &to) { // this function eturns if given location would hurt Bot with falling damage TraceResult tr {}; - const auto &direction = (to - pev->origin).normalize (); // 1 unit long + const auto &direction = (to - pev->origin).normalize_apx (); // 1 unit long Vector check = to, down = to; down.z -= 1000.0f; // straight down 1000 units @@ -3022,13 +2700,13 @@ bool Bot::isDeadlyMove (const Vector &to) { } float lastHeight = tr.flFraction * 1000.0f; // height from ground - float distance = to.distance (check); // distance from goal + float distance = to.distanceSq (check); // distance from goal - if (distance <= 30.0f && lastHeight > 150.0f) { + if (distance <= cr::sqrf (30.0f) && lastHeight > 150.0f) { return true; } - while (distance > 30.0f) { + while (distance > cr::sqrf (30.0f)) { check = check - direction * 30.0f; // move 10 units closer to the goal... down = check; @@ -3047,7 +2725,7 @@ bool Bot::isDeadlyMove (const Vector &to) { return true; } lastHeight = height; - distance = to.distance (check); // distance from goal + distance = to.distanceSq (check); // distance from goal } return false; } @@ -3116,9 +2794,7 @@ void Bot::changeYaw (float speed) { int Bot::getRandomCampDir () { // find a good node to look at when camping - if (m_currentNodeIndex == kInvalidNodeIndex) { - changeNodeIndex (findNearestNode ()); - } + ensureCurrentNodeIndex (); constexpr int kMaxNodesToSearch = 5; int count = 0, indices[kMaxNodesToSearch] {}; @@ -3126,7 +2802,7 @@ int Bot::getRandomCampDir () { uint16_t visibility[kMaxNodesToSearch] {}; for (const auto &path : graph) { - if (m_currentNodeIndex == path.number || !graph.isVisible (m_currentNodeIndex, path.number)) { + if (m_currentNodeIndex == path.number || !vistab.visible (m_currentNodeIndex, path.number)) { continue; } @@ -3196,7 +2872,7 @@ void Bot::updateLookAngles () { float stiffness = 200.0f; float damping = 25.0f; - if (((m_aimFlags & (AimFlags::Enemy | AimFlags::Entity | AimFlags::Grenade)) || m_isEnemyReachable) && m_difficulty > Difficulty::Normal) { + if (((m_aimFlags & (AimFlags::Enemy | AimFlags::Entity | AimFlags::Grenade)) || m_wantsToFire) && m_difficulty > Difficulty::Normal) { if (m_difficulty == Difficulty::Expert) { accelerate += 600.0f; } @@ -3355,7 +3031,7 @@ bool Bot::isOccupiedNode (int index, bool needZeroVelocity) { } // do not check clients far away from us - if (pev->origin.distanceSq (client.origin) > cr::square (320.0f)) { + if (pev->origin.distanceSq (client.origin) > cr::sqrf (320.0f)) { continue; } @@ -3364,7 +3040,7 @@ bool Bot::isOccupiedNode (int index, bool needZeroVelocity) { } auto length = client.origin.distanceSq (graph[index].origin); - if (length < cr::clamp (cr::square (graph[index].radius) * 2.0f, cr::square (40.0f), cr::square (90.0f))) { + if (length < cr::clamp (cr::sqrf (graph[index].radius) * 2.0f, cr::sqrf (40.0f), cr::sqrf (90.0f))) { return true; } auto bot = bots[client.ent]; @@ -3417,7 +3093,7 @@ bool Bot::isReachableNode (int index) { const Vector &dst = graph[index].origin; // is the destination close enough? - if (dst.distanceSq (src) >= cr::square (600.0f)) { + if (dst.distanceSq (src) >= cr::sqrf (600.0f)) { return false; } @@ -3473,3 +3149,100 @@ bool Bot::isBannedNode (int index) { } return false; } + +void Bot::findShortestPath (int srcIndex, int destIndex) { + // this function finds the shortest path from source index to destination index + + clearSearchNodes (); + + m_chosenGoalIndex = srcIndex; + m_goalValue = 0.0f; + + bool success = planner.find (srcIndex, destIndex, [&] (int index) { + m_pathWalk.add (index); + return true; + }); + + if (success) { + m_prevGoalIndex = kInvalidNodeIndex; + getTask ()->data = kInvalidNodeIndex; + } +} + +void Bot::findPath (int srcIndex, int destIndex, FindPath pathType /*= FindPath::Fast */) { + // this function finds a path from srcIndex to destIndex + + if (!graph.exists (srcIndex)) { + logger.error ("%s source path index not valid (%d).", __FUNCTION__, srcIndex); + return; + } + else if (!graph.exists (destIndex)) { + logger.error ("%s destination path index not valid (%d).", __FUNCTION__, destIndex); + return; + } + auto pathPlanner = planner.getAStar (); + + // get correct calculation for heuristic + if (pathType == FindPath::Optimal) { + if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) { + pathPlanner->setH (Heuristic::hfunctionPathDistWithHostage); + pathPlanner->setG (Heuristic::gfunctionKillsDistCTWithHostage); + } + else { + pathPlanner->setH (Heuristic::hfunctionPathDist); + pathPlanner->setG (Heuristic::gfunctionKillsDist); + } + } + else if (pathType == FindPath::Safe) { + if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) { + pathPlanner->setH (Heuristic::hfunctionNone); + pathPlanner->setG (Heuristic::gfunctionKillsCTWithHostage); + } + else { + pathPlanner->setH (Heuristic::hfunctionNone); + pathPlanner->setG (Heuristic::gfunctionKills); + } + } + else { + if (game.mapIs (MapFlags::HostageRescue) && hasHostage ()) { + pathPlanner->setH (Heuristic::hfunctionPathDistWithHostage); + pathPlanner->setG (Heuristic::gfunctionPathDistWithHostage); + } + else { + pathPlanner->setH (Heuristic::hfunctionPathDist); + pathPlanner->setG (Heuristic::gfunctionPathDist); + } + } + clearSearchNodes (); + + m_chosenGoalIndex = srcIndex; + m_goalValue = 0.0f; + + auto result = pathPlanner->find (m_team, srcIndex, destIndex, [&] (int index) { + m_pathWalk.add (index); + return true; + }); + + // view the results + switch (result) { + case AStarResult::Success: + m_pathWalk.reverse (); // reverse path for path follower + break; + + case AStarResult::InternalError: + // bot should not roam when this occurs + kick (); // kick the bot off... + + logger.error ("A* Search for bot \"%s\" failed with internal pathfinder error. Seems to be graph is broken. Bot removed (re-added).", pev->netname.chars ()); + break; + + case AStarResult::Failed: + // fallback to shortest path + findShortestPath (srcIndex, destIndex); // A* found no path, try floyd pathfinder instead + + if (cv_debug.bool_ ()) { + logger.error ("A* Search for bot \"%s\" has failed. Falling back to shortest-path algorithm. Seems to be graph is broken.", pev->netname.chars ()); + } + break; + } +} diff --git a/src/planner.cpp b/src/planner.cpp new file mode 100644 index 0000000..c014bb2 --- /dev/null +++ b/src/planner.cpp @@ -0,0 +1,531 @@ +// +// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge. +// Copyright © 2004-2023 YaPB Project . +// +// SPDX-License-Identifier: MIT +// + +#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_dijkstra_simple_distance ("yb_path_dijkstra_simple_distance", "1", "Use simple distance path calculation instead of running full Dijkstra path cycle. Used only when Floyd matrices unavailable due to memory limit."); +ConVar cv_path_astar_post_smooth ("yb_path_astar_post_smooth", "1", "Enables post-smoothing for A*. Reduces zig-zags on paths at cost of some CPU cycles."); + +float Heuristic::gfunctionKillsDist (int team, int currentIndex, int parentIndex) { + if (parentIndex == kInvalidNodeIndex) { + return 0.0f; + } + auto cost = static_cast (practice.getDamage (team, currentIndex, currentIndex) + practice.getHighestDamageForTeam (team)); + 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)); + } + } + + if (current.flags & NodeFlag::Crouch) { + cost *= 1.5f; + } + return cost; +} + +float Heuristic::gfunctionKillsDistCTWithHostage (int team, int currentIndex, int parentIndex) { + const auto ¤t = graph[currentIndex]; + + if (current.flags & NodeFlag::NoHostage) { + return kInfiniteHeuristic; + } + else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) { + return gfunctionKillsDist (team, currentIndex, parentIndex) * 500.0f; + } + return gfunctionKillsDist (team, currentIndex, parentIndex); +} + +float Heuristic::gfunctionKills (int team, int currentIndex, int) { + auto cost = static_cast (practice.getDamage (team, currentIndex, currentIndex)); + 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)); + } + } + + if (current.flags & NodeFlag::Crouch) { + cost *= 1.5f; + } + return cost; +} + +auto Heuristic::gfunctionKillsCTWithHostage (int team, int currentIndex, int parentIndex) -> float { + if (parentIndex == kInvalidNodeIndex) { + return 0.0f; + } + const auto ¤t = graph[currentIndex]; + + if (current.flags & NodeFlag::NoHostage) { + return kInfiniteHeuristic; + } + else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) { + return gfunctionKills (team, currentIndex, parentIndex) * 500.0f; + } + return gfunctionKills (team, currentIndex, parentIndex); +} + +float Heuristic::gfunctionPathDist (int, int currentIndex, int parentIndex) { + if (parentIndex == kInvalidNodeIndex) { + return 0.0f; + } + + const auto &parent = graph[parentIndex]; + const auto ¤t = graph[currentIndex]; + + for (const auto &link : parent.links) { + if (link.index == currentIndex) { + const auto distance = static_cast (link.distance); + + // we don't like ladder or crouch point + if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) { + return distance * 1.5f; + } + return distance; + } + } + return kInfiniteHeuristic; +} + +float Heuristic::gfunctionPathDistWithHostage (int, int currentIndex, int parentIndex) { + const auto ¤t = graph[currentIndex]; + + if (current.flags & NodeFlag::NoHostage) { + return kInfiniteHeuristic; + } + else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) { + return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex) * 500.0f; + } + return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex); +} + +float Heuristic::hfunctionPathDist (int index, int, int goalIndex) { + const auto &start = graph[index]; + const auto &goal = graph[goalIndex]; + + const float x = start.origin.x - goal.origin.x; + const float y = start.origin.y - goal.origin.y; + const float z = start.origin.z - goal.origin.z; + + switch (cv_path_heuristic_mode.int_ ()) { + case 0: + return cr::max (cr::max (cr::abs (x), cr::abs (y)), cr::abs (z)); // chebyshev distance + + case 1: + return cr::abs (x) + cr::abs (y) + cr::abs (z); // manhattan distance + + case 2: + return 0.0f; // no heuristic + + case 3: { + const float dx = cr::abs (x); + const float dy = cr::abs (y); + const float dz = cr::abs (z); + + const float dmin = cr::min (cr::min (dx, dy), dz); + const float dmax = cr::max (cr::max (dx, dy), dz); + const float dmid = dx + dy + dz - dmin - dmax; + + const float d1 = 1.0f; + const float d2 = cr::sqrtf (2.0f); + const float d3 = cr::sqrtf (3.0f); + + return (d3 - d2) * dmin + (d2 - d1) * dmid + d1 * dmax; // diagonal distance + } + + default: + case 4: + return 10.0f * cr::sqrtf (cr::sqrf (x) + cr::sqrf (y) + cr::sqrf (z)); // euclidean distance + } +} + +float Heuristic::hfunctionPathDistWithHostage (int index, int, int goalIndex) { + if (graph[index].flags & NodeFlag::NoHostage) { + return kInfiniteHeuristic; + } + return hfunctionPathDist (index, kInvalidNodeIndex, goalIndex); +} + +float Heuristic::hfunctionNone (int index, int, int goalIndex) { + return hfunctionPathDist (index, kInvalidNodeIndex, goalIndex) / (128.0f * 10.0f); +} + +void AStarAlgo::clearRoute () { + m_routes.resize (static_cast (m_length)); + + for (const auto &path : graph) { + auto route = &m_routes[path.number]; + + route->g = route->f = 0.0f; + route->parent = kInvalidNodeIndex; + route->state = RouteState::New; + } + m_routes.clear (); +} + +bool AStarAlgo::cantSkipNode (const int a, const int b) { + const auto &ag = graph[a]; + const auto &bg = graph[b]; + + const bool notVisible = !vistab.visible (ag.number, bg.number); + + if (notVisible) { + return true; + } + const bool tooHigh = cr::abs (ag.origin.z - bg.origin.z) > 17.0f; + + if (tooHigh) { + return true; + } + const bool tooNarrow = (ag.flags | bg.flags) & NodeFlag::Narrow; + + if (tooNarrow) { + return true; + } + const bool tooFar = ag.origin.distanceSq (bg.origin) > cr::sqrf (300.0f); + + if (tooFar) { + return true; + } + bool hasJumps = false; + + for (int i = 0; i < kMaxNodeLinks; ++i) { + if ((ag.links[i].flags | bg.links[i].flags) & PathFlag::Jump) { + hasJumps = true; + break; + } + } + return hasJumps; +} + +void AStarAlgo::postSmooth (NodeAdderFn onAddedNode) { + int index = 0; + m_smoothedPath.emplace (m_constructedPath.first ()); + + for (size_t i = 1; i < m_constructedPath.length () - 1; ++i) { + if (cantSkipNode (m_smoothedPath[index], m_constructedPath[i + 1])) { + ++index; + m_smoothedPath.emplace (m_constructedPath[i]); + } + } + m_smoothedPath.emplace (m_constructedPath.last ()); + + for (const auto &spn : m_smoothedPath) { + onAddedNode (spn); + } + m_constructedPath.clear (); + m_smoothedPath.clear (); +} + +AStarResult AStarAlgo::find (int botTeam, int srcIndex, int destIndex, NodeAdderFn onAddedNode) { + if (m_length < kMaxNodeLinks) { + return AStarResult::InternalError; // astar needs some nodes to work with + } + + clearRoute (); + auto srcRoute = &m_routes[srcIndex]; + + // put start node into open list + srcRoute->g = m_gcalc (botTeam, srcIndex, kInvalidNodeIndex); + srcRoute->f = srcRoute->g + m_hcalc (srcIndex, kInvalidNodeIndex, destIndex); + srcRoute->state = RouteState::Open; + + m_routeQue.clear (); + m_routeQue.emplace (srcIndex, srcRoute->g); + + bool postSmoothPath = cv_path_astar_post_smooth.bool_ (); + + while (!m_routeQue.empty ()) { + // remove the first node from the open list + int currentIndex = m_routeQue.pop ().index; + + // safes us from bad graph... + if (m_routeQue.length () >= getMaxLength () - 1) { + return AStarResult::InternalError; + } + + // is the current node the goal node? + if (currentIndex == destIndex) { + // build the complete path + do { + if (postSmoothPath) { + m_constructedPath.emplace (currentIndex); + } + else { + onAddedNode (currentIndex); + } + currentIndex = m_routes[currentIndex].parent; + } while (currentIndex != kInvalidNodeIndex); + + // do a post-smooth if requested + if (postSmoothPath) { + postSmooth (onAddedNode); + } + return AStarResult::Success; + } + auto curRoute = &m_routes[currentIndex]; + + if (curRoute->state != RouteState::Open) { + continue; + } + + // put current node into CLOSED list + curRoute->state = RouteState::Closed; + + // now expand the current node + for (const auto &child : graph[currentIndex].links) { + if (child.index == kInvalidNodeIndex) { + continue; + } + auto childRoute = &m_routes[child.index]; + + // calculate the F value as F = G + H + const float g = curRoute->g + m_gcalc (botTeam, child.index, currentIndex); + const float h = m_hcalc (child.index, kInvalidNodeIndex, destIndex); + const float f = g + h; + + if (childRoute->state == RouteState::New || childRoute->f > f) { + // put the current child into open list + childRoute->parent = currentIndex; + childRoute->state = RouteState::Open; + + childRoute->g = g; + childRoute->f = f; + + m_routeQue.emplace (child.index, g); + } + } + } + return AStarResult::Failed; +} + +void FloydWarshallAlgo::rebuild () { + m_length = graph.length (); + m_matrix.resize (cr::sqrf (m_length)); + + auto matrix = m_matrix.data (); + + // re-initialize matrix every load + for (int i = 0; i < m_length; ++i) { + for (int j = 0; j < m_length; ++j) { + *(matrix + (i * m_length) + j) = { kInvalidNodeIndex, SHRT_MAX }; + } + } + + for (int i = 0; i < m_length; ++i) { + for (const auto &link : graph[i].links) { + if (!graph.exists (link.index)) { + continue; + } + *(matrix + (i * m_length) + link.index) = { link.index, link.distance }; + } + } + + for (int i = 0; i < m_length; ++i) { + (matrix + (i * m_length) + i)->dist = 0; + } + + for (int k = 0; k < m_length; ++k) { + for (int i = 0; i < m_length; ++i) { + for (int j = 0; j < m_length; ++j) { + int distance = (matrix + (i * m_length) + k)->dist + (matrix + (k * m_length) + j)->dist; + + if (distance < (matrix + (i * m_length) + j)->dist) { + *(matrix + (i * m_length) + j) = { (matrix + (i * m_length) + k)->index, distance }; + } + } + } + } + save (); // save path matrix to file for faster access +} + +bool FloydWarshallAlgo::load () { + m_length = graph.length (); + + if (!m_length) { + return false; + } + bool dataLoaded = bstor.load (m_matrix); + + // do not rebuild if loaded + if (dataLoaded) { + return true; + } + rebuild (); // rebuilds matrix + + return true; +} + +void FloydWarshallAlgo::save () { + if (!m_length) { + return; + } + bstor.save (m_matrix); +} + +bool FloydWarshallAlgo::find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance) { + onAddedNode (srcIndex); + + while (srcIndex != destIndex) { + srcIndex = (m_matrix.data () + (srcIndex * m_length) + destIndex)->index; + + if (srcIndex < 0) { + // only fill path distance on full path + if (pathDistance != nullptr) { + *pathDistance = dist (srcIndex, destIndex); + } + return true; + } + + if (!onAddedNode (srcIndex)) { + return true; + } + } + return false; +} + +void DijkstraAlgo::init (const int length) { + m_length = length; + + m_distance.resize (length); + m_parent.resize (length); +} + +void DijkstraAlgo::resetState () { + m_queue.clear (); + + m_parent.fill (kInvalidNodeIndex); + m_distance.fill (kInfiniteDistanceLong); +} + +bool DijkstraAlgo::find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance) { + resetState (); + + m_queue.emplace (0, srcIndex); + m_distance[srcIndex] = 0; + + while (!m_queue.empty ()) { + auto p = cr::move (m_queue.pop ()); + auto current = p.second; + + // finished search + if (current == destIndex) { + break; + } + + if (m_distance[current] != p.first) { + continue; + } + + for (const auto &link : graph[current].links) { + if (link.index != kInvalidNodeIndex) { + auto dlink = m_distance[current] + link.distance; + + if (dlink < m_distance[link.index]) { + m_distance[link.index] = dlink; + m_parent[link.index] = current; + + m_queue.emplace (m_distance[link.index], link.index); + } + } + } + } + static SmallArray pathInReverse; + pathInReverse.clear (); + + for (int i = destIndex; i != kInvalidNodeIndex; i = m_parent[i]) { + pathInReverse.emplace (i); + } + pathInReverse.reverse (); + + for (const auto &node : pathInReverse) { + if (!onAddedNode (node)) { + break; + } + } + + // always fill path distance if we're need to + if (pathDistance != nullptr) { + *pathDistance = m_distance[destIndex]; + } + return m_distance[destIndex] < kInfiniteDistanceLong; +} + +int DijkstraAlgo::dist (int srcIndex, int destIndex) { + int pathDistance = 0; + + find (srcIndex, destIndex, [&] (int) { + return true; + }, &pathDistance); + + return pathDistance; +} + +PathPlanner::PathPlanner () { + m_dijkstra = cr::makeUnique (); + m_floyd = cr::makeUnique (); + m_astar = cr::makeUnique (); +} + +void PathPlanner::init () { + const int length = graph.length (); + + const float limitInMb = cv_path_floyd_memory_limit.float_ (); + const float memoryUse = static_cast (sizeof (FloydWarshallAlgo::Matrix) * cr::sqrf (length) / 1024 / 1024); + + // if we're have too much memory for floyd matrices, planner will use dijkstra or uniform planner for other than pathfinding needs + if (memoryUse > limitInMb) { + m_memoryLimitHit = true; + } + m_dijkstra->init (length); + m_astar->init (length); + + // load (re-create) floyds, if we're not hitting memory limits + if (!m_memoryLimitHit) { + m_floyd->load (); + } +} + +bool PathPlanner::hasRealPathDistance () const { + return !m_memoryLimitHit || !cv_path_dijkstra_simple_distance.bool_ (); +} + +bool PathPlanner::find (int srcIndex, int destIndex, NodeAdderFn onAddedNode, int *pathDistance) { + if (!graph.exists (srcIndex) || !graph.exists (destIndex)) { + return false; + } + + // limit hit, use dijkstra + if (m_memoryLimitHit) { + return m_dijkstra->find (srcIndex, destIndex, onAddedNode, pathDistance); + } + return m_floyd->find (srcIndex, destIndex, onAddedNode, pathDistance);; +} + +int PathPlanner::dist (int srcIndex, int destIndex) { + if (!graph.exists (srcIndex) || !graph.exists (destIndex)) { + return kInfiniteDistanceLong; + } + + if (srcIndex == destIndex) { + return 1; + } + + // limit hit, use dijkstra + if (m_memoryLimitHit) { + if (cv_path_dijkstra_simple_distance.bool_ ()) { + return static_cast (graph[srcIndex].origin.distance2d (graph[destIndex].origin)); + } + return m_dijkstra->dist (srcIndex, destIndex); + } + return m_floyd->dist (srcIndex, destIndex); +} diff --git a/src/practice.cpp b/src/practice.cpp new file mode 100644 index 0000000..82593cc --- /dev/null +++ b/src/practice.cpp @@ -0,0 +1,157 @@ +// +// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge. +// Copyright © 2004-2023 YaPB Project . +// +// SPDX-License-Identifier: MIT +// + +#include + +int32_t BotPractice::getIndex (int32_t team, int32_t start, int32_t goal) { + if (!exists (team, start, goal)) { + return kInvalidNodeIndex; + } + return m_data[{start, goal, team}].index; +} + +void BotPractice::setIndex (int32_t team, int32_t start, int32_t goal, int32_t value) { + if (team != Team::Terrorist && team != Team::CT) { + return; + } + + // reliability check + if (!graph.exists (start) || !graph.exists (goal) || !graph.exists (value)) { + return; + } + m_data[{start, goal, team}].index = static_cast (value); +} + +int32_t BotPractice::getValue (int32_t team, int32_t start, int32_t goal) { + if (!exists (team, start, goal)) { + return 0; + } + return m_data[{start, goal, team}].value; +} + +void BotPractice::setValue (int32_t team, int32_t start, int32_t goal, int32_t value) { + if (team != Team::Terrorist && team != Team::CT) { + return; + } + + // reliability check + if (!graph.exists (start) || !graph.exists (goal)) { + return; + } + m_data[{start, goal, team}].value = static_cast (value); +} + +int32_t BotPractice::getDamage (int32_t team, int32_t start, int32_t goal) { + if (!exists (team, start, goal)) { + return 0; + } + return m_data[{start, goal, team}].damage; +} + +void BotPractice::setDamage (int32_t team, int32_t start, int32_t goal, int32_t value) { + if (team != Team::Terrorist && team != Team::CT) { + return; + } + + // reliability check + if (!graph.exists (start) || !graph.exists (goal)) { + return; + } + m_data[{start, goal, team}].damage = static_cast (value); +} + +void BotPractice::update () { + // this function called after each end of the round to update knowledge about most dangerous nodes for each team. + + auto graphLength = graph.length (); + + // no nodes, no practice used or nodes edited or being edited? + if (!graphLength || graph.hasChanged () || !vistab.isReady ()) { + return; // no action + } + auto adjustValues = false; + + // get the most dangerous node for this position for both teams + for (int team = Team::Terrorist; team < kGameTeamNum; ++team) { + auto bestIndex = kInvalidNodeIndex; // best index to store + + for (int i = 0; i < graphLength; ++i) { + auto maxDamage = 0; + bestIndex = kInvalidNodeIndex; + + for (int j = 0; j < graphLength; ++j) { + if (i == j || !vistab.visible (i, j) || !exists (team, i, j)) { + continue; + } + auto actDamage = getDamage (team, i, j); + + if (actDamage > maxDamage) { + maxDamage = actDamage; + bestIndex = j; + } + } + + if (maxDamage > PracticeLimit::Damage) { + adjustValues = true; + } + setIndex (team, i, i, bestIndex); + } + } + constexpr auto kFullDamageVal = static_cast (PracticeLimit::Damage); + constexpr auto kHalfDamageVal = static_cast (PracticeLimit::Damage / 2); + + // adjust values if overflow is about to happen + if (adjustValues) { + for (int team = Team::Terrorist; team < kGameTeamNum; ++team) { + for (int i = 0; i < graphLength; ++i) { + for (int j = 0; j < graphLength; ++j) { + if (i == j || !exists (team, i, j)) { + continue; + } + setDamage (team, i, j, cr::clamp (getDamage (team, i, j) - kHalfDamageVal, 0, kFullDamageVal)); + } + } + } + } + + for (int team = Team::Terrorist; team < kGameTeamNum; ++team) { + m_teamHighestDamage[team] = cr::clamp (m_teamHighestDamage[team] - kHalfDamageVal, 1, kFullDamageVal); + } +} + +void BotPractice::save () { + if (!graph.length () || graph.hasChanged ()) { + return; // no action + } + SmallArray data; + + // copy hash-map data to our vector + m_data.foreach ([&data] (const DangerStorage &ds, const PracticeData &pd) { + data.emplace (ds, pd); + }); + bstor.save (data); +} + +void BotPractice::load () { + if (!graph.length ()) { + return; // no action + } + SmallArray data; + m_data.clear (); + + bool dataLoaded = bstor.load (data); + + // copy back to hash table + if (dataLoaded) { + for (const auto &dsr : data) { + if (dsr.data.damage > 0 || dsr.data.index != kInvalidNodeIndex || dsr.data.value > 0) { + m_data.insert (dsr.danger, dsr.data); + } + } + } +} + diff --git a/src/storage.cpp b/src/storage.cpp new file mode 100644 index 0000000..ce4e099 --- /dev/null +++ b/src/storage.cpp @@ -0,0 +1,394 @@ +// +// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge. +// Copyright © 2004-2023 YaPB Project . +// +// SPDX-License-Identifier: MIT +// + +#include + +#if defined (BOT_STORAGE_EXPLICIT_INSTANTIATIONS) + +template bool BotStorage::load (SmallArray &data, ExtenHeader *exten, int32_t *outOptions) { + auto type = guessType (); + String filename = buildPath (storageToBotFile (type.option), true); + + extern ConVar cv_debug, cv_graph_url; + + // graphs can be downloaded... + auto isGraph = !!(type.option & StorageOption::Graph); + auto isDebug = cv_debug.bool_ (); + + MemFile file (filename); // open the file + data.clear (); + + // resize data to fit the stuff + auto resizeData = [&] (const size_t length) { + data.resize (length); // for non-graph data the graph should be already loaded + data.shrink (); // free up memory to minimum + + // ensure we're have enough memory to decompress the data + data.ensure (length + ULZ::Excess); + }; + + // if graph & attempted to load multiple times, bail out, we're failed + if (isGraph && ++m_retries > 2) { + resetRetries (); + + return error (isGraph, isDebug, file, "Unable to load %s (filename: '%s'). Download process has failed as well. No nodes has been found.", type.name, filename); + } + + // downloader for graph + auto download = [&] () -> bool { + if (!graph.canDownload ()) { + return false; + } + auto downloadAddress = cv_graph_url.str (); + + auto toDownload = buildPath (storageToBotFile (type.option), false); + auto fromDownload = strings.format ("http://%s/graph/%s.graph", downloadAddress, game.getMapName ()); + + // try to download + if (http.downloadFile (fromDownload, toDownload)) { + ctrl.msg ("%s file '%s' successfully downloaded. Processing...", type.name, filename); + return true; + } + else { + ctrl.msg ("Can't download '%s' from '%s' to '%s'... (%d).", filename, fromDownload, toDownload, http.getLastStatusCode ()); + } + return false; + }; + + // tries to reload or open pwf file + auto tryReload = [&] () -> bool { + file.close (); + + if (!isGraph) { + return false; + } + + if (download ()) { + return load (data, exten, outOptions); + } + + if (graph.convertOldFormat ()) { + return load (data, exten, outOptions); + } + return false; + }; + + // no open no fun + if (!file) { + if (tryReload ()) { + return true; + } + return error (isGraph, isDebug, file, "Unable to open %s file for reading (filename: '%s').", type.name, filename); + } + + // read the header + StorageHeader hdr {}; + file.read (&hdr, sizeof (StorageHeader)); + + // check the magic + if (hdr.magic != kStorageMagic && hdr.magic != kStorageMagicUB) { + if (tryReload ()) { + return true; + } + return error (isGraph, isDebug, file, "Unable to read magic of %s (filename: '%s').", type.name, filename); + } + + // check the path-numbers + if (!isGraph && hdr.length != graph.length ()) { + return error (isGraph, isDebug, file, "Damaged %s (filename: '%s'). Mismatch number of nodes (got: '%d', need: '%d').", type.name, filename, hdr.length, graph.length ()); + } + + // check the count + if (hdr.length == 0 || hdr.length > kMaxNodes || hdr.length < kMaxNodeLinks) { + if (tryReload ()) { + return true; + } + return error (isGraph, isDebug, file, "Damaged %s (filename: '%s'). Paths length is overflowed (got: '%d').", type.name, filename, hdr.length); + } + + // check the version + if (hdr.version > type.version && isGraph) { + ctrl.msg ("Graph version mismatch %s (filename: '%s'). Version number differs (got: '%d', need: '%d') Please, upgrade %s.", type.name, filename, hdr.version, type.version, product.name); + } + else if (hdr.version != type.version && !isGraph) { + return error (isGraph, isDebug, file, "Damaged %s (filename: '%s'). Version number differs (got: '%d', need: '%d').", type.name, filename, hdr.version, type.version); + } + + // save graph version + if (isGraph) { + graph.setGraphHeader (&hdr); + } + + // check the storage type + if ((hdr.options & type.option) != type.option) { + return error (isGraph, isDebug, file, "Incorrect storage format for %s (filename: '%s').", type.name, filename); + } + auto compressedSize = static_cast (hdr.compressed); + auto numberNodes = static_cast (hdr.length); + + SmallArray compressed (compressedSize + sizeof (uint8_t) * ULZ::Excess); + + // graph is not resized upon load + if (isGraph) { + resizeData (numberNodes); + } + else { + resizeData (hdr.uncompressed / sizeof (U)); + } + + // read compressed data + if (file.read (compressed.data (), sizeof (uint8_t), compressedSize) == compressedSize) { + + // try to uncompress + if (ulz.uncompress (compressed.data (), hdr.compressed, reinterpret_cast (data.data ()), hdr.uncompressed) == ULZ::UncompressFailure) { + return error (isGraph, isDebug, file, "Unable to decompress ULZ data for %s (filename: '%s').", type.name, filename); + } + else { + + if (outOptions) { + outOptions = &hdr.options; + } + + // author of graph.. save + if ((hdr.options & StorageOption::Exten) && exten != nullptr) { + auto extenSize = sizeof (ExtenHeader); + auto actuallyRead = file.read (exten, extenSize) * extenSize; + + if (isGraph) { + resetRetries (); + + ExtenHeader extenHeader; + strings.copy (extenHeader.author, exten->author, cr::bufsize (exten->author)); + + if (extenSize <= actuallyRead) { + // write modified by, only if the name is different + if (!strings.isEmpty (extenHeader.author) && strncmp (extenHeader.author, exten->modified, cr::bufsize (extenHeader.author)) != 0) { + strings.copy (extenHeader.modified, exten->modified, cr::bufsize (exten->modified)); + } + } + else { + strings.copy (extenHeader.modified, "(none)", cr::bufsize (exten->modified)); + } + extenHeader.mapSize = exten->mapSize; + + // tell graph about exten header + graph.setExtenHeader (&extenHeader); + } + } + ctrl.msg ("Loaded Bots %s data v%d (Memory: %.2fMB).", type.name, hdr.version, static_cast (data.capacity () * sizeof (U)) / 1024.0f / 1024.0f); + file.close (); + + return true; + } + } + else { + return error (isGraph, isDebug, file, "Unable to read ULZ data for %s (filename: '%s').", type.name, filename); + } + return false; +} + +template bool BotStorage::save (const SmallArray &data, ExtenHeader *exten, int32_t passOptions) { + auto type = guessType (); + + // append additional options + if (passOptions != 0) { + type.option |= passOptions; + } + auto isGraph = !!(type.option & StorageOption::Graph); + + // do not allow to save graph with less than 8 nodes + if (isGraph && graph.length () < kMaxNodeLinks) { + ctrl.msg ("Can't save graph data with less than %d nodes. Please add some more before saving.", kMaxNodeLinks); + return false; + } + String filename = buildPath (storageToBotFile (type.option)); + + if (data.empty ()) { + logger.error ("Unable to save %s file. Empty data. (filename: '%s').", type.name, filename); + return false; + } + else if (isGraph) { + for (auto &path : graph) { + path.display = 0.0f; + path.light = illum.getLightLevel (path.origin); + } + } + + // open the file + File file (filename, "wb"); + + // no open no fun + if (!file) { + logger.error ("Unable to open %s file for writing (filename: '%s').", type.name, filename); + return false; + } + auto rawLength = data.length () * sizeof (U); + SmallArray compressed (rawLength + sizeof (uint8_t) * ULZ::Excess); + + // try to compress + auto compressedLength = static_cast (ulz.compress (reinterpret_cast (data.data ()), static_cast (rawLength), reinterpret_cast (compressed.data ()))); + + if (compressedLength > 0) { + StorageHeader hdr {}; + + hdr.magic = kStorageMagic; + hdr.version = type.version; + hdr.options = type.option; + hdr.length = graph.length (); + hdr.compressed = static_cast (compressedLength); + hdr.uncompressed = static_cast (rawLength); + + file.write (&hdr, sizeof (StorageHeader)); + file.write (compressed.data (), sizeof (uint8_t), compressedLength); + + // add extension + if ((type.option & StorageOption::Exten) && exten != nullptr) { + file.write (exten, sizeof (ExtenHeader)); + } + extern ConVar cv_debug; + + // notify only about graph + if (isGraph || cv_debug.bool_ ()) { + ctrl.msg ("Successfully saved Bots %s data.", type.name); + } + } + else { + logger.error ("Unable to compress %s data (filename: '%s').", type.name, filename); + return false; + } + return true; +} + +template bool BotStorage::error (bool isGraph, bool isDebug, MemFile &file, const char *fmt, Args &&...args) { + auto result = strings.format (fmt, cr::forward (args)...); + + // display error only for graph file + if (isGraph || isDebug) { + logger.error (result); + } + + // if graph reset paths + if (isGraph) { + bots.kickEveryone (true); + graph.reset (); + } + file.close (); + + return false; +} + +template BotStorage::SaveLoadData BotStorage::guessType () { + if constexpr (cr::is_same ::value) { + return { "Pathmatrix", StorageOption::Matrix, StorageVersion::Matrix }; + } + else if constexpr (cr::is_same ::value) { + return { "Practice", StorageOption::Practice, StorageVersion::Practice }; + } + else if constexpr (cr::is_same ::value) { + return { "Vistable", StorageOption::Vistable, StorageVersion::Vistable }; + } + else if constexpr (cr::is_same ::value) { + return { "Graph", StorageOption::Graph, StorageVersion::Graph }; + } +} + +#else + +String BotStorage::buildPath (int32_t file, bool isMemoryLoad) { + using FilePath = Twin ; + + static HashMap paths = { + { BotFile::Vistable, FilePath ("train", "vis")}, + { BotFile::Practice, FilePath ("train", "prc")}, + { BotFile::Pathmatrix, FilePath ("train", "pmx")}, + { BotFile::LogFile, FilePath ("logs", "txt")}, + { BotFile::Graph, FilePath ("graph", "graph")}, + { BotFile::PodbotPWF, FilePath ("pwf", "pwf")}, + { BotFile::EbotEWP, FilePath ("ewp", "ewp")}, + }; + + static StringArray path; + path.clear (); + + // if not memory file we're don't need game dir + if (!isMemoryLoad) { + path.emplace (game.getRunningModName ()); + } + + // allways append addons/product + path.emplace ("addons"); + path.emplace (product.folder); + + // the datadir + path.emplace ("data"); + + // append real filepath + path.emplace (paths[file].first); + + // if file is logfile use correct logfile name with date + if (file == BotFile::LogFile) { + time_t ticks = time (&ticks); + tm timeinfo {}; + + plat.loctime (&timeinfo, &ticks); + auto timebuf = strings.chars (); + + strftime (timebuf, StringBuffer::StaticBufferSize, "L%d%m%Y", &timeinfo); + path.emplace (strings.format ("%s_%s.%s", product.folder, timebuf, paths[file].second)); + } + else { + String mapName = game.getMapName (); + path.emplace (strings.format ("%s.%s", mapName.lowercase (), paths[file].second)); + } + + // finally use correct path separarators for us + return String::join (path, PATH_SEP); +} + +int32_t BotStorage::storageToBotFile (int32_t options) { + // converts storage option to stroage filename + + if (options & StorageOption::Graph) { + return BotFile::Graph; + } + else if (options & StorageOption::Matrix) { + return BotFile::Pathmatrix; + } + else if (options & StorageOption::Vistable) { + return BotFile::Vistable; + } + else if (options & StorageOption::Practice) { + return BotFile::Practice; + } + return BotFile::Graph; +} + +void BotStorage::unlinkFromDisk () { + // this function removes graph file from the hard disk + + StringArray unlinkable; + bots.kickEveryone (true); + + // if we're delete graph, delete all corresponding to it files + unlinkable.emplace (buildPath (BotFile::Graph)); // graph itself + unlinkable.emplace (buildPath (BotFile::Practice)); // corresponding to practice + unlinkable.emplace (buildPath (BotFile::Vistable)); // corresponding to vistable + unlinkable.emplace (buildPath (BotFile::Pathmatrix)); // corresponding to matrix + + for (const auto &item : unlinkable) { + if (File::exists (item)) { + plat.removeFile (item.chars ()); + ctrl.msg ("File %s, has been deleted from the hard disk", item); + } + else { + logger.error ("Unable to open %s", item); + } + } + graph.reset (); // re-intialize points +} + +#endif // BOT_STORAGE_EXPLICIT_INSTANTIATIONS diff --git a/src/support.cpp b/src/support.cpp index bbd1215..132702a 100644 --- a/src/support.cpp +++ b/src/support.cpp @@ -538,75 +538,6 @@ String BotSupport::getCurrentDateTime () { return String (timebuf); } -String BotSupport::buildPath (int32_t file, bool isMemoryLoad) { - using FilePath = Twin ; - - static HashMap paths = { - { BotFile::Vistable, FilePath ("train", "vis")}, - { BotFile::Practice, FilePath ("train", "prc")}, - { BotFile::Pathmatrix, FilePath ("train", "pmx")}, - { BotFile::LogFile, FilePath ("logs", "txt")}, - { BotFile::Graph, FilePath ("graph", "graph")}, - { BotFile::PodbotPWF, FilePath ("pwf", "pwf")}, - { BotFile::EbotEWP, FilePath ("ewp", "ewp")}, - }; - - static StringArray path; - path.clear (); - - // if not memory file we're don't need game dir - if (!isMemoryLoad) { - path.emplace (game.getRunningModName ()); - } - - // allways append addons/product - path.emplace ("addons"); - path.emplace (product.folder); - - // the datadir - path.emplace ("data"); - - // append real filepath - path.emplace (paths[file].first); - - // if file is logfile use correct logfile name with date - if (file == BotFile::LogFile) { - time_t ticks = time (&ticks); - tm timeinfo {}; - - plat.loctime (&timeinfo, &ticks); - auto timebuf = strings.chars (); - - strftime (timebuf, StringBuffer::StaticBufferSize, "L%d%m%Y", &timeinfo); - path.emplace (strings.format ("%s_%s.%s", product.folder, timebuf, paths[file].second)); - } - else { - String mapName = game.getMapName (); - path.emplace (strings.format ("%s.%s", mapName.lowercase (), paths[file].second)); - } - - // finally use correct path separarators for us - return String::join (path, plat.win ? "\\" : "/"); -} - -// converts storage option to stroage filename - -int32_t BotSupport::storageToBotFile (StorageOption options) { - if (options & StorageOption::Graph) { - return BotFile::Graph; - } - else if (options & StorageOption::Matrix) { - return BotFile::Pathmatrix; - } - else if (options & StorageOption::Vistable) { - return BotFile::Vistable; - } - else if (options & StorageOption::Practice) { - return BotFile::Practice; - } - return BotFile::Graph; -} - int32_t BotSupport::sendTo (int socket, const void *message, size_t length, int flags, const sockaddr *dest, int destLength) { const auto send = [&] (const Twin &msg) -> int32_t { return Socket::sendto (socket, msg.first, msg.second, flags, dest, destLength); diff --git a/src/vistable.cpp b/src/vistable.cpp new file mode 100644 index 0000000..f6804f2 --- /dev/null +++ b/src/vistable.cpp @@ -0,0 +1,178 @@ +// +// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge. +// Copyright © 2004-2023 YaPB Project . +// +// SPDX-License-Identifier: MIT +// + +#include + + +void GraphVistable::rebuild () { + if (!m_rebuild) { + return; + } + m_length = graph.length (); + + TraceResult tr {}; + uint8_t res, shift; + + if (!graph.exists (m_sliceIndex)) { + m_sliceIndex = 0; + } + + if (!graph.exists (m_curIndex)) { + m_curIndex = 0; + } + const auto &vis = graph[m_curIndex]; + + auto sourceCrouch = vis.origin; + auto sourceStand = vis.origin; + + if (vis.flags & NodeFlag::Crouch) { + sourceCrouch.z += 12.0f; + sourceStand.z += 18.0f + 28.0f; + } + else { + sourceCrouch.z += -18.0f + 12.0f; + sourceStand.z += 28.0f; + } + auto end = m_sliceIndex + rg.get (250, 400); + + if (end > m_length) { + end = m_length; + } + uint16_t standCount = 0, crouchCount = 0; + + for (int i = m_sliceIndex; i < end; ++i) { + const auto &path = graph[i]; + + // first check ducked visibility + Vector dest = path.origin; + + game.testLine (sourceCrouch, dest, TraceIgnore::Monsters, nullptr, &tr); + + // check if line of sight to object is not blocked (i.e. visible) + if (!cr::fequal (tr.flFraction, 1.0f)) { + res = VisIndex::Stand; + } + else { + res = VisIndex::None; + } + res <<= 1; + + game.testLine (sourceStand, dest, TraceIgnore::Monsters, nullptr, &tr); + + // check if line of sight to object is not blocked (i.e. visible) + if (!cr::fequal (tr.flFraction, 1.0f)) { + res |= VisIndex::Stand; + } + + if (res != VisIndex::None) { + dest = path.origin; + + // first check ducked visibility + if (path.flags & NodeFlag::Crouch) { + dest.z += 18.0f + 28.0f; + } + else { + dest.z += 28.0f; + } + game.testLine (sourceCrouch, dest, TraceIgnore::Monsters, nullptr, &tr); + + // check if line of sight to object is not blocked (i.e. visible) + if (!cr::fequal (tr.flFraction, 1.0f)) { + res |= VisIndex::Crouch;; + } + else { + res &= VisIndex::Stand;; + } + game.testLine (sourceStand, dest, TraceIgnore::Monsters, nullptr, &tr); + + // check if line of sight to object is not blocked (i.e. visible) + if (!cr::fequal (tr.flFraction, 1.0f)) { + res |= VisIndex::Stand; + } + else { + res &= VisIndex::Crouch;; + } + } + shift = static_cast ((path.number % 4) << 1); + + m_vistable[vis.number * m_length + path.number] &= ~static_cast (VisIndex::Any << shift); + m_vistable[vis.number * m_length + path.number] |= res << shift; + + if (!(res & VisIndex::Crouch)) { + ++crouchCount; + } + + if (!(res & VisIndex::Stand)) { + ++standCount; + } + } + graph[vis.number].vis.crouch = crouchCount; + graph[vis.number].vis.stand = standCount; + + if (end == m_length) { + m_sliceIndex = 0; + m_curIndex++; + } + else { + m_sliceIndex += rg.get (250, 400); + } + + // notify host about rebuilding + if (m_notifyMsgTimestamp > 0.0f && m_notifyMsgTimestamp < game.time () && end == m_length) { + game.print ("Rebuilding vistable... %d%% done.", m_curIndex * 100 / m_length); + m_notifyMsgTimestamp = game.time () + 1.0f; + } + + if (m_curIndex == m_length && end == m_length) { + m_rebuild = false; + m_notifyMsgTimestamp = 0.0f; + + save (); + } +} + +void GraphVistable::startRebuild () { + m_rebuild = true; + m_notifyMsgTimestamp = game.time (); +} + +bool GraphVistable::visible (int srcIndex, int destIndex, VisIndex vis) { + if (!graph.exists (srcIndex) || !graph.exists (destIndex)) { + return false; + } + return !(((m_vistable[srcIndex * m_length + destIndex] >> ((destIndex % 4) << 1)) & vis) == vis); +} + +void GraphVistable::load () { + m_rebuild = true; + m_length = graph.length (); + + m_sliceIndex = 0; + m_curIndex = 0; + m_notifyMsgTimestamp = 0.0f; + + if (!m_length) { + return; + } + bool dataLoaded = bstor.load (m_vistable); + + // if loaded, do not recalculate visibility + if (dataLoaded) { + m_rebuild = false; + } + else { + m_vistable.resize (cr::sqrf (m_length)); + m_notifyMsgTimestamp = game.time (); + } +} + +void GraphVistable::save () { + if (!graph.length () || graph.hasChanged () || m_rebuild) { + return; + } + bstor.save (m_vistable); +} diff --git a/vc/yapb.vcxproj b/vc/yapb.vcxproj index a11ede2..2e0e29e 100644 --- a/vc/yapb.vcxproj +++ b/vc/yapb.vcxproj @@ -44,6 +44,7 @@ + @@ -51,13 +52,18 @@ + + + + + true true @@ -74,8 +80,12 @@ + + + + @@ -175,7 +185,7 @@ Default true false - Default + stdcpp17 EditAndContinue @@ -256,7 +266,7 @@ true false true - Default + stdcpp17 NDEBUG;%(PreprocessorDefinitions) diff --git a/vc/yapb.vcxproj.filters b/vc/yapb.vcxproj.filters index 27eadb9..be29e7f 100644 --- a/vc/yapb.vcxproj.filters +++ b/vc/yapb.vcxproj.filters @@ -156,6 +156,21 @@ inc + + inc + + + inc + + + inc + + + inc + + + inc + @@ -203,6 +218,21 @@ src + + src + + + src + + + src + + + src + + + src +