From 6604145481dc805d4c95fafbeea3712abad504b2 Mon Sep 17 00:00:00 2001 From: jeefo Date: Wed, 10 Sep 2025 15:32:40 +0300 Subject: [PATCH 01/10] 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) --- inc/yapb.h | 2 +- src/analyze.cpp | 4 ++++ src/botlib.cpp | 22 ++++++++++++++++++---- src/combat.cpp | 29 ++++++++++++++++++++--------- src/tasks.cpp | 9 +++++++-- src/vision.cpp | 6 +++--- 6 files changed, 53 insertions(+), 19 deletions(-) diff --git a/inc/yapb.h b/inc/yapb.h index 23c109d..99aa05d 100644 --- a/inc/yapb.h +++ b/inc/yapb.h @@ -520,7 +520,7 @@ private: void setPathOrigin (); void fireWeapons (); 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 selectBestWeapon (); void selectSecondary (); diff --git a/src/analyze.cpp b/src/analyze.cpp index 90c0ae9..361eaf5 100644 --- a/src/analyze.cpp +++ b/src/analyze.cpp @@ -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."); void GraphAnalyze::start () { + if (m_isAnalyzing) { + return; + } + // start analyzer in few seconds after level initialized if (cv_graph_analyze_auto_start) { m_updateInterval = game.time () + 3.0f; diff --git a/src/botlib.cpp b/src/botlib.cpp index 0b38c0f..6010c52 100644 --- a/src/botlib.cpp +++ b/src/botlib.cpp @@ -881,7 +881,9 @@ void Bot::showChatterIcon (bool show, bool disconnect) const { // do not respect timers while disconnecting bot 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; } @@ -891,7 +893,7 @@ void Bot::showChatterIcon (bool show, bool disconnect) const { } // 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 ()); client.iconTimestamp[ownIndex] = 0.0f; @@ -1828,7 +1830,9 @@ void Bot::syncUpdatePredictedIndex () { 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; return false; } @@ -3283,7 +3287,15 @@ void Bot::checkSpawnConditions () { dropCurrentWeapon (); } else { - selectWeaponById (Weapon::Knife); + bool switchToKnife = true; + + if (m_currentWeapon == Weapon::Scout) { + switchToKnife = rg.chance (25); + } + + if (switchToKnife) { + selectWeaponById (Weapon::Knife); + } } } m_checkKnifeSwitch = false; @@ -3474,6 +3486,8 @@ void Bot::spawned () { if (game.is (GameFlags::CSDM | GameFlags::ZombieMod)) { newRound (); clearTasks (); + + m_buyingFinished = true; } } diff --git a/src/combat.cpp b/src/combat.cpp index 96f9c90..fa355d4 100644 --- a/src/combat.cpp +++ b/src/combat.cpp @@ -710,6 +710,11 @@ Vector Bot::getEnemyBodyOffset () { 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 (!m_enemyParts && (m_states & Sense::SuspectEnemy)) { spot += getBodyOffsetError (distance); @@ -728,11 +733,13 @@ Vector Bot::getEnemyBodyOffset () { } // now check is our skill match to aim at head, else aim at enemy body - if (m_enemyBodyPartSet == m_enemy || rg.chance (headshotPct)) { - spot = m_enemyOrigin + getCustomHeight (distance); + if (m_enemyBodyPartSet == m_enemy + || ((m_enemyBodyPartSet != m_enemy) && rg.chance (headshotPct))) { + + spot = headOrigin (m_enemy, distance); 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 @@ -741,6 +748,10 @@ Vector Bot::getEnemyBodyOffset () { } else { spot = m_enemy->v.origin; + + if (m_difficulty == Difficulty::Expert) { + spot.z += pev->view_ofs.z * 0.35f; + } } } else if (m_enemyParts & Visibility::Body) { @@ -750,10 +761,10 @@ Vector Bot::getEnemyBodyOffset () { spot = m_enemyOrigin; } 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)) { spot = idealSpot + ((spot - idealSpot) * 0.005f); // gradually adjust the aiming direction @@ -1079,7 +1090,7 @@ bool Bot::checkZoom (float distance) { 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 (); // we want to fire weapon, don't reload now @@ -1249,7 +1260,7 @@ void Bot::fireWeapons () { // if knife mode use knife only if (isKnifeMode ()) { - selectWeapons (distance, selectIndex, selectId, choosenWeapon); + handleWeapons (distance, selectIndex, selectId, choosenWeapon); return; } @@ -1263,7 +1274,7 @@ void Bot::fireWeapons () { && !isGroupOfEnemies (pev->origin) && getCurrentTaskId () != Task::Camp) { - selectWeapons (distance, selectIndex, selectId, choosenWeapon); + handleWeapons (distance, selectIndex, selectId, choosenWeapon); return; } @@ -1316,7 +1327,7 @@ void Bot::fireWeapons () { } 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) { diff --git a/src/tasks.cpp b/src/tasks.cpp index 6333930..5dbac9f 100644 --- a/src/tasks.cpp +++ b/src/tasks.cpp @@ -648,14 +648,19 @@ void Bot::camp_ () { auto pathLength = m_lastPredictLength; 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; } else { pathLength = 0; 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; } } diff --git a/src/vision.cpp b/src/vision.cpp index c1b9b6f..d016392 100644 --- a/src/vision.cpp +++ b/src/vision.cpp @@ -441,12 +441,12 @@ void Bot::setAimDirection () { // don't switch view right away after loosing focus with current enemy 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_lastEnemyOrigin.empty () - && util.isAlive (m_lastEnemy) - && game.isNullEntity (m_enemy)) { + && util.isPlayer (m_lastEnemy) + && !util.isPlayer (m_enemy)) { flags |= AimFlags::LastEnemy; } From 7b378ba3faeaba443b88e247d4a5e858d6c51738 Mon Sep 17 00:00:00 2001 From: jeefo Date: Wed, 8 Oct 2025 20:12:46 +0300 Subject: [PATCH 02/10] nav: various fixes to movement code refactor: move some things into new game state class --- inc/constant.h | 2 +- inc/engine.h | 136 +++++++++++++++++++++- inc/graph.h | 6 - inc/manager.h | 65 +---------- inc/practice.h | 2 +- inc/support.h | 37 +----- inc/vistable.h | 2 +- inc/yapb.h | 2 +- src/analyze.cpp | 2 +- src/botlib.cpp | 158 +++++++++++++------------- src/chatlib.cpp | 8 +- src/combat.cpp | 38 +++---- src/config.cpp | 16 ++- src/control.cpp | 16 +-- src/engine.cpp | 287 ++++++++++++++++++++++++++++++++++++++++++++--- src/entities.cpp | 2 +- src/fakeping.cpp | 6 +- src/graph.cpp | 47 +------- src/hooks.cpp | 2 +- src/linkage.cpp | 16 +-- src/manager.cpp | 260 +++++------------------------------------- src/message.cpp | 14 +-- src/navigate.cpp | 83 +++++++------- src/planner.cpp | 10 +- src/practice.cpp | 2 +- src/support.cpp | 238 +++++++++++++++++++-------------------- src/tasks.cpp | 64 ++++++----- src/vision.cpp | 22 ++-- src/vistable.cpp | 7 +- 29 files changed, 805 insertions(+), 745 deletions(-) diff --git a/inc/constant.h b/inc/constant.h index cdcdc7f..ebc8794 100644 --- a/inc/constant.h +++ b/inc/constant.h @@ -447,7 +447,7 @@ constexpr auto kSprayDistanceX2 = kSprayDistance * 2; constexpr auto kMaxChatterRepeatInterval = 99.0f; constexpr auto kViewFrameUpdate = 1.0f / 25.0f; constexpr auto kGrenadeDamageRadius = 385.0f; -constexpr auto kMinMovedDistance = 3.0f; +constexpr auto kMinMovedDistance = 2.5f; constexpr auto kInfiniteDistanceLong = static_cast (kInfiniteDistance); constexpr auto kMaxWeapons = 32; diff --git a/inc/engine.h b/inc/engine.h index d7d8d2c..60d67ef 100644 --- a/inc/engine.h +++ b/inc/engine.h @@ -265,7 +265,7 @@ public: void searchEntities (const Vector &position, float radius, EntitySearch functor) const; // check if map has entity - bool hasEntityInGame (StringRef classname); + bool hasEntityInGame (StringRef classname) const; // print the version to server console on startup void printBotVersion () const; @@ -282,6 +282,38 @@ public: // is developer mode ? 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: // get the current time on server @@ -343,7 +375,7 @@ public: } // get the wroldspawn entity - edict_t *getStartEntity () { + edict_t *getStartEntity () const { return m_startEntity; } @@ -353,7 +385,7 @@ public: } // gets the player team - int getTeam (edict_t *ent) const { + int getPlayerTeam (edict_t *ent) const { if (isNullEntity (ent)) { return Team::Unassigned; } @@ -361,7 +393,7 @@ public: } // gets the player team (real in ffa) - int getRealTeam (edict_t *ent) const { + int getRealPlayerTeam (edict_t *ent) const { if (isNullEntity (ent)) { return Team::Unassigned; } @@ -369,8 +401,8 @@ public: } // get real gamedll team (matches gamedll indices) - int getGameTeam (edict_t *ent) const { - return getRealTeam (ent) + 1; + int getPlayerTeamGame (edict_t *ent) const { + return getRealPlayerTeam (ent) + 1; } // sets the precache to uninitialized @@ -723,6 +755,98 @@ public: } }; +// offload bot manager class from things it shouldn't do +class GameState final : public Singleton { +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 m_activeGrenades {}; // holds currently active grenades on the map + Array 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 &getActiveGrenades () { + return m_activeGrenades; + } + + const Array &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 CR_EXPOSE_GLOBAL_SINGLETON (Game, game); +CR_EXPOSE_GLOBAL_SINGLETON (GameState, gameState); CR_EXPOSE_GLOBAL_SINGLETON (LightMeasure, illum); diff --git a/inc/graph.h b/inc/graph.h index 11f9e5e..d3e5518 100644 --- a/inc/graph.h +++ b/inc/graph.h @@ -173,7 +173,6 @@ private: Vector m_learnVelocity {}; Vector m_learnPosition {}; - Vector m_bombOrigin {}; Vector m_lastNode {}; IntArray m_terrorPoints {}; @@ -253,7 +252,6 @@ public: void clearVisited (); void eraseFromBucket (const Vector &pos, int index); - void setBombOrigin (bool reset = false, const Vector &pos = nullptr); void unassignPath (int from, int to); void convertFromPOD (Path &path, const PODPath &pod) const; void convertToPOD (const Path &path, PODPath &pod); @@ -292,10 +290,6 @@ public: m_editFlags &= ~flag; } - const Vector &getBombOrigin () const { - return m_bombOrigin; - } - // access paths Path &operator [] (int index) { return m_paths[index]; diff --git a/inc/manager.h b/inc/manager.h index c2748f6..579d006 100644 --- a/inc/manager.h +++ b/inc/manager.h @@ -24,32 +24,19 @@ public: using UniqueBot = UniquePtr ; 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_autoKillCheckTime {}; // time to kill all the bots ? float m_maintainTime {}; // time to maintain bot creation float m_quotaMaintainTime {}; // time to maintain bot quota - float m_grenadeUpdateTime {}; // time to update active grenades - float m_entityUpdateTime {}; // time to update interesting entities float m_plantSearchUpdateTime {}; // time to update for searching planted bomb float m_lastChatTime {}; // global chat time timestamp - float m_timeBombPlanted {}; // time the bomb were planted int m_lastWinner {}; // the team who won previous round int m_lastDifficulty {}; // last bots difficulty int m_bombSayStatus {}; // some bot is issued whine about bomb 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_roundOver {}; // well, round is over> - bool m_resetHud {}; // reset HUD is called for some one - - Array m_activeGrenades {}; // holds currently active grenades on the map - Array m_interestingEntities {}; // holds currently interesting entities on the map Deque m_saveBotNames {}; // bots names that persist upon changelevel Deque m_addRequests {}; // bot creation tab @@ -81,10 +68,9 @@ public: int getAliveHumansCount (); int getPlayerPriority (edict_t *ent); - float getConnectTime (StringRef name, float original); + float getConnectionTimes (StringRef name, float original); float getAverageTeamKPD (bool calcForBots); - void setBombPlanted (bool isPlanted); void frame (); void createKillerEntity (); void destroyKillerEntity (); @@ -113,8 +99,6 @@ public: void reset (); void initFilters (); void resetFilters (); - void updateActiveGrenade (); - void updateInterestingEntities (); void captureChatRadio (StringRef cmd, StringRef arg, edict_t *ent); void notifyBombDefuse (); void execGameEntity (edict_t *ent); @@ -130,27 +114,10 @@ public: bool kickRandom (bool decQuota = true, Team fromTeam = Team::Unassigned); bool balancedKickRandom (bool decQuota); bool hasCustomCSDMSpawnEntities (); - bool isLineBlockedBySmoke (const Vector &from, const Vector &to); bool isFrameSkipDisabled (); public: - const Array &getActiveGrenades () { - return m_activeGrenades; - } - - const Array &getInterestingEntities () { - return m_interestingEntities; - } - - bool hasActiveGrenades () const { - return !m_activeGrenades.empty (); - } - - bool hasInterestingEntities () const { - return !m_interestingEntities.empty (); - } - - bool checkTeamEco (int team) const { + bool getTeamEconomics (int team) const { return m_teamData[team].positiveEco; } @@ -171,30 +138,6 @@ public: 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 { return m_botsCanPause; } @@ -236,10 +179,6 @@ public: m_teamData[team].lastRadioSlot = radio; } - void setResetHUD (bool resetHud) { - m_resetHud = resetHud; - } - int getLastRadio (const int team) const { return m_teamData[team].lastRadioSlot; } diff --git a/inc/practice.h b/inc/practice.h index fff0bc1..aeb2f45 100644 --- a/inc/practice.h +++ b/inc/practice.h @@ -108,7 +108,7 @@ public: void setDamage (int32_t team, int32_t start, int32_t goal, int32_t value); // interlocked get damage - float plannerGetDamage (int32_t team, int32_t start, int32_t goal, bool addTeamHighestDamage); + float getDamageEx (int32_t team, int32_t start, int32_t goal, bool addTeamHighestDamage); public: void update (); diff --git a/inc/support.h b/inc/support.h index 965c754..0f6b792 100644 --- a/inc/support.h +++ b/inc/support.h @@ -31,36 +31,6 @@ public: // converts weapon id to alias name 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 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 void updateClients (); - // checks if same model omitting the models directory - bool isModel (const edict_t *ent, StringRef model); + // check if origin is visible from the entity side + bool isVisible (const Vector &origin, edict_t *ent); // get the current date and time as string String getCurrentDateTime (); @@ -85,6 +55,9 @@ public: // set custom cvar descriptions void setCustomCvarDescriptions (); + // check if line of sight blocked by a smoke + bool isLineBlockedBySmoke (const Vector &from, const Vector &to); + public: // re-show welcome after changelevel ? diff --git a/inc/vistable.h b/inc/vistable.h index bcc3a84..9f5c288 100644 --- a/inc/vistable.h +++ b/inc/vistable.h @@ -39,7 +39,7 @@ public: ~GraphVistable () = default; 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 save () const; diff --git a/inc/yapb.h b/inc/yapb.h index 99aa05d..8a68bb7 100644 --- a/inc/yapb.h +++ b/inc/yapb.h @@ -400,7 +400,7 @@ private: int numEnemiesNear (const Vector &origin, const float radius) const; int numFriendsNear (const Vector &origin, const float radius) const; - float getBombTimeleft () const; + float getEstimatedNodeReachTime (); float isInFOV (const Vector &dest) const; float getShiftSpeed (); diff --git a/src/analyze.cpp b/src/analyze.cpp index 361eaf5..c366d7d 100644 --- a/src/analyze.cpp +++ b/src/analyze.cpp @@ -311,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); // 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; } diff --git a/src/botlib.cpp b/src/botlib.cpp index 6010c52..1a36839 100644 --- a/src/botlib.cpp +++ b/src/botlib.cpp @@ -37,7 +37,7 @@ 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_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_smoke_grenade_checks ("smoke_grenade_checks", "2", "Affects the bot's vision by smoke clouds.", true, 0.0f, 2.0f); +ConVar cv_smoke_grenade_checks ("smoke_grenade_checks", "1", "Affects the bot's vision by smoke clouds.", true, 0.0f, 2.0f); // game console variables ConVar mp_c4timer ("mp_c4timer", nullptr, Var::GameRef); @@ -78,10 +78,10 @@ void Bot::avoidGrenades () { m_needAvoidGrenade = 0; } - if (!bots.hasActiveGrenades ()) { + if (!gameState.hasActiveGrenades ()) { return; } - const auto &activeGrenades = bots.getActiveGrenades (); + const auto &activeGrenades = gameState.getActiveGrenades (); // find all grenades on the map for (const auto &pent : activeGrenades) { @@ -105,7 +105,7 @@ void Bot::avoidGrenades () { } } 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; } @@ -209,7 +209,7 @@ void Bot::checkBreakablesAround () { continue; } - if (!util.isBreakableEntity (breakable)) { + if (!game.isBreakableEntity (breakable)) { continue; } @@ -257,7 +257,7 @@ edict_t *Bot::lookupBreakable () { // this function checks if bot is blocked by a shoot able breakable in his moving direction // we're got something already - if (util.isBreakableEntity (m_breakableEntity)) { + if (game.isBreakableEntity (m_breakableEntity)) { return m_breakableEntity; } const float detectBreakableDistance = (usesKnife () || isOnLadder ()) ? 32.0f : rg (72.0f, 256.0f); @@ -270,7 +270,7 @@ edict_t *Bot::lookupBreakable () { auto hit = tr.pHit; // 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_breakableEntity = hit; @@ -286,7 +286,7 @@ edict_t *Bot::lookupBreakable () { } // 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; } @@ -362,7 +362,7 @@ void Bot::updatePickups () { } // no interesting entities, how ? - else if (!bots.hasInterestingEntities ()) { + else if (!gameState.hasInterestingEntities ()) { return true; } return false; @@ -376,7 +376,7 @@ void Bot::updatePickups () { return; } - const auto &interesting = bots.getInterestingEntities (); + const auto &interesting = gameState.getInterestingEntities (); const float radiusSq = cr::sqrf (cv_object_pickup_radius.as ()); if (!game.isNullEntity (m_pickupItem)) { @@ -446,7 +446,7 @@ void Bot::updatePickups () { const bool isHostageRescueMap = game.mapIs (MapFlags::HostageRescue); const bool isCSDM = game.is (GameFlags::CSDM); - if (isHostageRescueMap && util.isHostageEntity (ent)) { + if (isHostageRescueMap && game.isHostageEntity (ent)) { allowPickup = true; pickupType = Pickup::Hostage; } @@ -551,7 +551,7 @@ void Bot::updatePickups () { allowPickup = true; 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; pickupType = Pickup::Items; } @@ -632,7 +632,7 @@ void Bot::updatePickups () { const auto &path = graph[index]; const float bombTimer = mp_c4timer.as (); - 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 ()) { clearTask (Task::MoveToPosition); // remove any move tasks @@ -683,7 +683,7 @@ void Bot::updatePickups () { } } else if (pickupType == Pickup::PlantedC4) { - if (util.isAlive (m_enemy)) { + if (game.isAliveEntity (m_enemy)) { return; } @@ -706,7 +706,7 @@ void Bot::updatePickups () { const int index = findDefendNode (origin); const auto &path = graph[index]; - const float timeToExplode = bots.getTimeBombPlanted () + mp_c4timer.as () - graph.calculateTravelTime (pev->maxspeed, pev->origin, path.origin); + const float timeToExplode = gameState.getTimeBombPlanted () + mp_c4timer.as () - graph.calculateTravelTime (pev->maxspeed, pev->origin, path.origin); clearTask (Task::MoveToPosition); // remove any move tasks @@ -1294,7 +1294,7 @@ void Bot::buyStuff () { const auto tab = conf.getRawWeapons (); 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 const bool isOldGame = game.is (GameFlags::Legacy); @@ -1696,7 +1696,7 @@ void Bot::overrideConditions () { // check if we need to escape from bomb if ((tid == Task::Normal || tid == Task::MoveToPosition) && game.mapIs (MapFlags::Demolition) - && bots.isBombPlanted () + && gameState.isBombPlanted () && m_isAlive && tid != Task::EscapeFromBomb && tid != Task::Camp @@ -1710,7 +1710,7 @@ void Bot::overrideConditions () { float reachEnemyKnifeDistanceSq = cr::sqrf (128.0f); // 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 nearestToEnemyPoint = graph.getNearest (m_enemy->v.origin); @@ -1760,7 +1760,7 @@ void Bot::overrideConditions () { } // special handling for reloading - if (!bots.isRoundOver () + if (!gameState.isRoundOver () && tid == Task::Normal && m_reloadState != Reload::None && m_isReloading @@ -1787,7 +1787,7 @@ void Bot::overrideConditions () { if (game.is (GameFlags::ZombieMod) && !m_isCreature && m_infectedEnemyTeam - && util.isAlive (m_enemy) + && game.isAliveEntity (m_enemy) && m_retreatTime < game.time () && pev->origin.distanceSq2d (m_enemy->v.origin) < cr::sqrf (512.0f)) { @@ -1811,7 +1811,7 @@ void Bot::syncUpdatePredictedIndex () { const auto &lastEnemyOrigin = m_lastEnemyOrigin; const auto currentNodeIndex = m_currentNodeIndex; - if (lastEnemyOrigin.empty () || !vistab.isReady () || !util.isAlive (m_lastEnemy)) { + if (lastEnemyOrigin.empty () || !vistab.isReady () || !game.isAliveEntity (m_lastEnemy)) { wipePredict (); return; } @@ -1849,7 +1849,7 @@ void Bot::syncUpdatePredictedIndex () { } 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 } @@ -1904,7 +1904,7 @@ void Bot::setConditions () { // did bot just kill an enemy? 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 m_agressionLevel += 0.1f; @@ -1969,7 +1969,7 @@ void Bot::setConditions () { } // 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); m_plantedBombNodeIndex = getNearestToPlantedBomb (); @@ -1990,7 +1990,7 @@ void Bot::setConditions () { // check if our current enemy is still valid 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; } } @@ -2089,7 +2089,7 @@ void Bot::filterTasks () { float &blindedDesire = filter[Task::Blind].desire; // 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 if (m_isCreature || @@ -2117,7 +2117,7 @@ void Bot::filterTasks () { if (m_isCreature) { 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 } else if (m_isVIP || m_isReloading || (sniping && usesSniper ())) { @@ -2139,7 +2139,7 @@ void Bot::filterTasks () { if (getCurrentTaskId () != Task::EscapeFromBomb && game.isNullEntity (m_enemy) && !m_isVIP - && bots.getRoundMidTime () < game.time () + && gameState.getRoundMidTime () < game.time () && !m_hasHostage && !m_isUsingGrenade && m_currentNodeIndex != graph.getNearest (m_lastEnemyOrigin) @@ -2256,10 +2256,11 @@ void Bot::startTask (Task id, float desire, int data, float time, bool resume) { } return; } + else { + clearSearchNodes (); + } } m_tasks.emplace (filter[id].func, id, desire, data, time, resume); - - clearSearchNodes (); ignoreCollision (); const auto tid = getCurrentTaskId (); @@ -2438,7 +2439,7 @@ void Bot::handleChatterTaskChange (Task tid) { } if (rg.chance (25) && tid == Task::Camp) { - if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted ()) { + if (game.mapIs (MapFlags::Demolition) && gameState.isBombPlanted ()) { pushChatterMessage (Chatter::GuardingPlantedC4); } else { @@ -2471,12 +2472,12 @@ void Bot::executeChatterFrameEvents () { if (!hasFriendNearby && rg.chance (45) && (m_enemy->v.weapons & cr::bit (Weapon::C4))) { 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); } else if (!hasFriendNearby && rg.chance (50) - && game.getTeam (m_enemy) != m_team + && game.getPlayerTeam (m_enemy) != m_team && isGroupOfEnemies (m_enemy->v.origin)) { pushChatterMessage (Chatter::ScaredEmotion); @@ -2495,7 +2496,7 @@ void Bot::executeChatterFrameEvents () { } // 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); bots.clearBombSay (BombPlantedSay::Chatter); } @@ -2706,7 +2707,7 @@ void Bot::checkRadioQueue () { break; 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); if (getCurrentTaskId () == Task::Camp) { @@ -2722,11 +2723,11 @@ void Bot::checkRadioQueue () { case Radio::RegroupTeam: // 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); clearSearchNodes (); - m_position = graph.getBombOrigin (); + m_position = gameState.getBombOrigin (); startTask (Task::MoveToPosition, TaskPri::MoveToPosition, kInvalidNodeIndex, 0.0f, true); pushRadioMessage (Radio::RogerThat); @@ -2850,7 +2851,7 @@ void Bot::checkRadioQueue () { case Task::Camp: if (rg.chance (m_radioPercent)) { - if (bots.isBombPlanted () && m_team == Team::Terrorist) { + if (gameState.isBombPlanted () && m_team == Team::Terrorist) { pushChatterMessage (Chatter::GuardingPlantedC4); } else if (m_inEscapeZone && m_team == Team::CT) { @@ -2915,14 +2916,14 @@ void Bot::checkRadioQueue () { case Radio::SectorClear: // is bomb planted and it's a ct - if (!bots.isBombPlanted ()) { + if (!gameState.isBombPlanted ()) { break; } // 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 - && util.isFakeClient (m_radioEntity) + && game.isFakeClientEntity (m_radioEntity) && bots.getPlantedBombSearchTimestamp () < game.time ()) { float nearestDistanceSq = kInfiniteDistance; @@ -3015,11 +3016,11 @@ void Bot::checkRadioQueue () { void Bot::tryHeadTowardRadioMessage () { 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; } - if ((util.isFakeClient (m_radioEntity) + if ((game.isFakeClientEntity (m_radioEntity) && rg.chance (m_radioPercent) && m_personality == Personality::Normal) || !(m_radioEntity->v.flags & FL_FAKECLIENT)) { @@ -3064,8 +3065,8 @@ void Bot::frame () { return; } - if (bots.isBombPlanted () && m_team == Team::CT && m_isAlive) { - const auto &bombPosition = graph.getBombOrigin (); + if (gameState.isBombPlanted () && m_team == Team::CT && m_isAlive) { + const auto &bombPosition = gameState.getBombOrigin (); if (!m_hasProgressBar && getCurrentTaskId () != Task::EscapeFromBomb @@ -3101,8 +3102,8 @@ void Bot::update () { const auto tid = getCurrentTaskId (); m_canChooseAimDirection = true; - m_isAlive = util.isAlive (ent ()); - m_team = game.getTeam (ent ()); + m_isAlive = game.isAliveEntity (ent ()); + m_team = game.getPlayerTeam (ent ()); m_healthValue = cr::clamp (pev->health, 0.0f, 99999.9f); if (m_team == Team::Terrorist && game.mapIs (MapFlags::Demolition)) { @@ -3148,7 +3149,7 @@ void Bot::update () { m_lastVoteKick = m_voteKickIndex; // if bot tk punishment is enabled slay the tk - if (cv_tkpunish.as () != 2 || util.isFakeClient (game.entityOfIndex (m_voteKickIndex))) { + if (cv_tkpunish.as () != 2 || game.isFakeClientEntity (game.entityOfIndex (m_voteKickIndex))) { return; } auto killer = game.entityOfIndex (m_lastVoteKick); @@ -3337,7 +3338,7 @@ void Bot::checkSpawnConditions () { void Bot::logic () { // 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) + m_movedDistance = kMinMovedDistance + 0.1f; // length of different vector (distance bot moved) resetMovement (); @@ -3369,7 +3370,7 @@ void Bot::logic () { // save current position as previous m_prevOrigin = pev->origin; - m_prevTime = game.time () + (0.2f - m_frameInterval * 2.0f); + m_prevTime = game.time () + (0.15f - m_frameInterval * 2.0f); } // if there's some radio message to respond, check it @@ -3607,7 +3608,7 @@ void Bot::showDebugOverlay () { static hudtextparms_t textParams {}; - textParams.channel = 1; + textParams.channel = 4; textParams.x = -1.0f; textParams.y = 0.0f; textParams.effect = 0; @@ -3669,7 +3670,7 @@ void Bot::takeDamage (edict_t *inflictor, int damage, int armor, int bits) { m_lastDamageType = bits; if (m_isCreature) { - if (util.isPlayer (inflictor) && game.isNullEntity (m_enemy)) { + if (game.isPlayerEntity (inflictor) && game.isNullEntity (m_enemy)) { if (seesEnemy (inflictor)) { m_enemy = inflictor; m_enemyOrigin = inflictor->v.origin; @@ -3683,10 +3684,10 @@ void Bot::takeDamage (edict_t *inflictor, int damage, int armor, int bits) { } m_lastDamageTimestamp = game.time (); - if (util.isPlayer (inflictor) || (cv_attack_monsters && util.isMonster (inflictor))) { - const auto inflictorTeam = game.getTeam (inflictor); + if (game.isPlayerEntity (inflictor) || (cv_attack_monsters && game.isMonsterEntity (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!!! m_actualReactionTime = 0.0f; m_seeEnemyTime = game.time (); @@ -3750,7 +3751,7 @@ void Bot::takeBlind (int alpha) { m_viewDistance = rg (10.0f, 20.0f); // 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_blindTime = game.time () + static_cast (alpha - 200) / 16.0f; @@ -3812,11 +3813,11 @@ void Bot::updatePracticeValue (int damage) const { 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. - if (!util.isPlayer (attacker)) { + if (!game.isPlayerEntity (attacker)) { return; } - const int attackerTeam = game.getTeam (attacker); + const int attackerTeam = game.getPlayerTeam (attacker); const int victimTeam = m_team; if (attackerTeam == victimTeam) { @@ -3847,7 +3848,7 @@ void Bot::updatePracticeDamage (edict_t *attacker, int damage) { 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 const auto damageValue = cr::clamp (practice.getDamage (m_team, victimIndex, attackerIndex) + damage / updateDamage, 0, kMaxDamageValue); @@ -3871,7 +3872,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* // 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_lookAt = user->v.origin; @@ -3962,16 +3963,16 @@ void Bot::debugMsgInternal (StringRef str) { Vector Bot::isBombAudible () { // 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 } 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 ()) * 100.0f; + const float timeElapsed = ((game.time () - gameState.getTimeBombPlanted ()) / mp_c4timer.as ()) * 100.0f; float desiredRadius = 768.0f; // start the manual calculations @@ -4052,12 +4053,7 @@ void Bot::runMovement () { m_oldButtons = pev->button; } -float Bot::getBombTimeleft () const { - if (!bots.isBombPlanted ()) { - return 0.0f; - } - return cr::max (bots.getTimeBombPlanted () + mp_c4timer.as () - game.time (), 0.0f); -} + bool Bot::isOutOfBombTimer () { if (!game.mapIs (MapFlags::Demolition)) { @@ -4069,13 +4065,13 @@ bool Bot::isOutOfBombTimer () { } // 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 (timeLeft > 13.0f) { return false; } - const auto &bombOrigin = graph.getBombOrigin (); + const auto &bombOrigin = gameState.getBombOrigin (); // for terrorist, if timer is lower than 13 seconds, return true if (timeLeft < 13.0f && m_team == Team::Terrorist && bombOrigin.distanceSq (pev->origin) < cr::sqrf (964.0f)) { @@ -4100,7 +4096,7 @@ bool Bot::isOutOfBombTimer () { 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 false; // return false otherwise @@ -4114,7 +4110,7 @@ void Bot::updateHearing () { float nearestDistanceSq = kInfiniteDistance; // 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_lastEnemyOrigin = m_lastEnemy->v.origin; @@ -4153,7 +4149,7 @@ void Bot::updateHearing () { } // did the bot hear someone ? - if (util.isPlayer (m_hearedEnemy)) { + if (game.isPlayerEntity (m_hearedEnemy)) { // change to best weapon if heard something if (m_shootTime < game.time () - 5.0f && isOnFloor () @@ -4257,8 +4253,8 @@ void Bot::enteredBuyZone (int buyState) { if (m_seeEnemyTime + 12.0f < game.time () && m_lastEquipTime + 30.0f < game.time () && m_inBuyZone - && (bots.getRoundStartTime () + rg (10.0f, 20.0f) + mp_buytime.as () < game.time ()) - && !bots.isBombPlanted () + && (gameState.getRoundStartTime () + rg (10.0f, 20.0f) + mp_buytime.as () < game.time ()) + && !gameState.isBombPlanted () && m_moneyAmount > econLimit[EcoLimit::PrimaryGreater]) { m_ignoreBuyDelay = true; @@ -4297,7 +4293,7 @@ void Bot::selectCampButtons (int index) { bool Bot::isBombDefusing (const Vector &bombOrigin) const { // this function finds if somebody currently defusing the bomb. - if (!bots.isBombPlanted ()) { + if (!gameState.isBombPlanted ()) { return false; } bool defusingInProgress = false; @@ -4370,8 +4366,8 @@ void Bot::refreshCreatureStatus (char *infobuffer) { } // if bot is on infected team, and zombie mode is active, assume bot is a creature/zombie - m_isOnInfectedTeam = game.getRealTeam (ent ()) == infectedTeam; - m_infectedEnemyTeam = game.getRealTeam (m_enemy) == infectedTeam; + m_isOnInfectedTeam = game.getRealPlayerTeam (ent ()) == infectedTeam; + m_infectedEnemyTeam = game.getRealPlayerTeam (m_enemy) == infectedTeam; // do not process next if already infected if (m_isOnInfectedTeam || m_infectedEnemyTeam) { @@ -4451,7 +4447,7 @@ void Bot::donateC4ToHuman () { // search world for just dropped bomb game.searchEntities ("classname", "weaponbox", [&] (edict_t *ent) { - if (util.isModel (ent, "backpack.mdl")) { + if (game.isEntityModelMatches (ent, "backpack.mdl")) { bomb = ent; if (!game.isNullEntity (bomb)) { diff --git a/src/chatlib.cpp b/src/chatlib.cpp index 83e78ba..388bda2 100644 --- a/src/chatlib.cpp +++ b/src/chatlib.cpp @@ -161,7 +161,7 @@ void Bot::prepareChatMessage (StringRef message) { auto humanizedName = [] (int index) -> String { auto ent = game.playerOfIndex (index); - if (!util.isPlayer (ent)) { + if (!game.isPlayerEntity (ent)) { return "unknown"; } String playerName = ent->v.netname.chars (); @@ -193,7 +193,7 @@ void Bot::prepareChatMessage (StringRef message) { // get roundtime auto getRoundTime = [] () -> String { - auto roundTimeSecs = static_cast (bots.getRoundEndTime () - game.time ()); + const auto roundTimeSecs = static_cast (gameState.getRoundEndTime () - game.time ()); String roundTime {}; 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); } else if (!needsEnemy && m_team == client.team) { - if (util.isPlayer (pev->dmg_inflictor) - && game.getRealTeam (pev->dmg_inflictor) == m_team) { + if (game.isPlayerEntity (pev->dmg_inflictor) + && game.getRealPlayerTeam (pev->dmg_inflictor) == m_team) { return humanizedName (game.indexOfPlayer (pev->dmg_inflictor)); } diff --git a/src/combat.cpp b/src/combat.cpp index fa355d4..64d4b38 100644 --- a/src/combat.cpp +++ b/src/combat.cpp @@ -335,7 +335,7 @@ bool Bot::checkBodyPartsWithHitboxes (edict_t *target) { bool Bot::seesEnemy (edict_t *player) { auto isBehindSmokeClouds = [&] (const Vector &pos) { if (cv_smoke_grenade_checks.as () == 2) { - return bots.isLineBlockedBySmoke (getEyesPos (), pos); + return util.isLineBlockedBySmoke (getEyesPos (), pos); } return false; }; @@ -345,7 +345,7 @@ bool Bot::seesEnemy (edict_t *player) { } 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; } @@ -387,7 +387,7 @@ bool Bot::lookupEnemies () { if (!game.isNullEntity (m_enemy) && (m_states & Sense::SeeingEnemy)) { 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; const bool denyLastEnemy = pev->velocity.lengthSq2d () > 0.0f @@ -406,7 +406,7 @@ bool Bot::lookupEnemies () { // is player is alive if (m_enemyUpdateTime > game.time () && player->v.origin.distanceSq (pev->origin) < nearestDistanceSq - && util.isAlive (player) + && game.isAliveEntity (player) && seesEnemy (player)) { newEnemy = player; @@ -427,8 +427,8 @@ bool Bot::lookupEnemies () { if (cv_attack_monsters) { // search the world for monsters... - for (const auto &interesting : bots.getInterestingEntities ()) { - if (!util.isMonster (interesting)) { + for (const auto &interesting : gameState.getInterestingEntities ()) { + if (!game.isMonsterEntity (interesting)) { continue; } @@ -485,7 +485,7 @@ bool Bot::lookupEnemies () { newEnemy = player; // aim VIP first on AS maps... - if (game.is (MapFlags::Assassination) && util.isPlayerVIP (newEnemy)) { + if (game.is (MapFlags::Assassination) && game.isPlayerVIP (newEnemy)) { 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); m_aimFlags |= AimFlags::Enemy; @@ -587,7 +587,7 @@ bool Bot::lookupEnemies () { newEnemy = m_enemy; m_lastEnemy = newEnemy; - if (!util.isAlive (newEnemy)) { + if (!game.isAliveEntity (newEnemy)) { m_enemy = nullptr; m_enemyBodyPartSet = nullptr; @@ -638,7 +638,7 @@ bool Bot::lookupEnemies () { && game.isNullEntity (m_enemy) && getCurrentTaskId () != Task::ShootBreakable && getCurrentTaskId () != Task::PlantBomb - && getCurrentTaskId () != Task::DefuseBomb) || bots.isRoundOver ()) { + && getCurrentTaskId () != Task::DefuseBomb) || gameState.isRoundOver ()) { if (!m_reloadState) { m_reloadState = Reload::Primary; @@ -719,7 +719,7 @@ Vector Bot::getEnemyBodyOffset () { if (!m_enemyParts && (m_states & Sense::SuspectEnemy)) { spot += getBodyOffsetError (distance); } - else if (util.isPlayer (m_enemy)) { + else if (game.isPlayerEntity (m_enemy)) { // now take in account different parts of enemy body if (m_enemyParts & (Visibility::Head | Visibility::Body)) { auto headshotPct = conf.getDifficultyTweaks (m_difficulty)->headshotPct; @@ -828,11 +828,11 @@ bool Bot::isFriendInLineOfFire (float distance) const { game.testLine (getEyesPos (), getEyesPos () + pev->v_angle.normalize_apx () * distance, TraceIgnore::None, ent (), &tr); // 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; // check valid range - if (game.getTeam (hit) == m_team && util.isAlive (hit)) { + if (game.getPlayerTeam (hit) == m_team && game.isAliveEntity (hit)) { return true; } } @@ -1459,7 +1459,7 @@ void Bot::attackMovement () { if (!game.is (GameFlags::CSDM) && !isKnifeMode ()) { if ((m_states & Sense::SeeingEnemy) && approach < 30 - && !bots.isBombPlanted () + && !gameState.isBombPlanted () && (isEnemyCone || m_isVIP || m_isReloading)) { if (m_retreatTime < game.time ()) { @@ -1973,7 +1973,7 @@ void Bot::decideFollowUser () { continue; } - if (seesEntity (client.origin) && !util.isFakeClient (client.ent)) { + if (seesEntity (client.origin) && !game.isFakeClientEntity (client.ent)) { users.push (client.ent); } } @@ -2248,7 +2248,7 @@ edict_t *Bot::setCorrectGrenadeVelocity (StringRef model) { edict_t *result = nullptr; 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; // set the correct velocity for the grenade @@ -2286,7 +2286,7 @@ void Bot::checkGrenadesThrow () { || cv_ignore_enemies || m_isUsingGrenade || m_isReloading - || (isKnifeMode () && !bots.isBombPlanted ()) + || (isKnifeMode () && !gameState.isBombPlanted ()) || m_grenadeCheckTime >= game.time () || m_lastEnemyOrigin.empty ()); @@ -2300,7 +2300,7 @@ void Bot::checkGrenadesThrow () { 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); return; } @@ -2343,7 +2343,7 @@ void Bot::checkGrenadesThrow () { // special condition if we're have valid current enemy if (!isGrenadeMode && ((m_states & Sense::SeeingEnemy) - && util.isAlive (m_enemy) + && game.isAliveEntity (m_enemy) && ((m_enemy->v.button | m_enemy->v.oldbuttons) & IN_ATTACK) && util.isVisible (pev->origin, m_enemy)) && util.isInViewCone (pev->origin, m_enemy)) { diff --git a/src/config.cpp b/src/config.cpp index 45998f0..1989db9 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -352,7 +352,7 @@ void BotConfig::loadChatterConfig () { { "Chatter_Camp", Chatter::Camping, 10.0f }, { "Chatter_OnARoll", Chatter::OnARoll, kMaxChatterRepeatInterval}, }; - Array badFiles {}; + Array missingWaves {}; while (file.getLine (line)) { line.trim (); @@ -394,7 +394,7 @@ void BotConfig::loadChatterConfig () { m_chatter[event.code].emplace (cr::move (sound), event.repeat, duration); } else { - badFiles.push (sound); + missingWaves.push (sound); } } sentences.clear (); @@ -404,8 +404,16 @@ void BotConfig::loadChatterConfig () { } file.close (); - if (!badFiles.empty ()) { - game.print ("Warning: Couldn't get duration of next chatter sounds: %s.", String::join (badFiles, ",")); + if (!missingWaves.empty ()) { + 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 { diff --git a/src/control.cpp b/src/control.cpp index 3fd7604..e39cedc 100644 --- a/src/control.cpp +++ b/src/control.cpp @@ -183,7 +183,7 @@ int BotControl::cmdMenu () { // reset the current menu closeMenu (); - if (arg (cmd) == "cmd" && util.isAlive (m_ent)) { + if (arg (cmd) == "cmd" && game.isAliveEntity (m_ent)) { showMenu (Menu::Commands); } else { @@ -205,7 +205,7 @@ int BotControl::cmdCvars () { auto match = arg (pattern); // stop printing if executed once more - flushPrintQueue (); + m_printQueue.clear (); // revert all the cvars to their default values if (match == "defaults") { @@ -1115,7 +1115,7 @@ int BotControl::menuFeatures (int item) { break; case 5: - if (util.isAlive (m_ent)) { + if (game.isAliveEntity (m_ent)) { showMenu (Menu::Commands); } else { @@ -1924,7 +1924,7 @@ bool BotControl::executeCommands () { } bool BotControl::executeMenus () { - if (!util.isPlayer (m_ent) || game.isBotCmd ()) { + if (!game.isPlayerEntity (m_ent) || game.isBotCmd ()) { return false; } const auto &issuer = util.getClient (game.indexOfPlayer (m_ent)); @@ -1966,7 +1966,7 @@ void BotControl::showMenu (int id) { menusParsed = true; } - if (!util.isPlayer (m_ent)) { + if (!game.isPlayerEntity (m_ent)) { return; } auto &client = util.getClient (game.indexOfPlayer (m_ent)); @@ -2007,7 +2007,7 @@ void BotControl::showMenu (int id) { } void BotControl::closeMenu () { - if (!util.isPlayer (m_ent)) { + if (!game.isPlayerEntity (m_ent)) { return; } auto &client = util.getClient (game.indexOfPlayer (m_ent)); @@ -2073,7 +2073,7 @@ void BotControl::kickBotByMenu (int page) { } void BotControl::assignAdminRights (edict_t *ent, char *infobuffer) { - if (!game.isDedicated () || util.isFakeClient (ent)) { + if (!game.isDedicated () || game.isFakeClientEntity (ent)) { return; } StringRef key = cv_password_key.as (); @@ -2100,7 +2100,7 @@ void BotControl::maintainAdminRights () { StringRef password = cv_password.as (); for (auto &client : util.getClients ()) { - if (!(client.flags & ClientFlags::Used) || util.isFakeClient (client.ent)) { + if (!(client.flags & ClientFlags::Used) || game.isFakeClientEntity (client.ent)) { continue; } auto ent = client.ent; diff --git a/src/engine.cpp b/src/engine.cpp index 44002e8..341939b 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -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 (plat.hardwareConcurrency ())); 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_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_g ("sv_skycolor_g", nullptr, Var::GameRef); @@ -77,7 +78,7 @@ void Game::levelInitialize (edict_t *entities, int max) { conf.loadMainConfig (); // ensure the server admin is confident about features he's using - game.ensureHealthyGameEnvironment (); + ensureHealthyGameEnvironment (); // load map-specific config conf.loadMapSpecificConfig (); @@ -138,7 +139,7 @@ void Game::levelInitialize (edict_t *entities, int max) { else if (classname == "func_vip_safetyzone" || classname == "info_vip_safetyzone") { m_mapFlags |= MapFlags::Assassination; // assassination map } - else if (util.isHostageEntity (ent)) { + else if (isHostageEntity (ent)) { m_mapFlags |= MapFlags::HostageRescue; // rescue map } else if (classname == "func_bomb_target" || classname == "info_bomb_target") { @@ -152,13 +153,13 @@ void Game::levelInitialize (edict_t *entities, int max) { m_mapFlags &= ~MapFlags::HostageRescue; } } - else if (util.isDoorEntity (ent)) { + else if (isDoorEntity (ent)) { m_mapFlags |= MapFlags::HasDoors; } else if (classname.startsWith ("func_button")) { m_mapFlags |= MapFlags::HasButtons; } - else if (util.isBreakableEntity (ent, true)) { + else if (isBreakableEntity (ent, true)) { // add breakable for material check m_checkedBreakables[indexOfEntity (ent)] = ent->v.impulse <= 0; @@ -197,12 +198,12 @@ void Game::levelShutdown () { bots.destroyKillerEntity (); // ensure players are off on xash3d - if (game.is (GameFlags::Xash3DLegacy)) { + if (is (GameFlags::Xash3DLegacy)) { bots.kickEveryone (true, false); } // set state to unprecached - game.setUnprecached (); + setUnprecached (); // enable lightstyle animations on level change illum.enableAnimation (true); @@ -211,7 +212,7 @@ void Game::levelShutdown () { util.setNeedForWelcome (false); // clear local entity - game.setLocalEntity (nullptr); + setLocalEntity (nullptr); // reset graph state graph.reset (); @@ -229,7 +230,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, // 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 } @@ -373,7 +374,7 @@ void Game::setPlayerStartDrawModels () { }; 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 ()); return EntitySearchResult::Continue; }); @@ -426,12 +427,12 @@ void Game::sendClientMessage (bool console, edict_t *ent, StringRef message) { // helper to sending the client message // do not send messages to fake clients - if (!util.isPlayer (ent) || util.isFakeClient (ent)) { + if (!isPlayerEntity (ent) || isFakeClientEntity (ent)) { return; } // 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); return; } @@ -482,7 +483,7 @@ void Game::sendServerMessage (StringRef message) { void Game::sendHudMessage (edict_t *ent, const hudtextparms_t &htp, StringRef message) { constexpr size_t kMaxSendLength = 512; - if (game.isNullEntity (ent)) { + if (isNullEntity (ent)) { return; } MessageWriter msg (MSG_ONE_UNRELIABLE, SVC_TEMPENTITY, nullptr, ent); @@ -1006,7 +1007,7 @@ bool Game::postload () { // register fake metamod command handler if we not! under mm 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); }); } @@ -1126,7 +1127,7 @@ void Game::slowFrame () { if (m_halfSecondFrame < time ()) { // 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 ensureHealthyGameEnvironment (); @@ -1209,7 +1210,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 ())); } @@ -1367,6 +1368,120 @@ bool Game::isDeveloperMode () const { 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 (); + + // 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 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 () { // this function initializes lighting information... @@ -1588,7 +1703,7 @@ Vector PlayerHitboxEnumerator::get (edict_t *ent, int part, float updateTimestam void PlayerHitboxEnumerator::update (edict_t *ent) { constexpr auto kInvalidHitbox = -1; - if (!util.isAlive (ent)) { + if (!game.isAliveEntity (ent)) { return; } // get info about player @@ -1677,3 +1792,143 @@ void PlayerHitboxEnumerator::reset () { 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 (); + m_timeRoundMid = m_timeRoundStart + mp_roundtime.as () * 60.0f * 0.5f; + m_timeRoundEnd = m_timeRoundStart + mp_roundtime.as () * 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 () - 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 (); +} diff --git a/src/entities.cpp b/src/entities.cpp index dbc810b..9a0f8cb 100644 --- a/src/entities.cpp +++ b/src/entities.cpp @@ -9,7 +9,7 @@ // 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 -// 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. #if defined(LINKENT_STATIC) void forwardEntity_helper (EntityProto &addr, const char *name, entvars_t *pev) { diff --git a/src/fakeping.cpp b/src/fakeping.cpp index 9cfbe2b..b7cdbc1 100644 --- a/src/fakeping.cpp +++ b/src/fakeping.cpp @@ -24,7 +24,7 @@ void BotFakePingManager::reset (edict_t *to) { } 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; } m_pbm.start (client.ent); @@ -46,7 +46,7 @@ void BotFakePingManager::syncCalculate () { int numHumans {}; 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; } numHumans++; @@ -101,7 +101,7 @@ void BotFakePingManager::calculate () { } void BotFakePingManager::emit (edict_t *ent) { - if (!util.isPlayer (ent)) { + if (!game.isPlayerEntity (ent)) { return; } diff --git a/src/graph.cpp b/src/graph.cpp index dda415a..3054b5d 100644 --- a/src/graph.cpp +++ b/src/graph.cpp @@ -1310,7 +1310,7 @@ void BotGraph::emitNotify (int32_t sound) const { }; // notify editor - if (util.isPlayer (m_editor) && !m_silenceMessages) { + if (game.isPlayerEntity (m_editor) && !m_silenceMessages) { game.playSound (m_editor, notifySounds[sound].chars ()); } } @@ -1483,7 +1483,7 @@ void BotGraph::calculatePathRadius (int index) { if (tr.flFraction < 1.0f) { game.testLine (radiusStart, radiusEnd, TraceIgnore::Monsters, nullptr, &tr); - if (util.isDoorEntity (tr.pHit)) { + if (game.isDoorEntity (tr.pHit)) { path.radius = 0.0f; wayBlocked = true; @@ -1976,7 +1976,7 @@ bool BotGraph::isNodeReacheableEx (const Vector &src, const Vector &destination, // check if this node is "visible"... 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 (tr.flFraction >= 1.0f || isDoor) { @@ -2059,7 +2059,7 @@ void BotGraph::frame () { } // 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; } @@ -2123,7 +2123,7 @@ void BotGraph::frame () { // check if node is within a distance, and is visible if (distanceSq < cr::sqrf (cv_graph_draw_distance.as ()) && ((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 if (distanceSq < nearestDistanceSq) { @@ -2766,43 +2766,6 @@ void BotGraph::addBasic () { 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 () { m_jumpLearnNode = true; } diff --git a/src/hooks.cpp b/src/hooks.cpp index 71e9283..fa6f8ef 100644 --- a/src/hooks.cpp +++ b/src/hooks.cpp @@ -27,7 +27,7 @@ int32_t ServerQueryHook::sendTo (int socket, const void *message, size_t length, buffer.skip (); // score auto ctime = buffer.read (); // override connection time - buffer.write (bots.getConnectTime (name, ctime)); + buffer.write (bots.getConnectionTimes (name, ctime)); } return send (buffer.data ()); } diff --git a/src/linkage.cpp b/src/linkage.cpp index 139a71c..186f576 100644 --- a/src/linkage.cpp +++ b/src/linkage.cpp @@ -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 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)) { RETURN_META (MRES_SUPERCEDE); // prevent bots to be forced to issue client commands } @@ -159,7 +159,7 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) { auto bot = bots[pentTouched]; - if (bot && util.isBreakableEntity (pentOther)) { + if (bot && game.isBreakableEntity (pentOther)) { bot->checkBreakable (pentOther); } } @@ -389,10 +389,10 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) { if (bots.hasBotsOnline ()) { // keep track of grenades on map - bots.updateActiveGrenade (); + gameState.updateActiveGrenade (); // keep track of interesting entities - bots.updateInterestingEntities (); + gameState.updateInterestingEntities (); } // keep bot number up to date @@ -429,7 +429,7 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) { auto ent = const_cast (reinterpret_cast (player)); 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); } } @@ -560,7 +560,7 @@ CR_C_LINKAGE int GetEntityAPI_Post (gamefuncs_t *table, int) { auto ent = const_cast (reinterpret_cast (player)); 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); } } @@ -591,7 +591,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 { // round starts in counter-strike 1.5 if (strcmp (value, "info_map_parameters") == 0) { - bots.initRound (); + gameState.roundStart (); } if (game.is (GameFlags::Metamod)) { @@ -786,7 +786,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 // 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)) { RETURN_META (MRES_SUPERCEDE); } diff --git a/src/manager.cpp b/src/manager.cpp index 1295985..9602939 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -469,7 +469,7 @@ void BotManager::maintainLeaders () { } // 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) { selectLeaders (team, false); } @@ -487,7 +487,7 @@ void BotManager::maintainRoundRestart () { && m_numPreviousPlayers == 0 && totalHumans == 1 && totalBots > 0 - && !m_resetHud) { + && !gameState.isResetHUD ()) { static ConVarRef sv_restartround ("sv_restartround"); @@ -496,13 +496,13 @@ void BotManager::maintainRoundRestart () { } } m_numPreviousPlayers = totalHumans; - m_resetHud = false; + gameState.setResetHUD (false); } void BotManager::maintainAutoKill () { const float killDelay = cv_autokill_delay.as (); - if (killDelay < 1.0f || m_roundOver) { + if (killDelay < 1.0f || gameState.isRoundOver ()) { return; } @@ -516,7 +516,7 @@ void BotManager::maintainAutoKill () { int aliveBots = 0; // do not interrupt bomb-defuse scenario - if (game.mapIs (MapFlags::Demolition) && isBombPlanted ()) { + if (game.mapIs (MapFlags::Demolition) && gameState.isBombPlanted ()) { return; } const int totalHumans = getHumansCount (true); // we're ignore spectators intentionally @@ -531,7 +531,7 @@ void BotManager::maintainAutoKill () { ++aliveBots; // 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; } } @@ -545,15 +545,9 @@ void BotManager::maintainAutoKill () { } void BotManager::reset () { - m_grenadeUpdateTime = 0.0f; - m_entityUpdateTime = 0.0f; m_plantSearchUpdateTime = 0.0f; m_lastChatTime = 0.0f; - m_timeBombPlanted = 0.0f; m_bombSayStatus = BombPlantedSay::ChatSay | BombPlantedSay::Chatter; - - m_interestingEntities.clear (); - m_activeGrenades.clear (); } void BotManager::initFilters () { @@ -681,7 +675,7 @@ void BotManager::kickFromTeam (Team team, bool removeAll) { } for (const auto &bot : m_bots) { - if (team == game.getRealTeam (bot->ent ())) { + if (team == game.getRealPlayerTeam (bot->ent ())) { bot->kick (removeAll); if (!removeAll) { @@ -696,7 +690,7 @@ void BotManager::killAllBots (int team, bool silent) { // this function kills all bots on server (only this dll controlled 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; } bot->kill (); @@ -732,7 +726,7 @@ bool BotManager::kickRandom (bool decQuota, Team fromTeam) { if (fromTeam == Team::Unassigned) { return true; } - return game.getRealTeam (bot->ent ()) == fromTeam; + return game.getRealPlayerTeam (bot->ent ()) == fromTeam; }; // first try to kick the bot that is currently dead @@ -830,7 +824,7 @@ bool BotManager::hasCustomCSDMSpawnEntities () { void BotManager::setLastWinner (int winner) { m_lastWinner = winner; - m_roundOver = true; + gameState.setRoundOver (true); if (cv_radio_mode.as () != 2) { return; @@ -839,7 +833,7 @@ void BotManager::setLastWinner (int winner) { if (notify) { if (notify->m_team == winner) { - if (getRoundMidTime () > game.time ()) { + if (gameState.getRoundMidTime () > game.time ()) { notify->pushChatterMessage (Chatter::QuickWonRound); } else { @@ -929,7 +923,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"); auto botTeam = [] (edict_t *ent) -> StringRef { - const auto team = game.getRealTeam (ent); + const auto team = game.getRealPlayerTeam (ent); switch (team) { case Team::CT: @@ -967,7 +961,7 @@ void BotManager::listBots () { 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. for (const auto &bot : m_bots) { @@ -1020,19 +1014,18 @@ Twin BotManager::countTeamPlayers () { } Bot *BotManager::findHighestFragBot (int team) { - int bestIndex = 0; - float bestScore = -1; + Twin best {}; // search bots in this team for (const auto &bot : bots) { - if (bot->m_isAlive && game.getRealTeam (bot->ent ()) == team) { - if (bot->pev->frags > bestScore) { - bestIndex = bot->index (); - bestScore = bot->pev->frags; + if (bot->m_isAlive && game.getRealPlayerTeam (bot->ent ()) == team) { + if (bot->pev->frags > best.second) { + best.first = bot->index (); + best.second = bot->pev->frags; } } } - return findBotByIndex (bestIndex); + return findBotByIndex (best.first); } void BotManager::updateTeamEconomics (int team, bool setTrue) { @@ -1398,8 +1391,8 @@ void BotManager::disconnectBot (Bot *bot) { } void BotManager::handleDeath (edict_t *killer, edict_t *victim) { - const auto killerTeam = game.getRealTeam (killer); - const auto victimTeam = game.getRealTeam (victim); + const auto killerTeam = game.getRealPlayerTeam (killer); + const auto victimTeam = game.getRealPlayerTeam (victim); if (cv_radio_mode.as () == 2) { // need to send congrats on well placed shot @@ -1512,7 +1505,7 @@ void Bot::newRound () { node = kInvalidNodeIndex; } m_navTimeset = game.time (); - m_team = game.getTeam (ent ()); + m_team = game.getPlayerTeam (ent ()); resetPathSearchType (); @@ -1841,7 +1834,7 @@ void Bot::updateTeamJoin () { if (!m_notStarted) { 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 if (game.is (GameFlags::Legacy)) { @@ -1948,15 +1941,15 @@ void BotManager::captureChatRadio (StringRef cmd, StringRef arg, edict_t *ent) { } if (cmd.startsWith ("say")) { - const bool alive = util.isAlive (ent); + const bool alive = game.isAliveEntity (ent); int team = -1; if (cmd.endsWith ("team")) { - team = game.getRealTeam (ent); + team = game.getRealPlayerTeam (ent); } 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; } auto target = bots[client.ent]; @@ -2003,7 +1996,7 @@ void BotManager::captureChatRadio (StringRef cmd, StringRef arg, edict_t *ent) { void BotManager::notifyBombDefuse () { // notify all terrorists that CT is starting bomb defusing - const auto &bombPos = graph.getBombOrigin (); + const auto &bombPos = gameState.getBombOrigin (); for (const auto &bot : bots) { const auto task = bot->getCurrentTaskId (); @@ -2027,68 +2020,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) { auto &leaderChoosen = m_teamData[team].leaderChoosen; @@ -2191,8 +2122,6 @@ void BotManager::selectLeaders (int team, bool reset) { void BotManager::initRound () { // this is called at the start of each round - m_roundOver = false; - // check team economics for (int team = 0; team < kGameTeamNum; ++team) { updateTeamEconomics (team); @@ -2211,35 +2140,15 @@ void BotManager::initRound () { for (auto &client : util.getClients ()) { client.radio = 0; } - - graph.setBombOrigin (true); graph.clearVisited (); m_bombSayStatus = BombPlantedSay::ChatSay | BombPlantedSay::Chatter; - m_timeBombPlanted = 0.0f; m_plantSearchUpdateTime = 0.0f; m_autoKillCheckTime = 0.0f; m_botsCanPause = false; resetFilters (); practice.update (); // update practice data on round start - - // calculate the round mid/end in world time - m_timeRoundStart = game.time () + mp_freezetime.as (); - m_timeRoundMid = m_timeRoundStart + mp_roundtime.as () * 60.0f * 0.5f; - m_timeRoundEnd = m_timeRoundStart + mp_roundtime.as () * 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 () { @@ -2293,121 +2202,6 @@ void BotThreadWorker::startup (int workers) { m_pool->startup (static_cast (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 () { if (game.is (GameFlags::Legacy)) { return true; diff --git a/src/message.cpp b/src/message.cpp index 4df7b1f..602e922 100644 --- a/src/message.cpp +++ b/src/message.cpp @@ -26,7 +26,7 @@ void MessageDispatcher::netMsgTextMsg () { // reset bomb position for all the bots const auto resetBombPosition = [] () -> void { 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 resetBombPosition (); } - else if ((cached & TextMsgCache::BombPlanted) && !bots.isBombPlanted ()) { - bots.setBombPlanted (true); + else if ((cached & TextMsgCache::BombPlanted) && !gameState.isBombPlanted ()) { + gameState.setBombPlanted (true); for (const auto ¬ify : bots) { if (notify->m_isAlive) { @@ -68,7 +68,7 @@ void MessageDispatcher::netMsgTextMsg () { } } } - graph.setBombOrigin (); + gameState.setBombOrigin (); } // check for burst fire message @@ -319,7 +319,7 @@ void MessageDispatcher::netMsgHLTV () { // need to start new round ? (we're tracking FOV reset message) 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 // 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 (); } } @@ -435,7 +435,7 @@ void MessageDispatcher::netMsgResetHUD () { if (m_bot) { m_bot->spawned (); } - bots.setResetHUD (true); + gameState.setResetHUD (true); } MessageDispatcher::MessageDispatcher () { diff --git a/src/navigate.cpp b/src/navigate.cpp index f15b909..ad536ec 100644 --- a/src/navigate.cpp +++ b/src/navigate.cpp @@ -60,12 +60,12 @@ int Bot::findBestGoal () { bool hasMoreHostagesAround = false; // try to search nearby-unused hostage, and if so, go to next goal - if (bots.hasInterestingEntities ()) { - const auto &interesting = bots.getInterestingEntities (); + if (gameState.hasInterestingEntities ()) { + const auto &interesting = gameState.getInterestingEntities (); // search world for hostages for (const auto &ent : interesting) { - if (!util.isHostageEntity (ent)) { + if (!game.isHostageEntity (ent)) { continue; } bool hostageInUse = false; @@ -134,7 +134,7 @@ int Bot::findBestGoal () { } } 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)) { pushChatMessage (Chat::Plant); @@ -149,10 +149,10 @@ int Bot::findBestGoal () { 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 - if (!m_defendedBomb && bots.isBombPlanted () && getCurrentTaskId () != Task::EscapeFromBomb && getBombTimeleft () >= 15.0f) { - return m_chosenGoalIndex = findDefendNode (graph.getBombOrigin ()); + if (!m_defendedBomb && gameState.isBombPlanted () && getCurrentTaskId () != Task::EscapeFromBomb && gameState.getBombTimeLeft () >= 15.0f) { + return m_chosenGoalIndex = findDefendNode (gameState.getBombOrigin ()); } } else if (game.mapIs (MapFlags::Escape)) { @@ -202,9 +202,9 @@ int Bot::findBestGoal () { int Bot::findBestGoalWhenBombAction () { int result = kInvalidNodeIndex; - if (!bots.isBombPlanted () && !cv_ignore_objectives) { + if (!gameState.isBombPlanted () && !cv_ignore_objectives) { 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)); if (graph.exists (result)) { @@ -230,7 +230,7 @@ int Bot::findBestGoalWhenBombAction () { } } else if (!m_defendedBomb) { - const auto &bombOrigin = graph.getBombOrigin (); + const auto &bombOrigin = gameState.getBombOrigin (); if (!bombOrigin.empty ()) { m_defendedBomb = true; @@ -239,7 +239,7 @@ int Bot::findBestGoalWhenBombAction () { const auto &path = graph[result]; const float bombTimer = mp_c4timer.as (); - 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 ()) { 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 // 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; int count = 0; for (const auto &point : graph.m_goalPoints) { 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; } if (distanceSq < nearestDistanceSq) { @@ -594,7 +595,7 @@ void Bot::doPlayerAvoidance (const Vector &normal) { void Bot::checkTerrain (const Vector &dirNormal) { - // if avoiding someone do not consider stuck + // if avoiding someone do not consider stuckn TraceResult tr {}; m_isStuck = false; @@ -602,7 +603,7 @@ void Bot::checkTerrain (const Vector &dirNormal) { // minimal speed for consider stuck const float minimalSpeed = isDucking () ? kMinMovedDistance : kMinMovedDistance * 4; - const auto randomProbeTime = rg (0.75f, 1.15f); + const auto randomProbeTime = rg (0.50f, 0.75f); // standing still, no need to check? if ((cr::abs (m_moveSpeed) >= minimalSpeed || cr::abs (m_strafeSpeed) >= minimalSpeed) @@ -900,10 +901,7 @@ void Bot::checkTerrain (const Vector &dirNormal) { } void Bot::checkFall () { - if (isPreviousLadder ()) { - return; - } - else if ((m_pathFlags & NodeFlag::Ladder) && isPreviousLadder () && isOnLadder ()) { + if (isPreviousLadder () || (m_pathFlags & NodeFlag::Ladder)) { return; } @@ -1105,7 +1103,7 @@ bool Bot::updateNavigation () { if (m_desiredVelocity.length2d () > 0.0f) { pev->velocity = m_desiredVelocity; } - else { + else if (graph.isAnalyzed ()) { 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) }; @@ -1151,7 +1149,7 @@ bool Bot::updateNavigation () { } if (m_pathFlags & NodeFlag::Ladder) { - constexpr auto kLadderOffset = Vector (0.0f, 0.0f, 36.0f); + constexpr auto kLadderOffset = Vector { 0.0f, 0.0f, 36.0f }; const auto prevNodeIndex = m_previousNodes[0]; const float ladderDistance = pev->origin.distance (m_pathOrigin); @@ -1231,7 +1229,7 @@ bool Bot::updateNavigation () { if (game.mapIs (MapFlags::HasDoors) || (m_pathFlags & NodeFlag::Button)) { 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 float distanceSq = pev->origin.distanceSq (origin); @@ -1285,7 +1283,7 @@ bool Bot::updateNavigation () { util.findNearestPlayer (reinterpret_cast (&nearest), ent (), 256.0f, false, false, true, true, false); // 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_states |= Sense::SeeingEnemy | Sense::SuspectEnemy; @@ -1324,7 +1322,7 @@ bool Bot::updateNavigation () { } } - float desiredDistanceSq = cr::sqrf (8.0f); + float desiredDistanceSq = cr::sqrf (32.0f); const float nodeDistanceSq = pev->origin.distanceSq (m_pathOrigin); // initialize the radius for a special node type, where the node is considered to be reached @@ -1336,7 +1334,7 @@ bool Bot::updateNavigation () { // 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)) { - desiredDistanceSq = cr::sqrf (128.0f); + desiredDistanceSq = cr::sqrf (96.0f); } } else if (m_pathFlags & NodeFlag::Ladder) { @@ -1374,7 +1372,7 @@ bool Bot::updateNavigation () { } // 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); if (predictRangeSq >= nodeDistanceSq || predictRangeSq <= desiredDistanceSq) { @@ -1386,7 +1384,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 // 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. - 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; m_currentNodeIndex = kInvalidNodeIndex; @@ -1418,7 +1419,7 @@ bool Bot::updateNavigation () { const int taskTarget = getTask ()->data; if (game.mapIs (MapFlags::Demolition) - && bots.isBombPlanted () + && gameState.isBombPlanted () && m_team == Team::CT && getCurrentTaskId () != Task::EscapeFromBomb && taskTarget != kInvalidNodeIndex) { @@ -1481,7 +1482,7 @@ bool Bot::updateLiftHandling () { game.testLine (pev->origin, m_pathOrigin, TraceIgnore::Everything, ent (), &tr); 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) && pev->groundentity != tr.pHit) { @@ -2117,7 +2118,7 @@ int Bot::findBombNode () { const auto &goals = graph.m_goalPoints; - const auto &bomb = graph.getBombOrigin (); + const auto &bomb = gameState.getBombOrigin (); const auto &audible = isBombAudible (); // take the nearest to bomb nodes instead of goal if close enough @@ -2535,9 +2536,9 @@ bool Bot::advanceMovement () { // only if we in normal task and bomb is not planted if (tid == Task::Normal - && bots.getRoundMidTime () + 5.0f < game.time () + && gameState.getRoundMidTime () + 5.0f < game.time () && m_timeCamping + 5.0f < game.time () - && !bots.isBombPlanted () + && !gameState.isBombPlanted () && m_personality != Personality::Rusher && !m_hasC4 && !m_isVIP && m_loosedBombNodeIndex == kInvalidNodeIndex @@ -2549,7 +2550,7 @@ bool Bot::advanceMovement () { auto kills = static_cast (practice.getDamage (m_team, nextIndex, nextIndex)); // 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) { case Personality::Normal: kills *= 0.33f; @@ -2647,7 +2648,7 @@ bool Bot::advanceMovement () { // mark as jump sequence, if the current and next paths are jumps 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 ? @@ -2752,11 +2753,11 @@ bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) { if (!game.mapIs (MapFlags::HasDoors)) { return false; } - return result->flFraction < 1.0f && !util.isDoorEntity (result->pHit); + return result->flFraction < 1.0f && !game.isDoorEntity (result->pHit); }; 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... @@ -2764,8 +2765,8 @@ bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) { // check if the trace hit something... if (tr->flFraction < 1.0f) { - if ((game.mapIs (MapFlags::HasDoors) && util.isDoorEntity (tr->pHit)) - || (m_team == Team::CT && util.isHostageEntity (tr->pHit))) { + if ((game.mapIs (MapFlags::HasDoors) && game.isDoorEntity (tr->pHit)) + || (m_team == Team::CT && game.isHostageEntity (tr->pHit))) { return false; } return true; // bot's head will hit something @@ -3120,7 +3121,7 @@ bool Bot::isBlockedLeft () { game.testLine (pev->origin, pev->origin - forward * direction - right * 48.0f, TraceIgnore::Monsters, ent (), &tr); // 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 false; @@ -3140,7 +3141,7 @@ bool Bot::isBlockedRight () { game.testLine (pev->origin, pev->origin + forward * direction + right * 48.0f, TraceIgnore::Monsters, ent (), &tr); // 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 false; @@ -3302,7 +3303,7 @@ int Bot::getNearestToPlantedBomb () { // search the bomb on the map game.searchEntities ("classname", "grenade", [&] (edict_t *ent) { - if (util.isModel (ent, bombModel)) { + if (game.isEntityModelMatches (ent, bombModel)) { result = graph.getNearest (game.getEntityOrigin (ent)); if (graph.exists (result)) { diff --git a/src/planner.cpp b/src/planner.cpp index f98eac6..4c7146f 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -17,12 +17,12 @@ float PlannerHeuristic::gfunctionKillsDist (int team, int currentIndex, int pare if (parentIndex == kInvalidNodeIndex) { return 0.0f; } - auto cost = practice.plannerGetDamage (team, currentIndex, currentIndex, true); + auto cost = practice.getDamageEx (team, currentIndex, currentIndex, true); const auto ¤t = graph[currentIndex]; for (const auto &neighbour : current.links) { if (neighbour.index != kInvalidNodeIndex) { - cost += practice.plannerGetDamage (team, neighbour.index, neighbour.index, false); + cost += practice.getDamageEx (team, neighbour.index, neighbour.index, false); } } @@ -45,12 +45,12 @@ float PlannerHeuristic::gfunctionKillsDistCTWithHostage (int team, int currentIn } 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 ¤t = graph[currentIndex]; for (const auto &neighbour : current.links) { if (neighbour.index != kInvalidNodeIndex) { - cost += practice.plannerGetDamage (team, neighbour.index, neighbour.index, false); + cost += practice.getDamageEx (team, neighbour.index, neighbour.index, false); } } @@ -264,7 +264,7 @@ AStarResult AStarAlgo::find (int botTeam, int srcIndex, int destIndex, NodeAdder auto rsRandomizer = 1.0f; // 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 (botTeam) * 2.0f); } diff --git a/src/practice.cpp b/src/practice.cpp index 4f51797..79bb2ef 100644 --- a/src/practice.cpp +++ b/src/practice.cpp @@ -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 (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 ()) { return 0.0f; } diff --git a/src/support.cpp b/src/support.cpp index 04f0006..ad6e4e3 100644 --- a/src/support.cpp +++ b/src/support.cpp @@ -9,7 +9,6 @@ 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_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."); BotSupport::BotSupport () { @@ -82,13 +81,6 @@ BotSupport::BotSupport () { 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) { if (game.isNullEntity (ent)) { return false; @@ -151,109 +143,6 @@ void BotSupport::decalTrace (TraceResult *trace, int decalIndex) { 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 (); - - // 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 () { // the purpose of this function, is to send quick welcome message, to the listenserver entity. @@ -264,7 +153,7 @@ void BotSupport::checkWelcome () { const bool needToSendMsg = (graph.length () > 0 ? m_needToSendWelcome : true); 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 (); // receive welcome message in four seconds after game has commencing } @@ -344,7 +233,7 @@ bool BotSupport::findNearestPlayer (void **pvHolder, edict_t *to, float searchDi continue; } - if ((sameTeam && client.team != game.getTeam (to)) + if ((sameTeam && client.team != game.getPlayerTeam (to)) || (needAlive && !(client.flags & ClientFlags::Alive)) || (needBot && !bots[client.ent]) || (needDrawn && (client.ent->v.effects & EF_NODRAW)) @@ -373,7 +262,7 @@ void BotSupport::updateClients () { client.ent = player; client.flags |= ClientFlags::Used; - if (isAlive (player)) { + if (game.isAliveEntity (player)) { client.flags |= ClientFlags::Alive; } else { @@ -392,10 +281,6 @@ void BotSupport::updateClients () { } } -bool BotSupport::isModel (const edict_t *ent, StringRef model) { - return model.startsWith (ent->v.model.chars (9)); -} - String BotSupport::getCurrentDateTime () { time_t ticks = time (&ticks); tm timeinfo {}; @@ -409,7 +294,7 @@ String BotSupport::getCurrentDateTime () { } StringRef BotSupport::getFakeSteamId (edict_t *ent) { - if (!cv_enable_fake_steamids || !isPlayer (ent)) { + if (!cv_enable_fake_steamids || !game.isPlayerEntity (ent)) { return "BOT"; } auto botNameHash = StringRef::fnv1a32 (ent->v.netname.chars ()); @@ -479,3 +364,118 @@ void BotSupport::setCustomCvarDescriptions () { }); game.setCvarDescription (cv_restricted_weapons, restrictInfo); } + +bool BotSupport::isLineBlockedBySmoke (const Vector &from, const Vector &to) { + if (!gameState.hasActiveGrenades ()) { + 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 : 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 (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; +} diff --git a/src/tasks.cpp b/src/tasks.cpp index 5dbac9f..b264f94 100644 --- a/src/tasks.cpp +++ b/src/tasks.cpp @@ -28,12 +28,18 @@ void Bot::normal_ () { getTask ()->data = 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 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_checkTerrain = false; @@ -48,7 +54,7 @@ void Bot::normal_ () { // bots rushing with knife, when have no enemy (thanks for idea to nicebot project) if (cv_random_knife_attacks && usesKnife () - && (game.isNullEntity (m_lastEnemy) || !util.isAlive (m_lastEnemy)) + && (game.isNullEntity (m_lastEnemy) || !game.isAliveEntity (m_lastEnemy)) && game.isNullEntity (m_enemy) && m_knifeAttackTime < game.time () && !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 (!m_bombSearchOverridden - && bots.isBombPlanted () + && gameState.isBombPlanted () && m_team == Team::CT && getTask ()->data != kInvalidNodeIndex && !(graph[getTask ()->data].flags & NodeFlag::Goal) @@ -84,7 +90,7 @@ void Bot::normal_ () { // reached the destination (goal) node? if (updateNavigation ()) { // if we're reached the goal, and there is not enemies, notify the team - if (!bots.isBombPlanted () + if (!gameState.isBombPlanted () && m_currentNodeIndex != kInvalidNodeIndex && (m_pathFlags & NodeFlag::Goal) && rg.chance (15) @@ -106,7 +112,7 @@ void Bot::normal_ () { && m_moveSpeed >= getShiftSpeed () && 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); } } @@ -134,7 +140,7 @@ void Bot::normal_ () { } // 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; } @@ -218,7 +224,7 @@ void Bot::normal_ () { } } 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); float campTime = rg (25.0f, 40.0f); @@ -258,7 +264,7 @@ void Bot::normal_ () { auto pathSearchType = m_pathType; // 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; } ensureCurrentNodeIndex (); @@ -280,7 +286,7 @@ void Bot::normal_ () { && (m_heardSoundTime + 6.0f >= game.time () || (m_states & Sense::HearingEnemy)) && numEnemiesNear (pev->origin, 768.0f) >= 1 && !isKnifeMode () - && !bots.isBombPlanted ()) { + && !gameState.isBombPlanted ()) { 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 if (cv_radio_mode.as () > 1 && 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_seeEnemyTime + rg (45.0f, 80.0f) < game.time () && numFriendsNear (pev->origin, 1024.0f) == 0) { @@ -357,7 +363,7 @@ void Bot::huntEnemy_ () { clearTask (Task::Hunt); 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... clearTask (Task::Hunt); @@ -413,7 +419,7 @@ void Bot::huntEnemy_ () { void Bot::seekCover_ () { m_aimFlags |= AimFlags::Nav; - if (!util.isAlive (m_lastEnemy)) { + if (!game.isAliveEntity (m_lastEnemy)) { completeTask (); m_prevGoalIndex = kInvalidNodeIndex; } @@ -565,7 +571,7 @@ void Bot::blind_ () { if (rg.chance (50) && m_difficulty >= Difficulty::Normal && !m_lastEnemyOrigin.empty () - && util.isPlayer (m_lastEnemy) + && game.isPlayerEntity (m_lastEnemy) && !usesSniper ()) { auto error = kSprayDistance * m_lastEnemyOrigin.distance (pev->origin) / 2048.0f; @@ -624,7 +630,7 @@ void Bot::camp_ () { m_checkTerrain = 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; completeTask (); } @@ -644,7 +650,7 @@ void Bot::camp_ () { // random camp dir, or prediction auto useRandomCampDirOrPredictEnemy = [&] () { - if (!m_lastEnemyOrigin.empty () && util.isAlive (m_lastEnemy)) { + if (!m_lastEnemyOrigin.empty () && game.isAliveEntity (m_lastEnemy)) { auto pathLength = m_lastPredictLength; auto predictNode = m_lastPredictIndex; @@ -843,7 +849,7 @@ void Bot::plantBomb_ () { selectWeaponById (Weapon::C4); } - if (util.isAlive (m_enemy) || !m_inBombZone) { + if (game.isAliveEntity (m_enemy) || !m_inBombZone) { completeTask (); } else { @@ -886,7 +892,7 @@ void Bot::plantBomb_ () { void Bot::defuseBomb_ () { const float fullDefuseTime = m_hasDefuser ? 7.0f : 12.0f; - const float timeToBlowUp = getBombTimeleft (); + const float timeToBlowUp = gameState.getBombTimeLeft (); float defuseRemainingTime = fullDefuseTime; @@ -894,7 +900,7 @@ void Bot::defuseBomb_ () { defuseRemainingTime = fullDefuseTime - game.time (); } - const auto &bombPos = graph.getBombOrigin (); + const auto &bombPos = gameState.getBombOrigin (); bool defuseError = false; // exception: bomb has been defused @@ -908,7 +914,7 @@ void Bot::defuseBomb_ () { 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 (timeToBlowUp <= 3.0f) { @@ -1064,7 +1070,7 @@ void Bot::defuseBomb_ () { } void Bot::followUser_ () { - if (game.isNullEntity (m_targetEntity) || !util.isAlive (m_targetEntity)) { + if (game.isNullEntity (m_targetEntity) || !game.isAliveEntity (m_targetEntity)) { m_targetEntity = nullptr; completeTask (); @@ -1075,7 +1081,7 @@ void Bot::followUser_ () { 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); - 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_lastEnemy = tr.pHit; m_lastEnemyOrigin = tr.pHit->v.origin; @@ -1325,7 +1331,7 @@ void Bot::throwSmoke_ () { } void Bot::doublejump_ () { - if (!util.isAlive (m_doubleJumpEntity) + if (!game.isAliveEntity (m_doubleJumpEntity) || (m_aimFlags & AimFlags::Enemy) || (m_travelStartIndex != kInvalidNodeIndex && getTask ()->time + (graph.calculateTravelTime (pev->maxspeed, graph[m_travelStartIndex].origin, m_doubleJumpOrigin) + 11.0f) < game.time ())) { @@ -1399,7 +1405,7 @@ void Bot::doublejump_ () { void Bot::escapeFromBomb_ () { m_aimFlags |= AimFlags::Nav; - if (!bots.isBombPlanted ()) { + if (!gameState.isBombPlanted ()) { completeTask (); } @@ -1407,7 +1413,7 @@ void Bot::escapeFromBomb_ () { 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); } @@ -1432,7 +1438,7 @@ void Bot::escapeFromBomb_ () { float nearestDistanceSq = kInfiniteDistance; 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; } const float distanceSq = pev->origin.distanceSq (path.origin); @@ -1465,7 +1471,7 @@ void Bot::escapeFromBomb_ () { void Bot::shootBreakable_ () { // breakable destroyed? - if (!util.isBreakableEntity (m_breakableEntity)) { + if (!game.isBreakableEntity (m_breakableEntity)) { completeTask (); return; } @@ -1647,7 +1653,7 @@ void Bot::pickupItem_ () { case Pickup::Hostage: m_aimFlags |= AimFlags::Entity; - if (!util.isAlive (m_pickupItem)) { + if (!game.isAliveEntity (m_pickupItem)) { // don't pickup dead hostages m_pickupItem = nullptr; completeTask (); @@ -1676,7 +1682,7 @@ void Bot::pickupItem_ () { // find the nearest 'unused' hostage within the area game.searchEntities (pev->origin, 1024.0f, [&] (edict_t *ent) { - if (!util.isHostageEntity (ent)) { + if (!game.isHostageEntity (ent)) { return EntitySearchResult::Continue; } diff --git a/src/vision.cpp b/src/vision.cpp index d016392..91b91bd 100644 --- a/src/vision.cpp +++ b/src/vision.cpp @@ -445,8 +445,8 @@ void Bot::setAimDirection () { && m_forgetLastVictimTimer.elapsed () && !m_lastEnemyOrigin.empty () - && util.isPlayer (m_lastEnemy) - && !util.isPlayer (m_enemy)) { + && game.isPlayerEntity (m_lastEnemy) + && !game.isPlayerEntity (m_enemy)) { flags |= AimFlags::LastEnemy; } @@ -503,7 +503,7 @@ void Bot::setAimDirection () { else if (flags & AimFlags::PredictPath) { 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; } @@ -573,7 +573,9 @@ void Bot::setAimDirection () { const auto &destOrigin = m_destOrigin + pev->view_ofs; m_lookAt = destOrigin; - if (m_moveToGoal && m_seeEnemyTime + 4.0f < game.time () + const bool horizontalMovement = (m_pathFlags & NodeFlag::Ladder) || isOnLadder (); + + if (!horizontalMovement && m_moveToGoal && m_seeEnemyTime + 4.0f < game.time () && !m_isStuck && !(pev->button & IN_DUCK) && m_currentNodeIndex != kInvalidNodeIndex && !(m_pathFlags & (NodeFlag::Ladder | NodeFlag::Crouch)) @@ -598,7 +600,6 @@ void Bot::setAimDirection () { else { m_lookAt = destOrigin; } - const bool horizontalMovement = (m_pathFlags & NodeFlag::Ladder) || isOnLadder (); if (m_numEnemiesLeft > 0 && m_canChooseAimDirection @@ -625,14 +626,17 @@ void Bot::setAimDirection () { } // try look at next node if on ladder - if (horizontalMovement && m_pathWalk.hasNext ()) { + if (horizontalMovement + && m_pathWalk.hasNext () + && !(m_currentTravelFlags & PathFlag::Jump)) { + const auto &nextPath = graph[m_pathWalk.next ()]; if ((nextPath.flags & NodeFlag::Ladder) - && m_destOrigin.distanceSq (pev->origin) < cr::sqrf (128.0f) - && nextPath.origin.z > m_pathOrigin.z + 26.0f) { + && m_destOrigin.distanceSq (pev->origin) < cr::sqrf (64.0f) + && nextPath.origin.z > m_pathOrigin.z + 30.0f) { - m_lookAt = nextPath.origin + pev->view_ofs; + m_lookAt = nextPath.origin; } } diff --git a/src/vistable.cpp b/src/vistable.cpp index c4e815a..92d6d4f 100644 --- a/src/vistable.cpp +++ b/src/vistable.cpp @@ -125,7 +125,9 @@ void GraphVistable::rebuild () { m_sliceIndex += rg (250, 400); } auto notifyProgress = [] (int value) { - game.print ("Rebuilding vistable... %d%% done.", value); + if (value >= 100 || cv_debug) { + game.print ("Rebuilding vistable... %d%% done.", value); + } }; // notify host about rebuilding @@ -139,6 +141,7 @@ void GraphVistable::rebuild () { m_rebuild = false; m_notifyMsgTimestamp = 0.0f; + m_curIndex = 0; save (); } @@ -149,7 +152,7 @@ void GraphVistable::startRebuild () { 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)) { return false; } From 17ed252b60d26620524a6e66c6a6a010cc964bb2 Mon Sep 17 00:00:00 2001 From: jeefo Date: Wed, 12 Nov 2025 21:31:23 +0300 Subject: [PATCH 03/10] 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) --- ext/crlib | 2 +- inc/config.h | 17 +--- inc/constant.h | 64 ++++++++------- inc/control.h | 18 ++--- inc/engine.h | 14 +++- inc/graph.h | 1 + inc/message.h | 8 +- inc/planner.h | 8 +- inc/storage.h | 3 +- inc/vistable.h | 5 ++ inc/yapb.h | 40 ++++++---- src/analyze.cpp | 7 +- src/botlib.cpp | 84 +++++++------------- src/combat.cpp | 25 +++--- src/config.cpp | 18 ++--- src/control.cpp | 42 ++++++++++ src/engine.cpp | 102 +++++++++++++----------- src/graph.cpp | 41 ++++++---- src/linkage.cpp | 4 + src/manager.cpp | 78 ++++++++++-------- src/navigate.cpp | 201 +++++++++++++++++++++++++---------------------- src/planner.cpp | 4 +- src/storage.cpp | 10 +-- src/support.cpp | 8 +- src/tasks.cpp | 10 +-- src/vision.cpp | 100 ++++++++++++----------- 26 files changed, 506 insertions(+), 408 deletions(-) diff --git a/ext/crlib b/ext/crlib index b5f7ccc..f7b1b02 160000 --- a/ext/crlib +++ b/ext/crlib @@ -1 +1 @@ -Subproject commit b5f7ccc23dec2d018048b40085f78255cc232e52 +Subproject commit f7b1b02a301f900082d2e05ebbbc2d7edc2a4e09 diff --git a/inc/config.h b/inc/config.h index 494d6e7..412c468 100644 --- a/inc/config.h +++ b/inc/config.h @@ -29,16 +29,6 @@ public: // mostly config stuff, and some stuff dealing with menus class BotConfig final : public Singleton { -public: - struct DifficultyData { - float reaction[2] {}; - int32_t headshotPct {}; - int32_t seenThruPct {}; - int32_t hearThruPct {}; - int32_t maxRecoil {}; - Vector aimError {}; - }; - private: Array m_chat {}; Array > m_chatter {}; @@ -52,7 +42,7 @@ private: StringArray m_avatars {}; HashMap > m_language {}; - HashMap m_difficulty {}; + HashMap m_difficulty {}; HashMap m_custom {}; // default tables for personality weapon preferences, overridden by weapon.cfg @@ -218,10 +208,7 @@ public: } // get's the difficulty level tweaks - DifficultyData *getDifficultyTweaks (int32_t level) { - if (level < Difficulty::Noob || level > Difficulty::Expert) { - return &m_difficulty[Difficulty::Expert]; - } + BotDifficultyData *getDifficultyTweaks (int32_t level) { return &m_difficulty[level]; } diff --git a/inc/constant.h b/inc/constant.h index ebc8794..f29975d 100644 --- a/inc/constant.h +++ b/inc/constant.h @@ -69,8 +69,8 @@ CR_DECLARE_SCOPED_ENUM (Menu, // bomb say string CR_DECLARE_SCOPED_ENUM (BombPlantedSay, - ChatSay = cr::bit (1), - Chatter = cr::bit (2) + ChatSay = cr::bit (1), + Chatter = cr::bit (2) ) // chat types id's @@ -419,7 +419,13 @@ CR_DECLARE_SCOPED_ENUM (GoalTactic, 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 { constexpr auto Normal { 35.0f }; constexpr auto Pause { 36.0f }; @@ -447,7 +453,7 @@ constexpr auto kSprayDistanceX2 = kSprayDistance * 2; constexpr auto kMaxChatterRepeatInterval = 99.0f; constexpr auto kViewFrameUpdate = 1.0f / 25.0f; constexpr auto kGrenadeDamageRadius = 385.0f; -constexpr auto kMinMovedDistance = 2.5f; +constexpr auto kMinMovedDistance = cr::sqrf (2.0f); constexpr auto kInfiniteDistanceLong = static_cast (kInfiniteDistance); constexpr auto kMaxWeapons = 32; @@ -462,35 +468,35 @@ constexpr auto kConfigExtension = "cfg"; // weapon masks constexpr auto kPrimaryWeaponMask = (cr::bit (Weapon::XM1014) | - cr::bit (Weapon::M3) | - cr::bit (Weapon::MAC10) | - cr::bit (Weapon::UMP45) | - cr::bit (Weapon::MP5) | - cr::bit (Weapon::TMP) | - cr::bit (Weapon::P90) | - cr::bit (Weapon::AUG) | - cr::bit (Weapon::M4A1) | - cr::bit (Weapon::SG552) | - cr::bit (Weapon::AK47) | - cr::bit (Weapon::Scout) | - cr::bit (Weapon::SG550) | - cr::bit (Weapon::AWP) | - cr::bit (Weapon::G3SG1) | - cr::bit (Weapon::M249) | - cr::bit (Weapon::Famas) | - cr::bit (Weapon::Galil)); + cr::bit (Weapon::M3) | + cr::bit (Weapon::MAC10) | + cr::bit (Weapon::UMP45) | + cr::bit (Weapon::MP5) | + cr::bit (Weapon::TMP) | + cr::bit (Weapon::P90) | + cr::bit (Weapon::AUG) | + cr::bit (Weapon::M4A1) | + cr::bit (Weapon::SG552) | + cr::bit (Weapon::AK47) | + cr::bit (Weapon::Scout) | + cr::bit (Weapon::SG550) | + cr::bit (Weapon::AWP) | + cr::bit (Weapon::G3SG1) | + cr::bit (Weapon::M249) | + cr::bit (Weapon::Famas) | + cr::bit (Weapon::Galil)); constexpr auto kSecondaryWeaponMask = (cr::bit (Weapon::P228) - | cr::bit (Weapon::Elite) - | cr::bit (Weapon::USP) - | cr::bit (Weapon::Glock18) - | cr::bit (Weapon::Deagle) - | cr::bit (Weapon::FiveSeven)); + | cr::bit (Weapon::Elite) + | cr::bit (Weapon::USP) + | cr::bit (Weapon::Glock18) + | cr::bit (Weapon::Deagle) + | cr::bit (Weapon::FiveSeven)); constexpr auto kSniperWeaponMask = (cr::bit (Weapon::Scout) - | cr::bit (Weapon::SG550) - | cr::bit (Weapon::AWP) - | cr::bit (Weapon::G3SG1)); + | cr::bit (Weapon::SG550) + | cr::bit (Weapon::AWP) + | cr::bit (Weapon::G3SG1)); // weapons < 7 are secondary constexpr auto kPrimaryWeaponMinIndex = 7; diff --git a/inc/control.h b/inc/control.h index b11c602..3f12c9e 100644 --- a/inc/control.h +++ b/inc/control.h @@ -23,8 +23,8 @@ CR_DECLARE_SCOPED_ENUM (PrintQueueDestination, // bot command manager class BotControl final : public Singleton { public: - using Handler = int (BotControl::*) (); - using MenuHandler = int (BotControl::*) (int); + using Handler = int (BotControl:: *) (); + using MenuHandler = int (BotControl:: *) (int); public: // generic bot command @@ -36,8 +36,7 @@ public: public: 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 @@ -47,8 +46,7 @@ public: MenuHandler handler {}; 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 @@ -57,10 +55,9 @@ public: String text {}; 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 @@ -118,6 +115,7 @@ private: int cmdNodeSave (); int cmdNodeLoad (); int cmdNodeErase (); + int cmdNodeRefresh (); int cmdNodeEraseTraining (); int cmdNodeDelete (); int cmdNodeCheck (); @@ -255,7 +253,7 @@ public: } } - edict_t *getIssuer() { + edict_t *getIssuer () { return m_ent; } diff --git a/inc/engine.h b/inc/engine.h index 60d67ef..5f883ad 100644 --- a/inc/engine.h +++ b/inc/engine.h @@ -60,7 +60,7 @@ CR_DECLARE_SCOPED_ENUM (MapFlags, Escape = cr::bit (3), KnifeArena = cr::bit (4), FightYard = cr::bit (5), - GrenadeWar = cr::bit(6), + GrenadeWar = cr::bit (6), HasDoors = cr::bit (10), // additional flags HasButtons = cr::bit (11) // map has buttons ) @@ -190,6 +190,9 @@ public: // initialize levels void levelInitialize (edict_t *entities, int max); + // when entity spawns + void onSpawnEntity (edict_t *ent); + // shutdown levels void levelShutdown (); @@ -291,7 +294,7 @@ public: bool isFakeClientEntity (edict_t *ent) const; // check if entity is a player - bool isPlayerEntity (edict_t *ent) const ; + bool isPlayerEntity (edict_t *ent) const; // check if entity is a monster bool isMonsterEntity (edict_t *ent) const; @@ -333,7 +336,10 @@ public: // gets custom engine args for client command 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 @@ -486,7 +492,7 @@ public: // helper to sending the client message void sendClientMessage (bool console, edict_t *ent, StringRef message); - + // helper to sending the server message void sendServerMessage (StringRef message); diff --git a/inc/graph.h b/inc/graph.h index d3e5518..6bd6782 100644 --- a/inc/graph.h +++ b/inc/graph.h @@ -227,6 +227,7 @@ public: bool loadGraphData (); bool canDownload (); bool isAnalyzed () const; + bool isConverted () const; void saveOldFormat (); void reset (); diff --git a/inc/message.h b/inc/message.h index 8b02679..c745784 100644 --- a/inc/message.h +++ b/inc/message.h @@ -69,7 +69,7 @@ CR_DECLARE_SCOPED_ENUM (StatusIconCache, class MessageDispatcher final : public Singleton { private: - using MsgFunc = void (MessageDispatcher::*) (); + using MsgFunc = void (MessageDispatcher:: *) (); using MsgHash = Hash ; private: @@ -81,9 +81,9 @@ private: }; public: - Args (float value) : float_ (value) { } - Args (int32_t value) : long_ (value) { } - Args (const char *value) : chars_ (value) { } + Args (float value) : float_ (value) {} + Args (int32_t value) : long_ (value) {} + Args (const char *value) : chars_ (value) {} }; private: diff --git a/inc/planner.h b/inc/planner.h index e3c2ebf..9128ebb 100644 --- a/inc/planner.h +++ b/inc/planner.h @@ -21,10 +21,10 @@ CR_DECLARE_SCOPED_ENUM (AStarResult, Success = 0, Failed, InternalError, -) + ) -// node added -using NodeAdderFn = const Lambda &; + // node added + using NodeAdderFn = const Lambda &; // route twin node template struct RouteTwin final { @@ -58,7 +58,7 @@ public: static float gfunctionKillsDist (int team, int currentIndex, int parentIndex); // least kills and number of nodes to goal for a team (when with hostage) - static float gfunctionKillsDistCTWithHostage (int team, int currentIndex, int parentIndex); + static float gfunctionKillsDistCTWithHostage (int team, int currentIndex, int parentIndex); // least kills to goal for a team static float gfunctionKills (int team, int currentIndex, int); diff --git a/inc/storage.h b/inc/storage.h index 3cfff02..ca3aba1 100644 --- a/inc/storage.h +++ b/inc/storage.h @@ -22,7 +22,8 @@ CR_DECLARE_SCOPED_ENUM (StorageOption, Official = cr::bit (4), // this is additional flag for graph indicates graph are official Recovered = cr::bit (5), // this is additional flag indicates graph converted from podbot and was bad Exten = cr::bit (6), // this is additional flag indicates that there's extension info - Analyzed = cr::bit (7) // this graph has been analyzed + Analyzed = cr::bit (7), // this graph has been analyzed + Converted = cr::bit (8) // converted from a pwf format ) // storage header versions diff --git a/inc/vistable.h b/inc/vistable.h index 9f5c288..3239ff1 100644 --- a/inc/vistable.h +++ b/inc/vistable.h @@ -54,6 +54,11 @@ public: bool isReady () const { 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 diff --git a/inc/yapb.h b/inc/yapb.h index 8a68bb7..1ba1447 100644 --- a/inc/yapb.h +++ b/inc/yapb.h @@ -23,7 +23,7 @@ using namespace cr; // tasks definition struct BotTask { - using Function = void (Bot::*) (); + using Function = void (Bot:: *) (); public: Function func {}; // corresponding exec function in bot class @@ -34,7 +34,7 @@ public: bool resume {}; // if task can be continued if interrupted 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 @@ -68,7 +68,7 @@ struct WeaponInfo { bool primaryFireHold {}; // hold down primary fire button to use? public: - WeaponInfo (int id, + WeaponInfo (int id, StringRef name, StringRef model, int price, @@ -78,14 +78,13 @@ public: int buyGroup, int buySelect, int buySelectT, - int buySelectCT, + int buySelectCT, int penetratePower, int maxClip, 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), - penetratePower (penetratePower), maxClip (maxClip), type (type), primaryFireHold (fireHold) - { } + penetratePower (penetratePower), maxClip (maxClip), type (type), primaryFireHold (fireHold) {} }; // clients noise @@ -123,6 +122,16 @@ struct BotTeamData { 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 #include @@ -210,6 +219,8 @@ private: mutable Mutex m_pathFindLock {}; mutable Mutex m_predictLock {}; + float f_wpt_tim_str_chg; + private: uint32_t m_states {}; // sensing bitstates uint32_t m_collideMoves[kMaxCollideMoves] {}; // sorted array of movements @@ -240,11 +251,10 @@ private: int m_lastPredictLength {}; // last predicted path length 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_heavyTimestamp {}; // is it time to execute heavy-weight functions 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_timeHitDoor {}; // specific time after hitting the door float m_lastChatTime {}; // time bot last chatted @@ -361,7 +371,6 @@ private: Vector m_lookAtPredict {}; // aiming vector when predicting Vector m_desiredVelocity {}; // desired velocity for jump nodes Vector m_breakableOrigin {}; // origin of breakable - Vector m_rightRef {}; // right referential vector Vector m_checkFallPoint[2] {}; // check fall point Array m_ignoredBreakable {}; // list of ignored breakables @@ -370,6 +379,7 @@ private: UniquePtr m_hitboxEnumerator {}; + BotDifficultyData *m_difficultyData {}; Path *m_path {}; // pointer to the current path node String m_chatBuffer {}; // space for strings (say text...) Frustum::Planes m_viewFrustum {}; @@ -400,7 +410,7 @@ private: int numEnemiesNear (const Vector &origin, const float radius) const; int numFriendsNear (const Vector &origin, const float radius) const; - + float getEstimatedNodeReachTime (); float isInFOV (const Vector &dest) const; float getShiftSpeed (); @@ -529,7 +539,6 @@ private: void syncUpdatePredictedIndex (); void updatePredictedIndex (); void refreshCreatureStatus (char *infobuffer); - void updateRightRef (); void donateC4ToHuman (); void clearAmmoInfo (); void handleChatterTaskChange (Task tid); @@ -681,6 +690,7 @@ public: int m_ammoInClip[kMaxWeapons] {}; // ammo in clip for each weapons int m_ammo[MAX_AMMO_SLOTS] {}; // total ammo amounts int m_deathCount {}; // number of bot deaths + int m_ladderDir {}; // ladder move direction bool m_isVIP {}; // bot is vip? 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_hasProgressBar {}; // has progress bar on a HUD 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_kickedByRotation {}; // is bot kicked due to rotation ? bool m_kickMeFromServer {}; // kick the bot off the server? @@ -768,6 +778,7 @@ public: void startDoubleJump (edict_t *ent); void sendBotToOrigin (const Vector &origin); void markStale (); + void setNewDifficulty (int32_t newDifficulty); bool hasHostage (); bool hasPrimaryWeapon () const; bool hasSecondaryWeapon () const; @@ -792,7 +803,7 @@ public: bool isDucking () const { return !!(pev->flags & FL_DUCKING); } - + Vector getCenter () const { return (pev->absmax + pev->absmin) * 0.5; }; @@ -942,6 +953,7 @@ extern ConVar cv_ignore_enemies_after_spawn_time; extern ConVar cv_camping_time_min; extern ConVar cv_camping_time_max; extern ConVar cv_smoke_grenade_checks; +extern ConVar cv_smoke_greande_checks_radius; extern ConVar cv_check_darkness; extern ConVar cv_use_hitbox_enemy_targeting; extern ConVar cv_restricted_weapons; diff --git a/src/analyze.cpp b/src/analyze.cpp index c366d7d..9a29aec 100644 --- a/src/analyze.cpp +++ b/src/analyze.cpp @@ -130,7 +130,7 @@ void GraphAnalyze::update () { } void GraphAnalyze::suspend () { - m_updateInterval = 0.0f; + m_updateInterval = kInfiniteDistance; m_isAnalyzing = false; m_isAnalyzed = false; m_basicsCreated = false; @@ -325,7 +325,7 @@ void GraphAnalyze::flood (const Vector &pos, const Vector &next, float range) { if (cr::fequal (tr.flFraction, 1.0f)) { 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 targetIndex = graph.getNearestNoBuckets (nextPos, 250.0f); @@ -333,7 +333,7 @@ void GraphAnalyze::flood (const Vector &pos, const Vector &next, float range) { if (graph.exists (endIndex) || !graph.exists (targetIndex)) { return; } - auto targetPos = graph[targetIndex].origin; + const auto &targetPos = graph[targetIndex].origin; // re-check there's nothing nearby, and add something we're want if (!graph.exists (graph.getNearestNoBuckets (nextPos, range))) { @@ -348,6 +348,7 @@ void GraphAnalyze::flood (const Vector &pos, const Vector &next, float range) { if ((graph.isNodeReacheable (targetPos, testPos) && graph.isNodeReacheable (testPos, targetPos)) || (graph.isNodeReacheableWithJump (testPos, targetPos) && graph.isNodeReacheableWithJump (targetPos, testPos))) { + graph.add (NodeAddFlag::Normal, m_isCrouch ? Vector { nextPos.x, nextPos.y, nextPos.z - 9.0f } : nextPos); } } diff --git a/src/botlib.cpp b/src/botlib.cpp index 1a36839..1817ecf 100644 --- a/src/botlib.cpp +++ b/src/botlib.cpp @@ -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_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_smoke_grenade_checks ("smoke_grenade_checks", "1", "Affects the bot's vision by smoke clouds.", true, 0.0f, 2.0f); // game console variables ConVar mp_c4timer ("mp_c4timer", nullptr, Var::GameRef); @@ -100,7 +99,7 @@ void Bot::avoidGrenades () { if (!(m_states & Sense::SeeingEnemy)) { 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); } } @@ -187,6 +186,7 @@ void Bot::checkBreakablesAround () { || !game.hasBreakables () || m_seeEnemyTime + 4.0f > game.time () || !game.isNullEntity (m_enemy) + || (m_aimFlags & (AimFlags::PredictPath | AimFlags::Danger)) || !hasPrimaryWeapon ()) { return; } @@ -320,15 +320,14 @@ void Bot::setIdealReactionTimers (bool actual) { return; // zero out reaction times for extreme mode } - const auto tweak = conf.getDifficultyTweaks (m_difficulty); if (actual) { - m_idealReactionTime = tweak->reaction[0]; - m_actualReactionTime = tweak->reaction[0]; + m_idealReactionTime = m_difficultyData->reaction[0]; + m_actualReactionTime = m_difficultyData->reaction[0]; return; } - m_idealReactionTime = rg (tweak->reaction[0], tweak->reaction[1]); + m_idealReactionTime = rg (m_difficultyData->reaction[0], m_difficultyData->reaction[1]); } void Bot::updatePickups () { @@ -1644,47 +1643,30 @@ void Bot::buyStuff () { } void Bot::updateEmotions () { - // slowly increase/decrease dynamic emotions back to their base level + constexpr auto kEmotionUpdateStep = 0.05f; + if (m_nextEmotionUpdate > game.time ()) { return; } if (m_seeEnemyTime + 1.0f > game.time ()) { - m_agressionLevel += 0.05f; - - if (m_agressionLevel > 1.0f) { - m_agressionLevel = 1.0f; - } + m_agressionLevel = cr::min (m_agressionLevel + kEmotionUpdateStep, 1.0f); } else if (m_seeEnemyTime + 5.0f < game.time ()) { + // smoothly return aggression to base level if (m_agressionLevel > m_baseAgressionLevel) { - m_agressionLevel -= 0.05f; + m_agressionLevel = cr::max (m_agressionLevel - kEmotionUpdateStep, m_baseAgressionLevel); } 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) { - m_fearLevel -= 0.05f; + m_fearLevel = cr::max (m_fearLevel - kEmotionUpdateStep, m_baseFearLevel); } else { - m_fearLevel += 0.05f; - } - - 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_fearLevel = cr::min (m_fearLevel + kEmotionUpdateStep, m_baseFearLevel); } } m_nextEmotionUpdate = game.time () + 0.5f; @@ -1922,7 +1904,6 @@ void Bot::setConditions () { } else if (rg.chance (60)) { if (m_lastVictim->v.weapons & kSniperWeaponMask) { - pushChatterMessage (Chatter::SniperKilled); } else { @@ -2500,7 +2481,7 @@ void Bot::executeChatterFrameEvents () { pushChatterMessage (Chatter::GottaFindC4); bots.clearBombSay (BombPlantedSay::Chatter); } - + } void Bot::checkRadioQueue () { @@ -3101,7 +3082,7 @@ void Bot::frame () { void Bot::update () { const auto tid = getCurrentTaskId (); - m_canChooseAimDirection = true; + m_canSetAimDirection = true; m_isAlive = game.isAliveEntity (ent ()); m_team = game.getPlayerTeam (ent ()); m_healthValue = cr::clamp (pev->health, 0.0f, 99999.9f); @@ -3242,7 +3223,7 @@ void Bot::logicDuringFreezetime () { if (ent) { m_lookAt = ent->v.origin + ent->v.view_ofs; - if (m_buyingFinished) { + if (m_buyingFinished && game.getPlayerTeam (ent) != m_team) { m_enemy = ent; m_enemyOrigin = ent->v.origin; } @@ -3338,12 +3319,11 @@ void Bot::checkSpawnConditions () { void Bot::logic () { // this function gets called each frame and is the core of all bot ai. from here all other subroutines are called - m_movedDistance = kMinMovedDistance + 0.1f; // length of different vector (distance bot moved) - resetMovement (); // increase reaction time m_actualReactionTime += 0.3f; + m_movedDistance = kMinMovedDistance + 0.1f; // length of different vector (distance bot moved) if (m_actualReactionTime > m_idealReactionTime) { m_actualReactionTime = m_idealReactionTime; @@ -3364,13 +3344,11 @@ void Bot::logic () { if (m_prevTime <= game.time ()) { // see how far bot has moved since the previous position... - if (m_checkTerrain) { - m_movedDistance = m_prevOrigin.distance (pev->origin); - } + m_movedDistance = m_prevOrigin.distanceSq (pev->origin); // save current position as previous m_prevOrigin = pev->origin; - m_prevTime = game.time () + (0.15f - m_frameInterval * 2.0f); + m_prevTime = game.time () + 0.2f - m_frameInterval; } // if there's some radio message to respond, check it @@ -3463,7 +3441,7 @@ void Bot::logic () { // ensure we're not stuck picking something 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)) { ensurePickupEntitiesClear (); } @@ -3478,8 +3456,6 @@ void Bot::logic () { // save the previous speed (for checking if stuck) m_prevSpeed = cr::abs (m_moveSpeed); - m_prevVelocity = cr::abs (pev->velocity.length2d ()); - m_lastDamageType = -1; // reset damage } @@ -3643,7 +3619,7 @@ void Bot::showDebugOverlay () { } bool Bot::hasHostage () { - if (cv_ignore_objectives || game.mapIs (MapFlags::Demolition)) { + if (cv_ignore_objectives || !game.mapIs (MapFlags::HostageRescue)) { return false; } @@ -3930,9 +3906,6 @@ void Bot::resetDoubleJump () { } void Bot::debugMsgInternal (StringRef str) { - if (game.isDedicated ()) { - return; - } const int level = cv_debug.as (); if (level <= 2) { @@ -3943,7 +3916,10 @@ void Bot::debugMsgInternal (StringRef str) { 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; } else if (level != 3) { @@ -3954,7 +3930,7 @@ void Bot::debugMsgInternal (StringRef str) { logger.message (printBuf.chars ()); } - if (playMessage) { + if (playMessage && !game.isDedicated ()) { ctrl.msg (printBuf.chars ()); sendToChat (printBuf, false); } @@ -4125,7 +4101,7 @@ void Bot::updateHearing () { if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive) || client.ent == ent () - || client.team == m_team + || client.team2 == m_team || !client.ent || client.noise.last < game.time ()) { @@ -4225,7 +4201,7 @@ void Bot::updateHearing () { else { if (cv_shoots_thru_walls && m_lastEnemy == m_hearedEnemy - && rg.chance (conf.getDifficultyTweaks (m_difficulty)->hearThruPct) + && rg.chance (m_difficultyData->hearThruPct) && m_seeEnemyTime + 3.0f > game.time () && isPenetrableObstacle (m_hearedEnemy->v.origin)) { @@ -4244,7 +4220,7 @@ void Bot::updateHearing () { void Bot::enteredBuyZone (int buyState) { // 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 } const int *econLimit = conf.getEconLimit (); diff --git a/src/combat.cpp b/src/combat.cpp index 64d4b38..f6ea202 100644 --- a/src/combat.cpp +++ b/src/combat.cpp @@ -614,7 +614,7 @@ bool Bot::lookupEnemies () { // if no enemy visible check if last one shoot able through wall if (cv_shoots_thru_walls - && rg.chance (conf.getDifficultyTweaks (m_difficulty)->seenThruPct) + && rg.chance (m_difficultyData->seenThruPct) && isPenetrableObstacle (newEnemy->v.origin)) { m_seeEnemyTime = game.time (); @@ -667,11 +667,11 @@ Vector Bot::getBodyOffsetError (float distance) { const auto &maxs = m_enemy->v.maxs, &mins = m_enemy->v.mins; m_aimLastError = Vector ( - rg (mins.x * hitError, maxs.x * hitError), + rg (mins.x * hitError, maxs.x * hitError), rg (mins.y * hitError, maxs.y * hitError), 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_aimErrorTime = game.time () + rg (0.4f, 0.8f); @@ -722,7 +722,7 @@ Vector Bot::getEnemyBodyOffset () { else if (game.isPlayerEntity (m_enemy)) { // now take in account different parts of enemy 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 if (distance > kSprayDistance && (isRecoilHigh () || usesShotgun ())) { @@ -789,10 +789,10 @@ Vector Bot::getCustomHeight (float distance) const { }; constexpr float kOffsetRanges[9][3] = { - { 0.0f, 0.0f, 0.0f }, // none - { 0.0f, 0.0f, 0.0f }, // melee + { 0.0f, 0.0f, 0.0f }, // none + { 0.0f, 0.0f, 0.0f }, // melee { 0.5f, -0.1f, -1.5f }, // pistol - { 6.5f, 6.0f, -2.0f }, // shotgun + { 6.5f, 6.0f, -2.0f }, // shotgun { 0.5f, -7.5f, -9.5f }, // zoomrifle { 0.5f, -7.5f, -9.5f }, // rifle { 0.5f, -7.5f, -9.5f }, // smg @@ -1014,7 +1014,7 @@ bool Bot::needToPauseFiring (float distance) { const float tolerance = (100.0f - static_cast (m_difficulty) * 25.0f) / 99.0f; const float baseTime = distance > kSprayDistance ? 0.55f : 0.38f; - const float maxRecoil = static_cast (conf.getDifficultyTweaks (m_difficulty)->maxRecoil); + const float maxRecoil = static_cast (m_difficultyData->maxRecoil); // check if we need to compensate recoil if (cr::tanf (cr::sqrtf (cr::abs (xPunch) + cr::abs (yPunch))) * distance > offset + maxRecoil + tolerance) { @@ -1626,9 +1626,9 @@ void Bot::attackMovement () { if (m_difficulty >= Difficulty::Normal && distanceSq < cr::sqrf (kSprayDistance) && (m_jumpTime + 5.0f < game.time () - && isOnFloor () - && rg (0, 1000) < (m_isReloading ? 8 : 2) - && pev->velocity.length2d () > 150.0f) && !usesSniper () && isEnemyCone) { + && isOnFloor () + && rg (0, 1000) < (m_isReloading ? 8 : 2) + && pev->velocity.length2d () > 150.0f) && !usesSniper () && isEnemyCone) { pev->button |= IN_JUMP; } @@ -1646,8 +1646,7 @@ void Bot::attackMovement () { const int enemyNearestIndex = graph.getNearest (m_enemy->v.origin); - if (vistab.visible (m_currentNodeIndex, enemyNearestIndex, VisIndex::Crouch) - && vistab.visible (enemyNearestIndex, m_currentNodeIndex, VisIndex::Crouch)) { + if (vistab.visibleBothSides (m_currentNodeIndex, enemyNearestIndex, VisIndex::Crouch)) { m_duckTime = game.time () + m_frameInterval * 3.0f; } } diff --git a/src/config.cpp b/src/config.cpp index 1989db9..873ccfd 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -46,7 +46,7 @@ void BotConfig::loadMainConfig (bool isFirstLoad) { return false; }; - auto storeVarValue = [] (cvar_t *c, StringRef value) { + auto storeVarValue = [] (cvar_t *c, StringRef value) { auto &cvars = game.getCvars (); for (auto &var : cvars) { @@ -303,9 +303,9 @@ void BotConfig::loadChatterConfig () { { "Chatter_GuardingEscapeZone", Chatter::GuardingEscapeZone, kMaxChatterRepeatInterval }, { "Chatter_GuardingVipSafety", Chatter::GuardingVIPSafety, kMaxChatterRepeatInterval }, { "Chatter_PlantingC4", Chatter::PlantingBomb, 10.0f }, - { "Chatter_InCombat", Chatter::InCombat, kMaxChatterRepeatInterval }, + { "Chatter_InCombat", Chatter::InCombat, kMaxChatterRepeatInterval }, { "Chatter_SeeksEnemy", Chatter::SeekingEnemies, kMaxChatterRepeatInterval }, - { "Chatter_Nothing", Chatter::Nothing, kMaxChatterRepeatInterval }, + { "Chatter_Nothing", Chatter::Nothing, kMaxChatterRepeatInterval }, { "Chatter_EnemyDown", Chatter::EnemyDown, 10.0f }, { "Chatter_UseHostage", Chatter::UsingHostages, kMaxChatterRepeatInterval }, { "Chatter_WonTheRound", Chatter::WonTheRound, kMaxChatterRepeatInterval }, @@ -350,7 +350,7 @@ void BotConfig::loadChatterConfig () { { "Chatter_BombSiteSecured", Chatter::BombsiteSecured, 3.5f }, { "Chatter_GoingToCamp", Chatter::GoingToCamp, 30.0f }, { "Chatter_Camp", Chatter::Camping, 10.0f }, - { "Chatter_OnARoll", Chatter::OnARoll, kMaxChatterRepeatInterval}, + { "Chatter_OnARoll", Chatter::OnARoll, kMaxChatterRepeatInterval }, }; Array missingWaves {}; @@ -632,7 +632,7 @@ void BotConfig::loadDifficultyConfig () { }; m_difficulty[Difficulty::Expert] = { - { 0.1f, 0.2f }, 100, 90, 90, 21, { 0.0f, 0.0f, 0.0f } + { 0.1f, 0.2f }, 100, 90, 90, 21, { 0.0f, 0.0f, 0.0f } }; // currently, mindelay, maxdelay, headprob, seenthruprob, heardthruprob, recoil, aim_error {x,y,z} @@ -721,12 +721,12 @@ void BotConfig::loadCustomConfig () { auto setDefaults = [&] () { m_custom = { - { "C4ModelName", "c4.mdl" }, - { "AMXParachuteCvar", "sv_parachute" }, - { "CustomCSDMSpawnPoint", "view_spawn" }, + { "C4ModelName", "c4.mdl" }, + { "AMXParachuteCvar", "sv_parachute" }, + { "CustomCSDMSpawnPoint", "view_spawn" }, { "CSDMDetectCvar", "csdm_active" }, { "ZMDetectCvar", "zp_delay" }, - { "ZMDelayCvar", "zp_delay" }, + { "ZMDelayCvar", "zp_delay" }, { "ZMInfectedTeam", "T" }, { "EnableFakeBotFeatures", "no" }, { "DisableLogFile", "no" }, diff --git a/src/control.cpp b/src/control.cpp index e39cedc..62761f0 100644 --- a/src/control.cpp +++ b/src/control.cpp @@ -423,6 +423,7 @@ int BotControl::cmdNode () { 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 ("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 addGraphCmd ("path_create", "path_create [noarguments]", "Opens and displays path creation menu.", &BotControl::cmdNodePathCreate); @@ -598,6 +599,13 @@ int BotControl::cmdNodeAddBasic () { int BotControl::cmdNodeSave () { 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 (arg (option) == "nocheck") { graph.saveGraphData (); @@ -629,6 +637,13 @@ int BotControl::cmdNodeSave () { int BotControl::cmdNodeLoad () { 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 if (graph.loadGraphData ()) { msg ("Graph successfully loaded."); @@ -642,6 +657,13 @@ int BotControl::cmdNodeLoad () { int BotControl::cmdNodeErase () { 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 if (arg (iamsure) == "iamsure") { bstor.unlinkFromDisk (false, false); @@ -652,6 +674,26 @@ int BotControl::cmdNodeErase () { 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 (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 () { enum args { graph_cmd = 1, cmd }; diff --git a/src/engine.cpp b/src/engine.cpp index 341939b..9e7fb6f 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -61,9 +61,6 @@ void Game::levelInitialize (edict_t *entities, int max) { // startup threaded worker worker.startup (cv_threadpool_workers.as ()); - m_spawnCount[Team::CT] = 0; - m_spawnCount[Team::Terrorist] = 0; - // clear all breakables before initialization m_breakables.clear (); m_checkedBreakables.clear (); @@ -126,15 +123,11 @@ void Game::levelInitialize (edict_t *entities, int max) { ent->v.rendermode = kRenderTransAlpha; // set its render mode to transparency ent->v.renderamt = 127; // set its transparency amount ent->v.effects |= EF_NODRAW; - - ++m_spawnCount[Team::CT]; } else if (classname == "info_player_deathmatch") { ent->v.rendermode = kRenderTransAlpha; // set its render mode to transparency ent->v.renderamt = 127; // set its transparency amount ent->v.effects |= EF_NODRAW; - - ++m_spawnCount[Team::Terrorist]; } else if (classname == "func_vip_safetyzone" || classname == "info_vip_safetyzone") { m_mapFlags |= MapFlags::Assassination; // assassination map @@ -187,6 +180,24 @@ void Game::levelInitialize (edict_t *entities, int max) { 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 () { // save collected practice on shutdown practice.save (); @@ -223,6 +234,10 @@ void Game::levelShutdown () { // disable command handling 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 { @@ -368,9 +383,9 @@ void Game::playSound (edict_t *ent, const char *sound) { void Game::setPlayerStartDrawModels () { static HashMap models { - {"info_player_start", "models/player/urban/urban.mdl"}, - {"info_player_deathmatch", "models/player/terror/terror.mdl"}, - {"info_vip_start", "models/player/vip/vip.mdl"} + { "info_player_start", "models/player/urban/urban.mdl" }, + { "info_player_deathmatch", "models/player/terror/terror.mdl" }, + { "info_vip_start", "models/player/vip/vip.mdl" } }; models.foreach ([&] (const String &key, const String &val) { @@ -791,53 +806,50 @@ void Game::registerCvars (bool gameVars) { } void Game::constructCSBinaryName (StringArray &libs) { - String libSuffix {}; // construct library suffix + String suffix {}; if (plat.android) { - libSuffix += "_android"; + suffix = "_android"; + if (plat.x64) { + suffix += "_arm64"; + } + else if (plat.arm) { + suffix += "_armv7l"; + } } else if (plat.psvita) { - libSuffix += "_psvita"; + suffix = "_psvita"; } - - if (plat.x64) { + else if (plat.x64) { if (plat.arm) { - libSuffix += "_arm64"; + suffix = "_arm64"; } else if (plat.ppc) { - libSuffix += "_ppc64le"; + suffix = "_ppc64le"; } else { - libSuffix += "_amd64"; + suffix = "_amd64"; } } - else { - if (plat.arm) { - // don't want to put whole build.h logic from xash3d, just set whatever is supported by the YaPB - if (plat.android) { - libSuffix += "_armv7l"; - } - else { - libSuffix += "_armv7hf"; - } - } - else if (!plat.nix && !plat.win && !plat.macos) { - libSuffix += "_i386"; - } + else if (plat.arm) { + // non-android arm32 + suffix = "_armv7hf"; } + else if (!plat.nix && !plat.win && !plat.macos) { + // 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 ()) - libs.insert (0, { "mp", "cs", "cs_i386" }); + // build base names + if (plat.android) { + // only "libcs" with suffix (no "mp", and must have "lib" prefix) + libs.insert (0, "libcs" + suffix); + } else { - // on Android, it's important to have `lib` prefix, otherwise package manager won't unpack the libraries - if (plat.android) - libs.insert (0, { "libcs" }); - else - libs.insert (0, { "mp", "cs" }); - - for (auto &lib : libs) { - lib += libSuffix; - } + // Standard: "mp" and "cs" with suffix + libs.insert (0, "cs" + suffix); + libs.insert (0, "mp" + suffix); } } @@ -881,10 +893,10 @@ bool Game::loadCSBinary () { } 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; // if we can't read file, skip it @@ -940,7 +952,7 @@ bool Game::loadCSBinary () { // no fake pings on xash3d if (!(m_gameFlags & (GameFlags::Xash3D | GameFlags::Xash3DLegacy))) { - m_gameFlags |= GameFlags::HasFakePings; + m_gameFlags |= GameFlags::HasFakePings; } } else { diff --git a/src/graph.cpp b/src/graph.cpp index 3054b5d..11ffc14 100644 --- a/src/graph.cpp +++ b/src/graph.cpp @@ -603,6 +603,10 @@ bool BotGraph::isAnalyzed () const { 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) { if (!hasEditor () && !analyzer.isAnalyzing ()) { return; @@ -1292,6 +1296,7 @@ void BotGraph::showFileInfo () { msg (" uncompressed_size: %dkB", info.uncompressed / 1024); msg (" options: %d", info.options); // 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 (""); @@ -1755,6 +1760,8 @@ bool BotGraph::convertOldFormat () { m_info.author = header.author; + m_info.header.options = StorageOption::Converted; + // clean editor so graph will be saved with header's author auto editor = m_editor; m_editor = nullptr; @@ -1813,9 +1820,10 @@ bool BotGraph::loadGraphData () { if (!modified.empty () && !modified.contains ("(none)")) { m_info.modified.assign (exten.modified); } + vistab.load (); // load/initialize visibility + planner.init (); // initialize our little path planner practice.load (); // load bots practice - vistab.load (); // load/initialize visibility populateNodes (); @@ -1851,7 +1859,7 @@ bool BotGraph::canDownload () { } bool BotGraph::saveGraphData () { - auto options = StorageOption::Graph | StorageOption::Exten; + auto options = m_info.header.options | StorageOption::Graph | StorageOption::Exten; String editorName {}; if (!hasEditor () && !m_info.author.empty ()) { @@ -1907,7 +1915,7 @@ void BotGraph::saveOldFormat () { String editorName {}; - if (!hasEditor () && !m_info.author.empty ()) { + if (!hasEditor () && !m_info.author.empty ()) { editorName = m_info.author; } else if (!game.isNullEntity (m_editor)) { @@ -2296,17 +2304,20 @@ void BotGraph::frame () { if (path.radius > 0.0f) { 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); - game.drawLine (m_editor, origin + Vector (sqr, -sqr, 0.0f), origin + Vector (0.0f, -path.radius, 0.0f), 5, 0, radiusColor, 200, 0, 10); + const Vector points[] = { + { 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); - 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 + 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); + for (auto i = 0; i < kMaxNodeLinks; ++i) { + game.drawLine (m_editor, origin + points[i], origin + points[(i + 1) % 8], 5, 0, radiusColor, 200, 0, 10); + } } else { const float sqr = cr::sqrtf (32.0f); @@ -2390,8 +2401,8 @@ void BotGraph::frame () { message.assignf (" %s node:\n" " Node %d of %d, Radius: %.1f, Light: %s\n" " Flags: %s\n" - " Origin: (%.1f, %.1f, %.1f)\n", - type, node, m_paths.length () - 1, p.radius, + " Origin: (%.1f, %.1f, %.1f)\n", + type, node, m_paths.length () - 1, p.radius, cr::fequal (p.light, kInvalidLightLevel) ? "Invalid" : strings.format ("%1.f", p.light), flags, p.origin.x, p.origin.y, p.origin.z ); diff --git a/src/linkage.cpp b/src/linkage.cpp index 186f576..079e61b 100644 --- a/src/linkage.cpp +++ b/src/linkage.cpp @@ -130,6 +130,9 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) { // precache everything game.precache (); + // notify about entity spawn + game.onSpawnEntity (ent); + if (game.is (GameFlags::Metamod)) { RETURN_META_VALUE (MRES_IGNORED, 0); } @@ -483,6 +486,7 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) { } } } + if (game.is (GameFlags::Metamod)) { RETURN_META (MRES_IGNORED); } diff --git a/src/manager.cpp b/src/manager.cpp index 9602939..2529a0f 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -186,9 +186,9 @@ BotCreateResult BotManager::create (StringRef name, int difficulty, int personal // try to set proffered personality static HashMap personalityMap { - {"normal", Personality::Normal }, - {"careful", Personality::Careful }, - {"rusher", Personality::Rusher }, + { "normal", Personality::Normal }, + { "careful", Personality::Careful }, + { "rusher", Personality::Rusher }, }; // 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 BotRequest request {}; - static StringRef any = "*"; + constexpr StringRef ANY = "*"; - request.name = (name.empty () || name == any) ? StringRef ("\0") : name; - request.difficulty = (difficulty.empty () || difficulty == any) ? -1 : difficulty.as (); - request.team = (team.empty () || team == any) ? -1 : team.as (); - request.skin = (skin.empty () || skin == any) ? -1 : skin.as (); - request.personality = (personality.empty () || personality == any) ? -1 : personality.as (); + auto handleParam = [&ANY] (StringRef value) { + return value.empty () || value == ANY ? -1 : value.as (); + }; + + 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; addbot (request.name, request.difficulty, request.personality, request.team, request.skin, request.manual); @@ -617,7 +621,7 @@ void BotManager::serverFill (int selection, int personality, int difficulty, int } 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; // limit manually added count as well @@ -884,25 +888,25 @@ void BotManager::setWeaponMode (int selection) { selection--; 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, 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, -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, -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, -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, -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, -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, 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] = { - {-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, -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, -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, 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, -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, -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, -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, 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 auto tab = conf.getRawWeapons (); @@ -1080,7 +1084,7 @@ void BotManager::updateBotDifficulties () { // sets new difficulty for all bots for (const auto &bot : m_bots) { - bot->m_difficulty = difficulty; + bot->setNewDifficulty (difficulty); } m_lastDifficulty = difficulty; } @@ -1089,7 +1093,7 @@ void BotManager::updateBotDifficulties () { void BotManager::balanceBotDifficulties () { // difficulty changing once per round (time) auto updateDifficulty = [] (Bot *bot, int32_t offset) { - bot->m_difficulty = cr::clamp (static_cast (bot->m_difficulty + offset), Difficulty::Noob, Difficulty::Expert); + bot->setNewDifficulty (cr::clamp (static_cast (bot->m_difficulty + offset), Difficulty::Noob, Difficulty::Expert)); }; // with nightmare difficulty, there is no balance @@ -1192,7 +1196,7 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int skin) { m_isAlive = false; m_weaponBurstMode = BurstMode::Off; - m_difficulty = cr::clamp (static_cast (difficulty), Difficulty::Noob, Difficulty::Expert); + setNewDifficulty (cr::clamp (static_cast (difficulty), Difficulty::Noob, Difficulty::Expert)); auto minDifficulty = cv_difficulty_min.as (); auto maxDifficulty = cv_difficulty_max.as (); @@ -1202,7 +1206,7 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int skin) { if (maxDifficulty > minDifficulty) { cr::swap (maxDifficulty, minDifficulty); } - m_difficulty = rg (minDifficulty, maxDifficulty); + setNewDifficulty (rg (minDifficulty, maxDifficulty)); } m_pingBase = fakeping.randomBase (); m_ping = fakeping.randomBase (); @@ -1515,14 +1519,13 @@ void Bot::newRound () { m_isLeader = false; m_hasProgressBar = false; - m_canChooseAimDirection = true; + m_canSetAimDirection = true; m_preventFlashing = 0.0f; m_timeTeamOrder = 0.0f; m_askCheckTime = rg (30.0f, 90.0f); m_minSpeed = 260.0f; m_prevSpeed = 0.0f; - m_prevVelocity = 0.0f; m_prevOrigin = Vector (kInfiniteDistance, kInfiniteDistance, kInfiniteDistance); m_prevTime = game.time (); m_lookUpdateTime = game.time (); @@ -1722,9 +1725,6 @@ void Bot::newRound () { m_enemyIgnoreTimer = 0.0f; } - // update refvec for blocked movement - updateRightRef (); - // and put buying into its message queue pushMsgQueue (BotMsg::Buy); startTask (Task::Normal, TaskPri::Normal, kInvalidNodeIndex, 0.0f, true); @@ -1828,6 +1828,16 @@ void Bot::markStale () { pev->flags |= FL_DORMANT; } +void Bot::setNewDifficulty (int32_t newDifficulty) { + if (newDifficulty < Difficulty::Noob || newDifficulty > Difficulty::Expert) { + m_difficulty = Difficulty::Hard; + m_difficultyData = conf.getDifficultyTweaks (Difficulty::Hard); + } + + m_difficulty = newDifficulty; + m_difficultyData = conf.getDifficultyTweaks (newDifficulty); +} + void Bot::updateTeamJoin () { // this function handles the selection of teams & class diff --git a/src/navigate.cpp b/src/navigate.cpp index ad536ec..80abc89 100644 --- a/src/navigate.cpp +++ b/src/navigate.cpp @@ -470,7 +470,7 @@ void Bot::resetCollision () { void Bot::ignoreCollision () { resetCollision (); - m_lastCollTime = game.time () + 0.5f; + m_lastCollTime = game.time () + 0.65f; m_checkTerrain = false; } @@ -602,11 +602,11 @@ void Bot::checkTerrain (const Vector &dirNormal) { const auto tid = getCurrentTaskId (); // 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.50f, 0.75f); // 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 () && tid != Task::Attack && tid != Task::Camp) { @@ -617,7 +617,7 @@ void Bot::checkTerrain (const Vector &dirNormal) { m_firstCollideTime = 0.0f; } // 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_isStuck = true; @@ -653,14 +653,16 @@ void Bot::checkTerrain (const Vector &dirNormal) { resetCollision (); // reset collision memory if not being stuck for 0.5 secs } else { + const auto state = m_collideMoves[m_collStateIndex]; + // 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; } - else if (m_collideMoves[m_collStateIndex] == CollisionState::StrafeLeft) { + else if (state == CollisionState::StrafeLeft) { setStrafeSpeed (dirNormal, -pev->maxspeed); } - else if (m_collideMoves[m_collStateIndex] == CollisionState::StrafeRight) { + else if (state == CollisionState::StrafeRight) { setStrafeSpeed (dirNormal, pev->maxspeed); } } @@ -683,6 +685,7 @@ void Bot::checkTerrain (const Vector &dirNormal) { bits |= CollisionProbe::Duck; } + // collision check allowed if not flying through the air if (isOnFloor () || isOnLadder () || isInWater ()) { uint32_t state[kMaxCollideMoves * 2 + 1] {}; @@ -696,7 +699,6 @@ void Bot::checkTerrain (const Vector &dirNormal) { state[i++] = CollisionState::StrafeRight; state[i++] = CollisionState::Duck; - // now weight all possible states if (bits & CollisionProbe::Jump) { state[i] = 0; @@ -745,7 +747,7 @@ void Bot::checkTerrain (const Vector &dirNormal) { } ++i; - + // now weight all possible states if (bits & CollisionProbe::Strafe) { state[i] = 0; state[i + 1] = 0; @@ -813,6 +815,7 @@ void Bot::checkTerrain (const Vector &dirNormal) { } } + if (bits & CollisionProbe::Duck) { state[i] = 0; @@ -901,7 +904,7 @@ void Bot::checkTerrain (const Vector &dirNormal) { } void Bot::checkFall () { - if (isPreviousLadder () || (m_pathFlags & NodeFlag::Ladder)) { + if (isPreviousLadder () || (m_pathFlags & NodeFlag::Ladder) || isOnLadder ()) { return; } @@ -945,26 +948,28 @@ void Bot::checkFall () { const float nowDistanceSq = pev->origin.distanceSq (m_checkFallPoint[1]); if (nowDistanceSq > baseDistanceSq - && (nowDistanceSq > baseDistanceSq * 1.2f || nowDistanceSq > baseDistanceSq + 200.0f) - && baseDistanceSq >= cr::sqrf (80.0f) && nowDistanceSq >= cr::sqrf (100.0f)) { + && (nowDistanceSq > baseDistanceSq * 1.8f || nowDistanceSq > baseDistanceSq + 260.0f) + && baseDistanceSq >= cr::sqrf (124.0f) && nowDistanceSq >= cr::sqrf (146.0f)) { fixFall = true; } - else if (m_checkFallPoint[1].z > pev->origin.z + 128.0f - && m_checkFallPoint[0].z > pev->origin.z + 128.0f) { + else if (m_checkFallPoint[1].z > pev->origin.z + 138.0f + || m_checkFallPoint[0].z > pev->origin.z + 138.0f) { fixFall = true; } else if (m_currentNodeIndex != kInvalidNodeIndex - && nowDistanceSq > cr::sqrf (16.0f) - && m_checkFallPoint[1].z > pev->origin.z + 62.0f) { + && nowDistanceSq > cr::sqrf (32.0f) + && m_checkFallPoint[1].z > pev->origin.z + 72.0f) { fixFall = true; } if (fixFall) { - m_currentNodeIndex = kInvalidNodeIndex; - findValidNode (); + if (graph.exists (m_currentNodeIndex) && !isReachableNode (m_currentNodeIndex)) { + m_currentNodeIndex = kInvalidNodeIndex; + findValidNode (); - m_fixFallTimer.start (1.0f); + m_fixFallTimer.start (1.0f); + } } } @@ -1000,22 +1005,22 @@ void Bot::moveToGoal () { // press jump button if we need to leave the ladder if (!(m_pathFlags & NodeFlag::Ladder) && isPreviousLadder () - && isOnFloor () && isOnLadder () && m_moveSpeed > 50.0f - && pev->velocity.lengthSq () < 50.0f) { + && pev->velocity.lengthSq () < 50.0f + && m_ladderDir == LadderDir::Down) { pev->button |= IN_JUMP; m_jumpTime = game.time () + 1.0f; } +#if 0 const auto distanceSq2d = m_destOrigin.distanceSq2d (pev->origin + pev->velocity * m_frameInterval); - if (distanceSq2d < cr::sqrf (m_moveSpeed) * m_frameInterval && getTask ()->data != kInvalidNodeIndex) { - m_moveSpeed = distanceSq2d; - } - if (m_moveSpeed > pev->maxspeed) { - m_moveSpeed = pev->maxspeed; + if (distanceSq2d < cr::sqrf (m_moveSpeed * m_frameInterval) && getTask ()->data != kInvalidNodeIndex) { + m_moveSpeed = distanceSq2d * 2.0f; } + m_moveSpeed = cr::clamp (m_moveSpeed, 4.0f, pev->maxspeed); +#endif m_lastUsedNodesTime = game.time (); // special movement for swimming here @@ -1135,10 +1140,11 @@ bool Bot::updateNavigation () { m_jumpFinished = true; m_checkTerrain = false; m_desiredVelocity.clear (); + m_currentTravelFlags &= ~PathFlag::Jump; // cool down a little if next path after current will be jump 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; } } @@ -1149,19 +1155,13 @@ bool Bot::updateNavigation () { } if (m_pathFlags & NodeFlag::Ladder) { - constexpr auto kLadderOffset = Vector { 0.0f, 0.0f, 36.0f }; - 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 - if (graph.exists (prevNodeIndex) && !(graph[prevNodeIndex].flags & NodeFlag::Ladder) && ladderDistance < 64.0f) { - if (m_pathOrigin.z >= pev->origin.z + 16.0f) { - m_pathOrigin = m_path->origin + kLadderOffset; - } - else if (m_pathOrigin.z < pev->origin.z - 16.0f) { - m_pathOrigin = m_path->origin - kLadderOffset; - } + if (graph.exists (prevNodeIndex) + && !(graph[prevNodeIndex].flags & NodeFlag::Ladder) + && ladderDistanceSq < cr::sqrf (64.0f)) { if (!isDucking ()) { m_moveSpeed = pev->maxspeed * 0.4f; @@ -1171,20 +1171,14 @@ bool Bot::updateNavigation () { if (!isOnLadder ()) { pev->button &= ~IN_DUCK; } - m_approachingLadderTimer.start (m_frameInterval * 6.0f); + m_approachingLadderTimer.start (2.0f * m_frameInterval); } if (!isOnLadder () && isOnFloor () && !isDucking ()) { if (!isPreviousLadder ()) { - m_moveSpeed = ladderDistance; - } - - if (m_moveSpeed < 150.0f) { - m_moveSpeed = 150.0f; - } - else if (m_moveSpeed > pev->maxspeed) { - m_moveSpeed = pev->maxspeed; + m_moveSpeed = cr::sqrtf (ladderDistanceSq); } + 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) @@ -1238,7 +1232,7 @@ bool Bot::updateNavigation () { ignoreCollision (); // don't consider being stuck // 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 if (game.is (GameFlags::Xash3D)) { pev->button |= IN_USE; @@ -1252,7 +1246,7 @@ bool Bot::updateNavigation () { // make sure we are always facing the door when going through it m_aimFlags &= ~(AimFlags::LastEnemy | AimFlags::PredictPath); - m_canChooseAimDirection = false; + m_canSetAimDirection = false; // delay task if (m_buttonPushTime < game.time ()) { @@ -1322,26 +1316,35 @@ bool Bot::updateNavigation () { } } - float desiredDistanceSq = cr::sqrf (32.0f); - const float nodeDistanceSq = pev->origin.distanceSq (m_pathOrigin); + float desiredDistanceSq = cr::sqrf (48.0f); + float nodeDistanceSq = pev->origin.distanceSq (m_pathOrigin); // initialize the radius for a special node type, where the node is considered to be reached if (m_pathFlags & NodeFlag::Lift) { desiredDistanceSq = cr::sqrf (50.0f); } 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 - if (game.mapIs (MapFlags::HostageRescue) && (m_pathFlags & NodeFlag::Goal)) { + if (game.mapIs (MapFlags::HostageRescue) + && (m_pathFlags & NodeFlag::Goal)) { + desiredDistanceSq = cr::sqrf (96.0f); } } - else if (m_pathFlags & NodeFlag::Ladder) { - desiredDistanceSq = cr::sqrf (16.0f); + else if (isOnLadder () || (m_pathFlags & NodeFlag::Ladder)) { + desiredDistanceSq = cr::sqrf (15.0f); } else if (m_currentTravelFlags & PathFlag::Jump) { 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 ()) { desiredDistanceSq = 0.0f; @@ -1368,14 +1371,14 @@ bool Bot::updateNavigation () { // if just recalculated path, assume reached current node 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 if (desiredDistanceSq < cr::sqrf (16.0f) && nodeDistanceSq < cr::sqrf (30.0f)) { 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; } } @@ -1410,6 +1413,9 @@ bool Bot::updateNavigation () { // update the practice for team practice.setValue (m_team, m_chosenGoalIndex, m_currentNodeIndex, goalValue); + + // ignore collision + ignoreCollision (); } return true; } @@ -1870,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 - 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) { const auto &path = graph[i]; @@ -2460,7 +2466,7 @@ bool Bot::selectBestNextNode () { const auto currentNodeIndex = m_pathWalk.first (); const auto prevNodeIndex = m_currentNodeIndex; - if (!isOccupiedNode (currentNodeIndex)) { + if (isOnLadder () || !isOccupiedNode (currentNodeIndex)) { return false; } @@ -2590,10 +2596,11 @@ bool Bot::advanceMovement () { bool isCurrentJump = false; // find out about connection flags - if (destIndex != kInvalidNodeIndex && m_currentNodeIndex != kInvalidNodeIndex) { + if (graph.exists (destIndex) && m_path) { for (const auto &link : m_path->links) { if (link.index == destIndex) { m_currentTravelFlags = link.flags; + m_desiredVelocity = link.velocity; m_jumpFinished = false; @@ -2603,7 +2610,7 @@ bool Bot::advanceMovement () { } // 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) { if (link.index == destIndex) { const float diff = cr::abs (m_path->origin.z - graph[destIndex].origin.z); @@ -2666,6 +2673,7 @@ bool Bot::advanceMovement () { // get ladder nodes used by other (first moving) bots for (const auto &other : bots) { + // if another bot uses this ladder, wait 3 secs if (other.get () != this && other->m_isAlive && other->m_currentNodeIndex == destIndex && other->isOnLadder ()) { startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + 3.0f, false); @@ -2725,21 +2733,30 @@ void Bot::setPathOrigin () { else if (radius > 0.0f) { setNonZeroPathOrigin (); } - if (isOnLadder ()) { - TraceResult tr {}; - game.testLine (Vector (pev->origin.x, pev->origin.y, pev->absmin.z), m_pathOrigin, TraceIgnore::Everything, ent (), &tr); + edict_t *ladder = nullptr; - if (tr.flFraction < 1.0f) { - m_pathOrigin = m_pathOrigin + (pev->origin - m_pathOrigin) * 0.5f + Vector (0.0f, 0.0f, 32.0f); + 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 {}; + game.testLine ({ pev->origin.x, pev->origin.y, ladder->v.absmin.z }, m_pathOrigin, TraceIgnore::Monsters, ent (), &tr); + + if (tr.flFraction < 1.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) { // checks if bot is blocked in his movement direction (excluding doors) @@ -2774,12 +2791,12 @@ bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) { constexpr auto kVec00N16 = Vector (0.0f, 0.0f, -16.0f); // right referential vector - updateRightRef (); + auto right = Vector { 0.0f, pev->angles.y, 0.0f }.right (); // bot's head is clear, check at shoulder level... // trace from the bot's shoulder left diagonal forward to the right shoulder... - src = getEyesPos () + kVec00N16 - m_rightRef * -16.0f; - forward = getEyesPos () + kVec00N16 + m_rightRef * 16.0f + normal * 24.0f; + src = getEyesPos () + kVec00N16 - right * -16.0f; + forward = getEyesPos () + kVec00N16 + right * 16.0f + normal * 24.0f; game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr); @@ -2790,8 +2807,8 @@ bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) { // bot's head is clear, check at shoulder level... // trace from the bot's shoulder right diagonal forward to the left shoulder... - src = getEyesPos () + kVec00N16 + m_rightRef * 16.0f; - forward = getEyesPos () + kVec00N16 - m_rightRef * -16.0f + normal * 24.0f; + src = getEyesPos () + kVec00N16 + right * 16.0f; + forward = getEyesPos () + kVec00N16 - right * -16.0f + normal * 24.0f; game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr); @@ -2826,8 +2843,8 @@ bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) { constexpr auto kVec00N24 = Vector (0.0f, 0.0f, -24.0f); // trace from the left waist to the right forward waist pos - src = pev->origin + kVec00N17 - m_rightRef * -16.0f; - forward = pev->origin + kVec00N17 + m_rightRef * 16.0f + normal * 24.0f; + src = pev->origin + kVec00N17 - right * -16.0f; + forward = pev->origin + kVec00N17 + right * 16.0f + normal * 24.0f; // trace from the bot's waist straight forward... game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr); @@ -2838,8 +2855,8 @@ bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) { } // trace from the left waist to the right forward waist pos - src = pev->origin + kVec00N24 + m_rightRef * 16.0f; - forward = pev->origin + kVec00N24 - m_rightRef * -16.0f + normal * 24.0f; + src = pev->origin + kVec00N24 + right * 16.0f; + forward = pev->origin + kVec00N24 - right * -16.0f + normal * 24.0f; game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr); @@ -2918,7 +2935,7 @@ bool Bot::canJumpUp (const Vector &normal) { if (!isOnFloor () && (isOnLadder () || !isInWater ())) { 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... auto src = pev->origin + Vector (0.0f, 0.0f, -36.0f + 45.0f); @@ -2928,7 +2945,7 @@ bool Bot::canJumpUp (const Vector &normal) { game.testLine (src, dest, TraceIgnore::Monsters, ent (), &tr); if (tr.flFraction < 1.0f) { - return doneCanJumpUp (normal, m_rightRef); + return doneCanJumpUp (normal, right); } else { // now trace from jump height upward to check for obstructions... @@ -2943,7 +2960,7 @@ bool Bot::canJumpUp (const Vector &normal) { } // 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; // trace a line forward at maximum jump height... @@ -2951,7 +2968,7 @@ bool Bot::canJumpUp (const Vector &normal) { // if trace hit something, return false if (tr.flFraction < 1.0f) { - return doneCanJumpUp (normal, m_rightRef); + return doneCanJumpUp (normal, right); } // now trace from jump height upward to check for obstructions... @@ -2966,7 +2983,7 @@ bool Bot::canJumpUp (const Vector &normal) { } // 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; // trace a line forward at maximum jump height... @@ -2974,7 +2991,7 @@ bool Bot::canJumpUp (const Vector &normal) { // if trace hit something, return false if (tr.flFraction < 1.0f) { - return doneCanJumpUp (normal, m_rightRef); + return doneCanJumpUp (normal, right); } // now trace from jump height upward to check for obstructions... @@ -3082,10 +3099,10 @@ bool Bot::canDuckUnder (const Vector &normal) { if (tr.flFraction < 1.0f) { return false; } - updateRightRef (); + auto right = Vector { 0.0f, pev->angles.y, 0.0f }.right (); // 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; // trace a line forward at duck height... @@ -3097,7 +3114,7 @@ bool Bot::canDuckUnder (const Vector &normal) { } // 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; // trace a line forward at duck height... @@ -3114,11 +3131,11 @@ bool Bot::isBlockedLeft () { if (m_moveSpeed < 0.0f) { direction = -48.0f; } - Vector right {}, forward {}; - pev->angles.angleVectors (&forward, &right, nullptr); + Vector left {}, forward {}; + pev->angles.angleVectors (&forward, &left, nullptr); // 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... if (game.mapIs (MapFlags::HasDoors) && tr.flFraction < 1.0f && !game.isDoorEntity (tr.pHit)) { @@ -3131,8 +3148,8 @@ bool Bot::isBlockedRight () { TraceResult tr {}; float direction = 48.0f; - if (m_moveSpeed > 0.0f) { - direction = 48.0f; + if (m_moveSpeed < 0.0f) { + direction = -48.0f; } Vector right {}, forward {}; pev->angles.angleVectors (&forward, &right, nullptr); diff --git a/src/planner.cpp b/src/planner.cpp index 4c7146f..64b1e13 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -184,7 +184,7 @@ bool AStarAlgo::cantSkipNode (const int a, const int b, bool 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) { return true; @@ -372,7 +372,7 @@ void FloydWarshallAlgo::syncRebuild () { for (int k = 0; k < m_length; ++k) { for (int i = 0; i < m_length; ++i) { for (int j = 0; j < m_length; ++j) { - int distance = (matrix + (i * m_length) + k)->dist + (matrix + (k * m_length) + j)->dist; + const auto distance = (matrix + (i * m_length) + k)->dist + (matrix + (k * m_length) + j)->dist; if (distance < (matrix + (i * m_length) + j)->dist) { *(matrix + (i * m_length) + j) = { (matrix + (i * m_length) + k)->index, distance }; diff --git a/src/storage.cpp b/src/storage.cpp index 9a2873c..56c8407 100644 --- a/src/storage.cpp +++ b/src/storage.cpp @@ -17,7 +17,7 @@ template bool BotStorage::load (SmallArray &data, ExtenHeader * // graphs can be downloaded... 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 data.clear (); @@ -85,11 +85,7 @@ template bool BotStorage::load (SmallArray &data, ExtenHeader * if (tryReload ()) { return true; } - - if (game.isDeveloperMode ()) { - return error (isGraph, isDebug, file, "Unable to open %s file for reading (filename: '%s').", type.name, filename); - } - return false; + return error (isGraph, isDebug, file, "Unable to open %s file for reading (filename: '%s').", type.name, filename); } // erase the current graph just in case @@ -178,7 +174,7 @@ template bool BotStorage::load (SmallArray &data, ExtenHeader * if (isGraph) { resetRetries (); - ExtenHeader extenHeader; + ExtenHeader extenHeader {}; strings.copy (extenHeader.author, exten->author, cr::bufsize (exten->author)); if (extenSize <= actuallyRead) { diff --git a/src/support.cpp b/src/support.cpp index ad6e4e3..d2819df 100644 --- a/src/support.cpp +++ b/src/support.cpp @@ -11,6 +11,9 @@ ConVar cv_display_welcome_text ("display_welcome_text", "1", "Enables or disable 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_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 () { m_needToSendWelcome = false; m_welcomeReceiveTime = 0.0f; @@ -369,7 +372,6 @@ bool BotSupport::isLineBlockedBySmoke (const Vector &from, const Vector &to) { if (!gameState.hasActiveGrenades ()) { return false; } - constexpr auto kSmokeGrenadeRadius = 115.0f; // distance along line of sight covered by smoke float totalSmokedLength = 0.0f; @@ -397,7 +399,7 @@ bool BotSupport::isLineBlockedBySmoke (const Vector &from, const Vector &to) { continue; } - const float smokeRadiusSq = cr::sqrf (kSmokeGrenadeRadius); + const float smokeRadiusSq = cr::sqrf (cv_smoke_greande_checks_radius.as ()); const Vector &smokeOrigin = game.getEntityOrigin (pent); Vector toGrenade = smokeOrigin - from; @@ -474,7 +476,7 @@ bool BotSupport::isLineBlockedBySmoke (const Vector &from, const Vector &to) { } // define how much smoke a bot can see thru - const float maxSmokedLength = 0.7f * kSmokeGrenadeRadius; + const float maxSmokedLength = 0.7f * cv_smoke_greande_checks_radius.as (); // return true if the total length of smoke-covered line-of-sight is too much return totalSmokedLength > maxSmokedLength; diff --git a/src/tasks.cpp b/src/tasks.cpp index b264f94..3a8d1f8 100644 --- a/src/tasks.cpp +++ b/src/tasks.cpp @@ -21,7 +21,7 @@ void Bot::normal_ () { const int debugGoal = cv_debug_goal.as (); // user forced a node as a goal? - if (debugGoal != kInvalidNodeIndex) { + if (graph.exists (debugGoal)) { if (getTask ()->data != debugGoal) { clearSearchNodes (); @@ -105,7 +105,7 @@ void Bot::normal_ () { // spray logo sometimes if allowed to do so if (!(m_states & (Sense::SeeingEnemy | Sense::SuspectEnemy)) && m_seeEnemyTime + 5.0f < game.time () - && m_reloadState == Reload::None + && m_reloadState == Reload::None && m_timeLogoSpray < game.time () && cv_spraypaints && pev->groundentity == game.getStartEntity () @@ -665,7 +665,7 @@ void Bot::camp_ () { predictNode = findAimingNode (m_lastEnemyOrigin, pathLength); if (isNodeValidForPredict (predictNode) && pathLength > 1 - && vistab.visible ( predictNode, m_currentNodeIndex)) { + && vistab.visible (predictNode, m_currentNodeIndex)) { m_lookAtSafe = graph[predictNode].origin + pev->view_ofs; } @@ -1477,7 +1477,7 @@ void Bot::shootBreakable_ () { } else { 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)) { m_ignoredBreakable.push (tr.pHit); @@ -1507,7 +1507,7 @@ void Bot::shootBreakable_ () { m_shootTime = game.time (); // enforce shooting - if (!usesKnife () && !m_isReloading && !(pev->button & IN_RELOAD) && getAmmoInClip () > 0) { + if (!usesKnife () && !m_isReloading && !(pev->button & IN_RELOAD) && getAmmoInClip () > 0) { if (!(m_oldButtons & IN_ATTACK)) { pev->button |= IN_ATTACK; } diff --git a/src/vision.cpp b/src/vision.cpp index 91b91bd..e10f9b8 100644 --- a/src/vision.cpp +++ b/src/vision.cpp @@ -230,7 +230,11 @@ void Bot::updateLookAngles () { float angleDiffYaw = cr::anglesDifference (direction.y, m_idealAngles.y); // 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 (); if (!cr::fzero (forward)) { @@ -436,7 +440,7 @@ void Bot::setAimDirection () { || (m_currentTravelFlags & PathFlag::Jump)) { flags &= ~(AimFlags::LastEnemy | AimFlags::PredictPath); - m_canChooseAimDirection = false; + m_canSetAimDirection = false; } // don't switch view right away after loosing focus with current enemy @@ -532,7 +536,8 @@ void Bot::setAimDirection () { } 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; } @@ -542,7 +547,8 @@ void Bot::setAimDirection () { return false; } - return isNodeValidForPredict (predictNode) && pathLength < cv_max_nodes_for_predict.as (); + return isNodeValidForPredict (predictNode) && pathLength < cv_max_nodes_for_predict.as () + && numEnemiesNear (graph[predictNode].origin, 1024.0f) > 0; }; if (changePredictedEnemy) { @@ -573,47 +579,21 @@ void Bot::setAimDirection () { const auto &destOrigin = m_destOrigin + pev->view_ofs; m_lookAt = destOrigin; - const bool horizontalMovement = (m_pathFlags & NodeFlag::Ladder) || isOnLadder (); - - if (!horizontalMovement && m_moveToGoal && m_seeEnemyTime + 4.0f < game.time () - && !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 verticalMove = (m_pathFlags & NodeFlag::Ladder) || isOnLadder () || isPreviousLadder () || pev->velocity.z > 16.0f; if (m_numEnemiesLeft > 0 - && m_canChooseAimDirection + && m_canSetAimDirection && m_seeEnemyTime + 4.0f < game.time () - && m_currentNodeIndex != kInvalidNodeIndex - && !horizontalMovement) { + && graph.exists (m_currentNodeIndex) + && !(m_aimFlags & AimFlags::PredictPath)) { const auto dangerIndex = practice.getIndex (m_team, m_currentNodeIndex, m_currentNodeIndex); if (graph.exists (dangerIndex) - && vistab.visible (m_currentNodeIndex, dangerIndex) + && vistab.visibleBothSides (m_currentNodeIndex, dangerIndex) && !(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; } else { @@ -624,24 +604,56 @@ 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 - if (horizontalMovement - && m_pathWalk.hasNext () - && !(m_currentTravelFlags & PathFlag::Jump)) { - + if (verticalMove && m_pathWalk.hasNext ()) { const auto &nextPath = graph[m_pathWalk.next ()]; if ((nextPath.flags & NodeFlag::Ladder) - && m_destOrigin.distanceSq (pev->origin) < cr::sqrf (64.0f) - && nextPath.origin.z > m_pathOrigin.z + 30.0f) { + && m_destOrigin.distanceSq (pev->origin) < cr::sqrf (96.0f) + && nextPath.origin.z > m_pathOrigin.z + 26.0f) { - m_lookAt = nextPath.origin; + m_lookAt = nextPath.origin + pev->view_ofs; } } // 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; } From bc67453b6c13ee5e7b06a802a4e9827d868422ab Mon Sep 17 00:00:00 2001 From: jeefo Date: Thu, 13 Nov 2025 15:41:30 +0300 Subject: [PATCH 04/10] platform: added basic riscv support --- ext/crlib | 2 +- meson.build | 11 +++++++---- src/engine.cpp | 9 ++++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/ext/crlib b/ext/crlib index f7b1b02..173f387 160000 --- a/ext/crlib +++ b/ext/crlib @@ -1 +1 @@ -Subproject commit f7b1b02a301f900082d2e05ebbbc2d7edc2a4e09 +Subproject commit 173f387022310148e1f9ef65d61164579ba509a9 diff --git a/meson.build b/meson.build index 9bbfd8a..5d04f24 100644 --- a/meson.build +++ b/meson.build @@ -37,6 +37,7 @@ os = host_machine.system() cpu = host_machine.cpu_family() cxx = compiler.get_id() build_type = get_option ('buildtype') +cpu_non_x86 = cpu == 'arm' or cpu.startswith('ppc') or cpu.startswith('riscv') opt_64bit = get_option('64bit') opt_native = get_option('native') @@ -111,7 +112,7 @@ if cxx == 'clang' or cxx == 'gcc' '-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' endif @@ -119,7 +120,7 @@ if cxx == 'clang' or cxx == 'gcc' cxxflags += [ '-march=armv8-a+fp+simd', ] - elif cpu != 'arm' and not cpu.startswith('ppc') + elif not cpu_non_x86 if not opt_nosimd cxxflags += [ '-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' ] - 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 cxxflags += [ '-fdata-sections', @@ -203,7 +204,7 @@ if cxx == 'clang' or cxx == 'gcc' endif # 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' ldflags += '-m32' @@ -319,6 +320,8 @@ target_name = meson.project_name() # xash specific postfix for binaries if cpu == 'aarch64' target_name += '_arm64' +elif cpu.startswith('riscv') + target_name += '_riscv64d' elif opt_64bit target_name += '_amd64' endif diff --git a/src/engine.cpp b/src/engine.cpp index 9e7fb6f..0a7c480 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -827,6 +827,9 @@ void Game::constructCSBinaryName (StringArray &libs) { else if (plat.ppc) { suffix = "_ppc64le"; } + else if (plat.riscv) { + suffix = "_riscv64d"; + } else { suffix = "_amd64"; } @@ -844,12 +847,12 @@ void Game::constructCSBinaryName (StringArray &libs) { // build base names if (plat.android) { // only "libcs" with suffix (no "mp", and must have "lib" prefix) - libs.insert (0, "libcs" + suffix); + libs.push ("libcs" + suffix); } else { // Standard: "mp" and "cs" with suffix - libs.insert (0, "cs" + suffix); - libs.insert (0, "mp" + suffix); + libs.push ("cs" + suffix); + libs.push ("mp" + suffix); } } From 6decb2bfb36c70379c94bbe65e880d4420a612e0 Mon Sep 17 00:00:00 2001 From: jeefo Date: Thu, 13 Nov 2025 16:27:32 +0300 Subject: [PATCH 05/10] ci: try to build for riscv64 --- .github/workflows/build.yml | 2 +- package.py | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e83c3df..cd93a11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: strategy: 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 steps: diff --git a/package.py b/package.py index 31b1fe2..2cf8c36 100644 --- a/package.py +++ b/package.py @@ -134,13 +134,14 @@ class BotRelease(object): self.pkg_matrix.append (BotPackage('linux', 'tar.xz', {'linux-x86': 'so'})) self.pkg_matrix.append (BotPackage('extras', 'zip', {'linux-arm64': 'so', - 'linux-amd64': 'so', - 'linux-x86-gcc': 'so', - 'linux-x86-nosimd': 'so', - 'windows-x86-gcc': 'dll', + 'linux-amd64': 'so', + 'linux-riscv64': 'so', + 'linux-x86-gcc': 'so', + 'linux-x86-nosimd': 'so', + 'windows-x86-gcc': 'dll', 'windows-x86-clang': 'dll', 'windows-x86-msvc-xp': 'dll', - 'windows-amd64': 'dll', + 'windows-amd64': 'dll', 'apple-x86': 'dylib', 'apple-arm64': 'dylib', }, extra=True)) @@ -263,7 +264,9 @@ class BotRelease(object): binary_name = binary_name + '_arm64' elif artifact.endswith('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_base = os.path.basename(binary) From cd02ca3a23144c7b1406dcd737ae38e5326e7a28 Mon Sep 17 00:00:00 2001 From: jeefo Date: Sat, 22 Nov 2025 15:14:13 +0300 Subject: [PATCH 06/10] fix: kllstreak counting (resolves #749) --- src/botlib.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/botlib.cpp b/src/botlib.cpp index 1817ecf..c0ab995 100644 --- a/src/botlib.cpp +++ b/src/botlib.cpp @@ -1935,7 +1935,7 @@ void Bot::setConditions () { } } else { - m_killsInterval = m_lastVictimTime - game.time (); + m_killsInterval = game.time () - m_lastVictimTime; if (m_killsInterval <= 5.0f) { ++m_killsCount; From 113cb5e9164fb7d9242343dfffd552b47098c771 Mon Sep 17 00:00:00 2001 From: jeefo Date: Sat, 20 Dec 2025 00:44:21 +0300 Subject: [PATCH 07/10] 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) --- src/botlib.cpp | 6 +++ src/control.cpp | 123 ++++++++++++++++++++++++++++++++++++++++++------ src/engine.cpp | 2 +- src/manager.cpp | 9 ++-- src/tasks.cpp | 3 +- 5 files changed, 123 insertions(+), 20 deletions(-) diff --git a/src/botlib.cpp b/src/botlib.cpp index c0ab995..13f2bda 100644 --- a/src/botlib.cpp +++ b/src/botlib.cpp @@ -154,6 +154,12 @@ void Bot::checkBreakable (edict_t *touch) { if (!game.hasBreakables ()) { 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)) { auto breakable = lookupBreakable (); diff --git a/src/control.cpp b/src/control.cpp index 62761f0..d0ea28e 100644 --- a/src/control.cpp +++ b/src/control.cpp @@ -2193,21 +2193,114 @@ BotControl::BotControl () { m_menuServerFillTeam = 5; 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.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); - m_cmds.emplace ("kill/killbots/killall/kill_ct/kill_t", "kill [team] [silent]", "Kills the specified team / all the bots.", &BotControl::cmdKillBots); - m_cmds.emplace ("fill/fillserver", "fill [team] [count] [difficulty] [personality]", "Fill the server (add bots) with specified parameters.", &BotControl::cmdFill); - m_cmds.emplace ("vote/votemap", "vote [map_id]", "Forces all the bot to vote to specified map.", &BotControl::cmdVote); - m_cmds.emplace ("weapons/weaponmode", "weapons [knife|pistol|shotgun|smg|rifle|sniper|standard]", "Sets the bots weapon mode to use.", &BotControl::cmdWeaponMode); - 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); - m_cmds.emplace ("list/listbots", "list [noarguments]", "Lists the bots currently playing on server.", &BotControl::cmdList); - m_cmds.emplace ("graph/g/w/wp/wpt/waypoint", "graph [help]", "Handles graph operations.", &BotControl::cmdNode); - 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); - m_cmds.emplace ("exec", "exec [user_id] [command]", "Executes a client command on bot entity.", &BotControl::cmdExec); + m_cmds = { + { + "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 + }, + { + "kick/kickone/kick_ct/kick_t/kickbot_ct/kickbot_t", + "kick [team]", + "Kicks off the random bot from the game.", + + &BotControl::cmdKickBot + }, + { + "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 createMenus (); diff --git a/src/engine.cpp b/src/engine.cpp index 0a7c480..6a6c245 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -1472,7 +1472,7 @@ bool Game::isBreakableEntity (edict_t *ent, bool initialSeed) const { const auto limit = cv_breakable_health_limit.as (); // not shoot-able - if (ent->v.health >= limit) { + if (ent->v.health < 5 || ent->v.health >= limit) { return false; } constexpr auto kFuncBreakable = StringRef::fnv1a32 ("func_breakable"); diff --git a/src/manager.cpp b/src/manager.cpp index 2529a0f..862661e 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -1093,6 +1093,7 @@ void BotManager::updateBotDifficulties () { void BotManager::balanceBotDifficulties () { // difficulty changing once per round (time) auto updateDifficulty = [] (Bot *bot, int32_t offset) { + game.print ("offset = %d", offset); bot->setNewDifficulty (cr::clamp (static_cast (bot->m_difficulty + offset), Difficulty::Noob, Difficulty::Expert)); }; @@ -1203,7 +1204,7 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int skin) { // if we're have min/max difficulty specified, choose value from they if (minDifficulty != Difficulty::Invalid && maxDifficulty != Difficulty::Invalid) { - if (maxDifficulty > minDifficulty) { + if (minDifficulty > maxDifficulty) { cr::swap (maxDifficulty, minDifficulty); } setNewDifficulty (rg (minDifficulty, maxDifficulty)); @@ -1830,8 +1831,10 @@ void Bot::markStale () { void Bot::setNewDifficulty (int32_t newDifficulty) { if (newDifficulty < Difficulty::Noob || newDifficulty > Difficulty::Expert) { - m_difficulty = Difficulty::Hard; - m_difficultyData = conf.getDifficultyTweaks (Difficulty::Hard); + const auto difficlutyDefault = Difficulty::Hard;; + + m_difficulty = difficlutyDefault; + m_difficultyData = conf.getDifficultyTweaks (difficlutyDefault); } m_difficulty = newDifficulty; diff --git a/src/tasks.cpp b/src/tasks.cpp index 3a8d1f8..29db80d 100644 --- a/src/tasks.cpp +++ b/src/tasks.cpp @@ -1469,9 +1469,10 @@ void Bot::escapeFromBomb_ () { } void Bot::shootBreakable_ () { + const bool hasEnemy = !game.isNullEntity (m_enemy); // breakable destroyed? - if (!game.isBreakableEntity (m_breakableEntity)) { + if (hasEnemy || !game.isBreakableEntity (m_breakableEntity)) { completeTask (); return; } From 4015f8de06e19e2d9b6a8f78a2e7ecea178e2be3 Mon Sep 17 00:00:00 2001 From: jeefo Date: Sat, 20 Dec 2025 16:00:32 +0300 Subject: [PATCH 08/10] mgr: allow to disable spawn count checks (ref #754) --- cfg/addons/yapb/conf/custom.cfg | 6 ++++++ src/config.cpp | 3 ++- src/manager.cpp | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cfg/addons/yapb/conf/custom.cfg b/cfg/addons/yapb/conf/custom.cfg index 483d9c6..5b42838 100644 --- a/cfg/addons/yapb/conf/custom.cfg +++ b/cfg/addons/yapb/conf/custom.cfg @@ -69,3 +69,9 @@ EnableFakeBotFeatures = no ; sent to the server console. This replaces yb_logger_disable_logfile cvar. ; DisableLogFile = no + +; +; Disables enforcement of player spawn limits during bot creation and quota management, +; allowing the use of custom spawn editors. +; +DisableSpawnControl = no diff --git a/src/config.cpp b/src/config.cpp index 873ccfd..8fe9a1f 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -730,7 +730,8 @@ void BotConfig::loadCustomConfig () { { "ZMInfectedTeam", "T" }, { "EnableFakeBotFeatures", "no" }, { "DisableLogFile", "no" }, - { "CheckConnectivityHost", "yapb.jeefo.net" } + { "CheckConnectivityHost", "yapb.jeefo.net" }, + { "DisableSpawnControl", "no" } }; }; setDefaults (); diff --git a/src/manager.cpp b/src/manager.cpp index 862661e..fef1a37 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -448,6 +448,11 @@ void BotManager::maintainQuota () { maxSpawnCount = maxClients + 1; } + // disable spawn control + if (conf.fetchCustom ("DisableSpawnControl").startsWith ("yes")) { + maxSpawnCount = game.maxClients () + 1; + } + // sent message only to console from here ctrl.setFromConsole (true); From 3ed7a71dd5ff1f52470fd3bb9deedee3439607ed Mon Sep 17 00:00:00 2001 From: markmental Date: Sat, 27 Dec 2025 16:28:31 -0500 Subject: [PATCH 09/10] Fix excessive path penalties --- src/planner.cpp | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/planner.cpp b/src/planner.cpp index 64b1e13..7c0e8a9 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -38,9 +38,13 @@ float PlannerHeuristic::gfunctionKillsDistCTWithHostage (int team, int currentIn if (current.flags & NodeFlag::NoHostage) { return kInfiniteHeuristic; } - else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) { - return gfunctionKillsDist (team, currentIndex, parentIndex) * 500.0f; + else if (current.flags & NodeFlag::Ladder) { + 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); } @@ -69,9 +73,13 @@ auto PlannerHeuristic::gfunctionKillsCTWithHostage (int team, int currentIndex, if (current.flags & NodeFlag::NoHostage) { return kInfiniteHeuristic; } - else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) { - return gfunctionKills (team, currentIndex, parentIndex) * 500.0f; + else if (current.flags & NodeFlag::Ladder) { + 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); } @@ -103,9 +111,13 @@ float PlannerHeuristic::gfunctionPathDistWithHostage (int, int currentIndex, int if (current.flags & NodeFlag::NoHostage) { return kInfiniteHeuristic; } - else if (current.flags & (NodeFlag::Crouch | NodeFlag::Ladder)) { - return gfunctionPathDist (Team::Unassigned, currentIndex, parentIndex) * 500.0f; + else if (current.flags & NodeFlag::Ladder) { + 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); } @@ -203,7 +215,7 @@ bool AStarAlgo::cantSkipNode (const int a, const int b, bool skipVisCheck) { const float distanceSq = ag.origin.distanceSq (bg.origin); 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) { return true; From 95f1261dda406c6c89f7e9d993980f1547b811f1 Mon Sep 17 00:00:00 2001 From: markmental Date: Sat, 27 Dec 2025 18:25:40 -0500 Subject: [PATCH 10/10] Adjust Newbie aim spring to reduce snap headshots --- src/vision.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/vision.cpp b/src/vision.cpp index e10f9b8..787c00a 100644 --- a/src/vision.cpp +++ b/src/vision.cpp @@ -278,8 +278,8 @@ void Bot::updateLookAngles () { } void Bot::updateLookAnglesNewbie (const Vector &direction, float delta) { - Vector spring { 13.0f, 13.0f, 0.0f }; - Vector damperCoefficient { 0.22f, 0.22f, 0.0f }; + Vector spring { 13.0f, 9.0f, 0.0f }; + Vector damperCoefficient { 0.28f, 0.26f, 0.0f }; const float offset = cr::clamp (static_cast (m_difficulty), 1.0f, 4.0f) * 25.0f; @@ -296,10 +296,14 @@ void Bot::updateLookAnglesNewbie (const Vector &direction, float delta) { if (m_aimFlags & (AimFlags::Enemy | AimFlags::Entity)) { 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 { // is it time for bot to randomize the aim direction again (more often where moving) ? if (m_randomizeAnglesTime < game.time () @@ -443,7 +447,7 @@ void Bot::setAimDirection () { 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 () || m_seeEnemyTime + rg (1.0f, 1.25f) > game.time ()) @@ -670,3 +674,4 @@ void Bot::setAimDirection () { m_lookAt = m_destOrigin; } } +