Compare commits

...

10 commits

Author SHA1 Message Date
95f1261dda Adjust Newbie aim spring to reduce snap headshots
Some checks are pending
build / bot-build (apple-x86) (push) Waiting to run
build / bot-build (linux-amd64) (push) Waiting to run
build / bot-build (linux-arm64) (push) Waiting to run
build / bot-build (linux-riscv64) (push) Waiting to run
build / bot-build (linux-x86) (push) Waiting to run
build / bot-build (linux-x86-gcc) (push) Waiting to run
build / bot-build (linux-x86-nosimd) (push) Waiting to run
build / bot-build (windows-amd64) (push) Waiting to run
build / bot-build (windows-x86-clang) (push) Waiting to run
build / bot-build (windows-x86-gcc) (push) Waiting to run
build / bot-msvc (windows-x86) (push) Waiting to run
build / bot-msvc (windows-x86-msvc-xp) (push) Waiting to run
build / bot-apple (apple-arm64) (push) Waiting to run
build / bot-continuous-release (push) Blocked by required conditions
build / bot-release (push) Blocked by required conditions
2025-12-27 18:25:40 -05:00
3ed7a71dd5 Fix excessive path penalties 2025-12-27 16:28:31 -05:00
jeefo
4015f8de06
mgr: allow to disable spawn count checks (ref #754) 2025-12-20 16:00:32 +03:00
jeefo
113cb5e916
bot: stop shoot breakable task in case enemy exists
manager: fix crash due miss-swap of max and min difficulty levels on bot adding (thx @stalin_alex)
2025-12-20 00:44:21 +03:00
jeefo
cd02ca3a23
fix: kllstreak counting (resolves #749) 2025-11-22 15:14:13 +03:00
jeefo
6decb2bfb3
ci: try to build for riscv64 2025-11-13 16:27:32 +03:00
jeefo
bc67453b6c
platform: added basic riscv support 2025-11-13 15:41:30 +03:00
jeefo
17ed252b60
nav: more fixes to ladder navigation
refactor: bot difficulty data and add graph refresh command
combat: fixes for smoke grenades (ref #743)
engine: fixes to spawn management (ref #744)
2025-11-12 21:31:23 +03:00
jeefo
7b378ba3fa
nav: various fixes to movement code
refactor: move some things into new game state class
2025-10-08 20:12:46 +03:00
jeefo
6604145481
fix: hide chatter icon even for players that changed team
fix: mark bot as finished buying on csdm spawn (ref #734)
fix: do not start any map analysis if already analyzing (ref #726)
combat: improved head/body aiming (ref #734)
2025-09-10 15:32:40 +03:00
40 changed files with 1519 additions and 1186 deletions

View file

@ -20,7 +20,7 @@ jobs:
strategy: strategy:
matrix: matrix:
arch: ['linux-x86', 'linux-amd64', 'linux-x86-gcc', 'linux-x86-nosimd', 'linux-arm64', 'apple-x86', 'windows-x86-clang', 'windows-amd64', 'windows-x86-gcc'] arch: ['linux-x86', 'linux-amd64', 'linux-x86-gcc', 'linux-x86-nosimd', 'linux-arm64', 'linux-riscv64', 'apple-x86', 'windows-x86-clang', 'windows-amd64', 'windows-x86-gcc']
fail-fast: false fail-fast: false
steps: steps:

View file

@ -69,3 +69,9 @@ EnableFakeBotFeatures = no
; sent to the server console. This replaces yb_logger_disable_logfile cvar. ; sent to the server console. This replaces yb_logger_disable_logfile cvar.
; ;
DisableLogFile = no DisableLogFile = no
;
; Disables enforcement of player spawn limits during bot creation and quota management,
; allowing the use of custom spawn editors.
;
DisableSpawnControl = no

@ -1 +1 @@
Subproject commit b5f7ccc23dec2d018048b40085f78255cc232e52 Subproject commit 173f387022310148e1f9ef65d61164579ba509a9

View file

@ -29,16 +29,6 @@ public:
// mostly config stuff, and some stuff dealing with menus // mostly config stuff, and some stuff dealing with menus
class BotConfig final : public Singleton <BotConfig> { class BotConfig final : public Singleton <BotConfig> {
public:
struct DifficultyData {
float reaction[2] {};
int32_t headshotPct {};
int32_t seenThruPct {};
int32_t hearThruPct {};
int32_t maxRecoil {};
Vector aimError {};
};
private: private:
Array <StringArray> m_chat {}; Array <StringArray> m_chat {};
Array <Array <ChatterItem>> m_chatter {}; Array <Array <ChatterItem>> m_chatter {};
@ -52,7 +42,7 @@ private:
StringArray m_avatars {}; StringArray m_avatars {};
HashMap <uint32_t, String, Hash <int32_t>> m_language {}; HashMap <uint32_t, String, Hash <int32_t>> m_language {};
HashMap <int32_t, DifficultyData> m_difficulty {}; HashMap <int32_t, BotDifficultyData> m_difficulty {};
HashMap <String, String> m_custom {}; HashMap <String, String> m_custom {};
// default tables for personality weapon preferences, overridden by weapon.cfg // default tables for personality weapon preferences, overridden by weapon.cfg
@ -218,10 +208,7 @@ public:
} }
// get's the difficulty level tweaks // get's the difficulty level tweaks
DifficultyData *getDifficultyTweaks (int32_t level) { BotDifficultyData *getDifficultyTweaks (int32_t level) {
if (level < Difficulty::Noob || level > Difficulty::Expert) {
return &m_difficulty[Difficulty::Expert];
}
return &m_difficulty[level]; return &m_difficulty[level];
} }

View file

@ -419,7 +419,13 @@ CR_DECLARE_SCOPED_ENUM (GoalTactic,
RescueHostage RescueHostage
) )
// some hard-coded desire defines used to override calculated ones // ladder move direction
CR_DECLARE_SCOPED_ENUM (LadderDir,
Up = 0,
Down,
)
// some hard-coded desire defines used to override calculated ones
namespace TaskPri { namespace TaskPri {
constexpr auto Normal { 35.0f }; constexpr auto Normal { 35.0f };
constexpr auto Pause { 36.0f }; constexpr auto Pause { 36.0f };
@ -447,7 +453,7 @@ constexpr auto kSprayDistanceX2 = kSprayDistance * 2;
constexpr auto kMaxChatterRepeatInterval = 99.0f; constexpr auto kMaxChatterRepeatInterval = 99.0f;
constexpr auto kViewFrameUpdate = 1.0f / 25.0f; constexpr auto kViewFrameUpdate = 1.0f / 25.0f;
constexpr auto kGrenadeDamageRadius = 385.0f; constexpr auto kGrenadeDamageRadius = 385.0f;
constexpr auto kMinMovedDistance = 3.0f; constexpr auto kMinMovedDistance = cr::sqrf (2.0f);
constexpr auto kInfiniteDistanceLong = static_cast <int> (kInfiniteDistance); constexpr auto kInfiniteDistanceLong = static_cast <int> (kInfiniteDistance);
constexpr auto kMaxWeapons = 32; constexpr auto kMaxWeapons = 32;

View file

@ -23,8 +23,8 @@ CR_DECLARE_SCOPED_ENUM (PrintQueueDestination,
// bot command manager // bot command manager
class BotControl final : public Singleton <BotControl> { class BotControl final : public Singleton <BotControl> {
public: public:
using Handler = int (BotControl::*) (); using Handler = int (BotControl:: *) ();
using MenuHandler = int (BotControl::*) (int); using MenuHandler = int (BotControl:: *) (int);
public: public:
// generic bot command // generic bot command
@ -36,8 +36,7 @@ public:
public: public:
explicit BotCmd () = default; explicit BotCmd () = default;
BotCmd (StringRef name, StringRef format, StringRef help, Handler handler, bool visible = true) : name (name), format (format), help (help), handler (cr::move (handler)), visible (visible) BotCmd (StringRef name, StringRef format, StringRef help, Handler handler, bool visible = true) : name (name), format (format), help (help), handler (cr::move (handler)), visible (visible) {}
{ }
}; };
// single bot menu // single bot menu
@ -47,8 +46,7 @@ public:
MenuHandler handler {}; MenuHandler handler {};
public: public:
explicit BotMenu (int ident, int slots, StringRef text, MenuHandler handler) : ident (ident), slots (slots), text (text), handler (cr::move (handler)) explicit BotMenu (int ident, int slots, StringRef text, MenuHandler handler) : ident (ident), slots (slots), text (text), handler (cr::move (handler)) {}
{ }
}; };
// queued text message to prevent overflow with rapid output // queued text message to prevent overflow with rapid output
@ -59,8 +57,7 @@ public:
public: public:
explicit PrintQueue () = default; explicit PrintQueue () = default;
PrintQueue (int32_t destination, StringRef text) : destination (destination), text (text) PrintQueue (int32_t destination, StringRef text) : destination (destination), text (text) {}
{ }
}; };
// save old values of changed cvars to revert them back when editing turned off // save old values of changed cvars to revert them back when editing turned off
@ -118,6 +115,7 @@ private:
int cmdNodeSave (); int cmdNodeSave ();
int cmdNodeLoad (); int cmdNodeLoad ();
int cmdNodeErase (); int cmdNodeErase ();
int cmdNodeRefresh ();
int cmdNodeEraseTraining (); int cmdNodeEraseTraining ();
int cmdNodeDelete (); int cmdNodeDelete ();
int cmdNodeCheck (); int cmdNodeCheck ();
@ -255,7 +253,7 @@ public:
} }
} }
edict_t *getIssuer() { edict_t *getIssuer () {
return m_ent; return m_ent;
} }

View file

@ -60,7 +60,7 @@ CR_DECLARE_SCOPED_ENUM (MapFlags,
Escape = cr::bit (3), Escape = cr::bit (3),
KnifeArena = cr::bit (4), KnifeArena = cr::bit (4),
FightYard = cr::bit (5), FightYard = cr::bit (5),
GrenadeWar = cr::bit(6), GrenadeWar = cr::bit (6),
HasDoors = cr::bit (10), // additional flags HasDoors = cr::bit (10), // additional flags
HasButtons = cr::bit (11) // map has buttons HasButtons = cr::bit (11) // map has buttons
) )
@ -190,6 +190,9 @@ public:
// initialize levels // initialize levels
void levelInitialize (edict_t *entities, int max); void levelInitialize (edict_t *entities, int max);
// when entity spawns
void onSpawnEntity (edict_t *ent);
// shutdown levels // shutdown levels
void levelShutdown (); void levelShutdown ();
@ -265,7 +268,7 @@ public:
void searchEntities (const Vector &position, float radius, EntitySearch functor) const; void searchEntities (const Vector &position, float radius, EntitySearch functor) const;
// check if map has entity // check if map has entity
bool hasEntityInGame (StringRef classname); bool hasEntityInGame (StringRef classname) const;
// print the version to server console on startup // print the version to server console on startup
void printBotVersion () const; void printBotVersion () const;
@ -282,6 +285,38 @@ public:
// is developer mode ? // is developer mode ?
bool isDeveloperMode () const; bool isDeveloperMode () const;
// entity utils
public:
// check if entity is alive
bool isAliveEntity (edict_t *ent) const;
// checks if entity is fakeclient
bool isFakeClientEntity (edict_t *ent) const;
// check if entity is a player
bool isPlayerEntity (edict_t *ent) const;
// check if entity is a monster
bool isMonsterEntity (edict_t *ent) const;
// check if entity is a item
bool isItemEntity (edict_t *ent) const;
// check if entity is a hostage entity
bool isHostageEntity (edict_t *ent) const;
// check if entity is a door entity
bool isDoorEntity (edict_t *ent) const;
// this function is checking that pointed by ent pointer obstacle, can be destroyed
bool isBreakableEntity (edict_t *ent, bool initialSeed = false) const;
// checks if same model omitting the models directory
bool isEntityModelMatches (const edict_t *ent, StringRef model) const;
// check if entity is a vip
bool isPlayerVIP (edict_t *ent) const;
// public inlines // public inlines
public: public:
// get the current time on server // get the current time on server
@ -301,7 +336,10 @@ public:
// gets custom engine args for client command // gets custom engine args for client command
const char *botArgs () const { const char *botArgs () const {
return strings.format (String::join (m_botArgs, " ", m_botArgs[0].startsWith ("say") ? 1 : 0).chars ()); auto result = strings.chars ();
strings.copy (result, String::join (m_botArgs, " ", m_botArgs[0].startsWith ("say") ? 1 : 0).chars (), cr::StringBuffer::StaticBufferSize);
return result;
} }
// gets custom engine argv for client command // gets custom engine argv for client command
@ -343,7 +381,7 @@ public:
} }
// get the wroldspawn entity // get the wroldspawn entity
edict_t *getStartEntity () { edict_t *getStartEntity () const {
return m_startEntity; return m_startEntity;
} }
@ -353,7 +391,7 @@ public:
} }
// gets the player team // gets the player team
int getTeam (edict_t *ent) const { int getPlayerTeam (edict_t *ent) const {
if (isNullEntity (ent)) { if (isNullEntity (ent)) {
return Team::Unassigned; return Team::Unassigned;
} }
@ -361,7 +399,7 @@ public:
} }
// gets the player team (real in ffa) // gets the player team (real in ffa)
int getRealTeam (edict_t *ent) const { int getRealPlayerTeam (edict_t *ent) const {
if (isNullEntity (ent)) { if (isNullEntity (ent)) {
return Team::Unassigned; return Team::Unassigned;
} }
@ -369,8 +407,8 @@ public:
} }
// get real gamedll team (matches gamedll indices) // get real gamedll team (matches gamedll indices)
int getGameTeam (edict_t *ent) const { int getPlayerTeamGame (edict_t *ent) const {
return getRealTeam (ent) + 1; return getRealPlayerTeam (ent) + 1;
} }
// sets the precache to uninitialized // sets the precache to uninitialized
@ -723,6 +761,98 @@ public:
} }
}; };
// offload bot manager class from things it shouldn't do
class GameState final : public Singleton <GameState> {
private:
bool m_bombPlanted {}; // is bomb planted ?
bool m_roundOver {}; // well, round is over>
bool m_resetHud {}; // reset HUD is called for some one
float m_timeBombPlanted {}; // time the bomb were planted
float m_timeRoundStart {}; // time round has started
float m_timeRoundEnd {}; // time round ended
float m_timeRoundMid {}; // middle point timestamp of a round
Vector m_bombOrigin {}; // stored bomb origin
Array <edict_t *> m_activeGrenades {}; // holds currently active grenades on the map
Array <edict_t *> m_interestingEntities {}; // holds currently interesting entities on the map
IntervalTimer m_interestingEntitiesUpdateTime {}; // time to update interesting entities
IntervalTimer m_activeGrenadesUpdateTime {}; // time to update active grenades
public:
GameState () = default;
~GameState () = default;
public:
const Vector &getBombOrigin () const {
return m_bombOrigin;
}
bool isBombPlanted () const {
return m_bombPlanted;
}
float getTimeBombPlanted () const {
return m_timeBombPlanted;
}
float getRoundStartTime () const {
return m_timeRoundStart;
}
float getRoundMidTime () const {
return m_timeRoundMid;
}
float getRoundEndTime () const {
return m_timeRoundEnd;
}
bool isRoundOver () const {
return m_roundOver;
}
bool isResetHUD () const {
return m_resetHud;
}
void setResetHUD (bool resetHud) {
m_resetHud = resetHud;
}
void setRoundOver (bool roundOver) {
m_roundOver = roundOver;
}
const Array <edict_t *> &getActiveGrenades () {
return m_activeGrenades;
}
const Array <edict_t *> &getInterestingEntities () {
return m_interestingEntities;
}
bool hasActiveGrenades () const {
return !m_activeGrenades.empty ();
}
bool hasInterestingEntities () const {
return !m_interestingEntities.empty ();
}
public:
float getBombTimeLeft () const;
void setBombPlanted (bool isPlanted);
void setBombOrigin (bool reset = false, const Vector &pos = nullptr);
void roundStart ();
void updateActiveGrenade ();
void updateInterestingEntities ();
};
// expose globals // expose globals
CR_EXPOSE_GLOBAL_SINGLETON (Game, game); CR_EXPOSE_GLOBAL_SINGLETON (Game, game);
CR_EXPOSE_GLOBAL_SINGLETON (GameState, gameState);
CR_EXPOSE_GLOBAL_SINGLETON (LightMeasure, illum); CR_EXPOSE_GLOBAL_SINGLETON (LightMeasure, illum);

View file

@ -173,7 +173,6 @@ private:
Vector m_learnVelocity {}; Vector m_learnVelocity {};
Vector m_learnPosition {}; Vector m_learnPosition {};
Vector m_bombOrigin {};
Vector m_lastNode {}; Vector m_lastNode {};
IntArray m_terrorPoints {}; IntArray m_terrorPoints {};
@ -228,6 +227,7 @@ public:
bool loadGraphData (); bool loadGraphData ();
bool canDownload (); bool canDownload ();
bool isAnalyzed () const; bool isAnalyzed () const;
bool isConverted () const;
void saveOldFormat (); void saveOldFormat ();
void reset (); void reset ();
@ -253,7 +253,6 @@ public:
void clearVisited (); void clearVisited ();
void eraseFromBucket (const Vector &pos, int index); void eraseFromBucket (const Vector &pos, int index);
void setBombOrigin (bool reset = false, const Vector &pos = nullptr);
void unassignPath (int from, int to); void unassignPath (int from, int to);
void convertFromPOD (Path &path, const PODPath &pod) const; void convertFromPOD (Path &path, const PODPath &pod) const;
void convertToPOD (const Path &path, PODPath &pod); void convertToPOD (const Path &path, PODPath &pod);
@ -292,10 +291,6 @@ public:
m_editFlags &= ~flag; m_editFlags &= ~flag;
} }
const Vector &getBombOrigin () const {
return m_bombOrigin;
}
// access paths // access paths
Path &operator [] (int index) { Path &operator [] (int index) {
return m_paths[index]; return m_paths[index];

View file

@ -24,32 +24,19 @@ public:
using UniqueBot = UniquePtr <Bot>; using UniqueBot = UniquePtr <Bot>;
private: private:
float m_timeRoundStart {}; // time round has started
float m_timeRoundEnd {}; // time round ended
float m_timeRoundMid {}; // middle point timestamp of a round
float m_difficultyBalanceTime {}; // time to balance difficulties ? float m_difficultyBalanceTime {}; // time to balance difficulties ?
float m_autoKillCheckTime {}; // time to kill all the bots ? float m_autoKillCheckTime {}; // time to kill all the bots ?
float m_maintainTime {}; // time to maintain bot creation float m_maintainTime {}; // time to maintain bot creation
float m_quotaMaintainTime {}; // time to maintain bot quota float m_quotaMaintainTime {}; // time to maintain bot quota
float m_grenadeUpdateTime {}; // time to update active grenades
float m_entityUpdateTime {}; // time to update interesting entities
float m_plantSearchUpdateTime {}; // time to update for searching planted bomb float m_plantSearchUpdateTime {}; // time to update for searching planted bomb
float m_lastChatTime {}; // global chat time timestamp float m_lastChatTime {}; // global chat time timestamp
float m_timeBombPlanted {}; // time the bomb were planted
int m_lastWinner {}; // the team who won previous round int m_lastWinner {}; // the team who won previous round
int m_lastDifficulty {}; // last bots difficulty int m_lastDifficulty {}; // last bots difficulty
int m_bombSayStatus {}; // some bot is issued whine about bomb int m_bombSayStatus {}; // some bot is issued whine about bomb
int m_numPreviousPlayers {}; // number of players in game im previous player check int m_numPreviousPlayers {}; // number of players in game im previous player check
bool m_bombPlanted {}; // is bomb planted ?
bool m_botsCanPause {}; // bots can do a little pause ? bool m_botsCanPause {}; // bots can do a little pause ?
bool m_roundOver {}; // well, round is over>
bool m_resetHud {}; // reset HUD is called for some one
Array <edict_t *> m_activeGrenades {}; // holds currently active grenades on the map
Array <edict_t *> m_interestingEntities {}; // holds currently interesting entities on the map
Deque <String> m_saveBotNames {}; // bots names that persist upon changelevel Deque <String> m_saveBotNames {}; // bots names that persist upon changelevel
Deque <BotRequest> m_addRequests {}; // bot creation tab Deque <BotRequest> m_addRequests {}; // bot creation tab
@ -81,10 +68,9 @@ public:
int getAliveHumansCount (); int getAliveHumansCount ();
int getPlayerPriority (edict_t *ent); int getPlayerPriority (edict_t *ent);
float getConnectTime (StringRef name, float original); float getConnectionTimes (StringRef name, float original);
float getAverageTeamKPD (bool calcForBots); float getAverageTeamKPD (bool calcForBots);
void setBombPlanted (bool isPlanted);
void frame (); void frame ();
void createKillerEntity (); void createKillerEntity ();
void destroyKillerEntity (); void destroyKillerEntity ();
@ -113,8 +99,6 @@ public:
void reset (); void reset ();
void initFilters (); void initFilters ();
void resetFilters (); void resetFilters ();
void updateActiveGrenade ();
void updateInterestingEntities ();
void captureChatRadio (StringRef cmd, StringRef arg, edict_t *ent); void captureChatRadio (StringRef cmd, StringRef arg, edict_t *ent);
void notifyBombDefuse (); void notifyBombDefuse ();
void execGameEntity (edict_t *ent); void execGameEntity (edict_t *ent);
@ -130,27 +114,10 @@ public:
bool kickRandom (bool decQuota = true, Team fromTeam = Team::Unassigned); bool kickRandom (bool decQuota = true, Team fromTeam = Team::Unassigned);
bool balancedKickRandom (bool decQuota); bool balancedKickRandom (bool decQuota);
bool hasCustomCSDMSpawnEntities (); bool hasCustomCSDMSpawnEntities ();
bool isLineBlockedBySmoke (const Vector &from, const Vector &to);
bool isFrameSkipDisabled (); bool isFrameSkipDisabled ();
public: public:
const Array <edict_t *> &getActiveGrenades () { bool getTeamEconomics (int team) const {
return m_activeGrenades;
}
const Array <edict_t *> &getInterestingEntities () {
return m_interestingEntities;
}
bool hasActiveGrenades () const {
return !m_activeGrenades.empty ();
}
bool hasInterestingEntities () const {
return !m_interestingEntities.empty ();
}
bool checkTeamEco (int team) const {
return m_teamData[team].positiveEco; return m_teamData[team].positiveEco;
} }
@ -171,30 +138,6 @@ public:
addbot ("", -1, -1, -1, -1, manual); addbot ("", -1, -1, -1, -1, manual);
} }
bool isBombPlanted () const {
return m_bombPlanted;
}
float getTimeBombPlanted () const {
return m_timeBombPlanted;
}
float getRoundStartTime () const {
return m_timeRoundStart;
}
float getRoundMidTime () const {
return m_timeRoundMid;
}
float getRoundEndTime () const {
return m_timeRoundEnd;
}
bool isRoundOver () const {
return m_roundOver;
}
bool canPause () const { bool canPause () const {
return m_botsCanPause; return m_botsCanPause;
} }
@ -236,10 +179,6 @@ public:
m_teamData[team].lastRadioSlot = radio; m_teamData[team].lastRadioSlot = radio;
} }
void setResetHUD (bool resetHud) {
m_resetHud = resetHud;
}
int getLastRadio (const int team) const { int getLastRadio (const int team) const {
return m_teamData[team].lastRadioSlot; return m_teamData[team].lastRadioSlot;
} }

View file

@ -69,7 +69,7 @@ CR_DECLARE_SCOPED_ENUM (StatusIconCache,
class MessageDispatcher final : public Singleton <MessageDispatcher> { class MessageDispatcher final : public Singleton <MessageDispatcher> {
private: private:
using MsgFunc = void (MessageDispatcher::*) (); using MsgFunc = void (MessageDispatcher:: *) ();
using MsgHash = Hash <int32_t>; using MsgHash = Hash <int32_t>;
private: private:
@ -81,9 +81,9 @@ private:
}; };
public: public:
Args (float value) : float_ (value) { } Args (float value) : float_ (value) {}
Args (int32_t value) : long_ (value) { } Args (int32_t value) : long_ (value) {}
Args (const char *value) : chars_ (value) { } Args (const char *value) : chars_ (value) {}
}; };
private: private:

View file

@ -21,10 +21,10 @@ CR_DECLARE_SCOPED_ENUM (AStarResult,
Success = 0, Success = 0,
Failed, Failed,
InternalError, InternalError,
) )
// node added // node added
using NodeAdderFn = const Lambda <bool (int)> &; using NodeAdderFn = const Lambda <bool (int)> &;
// route twin node // route twin node
template <typename HT> struct RouteTwin final { template <typename HT> struct RouteTwin final {

View file

@ -108,7 +108,7 @@ public:
void setDamage (int32_t team, int32_t start, int32_t goal, int32_t value); void setDamage (int32_t team, int32_t start, int32_t goal, int32_t value);
// interlocked get damage // interlocked get damage
float plannerGetDamage (int32_t team, int32_t start, int32_t goal, bool addTeamHighestDamage); float getDamageEx (int32_t team, int32_t start, int32_t goal, bool addTeamHighestDamage);
public: public:
void update (); void update ();

View file

@ -22,7 +22,8 @@ CR_DECLARE_SCOPED_ENUM (StorageOption,
Official = cr::bit (4), // this is additional flag for graph indicates graph are official 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 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 Exten = cr::bit (6), // this is additional flag indicates that there's extension info
Analyzed = cr::bit (7) // this graph has been analyzed Analyzed = cr::bit (7), // this graph has been analyzed
Converted = cr::bit (8) // converted from a pwf format
) )
// storage header versions // storage header versions

View file

@ -31,36 +31,6 @@ public:
// converts weapon id to alias name // converts weapon id to alias name
StringRef weaponIdToAlias (int32_t id); StringRef weaponIdToAlias (int32_t id);
// check if origin is visible from the entity side
bool isVisible (const Vector &origin, edict_t *ent);
// check if entity is alive
bool isAlive (edict_t *ent);
// checks if entity is fakeclient
bool isFakeClient (edict_t *ent);
// check if entity is a player
bool isPlayer (edict_t *ent);
// check if entity is a monster
bool isMonster (edict_t *ent);
// check if entity is a item
bool isItem (edict_t *ent);
// check if entity is a vip
bool isPlayerVIP (edict_t *ent);
// check if entity is a hostage entity
bool isHostageEntity (edict_t *ent);
// check if entity is a door entity
bool isDoorEntity (edict_t *ent);
// this function is checking that pointed by ent pointer obstacle, can be destroyed
bool isBreakableEntity (edict_t *ent, bool initialSeed = false);
// nearest player search helper // nearest player search helper
bool findNearestPlayer (void **holder, edict_t *to, float searchDistance = 4096.0, bool sameTeam = false, bool needBot = false, bool needAlive = false, bool needDrawn = false, bool needBotWithC4 = false); bool findNearestPlayer (void **holder, edict_t *to, float searchDistance = 4096.0, bool sameTeam = false, bool needBot = false, bool needAlive = false, bool needDrawn = false, bool needBotWithC4 = false);
@ -70,8 +40,8 @@ public:
// update stats on clients // update stats on clients
void updateClients (); void updateClients ();
// checks if same model omitting the models directory // check if origin is visible from the entity side
bool isModel (const edict_t *ent, StringRef model); bool isVisible (const Vector &origin, edict_t *ent);
// get the current date and time as string // get the current date and time as string
String getCurrentDateTime (); String getCurrentDateTime ();
@ -85,6 +55,9 @@ public:
// set custom cvar descriptions // set custom cvar descriptions
void setCustomCvarDescriptions (); void setCustomCvarDescriptions ();
// check if line of sight blocked by a smoke
bool isLineBlockedBySmoke (const Vector &from, const Vector &to);
public: public:
// re-show welcome after changelevel ? // re-show welcome after changelevel ?

View file

@ -39,7 +39,7 @@ public:
~GraphVistable () = default; ~GraphVistable () = default;
public: public:
bool visible (int srcIndex, int destIndex, VisIndex vis = VisIndex::Any); bool visible (int srcIndex, int destIndex, VisIndex vis = VisIndex::Any) const;
void load (); void load ();
void save () const; void save () const;
@ -54,6 +54,11 @@ public:
bool isReady () const { bool isReady () const {
return !m_rebuild; return !m_rebuild;
} }
// is visible fromr both points ?
bool visibleBothSides (int srcIndex, int destIndex, VisIndex vis = VisIndex::Any) const {
return visible (srcIndex, destIndex, vis) && visible (destIndex, srcIndex, vis);
}
}; };
// expose global // expose global

View file

@ -23,7 +23,7 @@ using namespace cr;
// tasks definition // tasks definition
struct BotTask { struct BotTask {
using Function = void (Bot::*) (); using Function = void (Bot:: *) ();
public: public:
Function func {}; // corresponding exec function in bot class Function func {}; // corresponding exec function in bot class
@ -34,7 +34,7 @@ public:
bool resume {}; // if task can be continued if interrupted bool resume {}; // if task can be continued if interrupted
public: public:
BotTask (Function func, Task id, float desire, int data, float time, bool resume) : func (func), id (id), desire (desire), data (data), time (time), resume (resume) { } BotTask (Function func, Task id, float desire, int data, float time, bool resume) : func (func), id (id), desire (desire), data (data), time (time), resume (resume) {}
}; };
// weapon properties structure // weapon properties structure
@ -84,8 +84,7 @@ public:
int type, int type,
bool fireHold) : id (id), name (name), model (model), price (price), minPrimaryAmmo (minPriAmmo), teamStandard (teamStd), bool fireHold) : id (id), name (name), model (model), price (price), minPrimaryAmmo (minPriAmmo), teamStandard (teamStd),
teamAS (teamAs), buyGroup (buyGroup), buySelect (buySelect), buySelectT (buySelectT), buySelectCT (buySelectCT), teamAS (teamAs), buyGroup (buyGroup), buySelect (buySelect), buySelectT (buySelectT), buySelectCT (buySelectCT),
penetratePower (penetratePower), maxClip (maxClip), type (type), primaryFireHold (fireHold) penetratePower (penetratePower), maxClip (maxClip), type (type), primaryFireHold (fireHold) {}
{ }
}; };
// clients noise // clients noise
@ -123,6 +122,16 @@ struct BotTeamData {
int32_t lastRadioSlot = { kInvalidRadioSlot }; // last radio message for team int32_t lastRadioSlot = { kInvalidRadioSlot }; // last radio message for team
}; };
// bot difficulty data
struct BotDifficultyData {
float reaction[2] {};
int32_t headshotPct {};
int32_t seenThruPct {};
int32_t hearThruPct {};
int32_t maxRecoil {};
Vector aimError {};
};
// include bot graph stuff // include bot graph stuff
#include <graph.h> #include <graph.h>
#include <vision.h> #include <vision.h>
@ -210,6 +219,8 @@ private:
mutable Mutex m_pathFindLock {}; mutable Mutex m_pathFindLock {};
mutable Mutex m_predictLock {}; mutable Mutex m_predictLock {};
float f_wpt_tim_str_chg;
private: private:
uint32_t m_states {}; // sensing bitstates uint32_t m_states {}; // sensing bitstates
uint32_t m_collideMoves[kMaxCollideMoves] {}; // sorted array of movements uint32_t m_collideMoves[kMaxCollideMoves] {}; // sorted array of movements
@ -240,11 +251,10 @@ private:
int m_lastPredictLength {}; // last predicted path length int m_lastPredictLength {}; // last predicted path length
int m_pickupType {}; // type of entity which needs to be used/picked up int m_pickupType {}; // type of entity which needs to be used/picked up
float m_headedTime {}; float m_headedTime {}; // last time followed by radio entity
float m_prevTime {}; // time previously checked movement speed float m_prevTime {}; // time previously checked movement speed
float m_heavyTimestamp {}; // is it time to execute heavy-weight functions float m_heavyTimestamp {}; // is it time to execute heavy-weight functions
float m_prevSpeed {}; // speed some frames before float m_prevSpeed {}; // speed some frames before
float m_prevVelocity {}; // velocity some frames before
float m_timeDoorOpen {}; // time to next door open check float m_timeDoorOpen {}; // time to next door open check
float m_timeHitDoor {}; // specific time after hitting the door float m_timeHitDoor {}; // specific time after hitting the door
float m_lastChatTime {}; // time bot last chatted float m_lastChatTime {}; // time bot last chatted
@ -361,7 +371,6 @@ private:
Vector m_lookAtPredict {}; // aiming vector when predicting Vector m_lookAtPredict {}; // aiming vector when predicting
Vector m_desiredVelocity {}; // desired velocity for jump nodes Vector m_desiredVelocity {}; // desired velocity for jump nodes
Vector m_breakableOrigin {}; // origin of breakable Vector m_breakableOrigin {}; // origin of breakable
Vector m_rightRef {}; // right referential vector
Vector m_checkFallPoint[2] {}; // check fall point Vector m_checkFallPoint[2] {}; // check fall point
Array <edict_t *> m_ignoredBreakable {}; // list of ignored breakables Array <edict_t *> m_ignoredBreakable {}; // list of ignored breakables
@ -370,6 +379,7 @@ private:
UniquePtr <class PlayerHitboxEnumerator> m_hitboxEnumerator {}; UniquePtr <class PlayerHitboxEnumerator> m_hitboxEnumerator {};
BotDifficultyData *m_difficultyData {};
Path *m_path {}; // pointer to the current path node Path *m_path {}; // pointer to the current path node
String m_chatBuffer {}; // space for strings (say text...) String m_chatBuffer {}; // space for strings (say text...)
Frustum::Planes m_viewFrustum {}; Frustum::Planes m_viewFrustum {};
@ -400,7 +410,7 @@ private:
int numEnemiesNear (const Vector &origin, const float radius) const; int numEnemiesNear (const Vector &origin, const float radius) const;
int numFriendsNear (const Vector &origin, const float radius) const; int numFriendsNear (const Vector &origin, const float radius) const;
float getBombTimeleft () const;
float getEstimatedNodeReachTime (); float getEstimatedNodeReachTime ();
float isInFOV (const Vector &dest) const; float isInFOV (const Vector &dest) const;
float getShiftSpeed (); float getShiftSpeed ();
@ -520,7 +530,7 @@ private:
void setPathOrigin (); void setPathOrigin ();
void fireWeapons (); void fireWeapons ();
void doFireWeapons (); void doFireWeapons ();
void selectWeapons (float distance, int index, int id, int choosen); void handleWeapons (float distance, int index, int id, int choosen);
void focusEnemy (); void focusEnemy ();
void selectBestWeapon (); void selectBestWeapon ();
void selectSecondary (); void selectSecondary ();
@ -529,7 +539,6 @@ private:
void syncUpdatePredictedIndex (); void syncUpdatePredictedIndex ();
void updatePredictedIndex (); void updatePredictedIndex ();
void refreshCreatureStatus (char *infobuffer); void refreshCreatureStatus (char *infobuffer);
void updateRightRef ();
void donateC4ToHuman (); void donateC4ToHuman ();
void clearAmmoInfo (); void clearAmmoInfo ();
void handleChatterTaskChange (Task tid); void handleChatterTaskChange (Task tid);
@ -681,6 +690,7 @@ public:
int m_ammoInClip[kMaxWeapons] {}; // ammo in clip for each weapons int m_ammoInClip[kMaxWeapons] {}; // ammo in clip for each weapons
int m_ammo[MAX_AMMO_SLOTS] {}; // total ammo amounts int m_ammo[MAX_AMMO_SLOTS] {}; // total ammo amounts
int m_deathCount {}; // number of bot deaths int m_deathCount {}; // number of bot deaths
int m_ladderDir {}; // ladder move direction
bool m_isVIP {}; // bot is vip? bool m_isVIP {}; // bot is vip?
bool m_isAlive {}; // has the player been killed or has he just respawned bool m_isAlive {}; // has the player been killed or has he just respawned
@ -700,7 +710,7 @@ public:
bool m_hasHostage {}; // does bot owns some hostages bool m_hasHostage {}; // does bot owns some hostages
bool m_hasProgressBar {}; // has progress bar on a HUD bool m_hasProgressBar {}; // has progress bar on a HUD
bool m_jumpReady {}; // is double jump ready bool m_jumpReady {}; // is double jump ready
bool m_canChooseAimDirection {}; // can choose aiming direction bool m_canSetAimDirection {}; // can choose aiming direction
bool m_isEnemyReachable {}; // direct line to enemy bool m_isEnemyReachable {}; // direct line to enemy
bool m_kickedByRotation {}; // is bot kicked due to rotation ? bool m_kickedByRotation {}; // is bot kicked due to rotation ?
bool m_kickMeFromServer {}; // kick the bot off the server? bool m_kickMeFromServer {}; // kick the bot off the server?
@ -768,6 +778,7 @@ public:
void startDoubleJump (edict_t *ent); void startDoubleJump (edict_t *ent);
void sendBotToOrigin (const Vector &origin); void sendBotToOrigin (const Vector &origin);
void markStale (); void markStale ();
void setNewDifficulty (int32_t newDifficulty);
bool hasHostage (); bool hasHostage ();
bool hasPrimaryWeapon () const; bool hasPrimaryWeapon () const;
bool hasSecondaryWeapon () const; bool hasSecondaryWeapon () const;
@ -942,6 +953,7 @@ extern ConVar cv_ignore_enemies_after_spawn_time;
extern ConVar cv_camping_time_min; extern ConVar cv_camping_time_min;
extern ConVar cv_camping_time_max; extern ConVar cv_camping_time_max;
extern ConVar cv_smoke_grenade_checks; extern ConVar cv_smoke_grenade_checks;
extern ConVar cv_smoke_greande_checks_radius;
extern ConVar cv_check_darkness; extern ConVar cv_check_darkness;
extern ConVar cv_use_hitbox_enemy_targeting; extern ConVar cv_use_hitbox_enemy_targeting;
extern ConVar cv_restricted_weapons; extern ConVar cv_restricted_weapons;

View file

@ -37,6 +37,7 @@ os = host_machine.system()
cpu = host_machine.cpu_family() cpu = host_machine.cpu_family()
cxx = compiler.get_id() cxx = compiler.get_id()
build_type = get_option ('buildtype') build_type = get_option ('buildtype')
cpu_non_x86 = cpu == 'arm' or cpu.startswith('ppc') or cpu.startswith('riscv')
opt_64bit = get_option('64bit') opt_64bit = get_option('64bit')
opt_native = get_option('native') opt_native = get_option('native')
@ -111,7 +112,7 @@ if cxx == 'clang' or cxx == 'gcc'
'-pthread' '-pthread'
] ]
if not opt_native and cpu != 'arm' and not cpu.startswith('ppc') if not opt_native and not cpu_non_x86
cxxflags += '-mtune=generic' cxxflags += '-mtune=generic'
endif endif
@ -119,7 +120,7 @@ if cxx == 'clang' or cxx == 'gcc'
cxxflags += [ cxxflags += [
'-march=armv8-a+fp+simd', '-march=armv8-a+fp+simd',
] ]
elif cpu != 'arm' and not cpu.startswith('ppc') elif not cpu_non_x86
if not opt_nosimd if not opt_nosimd
cxxflags += [ cxxflags += [
'-msse', '-msse2', '-msse3', '-msse3', '-mfpmath=sse' '-msse', '-msse2', '-msse3', '-msse3', '-mfpmath=sse'
@ -143,7 +144,7 @@ if cxx == 'clang' or cxx == 'gcc'
'-funroll-loops', '-fomit-frame-pointer', '-fno-stack-protector', '-fvisibility=hidden', '-fvisibility-inlines-hidden', '-fno-math-errno' '-funroll-loops', '-fomit-frame-pointer', '-fno-stack-protector', '-fvisibility=hidden', '-fvisibility-inlines-hidden', '-fno-math-errno'
] ]
if os != 'darwin' and os != 'windows' and cpu != 'aarch64' and cpu != 'arm' and not cpu.startswith('ppc') if os != 'darwin' and os != 'windows' and cpu != 'aarch64' and not cpu_non_x86
if not opt_static_linkent if not opt_static_linkent
cxxflags += [ cxxflags += [
'-fdata-sections', '-fdata-sections',
@ -203,7 +204,7 @@ if cxx == 'clang' or cxx == 'gcc'
endif endif
# by default we buid 32bit binaries # by default we buid 32bit binaries
if not opt_64bit and cpu != 'aarch64' and cpu != 'arm' and not cpu.startswith('ppc') if not opt_64bit and cpu != 'aarch64' and not cpu_non_x86
cxxflags += '-m32' cxxflags += '-m32'
ldflags += '-m32' ldflags += '-m32'
@ -319,6 +320,8 @@ target_name = meson.project_name()
# xash specific postfix for binaries # xash specific postfix for binaries
if cpu == 'aarch64' if cpu == 'aarch64'
target_name += '_arm64' target_name += '_arm64'
elif cpu.startswith('riscv')
target_name += '_riscv64d'
elif opt_64bit elif opt_64bit
target_name += '_amd64' target_name += '_amd64'
endif endif

View file

@ -135,6 +135,7 @@ class BotRelease(object):
self.pkg_matrix.append (BotPackage('extras', 'zip', self.pkg_matrix.append (BotPackage('extras', 'zip',
{'linux-arm64': 'so', {'linux-arm64': 'so',
'linux-amd64': 'so', 'linux-amd64': 'so',
'linux-riscv64': 'so',
'linux-x86-gcc': 'so', 'linux-x86-gcc': 'so',
'linux-x86-nosimd': 'so', 'linux-x86-nosimd': 'so',
'windows-x86-gcc': 'dll', 'windows-x86-gcc': 'dll',
@ -263,6 +264,8 @@ class BotRelease(object):
binary_name = binary_name + '_arm64' binary_name = binary_name + '_arm64'
elif artifact.endswith('amd64'): elif artifact.endswith('amd64'):
binary_name = binary_name + '_amd64' binary_name = binary_name + '_amd64'
elif artifact.endswith('riscv64'):
binary_name = binary_name + '_riscv64d'
binary = os.path.join(self.artifacts, artifact, f'{binary_name}.{pkg.artifact[artifact]}') binary = os.path.join(self.artifacts, artifact, f'{binary_name}.{pkg.artifact[artifact]}')
binary_base = os.path.basename(binary) binary_base = os.path.basename(binary)

View file

@ -17,6 +17,10 @@ ConVar cv_graph_analyze_optimize_nodes_on_finish ("graph_analyze_optimize_nodes_
ConVar cv_graph_analyze_mark_goals_on_finish ("graph_analyze_mark_goals_on_finish", "1", "Specifies if the analyzer should mark nodes as map goals automatically upon finishing."); ConVar cv_graph_analyze_mark_goals_on_finish ("graph_analyze_mark_goals_on_finish", "1", "Specifies if the analyzer should mark nodes as map goals automatically upon finishing.");
void GraphAnalyze::start () { void GraphAnalyze::start () {
if (m_isAnalyzing) {
return;
}
// start analyzer in few seconds after level initialized // start analyzer in few seconds after level initialized
if (cv_graph_analyze_auto_start) { if (cv_graph_analyze_auto_start) {
m_updateInterval = game.time () + 3.0f; m_updateInterval = game.time () + 3.0f;
@ -126,7 +130,7 @@ void GraphAnalyze::update () {
} }
void GraphAnalyze::suspend () { void GraphAnalyze::suspend () {
m_updateInterval = 0.0f; m_updateInterval = kInfiniteDistance;
m_isAnalyzing = false; m_isAnalyzing = false;
m_isAnalyzed = false; m_isAnalyzed = false;
m_basicsCreated = false; m_basicsCreated = false;
@ -307,7 +311,7 @@ void GraphAnalyze::flood (const Vector &pos, const Vector &next, float range) {
game.testHull (pos, { next.x, next.y, next.z + 19.0f }, TraceIgnore::Monsters, head_hull, nullptr, &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 // we're can't reach next point
if (!cr::fequal (tr.flFraction, 1.0f) && !util.isBreakableEntity (tr.pHit)) { if (!cr::fequal (tr.flFraction, 1.0f) && !game.isBreakableEntity (tr.pHit)) {
return; return;
} }
@ -321,7 +325,7 @@ void GraphAnalyze::flood (const Vector &pos, const Vector &next, float range) {
if (cr::fequal (tr.flFraction, 1.0f)) { if (cr::fequal (tr.flFraction, 1.0f)) {
return; return;
} }
Vector nextPos = { tr.vecEndPos.x, tr.vecEndPos.y, tr.vecEndPos.z + 19.0f }; const Vector &nextPos = { tr.vecEndPos.x, tr.vecEndPos.y, tr.vecEndPos.z + 19.0f };
const int endIndex = graph.getForAnalyzer (nextPos, range); const int endIndex = graph.getForAnalyzer (nextPos, range);
const int targetIndex = graph.getNearestNoBuckets (nextPos, 250.0f); const int targetIndex = graph.getNearestNoBuckets (nextPos, 250.0f);
@ -329,7 +333,7 @@ void GraphAnalyze::flood (const Vector &pos, const Vector &next, float range) {
if (graph.exists (endIndex) || !graph.exists (targetIndex)) { if (graph.exists (endIndex) || !graph.exists (targetIndex)) {
return; return;
} }
auto targetPos = graph[targetIndex].origin; const auto &targetPos = graph[targetIndex].origin;
// re-check there's nothing nearby, and add something we're want // re-check there's nothing nearby, and add something we're want
if (!graph.exists (graph.getNearestNoBuckets (nextPos, range))) { if (!graph.exists (graph.getNearestNoBuckets (nextPos, range))) {
@ -344,6 +348,7 @@ void GraphAnalyze::flood (const Vector &pos, const Vector &next, float range) {
if ((graph.isNodeReacheable (targetPos, testPos) if ((graph.isNodeReacheable (targetPos, testPos)
&& graph.isNodeReacheable (testPos, targetPos)) || (graph.isNodeReacheableWithJump (testPos, targetPos) && graph.isNodeReacheable (testPos, targetPos)) || (graph.isNodeReacheableWithJump (testPos, targetPos)
&& graph.isNodeReacheableWithJump (targetPos, testPos))) { && graph.isNodeReacheableWithJump (targetPos, testPos))) {
graph.add (NodeAddFlag::Normal, m_isCrouch ? Vector { nextPos.x, nextPos.y, nextPos.z - 9.0f } : nextPos); graph.add (NodeAddFlag::Normal, m_isCrouch ? Vector { nextPos.x, nextPos.y, nextPos.z - 9.0f } : nextPos);
} }
} }

View file

@ -37,7 +37,6 @@ ConVar cv_pickup_custom_items ("pickup_custom_items", "0", "Allows or disallows
ConVar cv_pickup_ammo_and_kits ("pickup_ammo_and_kits", "0", "Allows bots to pick up mod items like ammo, health kits, and suits."); ConVar cv_pickup_ammo_and_kits ("pickup_ammo_and_kits", "0", "Allows bots to pick up mod items like ammo, health kits, and suits.");
ConVar cv_pickup_best ("pickup_best", "1", "Allows or disallows bots to pick up the best weapons."); ConVar cv_pickup_best ("pickup_best", "1", "Allows or disallows bots to pick up the best weapons.");
ConVar cv_ignore_objectives ("ignore_objectives", "0", "Allows or disallows bots to do map objectives, i.e. plant/defuse bombs, and save hostages."); ConVar cv_ignore_objectives ("ignore_objectives", "0", "Allows or disallows bots to do map objectives, i.e. plant/defuse bombs, and save hostages.");
ConVar cv_smoke_grenade_checks ("smoke_grenade_checks", "2", "Affects the bot's vision by smoke clouds.", true, 0.0f, 2.0f);
// game console variables // game console variables
ConVar mp_c4timer ("mp_c4timer", nullptr, Var::GameRef); ConVar mp_c4timer ("mp_c4timer", nullptr, Var::GameRef);
@ -78,10 +77,10 @@ void Bot::avoidGrenades () {
m_needAvoidGrenade = 0; m_needAvoidGrenade = 0;
} }
if (!bots.hasActiveGrenades ()) { if (!gameState.hasActiveGrenades ()) {
return; return;
} }
const auto &activeGrenades = bots.getActiveGrenades (); const auto &activeGrenades = gameState.getActiveGrenades ();
// find all grenades on the map // find all grenades on the map
for (const auto &pent : activeGrenades) { for (const auto &pent : activeGrenades) {
@ -100,12 +99,12 @@ void Bot::avoidGrenades () {
if (!(m_states & Sense::SeeingEnemy)) { if (!(m_states & Sense::SeeingEnemy)) {
m_lookAt.y = cr::wrapAngle ((game.getEntityOrigin (pent) - getEyesPos ()).angles ().y + 180.0f); m_lookAt.y = cr::wrapAngle ((game.getEntityOrigin (pent) - getEyesPos ()).angles ().y + 180.0f);
m_canChooseAimDirection = false; m_canSetAimDirection = false;
m_preventFlashing = game.time () + rg (1.0f, 2.0f); m_preventFlashing = game.time () + rg (1.0f, 2.0f);
} }
} }
else if (game.isNullEntity (m_avoidGrenade) && model == kExplosiveModelName) { else if (game.isNullEntity (m_avoidGrenade) && model == kExplosiveModelName) {
if (game.getTeam (pent->v.owner) == m_team || pent->v.owner == ent ()) { if (game.getPlayerTeam (pent->v.owner) == m_team || pent->v.owner == ent ()) {
continue; continue;
} }
@ -155,6 +154,12 @@ void Bot::checkBreakable (edict_t *touch) {
if (!game.hasBreakables ()) { if (!game.hasBreakables ()) {
return; return;
} }
const bool hasEnemy = !game.isNullEntity (m_enemy);
// do n ot track for breakables if has some enemies
if (hasEnemy) {
return;
}
if (game.isNullEntity (touch)) { if (game.isNullEntity (touch)) {
auto breakable = lookupBreakable (); auto breakable = lookupBreakable ();
@ -187,6 +192,7 @@ void Bot::checkBreakablesAround () {
|| !game.hasBreakables () || !game.hasBreakables ()
|| m_seeEnemyTime + 4.0f > game.time () || m_seeEnemyTime + 4.0f > game.time ()
|| !game.isNullEntity (m_enemy) || !game.isNullEntity (m_enemy)
|| (m_aimFlags & (AimFlags::PredictPath | AimFlags::Danger))
|| !hasPrimaryWeapon ()) { || !hasPrimaryWeapon ()) {
return; return;
} }
@ -209,7 +215,7 @@ void Bot::checkBreakablesAround () {
continue; continue;
} }
if (!util.isBreakableEntity (breakable)) { if (!game.isBreakableEntity (breakable)) {
continue; continue;
} }
@ -257,7 +263,7 @@ edict_t *Bot::lookupBreakable () {
// this function checks if bot is blocked by a shoot able breakable in his moving direction // this function checks if bot is blocked by a shoot able breakable in his moving direction
// we're got something already // we're got something already
if (util.isBreakableEntity (m_breakableEntity)) { if (game.isBreakableEntity (m_breakableEntity)) {
return m_breakableEntity; return m_breakableEntity;
} }
const float detectBreakableDistance = (usesKnife () || isOnLadder ()) ? 32.0f : rg (72.0f, 256.0f); const float detectBreakableDistance = (usesKnife () || isOnLadder ()) ? 32.0f : rg (72.0f, 256.0f);
@ -270,7 +276,7 @@ edict_t *Bot::lookupBreakable () {
auto hit = tr.pHit; auto hit = tr.pHit;
// check if this isn't a triggered (bomb) breakable and if it takes damage. if true, shoot the crap! // check if this isn't a triggered (bomb) breakable and if it takes damage. if true, shoot the crap!
if (util.isBreakableEntity (hit)) { if (game.isBreakableEntity (hit)) {
m_breakableOrigin = game.getEntityOrigin (hit); m_breakableOrigin = game.getEntityOrigin (hit);
m_breakableEntity = hit; m_breakableEntity = hit;
@ -286,7 +292,7 @@ edict_t *Bot::lookupBreakable () {
} }
// check breakable team, needed for some plugins // check breakable team, needed for some plugins
if (ent->v.team > 0 && ent->v.team != game.getGameTeam (this->ent ())) { if (ent->v.team > 0 && ent->v.team != game.getPlayerTeamGame (this->ent ())) {
return false; return false;
} }
@ -320,15 +326,14 @@ void Bot::setIdealReactionTimers (bool actual) {
return; // zero out reaction times for extreme mode return; // zero out reaction times for extreme mode
} }
const auto tweak = conf.getDifficultyTweaks (m_difficulty);
if (actual) { if (actual) {
m_idealReactionTime = tweak->reaction[0]; m_idealReactionTime = m_difficultyData->reaction[0];
m_actualReactionTime = tweak->reaction[0]; m_actualReactionTime = m_difficultyData->reaction[0];
return; return;
} }
m_idealReactionTime = rg (tweak->reaction[0], tweak->reaction[1]); m_idealReactionTime = rg (m_difficultyData->reaction[0], m_difficultyData->reaction[1]);
} }
void Bot::updatePickups () { void Bot::updatePickups () {
@ -362,7 +367,7 @@ void Bot::updatePickups () {
} }
// no interesting entities, how ? // no interesting entities, how ?
else if (!bots.hasInterestingEntities ()) { else if (!gameState.hasInterestingEntities ()) {
return true; return true;
} }
return false; return false;
@ -376,7 +381,7 @@ void Bot::updatePickups () {
return; return;
} }
const auto &interesting = bots.getInterestingEntities (); const auto &interesting = gameState.getInterestingEntities ();
const float radiusSq = cr::sqrf (cv_object_pickup_radius.as <float> ()); const float radiusSq = cr::sqrf (cv_object_pickup_radius.as <float> ());
if (!game.isNullEntity (m_pickupItem)) { if (!game.isNullEntity (m_pickupItem)) {
@ -446,7 +451,7 @@ void Bot::updatePickups () {
const bool isHostageRescueMap = game.mapIs (MapFlags::HostageRescue); const bool isHostageRescueMap = game.mapIs (MapFlags::HostageRescue);
const bool isCSDM = game.is (GameFlags::CSDM); const bool isCSDM = game.is (GameFlags::CSDM);
if (isHostageRescueMap && util.isHostageEntity (ent)) { if (isHostageRescueMap && game.isHostageEntity (ent)) {
allowPickup = true; allowPickup = true;
pickupType = Pickup::Hostage; pickupType = Pickup::Hostage;
} }
@ -551,7 +556,7 @@ void Bot::updatePickups () {
allowPickup = true; allowPickup = true;
pickupType = Pickup::PlantedC4; pickupType = Pickup::PlantedC4;
} }
else if (cv_pickup_custom_items && util.isItem (ent) && !classname.startsWith ("item_thighpack")) { else if (cv_pickup_custom_items && game.isItemEntity (ent) && !classname.startsWith ("item_thighpack")) {
allowPickup = true; allowPickup = true;
pickupType = Pickup::Items; pickupType = Pickup::Items;
} }
@ -632,7 +637,7 @@ void Bot::updatePickups () {
const auto &path = graph[index]; const auto &path = graph[index];
const float bombTimer = mp_c4timer.as <float> (); const float bombTimer = mp_c4timer.as <float> ();
const float timeMidBlowup = bots.getTimeBombPlanted () + (bombTimer * 0.5f + bombTimer * 0.25f) - graph.calculateTravelTime (pev->maxspeed, pev->origin, path.origin); const float timeMidBlowup = gameState.getTimeBombPlanted () + (bombTimer * 0.5f + bombTimer * 0.25f) - graph.calculateTravelTime (pev->maxspeed, pev->origin, path.origin);
if (timeMidBlowup > game.time ()) { if (timeMidBlowup > game.time ()) {
clearTask (Task::MoveToPosition); // remove any move tasks clearTask (Task::MoveToPosition); // remove any move tasks
@ -683,7 +688,7 @@ void Bot::updatePickups () {
} }
} }
else if (pickupType == Pickup::PlantedC4) { else if (pickupType == Pickup::PlantedC4) {
if (util.isAlive (m_enemy)) { if (game.isAliveEntity (m_enemy)) {
return; return;
} }
@ -706,7 +711,7 @@ void Bot::updatePickups () {
const int index = findDefendNode (origin); const int index = findDefendNode (origin);
const auto &path = graph[index]; const auto &path = graph[index];
const float timeToExplode = bots.getTimeBombPlanted () + mp_c4timer.as <float> () - graph.calculateTravelTime (pev->maxspeed, pev->origin, path.origin); const float timeToExplode = gameState.getTimeBombPlanted () + mp_c4timer.as <float> () - graph.calculateTravelTime (pev->maxspeed, pev->origin, path.origin);
clearTask (Task::MoveToPosition); // remove any move tasks clearTask (Task::MoveToPosition); // remove any move tasks
@ -881,7 +886,9 @@ void Bot::showChatterIcon (bool show, bool disconnect) const {
// do not respect timers while disconnecting bot // do not respect timers while disconnecting bot
for (auto &client : util.getClients ()) { for (auto &client : util.getClients ()) {
if (!(client.flags & ClientFlags::Used) || (client.ent->v.flags & FL_FAKECLIENT) || client.team != m_team) { if (!(client.flags & ClientFlags::Used)
|| (client.ent->v.flags & FL_FAKECLIENT)
|| (client.team != m_team && !disconnect)) {
continue; continue;
} }
@ -891,7 +898,7 @@ void Bot::showChatterIcon (bool show, bool disconnect) const {
} }
// do not respect timers while disconnecting bot // do not respect timers while disconnecting bot
if (!show && (client.iconFlags[ownIndex] & ClientFlags::Icon) && (disconnect || client.iconTimestamp[ownIndex] < game.time ())) { if (!show && (disconnect || (client.iconFlags[ownIndex] & ClientFlags::Icon)) && (disconnect || client.iconTimestamp[ownIndex] < game.time ())) {
sendBotVoice (false, client.ent, entindex ()); sendBotVoice (false, client.ent, entindex ());
client.iconTimestamp[ownIndex] = 0.0f; client.iconTimestamp[ownIndex] = 0.0f;
@ -1292,7 +1299,7 @@ void Bot::buyStuff () {
const auto tab = conf.getRawWeapons (); const auto tab = conf.getRawWeapons ();
const bool isPistolMode = tab[25].teamStandard == -1 && tab[3].teamStandard == 2; const bool isPistolMode = tab[25].teamStandard == -1 && tab[3].teamStandard == 2;
const bool teamHasGoodEconomics = bots.checkTeamEco (m_team); const bool teamHasGoodEconomics = bots.getTeamEconomics (m_team);
// do this, because xash engine is not capable to run all the features goldsrc, but we have cs 1.6 on it, so buy table must be the same // do this, because xash engine is not capable to run all the features goldsrc, but we have cs 1.6 on it, so buy table must be the same
const bool isOldGame = game.is (GameFlags::Legacy); const bool isOldGame = game.is (GameFlags::Legacy);
@ -1642,47 +1649,30 @@ void Bot::buyStuff () {
} }
void Bot::updateEmotions () { void Bot::updateEmotions () {
// slowly increase/decrease dynamic emotions back to their base level constexpr auto kEmotionUpdateStep = 0.05f;
if (m_nextEmotionUpdate > game.time ()) { if (m_nextEmotionUpdate > game.time ()) {
return; return;
} }
if (m_seeEnemyTime + 1.0f > game.time ()) { if (m_seeEnemyTime + 1.0f > game.time ()) {
m_agressionLevel += 0.05f; m_agressionLevel = cr::min (m_agressionLevel + kEmotionUpdateStep, 1.0f);
if (m_agressionLevel > 1.0f) {
m_agressionLevel = 1.0f;
}
} }
else if (m_seeEnemyTime + 5.0f < game.time ()) { else if (m_seeEnemyTime + 5.0f < game.time ()) {
// smoothly return aggression to base level
if (m_agressionLevel > m_baseAgressionLevel) { if (m_agressionLevel > m_baseAgressionLevel) {
m_agressionLevel -= 0.05f; m_agressionLevel = cr::max (m_agressionLevel - kEmotionUpdateStep, m_baseAgressionLevel);
} }
else { else {
m_agressionLevel += 0.05f; m_agressionLevel = cr::min (m_agressionLevel + kEmotionUpdateStep, m_baseAgressionLevel);
} }
// smoothly return fear to base level
if (m_fearLevel > m_baseFearLevel) { if (m_fearLevel > m_baseFearLevel) {
m_fearLevel -= 0.05f; m_fearLevel = cr::max (m_fearLevel - kEmotionUpdateStep, m_baseFearLevel);
} }
else { else {
m_fearLevel += 0.05f; m_fearLevel = cr::min (m_fearLevel + kEmotionUpdateStep, m_baseFearLevel);
}
if (m_agressionLevel > 1.0f) {
m_agressionLevel = 1.0f;
}
if (m_fearLevel > 1.0f) {
m_fearLevel = 1.0f;
}
if (m_agressionLevel < 0.0f) {
m_agressionLevel = 0.0f;
}
if (m_fearLevel < 0.0f) {
m_fearLevel = 0.0f;
} }
} }
m_nextEmotionUpdate = game.time () + 0.5f; m_nextEmotionUpdate = game.time () + 0.5f;
@ -1694,7 +1684,7 @@ void Bot::overrideConditions () {
// check if we need to escape from bomb // check if we need to escape from bomb
if ((tid == Task::Normal || tid == Task::MoveToPosition) if ((tid == Task::Normal || tid == Task::MoveToPosition)
&& game.mapIs (MapFlags::Demolition) && game.mapIs (MapFlags::Demolition)
&& bots.isBombPlanted () && gameState.isBombPlanted ()
&& m_isAlive && m_isAlive
&& tid != Task::EscapeFromBomb && tid != Task::EscapeFromBomb
&& tid != Task::Camp && tid != Task::Camp
@ -1708,7 +1698,7 @@ void Bot::overrideConditions () {
float reachEnemyKnifeDistanceSq = cr::sqrf (128.0f); float reachEnemyKnifeDistanceSq = cr::sqrf (128.0f);
// special handling, if we have a knife in our hands // special handling, if we have a knife in our hands
if (isKnifeMode () && (util.isPlayer (m_enemy) || (cv_attack_monsters && util.isMonster (m_enemy)))) { if (isKnifeMode () && (game.isPlayerEntity (m_enemy) || (cv_attack_monsters && game.isMonsterEntity (m_enemy)))) {
const auto distanceSq2d = pev->origin.distanceSq2d (m_enemy->v.origin); const auto distanceSq2d = pev->origin.distanceSq2d (m_enemy->v.origin);
const auto nearestToEnemyPoint = graph.getNearest (m_enemy->v.origin); const auto nearestToEnemyPoint = graph.getNearest (m_enemy->v.origin);
@ -1758,7 +1748,7 @@ void Bot::overrideConditions () {
} }
// special handling for reloading // special handling for reloading
if (!bots.isRoundOver () if (!gameState.isRoundOver ()
&& tid == Task::Normal && tid == Task::Normal
&& m_reloadState != Reload::None && m_reloadState != Reload::None
&& m_isReloading && m_isReloading
@ -1785,7 +1775,7 @@ void Bot::overrideConditions () {
if (game.is (GameFlags::ZombieMod) if (game.is (GameFlags::ZombieMod)
&& !m_isCreature && !m_isCreature
&& m_infectedEnemyTeam && m_infectedEnemyTeam
&& util.isAlive (m_enemy) && game.isAliveEntity (m_enemy)
&& m_retreatTime < game.time () && m_retreatTime < game.time ()
&& pev->origin.distanceSq2d (m_enemy->v.origin) < cr::sqrf (512.0f)) { && pev->origin.distanceSq2d (m_enemy->v.origin) < cr::sqrf (512.0f)) {
@ -1809,7 +1799,7 @@ void Bot::syncUpdatePredictedIndex () {
const auto &lastEnemyOrigin = m_lastEnemyOrigin; const auto &lastEnemyOrigin = m_lastEnemyOrigin;
const auto currentNodeIndex = m_currentNodeIndex; const auto currentNodeIndex = m_currentNodeIndex;
if (lastEnemyOrigin.empty () || !vistab.isReady () || !util.isAlive (m_lastEnemy)) { if (lastEnemyOrigin.empty () || !vistab.isReady () || !game.isAliveEntity (m_lastEnemy)) {
wipePredict (); wipePredict ();
return; return;
} }
@ -1828,7 +1818,9 @@ void Bot::syncUpdatePredictedIndex () {
const float distToBotSq = botOrigin.distanceSq (graph[index].origin); const float distToBotSq = botOrigin.distanceSq (graph[index].origin);
if (vistab.visible (currentNodeIndex, index) && distToBotSq < cr::sqrf (2048.0f)) { if (vistab.visible (currentNodeIndex, index)
&& distToBotSq < cr::sqrf (2048.0f)
&& distToBotSq > cr::sqrf (128.0f)) {
bestIndex = index; bestIndex = index;
return false; return false;
} }
@ -1845,7 +1837,7 @@ void Bot::syncUpdatePredictedIndex () {
} }
void Bot::updatePredictedIndex () { void Bot::updatePredictedIndex () {
if (!m_isAlive || m_lastEnemyOrigin.empty () || !vistab.isReady () || !util.isAlive (m_lastEnemy)) { if (!m_isAlive || m_lastEnemyOrigin.empty () || !vistab.isReady () || !game.isAliveEntity (m_lastEnemy)) {
return; // do not run task if no last enemy return; // do not run task if no last enemy
} }
@ -1900,7 +1892,7 @@ void Bot::setConditions () {
// did bot just kill an enemy? // did bot just kill an enemy?
if (!game.isNullEntity (m_lastVictim)) { if (!game.isNullEntity (m_lastVictim)) {
if (game.getTeam (m_lastVictim) != m_team) { if (game.getPlayerTeam (m_lastVictim) != m_team) {
// add some aggression because we just killed somebody // add some aggression because we just killed somebody
m_agressionLevel += 0.1f; m_agressionLevel += 0.1f;
@ -1918,7 +1910,6 @@ void Bot::setConditions () {
} }
else if (rg.chance (60)) { else if (rg.chance (60)) {
if (m_lastVictim->v.weapons & kSniperWeaponMask) { if (m_lastVictim->v.weapons & kSniperWeaponMask) {
pushChatterMessage (Chatter::SniperKilled); pushChatterMessage (Chatter::SniperKilled);
} }
else { else {
@ -1950,7 +1941,7 @@ void Bot::setConditions () {
} }
} }
else { else {
m_killsInterval = m_lastVictimTime - game.time (); m_killsInterval = game.time () - m_lastVictimTime;
if (m_killsInterval <= 5.0f) { if (m_killsInterval <= 5.0f) {
++m_killsCount; ++m_killsCount;
@ -1965,7 +1956,7 @@ void Bot::setConditions () {
} }
// if no more enemies found AND bomb planted, switch to knife to get to bomb place faster // if no more enemies found AND bomb planted, switch to knife to get to bomb place faster
if (m_team == Team::CT && !usesKnife () && m_numEnemiesLeft == 0 && bots.isBombPlanted ()) { if (m_team == Team::CT && !usesKnife () && m_numEnemiesLeft == 0 && gameState.isBombPlanted ()) {
selectWeaponById (Weapon::Knife); selectWeaponById (Weapon::Knife);
m_plantedBombNodeIndex = getNearestToPlantedBomb (); m_plantedBombNodeIndex = getNearestToPlantedBomb ();
@ -1986,7 +1977,7 @@ void Bot::setConditions () {
// check if our current enemy is still valid // check if our current enemy is still valid
if (!game.isNullEntity (m_lastEnemy)) { if (!game.isNullEntity (m_lastEnemy)) {
if (!util.isAlive (m_lastEnemy) && m_shootAtDeadTime < game.time ()) { if (!game.isAliveEntity (m_lastEnemy) && m_shootAtDeadTime < game.time ()) {
m_lastEnemy = nullptr; m_lastEnemy = nullptr;
} }
} }
@ -2085,7 +2076,7 @@ void Bot::filterTasks () {
float &blindedDesire = filter[Task::Blind].desire; float &blindedDesire = filter[Task::Blind].desire;
// calculate desires to seek cover or hunt // calculate desires to seek cover or hunt
if (util.isPlayer (m_lastEnemy) && !m_lastEnemyOrigin.empty () && !m_hasC4) { if (game.isPlayerEntity (m_lastEnemy) && !m_lastEnemyOrigin.empty () && !m_hasC4) {
const float retreatLevel = (100.0f - (m_healthValue > 70.0f ? 100.0f : m_healthValue)) * tempFear; // retreat level depends on bot health const float retreatLevel = (100.0f - (m_healthValue > 70.0f ? 100.0f : m_healthValue)) * tempFear; // retreat level depends on bot health
if (m_isCreature || if (m_isCreature ||
@ -2113,7 +2104,7 @@ void Bot::filterTasks () {
if (m_isCreature) { if (m_isCreature) {
ratio = 0.0f; ratio = 0.0f;
} }
if (bots.isBombPlanted () || m_isStuck || usesKnife ()) { if (gameState.isBombPlanted () || m_isStuck || usesKnife ()) {
ratio /= 3.0f; // reduce the seek cover desire if bomb is planted ratio /= 3.0f; // reduce the seek cover desire if bomb is planted
} }
else if (m_isVIP || m_isReloading || (sniping && usesSniper ())) { else if (m_isVIP || m_isReloading || (sniping && usesSniper ())) {
@ -2135,7 +2126,7 @@ void Bot::filterTasks () {
if (getCurrentTaskId () != Task::EscapeFromBomb if (getCurrentTaskId () != Task::EscapeFromBomb
&& game.isNullEntity (m_enemy) && game.isNullEntity (m_enemy)
&& !m_isVIP && !m_isVIP
&& bots.getRoundMidTime () < game.time () && gameState.getRoundMidTime () < game.time ()
&& !m_hasHostage && !m_hasHostage
&& !m_isUsingGrenade && !m_isUsingGrenade
&& m_currentNodeIndex != graph.getNearest (m_lastEnemyOrigin) && m_currentNodeIndex != graph.getNearest (m_lastEnemyOrigin)
@ -2252,10 +2243,11 @@ void Bot::startTask (Task id, float desire, int data, float time, bool resume) {
} }
return; return;
} }
else {
clearSearchNodes ();
}
} }
m_tasks.emplace (filter[id].func, id, desire, data, time, resume); m_tasks.emplace (filter[id].func, id, desire, data, time, resume);
clearSearchNodes ();
ignoreCollision (); ignoreCollision ();
const auto tid = getCurrentTaskId (); const auto tid = getCurrentTaskId ();
@ -2434,7 +2426,7 @@ void Bot::handleChatterTaskChange (Task tid) {
} }
if (rg.chance (25) && tid == Task::Camp) { if (rg.chance (25) && tid == Task::Camp) {
if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted ()) { if (game.mapIs (MapFlags::Demolition) && gameState.isBombPlanted ()) {
pushChatterMessage (Chatter::GuardingPlantedC4); pushChatterMessage (Chatter::GuardingPlantedC4);
} }
else { else {
@ -2467,12 +2459,12 @@ void Bot::executeChatterFrameEvents () {
if (!hasFriendNearby && rg.chance (45) && (m_enemy->v.weapons & cr::bit (Weapon::C4))) { if (!hasFriendNearby && rg.chance (45) && (m_enemy->v.weapons & cr::bit (Weapon::C4))) {
pushChatterMessage (Chatter::SpotTheBomber); pushChatterMessage (Chatter::SpotTheBomber);
} }
else if (!hasFriendNearby && rg.chance (45) && m_team == Team::Terrorist && util.isPlayerVIP (m_enemy)) { else if (!hasFriendNearby && rg.chance (45) && m_team == Team::Terrorist && game.isPlayerVIP (m_enemy)) {
pushChatterMessage (Chatter::VIPSpotted); pushChatterMessage (Chatter::VIPSpotted);
} }
else if (!hasFriendNearby else if (!hasFriendNearby
&& rg.chance (50) && rg.chance (50)
&& game.getTeam (m_enemy) != m_team && game.getPlayerTeam (m_enemy) != m_team
&& isGroupOfEnemies (m_enemy->v.origin)) { && isGroupOfEnemies (m_enemy->v.origin)) {
pushChatterMessage (Chatter::ScaredEmotion); pushChatterMessage (Chatter::ScaredEmotion);
@ -2491,7 +2483,7 @@ void Bot::executeChatterFrameEvents () {
} }
// if bomb planted warn players ! // if bomb planted warn players !
if (bots.hasBombSay (BombPlantedSay::Chatter) && bots.isBombPlanted () && m_team == Team::CT) { if (bots.hasBombSay (BombPlantedSay::Chatter) && gameState.isBombPlanted () && m_team == Team::CT) {
pushChatterMessage (Chatter::GottaFindC4); pushChatterMessage (Chatter::GottaFindC4);
bots.clearBombSay (BombPlantedSay::Chatter); bots.clearBombSay (BombPlantedSay::Chatter);
} }
@ -2702,7 +2694,7 @@ void Bot::checkRadioQueue () {
break; break;
case Radio::ShesGonnaBlow: case Radio::ShesGonnaBlow:
if (game.isNullEntity (m_enemy) && distanceSq < cr::sqrf (2048.0f) && bots.isBombPlanted () && m_team == Team::Terrorist) { if (game.isNullEntity (m_enemy) && distanceSq < cr::sqrf (2048.0f) && gameState.isBombPlanted () && m_team == Team::Terrorist) {
pushRadioMessage (Radio::RogerThat); pushRadioMessage (Radio::RogerThat);
if (getCurrentTaskId () == Task::Camp) { if (getCurrentTaskId () == Task::Camp) {
@ -2718,11 +2710,11 @@ void Bot::checkRadioQueue () {
case Radio::RegroupTeam: case Radio::RegroupTeam:
// if no more enemies found AND bomb planted, switch to knife to get to bombplace faster // if no more enemies found AND bomb planted, switch to knife to get to bombplace faster
if (m_team == Team::CT && !usesKnife () && m_numEnemiesLeft == 0 && bots.isBombPlanted () && getCurrentTaskId () != Task::DefuseBomb) { if (m_team == Team::CT && !usesKnife () && m_numEnemiesLeft == 0 && gameState.isBombPlanted () && getCurrentTaskId () != Task::DefuseBomb) {
selectWeaponById (Weapon::Knife); selectWeaponById (Weapon::Knife);
clearSearchNodes (); clearSearchNodes ();
m_position = graph.getBombOrigin (); m_position = gameState.getBombOrigin ();
startTask (Task::MoveToPosition, TaskPri::MoveToPosition, kInvalidNodeIndex, 0.0f, true); startTask (Task::MoveToPosition, TaskPri::MoveToPosition, kInvalidNodeIndex, 0.0f, true);
pushRadioMessage (Radio::RogerThat); pushRadioMessage (Radio::RogerThat);
@ -2846,7 +2838,7 @@ void Bot::checkRadioQueue () {
case Task::Camp: case Task::Camp:
if (rg.chance (m_radioPercent)) { if (rg.chance (m_radioPercent)) {
if (bots.isBombPlanted () && m_team == Team::Terrorist) { if (gameState.isBombPlanted () && m_team == Team::Terrorist) {
pushChatterMessage (Chatter::GuardingPlantedC4); pushChatterMessage (Chatter::GuardingPlantedC4);
} }
else if (m_inEscapeZone && m_team == Team::CT) { else if (m_inEscapeZone && m_team == Team::CT) {
@ -2911,14 +2903,14 @@ void Bot::checkRadioQueue () {
case Radio::SectorClear: case Radio::SectorClear:
// is bomb planted and it's a ct // is bomb planted and it's a ct
if (!bots.isBombPlanted ()) { if (!gameState.isBombPlanted ()) {
break; break;
} }
// check if it's a ct command // check if it's a ct command
if (game.getTeam (m_radioEntity) == Team::CT if (game.getPlayerTeam (m_radioEntity) == Team::CT
&& m_team == Team::CT && m_team == Team::CT
&& util.isFakeClient (m_radioEntity) && game.isFakeClientEntity (m_radioEntity)
&& bots.getPlantedBombSearchTimestamp () < game.time ()) { && bots.getPlantedBombSearchTimestamp () < game.time ()) {
float nearestDistanceSq = kInfiniteDistance; float nearestDistanceSq = kInfiniteDistance;
@ -3011,11 +3003,11 @@ void Bot::checkRadioQueue () {
void Bot::tryHeadTowardRadioMessage () { void Bot::tryHeadTowardRadioMessage () {
const auto tid = getCurrentTaskId (); const auto tid = getCurrentTaskId ();
if (tid == Task::MoveToPosition || m_headedTime + 15.0f < game.time () || !util.isAlive (m_radioEntity) || m_hasC4) { if (tid == Task::MoveToPosition || m_headedTime + 15.0f < game.time () || !game.isAliveEntity (m_radioEntity) || m_hasC4) {
return; return;
} }
if ((util.isFakeClient (m_radioEntity) if ((game.isFakeClientEntity (m_radioEntity)
&& rg.chance (m_radioPercent) && rg.chance (m_radioPercent)
&& m_personality == Personality::Normal) || !(m_radioEntity->v.flags & FL_FAKECLIENT)) { && m_personality == Personality::Normal) || !(m_radioEntity->v.flags & FL_FAKECLIENT)) {
@ -3060,8 +3052,8 @@ void Bot::frame () {
return; return;
} }
if (bots.isBombPlanted () && m_team == Team::CT && m_isAlive) { if (gameState.isBombPlanted () && m_team == Team::CT && m_isAlive) {
const auto &bombPosition = graph.getBombOrigin (); const auto &bombPosition = gameState.getBombOrigin ();
if (!m_hasProgressBar if (!m_hasProgressBar
&& getCurrentTaskId () != Task::EscapeFromBomb && getCurrentTaskId () != Task::EscapeFromBomb
@ -3096,9 +3088,9 @@ void Bot::frame () {
void Bot::update () { void Bot::update () {
const auto tid = getCurrentTaskId (); const auto tid = getCurrentTaskId ();
m_canChooseAimDirection = true; m_canSetAimDirection = true;
m_isAlive = util.isAlive (ent ()); m_isAlive = game.isAliveEntity (ent ());
m_team = game.getTeam (ent ()); m_team = game.getPlayerTeam (ent ());
m_healthValue = cr::clamp (pev->health, 0.0f, 99999.9f); m_healthValue = cr::clamp (pev->health, 0.0f, 99999.9f);
if (m_team == Team::Terrorist && game.mapIs (MapFlags::Demolition)) { if (m_team == Team::Terrorist && game.mapIs (MapFlags::Demolition)) {
@ -3144,7 +3136,7 @@ void Bot::update () {
m_lastVoteKick = m_voteKickIndex; m_lastVoteKick = m_voteKickIndex;
// if bot tk punishment is enabled slay the tk // if bot tk punishment is enabled slay the tk
if (cv_tkpunish.as <int> () != 2 || util.isFakeClient (game.entityOfIndex (m_voteKickIndex))) { if (cv_tkpunish.as <int> () != 2 || game.isFakeClientEntity (game.entityOfIndex (m_voteKickIndex))) {
return; return;
} }
auto killer = game.entityOfIndex (m_lastVoteKick); auto killer = game.entityOfIndex (m_lastVoteKick);
@ -3237,7 +3229,7 @@ void Bot::logicDuringFreezetime () {
if (ent) { if (ent) {
m_lookAt = ent->v.origin + ent->v.view_ofs; m_lookAt = ent->v.origin + ent->v.view_ofs;
if (m_buyingFinished) { if (m_buyingFinished && game.getPlayerTeam (ent) != m_team) {
m_enemy = ent; m_enemy = ent;
m_enemyOrigin = ent->v.origin; m_enemyOrigin = ent->v.origin;
} }
@ -3283,9 +3275,17 @@ void Bot::checkSpawnConditions () {
dropCurrentWeapon (); dropCurrentWeapon ();
} }
else { else {
bool switchToKnife = true;
if (m_currentWeapon == Weapon::Scout) {
switchToKnife = rg.chance (25);
}
if (switchToKnife) {
selectWeaponById (Weapon::Knife); selectWeaponById (Weapon::Knife);
} }
} }
}
m_checkKnifeSwitch = false; m_checkKnifeSwitch = false;
if (rg.chance (cv_user_follow_percent.as <int> ()) && game.isNullEntity (m_targetEntity) && !m_isLeader && !m_hasC4 && rg.chance (50)) { if (rg.chance (cv_user_follow_percent.as <int> ()) && game.isNullEntity (m_targetEntity) && !m_isLeader && !m_hasC4 && rg.chance (50)) {
@ -3325,12 +3325,11 @@ void Bot::checkSpawnConditions () {
void Bot::logic () { void Bot::logic () {
// this function gets called each frame and is the core of all bot ai. from here all other subroutines are called // this function gets called each frame and is the core of all bot ai. from here all other subroutines are called
m_movedDistance = kMinMovedDistance; // length of different vector (distance bot moved)
resetMovement (); resetMovement ();
// increase reaction time // increase reaction time
m_actualReactionTime += 0.3f; m_actualReactionTime += 0.3f;
m_movedDistance = kMinMovedDistance + 0.1f; // length of different vector (distance bot moved)
if (m_actualReactionTime > m_idealReactionTime) { if (m_actualReactionTime > m_idealReactionTime) {
m_actualReactionTime = m_idealReactionTime; m_actualReactionTime = m_idealReactionTime;
@ -3351,13 +3350,11 @@ void Bot::logic () {
if (m_prevTime <= game.time ()) { if (m_prevTime <= game.time ()) {
// see how far bot has moved since the previous position... // see how far bot has moved since the previous position...
if (m_checkTerrain) { m_movedDistance = m_prevOrigin.distanceSq (pev->origin);
m_movedDistance = m_prevOrigin.distance (pev->origin);
}
// save current position as previous // save current position as previous
m_prevOrigin = pev->origin; m_prevOrigin = pev->origin;
m_prevTime = game.time () + (0.2f - m_frameInterval * 2.0f); m_prevTime = game.time () + 0.2f - m_frameInterval;
} }
// if there's some radio message to respond, check it // if there's some radio message to respond, check it
@ -3450,7 +3447,7 @@ void Bot::logic () {
// ensure we're not stuck picking something // ensure we're not stuck picking something
if (m_moveToGoal && m_moveSpeed > 0.0f if (m_moveToGoal && m_moveSpeed > 0.0f
&& rg (2.5f, 3.5f) + m_navTimeset + m_destOrigin.distanceSq2d (pev->origin) / cr::sqrf (m_moveSpeed) < game.time () && rg (2.5f, 3.5f) + m_navTimeset + m_destOrigin.distanceSq2d (pev->origin) / cr::sqrf (cr::max (1.0f, m_moveSpeed)) < game.time ()
&& !(m_states & Sense::SeeingEnemy)) { && !(m_states & Sense::SeeingEnemy)) {
ensurePickupEntitiesClear (); ensurePickupEntitiesClear ();
} }
@ -3465,8 +3462,6 @@ void Bot::logic () {
// save the previous speed (for checking if stuck) // save the previous speed (for checking if stuck)
m_prevSpeed = cr::abs (m_moveSpeed); m_prevSpeed = cr::abs (m_moveSpeed);
m_prevVelocity = cr::abs (pev->velocity.length2d ());
m_lastDamageType = -1; // reset damage m_lastDamageType = -1; // reset damage
} }
@ -3474,6 +3469,8 @@ void Bot::spawned () {
if (game.is (GameFlags::CSDM | GameFlags::ZombieMod)) { if (game.is (GameFlags::CSDM | GameFlags::ZombieMod)) {
newRound (); newRound ();
clearTasks (); clearTasks ();
m_buyingFinished = true;
} }
} }
@ -3593,7 +3590,7 @@ void Bot::showDebugOverlay () {
static hudtextparms_t textParams {}; static hudtextparms_t textParams {};
textParams.channel = 1; textParams.channel = 4;
textParams.x = -1.0f; textParams.x = -1.0f;
textParams.y = 0.0f; textParams.y = 0.0f;
textParams.effect = 0; textParams.effect = 0;
@ -3628,7 +3625,7 @@ void Bot::showDebugOverlay () {
} }
bool Bot::hasHostage () { bool Bot::hasHostage () {
if (cv_ignore_objectives || game.mapIs (MapFlags::Demolition)) { if (cv_ignore_objectives || !game.mapIs (MapFlags::HostageRescue)) {
return false; return false;
} }
@ -3655,7 +3652,7 @@ void Bot::takeDamage (edict_t *inflictor, int damage, int armor, int bits) {
m_lastDamageType = bits; m_lastDamageType = bits;
if (m_isCreature) { if (m_isCreature) {
if (util.isPlayer (inflictor) && game.isNullEntity (m_enemy)) { if (game.isPlayerEntity (inflictor) && game.isNullEntity (m_enemy)) {
if (seesEnemy (inflictor)) { if (seesEnemy (inflictor)) {
m_enemy = inflictor; m_enemy = inflictor;
m_enemyOrigin = inflictor->v.origin; m_enemyOrigin = inflictor->v.origin;
@ -3669,10 +3666,10 @@ void Bot::takeDamage (edict_t *inflictor, int damage, int armor, int bits) {
} }
m_lastDamageTimestamp = game.time (); m_lastDamageTimestamp = game.time ();
if (util.isPlayer (inflictor) || (cv_attack_monsters && util.isMonster (inflictor))) { if (game.isPlayerEntity (inflictor) || (cv_attack_monsters && game.isMonsterEntity (inflictor))) {
const auto inflictorTeam = game.getTeam (inflictor); const auto inflictorTeam = game.getPlayerTeam (inflictor);
if (!util.isMonster (inflictor) && cv_tkpunish && inflictorTeam == m_team && !util.isFakeClient (inflictor)) { if (!game.isMonsterEntity (inflictor) && cv_tkpunish && inflictorTeam == m_team && !game.isFakeClientEntity (inflictor)) {
// alright, die you team killer!!! // alright, die you team killer!!!
m_actualReactionTime = 0.0f; m_actualReactionTime = 0.0f;
m_seeEnemyTime = game.time (); m_seeEnemyTime = game.time ();
@ -3736,7 +3733,7 @@ void Bot::takeBlind (int alpha) {
m_viewDistance = rg (10.0f, 20.0f); m_viewDistance = rg (10.0f, 20.0f);
// do not take in effect some unique map effects on round start // do not take in effect some unique map effects on round start
if (bots.getRoundStartTime () + 5.0f < game.time ()) { if (gameState.getRoundStartTime () + 5.0f < game.time ()) {
m_viewDistance = m_maxViewDistance; m_viewDistance = m_maxViewDistance;
} }
m_blindTime = game.time () + static_cast <float> (alpha - 200) / 16.0f; m_blindTime = game.time () + static_cast <float> (alpha - 200) / 16.0f;
@ -3798,11 +3795,11 @@ void Bot::updatePracticeValue (int damage) const {
void Bot::updatePracticeDamage (edict_t *attacker, int damage) { void Bot::updatePracticeDamage (edict_t *attacker, int damage) {
// this function gets called each time a bot gets damaged by some enemy. stores the damage (team-specific) done by victim. // this function gets called each time a bot gets damaged by some enemy. stores the damage (team-specific) done by victim.
if (!util.isPlayer (attacker)) { if (!game.isPlayerEntity (attacker)) {
return; return;
} }
const int attackerTeam = game.getTeam (attacker); const int attackerTeam = game.getPlayerTeam (attacker);
const int victimTeam = m_team; const int victimTeam = m_team;
if (attackerTeam == victimTeam) { if (attackerTeam == victimTeam) {
@ -3833,7 +3830,7 @@ void Bot::updatePracticeDamage (edict_t *attacker, int damage) {
practice.setDamage (victimIndex, victimIndex, victimIndex, cr::clamp (practice.getDamage (victimTeam, victimIndex, victimIndex), 0, kMaxDamageValue)); practice.setDamage (victimIndex, victimIndex, victimIndex, cr::clamp (practice.getDamage (victimTeam, victimIndex, victimIndex), 0, kMaxDamageValue));
} }
} }
const auto updateDamage = util.isFakeClient (attacker) ? 10 : 7; const auto updateDamage = game.isFakeClientEntity (attacker) ? 10 : 7;
// store away the damage done // store away the damage done
const auto damageValue = cr::clamp (practice.getDamage (m_team, victimIndex, attackerIndex) + damage / updateDamage, 0, kMaxDamageValue); const auto damageValue = cr::clamp (practice.getDamage (m_team, victimIndex, attackerIndex) + damage / updateDamage, 0, kMaxDamageValue);
@ -3857,7 +3854,7 @@ void Bot::dropWeaponForUser (edict_t *user, bool discardC4) {
// this function, asks bot to discard his current primary weapon (or c4) to the user that requested it with /drop* // this function, asks bot to discard his current primary weapon (or c4) to the user that requested it with /drop*
// command, very useful, when i'm don't have money to buy anything... ) // command, very useful, when i'm don't have money to buy anything... )
if (util.isAlive (user) && m_moneyAmount >= 2000 && hasPrimaryWeapon () && user->v.origin.distanceSq (pev->origin) <= cr::sqrf (450.0f)) { if (game.isAliveEntity (user) && m_moneyAmount >= 2000 && hasPrimaryWeapon () && user->v.origin.distanceSq (pev->origin) <= cr::sqrf (450.0f)) {
m_aimFlags |= AimFlags::Entity; m_aimFlags |= AimFlags::Entity;
m_lookAt = user->v.origin; m_lookAt = user->v.origin;
@ -3915,9 +3912,6 @@ void Bot::resetDoubleJump () {
} }
void Bot::debugMsgInternal (StringRef str) { void Bot::debugMsgInternal (StringRef str) {
if (game.isDedicated ()) {
return;
}
const int level = cv_debug.as <int> (); const int level = cv_debug.as <int> ();
if (level <= 2) { if (level <= 2) {
@ -3928,7 +3922,10 @@ void Bot::debugMsgInternal (StringRef str) {
bool playMessage = false; bool playMessage = false;
if (level == 3 && !game.isNullEntity (game.getLocalEntity ()) && game.getLocalEntity ()->v.iuser2 == entindex ()) { if (level == 3
&& !game.isNullEntity (game.getLocalEntity ())
&& game.getLocalEntity ()->v.iuser2 == entindex ()) {
playMessage = true; playMessage = true;
} }
else if (level != 3) { else if (level != 3) {
@ -3939,7 +3936,7 @@ void Bot::debugMsgInternal (StringRef str) {
logger.message (printBuf.chars ()); logger.message (printBuf.chars ());
} }
if (playMessage) { if (playMessage && !game.isDedicated ()) {
ctrl.msg (printBuf.chars ()); ctrl.msg (printBuf.chars ());
sendToChat (printBuf, false); sendToChat (printBuf, false);
} }
@ -3948,16 +3945,16 @@ void Bot::debugMsgInternal (StringRef str) {
Vector Bot::isBombAudible () { Vector Bot::isBombAudible () {
// this function checks if bomb is can be heard by the bot, calculations done by manual testing. // this function checks if bomb is can be heard by the bot, calculations done by manual testing.
if (!bots.isBombPlanted () || getCurrentTaskId () == Task::EscapeFromBomb) { if (!gameState.isBombPlanted () || getCurrentTaskId () == Task::EscapeFromBomb) {
return nullptr; // reliability check return nullptr; // reliability check
} }
if (m_difficulty > Difficulty::Hard) { if (m_difficulty > Difficulty::Hard) {
return graph.getBombOrigin (); return gameState.getBombOrigin ();
} }
const auto &bombOrigin = graph.getBombOrigin (); const auto &bombOrigin = gameState.getBombOrigin ();
const float timeElapsed = ((game.time () - bots.getTimeBombPlanted ()) / mp_c4timer.as <float> ()) * 100.0f; const float timeElapsed = ((game.time () - gameState.getTimeBombPlanted ()) / mp_c4timer.as <float> ()) * 100.0f;
float desiredRadius = 768.0f; float desiredRadius = 768.0f;
// start the manual calculations // start the manual calculations
@ -4038,12 +4035,7 @@ void Bot::runMovement () {
m_oldButtons = pev->button; m_oldButtons = pev->button;
} }
float Bot::getBombTimeleft () const {
if (!bots.isBombPlanted ()) {
return 0.0f;
}
return cr::max (bots.getTimeBombPlanted () + mp_c4timer.as <float> () - game.time (), 0.0f);
}
bool Bot::isOutOfBombTimer () { bool Bot::isOutOfBombTimer () {
if (!game.mapIs (MapFlags::Demolition)) { if (!game.mapIs (MapFlags::Demolition)) {
@ -4055,13 +4047,13 @@ bool Bot::isOutOfBombTimer () {
} }
// calculate left time // calculate left time
const float timeLeft = getBombTimeleft (); const float timeLeft = gameState.getBombTimeLeft ();
// if time left greater than 13, no need to do other checks // if time left greater than 13, no need to do other checks
if (timeLeft > 13.0f) { if (timeLeft > 13.0f) {
return false; return false;
} }
const auto &bombOrigin = graph.getBombOrigin (); const auto &bombOrigin = gameState.getBombOrigin ();
// for terrorist, if timer is lower than 13 seconds, return true // for terrorist, if timer is lower than 13 seconds, return true
if (timeLeft < 13.0f && m_team == Team::Terrorist && bombOrigin.distanceSq (pev->origin) < cr::sqrf (964.0f)) { if (timeLeft < 13.0f && m_team == Team::Terrorist && bombOrigin.distanceSq (pev->origin) < cr::sqrf (964.0f)) {
@ -4086,7 +4078,7 @@ bool Bot::isOutOfBombTimer () {
return true; return true;
} }
if (m_hasProgressBar && isOnFloor () && ((m_hasDefuser ? 10.0f : 15.0f) > getBombTimeleft ())) { if (m_hasProgressBar && isOnFloor () && ((m_hasDefuser ? 10.0f : 15.0f) > gameState.getBombTimeLeft ())) {
return true; return true;
} }
return false; // return false otherwise return false; // return false otherwise
@ -4100,7 +4092,7 @@ void Bot::updateHearing () {
float nearestDistanceSq = kInfiniteDistance; float nearestDistanceSq = kInfiniteDistance;
// do not hear to other enemies if just tracked old one // do not hear to other enemies if just tracked old one
if (m_timeNextTracking < game.time () && m_lastEnemy == m_trackingEdict && util.isAlive (m_lastEnemy)) { if (m_timeNextTracking < game.time () && m_lastEnemy == m_trackingEdict && game.isAliveEntity (m_lastEnemy)) {
m_hearedEnemy = m_lastEnemy; m_hearedEnemy = m_lastEnemy;
m_lastEnemyOrigin = m_lastEnemy->v.origin; m_lastEnemyOrigin = m_lastEnemy->v.origin;
@ -4115,7 +4107,7 @@ void Bot::updateHearing () {
if (!(client.flags & ClientFlags::Used) if (!(client.flags & ClientFlags::Used)
|| !(client.flags & ClientFlags::Alive) || !(client.flags & ClientFlags::Alive)
|| client.ent == ent () || client.ent == ent ()
|| client.team == m_team || client.team2 == m_team
|| !client.ent || !client.ent
|| client.noise.last < game.time ()) { || client.noise.last < game.time ()) {
@ -4139,7 +4131,7 @@ void Bot::updateHearing () {
} }
// did the bot hear someone ? // did the bot hear someone ?
if (util.isPlayer (m_hearedEnemy)) { if (game.isPlayerEntity (m_hearedEnemy)) {
// change to best weapon if heard something // change to best weapon if heard something
if (m_shootTime < game.time () - 5.0f if (m_shootTime < game.time () - 5.0f
&& isOnFloor () && isOnFloor ()
@ -4215,7 +4207,7 @@ void Bot::updateHearing () {
else { else {
if (cv_shoots_thru_walls if (cv_shoots_thru_walls
&& m_lastEnemy == m_hearedEnemy && m_lastEnemy == m_hearedEnemy
&& rg.chance (conf.getDifficultyTweaks (m_difficulty)->hearThruPct) && rg.chance (m_difficultyData->hearThruPct)
&& m_seeEnemyTime + 3.0f > game.time () && m_seeEnemyTime + 3.0f > game.time ()
&& isPenetrableObstacle (m_hearedEnemy->v.origin)) { && isPenetrableObstacle (m_hearedEnemy->v.origin)) {
@ -4234,7 +4226,7 @@ void Bot::updateHearing () {
void Bot::enteredBuyZone (int buyState) { void Bot::enteredBuyZone (int buyState) {
// this function is gets called when bot enters a buyzone, to allow bot to buy some stuff // this function is gets called when bot enters a buyzone, to allow bot to buy some stuff
if (m_isCreature) { if (m_isCreature || hasHostage ()) {
return; // creatures can't buy anything return; // creatures can't buy anything
} }
const int *econLimit = conf.getEconLimit (); const int *econLimit = conf.getEconLimit ();
@ -4243,8 +4235,8 @@ void Bot::enteredBuyZone (int buyState) {
if (m_seeEnemyTime + 12.0f < game.time () if (m_seeEnemyTime + 12.0f < game.time ()
&& m_lastEquipTime + 30.0f < game.time () && m_lastEquipTime + 30.0f < game.time ()
&& m_inBuyZone && m_inBuyZone
&& (bots.getRoundStartTime () + rg (10.0f, 20.0f) + mp_buytime.as <float> () < game.time ()) && (gameState.getRoundStartTime () + rg (10.0f, 20.0f) + mp_buytime.as <float> () < game.time ())
&& !bots.isBombPlanted () && !gameState.isBombPlanted ()
&& m_moneyAmount > econLimit[EcoLimit::PrimaryGreater]) { && m_moneyAmount > econLimit[EcoLimit::PrimaryGreater]) {
m_ignoreBuyDelay = true; m_ignoreBuyDelay = true;
@ -4283,7 +4275,7 @@ void Bot::selectCampButtons (int index) {
bool Bot::isBombDefusing (const Vector &bombOrigin) const { bool Bot::isBombDefusing (const Vector &bombOrigin) const {
// this function finds if somebody currently defusing the bomb. // this function finds if somebody currently defusing the bomb.
if (!bots.isBombPlanted ()) { if (!gameState.isBombPlanted ()) {
return false; return false;
} }
bool defusingInProgress = false; bool defusingInProgress = false;
@ -4356,8 +4348,8 @@ void Bot::refreshCreatureStatus (char *infobuffer) {
} }
// if bot is on infected team, and zombie mode is active, assume bot is a creature/zombie // if bot is on infected team, and zombie mode is active, assume bot is a creature/zombie
m_isOnInfectedTeam = game.getRealTeam (ent ()) == infectedTeam; m_isOnInfectedTeam = game.getRealPlayerTeam (ent ()) == infectedTeam;
m_infectedEnemyTeam = game.getRealTeam (m_enemy) == infectedTeam; m_infectedEnemyTeam = game.getRealPlayerTeam (m_enemy) == infectedTeam;
// do not process next if already infected // do not process next if already infected
if (m_isOnInfectedTeam || m_infectedEnemyTeam) { if (m_isOnInfectedTeam || m_infectedEnemyTeam) {
@ -4437,7 +4429,7 @@ void Bot::donateC4ToHuman () {
// search world for just dropped bomb // search world for just dropped bomb
game.searchEntities ("classname", "weaponbox", [&] (edict_t *ent) { game.searchEntities ("classname", "weaponbox", [&] (edict_t *ent) {
if (util.isModel (ent, "backpack.mdl")) { if (game.isEntityModelMatches (ent, "backpack.mdl")) {
bomb = ent; bomb = ent;
if (!game.isNullEntity (bomb)) { if (!game.isNullEntity (bomb)) {

View file

@ -161,7 +161,7 @@ void Bot::prepareChatMessage (StringRef message) {
auto humanizedName = [] (int index) -> String { auto humanizedName = [] (int index) -> String {
auto ent = game.playerOfIndex (index); auto ent = game.playerOfIndex (index);
if (!util.isPlayer (ent)) { if (!game.isPlayerEntity (ent)) {
return "unknown"; return "unknown";
} }
String playerName = ent->v.netname.chars (); String playerName = ent->v.netname.chars ();
@ -193,7 +193,7 @@ void Bot::prepareChatMessage (StringRef message) {
// get roundtime // get roundtime
auto getRoundTime = [] () -> String { auto getRoundTime = [] () -> String {
auto roundTimeSecs = static_cast <int> (bots.getRoundEndTime () - game.time ()); const auto roundTimeSecs = static_cast <int> (gameState.getRoundEndTime () - game.time ());
String roundTime {}; String roundTime {};
roundTime.assignf ("%02d:%02d", cr::clamp (roundTimeSecs / 60, 0, 59), cr::clamp (cr::abs (roundTimeSecs % 60), 0, 59)); roundTime.assignf ("%02d:%02d", cr::clamp (roundTimeSecs / 60, 0, 59), cr::clamp (cr::abs (roundTimeSecs % 60), 0, 59));
@ -241,8 +241,8 @@ void Bot::prepareChatMessage (StringRef message) {
return humanizedName (playerIndex); return humanizedName (playerIndex);
} }
else if (!needsEnemy && m_team == client.team) { else if (!needsEnemy && m_team == client.team) {
if (util.isPlayer (pev->dmg_inflictor) if (game.isPlayerEntity (pev->dmg_inflictor)
&& game.getRealTeam (pev->dmg_inflictor) == m_team) { && game.getRealPlayerTeam (pev->dmg_inflictor) == m_team) {
return humanizedName (game.indexOfPlayer (pev->dmg_inflictor)); return humanizedName (game.indexOfPlayer (pev->dmg_inflictor));
} }

View file

@ -335,7 +335,7 @@ bool Bot::checkBodyPartsWithHitboxes (edict_t *target) {
bool Bot::seesEnemy (edict_t *player) { bool Bot::seesEnemy (edict_t *player) {
auto isBehindSmokeClouds = [&] (const Vector &pos) { auto isBehindSmokeClouds = [&] (const Vector &pos) {
if (cv_smoke_grenade_checks.as <int> () == 2) { if (cv_smoke_grenade_checks.as <int> () == 2) {
return bots.isLineBlockedBySmoke (getEyesPos (), pos); return util.isLineBlockedBySmoke (getEyesPos (), pos);
} }
return false; return false;
}; };
@ -345,7 +345,7 @@ bool Bot::seesEnemy (edict_t *player) {
} }
bool ignoreFieldOfView = false; bool ignoreFieldOfView = false;
if (cv_whose_your_daddy && util.isPlayer (pev->dmg_inflictor) && game.getTeam (pev->dmg_inflictor) != m_team) { if (cv_whose_your_daddy && game.isPlayerEntity (pev->dmg_inflictor) && game.getPlayerTeam (pev->dmg_inflictor) != m_team) {
ignoreFieldOfView = true; ignoreFieldOfView = true;
} }
@ -387,7 +387,7 @@ bool Bot::lookupEnemies () {
if (!game.isNullEntity (m_enemy) && (m_states & Sense::SeeingEnemy)) { if (!game.isNullEntity (m_enemy) && (m_states & Sense::SeeingEnemy)) {
m_states &= ~Sense::SuspectEnemy; m_states &= ~Sense::SuspectEnemy;
} }
else if (game.isNullEntity (m_enemy) && m_seeEnemyTime + 4.0f > game.time () && util.isAlive (m_lastEnemy)) { else if (game.isNullEntity (m_enemy) && m_seeEnemyTime + 4.0f > game.time () && game.isAliveEntity (m_lastEnemy)) {
m_states |= Sense::SuspectEnemy; m_states |= Sense::SuspectEnemy;
const bool denyLastEnemy = pev->velocity.lengthSq2d () > 0.0f const bool denyLastEnemy = pev->velocity.lengthSq2d () > 0.0f
@ -406,7 +406,7 @@ bool Bot::lookupEnemies () {
// is player is alive // is player is alive
if (m_enemyUpdateTime > game.time () if (m_enemyUpdateTime > game.time ()
&& player->v.origin.distanceSq (pev->origin) < nearestDistanceSq && player->v.origin.distanceSq (pev->origin) < nearestDistanceSq
&& util.isAlive (player) && game.isAliveEntity (player)
&& seesEnemy (player)) { && seesEnemy (player)) {
newEnemy = player; newEnemy = player;
@ -427,8 +427,8 @@ bool Bot::lookupEnemies () {
if (cv_attack_monsters) { if (cv_attack_monsters) {
// search the world for monsters... // search the world for monsters...
for (const auto &interesting : bots.getInterestingEntities ()) { for (const auto &interesting : gameState.getInterestingEntities ()) {
if (!util.isMonster (interesting)) { if (!game.isMonsterEntity (interesting)) {
continue; continue;
} }
@ -485,7 +485,7 @@ bool Bot::lookupEnemies () {
newEnemy = player; newEnemy = player;
// aim VIP first on AS maps... // aim VIP first on AS maps...
if (game.is (MapFlags::Assassination) && util.isPlayerVIP (newEnemy)) { if (game.is (MapFlags::Assassination) && game.isPlayerVIP (newEnemy)) {
break; break;
} }
} }
@ -498,7 +498,7 @@ bool Bot::lookupEnemies () {
} }
} }
if (newEnemy != nullptr && (util.isPlayer (newEnemy) || (cv_attack_monsters && util.isMonster (newEnemy)))) { if (newEnemy != nullptr && (game.isPlayerEntity (newEnemy) || (cv_attack_monsters && game.isMonsterEntity (newEnemy)))) {
bots.setCanPause (true); bots.setCanPause (true);
m_aimFlags |= AimFlags::Enemy; m_aimFlags |= AimFlags::Enemy;
@ -587,7 +587,7 @@ bool Bot::lookupEnemies () {
newEnemy = m_enemy; newEnemy = m_enemy;
m_lastEnemy = newEnemy; m_lastEnemy = newEnemy;
if (!util.isAlive (newEnemy)) { if (!game.isAliveEntity (newEnemy)) {
m_enemy = nullptr; m_enemy = nullptr;
m_enemyBodyPartSet = nullptr; m_enemyBodyPartSet = nullptr;
@ -614,7 +614,7 @@ bool Bot::lookupEnemies () {
// if no enemy visible check if last one shoot able through wall // if no enemy visible check if last one shoot able through wall
if (cv_shoots_thru_walls if (cv_shoots_thru_walls
&& rg.chance (conf.getDifficultyTweaks (m_difficulty)->seenThruPct) && rg.chance (m_difficultyData->seenThruPct)
&& isPenetrableObstacle (newEnemy->v.origin)) { && isPenetrableObstacle (newEnemy->v.origin)) {
m_seeEnemyTime = game.time (); m_seeEnemyTime = game.time ();
@ -638,7 +638,7 @@ bool Bot::lookupEnemies () {
&& game.isNullEntity (m_enemy) && game.isNullEntity (m_enemy)
&& getCurrentTaskId () != Task::ShootBreakable && getCurrentTaskId () != Task::ShootBreakable
&& getCurrentTaskId () != Task::PlantBomb && getCurrentTaskId () != Task::PlantBomb
&& getCurrentTaskId () != Task::DefuseBomb) || bots.isRoundOver ()) { && getCurrentTaskId () != Task::DefuseBomb) || gameState.isRoundOver ()) {
if (!m_reloadState) { if (!m_reloadState) {
m_reloadState = Reload::Primary; m_reloadState = Reload::Primary;
@ -671,7 +671,7 @@ Vector Bot::getBodyOffsetError (float distance) {
rg (mins.y * hitError, maxs.y * hitError), rg (mins.y * hitError, maxs.y * hitError),
rg (mins.z * hitError * 0.5f, maxs.z * hitError * 0.5f)); rg (mins.z * hitError * 0.5f, maxs.z * hitError * 0.5f));
const auto &aimError = conf.getDifficultyTweaks (m_difficulty)->aimError; const auto &aimError = m_difficultyData->aimError;
m_aimLastError += Vector (rg (-aimError.x, aimError.x), rg (-aimError.y, aimError.y), rg (-aimError.z, aimError.z)); m_aimLastError += Vector (rg (-aimError.x, aimError.x), rg (-aimError.y, aimError.y), rg (-aimError.z, aimError.z));
m_aimErrorTime = game.time () + rg (0.4f, 0.8f); m_aimErrorTime = game.time () + rg (0.4f, 0.8f);
@ -710,14 +710,19 @@ Vector Bot::getEnemyBodyOffset () {
compensation.clear (); compensation.clear ();
} }
// get the correct head origin
const auto &headOrigin = [&] (edict_t *e, const float distance) -> Vector {
return Vector { e->v.origin.x, e->v.origin.y, e->v.absmin.z + e->v.size.z * 0.81f } + getCustomHeight (distance);
};
// if we only suspect an enemy behind a wall take the worst skill // if we only suspect an enemy behind a wall take the worst skill
if (!m_enemyParts && (m_states & Sense::SuspectEnemy)) { if (!m_enemyParts && (m_states & Sense::SuspectEnemy)) {
spot += getBodyOffsetError (distance); spot += getBodyOffsetError (distance);
} }
else if (util.isPlayer (m_enemy)) { else if (game.isPlayerEntity (m_enemy)) {
// now take in account different parts of enemy body // now take in account different parts of enemy body
if (m_enemyParts & (Visibility::Head | Visibility::Body)) { if (m_enemyParts & (Visibility::Head | Visibility::Body)) {
auto headshotPct = conf.getDifficultyTweaks (m_difficulty)->headshotPct; auto headshotPct = m_difficultyData->headshotPct;
// with to much recoil or using specific weapons choice to aim to the chest // with to much recoil or using specific weapons choice to aim to the chest
if (distance > kSprayDistance && (isRecoilHigh () || usesShotgun ())) { if (distance > kSprayDistance && (isRecoilHigh () || usesShotgun ())) {
@ -728,11 +733,13 @@ Vector Bot::getEnemyBodyOffset () {
} }
// now check is our skill match to aim at head, else aim at enemy body // now check is our skill match to aim at head, else aim at enemy body
if (m_enemyBodyPartSet == m_enemy || rg.chance (headshotPct)) { if (m_enemyBodyPartSet == m_enemy
spot = m_enemyOrigin + getCustomHeight (distance); || ((m_enemyBodyPartSet != m_enemy) && rg.chance (headshotPct))) {
spot = headOrigin (m_enemy, distance);
if (usesSniper ()) { if (usesSniper ()) {
spot.z -= pev->view_ofs.z * 0.5f; spot.z -= pev->view_ofs.z * 0.35f;
} }
// set's the enemy shooting spot to head, if headshot pct allows, and use head for that // set's the enemy shooting spot to head, if headshot pct allows, and use head for that
@ -741,6 +748,10 @@ Vector Bot::getEnemyBodyOffset () {
} }
else { else {
spot = m_enemy->v.origin; spot = m_enemy->v.origin;
if (m_difficulty == Difficulty::Expert) {
spot.z += pev->view_ofs.z * 0.35f;
}
} }
} }
else if (m_enemyParts & Visibility::Body) { else if (m_enemyParts & Visibility::Body) {
@ -750,10 +761,10 @@ Vector Bot::getEnemyBodyOffset () {
spot = m_enemyOrigin; spot = m_enemyOrigin;
} }
else if (m_enemyParts & Visibility::Head) { else if (m_enemyParts & Visibility::Head) {
spot = m_enemyOrigin + getCustomHeight (distance); spot = headOrigin (m_enemy, distance);
} }
} }
auto idealSpot = m_enemyOrigin; auto idealSpot = spot;
if (m_difficulty < Difficulty::Hard && isEnemyInSight (idealSpot)) { if (m_difficulty < Difficulty::Hard && isEnemyInSight (idealSpot)) {
spot = idealSpot + ((spot - idealSpot) * 0.005f); // gradually adjust the aiming direction spot = idealSpot + ((spot - idealSpot) * 0.005f); // gradually adjust the aiming direction
@ -817,11 +828,11 @@ bool Bot::isFriendInLineOfFire (float distance) const {
game.testLine (getEyesPos (), getEyesPos () + pev->v_angle.normalize_apx () * distance, TraceIgnore::None, ent (), &tr); game.testLine (getEyesPos (), getEyesPos () + pev->v_angle.normalize_apx () * distance, TraceIgnore::None, ent (), &tr);
// check if we hit something // check if we hit something
if (util.isPlayer (tr.pHit) && tr.pHit != ent ()) { if (game.isPlayerEntity (tr.pHit) && tr.pHit != ent ()) {
auto hit = tr.pHit; auto hit = tr.pHit;
// check valid range // check valid range
if (game.getTeam (hit) == m_team && util.isAlive (hit)) { if (game.getPlayerTeam (hit) == m_team && game.isAliveEntity (hit)) {
return true; return true;
} }
} }
@ -1003,7 +1014,7 @@ bool Bot::needToPauseFiring (float distance) {
const float tolerance = (100.0f - static_cast <float> (m_difficulty) * 25.0f) / 99.0f; const float tolerance = (100.0f - static_cast <float> (m_difficulty) * 25.0f) / 99.0f;
const float baseTime = distance > kSprayDistance ? 0.55f : 0.38f; const float baseTime = distance > kSprayDistance ? 0.55f : 0.38f;
const float maxRecoil = static_cast <float> (conf.getDifficultyTweaks (m_difficulty)->maxRecoil); const float maxRecoil = static_cast <float> (m_difficultyData->maxRecoil);
// check if we need to compensate recoil // check if we need to compensate recoil
if (cr::tanf (cr::sqrtf (cr::abs (xPunch) + cr::abs (yPunch))) * distance > offset + maxRecoil + tolerance) { if (cr::tanf (cr::sqrtf (cr::abs (xPunch) + cr::abs (yPunch))) * distance > offset + maxRecoil + tolerance) {
@ -1079,7 +1090,7 @@ bool Bot::checkZoom (float distance) {
return zoomChange; return zoomChange;
} }
void Bot::selectWeapons (float distance, int, int id, int choosen) { void Bot::handleWeapons (float distance, int, int id, int choosen) {
const auto tab = conf.getRawWeapons (); const auto tab = conf.getRawWeapons ();
// we want to fire weapon, don't reload now // we want to fire weapon, don't reload now
@ -1249,7 +1260,7 @@ void Bot::fireWeapons () {
// if knife mode use knife only // if knife mode use knife only
if (isKnifeMode ()) { if (isKnifeMode ()) {
selectWeapons (distance, selectIndex, selectId, choosenWeapon); handleWeapons (distance, selectIndex, selectId, choosenWeapon);
return; return;
} }
@ -1263,7 +1274,7 @@ void Bot::fireWeapons () {
&& !isGroupOfEnemies (pev->origin) && !isGroupOfEnemies (pev->origin)
&& getCurrentTaskId () != Task::Camp) { && getCurrentTaskId () != Task::Camp) {
selectWeapons (distance, selectIndex, selectId, choosenWeapon); handleWeapons (distance, selectIndex, selectId, choosenWeapon);
return; return;
} }
@ -1316,7 +1327,7 @@ void Bot::fireWeapons () {
} }
selectId = Weapon::Knife; // no available ammo, use knife! selectId = Weapon::Knife; // no available ammo, use knife!
} }
selectWeapons (distance, selectIndex, selectId, choosenWeapon); handleWeapons (distance, selectIndex, selectId, choosenWeapon);
} }
bool Bot::isWeaponBadAtDistance (int weaponIndex, float distance) { bool Bot::isWeaponBadAtDistance (int weaponIndex, float distance) {
@ -1448,7 +1459,7 @@ void Bot::attackMovement () {
if (!game.is (GameFlags::CSDM) && !isKnifeMode ()) { if (!game.is (GameFlags::CSDM) && !isKnifeMode ()) {
if ((m_states & Sense::SeeingEnemy) if ((m_states & Sense::SeeingEnemy)
&& approach < 30 && approach < 30
&& !bots.isBombPlanted () && !gameState.isBombPlanted ()
&& (isEnemyCone || m_isVIP || m_isReloading)) { && (isEnemyCone || m_isVIP || m_isReloading)) {
if (m_retreatTime < game.time ()) { if (m_retreatTime < game.time ()) {
@ -1635,8 +1646,7 @@ void Bot::attackMovement () {
const int enemyNearestIndex = graph.getNearest (m_enemy->v.origin); const int enemyNearestIndex = graph.getNearest (m_enemy->v.origin);
if (vistab.visible (m_currentNodeIndex, enemyNearestIndex, VisIndex::Crouch) if (vistab.visibleBothSides (m_currentNodeIndex, enemyNearestIndex, VisIndex::Crouch)) {
&& vistab.visible (enemyNearestIndex, m_currentNodeIndex, VisIndex::Crouch)) {
m_duckTime = game.time () + m_frameInterval * 3.0f; m_duckTime = game.time () + m_frameInterval * 3.0f;
} }
} }
@ -1962,7 +1972,7 @@ void Bot::decideFollowUser () {
continue; continue;
} }
if (seesEntity (client.origin) && !util.isFakeClient (client.ent)) { if (seesEntity (client.origin) && !game.isFakeClientEntity (client.ent)) {
users.push (client.ent); users.push (client.ent);
} }
} }
@ -2237,7 +2247,7 @@ edict_t *Bot::setCorrectGrenadeVelocity (StringRef model) {
edict_t *result = nullptr; edict_t *result = nullptr;
game.searchEntities ("classname", "grenade", [&] (edict_t *ent) { game.searchEntities ("classname", "grenade", [&] (edict_t *ent) {
if (ent->v.owner == this->ent () && util.isModel (ent, model)) { if (ent->v.owner == this->ent () && game.isEntityModelMatches (ent, model)) {
result = ent; result = ent;
// set the correct velocity for the grenade // set the correct velocity for the grenade
@ -2275,7 +2285,7 @@ void Bot::checkGrenadesThrow () {
|| cv_ignore_enemies || cv_ignore_enemies
|| m_isUsingGrenade || m_isUsingGrenade
|| m_isReloading || m_isReloading
|| (isKnifeMode () && !bots.isBombPlanted ()) || (isKnifeMode () && !gameState.isBombPlanted ())
|| m_grenadeCheckTime >= game.time () || m_grenadeCheckTime >= game.time ()
|| m_lastEnemyOrigin.empty ()); || m_lastEnemyOrigin.empty ());
@ -2289,7 +2299,7 @@ void Bot::checkGrenadesThrow () {
const auto senseCondition = isGrenadeMode ? false : !(m_states & (Sense::SuspectEnemy | Sense::HearingEnemy)); const auto senseCondition = isGrenadeMode ? false : !(m_states & (Sense::SuspectEnemy | Sense::HearingEnemy));
if (!util.isAlive (m_lastEnemy) || senseCondition) { if (!game.isAliveEntity (m_lastEnemy) || senseCondition) {
clearThrowStates (m_states); clearThrowStates (m_states);
return; return;
} }
@ -2332,7 +2342,7 @@ void Bot::checkGrenadesThrow () {
// special condition if we're have valid current enemy // special condition if we're have valid current enemy
if (!isGrenadeMode && ((m_states & Sense::SeeingEnemy) if (!isGrenadeMode && ((m_states & Sense::SeeingEnemy)
&& util.isAlive (m_enemy) && game.isAliveEntity (m_enemy)
&& ((m_enemy->v.button | m_enemy->v.oldbuttons) & IN_ATTACK) && ((m_enemy->v.button | m_enemy->v.oldbuttons) & IN_ATTACK)
&& util.isVisible (pev->origin, m_enemy)) && util.isVisible (pev->origin, m_enemy))
&& util.isInViewCone (pev->origin, m_enemy)) { && util.isInViewCone (pev->origin, m_enemy)) {

View file

@ -350,9 +350,9 @@ void BotConfig::loadChatterConfig () {
{ "Chatter_BombSiteSecured", Chatter::BombsiteSecured, 3.5f }, { "Chatter_BombSiteSecured", Chatter::BombsiteSecured, 3.5f },
{ "Chatter_GoingToCamp", Chatter::GoingToCamp, 30.0f }, { "Chatter_GoingToCamp", Chatter::GoingToCamp, 30.0f },
{ "Chatter_Camp", Chatter::Camping, 10.0f }, { "Chatter_Camp", Chatter::Camping, 10.0f },
{ "Chatter_OnARoll", Chatter::OnARoll, kMaxChatterRepeatInterval}, { "Chatter_OnARoll", Chatter::OnARoll, kMaxChatterRepeatInterval },
}; };
Array <String> badFiles {}; Array <String> missingWaves {};
while (file.getLine (line)) { while (file.getLine (line)) {
line.trim (); line.trim ();
@ -394,7 +394,7 @@ void BotConfig::loadChatterConfig () {
m_chatter[event.code].emplace (cr::move (sound), event.repeat, duration); m_chatter[event.code].emplace (cr::move (sound), event.repeat, duration);
} }
else { else {
badFiles.push (sound); missingWaves.push (sound);
} }
} }
sentences.clear (); sentences.clear ();
@ -404,8 +404,16 @@ void BotConfig::loadChatterConfig () {
} }
file.close (); file.close ();
if (!badFiles.empty ()) { if (!missingWaves.empty ()) {
game.print ("Warning: Couldn't get duration of next chatter sounds: %s.", String::join (badFiles, ",")); constexpr auto kMaxErroredWaves = 10;
// too much erros bail out
if (missingWaves.length () > kMaxErroredWaves) {
cv_radio_mode.set (1);
missingWaves.resize (kMaxErroredWaves);
}
game.print ("Warning: Couldn't get duration of next chatter sounds: %s ...", String::join (missingWaves, ","));
} }
} }
else { else {
@ -722,7 +730,8 @@ void BotConfig::loadCustomConfig () {
{ "ZMInfectedTeam", "T" }, { "ZMInfectedTeam", "T" },
{ "EnableFakeBotFeatures", "no" }, { "EnableFakeBotFeatures", "no" },
{ "DisableLogFile", "no" }, { "DisableLogFile", "no" },
{ "CheckConnectivityHost", "yapb.jeefo.net" } { "CheckConnectivityHost", "yapb.jeefo.net" },
{ "DisableSpawnControl", "no" }
}; };
}; };
setDefaults (); setDefaults ();

View file

@ -183,7 +183,7 @@ int BotControl::cmdMenu () {
// reset the current menu // reset the current menu
closeMenu (); closeMenu ();
if (arg <StringRef> (cmd) == "cmd" && util.isAlive (m_ent)) { if (arg <StringRef> (cmd) == "cmd" && game.isAliveEntity (m_ent)) {
showMenu (Menu::Commands); showMenu (Menu::Commands);
} }
else { else {
@ -205,7 +205,7 @@ int BotControl::cmdCvars () {
auto match = arg <StringRef> (pattern); auto match = arg <StringRef> (pattern);
// stop printing if executed once more // stop printing if executed once more
flushPrintQueue (); m_printQueue.clear ();
// revert all the cvars to their default values // revert all the cvars to their default values
if (match == "defaults") { if (match == "defaults") {
@ -423,6 +423,7 @@ int BotControl::cmdNode () {
addGraphCmd ("stats", "stats [noarguments]", "Shows the stats about node types on the map.", &BotControl::cmdNodeShowStats); addGraphCmd ("stats", "stats [noarguments]", "Shows the stats about node types on the map.", &BotControl::cmdNodeShowStats);
addGraphCmd ("fileinfo", "fileinfo [noarguments]", "Shows basic information about graph file.", &BotControl::cmdNodeFileInfo); addGraphCmd ("fileinfo", "fileinfo [noarguments]", "Shows basic information about graph file.", &BotControl::cmdNodeFileInfo);
addGraphCmd ("adjust_height", "adjust_height [height offset]", "Modifies all the graph nodes height (z-component) with specified offset.", &BotControl::cmdNodeAdjustHeight); addGraphCmd ("adjust_height", "adjust_height [height offset]", "Modifies all the graph nodes height (z-component) with specified offset.", &BotControl::cmdNodeAdjustHeight);
addGraphCmd ("refresh", "refresh [noarguments]", "Deletes a current graph and downloads one from graph database.", &BotControl::cmdNodeRefresh);
// add path commands // add path commands
addGraphCmd ("path_create", "path_create [noarguments]", "Opens and displays path creation menu.", &BotControl::cmdNodePathCreate); addGraphCmd ("path_create", "path_create [noarguments]", "Opens and displays path creation menu.", &BotControl::cmdNodePathCreate);
@ -598,6 +599,13 @@ int BotControl::cmdNodeAddBasic () {
int BotControl::cmdNodeSave () { int BotControl::cmdNodeSave () {
enum args { graph_cmd = 1, cmd, option }; enum args { graph_cmd = 1, cmd, option };
// prevent some commands while analyzing graph
if (analyzer.isAnalyzing ()) {
msg ("This command is unavailable while map analysis is ongoing.");
return BotCommandResult::Handled;
}
// if no check is set save anyway // if no check is set save anyway
if (arg <StringRef> (option) == "nocheck") { if (arg <StringRef> (option) == "nocheck") {
graph.saveGraphData (); graph.saveGraphData ();
@ -629,6 +637,13 @@ int BotControl::cmdNodeSave () {
int BotControl::cmdNodeLoad () { int BotControl::cmdNodeLoad () {
enum args { graph_cmd = 1, cmd }; enum args { graph_cmd = 1, cmd };
// prevent some commands while analyzing graph
if (analyzer.isAnalyzing ()) {
msg ("This command is unavailable while map analysis is ongoing.");
return BotCommandResult::Handled;
}
// just save graph on request // just save graph on request
if (graph.loadGraphData ()) { if (graph.loadGraphData ()) {
msg ("Graph successfully loaded."); msg ("Graph successfully loaded.");
@ -642,6 +657,13 @@ int BotControl::cmdNodeLoad () {
int BotControl::cmdNodeErase () { int BotControl::cmdNodeErase () {
enum args { graph_cmd = 1, cmd, iamsure }; enum args { graph_cmd = 1, cmd, iamsure };
// prevent some commands while analyzing graph
if (analyzer.isAnalyzing ()) {
msg ("This command is unavailable while map analysis is ongoing.");
return BotCommandResult::Handled;
}
// prevent accidents when graph are deleted unintentionally // prevent accidents when graph are deleted unintentionally
if (arg <StringRef> (iamsure) == "iamsure") { if (arg <StringRef> (iamsure) == "iamsure") {
bstor.unlinkFromDisk (false, false); bstor.unlinkFromDisk (false, false);
@ -652,6 +674,26 @@ int BotControl::cmdNodeErase () {
return BotCommandResult::Handled; return BotCommandResult::Handled;
} }
int BotControl::cmdNodeRefresh () {
enum args { graph_cmd = 1, cmd, iamsure };
if (!graph.canDownload ()) {
msg ("Can't sync graph with database while graph url is not set.");
return BotCommandResult::Handled;
}
// prevent accidents when graph are deleted unintentionally
if (arg <StringRef> (iamsure) == "iamsure") {
bstor.unlinkFromDisk (false, false);
graph.loadGraphData ();
}
else {
msg ("Please, append \"iamsure\" as parameter to get graph refreshed from the graph database.");
}
return BotCommandResult::Handled;
}
int BotControl::cmdNodeEraseTraining () { int BotControl::cmdNodeEraseTraining () {
enum args { graph_cmd = 1, cmd }; enum args { graph_cmd = 1, cmd };
@ -1115,7 +1157,7 @@ int BotControl::menuFeatures (int item) {
break; break;
case 5: case 5:
if (util.isAlive (m_ent)) { if (game.isAliveEntity (m_ent)) {
showMenu (Menu::Commands); showMenu (Menu::Commands);
} }
else { else {
@ -1924,7 +1966,7 @@ bool BotControl::executeCommands () {
} }
bool BotControl::executeMenus () { bool BotControl::executeMenus () {
if (!util.isPlayer (m_ent) || game.isBotCmd ()) { if (!game.isPlayerEntity (m_ent) || game.isBotCmd ()) {
return false; return false;
} }
const auto &issuer = util.getClient (game.indexOfPlayer (m_ent)); const auto &issuer = util.getClient (game.indexOfPlayer (m_ent));
@ -1966,7 +2008,7 @@ void BotControl::showMenu (int id) {
menusParsed = true; menusParsed = true;
} }
if (!util.isPlayer (m_ent)) { if (!game.isPlayerEntity (m_ent)) {
return; return;
} }
auto &client = util.getClient (game.indexOfPlayer (m_ent)); auto &client = util.getClient (game.indexOfPlayer (m_ent));
@ -2007,7 +2049,7 @@ void BotControl::showMenu (int id) {
} }
void BotControl::closeMenu () { void BotControl::closeMenu () {
if (!util.isPlayer (m_ent)) { if (!game.isPlayerEntity (m_ent)) {
return; return;
} }
auto &client = util.getClient (game.indexOfPlayer (m_ent)); auto &client = util.getClient (game.indexOfPlayer (m_ent));
@ -2073,7 +2115,7 @@ void BotControl::kickBotByMenu (int page) {
} }
void BotControl::assignAdminRights (edict_t *ent, char *infobuffer) { void BotControl::assignAdminRights (edict_t *ent, char *infobuffer) {
if (!game.isDedicated () || util.isFakeClient (ent)) { if (!game.isDedicated () || game.isFakeClientEntity (ent)) {
return; return;
} }
StringRef key = cv_password_key.as <StringRef> (); StringRef key = cv_password_key.as <StringRef> ();
@ -2100,7 +2142,7 @@ void BotControl::maintainAdminRights () {
StringRef password = cv_password.as <StringRef> (); StringRef password = cv_password.as <StringRef> ();
for (auto &client : util.getClients ()) { for (auto &client : util.getClients ()) {
if (!(client.flags & ClientFlags::Used) || util.isFakeClient (client.ent)) { if (!(client.flags & ClientFlags::Used) || game.isFakeClientEntity (client.ent)) {
continue; continue;
} }
auto ent = client.ent; auto ent = client.ent;
@ -2151,21 +2193,114 @@ BotControl::BotControl () {
m_menuServerFillTeam = 5; m_menuServerFillTeam = 5;
m_printQueueFlushTimestamp = 0.0f; m_printQueueFlushTimestamp = 0.0f;
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 = {
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/kickall_ct/kickall_t", "removebots [instant] [team]", "Kicks all the bots from the game.", &BotControl::cmdKickBots); "add/addbot/add_ct/addbot_ct/add_t/addbot_t/addhs/addhs_t/addhs_ct",
m_cmds.emplace ("kill/killbots/killall/kill_ct/kill_t", "kill [team] [silent]", "Kills the specified team / all the bots.", &BotControl::cmdKillBots); "add [difficulty] [personality] [team] [model] [name]",
m_cmds.emplace ("fill/fillserver", "fill [team] [count] [difficulty] [personality]", "Fill the server (add bots) with specified parameters.", &BotControl::cmdFill); "Adding specific bot into the game.",
m_cmds.emplace ("vote/votemap", "vote [map_id]", "Forces all the bot to vote to specified map.", &BotControl::cmdVote);
m_cmds.emplace ("weapons/weaponmode", "weapons [knife|pistol|shotgun|smg|rifle|sniper|standard]", "Sets the bots weapon mode to use.", &BotControl::cmdWeaponMode); &BotControl::cmdAddBot
m_cmds.emplace ("menu/botmenu", "menu [cmd]", "Opens the main bot menu, or command menu if specified.", &BotControl::cmdMenu); },
m_cmds.emplace ("version/ver/about", "version [no arguments]", "Displays version information about bot build.", &BotControl::cmdVersion); {
m_cmds.emplace ("graphmenu/wpmenu/wptmenu", "graphmenu [noarguments]", "Opens and displays bots graph editor.", &BotControl::cmdNodeMenu); "kick/kickone/kick_ct/kick_t/kickbot_ct/kickbot_t",
m_cmds.emplace ("list/listbots", "list [noarguments]", "Lists the bots currently playing on server.", &BotControl::cmdList); "kick [team]",
m_cmds.emplace ("graph/g/w/wp/wpt/waypoint", "graph [help]", "Handles graph operations.", &BotControl::cmdNode); "Kicks off the random bot from the game.",
m_cmds.emplace ("cvars", "cvars [save|save_map|cvar|defaults]", "Display all the cvars with their descriptions.", &BotControl::cmdCvars);
m_cmds.emplace ("show_custom", "show_custom [noarguments]", "Shows the current values from custom.cfg.", &BotControl::cmdShowCustom, false); &BotControl::cmdKickBot
m_cmds.emplace ("exec", "exec [user_id] [command]", "Executes a client command on bot entity.", &BotControl::cmdExec); },
{
"removebots/kickbots/kickall/kickall_ct/kickall_t",
"removebots [instant] [team]",
"Kicks all the bots from the game.",
&BotControl::cmdKickBots
},
{
"kill/killbots/killall/kill_ct/kill_t",
"kill [team] [silent]",
"Kills the specified team / all the bots.",
&BotControl::cmdKillBots
},
{
"fill/fillserver",
"fill [team] [count] [difficulty] [personality]",
"Fill the server (add bots) with specified parameters.",
&BotControl::cmdFill
},
{
"vote/votemap",
"vote [map_id]",
"Forces all the bots to vote for the specified map.",
&BotControl::cmdVote
},
{
"weapons/weaponmode",
"weapons [knife|pistol|shotgun|smg|rifle|sniper|standard]",
"Sets the bots' weapon mode to use.",
&BotControl::cmdWeaponMode
},
{
"menu/botmenu",
"menu [cmd]",
"Opens the main bot menu, or command menu if specified.",
&BotControl::cmdMenu
},
{
"version/ver/about",
"version [no arguments]",
"Displays version information about bot build.",
&BotControl::cmdVersion
},
{
"graphmenu/wpmenu/wptmenu",
"graphmenu [noarguments]",
"Opens and displays bots graph editor.",
&BotControl::cmdNodeMenu
},
{
"list/listbots",
"list [noarguments]",
"Lists the bots currently playing on server.",
&BotControl::cmdList
},
{
"graph/g/w/wp/wpt/waypoint",
"graph [help]",
"Handles graph operations.",
&BotControl::cmdNode
},
{
"cvars",
"cvars [save|save_map|cvar|defaults]",
"Display all the cvars with their descriptions.",
&BotControl::cmdCvars
},
{
"show_custom",
"show_custom [noarguments]",
"Shows the current values from custom.cfg.",
&BotControl::cmdShowCustom,
false
},
{
"exec",
"exec [user_id] [command]",
"Executes a client command on bot entity.",
&BotControl::cmdExec
}
};
// declare the menus // declare the menus
createMenus (); createMenus ();

View file

@ -12,6 +12,7 @@ ConVar cv_ignore_map_prefix_game_mode ("ignore_map_prefix_game_mode", "0", "If e
ConVar cv_threadpool_workers ("threadpool_workers", "-1", "Maximum number of threads the bot will run to process some tasks. -1 means half of the CPU cores are used.", true, -1.0f, static_cast <float> (plat.hardwareConcurrency ())); ConVar cv_threadpool_workers ("threadpool_workers", "-1", "Maximum number of threads the bot will run to process some tasks. -1 means half of the CPU cores are used.", true, -1.0f, static_cast <float> (plat.hardwareConcurrency ()));
ConVar cv_grenadier_mode ("grenadier_mode", "0", "If enabled, bots will not apply throwing conditions on grenades."); ConVar cv_grenadier_mode ("grenadier_mode", "0", "If enabled, bots will not apply throwing conditions on grenades.");
ConVar cv_ignore_enemies_after_spawn_time ("ignore_enemies_after_spawn_time", "0", "Makes bots ignore enemies for a specified time in seconds on a new round. Useful for Zombie Plague mods.", false); ConVar cv_ignore_enemies_after_spawn_time ("ignore_enemies_after_spawn_time", "0", "Makes bots ignore enemies for a specified time in seconds on a new round. Useful for Zombie Plague mods.", false);
ConVar cv_breakable_health_limit ("breakable_health_limit", "500.0", "Specifies the maximum health of a breakable object that the bot will consider destroying.", true, 1.0f, 3000.0);
ConVar sv_skycolor_r ("sv_skycolor_r", nullptr, Var::GameRef); ConVar sv_skycolor_r ("sv_skycolor_r", nullptr, Var::GameRef);
ConVar sv_skycolor_g ("sv_skycolor_g", nullptr, Var::GameRef); ConVar sv_skycolor_g ("sv_skycolor_g", nullptr, Var::GameRef);
@ -60,9 +61,6 @@ void Game::levelInitialize (edict_t *entities, int max) {
// startup threaded worker // startup threaded worker
worker.startup (cv_threadpool_workers.as <int> ()); worker.startup (cv_threadpool_workers.as <int> ());
m_spawnCount[Team::CT] = 0;
m_spawnCount[Team::Terrorist] = 0;
// clear all breakables before initialization // clear all breakables before initialization
m_breakables.clear (); m_breakables.clear ();
m_checkedBreakables.clear (); m_checkedBreakables.clear ();
@ -77,7 +75,7 @@ void Game::levelInitialize (edict_t *entities, int max) {
conf.loadMainConfig (); conf.loadMainConfig ();
// ensure the server admin is confident about features he's using // ensure the server admin is confident about features he's using
game.ensureHealthyGameEnvironment (); ensureHealthyGameEnvironment ();
// load map-specific config // load map-specific config
conf.loadMapSpecificConfig (); conf.loadMapSpecificConfig ();
@ -125,20 +123,16 @@ void Game::levelInitialize (edict_t *entities, int max) {
ent->v.rendermode = kRenderTransAlpha; // set its render mode to transparency ent->v.rendermode = kRenderTransAlpha; // set its render mode to transparency
ent->v.renderamt = 127; // set its transparency amount ent->v.renderamt = 127; // set its transparency amount
ent->v.effects |= EF_NODRAW; ent->v.effects |= EF_NODRAW;
++m_spawnCount[Team::CT];
} }
else if (classname == "info_player_deathmatch") { else if (classname == "info_player_deathmatch") {
ent->v.rendermode = kRenderTransAlpha; // set its render mode to transparency ent->v.rendermode = kRenderTransAlpha; // set its render mode to transparency
ent->v.renderamt = 127; // set its transparency amount ent->v.renderamt = 127; // set its transparency amount
ent->v.effects |= EF_NODRAW; ent->v.effects |= EF_NODRAW;
++m_spawnCount[Team::Terrorist];
} }
else if (classname == "func_vip_safetyzone" || classname == "info_vip_safetyzone") { else if (classname == "func_vip_safetyzone" || classname == "info_vip_safetyzone") {
m_mapFlags |= MapFlags::Assassination; // assassination map m_mapFlags |= MapFlags::Assassination; // assassination map
} }
else if (util.isHostageEntity (ent)) { else if (isHostageEntity (ent)) {
m_mapFlags |= MapFlags::HostageRescue; // rescue map m_mapFlags |= MapFlags::HostageRescue; // rescue map
} }
else if (classname == "func_bomb_target" || classname == "info_bomb_target") { else if (classname == "func_bomb_target" || classname == "info_bomb_target") {
@ -152,13 +146,13 @@ void Game::levelInitialize (edict_t *entities, int max) {
m_mapFlags &= ~MapFlags::HostageRescue; m_mapFlags &= ~MapFlags::HostageRescue;
} }
} }
else if (util.isDoorEntity (ent)) { else if (isDoorEntity (ent)) {
m_mapFlags |= MapFlags::HasDoors; m_mapFlags |= MapFlags::HasDoors;
} }
else if (classname.startsWith ("func_button")) { else if (classname.startsWith ("func_button")) {
m_mapFlags |= MapFlags::HasButtons; m_mapFlags |= MapFlags::HasButtons;
} }
else if (util.isBreakableEntity (ent, true)) { else if (isBreakableEntity (ent, true)) {
// add breakable for material check // add breakable for material check
m_checkedBreakables[indexOfEntity (ent)] = ent->v.impulse <= 0; m_checkedBreakables[indexOfEntity (ent)] = ent->v.impulse <= 0;
@ -186,6 +180,24 @@ void Game::levelInitialize (edict_t *entities, int max) {
m_halfSecondFrame = 0.0f; m_halfSecondFrame = 0.0f;
} }
void Game::onSpawnEntity (edict_t *ent) {
constexpr auto kEntityInfoPlayerStart = StringRef::fnv1a32 ("info_player_start");
constexpr auto kEntityInfoVIPStart = StringRef::fnv1a32 ("info_vip_start");
constexpr auto kEntityInfoPlayerDeathmatch = StringRef::fnv1a32 ("info_player_deathmatch");
if (game.isNullEntity (ent) || ent->v.classname == 0) {
return;
}
const auto classNameHash = ent->v.classname.str ().hash ();
if (classNameHash == kEntityInfoPlayerStart || classNameHash == kEntityInfoVIPStart) {
++m_spawnCount[Team::CT];
}
else if (classNameHash == kEntityInfoPlayerDeathmatch) {
++m_spawnCount[Team::Terrorist];
}
}
void Game::levelShutdown () { void Game::levelShutdown () {
// save collected practice on shutdown // save collected practice on shutdown
practice.save (); practice.save ();
@ -197,12 +209,12 @@ void Game::levelShutdown () {
bots.destroyKillerEntity (); bots.destroyKillerEntity ();
// ensure players are off on xash3d // ensure players are off on xash3d
if (game.is (GameFlags::Xash3DLegacy)) { if (is (GameFlags::Xash3DLegacy)) {
bots.kickEveryone (true, false); bots.kickEveryone (true, false);
} }
// set state to unprecached // set state to unprecached
game.setUnprecached (); setUnprecached ();
// enable lightstyle animations on level change // enable lightstyle animations on level change
illum.enableAnimation (true); illum.enableAnimation (true);
@ -211,7 +223,7 @@ void Game::levelShutdown () {
util.setNeedForWelcome (false); util.setNeedForWelcome (false);
// clear local entity // clear local entity
game.setLocalEntity (nullptr); setLocalEntity (nullptr);
// reset graph state // reset graph state
graph.reset (); graph.reset ();
@ -222,6 +234,10 @@ void Game::levelShutdown () {
// disable command handling // disable command handling
ctrl.setDenyCommands (true); ctrl.setDenyCommands (true);
// reset spawn counts
for (auto &sc : m_spawnCount) {
sc = 0;
}
} }
void Game::drawLine (edict_t *ent, const Vector &start, const Vector &end, int width, int noise, const Color &color, int brightness, int speed, int life, DrawLine type) const { void Game::drawLine (edict_t *ent, const Vector &start, const Vector &end, int width, int noise, const Color &color, int brightness, int speed, int life, DrawLine type) const {
@ -229,7 +245,7 @@ void Game::drawLine (edict_t *ent, const Vector &start, const Vector &end, int w
// is pointed to by ent, from the vector location start to the vector location end, // is pointed to by ent, from the vector location start to the vector location end,
// which is supposed to last life tenths seconds, and having the color defined by RGB. // which is supposed to last life tenths seconds, and having the color defined by RGB.
if (!util.isPlayer (ent)) { if (!isPlayerEntity (ent)) {
return; // reliability check return; // reliability check
} }
@ -367,13 +383,13 @@ void Game::playSound (edict_t *ent, const char *sound) {
void Game::setPlayerStartDrawModels () { void Game::setPlayerStartDrawModels () {
static HashMap <String, String> models { static HashMap <String, String> models {
{"info_player_start", "models/player/urban/urban.mdl"}, { "info_player_start", "models/player/urban/urban.mdl" },
{"info_player_deathmatch", "models/player/terror/terror.mdl"}, { "info_player_deathmatch", "models/player/terror/terror.mdl" },
{"info_vip_start", "models/player/vip/vip.mdl"} { "info_vip_start", "models/player/vip/vip.mdl" }
}; };
models.foreach ([&] (const String &key, const String &val) { models.foreach ([&] (const String &key, const String &val) {
game.searchEntities ("classname", key, [&] (edict_t *ent) { searchEntities ("classname", key, [&] (edict_t *ent) {
m_engineWrap.setModel (ent, val.chars ()); m_engineWrap.setModel (ent, val.chars ());
return EntitySearchResult::Continue; return EntitySearchResult::Continue;
}); });
@ -426,12 +442,12 @@ void Game::sendClientMessage (bool console, edict_t *ent, StringRef message) {
// helper to sending the client message // helper to sending the client message
// do not send messages to fake clients // do not send messages to fake clients
if (!util.isPlayer (ent) || util.isFakeClient (ent)) { if (!isPlayerEntity (ent) || isFakeClientEntity (ent)) {
return; return;
} }
// if console message and destination is listenserver entity, just print via server message instead of through unreliable channel // if console message and destination is listenserver entity, just print via server message instead of through unreliable channel
if (console && ent == game.getLocalEntity ()) { if (console && ent == getLocalEntity ()) {
sendServerMessage (message); sendServerMessage (message);
return; return;
} }
@ -482,7 +498,7 @@ void Game::sendServerMessage (StringRef message) {
void Game::sendHudMessage (edict_t *ent, const hudtextparms_t &htp, StringRef message) { void Game::sendHudMessage (edict_t *ent, const hudtextparms_t &htp, StringRef message) {
constexpr size_t kMaxSendLength = 512; constexpr size_t kMaxSendLength = 512;
if (game.isNullEntity (ent)) { if (isNullEntity (ent)) {
return; return;
} }
MessageWriter msg (MSG_ONE_UNRELIABLE, SVC_TEMPENTITY, nullptr, ent); MessageWriter msg (MSG_ONE_UNRELIABLE, SVC_TEMPENTITY, nullptr, ent);
@ -790,53 +806,53 @@ void Game::registerCvars (bool gameVars) {
} }
void Game::constructCSBinaryName (StringArray &libs) { void Game::constructCSBinaryName (StringArray &libs) {
String libSuffix {}; // construct library suffix String suffix {};
if (plat.android) { if (plat.android) {
libSuffix += "_android"; suffix = "_android";
if (plat.x64) {
suffix += "_arm64";
}
else if (plat.arm) {
suffix += "_armv7l";
}
} }
else if (plat.psvita) { else if (plat.psvita) {
libSuffix += "_psvita"; suffix = "_psvita";
} }
else if (plat.x64) {
if (plat.x64) {
if (plat.arm) { if (plat.arm) {
libSuffix += "_arm64"; suffix = "_arm64";
} }
else if (plat.ppc) { else if (plat.ppc) {
libSuffix += "_ppc64le"; suffix = "_ppc64le";
}
else if (plat.riscv) {
suffix = "_riscv64d";
} }
else { else {
libSuffix += "_amd64"; suffix = "_amd64";
} }
} }
else { else if (plat.arm) {
if (plat.arm) { // non-android arm32
// don't want to put whole build.h logic from xash3d, just set whatever is supported by the YaPB suffix = "_armv7hf";
if (plat.android) {
libSuffix += "_armv7l";
}
else {
libSuffix += "_armv7hf";
}
} }
else if (!plat.nix && !plat.win && !plat.macos) { else if (!plat.nix && !plat.win && !plat.macos) {
libSuffix += "_i386"; // fallback for unknown 32-bit x86 (e.g., legacy linux/bsd)
} suffix = "_i386";
} }
// else: suffix remains empty (e.g., x86 linux/windows/macos)
if (libSuffix.empty ()) // build base names
libs.insert (0, { "mp", "cs", "cs_i386" }); if (plat.android) {
// only "libcs" with suffix (no "mp", and must have "lib" prefix)
libs.push ("libcs" + suffix);
}
else { else {
// on Android, it's important to have `lib` prefix, otherwise package manager won't unpack the libraries // Standard: "mp" and "cs" with suffix
if (plat.android) libs.push ("cs" + suffix);
libs.insert (0, { "libcs" }); libs.push ("mp" + suffix);
else
libs.insert (0, { "mp", "cs" });
for (auto &lib : libs) {
lib += libSuffix;
}
} }
} }
@ -880,10 +896,10 @@ bool Game::loadCSBinary () {
} }
if (plat.emscripten) { if (plat.emscripten) {
path = String(plat.env ("XASH3D_GAMELIBPATH")); // defined by launcher path = String (plat.env ("XASH3D_GAMELIBPATH")); // defined by launcher
} }
if (path.empty()) { if (path.empty ()) {
path = strings.joinPath (modname, "dlls", lib) + kLibrarySuffix; path = strings.joinPath (modname, "dlls", lib) + kLibrarySuffix;
// if we can't read file, skip it // if we can't read file, skip it
@ -1006,7 +1022,7 @@ bool Game::postload () {
// register fake metamod command handler if we not! under mm // register fake metamod command handler if we not! under mm
if (!(is (GameFlags::Metamod))) { if (!(is (GameFlags::Metamod))) {
game.registerEngineCommand ("meta", [] () { registerEngineCommand ("meta", [] () {
game.print ("You're launched standalone version of %s. Metamod is not installed or not enabled!", product.name); game.print ("You're launched standalone version of %s. Metamod is not installed or not enabled!", product.name);
}); });
} }
@ -1126,7 +1142,7 @@ void Game::slowFrame () {
if (m_halfSecondFrame < time ()) { if (m_halfSecondFrame < time ()) {
// refresh bomb origin in case some plugin moved it out // refresh bomb origin in case some plugin moved it out
graph.setBombOrigin (); gameState.setBombOrigin ();
// ensure the server admin is confident about features he's using // ensure the server admin is confident about features he's using
ensureHealthyGameEnvironment (); ensureHealthyGameEnvironment ();
@ -1209,7 +1225,7 @@ void Game::searchEntities (const Vector &position, float radius, EntitySearch fu
} }
} }
bool Game::hasEntityInGame (StringRef classname) { bool Game::hasEntityInGame (StringRef classname) const {
return !isNullEntity (engfuncs.pfnFindEntityByString (nullptr, "classname", classname.chars ())); return !isNullEntity (engfuncs.pfnFindEntityByString (nullptr, "classname", classname.chars ()));
} }
@ -1367,6 +1383,120 @@ bool Game::isDeveloperMode () const {
return developer.exists () && developer.value () > 0.0f; return developer.exists () && developer.value () > 0.0f;
} }
bool Game::isAliveEntity (edict_t *ent) const {
if (isNullEntity (ent)) {
return false;
}
return ent->v.deadflag == DEAD_NO && ent->v.health > 0.0f && ent->v.movetype != MOVETYPE_NOCLIP;
}
bool Game::isPlayerEntity (edict_t *ent) const {
if (isNullEntity (ent)) {
return false;
}
if (ent->v.flags & FL_PROXY) {
return false;
}
if ((ent->v.flags & (FL_CLIENT | FL_FAKECLIENT)) || bots[ent] != nullptr) {
return !strings.isEmpty (ent->v.netname.chars ());
}
return false;
}
bool Game::isMonsterEntity (edict_t *ent) const {
if (isNullEntity (ent)) {
return false;
}
if (~ent->v.flags & FL_MONSTER) {
return false;
}
if (isHostageEntity (ent)) {
return false;
}
return true;
}
bool Game::isItemEntity (edict_t *ent) const {
return ent && ent->v.classname.str ().contains ("item_");
}
bool Game::isPlayerVIP (edict_t *ent) const {
if (!mapIs (MapFlags::Assassination)) {
return false;
}
if (!isPlayerEntity (ent)) {
return false;
}
return *(engfuncs.pfnInfoKeyValue (engfuncs.pfnGetInfoKeyBuffer (ent), "model")) == 'v';
}
bool Game::isDoorEntity (edict_t *ent) const {
if (isNullEntity (ent)) {
return false;
}
const auto classHash = ent->v.classname.str ().hash ();
constexpr auto kFuncDoor = StringRef::fnv1a32 ("func_door");
constexpr auto kFuncDoorRotating = StringRef::fnv1a32 ("func_door_rotating");
return classHash == kFuncDoor || classHash == kFuncDoorRotating;
}
bool Game::isHostageEntity (edict_t *ent) const {
if (isNullEntity (ent)) {
return false;
}
const auto classHash = ent->v.classname.str ().hash ();
constexpr auto kHostageEntity = StringRef::fnv1a32 ("hostage_entity");
constexpr auto kMonsterScientist = StringRef::fnv1a32 ("monster_scientist");
return classHash == kHostageEntity || classHash == kMonsterScientist;
}
bool Game::isBreakableEntity (edict_t *ent, bool initialSeed) const {
if (!initialSeed) {
if (!hasBreakables ()) {
return false;
}
}
if (isNullEntity (ent) || ent == getStartEntity () || (!initialSeed && !game.isBreakableValid (ent))) {
return false;
}
const auto limit = cv_breakable_health_limit.as <float> ();
// not shoot-able
if (ent->v.health < 5 || ent->v.health >= limit) {
return false;
}
constexpr auto kFuncBreakable = StringRef::fnv1a32 ("func_breakable");
constexpr auto kFuncPushable = StringRef::fnv1a32 ("func_pushable");
constexpr auto kFuncWall = StringRef::fnv1a32 ("func_wall");
if (ent->v.takedamage > 0.0f && ent->v.impulse <= 0 && !(ent->v.flags & FL_WORLDBRUSH) && !(ent->v.spawnflags & SF_BREAK_TRIGGER_ONLY)) {
const auto classHash = ent->v.classname.str ().hash ();
if (classHash == kFuncBreakable || (classHash == kFuncPushable && (ent->v.spawnflags & SF_PUSH_BREAKABLE)) || classHash == kFuncWall) {
return ent->v.movetype == MOVETYPE_PUSH || ent->v.movetype == MOVETYPE_PUSHSTEP;
}
}
return false;
}
bool Game::isFakeClientEntity (edict_t *ent) const {
return bots[ent] != nullptr || (!isNullEntity (ent) && (ent->v.flags & FL_FAKECLIENT));
}
bool Game::isEntityModelMatches (const edict_t *ent, StringRef model) const {
return model.startsWith (ent->v.model.chars (9));
}
void LightMeasure::initializeLightstyles () { void LightMeasure::initializeLightstyles () {
// this function initializes lighting information... // this function initializes lighting information...
@ -1588,7 +1718,7 @@ Vector PlayerHitboxEnumerator::get (edict_t *ent, int part, float updateTimestam
void PlayerHitboxEnumerator::update (edict_t *ent) { void PlayerHitboxEnumerator::update (edict_t *ent) {
constexpr auto kInvalidHitbox = -1; constexpr auto kInvalidHitbox = -1;
if (!util.isAlive (ent)) { if (!game.isAliveEntity (ent)) {
return; return;
} }
// get info about player // get info about player
@ -1677,3 +1807,143 @@ void PlayerHitboxEnumerator::reset () {
part = {}; part = {};
} }
} }
void GameState::setBombOrigin (bool reset, const Vector &pos) {
// this function stores the bomb position as a vector
if (!game.mapIs (MapFlags::Demolition) || !gameState.isBombPlanted ()) {
return;
}
if (reset) {
m_bombOrigin.clear ();
setBombPlanted (false);
return;
}
if (!pos.empty ()) {
m_bombOrigin = pos;
return;
}
bool wasFound = false;
auto bombModel = conf.getBombModelName ();
game.searchEntities ("classname", "grenade", [&] (edict_t *ent) {
if (game.isEntityModelMatches (ent, bombModel)) {
m_bombOrigin = game.getEntityOrigin (ent);
wasFound = true;
return EntitySearchResult::Break;
}
return EntitySearchResult::Continue;
});
if (!wasFound) {
m_bombOrigin.clear ();
setBombPlanted (false);
}
}
void GameState::roundStart () {
m_roundOver = false;
m_timeBombPlanted = 0.0f;
// tell the bots
bots.initRound ();
setBombOrigin (true);
// calculate the round mid/end in world time
m_timeRoundStart = game.time () + mp_freezetime.as <float> ();
m_timeRoundMid = m_timeRoundStart + mp_roundtime.as <float> () * 60.0f * 0.5f;
m_timeRoundEnd = m_timeRoundStart + mp_roundtime.as <float> () * 60.0f;
m_interestingEntities.clear ();
m_activeGrenades.clear ();
m_activeGrenadesUpdateTime.reset ();
m_interestingEntitiesUpdateTime.reset ();
}
float GameState::getBombTimeLeft () const {
if (!m_bombPlanted) {
return 0.0f;
}
return cr::max (m_timeBombPlanted + mp_c4timer.as <float> () - game.time (), 0.0f);
}
void GameState::setBombPlanted (bool isPlanted) {
if (cv_ignore_objectives) {
m_bombPlanted = false;
return;
}
if (isPlanted) {
m_timeBombPlanted = game.time ();
}
m_bombPlanted = isPlanted;
}
void GameState::updateActiveGrenade () {
constexpr auto kUpdateTime = 0.25f;
if (m_activeGrenadesUpdateTime.lessThen (kUpdateTime)) {
return;
}
m_activeGrenades.clear (); // clear previously stored grenades
// need to ignore bomb model in active grenades...
auto bombModel = conf.getBombModelName ();
// search the map for any type of grenade
game.searchEntities ("classname", "grenade", [&] (edict_t *e) {
// do not count c4 as a grenade
if (!game.isEntityModelMatches (e, bombModel)) {
m_activeGrenades.push (e);
}
return EntitySearchResult::Continue; // continue iteration
});
m_activeGrenadesUpdateTime.start ();
}
void GameState::updateInterestingEntities () {
constexpr auto kUpdateTime = 0.5f;
if (m_interestingEntitiesUpdateTime.lessThen (kUpdateTime)) {
return;
}
m_interestingEntities.clear (); // clear previously stored entities
// search the map for any type of grenade
game.searchEntities (nullptr, kInfiniteDistance, [&] (edict_t *e) {
auto classname = e->v.classname.str ();
// search for grenades, weaponboxes, weapons, items and armoury entities
if (classname.startsWith ("weaponbox") || classname.startsWith ("grenade") || game.isItemEntity (e) || classname.startsWith ("armoury")) {
m_interestingEntities.push (e);
}
// pickup some hostage if on cs_ maps
if (game.mapIs (MapFlags::HostageRescue) && game.isHostageEntity (e)) {
m_interestingEntities.push (e);
}
// add buttons
if (game.mapIs (MapFlags::HasButtons) && classname.startsWith ("func_button")) {
m_interestingEntities.push (e);
}
// pickup some csdm stuff if we're running csdm
if (game.is (GameFlags::CSDM) && classname.startsWith ("csdm")) {
m_interestingEntities.push (e);
}
if (cv_attack_monsters && game.isMonsterEntity (e)) {
m_interestingEntities.push (e);
}
// continue iteration
return EntitySearchResult::Continue;
});
m_interestingEntitiesUpdateTime.start ();
}

View file

@ -9,7 +9,7 @@
// on other than win32/linux platforms i.e. arm we're using xash3d engine to run which exposes // on other than win32/linux platforms i.e. arm we're using xash3d engine to run which exposes
// nice interface to handle with linkents. if ever rehlds or hlds engine will ever run on ARM or // nice interface to handle with linkents. if ever rehlds or hlds engine will ever run on ARM or
// other platforms, and you want to run bot on it without metamod, consider enabling LINKENT_STATIC_THUNKS // other platforms, and you want to run bot on it without metamod, consider enabling LINKENT_STATIC
// when compiling the bot, to get it supported. // when compiling the bot, to get it supported.
#if defined(LINKENT_STATIC) #if defined(LINKENT_STATIC)
void forwardEntity_helper (EntityProto &addr, const char *name, entvars_t *pev) { void forwardEntity_helper (EntityProto &addr, const char *name, entvars_t *pev) {

View file

@ -24,7 +24,7 @@ void BotFakePingManager::reset (edict_t *to) {
} }
for (const auto &client : util.getClients ()) { for (const auto &client : util.getClients ()) {
if (!(client.flags & ClientFlags::Used) || util.isFakeClient (client.ent)) { if (!(client.flags & ClientFlags::Used) || game.isFakeClientEntity (client.ent)) {
continue; continue;
} }
m_pbm.start (client.ent); m_pbm.start (client.ent);
@ -46,7 +46,7 @@ void BotFakePingManager::syncCalculate () {
int numHumans {}; int numHumans {};
for (const auto &client : util.getClients ()) { for (const auto &client : util.getClients ()) {
if (!(client.flags & ClientFlags::Used) || util.isFakeClient (client.ent)) { if (!(client.flags & ClientFlags::Used) || game.isFakeClientEntity (client.ent)) {
continue; continue;
} }
numHumans++; numHumans++;
@ -101,7 +101,7 @@ void BotFakePingManager::calculate () {
} }
void BotFakePingManager::emit (edict_t *ent) { void BotFakePingManager::emit (edict_t *ent) {
if (!util.isPlayer (ent)) { if (!game.isPlayerEntity (ent)) {
return; return;
} }

View file

@ -603,6 +603,10 @@ bool BotGraph::isAnalyzed () const {
return (m_info.header.options & StorageOption::Analyzed); return (m_info.header.options & StorageOption::Analyzed);
} }
bool BotGraph::isConverted () const {
return (m_info.header.options & StorageOption::Converted);
}
void BotGraph::add (int type, const Vector &pos) { void BotGraph::add (int type, const Vector &pos) {
if (!hasEditor () && !analyzer.isAnalyzing ()) { if (!hasEditor () && !analyzer.isAnalyzing ()) {
return; return;
@ -1292,6 +1296,7 @@ void BotGraph::showFileInfo () {
msg (" uncompressed_size: %dkB", info.uncompressed / 1024); msg (" uncompressed_size: %dkB", info.uncompressed / 1024);
msg (" options: %d", info.options); // display as string ? msg (" options: %d", info.options); // display as string ?
msg (" analyzed: %s", isAnalyzed () ? conf.translate ("yes") : conf.translate ("no")); // display as string ? msg (" analyzed: %s", isAnalyzed () ? conf.translate ("yes") : conf.translate ("no")); // display as string ?
msg (" converted: %s", isConverted () ? conf.translate ("yes") : conf.translate ("no")); // display as string ?
msg (" pathfinder: %s", planner.isPathsCheckFailed () ? "floyd" : "astar"); msg (" pathfinder: %s", planner.isPathsCheckFailed () ? "floyd" : "astar");
msg (""); msg ("");
@ -1310,7 +1315,7 @@ void BotGraph::emitNotify (int32_t sound) const {
}; };
// notify editor // notify editor
if (util.isPlayer (m_editor) && !m_silenceMessages) { if (game.isPlayerEntity (m_editor) && !m_silenceMessages) {
game.playSound (m_editor, notifySounds[sound].chars ()); game.playSound (m_editor, notifySounds[sound].chars ());
} }
} }
@ -1483,7 +1488,7 @@ void BotGraph::calculatePathRadius (int index) {
if (tr.flFraction < 1.0f) { if (tr.flFraction < 1.0f) {
game.testLine (radiusStart, radiusEnd, TraceIgnore::Monsters, nullptr, &tr); game.testLine (radiusStart, radiusEnd, TraceIgnore::Monsters, nullptr, &tr);
if (util.isDoorEntity (tr.pHit)) { if (game.isDoorEntity (tr.pHit)) {
path.radius = 0.0f; path.radius = 0.0f;
wayBlocked = true; wayBlocked = true;
@ -1755,6 +1760,8 @@ bool BotGraph::convertOldFormat () {
m_info.author = header.author; m_info.author = header.author;
m_info.header.options = StorageOption::Converted;
// clean editor so graph will be saved with header's author // clean editor so graph will be saved with header's author
auto editor = m_editor; auto editor = m_editor;
m_editor = nullptr; m_editor = nullptr;
@ -1813,9 +1820,10 @@ bool BotGraph::loadGraphData () {
if (!modified.empty () && !modified.contains ("(none)")) { if (!modified.empty () && !modified.contains ("(none)")) {
m_info.modified.assign (exten.modified); m_info.modified.assign (exten.modified);
} }
vistab.load (); // load/initialize visibility
planner.init (); // initialize our little path planner planner.init (); // initialize our little path planner
practice.load (); // load bots practice practice.load (); // load bots practice
vistab.load (); // load/initialize visibility
populateNodes (); populateNodes ();
@ -1851,7 +1859,7 @@ bool BotGraph::canDownload () {
} }
bool BotGraph::saveGraphData () { bool BotGraph::saveGraphData () {
auto options = StorageOption::Graph | StorageOption::Exten; auto options = m_info.header.options | StorageOption::Graph | StorageOption::Exten;
String editorName {}; String editorName {};
if (!hasEditor () && !m_info.author.empty ()) { if (!hasEditor () && !m_info.author.empty ()) {
@ -1976,7 +1984,7 @@ bool BotGraph::isNodeReacheableEx (const Vector &src, const Vector &destination,
// check if this node is "visible"... // check if this node is "visible"...
game.testLine (src, destination, TraceIgnore::Monsters, m_editor, &tr); game.testLine (src, destination, TraceIgnore::Monsters, m_editor, &tr);
const bool isDoor = util.isDoorEntity (tr.pHit); const bool isDoor = game.isDoorEntity (tr.pHit);
// if node is visible from current position (even behind head)... // if node is visible from current position (even behind head)...
if (tr.flFraction >= 1.0f || isDoor) { if (tr.flFraction >= 1.0f || isDoor) {
@ -2059,7 +2067,7 @@ void BotGraph::frame () {
} }
// keep the clipping mode enabled, or it can be turned off after new round has started // keep the clipping mode enabled, or it can be turned off after new round has started
if (graph.hasEditFlag (GraphEdit::Noclip) && util.isAlive (m_editor)) { if (graph.hasEditFlag (GraphEdit::Noclip) && game.isAliveEntity (m_editor)) {
m_editor->v.movetype = MOVETYPE_NOCLIP; m_editor->v.movetype = MOVETYPE_NOCLIP;
} }
@ -2123,7 +2131,7 @@ void BotGraph::frame () {
// check if node is within a distance, and is visible // check if node is within a distance, and is visible
if (distanceSq < cr::sqrf (cv_graph_draw_distance.as <float> ()) if (distanceSq < cr::sqrf (cv_graph_draw_distance.as <float> ())
&& ((util.isVisible (path.origin, m_editor) && ((util.isVisible (path.origin, m_editor)
&& util.isInViewCone (path.origin, m_editor)) || !util.isAlive (m_editor) || distanceSq < cr::sqrf (64.0f))) { && util.isInViewCone (path.origin, m_editor)) || !game.isAliveEntity (m_editor) || distanceSq < cr::sqrf (64.0f))) {
// check the distance // check the distance
if (distanceSq < nearestDistanceSq) { if (distanceSq < nearestDistanceSq) {
@ -2296,17 +2304,20 @@ void BotGraph::frame () {
if (path.radius > 0.0f) { if (path.radius > 0.0f) {
const float sqr = cr::sqrtf (cr::sqrf (path.radius) * 0.5f); const 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); const Vector points[] = {
game.drawLine (m_editor, origin + Vector (sqr, -sqr, 0.0f), origin + Vector (0.0f, -path.radius, 0.0f), 5, 0, radiusColor, 200, 0, 10); { path.radius, 0.0f, 0.0f },
{ sqr, -sqr, 0.0f },
{ 0.0f, -path.radius, 0.0f },
{ -sqr, -sqr, 0.0f },
{ -path.radius, 0.0f, 0.0f },
{ -sqr, sqr, 0.0f },
{ 0.0f, path.radius, 0.0f },
{ sqr, sqr, 0.0f }
};
game.drawLine (m_editor, origin + Vector (0.0f, -path.radius, 0.0f), origin + Vector (-sqr, -sqr, 0.0f), 5, 0, radiusColor, 200, 0, 10); for (auto i = 0; i < kMaxNodeLinks; ++i) {
game.drawLine (m_editor, origin + Vector (-sqr, -sqr, 0.0f), origin + Vector (-path.radius, 0.0f, 0.0f), 5, 0, radiusColor, 200, 0, 10); game.drawLine (m_editor, origin + points[i], origin + points[(i + 1) % 8], 5, 0, radiusColor, 200, 0, 10);
}
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);
game.drawLine (m_editor, origin + Vector (0.0f, path.radius, 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 (path.radius, 0.0f, 0.0f), 5, 0, radiusColor, 200, 0, 10);
} }
else { else {
const float sqr = cr::sqrtf (32.0f); const float sqr = cr::sqrtf (32.0f);
@ -2766,43 +2777,6 @@ void BotGraph::addBasic () {
autoCreateForEntity (NodeAddFlag::Goal, "func_escapezone"); // terrorist escape zone autoCreateForEntity (NodeAddFlag::Goal, "func_escapezone"); // terrorist escape zone
} }
void BotGraph::setBombOrigin (bool reset, const Vector &pos) {
// this function stores the bomb position as a vector
if (!game.mapIs (MapFlags::Demolition) || !bots.isBombPlanted ()) {
return;
}
if (reset) {
m_bombOrigin.clear ();
bots.setBombPlanted (false);
return;
}
if (!pos.empty ()) {
m_bombOrigin = pos;
return;
}
bool wasFound = false;
auto bombModel = conf.getBombModelName ();
game.searchEntities ("classname", "grenade", [&] (edict_t *ent) {
if (util.isModel (ent, bombModel)) {
m_bombOrigin = game.getEntityOrigin (ent);
wasFound = true;
return EntitySearchResult::Break;
}
return EntitySearchResult::Continue;
});
if (!wasFound) {
m_bombOrigin.clear ();
bots.setBombPlanted (false);
}
}
void BotGraph::startLearnJump () { void BotGraph::startLearnJump () {
m_jumpLearnNode = true; m_jumpLearnNode = true;
} }

View file

@ -27,7 +27,7 @@ int32_t ServerQueryHook::sendTo (int socket, const void *message, size_t length,
buffer.skip <int32_t> (); // score buffer.skip <int32_t> (); // score
auto ctime = buffer.read <float> (); // override connection time auto ctime = buffer.read <float> (); // override connection time
buffer.write <float> (bots.getConnectTime (name, ctime)); buffer.write <float> (bots.getConnectionTimes (name, ctime));
} }
return send (buffer.data ()); return send (buffer.data ());
} }

View file

@ -50,7 +50,7 @@ CR_FORCE_STACK_ALIGN void handler_engClientCommand (edict_t *ent, char const *fo
// case it's a bot asking for a client command, we handle it like we do for bot commands // case it's a bot asking for a client command, we handle it like we do for bot commands
if (!game.isNullEntity (ent)) { if (!game.isNullEntity (ent)) {
if (bots[ent] || util.isFakeClient (ent) || (ent->v.flags & FL_DORMANT)) { if (bots[ent] || game.isFakeClientEntity (ent) || (ent->v.flags & FL_DORMANT)) {
if (game.is (GameFlags::Metamod)) { if (game.is (GameFlags::Metamod)) {
RETURN_META (MRES_SUPERCEDE); // prevent bots to be forced to issue client commands RETURN_META (MRES_SUPERCEDE); // prevent bots to be forced to issue client commands
} }
@ -130,6 +130,9 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) {
// precache everything // precache everything
game.precache (); game.precache ();
// notify about entity spawn
game.onSpawnEntity (ent);
if (game.is (GameFlags::Metamod)) { if (game.is (GameFlags::Metamod)) {
RETURN_META_VALUE (MRES_IGNORED, 0); RETURN_META_VALUE (MRES_IGNORED, 0);
} }
@ -159,7 +162,7 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) {
auto bot = bots[pentTouched]; auto bot = bots[pentTouched];
if (bot && util.isBreakableEntity (pentOther)) { if (bot && game.isBreakableEntity (pentOther)) {
bot->checkBreakable (pentOther); bot->checkBreakable (pentOther);
} }
} }
@ -389,10 +392,10 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) {
if (bots.hasBotsOnline ()) { if (bots.hasBotsOnline ()) {
// keep track of grenades on map // keep track of grenades on map
bots.updateActiveGrenade (); gameState.updateActiveGrenade ();
// keep track of interesting entities // keep track of interesting entities
bots.updateInterestingEntities (); gameState.updateInterestingEntities ();
} }
// keep bot number up to date // keep bot number up to date
@ -429,7 +432,7 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) {
auto ent = const_cast <edict_t *> (reinterpret_cast <const edict_t *> (player)); auto ent = const_cast <edict_t *> (reinterpret_cast <const edict_t *> (player));
if (fakeping.hasFeature ()) { if (fakeping.hasFeature ()) {
if (!util.isFakeClient (ent) && (ent->v.oldbuttons | ent->v.button) & IN_SCORE) { if (!game.isFakeClientEntity (ent) && (ent->v.oldbuttons | ent->v.button) & IN_SCORE) {
fakeping.emit (ent); fakeping.emit (ent);
} }
} }
@ -483,6 +486,7 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) {
} }
} }
} }
if (game.is (GameFlags::Metamod)) { if (game.is (GameFlags::Metamod)) {
RETURN_META (MRES_IGNORED); RETURN_META (MRES_IGNORED);
} }
@ -560,7 +564,7 @@ CR_C_LINKAGE int GetEntityAPI_Post (gamefuncs_t *table, int) {
auto ent = const_cast <edict_t *> (reinterpret_cast <const edict_t *> (player)); auto ent = const_cast <edict_t *> (reinterpret_cast <const edict_t *> (player));
if (fakeping.hasFeature ()) { if (fakeping.hasFeature ()) {
if (!util.isFakeClient (ent) && (ent->v.oldbuttons | ent->v.button) & IN_SCORE) { if (!game.isFakeClientEntity (ent) && (ent->v.oldbuttons | ent->v.button) & IN_SCORE) {
fakeping.emit (ent); fakeping.emit (ent);
} }
} }
@ -591,7 +595,7 @@ CR_C_LINKAGE int GetEngineFunctions (enginefuncs_t *table, int *) {
table->pfnFindEntityByString = [] (edict_t *edictStartSearchAfter, const char *field, const char *value) CR_FORCE_STACK_ALIGN { table->pfnFindEntityByString = [] (edict_t *edictStartSearchAfter, const char *field, const char *value) CR_FORCE_STACK_ALIGN {
// round starts in counter-strike 1.5 // round starts in counter-strike 1.5
if (strcmp (value, "info_map_parameters") == 0) { if (strcmp (value, "info_map_parameters") == 0) {
bots.initRound (); gameState.roundStart ();
} }
if (game.is (GameFlags::Metamod)) { if (game.is (GameFlags::Metamod)) {
@ -786,7 +790,7 @@ CR_C_LINKAGE int GetEngineFunctions (enginefuncs_t *table, int *) {
// as it will crash your server. Why would you, anyway ? bots have no client DLL as far as // as it will crash your server. Why would you, anyway ? bots have no client DLL as far as
// we know, right ? But since stupidity rules this world, we do a preventive check :) // we know, right ? But since stupidity rules this world, we do a preventive check :)
if (util.isFakeClient (ent)) { if (game.isFakeClientEntity (ent)) {
if (game.is (GameFlags::Metamod)) { if (game.is (GameFlags::Metamod)) {
RETURN_META (MRES_SUPERCEDE); RETURN_META (MRES_SUPERCEDE);
} }

View file

@ -186,9 +186,9 @@ BotCreateResult BotManager::create (StringRef name, int difficulty, int personal
// try to set proffered personality // try to set proffered personality
static HashMap <String, Personality> personalityMap { static HashMap <String, Personality> personalityMap {
{"normal", Personality::Normal }, { "normal", Personality::Normal },
{"careful", Personality::Careful }, { "careful", Personality::Careful },
{"rusher", Personality::Rusher }, { "rusher", Personality::Rusher },
}; };
// set personality if requested // set personality if requested
@ -332,13 +332,17 @@ void BotManager::addbot (StringRef name, StringRef difficulty, StringRef persona
// this function is same as the function above, but accept as parameters string instead of integers // this function is same as the function above, but accept as parameters string instead of integers
BotRequest request {}; BotRequest request {};
static StringRef any = "*"; constexpr StringRef ANY = "*";
request.name = (name.empty () || name == any) ? StringRef ("\0") : name; auto handleParam = [&ANY] (StringRef value) {
request.difficulty = (difficulty.empty () || difficulty == any) ? -1 : difficulty.as <int> (); return value.empty () || value == ANY ? -1 : value.as <int> ();
request.team = (team.empty () || team == any) ? -1 : team.as <int> (); };
request.skin = (skin.empty () || skin == any) ? -1 : skin.as <int> ();
request.personality = (personality.empty () || personality == any) ? -1 : personality.as <int> (); request.name = name.empty () || name == ANY ? StringRef ("\0") : name;
request.difficulty = handleParam (difficulty);
request.team = handleParam (team);
request.skin = handleParam (skin);
request.personality = handleParam (personality);
request.manual = manual; request.manual = manual;
addbot (request.name, request.difficulty, request.personality, request.team, request.skin, request.manual); addbot (request.name, request.difficulty, request.personality, request.team, request.skin, request.manual);
@ -444,6 +448,11 @@ void BotManager::maintainQuota () {
maxSpawnCount = maxClients + 1; maxSpawnCount = maxClients + 1;
} }
// disable spawn control
if (conf.fetchCustom ("DisableSpawnControl").startsWith ("yes")) {
maxSpawnCount = game.maxClients () + 1;
}
// sent message only to console from here // sent message only to console from here
ctrl.setFromConsole (true); ctrl.setFromConsole (true);
@ -469,7 +478,7 @@ void BotManager::maintainLeaders () {
} }
// select leader each team somewhere in round start // select leader each team somewhere in round start
if (m_timeRoundStart + rg (1.5f, 3.0f) < game.time ()) { if (gameState.getRoundStartTime () + rg (1.5f, 3.0f) < game.time ()) {
for (int team = 0; team < kGameTeamNum; ++team) { for (int team = 0; team < kGameTeamNum; ++team) {
selectLeaders (team, false); selectLeaders (team, false);
} }
@ -487,7 +496,7 @@ void BotManager::maintainRoundRestart () {
&& m_numPreviousPlayers == 0 && m_numPreviousPlayers == 0
&& totalHumans == 1 && totalHumans == 1
&& totalBots > 0 && totalBots > 0
&& !m_resetHud) { && !gameState.isResetHUD ()) {
static ConVarRef sv_restartround ("sv_restartround"); static ConVarRef sv_restartround ("sv_restartround");
@ -496,13 +505,13 @@ void BotManager::maintainRoundRestart () {
} }
} }
m_numPreviousPlayers = totalHumans; m_numPreviousPlayers = totalHumans;
m_resetHud = false; gameState.setResetHUD (false);
} }
void BotManager::maintainAutoKill () { void BotManager::maintainAutoKill () {
const float killDelay = cv_autokill_delay.as <float> (); const float killDelay = cv_autokill_delay.as <float> ();
if (killDelay < 1.0f || m_roundOver) { if (killDelay < 1.0f || gameState.isRoundOver ()) {
return; return;
} }
@ -516,7 +525,7 @@ void BotManager::maintainAutoKill () {
int aliveBots = 0; int aliveBots = 0;
// do not interrupt bomb-defuse scenario // do not interrupt bomb-defuse scenario
if (game.mapIs (MapFlags::Demolition) && isBombPlanted ()) { if (game.mapIs (MapFlags::Demolition) && gameState.isBombPlanted ()) {
return; return;
} }
const int totalHumans = getHumansCount (true); // we're ignore spectators intentionally const int totalHumans = getHumansCount (true); // we're ignore spectators intentionally
@ -531,7 +540,7 @@ void BotManager::maintainAutoKill () {
++aliveBots; ++aliveBots;
// do not interrupt assassination scenario, if vip is a bot // do not interrupt assassination scenario, if vip is a bot
if (game.is (MapFlags::Assassination) && util.isPlayerVIP (bot->ent ())) { if (game.is (MapFlags::Assassination) && game.isPlayerVIP (bot->ent ())) {
return; return;
} }
} }
@ -545,15 +554,9 @@ void BotManager::maintainAutoKill () {
} }
void BotManager::reset () { void BotManager::reset () {
m_grenadeUpdateTime = 0.0f;
m_entityUpdateTime = 0.0f;
m_plantSearchUpdateTime = 0.0f; m_plantSearchUpdateTime = 0.0f;
m_lastChatTime = 0.0f; m_lastChatTime = 0.0f;
m_timeBombPlanted = 0.0f;
m_bombSayStatus = BombPlantedSay::ChatSay | BombPlantedSay::Chatter; m_bombSayStatus = BombPlantedSay::ChatSay | BombPlantedSay::Chatter;
m_interestingEntities.clear ();
m_activeGrenades.clear ();
} }
void BotManager::initFilters () { void BotManager::initFilters () {
@ -623,7 +626,7 @@ void BotManager::serverFill (int selection, int personality, int difficulty, int
} }
const auto maxToAdd = maxClients - (getHumansCount () + getBotCount ()); const auto maxToAdd = maxClients - (getHumansCount () + getBotCount ());
constexpr char kTeams[6][12] = { "", {"Terrorists"}, {"CTs"}, "", "", {"Random"}, }; constexpr char kTeams[6][12] = { "", { "Terrorists" }, { "CTs" }, "", "", { "Random" }, };
auto toAdd = numToAdd == -1 ? maxToAdd : numToAdd; auto toAdd = numToAdd == -1 ? maxToAdd : numToAdd;
// limit manually added count as well // limit manually added count as well
@ -681,7 +684,7 @@ void BotManager::kickFromTeam (Team team, bool removeAll) {
} }
for (const auto &bot : m_bots) { for (const auto &bot : m_bots) {
if (team == game.getRealTeam (bot->ent ())) { if (team == game.getRealPlayerTeam (bot->ent ())) {
bot->kick (removeAll); bot->kick (removeAll);
if (!removeAll) { if (!removeAll) {
@ -696,7 +699,7 @@ void BotManager::killAllBots (int team, bool silent) {
// this function kills all bots on server (only this dll controlled bots) // this function kills all bots on server (only this dll controlled bots)
for (const auto &bot : m_bots) { for (const auto &bot : m_bots) {
if (team != Team::Invalid && game.getRealTeam (bot->ent ()) != team) { if (team != Team::Invalid && game.getRealPlayerTeam (bot->ent ()) != team) {
continue; continue;
} }
bot->kill (); bot->kill ();
@ -732,7 +735,7 @@ bool BotManager::kickRandom (bool decQuota, Team fromTeam) {
if (fromTeam == Team::Unassigned) { if (fromTeam == Team::Unassigned) {
return true; return true;
} }
return game.getRealTeam (bot->ent ()) == fromTeam; return game.getRealPlayerTeam (bot->ent ()) == fromTeam;
}; };
// first try to kick the bot that is currently dead // first try to kick the bot that is currently dead
@ -830,7 +833,7 @@ bool BotManager::hasCustomCSDMSpawnEntities () {
void BotManager::setLastWinner (int winner) { void BotManager::setLastWinner (int winner) {
m_lastWinner = winner; m_lastWinner = winner;
m_roundOver = true; gameState.setRoundOver (true);
if (cv_radio_mode.as <int> () != 2) { if (cv_radio_mode.as <int> () != 2) {
return; return;
@ -839,7 +842,7 @@ void BotManager::setLastWinner (int winner) {
if (notify) { if (notify) {
if (notify->m_team == winner) { if (notify->m_team == winner) {
if (getRoundMidTime () > game.time ()) { if (gameState.getRoundMidTime () > game.time ()) {
notify->pushChatterMessage (Chatter::QuickWonRound); notify->pushChatterMessage (Chatter::QuickWonRound);
} }
else { else {
@ -890,25 +893,25 @@ void BotManager::setWeaponMode (int selection) {
selection--; selection--;
constexpr int kStdMaps[7][kNumWeapons] = { constexpr int kStdMaps[7][kNumWeapons] = {
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Knife only { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // Knife only
{-1, -1, -1, 2, 2, 0, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Pistols only { -1, -1, -1, 2, 2, 0, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // Pistols only
{-1, -1, -1, -1, -1, -1, -1, 2, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Shotgun only { -1, -1, -1, -1, -1, -1, -1, 2, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // Shotgun only
{-1, -1, -1, -1, -1, -1, -1, -1, -1, 2, 1, 2, 0, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, -1}, // Machine Guns only { -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, 1, 2, 0, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, -1 }, // Machine Guns only
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 1, 0, 1, 1, -1, -1, -1, -1, -1, -1}, // Rifles only { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 1, 0, 1, 1, -1, -1, -1, -1, -1, -1 }, // Rifles only
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, 2, 0, 1, -1, -1}, // Snipers only { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, 2, 0, 1, -1, -1 }, // Snipers only
{-1, -1, -1, 2, 2, 0, 1, 2, 2, 2, 1, 2, 0, 2, 0, 0, 1, 0, 1, 1, 2, 2, 0, 1, 2, 1} // Standard { -1, -1, -1, 2, 2, 0, 1, 2, 2, 2, 1, 2, 0, 2, 0, 0, 1, 0, 1, 1, 2, 2, 0, 1, 2, 1 } // Standard
}; };
constexpr int kAsMaps[7][kNumWeapons] = { constexpr int kAsMaps[7][kNumWeapons] = {
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Knife only { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // Knife only
{-1, -1, -1, 2, 2, 0, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Pistols only { -1, -1, -1, 2, 2, 0, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // Pistols only
{-1, -1, -1, -1, -1, -1, -1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Shotgun only { -1, -1, -1, -1, -1, -1, -1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // Shotgun only
{-1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 1, 1, 0, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1}, // Machine Guns only { -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 1, 1, 0, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1 }, // Machine Guns only
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, 1, 0, 1, 1, -1, -1, -1, -1, -1, -1}, // Rifles only { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, 1, 0, 1, 1, -1, -1, -1, -1, -1, -1 }, // Rifles only
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, -1, 1, -1, -1}, // Snipers only { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, -1, 1, -1, -1 }, // Snipers only
{-1, -1, -1, 2, 2, 0, 1, 1, 1, 1, 1, 1, 0, 2, 0, -1, 1, 0, 1, 1, 0, 0, -1, 1, 1, 1} // Standard { -1, -1, -1, 2, 2, 0, 1, 1, 1, 1, 1, 1, 0, 2, 0, -1, 1, 0, 1, 1, 0, 0, -1, 1, 1, 1 } // Standard
}; };
constexpr char kModes[7][12] = { {"Knife"}, {"Pistol"}, {"Shotgun"}, {"Machine Gun"}, {"Rifle"}, {"Sniper"}, {"Standard"} }; constexpr char kModes[7][12] = { { "Knife" }, { "Pistol" }, { "Shotgun" }, { "Machine Gun" }, { "Rifle" }, { "Sniper" }, { "Standard" } };
// get the raw weapons array // get the raw weapons array
auto tab = conf.getRawWeapons (); auto tab = conf.getRawWeapons ();
@ -929,7 +932,7 @@ void BotManager::listBots () {
ctrl.msg ("%-3.5s\t%-19.16s\t%-10.12s\t%-3.4s\t%-3.4s\t%-3.4s\t%-3.6s\t%-3.5s\t%-3.8s", "index", "name", "personality", "team", "difficulty", "frags", "deaths", "alive", "timeleft"); ctrl.msg ("%-3.5s\t%-19.16s\t%-10.12s\t%-3.4s\t%-3.4s\t%-3.4s\t%-3.6s\t%-3.5s\t%-3.8s", "index", "name", "personality", "team", "difficulty", "frags", "deaths", "alive", "timeleft");
auto botTeam = [] (edict_t *ent) -> StringRef { auto botTeam = [] (edict_t *ent) -> StringRef {
const auto team = game.getRealTeam (ent); const auto team = game.getRealPlayerTeam (ent);
switch (team) { switch (team) {
case Team::CT: case Team::CT:
@ -967,7 +970,7 @@ void BotManager::listBots () {
ctrl.msg ("%d bots", m_bots.length ()); ctrl.msg ("%d bots", m_bots.length ());
} }
float BotManager::getConnectTime (StringRef name, float original) { float BotManager::getConnectionTimes (StringRef name, float original) {
// this function get's fake bot player time. // this function get's fake bot player time.
for (const auto &bot : m_bots) { for (const auto &bot : m_bots) {
@ -1020,19 +1023,18 @@ Twin <int, int> BotManager::countTeamPlayers () {
} }
Bot *BotManager::findHighestFragBot (int team) { Bot *BotManager::findHighestFragBot (int team) {
int bestIndex = 0; Twin <int32_t, float> best {};
float bestScore = -1;
// search bots in this team // search bots in this team
for (const auto &bot : bots) { for (const auto &bot : bots) {
if (bot->m_isAlive && game.getRealTeam (bot->ent ()) == team) { if (bot->m_isAlive && game.getRealPlayerTeam (bot->ent ()) == team) {
if (bot->pev->frags > bestScore) { if (bot->pev->frags > best.second) {
bestIndex = bot->index (); best.first = bot->index ();
bestScore = bot->pev->frags; best.second = bot->pev->frags;
} }
} }
} }
return findBotByIndex (bestIndex); return findBotByIndex (best.first);
} }
void BotManager::updateTeamEconomics (int team, bool setTrue) { void BotManager::updateTeamEconomics (int team, bool setTrue) {
@ -1087,7 +1089,7 @@ void BotManager::updateBotDifficulties () {
// sets new difficulty for all bots // sets new difficulty for all bots
for (const auto &bot : m_bots) { for (const auto &bot : m_bots) {
bot->m_difficulty = difficulty; bot->setNewDifficulty (difficulty);
} }
m_lastDifficulty = difficulty; m_lastDifficulty = difficulty;
} }
@ -1096,7 +1098,8 @@ void BotManager::updateBotDifficulties () {
void BotManager::balanceBotDifficulties () { void BotManager::balanceBotDifficulties () {
// difficulty changing once per round (time) // difficulty changing once per round (time)
auto updateDifficulty = [] (Bot *bot, int32_t offset) { auto updateDifficulty = [] (Bot *bot, int32_t offset) {
bot->m_difficulty = cr::clamp (static_cast <Difficulty> (bot->m_difficulty + offset), Difficulty::Noob, Difficulty::Expert); game.print ("offset = %d", offset);
bot->setNewDifficulty (cr::clamp (static_cast <Difficulty> (bot->m_difficulty + offset), Difficulty::Noob, Difficulty::Expert));
}; };
// with nightmare difficulty, there is no balance // with nightmare difficulty, there is no balance
@ -1199,17 +1202,17 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int skin) {
m_isAlive = false; m_isAlive = false;
m_weaponBurstMode = BurstMode::Off; m_weaponBurstMode = BurstMode::Off;
m_difficulty = cr::clamp (static_cast <Difficulty> (difficulty), Difficulty::Noob, Difficulty::Expert); setNewDifficulty (cr::clamp (static_cast <Difficulty> (difficulty), Difficulty::Noob, Difficulty::Expert));
auto minDifficulty = cv_difficulty_min.as <int> (); auto minDifficulty = cv_difficulty_min.as <int> ();
auto maxDifficulty = cv_difficulty_max.as <int> (); auto maxDifficulty = cv_difficulty_max.as <int> ();
// if we're have min/max difficulty specified, choose value from they // if we're have min/max difficulty specified, choose value from they
if (minDifficulty != Difficulty::Invalid && maxDifficulty != Difficulty::Invalid) { if (minDifficulty != Difficulty::Invalid && maxDifficulty != Difficulty::Invalid) {
if (maxDifficulty > minDifficulty) { if (minDifficulty > maxDifficulty) {
cr::swap (maxDifficulty, minDifficulty); cr::swap (maxDifficulty, minDifficulty);
} }
m_difficulty = rg (minDifficulty, maxDifficulty); setNewDifficulty (rg (minDifficulty, maxDifficulty));
} }
m_pingBase = fakeping.randomBase (); m_pingBase = fakeping.randomBase ();
m_ping = fakeping.randomBase (); m_ping = fakeping.randomBase ();
@ -1398,8 +1401,8 @@ void BotManager::disconnectBot (Bot *bot) {
} }
void BotManager::handleDeath (edict_t *killer, edict_t *victim) { void BotManager::handleDeath (edict_t *killer, edict_t *victim) {
const auto killerTeam = game.getRealTeam (killer); const auto killerTeam = game.getRealPlayerTeam (killer);
const auto victimTeam = game.getRealTeam (victim); const auto victimTeam = game.getRealPlayerTeam (victim);
if (cv_radio_mode.as <int> () == 2) { if (cv_radio_mode.as <int> () == 2) {
// need to send congrats on well placed shot // need to send congrats on well placed shot
@ -1512,7 +1515,7 @@ void Bot::newRound () {
node = kInvalidNodeIndex; node = kInvalidNodeIndex;
} }
m_navTimeset = game.time (); m_navTimeset = game.time ();
m_team = game.getTeam (ent ()); m_team = game.getPlayerTeam (ent ());
resetPathSearchType (); resetPathSearchType ();
@ -1522,14 +1525,13 @@ void Bot::newRound () {
m_isLeader = false; m_isLeader = false;
m_hasProgressBar = false; m_hasProgressBar = false;
m_canChooseAimDirection = true; m_canSetAimDirection = true;
m_preventFlashing = 0.0f; m_preventFlashing = 0.0f;
m_timeTeamOrder = 0.0f; m_timeTeamOrder = 0.0f;
m_askCheckTime = rg (30.0f, 90.0f); m_askCheckTime = rg (30.0f, 90.0f);
m_minSpeed = 260.0f; m_minSpeed = 260.0f;
m_prevSpeed = 0.0f; m_prevSpeed = 0.0f;
m_prevVelocity = 0.0f;
m_prevOrigin = Vector (kInfiniteDistance, kInfiniteDistance, kInfiniteDistance); m_prevOrigin = Vector (kInfiniteDistance, kInfiniteDistance, kInfiniteDistance);
m_prevTime = game.time (); m_prevTime = game.time ();
m_lookUpdateTime = game.time (); m_lookUpdateTime = game.time ();
@ -1729,9 +1731,6 @@ void Bot::newRound () {
m_enemyIgnoreTimer = 0.0f; m_enemyIgnoreTimer = 0.0f;
} }
// update refvec for blocked movement
updateRightRef ();
// and put buying into its message queue // and put buying into its message queue
pushMsgQueue (BotMsg::Buy); pushMsgQueue (BotMsg::Buy);
startTask (Task::Normal, TaskPri::Normal, kInvalidNodeIndex, 0.0f, true); startTask (Task::Normal, TaskPri::Normal, kInvalidNodeIndex, 0.0f, true);
@ -1835,13 +1834,25 @@ void Bot::markStale () {
pev->flags |= FL_DORMANT; pev->flags |= FL_DORMANT;
} }
void Bot::setNewDifficulty (int32_t newDifficulty) {
if (newDifficulty < Difficulty::Noob || newDifficulty > Difficulty::Expert) {
const auto difficlutyDefault = Difficulty::Hard;;
m_difficulty = difficlutyDefault;
m_difficultyData = conf.getDifficultyTweaks (difficlutyDefault);
}
m_difficulty = newDifficulty;
m_difficultyData = conf.getDifficultyTweaks (newDifficulty);
}
void Bot::updateTeamJoin () { void Bot::updateTeamJoin () {
// this function handles the selection of teams & class // this function handles the selection of teams & class
if (!m_notStarted) { if (!m_notStarted) {
return; return;
} }
const auto botTeam = game.getRealTeam (ent ()); const auto botTeam = game.getRealPlayerTeam (ent ());
// cs prior beta 7.0 uses hud-based motd, so press fire once // cs prior beta 7.0 uses hud-based motd, so press fire once
if (game.is (GameFlags::Legacy)) { if (game.is (GameFlags::Legacy)) {
@ -1948,15 +1959,15 @@ void BotManager::captureChatRadio (StringRef cmd, StringRef arg, edict_t *ent) {
} }
if (cmd.startsWith ("say")) { if (cmd.startsWith ("say")) {
const bool alive = util.isAlive (ent); const bool alive = game.isAliveEntity (ent);
int team = -1; int team = -1;
if (cmd.endsWith ("team")) { if (cmd.endsWith ("team")) {
team = game.getRealTeam (ent); team = game.getRealPlayerTeam (ent);
} }
for (const auto &client : util.getClients ()) { for (const auto &client : util.getClients ()) {
if (!(client.flags & ClientFlags::Used) || (team != -1 && team != client.team2) || alive != util.isAlive (client.ent)) { if (!(client.flags & ClientFlags::Used) || (team != -1 && team != client.team2) || alive != game.isAliveEntity (client.ent)) {
continue; continue;
} }
auto target = bots[client.ent]; auto target = bots[client.ent];
@ -2003,7 +2014,7 @@ void BotManager::captureChatRadio (StringRef cmd, StringRef arg, edict_t *ent) {
void BotManager::notifyBombDefuse () { void BotManager::notifyBombDefuse () {
// notify all terrorists that CT is starting bomb defusing // notify all terrorists that CT is starting bomb defusing
const auto &bombPos = graph.getBombOrigin (); const auto &bombPos = gameState.getBombOrigin ();
for (const auto &bot : bots) { for (const auto &bot : bots) {
const auto task = bot->getCurrentTaskId (); const auto task = bot->getCurrentTaskId ();
@ -2027,68 +2038,6 @@ void BotManager::notifyBombDefuse () {
} }
} }
void BotManager::updateActiveGrenade () {
if (m_grenadeUpdateTime > game.time ()) {
return;
}
m_activeGrenades.clear (); // clear previously stored grenades
// need to ignore bomb model in active grenades...
auto bombModel = conf.getBombModelName ();
// search the map for any type of grenade
game.searchEntities ("classname", "grenade", [&] (edict_t *e) {
// do not count c4 as a grenade
if (!util.isModel (e, bombModel)) {
m_activeGrenades.push (e);
}
return EntitySearchResult::Continue; // continue iteration
});
m_grenadeUpdateTime = game.time () + 0.25f;
}
void BotManager::updateInterestingEntities () {
if (m_entityUpdateTime > game.time ()) {
return;
}
// clear previously stored entities
m_interestingEntities.clear ();
// search the map for any type of grenade
game.searchEntities (nullptr, kInfiniteDistance, [&] (edict_t *e) {
auto classname = e->v.classname.str ();
// search for grenades, weaponboxes, weapons, items and armoury entities
if (classname.startsWith ("weaponbox") || classname.startsWith ("grenade") || util.isItem (e) || classname.startsWith ("armoury")) {
m_interestingEntities.push (e);
}
// pickup some hostage if on cs_ maps
if (game.mapIs (MapFlags::HostageRescue) && util.isHostageEntity (e)) {
m_interestingEntities.push (e);
}
// add buttons
if (game.mapIs (MapFlags::HasButtons) && classname.startsWith ("func_button")) {
m_interestingEntities.push (e);
}
// pickup some csdm stuff if we're running csdm
if (game.is (GameFlags::CSDM) && classname.startsWith ("csdm")) {
m_interestingEntities.push (e);
}
if (cv_attack_monsters && util.isMonster (e)) {
m_interestingEntities.push (e);
}
// continue iteration
return EntitySearchResult::Continue;
});
m_entityUpdateTime = game.time () + 0.5f;
}
void BotManager::selectLeaders (int team, bool reset) { void BotManager::selectLeaders (int team, bool reset) {
auto &leaderChoosen = m_teamData[team].leaderChoosen; auto &leaderChoosen = m_teamData[team].leaderChoosen;
@ -2191,8 +2140,6 @@ void BotManager::selectLeaders (int team, bool reset) {
void BotManager::initRound () { void BotManager::initRound () {
// this is called at the start of each round // this is called at the start of each round
m_roundOver = false;
// check team economics // check team economics
for (int team = 0; team < kGameTeamNum; ++team) { for (int team = 0; team < kGameTeamNum; ++team) {
updateTeamEconomics (team); updateTeamEconomics (team);
@ -2211,35 +2158,15 @@ void BotManager::initRound () {
for (auto &client : util.getClients ()) { for (auto &client : util.getClients ()) {
client.radio = 0; client.radio = 0;
} }
graph.setBombOrigin (true);
graph.clearVisited (); graph.clearVisited ();
m_bombSayStatus = BombPlantedSay::ChatSay | BombPlantedSay::Chatter; m_bombSayStatus = BombPlantedSay::ChatSay | BombPlantedSay::Chatter;
m_timeBombPlanted = 0.0f;
m_plantSearchUpdateTime = 0.0f; m_plantSearchUpdateTime = 0.0f;
m_autoKillCheckTime = 0.0f; m_autoKillCheckTime = 0.0f;
m_botsCanPause = false; m_botsCanPause = false;
resetFilters (); resetFilters ();
practice.update (); // update practice 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.as <float> ();
m_timeRoundMid = m_timeRoundStart + mp_roundtime.as <float> () * 60.0f * 0.5f;
m_timeRoundEnd = m_timeRoundStart + mp_roundtime.as <float> () * 60.0f;
}
void BotManager::setBombPlanted (bool isPlanted) {
if (cv_ignore_objectives) {
m_bombPlanted = false;
return;
}
if (isPlanted) {
m_timeBombPlanted = game.time ();
}
m_bombPlanted = isPlanted;
} }
void BotThreadWorker::shutdown () { void BotThreadWorker::shutdown () {
@ -2293,121 +2220,6 @@ void BotThreadWorker::startup (int workers) {
m_pool->startup (static_cast <size_t> (requestedThreads)); m_pool->startup (static_cast <size_t> (requestedThreads));
} }
bool BotManager::isLineBlockedBySmoke (const Vector &from, const Vector &to) {
if (m_activeGrenades.empty ()) {
return false;
}
constexpr auto kSmokeGrenadeRadius = 115.0f;
// distance along line of sight covered by smoke
float totalSmokedLength = 0.0f;
Vector sightDir = to - from;
const float sightLength = sightDir.normalizeInPlace ();
for (auto pent : m_activeGrenades) {
if (game.isNullEntity (pent)) {
continue;
}
// need drawn models
if (pent->v.effects & EF_NODRAW) {
continue;
}
// smoke must be on a ground
if (!(pent->v.flags & FL_ONGROUND)) {
continue;
}
// must be a smoke grenade
if (!util.isModel (pent, kSmokeModelName)) {
continue;
}
const float smokeRadiusSq = cr::sqrf (kSmokeGrenadeRadius);
const Vector &smokeOrigin = game.getEntityOrigin (pent);
Vector toGrenade = smokeOrigin - from;
float alongDist = toGrenade | sightDir;
// compute closest point to grenade along line of sight ray
Vector close {};
// constrain closest point to line segment
if (alongDist < 0.0f) {
close = from;
}
else if (alongDist >= sightLength) {
close = to;
}
else {
close = from + sightDir * alongDist;
}
// if closest point is within smoke radius, the line overlaps the smoke cloud
Vector toClose = close - smokeOrigin;
float lengthSq = toClose.lengthSq ();
if (lengthSq < smokeRadiusSq) {
// some portion of the ray intersects the cloud
const float fromSq = toGrenade.lengthSq ();
const float toSq = (smokeOrigin - to).lengthSq ();
if (fromSq < smokeRadiusSq) {
if (toSq < smokeRadiusSq) {
// both 'from' and 'to' lie within the cloud
// entire length is smoked
totalSmokedLength += (to - from).length ();
}
else {
// 'from' is inside the cloud, 'to' is outside
// compute half of total smoked length as if ray crosses entire cloud chord
float halfSmokedLength = cr::sqrtf (smokeRadiusSq - lengthSq);
if (alongDist > 0.0f) {
// ray goes thru 'close'
totalSmokedLength += halfSmokedLength + (close - from).length ();
}
else {
// ray starts after 'close'
totalSmokedLength += halfSmokedLength - (close - from).length ();
}
}
}
else if (toSq < smokeRadiusSq) {
// 'from' is outside the cloud, 'to' is inside
// compute half of total smoked length as if ray crosses entire cloud chord
const float halfSmokedLength = cr::sqrtf (smokeRadiusSq - lengthSq);
Vector v = to - smokeOrigin;
if ((v | sightDir) > 0.0f) {
// ray goes thru 'close'
totalSmokedLength += halfSmokedLength + (close - to).length ();
}
else {
// ray ends before 'close'
totalSmokedLength += halfSmokedLength - (close - to).length ();
}
}
else {
// 'from' and 'to' lie outside of the cloud - the line of sight completely crosses it
// determine the length of the chord that crosses the cloud
const float smokedLength = 2.0f * cr::sqrtf (smokeRadiusSq - lengthSq);
totalSmokedLength += smokedLength;
}
}
}
// define how much smoke a bot can see thru
const float maxSmokedLength = 0.7f * kSmokeGrenadeRadius;
// return true if the total length of smoke-covered line-of-sight is too much
return totalSmokedLength > maxSmokedLength;
}
bool BotManager::isFrameSkipDisabled () { bool BotManager::isFrameSkipDisabled () {
if (game.is (GameFlags::Legacy)) { if (game.is (GameFlags::Legacy)) {
return true; return true;

View file

@ -26,7 +26,7 @@ void MessageDispatcher::netMsgTextMsg () {
// reset bomb position for all the bots // reset bomb position for all the bots
const auto resetBombPosition = [] () -> void { const auto resetBombPosition = [] () -> void {
if (game.mapIs (MapFlags::Demolition)) { if (game.mapIs (MapFlags::Demolition)) {
graph.setBombOrigin (true); gameState.setBombOrigin (true);
} }
}; };
@ -53,8 +53,8 @@ void MessageDispatcher::netMsgTextMsg () {
bots.setLastWinner (Team::Terrorist); // update last winner for economics bots.setLastWinner (Team::Terrorist); // update last winner for economics
resetBombPosition (); resetBombPosition ();
} }
else if ((cached & TextMsgCache::BombPlanted) && !bots.isBombPlanted ()) { else if ((cached & TextMsgCache::BombPlanted) && !gameState.isBombPlanted ()) {
bots.setBombPlanted (true); gameState.setBombPlanted (true);
for (const auto &notify : bots) { for (const auto &notify : bots) {
if (notify->m_isAlive) { if (notify->m_isAlive) {
@ -68,7 +68,7 @@ void MessageDispatcher::netMsgTextMsg () {
} }
} }
} }
graph.setBombOrigin (); gameState.setBombOrigin ();
} }
// check for burst fire message // check for burst fire message
@ -319,7 +319,7 @@ void MessageDispatcher::netMsgHLTV () {
// need to start new round ? (we're tracking FOV reset message) // need to start new round ? (we're tracking FOV reset message)
if (m_args[players].long_ == 0 && m_args[fov].long_ == 0) { if (m_args[players].long_ == 0 && m_args[fov].long_ == 0) {
bots.initRound (); gameState.roundStart ();
} }
} }
@ -389,7 +389,7 @@ void MessageDispatcher::netMsgBarTime () {
m_bot->m_hasProgressBar = true; // the progress bar on a hud m_bot->m_hasProgressBar = true; // the progress bar on a hud
// notify bots about defusing has started // notify bots about defusing has started
if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted () && m_bot->m_team == Team::CT) { if (game.mapIs (MapFlags::Demolition) && gameState.isBombPlanted () && m_bot->m_team == Team::CT) {
bots.notifyBombDefuse (); bots.notifyBombDefuse ();
} }
} }
@ -435,7 +435,7 @@ void MessageDispatcher::netMsgResetHUD () {
if (m_bot) { if (m_bot) {
m_bot->spawned (); m_bot->spawned ();
} }
bots.setResetHUD (true); gameState.setResetHUD (true);
} }
MessageDispatcher::MessageDispatcher () { MessageDispatcher::MessageDispatcher () {

View file

@ -60,12 +60,12 @@ int Bot::findBestGoal () {
bool hasMoreHostagesAround = false; bool hasMoreHostagesAround = false;
// try to search nearby-unused hostage, and if so, go to next goal // try to search nearby-unused hostage, and if so, go to next goal
if (bots.hasInterestingEntities ()) { if (gameState.hasInterestingEntities ()) {
const auto &interesting = bots.getInterestingEntities (); const auto &interesting = gameState.getInterestingEntities ();
// search world for hostages // search world for hostages
for (const auto &ent : interesting) { for (const auto &ent : interesting) {
if (!util.isHostageEntity (ent)) { if (!game.isHostageEntity (ent)) {
continue; continue;
} }
bool hostageInUse = false; bool hostageInUse = false;
@ -134,7 +134,7 @@ int Bot::findBestGoal () {
} }
} }
else if (game.mapIs (MapFlags::Demolition) && m_team == Team::CT) { else if (game.mapIs (MapFlags::Demolition) && m_team == Team::CT) {
if (bots.isBombPlanted () && getCurrentTaskId () != Task::EscapeFromBomb && !graph.getBombOrigin ().empty ()) { if (gameState.isBombPlanted () && getCurrentTaskId () != Task::EscapeFromBomb && !gameState.getBombOrigin ().empty ()) {
if (bots.hasBombSay (BombPlantedSay::ChatSay)) { if (bots.hasBombSay (BombPlantedSay::ChatSay)) {
pushChatMessage (Chat::Plant); pushChatMessage (Chat::Plant);
@ -149,10 +149,10 @@ int Bot::findBestGoal () {
defensive += 10.0f; defensive += 10.0f;
} }
} }
else if (game.mapIs (MapFlags::Demolition) && m_team == Team::Terrorist && bots.getRoundStartTime () + 10.0f < game.time ()) { else if (game.mapIs (MapFlags::Demolition) && m_team == Team::Terrorist && gameState.getRoundStartTime () + 10.0f < game.time ()) {
// send some terrorists to guard planted bomb // send some terrorists to guard planted bomb
if (!m_defendedBomb && bots.isBombPlanted () && getCurrentTaskId () != Task::EscapeFromBomb && getBombTimeleft () >= 15.0f) { if (!m_defendedBomb && gameState.isBombPlanted () && getCurrentTaskId () != Task::EscapeFromBomb && gameState.getBombTimeLeft () >= 15.0f) {
return m_chosenGoalIndex = findDefendNode (graph.getBombOrigin ()); return m_chosenGoalIndex = findDefendNode (gameState.getBombOrigin ());
} }
} }
else if (game.mapIs (MapFlags::Escape)) { else if (game.mapIs (MapFlags::Escape)) {
@ -202,9 +202,9 @@ int Bot::findBestGoal () {
int Bot::findBestGoalWhenBombAction () { int Bot::findBestGoalWhenBombAction () {
int result = kInvalidNodeIndex; int result = kInvalidNodeIndex;
if (!bots.isBombPlanted () && !cv_ignore_objectives) { if (!gameState.isBombPlanted () && !cv_ignore_objectives) {
game.searchEntities ("classname", "weaponbox", [&] (edict_t *ent) { game.searchEntities ("classname", "weaponbox", [&] (edict_t *ent) {
if (util.isModel (ent, "backpack.mdl")) { if (game.isEntityModelMatches (ent, "backpack.mdl")) {
result = graph.getNearest (game.getEntityOrigin (ent)); result = graph.getNearest (game.getEntityOrigin (ent));
if (graph.exists (result)) { if (graph.exists (result)) {
@ -230,7 +230,7 @@ int Bot::findBestGoalWhenBombAction () {
} }
} }
else if (!m_defendedBomb) { else if (!m_defendedBomb) {
const auto &bombOrigin = graph.getBombOrigin (); const auto &bombOrigin = gameState.getBombOrigin ();
if (!bombOrigin.empty ()) { if (!bombOrigin.empty ()) {
m_defendedBomb = true; m_defendedBomb = true;
@ -239,7 +239,7 @@ int Bot::findBestGoalWhenBombAction () {
const auto &path = graph[result]; const auto &path = graph[result];
const float bombTimer = mp_c4timer.as <float> (); const float bombTimer = mp_c4timer.as <float> ();
const float timeMidBlowup = bots.getTimeBombPlanted () + (bombTimer * 0.5f + bombTimer * 0.25f) - graph.calculateTravelTime (pev->maxspeed, pev->origin, path.origin); const float timeMidBlowup = gameState.getTimeBombPlanted () + (bombTimer * 0.5f + bombTimer * 0.25f) - graph.calculateTravelTime (pev->maxspeed, pev->origin, path.origin);
if (timeMidBlowup > game.time ()) { if (timeMidBlowup > game.time ()) {
clearTask (Task::MoveToPosition); // remove any move tasks clearTask (Task::MoveToPosition); // remove any move tasks
@ -288,14 +288,15 @@ int Bot::findGoalPost (int tactic, IntArray *defensive, IntArray *offensive) {
else if (tactic == GoalTactic::Goal && !graph.m_goalPoints.empty ()) { // map goal node else if (tactic == GoalTactic::Goal && !graph.m_goalPoints.empty ()) { // map goal node
// force bomber to select closest goal, if round-start goal was reset by something // force bomber to select closest goal, if round-start goal was reset by something
if (m_isVIP || (m_hasC4 && bots.getRoundStartTime () + 20.0f < game.time ())) { if (m_isVIP || (m_hasC4 && gameState.getRoundStartTime () + 20.0f < game.time ())) {
float nearestDistanceSq = kInfiniteDistance; float nearestDistanceSq = kInfiniteDistance;
int count = 0; int count = 0;
for (const auto &point : graph.m_goalPoints) { for (const auto &point : graph.m_goalPoints) {
const float distanceSq = graph[point].origin.distanceSq (pev->origin); const float distanceSq = graph[point].origin.distanceSq (pev->origin);
if (distanceSq > cr::sqrf (1024.0f) || isGroupOfEnemies (graph[point].origin)) { if (distanceSq > cr::sqrf (1024.0f)
|| (rg.chance (25) && isGroupOfEnemies (graph[point].origin))) {
continue; continue;
} }
if (distanceSq < nearestDistanceSq) { if (distanceSq < nearestDistanceSq) {
@ -469,7 +470,7 @@ void Bot::resetCollision () {
void Bot::ignoreCollision () { void Bot::ignoreCollision () {
resetCollision (); resetCollision ();
m_lastCollTime = game.time () + 0.5f; m_lastCollTime = game.time () + 0.65f;
m_checkTerrain = false; m_checkTerrain = false;
} }
@ -594,18 +595,18 @@ void Bot::doPlayerAvoidance (const Vector &normal) {
void Bot::checkTerrain (const Vector &dirNormal) { void Bot::checkTerrain (const Vector &dirNormal) {
// if avoiding someone do not consider stuck // if avoiding someone do not consider stuckn
TraceResult tr {}; TraceResult tr {};
m_isStuck = false; m_isStuck = false;
const auto tid = getCurrentTaskId (); const auto tid = getCurrentTaskId ();
// minimal speed for consider stuck // minimal speed for consider stuck
const float minimalSpeed = isDucking () ? kMinMovedDistance : kMinMovedDistance * 4; const float minimalSpeed = isDucking () ? 7.0f : 10.0f;
const auto randomProbeTime = rg (0.75f, 1.15f); const auto randomProbeTime = rg (0.50f, 0.75f);
// standing still, no need to check? // standing still, no need to check?
if ((cr::abs (m_moveSpeed) >= minimalSpeed || cr::abs (m_strafeSpeed) >= minimalSpeed) if ((m_moveSpeed >= minimalSpeed || m_strafeSpeed >= minimalSpeed)
&& m_lastCollTime < game.time () && m_lastCollTime < game.time ()
&& tid != Task::Attack && tid != Task::Attack
&& tid != Task::Camp) { && tid != Task::Camp) {
@ -616,7 +617,7 @@ void Bot::checkTerrain (const Vector &dirNormal) {
m_firstCollideTime = 0.0f; m_firstCollideTime = 0.0f;
} }
// didn't we move enough previously? // didn't we move enough previously?
else if (m_movedDistance < kMinMovedDistance && (m_prevSpeed > 20.0f || m_prevVelocity < m_moveSpeed / 2)) { else if (m_movedDistance < kMinMovedDistance && m_prevSpeed > 20.0f) {
m_prevTime = game.time (); // then consider being stuck m_prevTime = game.time (); // then consider being stuck
m_isStuck = true; m_isStuck = true;
@ -652,14 +653,16 @@ void Bot::checkTerrain (const Vector &dirNormal) {
resetCollision (); // reset collision memory if not being stuck for 0.5 secs resetCollision (); // reset collision memory if not being stuck for 0.5 secs
} }
else { else {
const auto state = m_collideMoves[m_collStateIndex];
// remember to keep pressing stuff if it was necessary ago // remember to keep pressing stuff if it was necessary ago
if (m_collideMoves[m_collStateIndex] == CollisionState::Duck && (isOnFloor () || isInWater ())) { if (state == CollisionState::Duck && (isOnFloor () || isInWater ())) {
pev->button |= IN_DUCK; pev->button |= IN_DUCK;
} }
else if (m_collideMoves[m_collStateIndex] == CollisionState::StrafeLeft) { else if (state == CollisionState::StrafeLeft) {
setStrafeSpeed (dirNormal, -pev->maxspeed); setStrafeSpeed (dirNormal, -pev->maxspeed);
} }
else if (m_collideMoves[m_collStateIndex] == CollisionState::StrafeRight) { else if (state == CollisionState::StrafeRight) {
setStrafeSpeed (dirNormal, pev->maxspeed); setStrafeSpeed (dirNormal, pev->maxspeed);
} }
} }
@ -682,6 +685,7 @@ void Bot::checkTerrain (const Vector &dirNormal) {
bits |= CollisionProbe::Duck; bits |= CollisionProbe::Duck;
} }
// collision check allowed if not flying through the air // collision check allowed if not flying through the air
if (isOnFloor () || isOnLadder () || isInWater ()) { if (isOnFloor () || isOnLadder () || isInWater ()) {
uint32_t state[kMaxCollideMoves * 2 + 1] {}; uint32_t state[kMaxCollideMoves * 2 + 1] {};
@ -695,7 +699,6 @@ void Bot::checkTerrain (const Vector &dirNormal) {
state[i++] = CollisionState::StrafeRight; state[i++] = CollisionState::StrafeRight;
state[i++] = CollisionState::Duck; state[i++] = CollisionState::Duck;
// now weight all possible states
if (bits & CollisionProbe::Jump) { if (bits & CollisionProbe::Jump) {
state[i] = 0; state[i] = 0;
@ -744,7 +747,7 @@ void Bot::checkTerrain (const Vector &dirNormal) {
} }
++i; ++i;
// now weight all possible states
if (bits & CollisionProbe::Strafe) { if (bits & CollisionProbe::Strafe) {
state[i] = 0; state[i] = 0;
state[i + 1] = 0; state[i + 1] = 0;
@ -812,6 +815,7 @@ void Bot::checkTerrain (const Vector &dirNormal) {
} }
} }
if (bits & CollisionProbe::Duck) { if (bits & CollisionProbe::Duck) {
state[i] = 0; state[i] = 0;
@ -900,10 +904,7 @@ void Bot::checkTerrain (const Vector &dirNormal) {
} }
void Bot::checkFall () { void Bot::checkFall () {
if (isPreviousLadder ()) { if (isPreviousLadder () || (m_pathFlags & NodeFlag::Ladder) || isOnLadder ()) {
return;
}
else if ((m_pathFlags & NodeFlag::Ladder) && isPreviousLadder () && isOnLadder ()) {
return; return;
} }
@ -947,27 +948,29 @@ void Bot::checkFall () {
const float nowDistanceSq = pev->origin.distanceSq (m_checkFallPoint[1]); const float nowDistanceSq = pev->origin.distanceSq (m_checkFallPoint[1]);
if (nowDistanceSq > baseDistanceSq if (nowDistanceSq > baseDistanceSq
&& (nowDistanceSq > baseDistanceSq * 1.2f || nowDistanceSq > baseDistanceSq + 200.0f) && (nowDistanceSq > baseDistanceSq * 1.8f || nowDistanceSq > baseDistanceSq + 260.0f)
&& baseDistanceSq >= cr::sqrf (80.0f) && nowDistanceSq >= cr::sqrf (100.0f)) { && baseDistanceSq >= cr::sqrf (124.0f) && nowDistanceSq >= cr::sqrf (146.0f)) {
fixFall = true; fixFall = true;
} }
else if (m_checkFallPoint[1].z > pev->origin.z + 128.0f else if (m_checkFallPoint[1].z > pev->origin.z + 138.0f
&& m_checkFallPoint[0].z > pev->origin.z + 128.0f) { || m_checkFallPoint[0].z > pev->origin.z + 138.0f) {
fixFall = true; fixFall = true;
} }
else if (m_currentNodeIndex != kInvalidNodeIndex else if (m_currentNodeIndex != kInvalidNodeIndex
&& nowDistanceSq > cr::sqrf (16.0f) && nowDistanceSq > cr::sqrf (32.0f)
&& m_checkFallPoint[1].z > pev->origin.z + 62.0f) { && m_checkFallPoint[1].z > pev->origin.z + 72.0f) {
fixFall = true; fixFall = true;
} }
if (fixFall) { if (fixFall) {
if (graph.exists (m_currentNodeIndex) && !isReachableNode (m_currentNodeIndex)) {
m_currentNodeIndex = kInvalidNodeIndex; m_currentNodeIndex = kInvalidNodeIndex;
findValidNode (); findValidNode ();
m_fixFallTimer.start (1.0f); m_fixFallTimer.start (1.0f);
} }
}
} }
void Bot::moveToGoal () { void Bot::moveToGoal () {
@ -1002,22 +1005,22 @@ void Bot::moveToGoal () {
// press jump button if we need to leave the ladder // press jump button if we need to leave the ladder
if (!(m_pathFlags & NodeFlag::Ladder) if (!(m_pathFlags & NodeFlag::Ladder)
&& isPreviousLadder () && isPreviousLadder ()
&& isOnFloor ()
&& isOnLadder () && isOnLadder ()
&& m_moveSpeed > 50.0f && m_moveSpeed > 50.0f
&& pev->velocity.lengthSq () < 50.0f) { && pev->velocity.lengthSq () < 50.0f
&& m_ladderDir == LadderDir::Down) {
pev->button |= IN_JUMP; pev->button |= IN_JUMP;
m_jumpTime = game.time () + 1.0f; m_jumpTime = game.time () + 1.0f;
} }
#if 0
const auto distanceSq2d = m_destOrigin.distanceSq2d (pev->origin + pev->velocity * m_frameInterval); const auto distanceSq2d = m_destOrigin.distanceSq2d (pev->origin + pev->velocity * m_frameInterval);
if (distanceSq2d < cr::sqrf (m_moveSpeed) * m_frameInterval && getTask ()->data != kInvalidNodeIndex) { if (distanceSq2d < cr::sqrf (m_moveSpeed * m_frameInterval) && getTask ()->data != kInvalidNodeIndex) {
m_moveSpeed = distanceSq2d; m_moveSpeed = distanceSq2d * 2.0f;
}
if (m_moveSpeed > pev->maxspeed) {
m_moveSpeed = pev->maxspeed;
} }
m_moveSpeed = cr::clamp (m_moveSpeed, 4.0f, pev->maxspeed);
#endif
m_lastUsedNodesTime = game.time (); m_lastUsedNodesTime = game.time ();
// special movement for swimming here // special movement for swimming here
@ -1105,7 +1108,7 @@ bool Bot::updateNavigation () {
if (m_desiredVelocity.length2d () > 0.0f) { if (m_desiredVelocity.length2d () > 0.0f) {
pev->velocity = m_desiredVelocity; pev->velocity = m_desiredVelocity;
} }
else { else if (graph.isAnalyzed ()) {
auto feet = pev->origin + pev->mins; auto feet = pev->origin + pev->mins;
auto node = Vector { m_pathOrigin.x, m_pathOrigin.y, m_pathOrigin.z - ((m_pathFlags & NodeFlag::Crouch) ? 18.0f : 36.0f) }; auto node = Vector { m_pathOrigin.x, m_pathOrigin.y, m_pathOrigin.z - ((m_pathFlags & NodeFlag::Crouch) ? 18.0f : 36.0f) };
@ -1137,10 +1140,11 @@ bool Bot::updateNavigation () {
m_jumpFinished = true; m_jumpFinished = true;
m_checkTerrain = false; m_checkTerrain = false;
m_desiredVelocity.clear (); m_desiredVelocity.clear ();
m_currentTravelFlags &= ~PathFlag::Jump;
// cool down a little if next path after current will be jump // cool down a little if next path after current will be jump
if (m_jumpSequence) { if (m_jumpSequence) {
startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + rg (0.75f, 1.2f) + m_frameInterval, false); startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + rg (0.75f, 1.25f) + m_frameInterval, false);
m_jumpSequence = false; m_jumpSequence = false;
} }
} }
@ -1151,19 +1155,13 @@ bool Bot::updateNavigation () {
} }
if (m_pathFlags & NodeFlag::Ladder) { if (m_pathFlags & NodeFlag::Ladder) {
constexpr auto kLadderOffset = Vector (0.0f, 0.0f, 36.0f);
const auto prevNodeIndex = m_previousNodes[0]; const auto prevNodeIndex = m_previousNodes[0];
const float ladderDistance = pev->origin.distance (m_pathOrigin); const auto ladderDistanceSq = pev->origin.distanceSq (m_pathOrigin);
// do a precise movement when very near // do a precise movement when very near
if (graph.exists (prevNodeIndex) && !(graph[prevNodeIndex].flags & NodeFlag::Ladder) && ladderDistance < 64.0f) { if (graph.exists (prevNodeIndex)
if (m_pathOrigin.z >= pev->origin.z + 16.0f) { && !(graph[prevNodeIndex].flags & NodeFlag::Ladder)
m_pathOrigin = m_path->origin + kLadderOffset; && ladderDistanceSq < cr::sqrf (64.0f)) {
}
else if (m_pathOrigin.z < pev->origin.z - 16.0f) {
m_pathOrigin = m_path->origin - kLadderOffset;
}
if (!isDucking ()) { if (!isDucking ()) {
m_moveSpeed = pev->maxspeed * 0.4f; m_moveSpeed = pev->maxspeed * 0.4f;
@ -1173,20 +1171,14 @@ bool Bot::updateNavigation () {
if (!isOnLadder ()) { if (!isOnLadder ()) {
pev->button &= ~IN_DUCK; pev->button &= ~IN_DUCK;
} }
m_approachingLadderTimer.start (m_frameInterval * 6.0f); m_approachingLadderTimer.start (2.0f * m_frameInterval);
} }
if (!isOnLadder () && isOnFloor () && !isDucking ()) { if (!isOnLadder () && isOnFloor () && !isDucking ()) {
if (!isPreviousLadder ()) { if (!isPreviousLadder ()) {
m_moveSpeed = ladderDistance; m_moveSpeed = cr::sqrtf (ladderDistanceSq);
}
if (m_moveSpeed < 150.0f) {
m_moveSpeed = 150.0f;
}
else if (m_moveSpeed > pev->maxspeed) {
m_moveSpeed = pev->maxspeed;
} }
m_moveSpeed = cr::clamp (m_moveSpeed, 160.0f, pev->maxspeed);
} }
// special detection if someone is using the ladder (to prevent to have bots-towers on ladders) // special detection if someone is using the ladder (to prevent to have bots-towers on ladders)
@ -1231,7 +1223,7 @@ bool Bot::updateNavigation () {
if (game.mapIs (MapFlags::HasDoors) || (m_pathFlags & NodeFlag::Button)) { if (game.mapIs (MapFlags::HasDoors) || (m_pathFlags & NodeFlag::Button)) {
game.testLine (pev->origin, m_pathOrigin, TraceIgnore::Monsters, ent (), &tr); game.testLine (pev->origin, m_pathOrigin, TraceIgnore::Monsters, ent (), &tr);
if (!game.isNullEntity (tr.pHit) && game.isNullEntity (m_liftEntity) && util.isDoorEntity (tr.pHit)) { if (!game.isNullEntity (tr.pHit) && game.isNullEntity (m_liftEntity) && game.isDoorEntity (tr.pHit)) {
const auto &origin = game.getEntityOrigin (tr.pHit); const auto &origin = game.getEntityOrigin (tr.pHit);
const float distanceSq = pev->origin.distanceSq (origin); const float distanceSq = pev->origin.distanceSq (origin);
@ -1240,7 +1232,7 @@ bool Bot::updateNavigation () {
ignoreCollision (); // don't consider being stuck ignoreCollision (); // don't consider being stuck
// also 'use' the door randomly // also 'use' the door randomly
if (rg.chance (50)) { if (m_buttonPushTime < game.time () && rg.chance (50)) {
// do not use door directly under xash, or we will get failed assert in gamedll code // do not use door directly under xash, or we will get failed assert in gamedll code
if (game.is (GameFlags::Xash3D)) { if (game.is (GameFlags::Xash3D)) {
pev->button |= IN_USE; pev->button |= IN_USE;
@ -1254,7 +1246,7 @@ bool Bot::updateNavigation () {
// make sure we are always facing the door when going through it // make sure we are always facing the door when going through it
m_aimFlags &= ~(AimFlags::LastEnemy | AimFlags::PredictPath); m_aimFlags &= ~(AimFlags::LastEnemy | AimFlags::PredictPath);
m_canChooseAimDirection = false; m_canSetAimDirection = false;
// delay task // delay task
if (m_buttonPushTime < game.time ()) { if (m_buttonPushTime < game.time ()) {
@ -1285,7 +1277,7 @@ bool Bot::updateNavigation () {
util.findNearestPlayer (reinterpret_cast <void **> (&nearest), ent (), 256.0f, false, false, true, true, false); util.findNearestPlayer (reinterpret_cast <void **> (&nearest), ent (), 256.0f, false, false, true, true, false);
// check if enemy is penetrable // check if enemy is penetrable
if (util.isAlive (nearest) && isPenetrableObstacle (nearest->v.origin) && !cv_ignore_enemies) { if (game.isAliveEntity (nearest) && isPenetrableObstacle (nearest->v.origin) && !cv_ignore_enemies) {
m_seeEnemyTime = game.time (); m_seeEnemyTime = game.time ();
m_states |= Sense::SeeingEnemy | Sense::SuspectEnemy; m_states |= Sense::SeeingEnemy | Sense::SuspectEnemy;
@ -1324,26 +1316,35 @@ bool Bot::updateNavigation () {
} }
} }
float desiredDistanceSq = cr::sqrf (8.0f); float desiredDistanceSq = cr::sqrf (48.0f);
const float nodeDistanceSq = pev->origin.distanceSq (m_pathOrigin); float nodeDistanceSq = pev->origin.distanceSq (m_pathOrigin);
// initialize the radius for a special node type, where the node is considered to be reached // initialize the radius for a special node type, where the node is considered to be reached
if (m_pathFlags & NodeFlag::Lift) { if (m_pathFlags & NodeFlag::Lift) {
desiredDistanceSq = cr::sqrf (50.0f); desiredDistanceSq = cr::sqrf (50.0f);
} }
else if (isDucking () || (m_pathFlags & NodeFlag::Goal)) { else if (isDucking () || (m_pathFlags & NodeFlag::Goal)) {
desiredDistanceSq = cr::sqrf (9.0f); desiredDistanceSq = cr::sqrf (25.0f);
// on cs_ maps goals are usually hostages, so increase reachability distance for them, they (hostages) picked anyway // on cs_ maps goals are usually hostages, so increase reachability distance for them, they (hostages) picked anyway
if (game.mapIs (MapFlags::HostageRescue) && (m_pathFlags & NodeFlag::Goal)) { if (game.mapIs (MapFlags::HostageRescue)
desiredDistanceSq = cr::sqrf (128.0f); && (m_pathFlags & NodeFlag::Goal)) {
desiredDistanceSq = cr::sqrf (96.0f);
} }
} }
else if (m_pathFlags & NodeFlag::Ladder) { else if (isOnLadder () || (m_pathFlags & NodeFlag::Ladder)) {
desiredDistanceSq = cr::sqrf (16.0f); desiredDistanceSq = cr::sqrf (15.0f);
} }
else if (m_currentTravelFlags & PathFlag::Jump) { else if (m_currentTravelFlags & PathFlag::Jump) {
desiredDistanceSq = 0.0f; desiredDistanceSq = 0.0f;
if (pev->velocity.z > 16.0f) {
desiredDistanceSq = cr::sqrf (8.0f);
}
}
else if (m_pathFlags & NodeFlag::Crouch) {
desiredDistanceSq = cr::sqrf (6.0f);
} }
else if (m_path->number == cv_debug_goal.as <int> ()) { else if (m_path->number == cv_debug_goal.as <int> ()) {
desiredDistanceSq = 0.0f; desiredDistanceSq = 0.0f;
@ -1370,14 +1371,14 @@ bool Bot::updateNavigation () {
// if just recalculated path, assume reached current node // if just recalculated path, assume reached current node
if (!m_repathTimer.elapsed () && !pathHasFlags) { if (!m_repathTimer.elapsed () && !pathHasFlags) {
desiredDistanceSq = cr::sqrf (72.0f); desiredDistanceSq = cr::sqrf (48.0f);
} }
// needs precise placement - check if we get past the point // needs precise placement - check if we get past the point
if (desiredDistanceSq < cr::sqrf (20.0f) && nodeDistanceSq < cr::sqrf (30.0f)) { if (desiredDistanceSq < cr::sqrf (16.0f) && nodeDistanceSq < cr::sqrf (30.0f)) {
const auto predictRangeSq = m_pathOrigin.distanceSq (pev->origin + pev->velocity * m_frameInterval); const auto predictRangeSq = m_pathOrigin.distanceSq (pev->origin + pev->velocity * m_frameInterval);
if (predictRangeSq >= nodeDistanceSq || predictRangeSq <= desiredDistanceSq) { if (predictRangeSq > nodeDistanceSq || predictRangeSq <= desiredDistanceSq) {
desiredDistanceSq = nodeDistanceSq + 1.0f; desiredDistanceSq = nodeDistanceSq + 1.0f;
} }
} }
@ -1386,7 +1387,10 @@ bool Bot::updateNavigation () {
// is sitting there, so the bot is unable to reach the node because of other player on it, and he starts to jumping and so on // is sitting there, so the bot is unable to reach the node because of other player on it, and he starts to jumping and so on
// here we're clearing task memory data (important!), since task executor may restart goal with one from memory, so this process // here we're clearing task memory data (important!), since task executor may restart goal with one from memory, so this process
// will go in cycle, and forcing bot to re-create new route. // will go in cycle, and forcing bot to re-create new route.
if (m_pathWalk.hasNext () && m_pathWalk.next () == m_pathWalk.last () && isOccupiedNode (m_pathWalk.next (), pathHasFlags)) { if (m_pathWalk.hasNext ()
&& m_pathWalk.next () == m_pathWalk.last ()
&& isOccupiedNode (m_pathWalk.next (), pathHasFlags)) {
getTask ()->data = kInvalidNodeIndex; getTask ()->data = kInvalidNodeIndex;
m_currentNodeIndex = kInvalidNodeIndex; m_currentNodeIndex = kInvalidNodeIndex;
@ -1409,6 +1413,9 @@ bool Bot::updateNavigation () {
// update the practice for team // update the practice for team
practice.setValue (m_team, m_chosenGoalIndex, m_currentNodeIndex, goalValue); practice.setValue (m_team, m_chosenGoalIndex, m_currentNodeIndex, goalValue);
// ignore collision
ignoreCollision ();
} }
return true; return true;
} }
@ -1418,7 +1425,7 @@ bool Bot::updateNavigation () {
const int taskTarget = getTask ()->data; const int taskTarget = getTask ()->data;
if (game.mapIs (MapFlags::Demolition) if (game.mapIs (MapFlags::Demolition)
&& bots.isBombPlanted () && gameState.isBombPlanted ()
&& m_team == Team::CT && m_team == Team::CT
&& getCurrentTaskId () != Task::EscapeFromBomb && getCurrentTaskId () != Task::EscapeFromBomb
&& taskTarget != kInvalidNodeIndex) { && taskTarget != kInvalidNodeIndex) {
@ -1481,7 +1488,7 @@ bool Bot::updateLiftHandling () {
game.testLine (pev->origin, m_pathOrigin, TraceIgnore::Everything, ent (), &tr); game.testLine (pev->origin, m_pathOrigin, TraceIgnore::Everything, ent (), &tr);
if (tr.flFraction < 1.0f if (tr.flFraction < 1.0f
&& util.isDoorEntity (tr.pHit) && game.isDoorEntity (tr.pHit)
&& (m_liftState == LiftState::None || m_liftState == LiftState::WaitingFor || m_liftState == LiftState::LookingButtonOutside) && (m_liftState == LiftState::None || m_liftState == LiftState::WaitingFor || m_liftState == LiftState::LookingButtonOutside)
&& pev->groundentity != tr.pHit) { && pev->groundentity != tr.pHit) {
@ -1869,7 +1876,7 @@ bool Bot::findNextBestNodeEx (const IntArray &data, bool handleFails) {
} }
// in case low node density do not skip previous ones, in case of fail reduce max nodes to skip // in case low node density do not skip previous ones, in case of fail reduce max nodes to skip
const auto &numToSkip = graph.length () < 512 ? 0 : (handleFails ? 1 : rg (1, 4)); const auto &numToSkip = graph.length () < 512 || m_isStuck ? 0 : (handleFails ? 1 : rg (1, 4));
for (const auto &i : data) { for (const auto &i : data) {
const auto &path = graph[i]; const auto &path = graph[i];
@ -2117,7 +2124,7 @@ int Bot::findBombNode () {
const auto &goals = graph.m_goalPoints; const auto &goals = graph.m_goalPoints;
const auto &bomb = graph.getBombOrigin (); const auto &bomb = gameState.getBombOrigin ();
const auto &audible = isBombAudible (); const auto &audible = isBombAudible ();
// take the nearest to bomb nodes instead of goal if close enough // take the nearest to bomb nodes instead of goal if close enough
@ -2459,7 +2466,7 @@ bool Bot::selectBestNextNode () {
const auto currentNodeIndex = m_pathWalk.first (); const auto currentNodeIndex = m_pathWalk.first ();
const auto prevNodeIndex = m_currentNodeIndex; const auto prevNodeIndex = m_currentNodeIndex;
if (!isOccupiedNode (currentNodeIndex)) { if (isOnLadder () || !isOccupiedNode (currentNodeIndex)) {
return false; return false;
} }
@ -2535,9 +2542,9 @@ bool Bot::advanceMovement () {
// only if we in normal task and bomb is not planted // only if we in normal task and bomb is not planted
if (tid == Task::Normal if (tid == Task::Normal
&& bots.getRoundMidTime () + 5.0f < game.time () && gameState.getRoundMidTime () + 5.0f < game.time ()
&& m_timeCamping + 5.0f < game.time () && m_timeCamping + 5.0f < game.time ()
&& !bots.isBombPlanted () && !gameState.isBombPlanted ()
&& m_personality != Personality::Rusher && m_personality != Personality::Rusher
&& !m_hasC4 && !m_isVIP && !m_hasC4 && !m_isVIP
&& m_loosedBombNodeIndex == kInvalidNodeIndex && m_loosedBombNodeIndex == kInvalidNodeIndex
@ -2549,7 +2556,7 @@ bool Bot::advanceMovement () {
auto kills = static_cast <float> (practice.getDamage (m_team, nextIndex, nextIndex)); auto kills = static_cast <float> (practice.getDamage (m_team, nextIndex, nextIndex));
// if damage done higher than one // if damage done higher than one
if (kills > 1.0f && bots.getRoundMidTime () > game.time ()) { if (kills > 1.0f && gameState.getRoundMidTime () > game.time ()) {
switch (m_personality) { switch (m_personality) {
case Personality::Normal: case Personality::Normal:
kills *= 0.33f; kills *= 0.33f;
@ -2589,10 +2596,11 @@ bool Bot::advanceMovement () {
bool isCurrentJump = false; bool isCurrentJump = false;
// find out about connection flags // find out about connection flags
if (destIndex != kInvalidNodeIndex && m_currentNodeIndex != kInvalidNodeIndex) { if (graph.exists (destIndex) && m_path) {
for (const auto &link : m_path->links) { for (const auto &link : m_path->links) {
if (link.index == destIndex) { if (link.index == destIndex) {
m_currentTravelFlags = link.flags; m_currentTravelFlags = link.flags;
m_desiredVelocity = link.velocity; m_desiredVelocity = link.velocity;
m_jumpFinished = false; m_jumpFinished = false;
@ -2602,7 +2610,7 @@ bool Bot::advanceMovement () {
} }
// if graph is analyzed try our special jumps // if graph is analyzed try our special jumps
if (graph.isAnalyzed () || analyzer.isAnalyzed ()) { if ((graph.isAnalyzed () || analyzer.isAnalyzed ()) && !(m_path->flags & NodeFlag::Ladder)) {
for (const auto &link : m_path->links) { for (const auto &link : m_path->links) {
if (link.index == destIndex) { if (link.index == destIndex) {
const float diff = cr::abs (m_path->origin.z - graph[destIndex].origin.z); const float diff = cr::abs (m_path->origin.z - graph[destIndex].origin.z);
@ -2647,7 +2655,7 @@ bool Bot::advanceMovement () {
// mark as jump sequence, if the current and next paths are jumps // mark as jump sequence, if the current and next paths are jumps
if (isCurrentJump) { if (isCurrentJump) {
m_jumpSequence = willJump; m_jumpSequence = willJump && jumpDistanceSq > cr::sqrf (96.0f);
} }
// is there a jump node right ahead and do we need to draw out the light weapon ? // is there a jump node right ahead and do we need to draw out the light weapon ?
@ -2665,6 +2673,7 @@ bool Bot::advanceMovement () {
// get ladder nodes used by other (first moving) bots // get ladder nodes used by other (first moving) bots
for (const auto &other : bots) { for (const auto &other : bots) {
// if another bot uses this ladder, wait 3 secs // if another bot uses this ladder, wait 3 secs
if (other.get () != this && other->m_isAlive && other->m_currentNodeIndex == destIndex && other->isOnLadder ()) { if (other.get () != this && other->m_isAlive && other->m_currentNodeIndex == destIndex && other->isOnLadder ()) {
startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + 3.0f, false); startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + 3.0f, false);
@ -2724,21 +2733,30 @@ void Bot::setPathOrigin () {
else if (radius > 0.0f) { else if (radius > 0.0f) {
setNonZeroPathOrigin (); setNonZeroPathOrigin ();
} }
if (isOnLadder ()) { if (isOnLadder ()) {
edict_t *ladder = nullptr;
game.searchEntities (m_pathOrigin, 96.0f, [&] (edict_t *e) {
if (e->v.classname.str () == "func_ladder") {
ladder = e;
return EntitySearchResult::Break;
}
return EntitySearchResult::Continue;
});
if (!game.isNullEntity (ladder)) {
TraceResult tr {}; TraceResult tr {};
game.testLine (Vector (pev->origin.x, pev->origin.y, pev->absmin.z), m_pathOrigin, TraceIgnore::Everything, ent (), &tr); game.testLine ({ pev->origin.x, pev->origin.y, ladder->v.absmin.z }, m_pathOrigin, TraceIgnore::Monsters, ent (), &tr);
if (tr.flFraction < 1.0f) { if (tr.flFraction < 1.0f) {
m_pathOrigin = m_pathOrigin + (pev->origin - m_pathOrigin) * 0.5f + Vector (0.0f, 0.0f, 32.0f); m_pathOrigin = graph[m_currentNodeIndex].origin + (pev->origin - m_pathOrigin) * 0.5 + Vector (0.0f, 0.0f, 32.0f);
}
m_ladderDir = m_pathOrigin.z < pev->origin.z ? LadderDir::Down : LadderDir::Up;
} }
} }
} }
void Bot::updateRightRef () {
m_rightRef = Vector { 0.0f, pev->angles.y, 0.0f }.right (); // convert current view angle to vectors for traceline math...
}
bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) { bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) {
// checks if bot is blocked in his movement direction (excluding doors) // checks if bot is blocked in his movement direction (excluding doors)
@ -2752,11 +2770,11 @@ bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) {
if (!game.mapIs (MapFlags::HasDoors)) { if (!game.mapIs (MapFlags::HasDoors)) {
return false; return false;
} }
return result->flFraction < 1.0f && !util.isDoorEntity (result->pHit); return result->flFraction < 1.0f && !game.isDoorEntity (result->pHit);
}; };
auto checkHostage = [&] (TraceResult *result) { auto checkHostage = [&] (TraceResult *result) {
return result->flFraction < 1.0f && m_team == Team::Terrorist && !util.isHostageEntity (result->pHit); return result->flFraction < 1.0f && m_team == Team::Terrorist && !game.isHostageEntity (result->pHit);
}; };
// trace from the bot's eyes straight forward... // trace from the bot's eyes straight forward...
@ -2764,8 +2782,8 @@ bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) {
// check if the trace hit something... // check if the trace hit something...
if (tr->flFraction < 1.0f) { if (tr->flFraction < 1.0f) {
if ((game.mapIs (MapFlags::HasDoors) && util.isDoorEntity (tr->pHit)) if ((game.mapIs (MapFlags::HasDoors) && game.isDoorEntity (tr->pHit))
|| (m_team == Team::CT && util.isHostageEntity (tr->pHit))) { || (m_team == Team::CT && game.isHostageEntity (tr->pHit))) {
return false; return false;
} }
return true; // bot's head will hit something return true; // bot's head will hit something
@ -2773,12 +2791,12 @@ bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) {
constexpr auto kVec00N16 = Vector (0.0f, 0.0f, -16.0f); constexpr auto kVec00N16 = Vector (0.0f, 0.0f, -16.0f);
// right referential vector // right referential vector
updateRightRef (); auto right = Vector { 0.0f, pev->angles.y, 0.0f }.right ();
// bot's head is clear, check at shoulder level... // bot's head is clear, check at shoulder level...
// trace from the bot's shoulder left diagonal forward to the right shoulder... // trace from the bot's shoulder left diagonal forward to the right shoulder...
src = getEyesPos () + kVec00N16 - m_rightRef * -16.0f; src = getEyesPos () + kVec00N16 - right * -16.0f;
forward = getEyesPos () + kVec00N16 + m_rightRef * 16.0f + normal * 24.0f; forward = getEyesPos () + kVec00N16 + right * 16.0f + normal * 24.0f;
game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr); game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr);
@ -2789,8 +2807,8 @@ bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) {
// bot's head is clear, check at shoulder level... // bot's head is clear, check at shoulder level...
// trace from the bot's shoulder right diagonal forward to the left shoulder... // trace from the bot's shoulder right diagonal forward to the left shoulder...
src = getEyesPos () + kVec00N16 + m_rightRef * 16.0f; src = getEyesPos () + kVec00N16 + right * 16.0f;
forward = getEyesPos () + kVec00N16 - m_rightRef * -16.0f + normal * 24.0f; forward = getEyesPos () + kVec00N16 - right * -16.0f + normal * 24.0f;
game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr); game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr);
@ -2825,8 +2843,8 @@ bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) {
constexpr auto kVec00N24 = Vector (0.0f, 0.0f, -24.0f); constexpr auto kVec00N24 = Vector (0.0f, 0.0f, -24.0f);
// trace from the left waist to the right forward waist pos // trace from the left waist to the right forward waist pos
src = pev->origin + kVec00N17 - m_rightRef * -16.0f; src = pev->origin + kVec00N17 - right * -16.0f;
forward = pev->origin + kVec00N17 + m_rightRef * 16.0f + normal * 24.0f; forward = pev->origin + kVec00N17 + right * 16.0f + normal * 24.0f;
// trace from the bot's waist straight forward... // trace from the bot's waist straight forward...
game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr); game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr);
@ -2837,8 +2855,8 @@ bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) {
} }
// trace from the left waist to the right forward waist pos // trace from the left waist to the right forward waist pos
src = pev->origin + kVec00N24 + m_rightRef * 16.0f; src = pev->origin + kVec00N24 + right * 16.0f;
forward = pev->origin + kVec00N24 - m_rightRef * -16.0f + normal * 24.0f; forward = pev->origin + kVec00N24 - right * -16.0f + normal * 24.0f;
game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr); game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr);
@ -2917,7 +2935,7 @@ bool Bot::canJumpUp (const Vector &normal) {
if (!isOnFloor () && (isOnLadder () || !isInWater ())) { if (!isOnFloor () && (isOnLadder () || !isInWater ())) {
return false; return false;
} }
updateRightRef (); auto right = Vector { 0.0f, pev->angles.y, 0.0f }.right (); // convert current view angle to vectors for traceline math...
// check for normal jump height first... // check for normal jump height first...
auto src = pev->origin + Vector (0.0f, 0.0f, -36.0f + 45.0f); auto src = pev->origin + Vector (0.0f, 0.0f, -36.0f + 45.0f);
@ -2927,7 +2945,7 @@ bool Bot::canJumpUp (const Vector &normal) {
game.testLine (src, dest, TraceIgnore::Monsters, ent (), &tr); game.testLine (src, dest, TraceIgnore::Monsters, ent (), &tr);
if (tr.flFraction < 1.0f) { if (tr.flFraction < 1.0f) {
return doneCanJumpUp (normal, m_rightRef); return doneCanJumpUp (normal, right);
} }
else { else {
// now trace from jump height upward to check for obstructions... // now trace from jump height upward to check for obstructions...
@ -2942,7 +2960,7 @@ bool Bot::canJumpUp (const Vector &normal) {
} }
// now check same height to one side of the bot... // now check same height to one side of the bot...
src = pev->origin + m_rightRef * 16.0f + Vector (0.0f, 0.0f, -36.0f + 45.0f); src = pev->origin + right * 16.0f + Vector (0.0f, 0.0f, -36.0f + 45.0f);
dest = src + normal * 32.0f; dest = src + normal * 32.0f;
// trace a line forward at maximum jump height... // trace a line forward at maximum jump height...
@ -2950,7 +2968,7 @@ bool Bot::canJumpUp (const Vector &normal) {
// if trace hit something, return false // if trace hit something, return false
if (tr.flFraction < 1.0f) { if (tr.flFraction < 1.0f) {
return doneCanJumpUp (normal, m_rightRef); return doneCanJumpUp (normal, right);
} }
// now trace from jump height upward to check for obstructions... // now trace from jump height upward to check for obstructions...
@ -2965,7 +2983,7 @@ bool Bot::canJumpUp (const Vector &normal) {
} }
// now check same height on the other side of the bot... // now check same height on the other side of the bot...
src = pev->origin + (-m_rightRef * 16.0f) + Vector (0.0f, 0.0f, -36.0f + 45.0f); src = pev->origin + (-right * 16.0f) + Vector (0.0f, 0.0f, -36.0f + 45.0f);
dest = src + normal * 32.0f; dest = src + normal * 32.0f;
// trace a line forward at maximum jump height... // trace a line forward at maximum jump height...
@ -2973,7 +2991,7 @@ bool Bot::canJumpUp (const Vector &normal) {
// if trace hit something, return false // if trace hit something, return false
if (tr.flFraction < 1.0f) { if (tr.flFraction < 1.0f) {
return doneCanJumpUp (normal, m_rightRef); return doneCanJumpUp (normal, right);
} }
// now trace from jump height upward to check for obstructions... // now trace from jump height upward to check for obstructions...
@ -3081,10 +3099,10 @@ bool Bot::canDuckUnder (const Vector &normal) {
if (tr.flFraction < 1.0f) { if (tr.flFraction < 1.0f) {
return false; return false;
} }
updateRightRef (); auto right = Vector { 0.0f, pev->angles.y, 0.0f }.right ();
// now check same height to one side of the bot... // now check same height to one side of the bot...
src = baseHeight + m_rightRef * 16.0f; src = baseHeight + right * 16.0f;
dest = src + normal * 32.0f; dest = src + normal * 32.0f;
// trace a line forward at duck height... // trace a line forward at duck height...
@ -3096,7 +3114,7 @@ bool Bot::canDuckUnder (const Vector &normal) {
} }
// now check same height on the other side of the bot... // now check same height on the other side of the bot...
src = baseHeight + (-m_rightRef * 16.0f); src = baseHeight + (-right * 16.0f);
dest = src + normal * 32.0f; dest = src + normal * 32.0f;
// trace a line forward at duck height... // trace a line forward at duck height...
@ -3113,14 +3131,14 @@ bool Bot::isBlockedLeft () {
if (m_moveSpeed < 0.0f) { if (m_moveSpeed < 0.0f) {
direction = -48.0f; direction = -48.0f;
} }
Vector right {}, forward {}; Vector left {}, forward {};
pev->angles.angleVectors (&forward, &right, nullptr); pev->angles.angleVectors (&forward, &left, nullptr);
// do a trace to the left... // do a trace to the left...
game.testLine (pev->origin, pev->origin - forward * direction - right * 48.0f, TraceIgnore::Monsters, ent (), &tr); game.testLine (pev->origin, pev->origin + forward * direction - left * -48.0f, TraceIgnore::Monsters, ent (), &tr);
// check if the trace hit something... // check if the trace hit something...
if (game.mapIs (MapFlags::HasDoors) && tr.flFraction < 1.0f && !util.isDoorEntity (tr.pHit)) { if (game.mapIs (MapFlags::HasDoors) && tr.flFraction < 1.0f && !game.isDoorEntity (tr.pHit)) {
return true; // bot's body will hit something return true; // bot's body will hit something
} }
return false; return false;
@ -3130,8 +3148,8 @@ bool Bot::isBlockedRight () {
TraceResult tr {}; TraceResult tr {};
float direction = 48.0f; float direction = 48.0f;
if (m_moveSpeed > 0.0f) { if (m_moveSpeed < 0.0f) {
direction = 48.0f; direction = -48.0f;
} }
Vector right {}, forward {}; Vector right {}, forward {};
pev->angles.angleVectors (&forward, &right, nullptr); pev->angles.angleVectors (&forward, &right, nullptr);
@ -3140,7 +3158,7 @@ bool Bot::isBlockedRight () {
game.testLine (pev->origin, pev->origin + forward * direction + right * 48.0f, TraceIgnore::Monsters, ent (), &tr); game.testLine (pev->origin, pev->origin + forward * direction + right * 48.0f, TraceIgnore::Monsters, ent (), &tr);
// check if the trace hit something... // check if the trace hit something...
if (game.mapIs (MapFlags::HasDoors) && tr.flFraction < 1.0f && !util.isDoorEntity (tr.pHit)) { if (game.mapIs (MapFlags::HasDoors) && tr.flFraction < 1.0f && !game.isDoorEntity (tr.pHit)) {
return true; // bot's body will hit something return true; // bot's body will hit something
} }
return false; return false;
@ -3302,7 +3320,7 @@ int Bot::getNearestToPlantedBomb () {
// search the bomb on the map // search the bomb on the map
game.searchEntities ("classname", "grenade", [&] (edict_t *ent) { game.searchEntities ("classname", "grenade", [&] (edict_t *ent) {
if (util.isModel (ent, bombModel)) { if (game.isEntityModelMatches (ent, bombModel)) {
result = graph.getNearest (game.getEntityOrigin (ent)); result = graph.getNearest (game.getEntityOrigin (ent));
if (graph.exists (result)) { if (graph.exists (result)) {

View file

@ -17,12 +17,12 @@ float PlannerHeuristic::gfunctionKillsDist (int team, int currentIndex, int pare
if (parentIndex == kInvalidNodeIndex) { if (parentIndex == kInvalidNodeIndex) {
return 0.0f; return 0.0f;
} }
auto cost = practice.plannerGetDamage (team, currentIndex, currentIndex, true); auto cost = practice.getDamageEx (team, currentIndex, currentIndex, true);
const auto &current = graph[currentIndex]; const auto &current = graph[currentIndex];
for (const auto &neighbour : current.links) { for (const auto &neighbour : current.links) {
if (neighbour.index != kInvalidNodeIndex) { if (neighbour.index != kInvalidNodeIndex) {
cost += practice.plannerGetDamage (team, neighbour.index, neighbour.index, false); cost += practice.getDamageEx (team, neighbour.index, neighbour.index, false);
} }
} }
@ -38,19 +38,23 @@ float PlannerHeuristic::gfunctionKillsDistCTWithHostage (int team, int currentIn
if (current.flags & NodeFlag::NoHostage) { if (current.flags & NodeFlag::NoHostage) {
return kInfiniteHeuristic; return kInfiniteHeuristic;
} }
else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) { else if (current.flags & NodeFlag::Ladder) {
return gfunctionKillsDist (team, currentIndex, parentIndex) * 500.0f; return gfunctionKillsDist (team, currentIndex, parentIndex) * 6.0f;
} }
else if (current.flags & NodeFlag::Crouch) {
return gfunctionKillsDist (team, currentIndex, parentIndex) * 3.0f;
}
return gfunctionKillsDist (team, currentIndex, parentIndex); return gfunctionKillsDist (team, currentIndex, parentIndex);
} }
float PlannerHeuristic::gfunctionKills (int team, int currentIndex, int) { float PlannerHeuristic::gfunctionKills (int team, int currentIndex, int) {
auto cost = practice.plannerGetDamage (team, currentIndex, currentIndex, false); auto cost = practice.getDamageEx (team, currentIndex, currentIndex, false);
const auto &current = graph[currentIndex]; const auto &current = graph[currentIndex];
for (const auto &neighbour : current.links) { for (const auto &neighbour : current.links) {
if (neighbour.index != kInvalidNodeIndex) { if (neighbour.index != kInvalidNodeIndex) {
cost += practice.plannerGetDamage (team, neighbour.index, neighbour.index, false); cost += practice.getDamageEx (team, neighbour.index, neighbour.index, false);
} }
} }
@ -69,9 +73,13 @@ auto PlannerHeuristic::gfunctionKillsCTWithHostage (int team, int currentIndex,
if (current.flags & NodeFlag::NoHostage) { if (current.flags & NodeFlag::NoHostage) {
return kInfiniteHeuristic; return kInfiniteHeuristic;
} }
else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) { else if (current.flags & NodeFlag::Ladder) {
return gfunctionKills (team, currentIndex, parentIndex) * 500.0f; return gfunctionKills (team, currentIndex, parentIndex) * 6.0f;
} }
else if (current.flags & NodeFlag::Crouch) {
return gfunctionKills (team, currentIndex, parentIndex) * 3.0f;
}
return gfunctionKills (team, currentIndex, parentIndex); return gfunctionKills (team, currentIndex, parentIndex);
} }
@ -103,9 +111,13 @@ float PlannerHeuristic::gfunctionPathDistWithHostage (int, int currentIndex, int
if (current.flags & NodeFlag::NoHostage) { if (current.flags & NodeFlag::NoHostage) {
return kInfiniteHeuristic; return kInfiniteHeuristic;
} }
else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) { else if (current.flags & NodeFlag::Ladder) {
return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex) * 500.0f; return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex) * 6.0f;
} }
else if (current.flags & NodeFlag::Crouch) {
return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex) * 3.0f;
}
return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex); return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex);
} }
@ -184,7 +196,7 @@ bool AStarAlgo::cantSkipNode (const int a, const int b, bool skipVisCheck) {
} }
if (!skipVisCheck) { if (!skipVisCheck) {
const bool notVisible = !vistab.visible (ag.number, bg.number) || !vistab.visible (bg.number, ag.number); const bool notVisible = !vistab.visibleBothSides (ag.number, bg.number);
if (notVisible) { if (notVisible) {
return true; return true;
@ -203,7 +215,7 @@ bool AStarAlgo::cantSkipNode (const int a, const int b, bool skipVisCheck) {
const float distanceSq = ag.origin.distanceSq (bg.origin); const float distanceSq = ag.origin.distanceSq (bg.origin);
const bool tooFar = distanceSq > cr::sqrf (400.0f); const bool tooFar = distanceSq > cr::sqrf (400.0f);
const bool tooClose = distanceSq < cr::sqrtf (40.0f); const bool tooClose = distanceSq < cr::sqrf (40.0f);
if (tooFar || tooClose) { if (tooFar || tooClose) {
return true; return true;
@ -264,7 +276,7 @@ AStarResult AStarAlgo::find (int botTeam, int srcIndex, int destIndex, NodeAdder
auto rsRandomizer = 1.0f; auto rsRandomizer = 1.0f;
// randomize path on round start now and then // randomize path on round start now and then
if (cv_path_randomize_on_round_start && bots.getRoundStartTime () + 2.0f > game.time ()) { if (cv_path_randomize_on_round_start && gameState.getRoundStartTime () + 2.0f > game.time ()) {
rsRandomizer = rg (0.5f, static_cast <float> (botTeam) * 2.0f); rsRandomizer = rg (0.5f, static_cast <float> (botTeam) * 2.0f);
} }
@ -372,7 +384,7 @@ void FloydWarshallAlgo::syncRebuild () {
for (int k = 0; k < m_length; ++k) { for (int k = 0; k < m_length; ++k) {
for (int i = 0; i < m_length; ++i) { for (int i = 0; i < m_length; ++i) {
for (int j = 0; j < m_length; ++j) { for (int j = 0; j < m_length; ++j) {
int distance = (matrix + (i * m_length) + k)->dist + (matrix + (k * m_length) + j)->dist; const auto distance = (matrix + (i * m_length) + k)->dist + (matrix + (k * m_length) + j)->dist;
if (distance < (matrix + (i * m_length) + j)->dist) { if (distance < (matrix + (i * m_length) + j)->dist) {
*(matrix + (i * m_length) + j) = { (matrix + (i * m_length) + k)->index, distance }; *(matrix + (i * m_length) + j) = { (matrix + (i * m_length) + k)->index, distance };

View file

@ -64,7 +64,7 @@ void BotPractice::setDamage (int32_t team, int32_t start, int32_t goal, int32_t
m_data[{start, goal, team}].damage = static_cast <int16_t> (value); m_data[{start, goal, team}].damage = static_cast <int16_t> (value);
} }
float BotPractice::plannerGetDamage (int32_t team, int32_t start, int32_t goal, bool addTeamHighestDamage) { float BotPractice::getDamageEx (int32_t team, int32_t start, int32_t goal, bool addTeamHighestDamage) {
if (!m_damageUpdateLock.tryLock ()) { if (!m_damageUpdateLock.tryLock ()) {
return 0.0f; return 0.0f;
} }

View file

@ -17,7 +17,7 @@ template <typename U> bool BotStorage::load (SmallArray <U> &data, ExtenHeader *
// graphs can be downloaded... // graphs can be downloaded...
const bool isGraph = !!(type.option & StorageOption::Graph); const bool isGraph = !!(type.option & StorageOption::Graph);
const bool isDebug = cv_debug; const bool isDebug = cv_debug || game.isDeveloperMode ();
MemFile file (filename); // open the file MemFile file (filename); // open the file
data.clear (); data.clear ();
@ -85,12 +85,8 @@ template <typename U> bool BotStorage::load (SmallArray <U> &data, ExtenHeader *
if (tryReload ()) { if (tryReload ()) {
return true; return true;
} }
if (game.isDeveloperMode ()) {
return error (isGraph, isDebug, file, "Unable to open %s file for reading (filename: '%s').", type.name, filename); return error (isGraph, isDebug, file, "Unable to open %s file for reading (filename: '%s').", type.name, filename);
} }
return false;
}
// erase the current graph just in case // erase the current graph just in case
auto unlinkIfGraph = [&] () { auto unlinkIfGraph = [&] () {
@ -178,7 +174,7 @@ template <typename U> bool BotStorage::load (SmallArray <U> &data, ExtenHeader *
if (isGraph) { if (isGraph) {
resetRetries (); resetRetries ();
ExtenHeader extenHeader; ExtenHeader extenHeader {};
strings.copy (extenHeader.author, exten->author, cr::bufsize (exten->author)); strings.copy (extenHeader.author, exten->author, cr::bufsize (exten->author));
if (extenSize <= actuallyRead) { if (extenSize <= actuallyRead) {

View file

@ -9,9 +9,11 @@
ConVar cv_display_welcome_text ("display_welcome_text", "1", "Enables or disables showing a welcome message to the host entity on game start."); ConVar cv_display_welcome_text ("display_welcome_text", "1", "Enables or disables showing a welcome message to the host entity on game start.");
ConVar cv_enable_query_hook ("enable_query_hook", "0", "Enables or disables fake server query responses, which show bots as real players in the server browser."); ConVar cv_enable_query_hook ("enable_query_hook", "0", "Enables or disables fake server query responses, which show bots as real players in the server browser.");
ConVar cv_breakable_health_limit ("breakable_health_limit", "500.0", "Specifies the maximum health of a breakable object that the bot will consider destroying.", true, 1.0f, 3000.0);
ConVar cv_enable_fake_steamids ("enable_fake_steamids", "0", "Allows or disallows bots to return a fake Steam ID."); ConVar cv_enable_fake_steamids ("enable_fake_steamids", "0", "Allows or disallows bots to return a fake Steam ID.");
ConVar cv_smoke_grenade_checks ("smoke_grenade_checks", "2", "Affects the bot's vision by smoke clouds.", true, 0.0f, 2.0f);
ConVar cv_smoke_greande_checks_radius ("greande_checks_radius", "220", "Radius to check for smoke clouds around a detonated grenade.", true, 32.0f, 320.0f);
BotSupport::BotSupport () { BotSupport::BotSupport () {
m_needToSendWelcome = false; m_needToSendWelcome = false;
m_welcomeReceiveTime = 0.0f; m_welcomeReceiveTime = 0.0f;
@ -82,13 +84,6 @@ BotSupport::BotSupport () {
m_clients.resize (kGameMaxPlayers + 1); m_clients.resize (kGameMaxPlayers + 1);
} }
bool BotSupport::isAlive (edict_t *ent) {
if (game.isNullEntity (ent)) {
return false;
}
return ent->v.deadflag == DEAD_NO && ent->v.health > 0.0f && ent->v.movetype != MOVETYPE_NOCLIP;
}
bool BotSupport::isVisible (const Vector &origin, edict_t *ent) { bool BotSupport::isVisible (const Vector &origin, edict_t *ent) {
if (game.isNullEntity (ent)) { if (game.isNullEntity (ent)) {
return false; return false;
@ -151,109 +146,6 @@ void BotSupport::decalTrace (TraceResult *trace, int decalIndex) {
msg.end (); msg.end ();
} }
bool BotSupport::isPlayer (edict_t *ent) {
if (game.isNullEntity (ent)) {
return false;
}
if (ent->v.flags & FL_PROXY) {
return false;
}
if ((ent->v.flags & (FL_CLIENT | FL_FAKECLIENT)) || bots[ent] != nullptr) {
return !strings.isEmpty (ent->v.netname.chars ());
}
return false;
}
bool BotSupport::isMonster (edict_t *ent) {
if (game.isNullEntity (ent)) {
return false;
}
if (~ent->v.flags & FL_MONSTER) {
return false;
}
if (isHostageEntity (ent)) {
return false;
}
return true;
}
bool BotSupport::isItem (edict_t *ent) {
return ent && ent->v.classname.str ().contains ("item_");
}
bool BotSupport::isPlayerVIP (edict_t *ent) {
if (!game.mapIs (MapFlags::Assassination)) {
return false;
}
if (!isPlayer (ent)) {
return false;
}
return *(engfuncs.pfnInfoKeyValue (engfuncs.pfnGetInfoKeyBuffer (ent), "model")) == 'v';
}
bool BotSupport::isDoorEntity (edict_t *ent) {
if (game.isNullEntity (ent)) {
return false;
}
const auto classHash = ent->v.classname.str ().hash ();
constexpr auto kFuncDoor = StringRef::fnv1a32 ("func_door");
constexpr auto kFuncDoorRotating = StringRef::fnv1a32 ("func_door_rotating");
return classHash == kFuncDoor || classHash == kFuncDoorRotating;
}
bool BotSupport::isHostageEntity (edict_t *ent) {
if (game.isNullEntity (ent)) {
return false;
}
const auto classHash = ent->v.classname.str ().hash ();
constexpr auto kHostageEntity = StringRef::fnv1a32 ("hostage_entity");
constexpr auto kMonsterScientist = StringRef::fnv1a32 ("monster_scientist");
return classHash == kHostageEntity || classHash == kMonsterScientist;
}
bool BotSupport::isBreakableEntity (edict_t *ent, bool initialSeed) {
if (!initialSeed) {
if (!game.hasBreakables ()) {
return false;
}
}
if (game.isNullEntity (ent) || ent == game.getStartEntity () || (!initialSeed && !game.isBreakableValid (ent))) {
return false;
}
const auto limit = cv_breakable_health_limit.as <float> ();
// not shoot-able
if (ent->v.health >= limit) {
return false;
}
constexpr auto kFuncBreakable = StringRef::fnv1a32 ("func_breakable");
constexpr auto kFuncPushable = StringRef::fnv1a32 ("func_pushable");
constexpr auto kFuncWall = StringRef::fnv1a32 ("func_wall");
if (ent->v.takedamage > 0.0f && ent->v.impulse <= 0 && !(ent->v.flags & FL_WORLDBRUSH) && !(ent->v.spawnflags & SF_BREAK_TRIGGER_ONLY)) {
const auto classHash = ent->v.classname.str ().hash ();
if (classHash == kFuncBreakable || (classHash == kFuncPushable && (ent->v.spawnflags & SF_PUSH_BREAKABLE)) || classHash == kFuncWall) {
return ent->v.movetype == MOVETYPE_PUSH || ent->v.movetype == MOVETYPE_PUSHSTEP;
}
}
return false;
}
bool BotSupport::isFakeClient (edict_t *ent) {
return bots[ent] != nullptr || (!game.isNullEntity (ent) && (ent->v.flags & FL_FAKECLIENT));
}
void BotSupport::checkWelcome () { void BotSupport::checkWelcome () {
// the purpose of this function, is to send quick welcome message, to the listenserver entity. // the purpose of this function, is to send quick welcome message, to the listenserver entity.
@ -264,7 +156,7 @@ void BotSupport::checkWelcome () {
const bool needToSendMsg = (graph.length () > 0 ? m_needToSendWelcome : true); const bool needToSendMsg = (graph.length () > 0 ? m_needToSendWelcome : true);
auto receiveEnt = game.getLocalEntity (); auto receiveEnt = game.getLocalEntity ();
if (isAlive (receiveEnt) && m_welcomeReceiveTime < 1.0f && needToSendMsg) { if (game.isAliveEntity (receiveEnt) && m_welcomeReceiveTime < 1.0f && needToSendMsg) {
m_welcomeReceiveTime = game.time () + 2.0f + mp_freezetime.as <float> (); // receive welcome message in four seconds after game has commencing m_welcomeReceiveTime = game.time () + 2.0f + mp_freezetime.as <float> (); // receive welcome message in four seconds after game has commencing
} }
@ -344,7 +236,7 @@ bool BotSupport::findNearestPlayer (void **pvHolder, edict_t *to, float searchDi
continue; continue;
} }
if ((sameTeam && client.team != game.getTeam (to)) if ((sameTeam && client.team != game.getPlayerTeam (to))
|| (needAlive && !(client.flags & ClientFlags::Alive)) || (needAlive && !(client.flags & ClientFlags::Alive))
|| (needBot && !bots[client.ent]) || (needBot && !bots[client.ent])
|| (needDrawn && (client.ent->v.effects & EF_NODRAW)) || (needDrawn && (client.ent->v.effects & EF_NODRAW))
@ -373,7 +265,7 @@ void BotSupport::updateClients () {
client.ent = player; client.ent = player;
client.flags |= ClientFlags::Used; client.flags |= ClientFlags::Used;
if (isAlive (player)) { if (game.isAliveEntity (player)) {
client.flags |= ClientFlags::Alive; client.flags |= ClientFlags::Alive;
} }
else { else {
@ -392,10 +284,6 @@ void BotSupport::updateClients () {
} }
} }
bool BotSupport::isModel (const edict_t *ent, StringRef model) {
return model.startsWith (ent->v.model.chars (9));
}
String BotSupport::getCurrentDateTime () { String BotSupport::getCurrentDateTime () {
time_t ticks = time (&ticks); time_t ticks = time (&ticks);
tm timeinfo {}; tm timeinfo {};
@ -409,7 +297,7 @@ String BotSupport::getCurrentDateTime () {
} }
StringRef BotSupport::getFakeSteamId (edict_t *ent) { StringRef BotSupport::getFakeSteamId (edict_t *ent) {
if (!cv_enable_fake_steamids || !isPlayer (ent)) { if (!cv_enable_fake_steamids || !game.isPlayerEntity (ent)) {
return "BOT"; return "BOT";
} }
auto botNameHash = StringRef::fnv1a32 (ent->v.netname.chars ()); auto botNameHash = StringRef::fnv1a32 (ent->v.netname.chars ());
@ -479,3 +367,117 @@ void BotSupport::setCustomCvarDescriptions () {
}); });
game.setCvarDescription (cv_restricted_weapons, restrictInfo); game.setCvarDescription (cv_restricted_weapons, restrictInfo);
} }
bool BotSupport::isLineBlockedBySmoke (const Vector &from, const Vector &to) {
if (!gameState.hasActiveGrenades ()) {
return false;
}
// distance along line of sight covered by smoke
float totalSmokedLength = 0.0f;
Vector sightDir = to - from;
const float sightLength = sightDir.normalizeInPlace ();
for (auto pent : gameState.getActiveGrenades ()) {
if (game.isNullEntity (pent)) {
continue;
}
// need drawn models
if (pent->v.effects & EF_NODRAW) {
continue;
}
// smoke must be on a ground
if (!(pent->v.flags & FL_ONGROUND)) {
continue;
}
// must be a smoke grenade
if (!game.isEntityModelMatches (pent, kSmokeModelName)) {
continue;
}
const float smokeRadiusSq = cr::sqrf (cv_smoke_greande_checks_radius.as <float> ());
const Vector &smokeOrigin = game.getEntityOrigin (pent);
Vector toGrenade = smokeOrigin - from;
float alongDist = toGrenade | sightDir;
// compute closest point to grenade along line of sight ray
Vector close {};
// constrain closest point to line segment
if (alongDist < 0.0f) {
close = from;
}
else if (alongDist >= sightLength) {
close = to;
}
else {
close = from + sightDir * alongDist;
}
// if closest point is within smoke radius, the line overlaps the smoke cloud
Vector toClose = close - smokeOrigin;
float lengthSq = toClose.lengthSq ();
if (lengthSq < smokeRadiusSq) {
// some portion of the ray intersects the cloud
const float fromSq = toGrenade.lengthSq ();
const float toSq = (smokeOrigin - to).lengthSq ();
if (fromSq < smokeRadiusSq) {
if (toSq < smokeRadiusSq) {
// both 'from' and 'to' lie within the cloud
// entire length is smoked
totalSmokedLength += (to - from).length ();
}
else {
// 'from' is inside the cloud, 'to' is outside
// compute half of total smoked length as if ray crosses entire cloud chord
float halfSmokedLength = cr::sqrtf (smokeRadiusSq - lengthSq);
if (alongDist > 0.0f) {
// ray goes thru 'close'
totalSmokedLength += halfSmokedLength + (close - from).length ();
}
else {
// ray starts after 'close'
totalSmokedLength += halfSmokedLength - (close - from).length ();
}
}
}
else if (toSq < smokeRadiusSq) {
// 'from' is outside the cloud, 'to' is inside
// compute half of total smoked length as if ray crosses entire cloud chord
const float halfSmokedLength = cr::sqrtf (smokeRadiusSq - lengthSq);
Vector v = to - smokeOrigin;
if ((v | sightDir) > 0.0f) {
// ray goes thru 'close'
totalSmokedLength += halfSmokedLength + (close - to).length ();
}
else {
// ray ends before 'close'
totalSmokedLength += halfSmokedLength - (close - to).length ();
}
}
else {
// 'from' and 'to' lie outside of the cloud - the line of sight completely crosses it
// determine the length of the chord that crosses the cloud
const float smokedLength = 2.0f * cr::sqrtf (smokeRadiusSq - lengthSq);
totalSmokedLength += smokedLength;
}
}
}
// define how much smoke a bot can see thru
const float maxSmokedLength = 0.7f * cv_smoke_greande_checks_radius.as <float> ();
// return true if the total length of smoke-covered line-of-sight is too much
return totalSmokedLength > maxSmokedLength;
}

View file

@ -21,19 +21,25 @@ void Bot::normal_ () {
const int debugGoal = cv_debug_goal.as <int> (); const int debugGoal = cv_debug_goal.as <int> ();
// user forced a node as a goal? // user forced a node as a goal?
if (debugGoal != kInvalidNodeIndex) { if (graph.exists (debugGoal)) {
if (getTask ()->data != debugGoal) { if (getTask ()->data != debugGoal) {
clearSearchNodes (); clearSearchNodes ();
getTask ()->data = debugGoal; getTask ()->data = debugGoal;
m_chosenGoalIndex = debugGoal; m_chosenGoalIndex = debugGoal;
} }
const auto &debugOrigin = graph[debugGoal].origin;
const auto distanceToDebugOriginSq = debugOrigin.distanceSq (pev->origin);
if (!isDucking () && distanceToDebugOriginSq < cr::sqrf (172.0f)) {
m_moveSpeed = pev->maxspeed * 0.4f;
}
// stop the bot if precisely reached debug goal // stop the bot if precisely reached debug goal
if (m_currentNodeIndex == debugGoal) { if (m_currentNodeIndex == debugGoal) {
const auto &debugOrigin = graph[debugGoal].origin; if (distanceToDebugOriginSq < cr::sqrf (22.0f)
&& util.isVisible (debugOrigin, ent ())) {
if (debugOrigin.distanceSq2d (pev->origin) < cr::sqrf (22.0f) && util.isVisible (debugOrigin, ent ())) {
m_moveToGoal = false; m_moveToGoal = false;
m_checkTerrain = false; m_checkTerrain = false;
@ -48,7 +54,7 @@ void Bot::normal_ () {
// bots rushing with knife, when have no enemy (thanks for idea to nicebot project) // bots rushing with knife, when have no enemy (thanks for idea to nicebot project)
if (cv_random_knife_attacks if (cv_random_knife_attacks
&& usesKnife () && usesKnife ()
&& (game.isNullEntity (m_lastEnemy) || !util.isAlive (m_lastEnemy)) && (game.isNullEntity (m_lastEnemy) || !game.isAliveEntity (m_lastEnemy))
&& game.isNullEntity (m_enemy) && game.isNullEntity (m_enemy)
&& m_knifeAttackTime < game.time () && m_knifeAttackTime < game.time ()
&& !m_hasHostage && !m_hasHostage
@ -71,7 +77,7 @@ void Bot::normal_ () {
// if bomb planted and it's a CT calculate new path to bomb point if he's not already heading for // if bomb planted and it's a CT calculate new path to bomb point if he's not already heading for
if (!m_bombSearchOverridden if (!m_bombSearchOverridden
&& bots.isBombPlanted () && gameState.isBombPlanted ()
&& m_team == Team::CT && m_team == Team::CT
&& getTask ()->data != kInvalidNodeIndex && getTask ()->data != kInvalidNodeIndex
&& !(graph[getTask ()->data].flags & NodeFlag::Goal) && !(graph[getTask ()->data].flags & NodeFlag::Goal)
@ -84,7 +90,7 @@ void Bot::normal_ () {
// reached the destination (goal) node? // reached the destination (goal) node?
if (updateNavigation ()) { if (updateNavigation ()) {
// if we're reached the goal, and there is not enemies, notify the team // if we're reached the goal, and there is not enemies, notify the team
if (!bots.isBombPlanted () if (!gameState.isBombPlanted ()
&& m_currentNodeIndex != kInvalidNodeIndex && m_currentNodeIndex != kInvalidNodeIndex
&& (m_pathFlags & NodeFlag::Goal) && (m_pathFlags & NodeFlag::Goal)
&& rg.chance (15) && rg.chance (15)
@ -106,7 +112,7 @@ void Bot::normal_ () {
&& m_moveSpeed >= getShiftSpeed () && m_moveSpeed >= getShiftSpeed ()
&& game.isNullEntity (m_pickupItem)) { && game.isNullEntity (m_pickupItem)) {
if (!(game.mapIs (MapFlags::Demolition) && bots.isBombPlanted () && m_team == Team::CT)) { if (!(game.mapIs (MapFlags::Demolition) && gameState.isBombPlanted () && m_team == Team::CT)) {
startTask (Task::Spraypaint, TaskPri::Spraypaint, kInvalidNodeIndex, game.time () + 1.0f, false); startTask (Task::Spraypaint, TaskPri::Spraypaint, kInvalidNodeIndex, game.time () + 1.0f, false);
} }
} }
@ -134,7 +140,7 @@ void Bot::normal_ () {
} }
// don't allow vip on as_ maps to camp + don't allow terrorist carrying c4 to camp // don't allow vip on as_ maps to camp + don't allow terrorist carrying c4 to camp
if (m_isVIP || (game.mapIs (MapFlags::Demolition) && m_team == Team::Terrorist && !bots.isBombPlanted () && m_hasC4)) { if (m_isVIP || (game.mapIs (MapFlags::Demolition) && m_team == Team::Terrorist && !gameState.isBombPlanted () && m_hasC4)) {
campingAllowed = false; campingAllowed = false;
} }
@ -218,7 +224,7 @@ void Bot::normal_ () {
} }
} }
else if (m_team == Team::CT) { else if (m_team == Team::CT) {
if (!bots.isBombPlanted () && numFriendsNear (pev->origin, 210.0f) < 4) { if (!gameState.isBombPlanted () && numFriendsNear (pev->origin, 210.0f) < 4) {
const int index = findDefendNode (m_path->origin); const int index = findDefendNode (m_path->origin);
float campTime = rg (25.0f, 40.0f); float campTime = rg (25.0f, 40.0f);
@ -258,7 +264,7 @@ void Bot::normal_ () {
auto pathSearchType = m_pathType; auto pathSearchType = m_pathType;
// override with fast path // override with fast path
if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted ()) { if (game.mapIs (MapFlags::Demolition) && gameState.isBombPlanted ()) {
pathSearchType = rg.chance (80) ? FindPath::Fast : FindPath::Optimal; pathSearchType = rg.chance (80) ? FindPath::Fast : FindPath::Optimal;
} }
ensureCurrentNodeIndex (); ensureCurrentNodeIndex ();
@ -280,7 +286,7 @@ void Bot::normal_ () {
&& (m_heardSoundTime + 6.0f >= game.time () || (m_states & Sense::HearingEnemy)) && (m_heardSoundTime + 6.0f >= game.time () || (m_states & Sense::HearingEnemy))
&& numEnemiesNear (pev->origin, 768.0f) >= 1 && numEnemiesNear (pev->origin, 768.0f) >= 1
&& !isKnifeMode () && !isKnifeMode ()
&& !bots.isBombPlanted ()) { && !gameState.isBombPlanted ()) {
m_moveSpeed = shiftSpeed; m_moveSpeed = shiftSpeed;
} }
@ -288,7 +294,7 @@ void Bot::normal_ () {
// bot hasn't seen anything in a long time and is asking his teammates to report in // bot hasn't seen anything in a long time and is asking his teammates to report in
if (cv_radio_mode.as <int> () > 1 if (cv_radio_mode.as <int> () > 1
&& bots.getLastRadio (m_team) != Radio::ReportInTeam && bots.getLastRadio (m_team) != Radio::ReportInTeam
&& bots.getRoundStartTime () + 20.0f < game.time () && gameState.getRoundStartTime () + 20.0f < game.time ()
&& m_askCheckTime < game.time () && rg.chance (15) && m_askCheckTime < game.time () && rg.chance (15)
&& m_seeEnemyTime + rg (45.0f, 80.0f) < game.time () && m_seeEnemyTime + rg (45.0f, 80.0f) < game.time ()
&& numFriendsNear (pev->origin, 1024.0f) == 0) { && numFriendsNear (pev->origin, 1024.0f) == 0) {
@ -357,7 +363,7 @@ void Bot::huntEnemy_ () {
clearTask (Task::Hunt); clearTask (Task::Hunt);
m_prevGoalIndex = kInvalidNodeIndex; m_prevGoalIndex = kInvalidNodeIndex;
} }
else if (game.getTeam (m_lastEnemy) == m_team) { else if (game.getPlayerTeam (m_lastEnemy) == m_team) {
// don't hunt down our teammate... // don't hunt down our teammate...
clearTask (Task::Hunt); clearTask (Task::Hunt);
@ -413,7 +419,7 @@ void Bot::huntEnemy_ () {
void Bot::seekCover_ () { void Bot::seekCover_ () {
m_aimFlags |= AimFlags::Nav; m_aimFlags |= AimFlags::Nav;
if (!util.isAlive (m_lastEnemy)) { if (!game.isAliveEntity (m_lastEnemy)) {
completeTask (); completeTask ();
m_prevGoalIndex = kInvalidNodeIndex; m_prevGoalIndex = kInvalidNodeIndex;
} }
@ -565,7 +571,7 @@ void Bot::blind_ () {
if (rg.chance (50) if (rg.chance (50)
&& m_difficulty >= Difficulty::Normal && m_difficulty >= Difficulty::Normal
&& !m_lastEnemyOrigin.empty () && !m_lastEnemyOrigin.empty ()
&& util.isPlayer (m_lastEnemy) && game.isPlayerEntity (m_lastEnemy)
&& !usesSniper ()) { && !usesSniper ()) {
auto error = kSprayDistance * m_lastEnemyOrigin.distance (pev->origin) / 2048.0f; auto error = kSprayDistance * m_lastEnemyOrigin.distance (pev->origin) / 2048.0f;
@ -624,7 +630,7 @@ void Bot::camp_ () {
m_checkTerrain = false; m_checkTerrain = false;
m_moveToGoal = false; m_moveToGoal = false;
if (m_team == Team::CT && bots.isBombPlanted () && m_defendedBomb && !isBombDefusing (graph.getBombOrigin ()) && !isOutOfBombTimer ()) { if (m_team == Team::CT && gameState.isBombPlanted () && m_defendedBomb && !isBombDefusing (gameState.getBombOrigin ()) && !isOutOfBombTimer ()) {
m_defendedBomb = false; m_defendedBomb = false;
completeTask (); completeTask ();
} }
@ -644,18 +650,23 @@ void Bot::camp_ () {
// random camp dir, or prediction // random camp dir, or prediction
auto useRandomCampDirOrPredictEnemy = [&] () { auto useRandomCampDirOrPredictEnemy = [&] () {
if (!m_lastEnemyOrigin.empty () && util.isAlive (m_lastEnemy)) { if (!m_lastEnemyOrigin.empty () && game.isAliveEntity (m_lastEnemy)) {
auto pathLength = m_lastPredictLength; auto pathLength = m_lastPredictLength;
auto predictNode = m_lastPredictIndex; auto predictNode = m_lastPredictIndex;
if (isNodeValidForPredict (predictNode) && pathLength > 1) { if (isNodeValidForPredict (predictNode)
&& pathLength > 1
&& vistab.visible (predictNode, m_currentNodeIndex)) {
m_lookAtSafe = graph[predictNode].origin + pev->view_ofs; m_lookAtSafe = graph[predictNode].origin + pev->view_ofs;
} }
else { else {
pathLength = 0; pathLength = 0;
predictNode = findAimingNode (m_lastEnemyOrigin, pathLength); predictNode = findAimingNode (m_lastEnemyOrigin, pathLength);
if (isNodeValidForPredict (predictNode) && pathLength > 1) { if (isNodeValidForPredict (predictNode) && pathLength > 1
&& vistab.visible (predictNode, m_currentNodeIndex)) {
m_lookAtSafe = graph[predictNode].origin + pev->view_ofs; m_lookAtSafe = graph[predictNode].origin + pev->view_ofs;
} }
} }
@ -838,7 +849,7 @@ void Bot::plantBomb_ () {
selectWeaponById (Weapon::C4); selectWeaponById (Weapon::C4);
} }
if (util.isAlive (m_enemy) || !m_inBombZone) { if (game.isAliveEntity (m_enemy) || !m_inBombZone) {
completeTask (); completeTask ();
} }
else { else {
@ -881,7 +892,7 @@ void Bot::plantBomb_ () {
void Bot::defuseBomb_ () { void Bot::defuseBomb_ () {
const float fullDefuseTime = m_hasDefuser ? 7.0f : 12.0f; const float fullDefuseTime = m_hasDefuser ? 7.0f : 12.0f;
const float timeToBlowUp = getBombTimeleft (); const float timeToBlowUp = gameState.getBombTimeLeft ();
float defuseRemainingTime = fullDefuseTime; float defuseRemainingTime = fullDefuseTime;
@ -889,7 +900,7 @@ void Bot::defuseBomb_ () {
defuseRemainingTime = fullDefuseTime - game.time (); defuseRemainingTime = fullDefuseTime - game.time ();
} }
const auto &bombPos = graph.getBombOrigin (); const auto &bombPos = gameState.getBombOrigin ();
bool defuseError = false; bool defuseError = false;
// exception: bomb has been defused // exception: bomb has been defused
@ -903,7 +914,7 @@ void Bot::defuseBomb_ () {
startTask (Task::MoveToPosition, TaskPri::MoveToPosition, defendPoint, game.time () + rg (3.0f, 6.0f), true); // push move command startTask (Task::MoveToPosition, TaskPri::MoveToPosition, defendPoint, game.time () + rg (3.0f, 6.0f), true); // push move command
} }
} }
graph.setBombOrigin (true); gameState.setBombOrigin (true);
if (m_numFriendsLeft != 0 && rg.chance (50)) { if (m_numFriendsLeft != 0 && rg.chance (50)) {
if (timeToBlowUp <= 3.0f) { if (timeToBlowUp <= 3.0f) {
@ -1059,7 +1070,7 @@ void Bot::defuseBomb_ () {
} }
void Bot::followUser_ () { void Bot::followUser_ () {
if (game.isNullEntity (m_targetEntity) || !util.isAlive (m_targetEntity)) { if (game.isNullEntity (m_targetEntity) || !game.isAliveEntity (m_targetEntity)) {
m_targetEntity = nullptr; m_targetEntity = nullptr;
completeTask (); completeTask ();
@ -1070,7 +1081,7 @@ void Bot::followUser_ () {
TraceResult tr {}; TraceResult tr {};
game.testLine (m_targetEntity->v.origin + m_targetEntity->v.view_ofs, m_targetEntity->v.v_angle.forward () * 500.0f, TraceIgnore::Everything, ent (), &tr); game.testLine (m_targetEntity->v.origin + m_targetEntity->v.view_ofs, m_targetEntity->v.v_angle.forward () * 500.0f, TraceIgnore::Everything, ent (), &tr);
if (!game.isNullEntity (tr.pHit) && util.isPlayer (tr.pHit) && game.getTeam (tr.pHit) != m_team) { if (!game.isNullEntity (tr.pHit) && game.isPlayerEntity (tr.pHit) && game.getPlayerTeam (tr.pHit) != m_team) {
m_targetEntity = nullptr; m_targetEntity = nullptr;
m_lastEnemy = tr.pHit; m_lastEnemy = tr.pHit;
m_lastEnemyOrigin = tr.pHit->v.origin; m_lastEnemyOrigin = tr.pHit->v.origin;
@ -1320,7 +1331,7 @@ void Bot::throwSmoke_ () {
} }
void Bot::doublejump_ () { void Bot::doublejump_ () {
if (!util.isAlive (m_doubleJumpEntity) if (!game.isAliveEntity (m_doubleJumpEntity)
|| (m_aimFlags & AimFlags::Enemy) || (m_aimFlags & AimFlags::Enemy)
|| (m_travelStartIndex != kInvalidNodeIndex || (m_travelStartIndex != kInvalidNodeIndex
&& getTask ()->time + (graph.calculateTravelTime (pev->maxspeed, graph[m_travelStartIndex].origin, m_doubleJumpOrigin) + 11.0f) < game.time ())) { && getTask ()->time + (graph.calculateTravelTime (pev->maxspeed, graph[m_travelStartIndex].origin, m_doubleJumpOrigin) + 11.0f) < game.time ())) {
@ -1394,7 +1405,7 @@ void Bot::doublejump_ () {
void Bot::escapeFromBomb_ () { void Bot::escapeFromBomb_ () {
m_aimFlags |= AimFlags::Nav; m_aimFlags |= AimFlags::Nav;
if (!bots.isBombPlanted ()) { if (!gameState.isBombPlanted ()) {
completeTask (); completeTask ();
} }
@ -1402,7 +1413,7 @@ void Bot::escapeFromBomb_ () {
pev->button |= IN_ATTACK2; pev->button |= IN_ATTACK2;
} }
if (!usesKnife () && game.isNullEntity (m_enemy) && !util.isAlive (m_lastEnemy)) { if (!usesKnife () && game.isNullEntity (m_enemy) && !game.isAliveEntity (m_lastEnemy)) {
selectWeaponById (Weapon::Knife); selectWeaponById (Weapon::Knife);
} }
@ -1427,7 +1438,7 @@ void Bot::escapeFromBomb_ () {
float nearestDistanceSq = kInfiniteDistance; float nearestDistanceSq = kInfiniteDistance;
for (const auto &path : graph) { for (const auto &path : graph) {
if (path.origin.distanceSq (graph.getBombOrigin ()) < cr::sqrf (safeRadius) || isOccupiedNode (path.number)) { if (path.origin.distanceSq (gameState.getBombOrigin ()) < cr::sqrf (safeRadius) || isOccupiedNode (path.number)) {
continue; continue;
} }
const float distanceSq = pev->origin.distanceSq (path.origin); const float distanceSq = pev->origin.distanceSq (path.origin);
@ -1458,15 +1469,16 @@ void Bot::escapeFromBomb_ () {
} }
void Bot::shootBreakable_ () { void Bot::shootBreakable_ () {
const bool hasEnemy = !game.isNullEntity (m_enemy);
// breakable destroyed? // breakable destroyed?
if (!util.isBreakableEntity (m_breakableEntity)) { if (hasEnemy || !game.isBreakableEntity (m_breakableEntity)) {
completeTask (); completeTask ();
return; return;
} }
else { else {
TraceResult tr {}; TraceResult tr {};
game.testLine (pev->origin, m_breakableOrigin, TraceIgnore::Monsters , ent (), &tr); game.testLine (pev->origin, m_breakableOrigin, TraceIgnore::Monsters, ent (), &tr);
if (tr.pHit != m_breakableEntity && !cr::fequal (tr.flFraction, 1.0f)) { if (tr.pHit != m_breakableEntity && !cr::fequal (tr.flFraction, 1.0f)) {
m_ignoredBreakable.push (tr.pHit); m_ignoredBreakable.push (tr.pHit);
@ -1642,7 +1654,7 @@ void Bot::pickupItem_ () {
case Pickup::Hostage: case Pickup::Hostage:
m_aimFlags |= AimFlags::Entity; m_aimFlags |= AimFlags::Entity;
if (!util.isAlive (m_pickupItem)) { if (!game.isAliveEntity (m_pickupItem)) {
// don't pickup dead hostages // don't pickup dead hostages
m_pickupItem = nullptr; m_pickupItem = nullptr;
completeTask (); completeTask ();
@ -1671,7 +1683,7 @@ void Bot::pickupItem_ () {
// find the nearest 'unused' hostage within the area // find the nearest 'unused' hostage within the area
game.searchEntities (pev->origin, 1024.0f, [&] (edict_t *ent) { game.searchEntities (pev->origin, 1024.0f, [&] (edict_t *ent) {
if (!util.isHostageEntity (ent)) { if (!game.isHostageEntity (ent)) {
return EntitySearchResult::Continue; return EntitySearchResult::Continue;
} }

View file

@ -230,7 +230,11 @@ void Bot::updateLookAngles () {
float angleDiffYaw = cr::anglesDifference (direction.y, m_idealAngles.y); float angleDiffYaw = cr::anglesDifference (direction.y, m_idealAngles.y);
// prevent reverse facing angles when navigating normally // prevent reverse facing angles when navigating normally
if (m_moveToGoal && !importantAimFlags && !m_pathOrigin.empty ()) { if (m_moveToGoal
&& !importantAimFlags
&& !m_pathOrigin.empty ()
&& !isOnLadder ()) {
const float forward = (m_lookAt - pev->origin).yaw (); const float forward = (m_lookAt - pev->origin).yaw ();
if (!cr::fzero (forward)) { if (!cr::fzero (forward)) {
@ -274,8 +278,8 @@ void Bot::updateLookAngles () {
} }
void Bot::updateLookAnglesNewbie (const Vector &direction, float delta) { void Bot::updateLookAnglesNewbie (const Vector &direction, float delta) {
Vector spring { 13.0f, 13.0f, 0.0f }; Vector spring { 13.0f, 9.0f, 0.0f };
Vector damperCoefficient { 0.22f, 0.22f, 0.0f }; Vector damperCoefficient { 0.28f, 0.26f, 0.0f };
const float offset = cr::clamp (static_cast <float> (m_difficulty), 1.0f, 4.0f) * 25.0f; const float offset = cr::clamp (static_cast <float> (m_difficulty), 1.0f, 4.0f) * 25.0f;
@ -292,10 +296,14 @@ void Bot::updateLookAnglesNewbie (const Vector &direction, float delta) {
if (m_aimFlags & (AimFlags::Enemy | AimFlags::Entity)) { if (m_aimFlags & (AimFlags::Enemy | AimFlags::Entity)) {
m_playerTargetTime = game.time (); m_playerTargetTime = game.time ();
m_randomizedIdealAngles = m_idealAngles;
stiffness = spring * (0.2f + offset / 125.0f); // bias aim slightly below exact target (chest > head)
m_randomizedIdealAngles = m_idealAngles;
m_randomizedIdealAngles.x += rg(2.0f, 6.0f); // pitch down
stiffness = spring * (0.18f + offset / 140.0f);
} }
else { else {
// is it time for bot to randomize the aim direction again (more often where moving) ? // is it time for bot to randomize the aim direction again (more often where moving) ?
if (m_randomizeAnglesTime < game.time () if (m_randomizeAnglesTime < game.time ()
@ -436,17 +444,17 @@ void Bot::setAimDirection () {
|| (m_currentTravelFlags & PathFlag::Jump)) { || (m_currentTravelFlags & PathFlag::Jump)) {
flags &= ~(AimFlags::LastEnemy | AimFlags::PredictPath); flags &= ~(AimFlags::LastEnemy | AimFlags::PredictPath);
m_canChooseAimDirection = false; m_canSetAimDirection = false;
} }
// don't switch view right away after loosing focus with current enemy // don't switch view right away after loosing focus with current enemy
if ((m_shootTime + rg (0.75f, 1.25f) > game.time () if ((m_shootTime + rg (0.75f, 1.25f) > game.time ()
|| m_seeEnemyTime + 1.5f > game.time ()) || m_seeEnemyTime + rg (1.0f, 1.25f) > game.time ())
&& m_forgetLastVictimTimer.elapsed () && m_forgetLastVictimTimer.elapsed ()
&& !m_lastEnemyOrigin.empty () && !m_lastEnemyOrigin.empty ()
&& util.isAlive (m_lastEnemy) && game.isPlayerEntity (m_lastEnemy)
&& game.isNullEntity (m_enemy)) { && !game.isPlayerEntity (m_enemy)) {
flags |= AimFlags::LastEnemy; flags |= AimFlags::LastEnemy;
} }
@ -503,7 +511,7 @@ void Bot::setAimDirection () {
else if (flags & AimFlags::PredictPath) { else if (flags & AimFlags::PredictPath) {
bool changePredictedEnemy = true; bool changePredictedEnemy = true;
if (m_timeNextTracking < game.time () && m_trackingEdict == m_lastEnemy && util.isAlive (m_lastEnemy)) { if (m_timeNextTracking < game.time () && m_trackingEdict == m_lastEnemy && game.isAliveEntity (m_lastEnemy)) {
changePredictedEnemy = false; changePredictedEnemy = false;
} }
@ -532,7 +540,8 @@ void Bot::setAimDirection () {
} }
const float distToPredictNodeSq = graph[predictNode].origin.distanceSq (pev->origin); const float distToPredictNodeSq = graph[predictNode].origin.distanceSq (pev->origin);
if (distToPredictNodeSq >= cr::sqrf (2048.0f)) { if (distToPredictNodeSq >= cr::sqrf (2048.0f) ||
distToPredictNodeSq <= cr::sqrf (256.0f)) {
return false; return false;
} }
@ -542,7 +551,8 @@ void Bot::setAimDirection () {
return false; return false;
} }
return isNodeValidForPredict (predictNode) && pathLength < cv_max_nodes_for_predict.as <int> (); return isNodeValidForPredict (predictNode) && pathLength < cv_max_nodes_for_predict.as <int> ()
&& numEnemiesNear (graph[predictNode].origin, 1024.0f) > 0;
}; };
if (changePredictedEnemy) { if (changePredictedEnemy) {
@ -573,46 +583,21 @@ void Bot::setAimDirection () {
const auto &destOrigin = m_destOrigin + pev->view_ofs; const auto &destOrigin = m_destOrigin + pev->view_ofs;
m_lookAt = destOrigin; m_lookAt = destOrigin;
if (m_moveToGoal && m_seeEnemyTime + 4.0f < game.time () const bool verticalMove = (m_pathFlags & NodeFlag::Ladder) || isOnLadder () || isPreviousLadder () || pev->velocity.z > 16.0f;
&& !m_isStuck && !(pev->button & IN_DUCK)
&& m_currentNodeIndex != kInvalidNodeIndex
&& !(m_pathFlags & (NodeFlag::Ladder | NodeFlag::Crouch))
&& m_pathWalk.hasNext () && !isOnLadder ()
&& pev->origin.distanceSq (destOrigin) < cr::sqrf (512.0f)) {
const auto nextPathIndex = m_pathWalk.next ();
const auto nextPathX2 = m_pathWalk.nextX2 ();
if (vistab.visible (m_currentNodeIndex, nextPathX2)) {
const auto &gn = graph[nextPathX2];
m_lookAt = gn.origin + pev->view_ofs;
}
else if (vistab.visible (m_currentNodeIndex, nextPathIndex)) {
const auto &gn = graph[nextPathIndex];
m_lookAt = gn.origin + pev->view_ofs;
}
else {
m_lookAt = pev->origin + pev->view_ofs + pev->v_angle.forward () * 300.0f;
}
}
else {
m_lookAt = destOrigin;
}
const bool horizontalMovement = (m_pathFlags & NodeFlag::Ladder) || isOnLadder ();
if (m_numEnemiesLeft > 0 if (m_numEnemiesLeft > 0
&& m_canChooseAimDirection && m_canSetAimDirection
&& m_seeEnemyTime + 4.0f < game.time () && m_seeEnemyTime + 4.0f < game.time ()
&& m_currentNodeIndex != kInvalidNodeIndex && graph.exists (m_currentNodeIndex)
&& !horizontalMovement) { && !(m_aimFlags & AimFlags::PredictPath)) {
const auto dangerIndex = practice.getIndex (m_team, m_currentNodeIndex, m_currentNodeIndex); const auto dangerIndex = practice.getIndex (m_team, m_currentNodeIndex, m_currentNodeIndex);
if (graph.exists (dangerIndex) if (graph.exists (dangerIndex)
&& vistab.visible (m_currentNodeIndex, dangerIndex) && vistab.visibleBothSides (m_currentNodeIndex, dangerIndex)
&& !(graph[dangerIndex].flags & NodeFlag::Crouch)) { && !(graph[dangerIndex].flags & NodeFlag::Crouch)) {
if (pev->origin.distanceSq (graph[dangerIndex].origin) < cr::sqrf (512.0f)) { if (pev->origin.distanceSq (graph[dangerIndex].origin) < cr::sqrf (240.0f)) {
m_lookAt = destOrigin; m_lookAt = destOrigin;
} }
else { else {
@ -623,13 +608,48 @@ void Bot::setAimDirection () {
} }
} }
} }
else if (!verticalMove
&& m_moveToGoal
&& m_canSetAimDirection
&& isOnFloor ()
&& !isDucking ()
&& graph.exists (m_currentNodeIndex)
&& m_pathWalk.hasNext ()
&& pev->origin.distanceSq (destOrigin) < cr::sqrf (384.0f)
&& m_path->radius >= 16.0f
&& m_path->flags == 0 && graph[m_pathWalk.next ()].flags == 0) {
const auto nextPathIndex = m_pathWalk.next ();
const auto isNarrowPlace = isInNarrowPlace ();
if (graph.exists (nextPathIndex)
&& cr::abs (graph[nextPathIndex].origin.z - m_pathOrigin.z) < 8.0f) {
if (m_pathWalk.length () > 2 && !isNarrowPlace) {
const auto nextPathIndexX2 = m_pathWalk.nextX2 ();
if (vistab.visibleBothSides (m_currentNodeIndex, nextPathIndexX2)) {
m_lookAt = graph[nextPathIndexX2].origin + pev->view_ofs;
}
}
else if (!isNarrowPlace && vistab.visibleBothSides (m_currentNodeIndex, nextPathIndex)) {
m_lookAt = graph[nextPathIndex].origin + pev->view_ofs;
}
else {
m_lookAt = destOrigin;
}
}
else {
m_lookAt = destOrigin;
}
}
// try look at next node if on ladder // try look at next node if on ladder
if (horizontalMovement && m_pathWalk.hasNext ()) { if (verticalMove && m_pathWalk.hasNext ()) {
const auto &nextPath = graph[m_pathWalk.next ()]; const auto &nextPath = graph[m_pathWalk.next ()];
if ((nextPath.flags & NodeFlag::Ladder) if ((nextPath.flags & NodeFlag::Ladder)
&& m_destOrigin.distanceSq (pev->origin) < cr::sqrf (128.0f) && m_destOrigin.distanceSq (pev->origin) < cr::sqrf (96.0f)
&& nextPath.origin.z > m_pathOrigin.z + 26.0f) { && nextPath.origin.z > m_pathOrigin.z + 26.0f) {
m_lookAt = nextPath.origin + pev->view_ofs; m_lookAt = nextPath.origin + pev->view_ofs;
@ -637,7 +657,7 @@ void Bot::setAimDirection () {
} }
// don't look at bottom of node, if reached it // don't look at bottom of node, if reached it
if (m_lookAt == destOrigin && !horizontalMovement) { if (m_lookAt == destOrigin && !verticalMove) {
m_lookAt.z = getEyesPos ().z; m_lookAt.z = getEyesPos ().z;
} }
@ -654,3 +674,4 @@ void Bot::setAimDirection () {
m_lookAt = m_destOrigin; m_lookAt = m_destOrigin;
} }
} }

View file

@ -125,7 +125,9 @@ void GraphVistable::rebuild () {
m_sliceIndex += rg (250, 400); m_sliceIndex += rg (250, 400);
} }
auto notifyProgress = [] (int value) { auto notifyProgress = [] (int value) {
if (value >= 100 || cv_debug) {
game.print ("Rebuilding vistable... %d%% done.", value); game.print ("Rebuilding vistable... %d%% done.", value);
}
}; };
// notify host about rebuilding // notify host about rebuilding
@ -139,6 +141,7 @@ void GraphVistable::rebuild () {
m_rebuild = false; m_rebuild = false;
m_notifyMsgTimestamp = 0.0f; m_notifyMsgTimestamp = 0.0f;
m_curIndex = 0;
save (); save ();
} }
@ -149,7 +152,7 @@ void GraphVistable::startRebuild () {
m_notifyMsgTimestamp = game.time (); m_notifyMsgTimestamp = game.time ();
} }
bool GraphVistable::visible (int srcIndex, int destIndex, VisIndex vis) { bool GraphVistable::visible (int srcIndex, int destIndex, VisIndex vis) const {
if (!graph.exists (srcIndex) || !graph.exists (destIndex)) { if (!graph.exists (srcIndex) || !graph.exists (destIndex)) {
return false; return false;
} }