From 4fb78d4d12559ec333a64989e3a2cf46937d8a68 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 21 Nov 2025 18:49:23 -0700 Subject: [PATCH 1/7] Bots follow you when you press +use on them --- src/game/client/neo/c_neo_player.cpp | 2 + src/game/client/neo/c_neo_player.h | 3 + .../client/neo/ui/neo_hud_round_state.cpp | 109 +++-- src/game/client/neo/ui/neo_hud_round_state.h | 2 +- src/game/server/CMakeLists.txt | 4 + .../bot/behavior/neo_bot_command_follow.cpp | 374 ++++++++++++++++++ .../neo/bot/behavior/neo_bot_command_follow.h | 34 ++ .../server/neo/bot/behavior/neo_bot_pause.cpp | 61 +++ .../server/neo/bot/behavior/neo_bot_pause.h | 19 + .../bot/behavior/neo_bot_tactical_monitor.cpp | 13 + src/game/server/neo/neo_player.cpp | 131 +++++- src/game/server/neo/neo_player.h | 10 + src/game/shared/neo/neo_player_shared.cpp | 37 ++ src/game/shared/neo/neo_player_shared.h | 1 + 14 files changed, 755 insertions(+), 45 deletions(-) create mode 100644 src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_command_follow.h create mode 100644 src/game/server/neo/bot/behavior/neo_bot_pause.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_pause.h diff --git a/src/game/client/neo/c_neo_player.cpp b/src/game/client/neo/c_neo_player.cpp index 19a63a3b66..f610ee76a3 100644 --- a/src/game/client/neo/c_neo_player.cpp +++ b/src/game/client/neo/c_neo_player.cpp @@ -83,6 +83,7 @@ IMPLEMENT_CLIENTCLASS_DT(C_NEO_Player, DT_NEO_Player, CNEO_Player) RecvPropInt(RECVINFO(m_iNextSpawnClassChoice)), RecvPropInt(RECVINFO(m_bInLean)), RecvPropEHandle(RECVINFO(m_hServerRagdoll)), + RecvPropEHandle(RECVINFO(m_hCommandingPlayer)), RecvPropBool(RECVINFO(m_bInThermOpticCamo)), RecvPropBool(RECVINFO(m_bLastTickInThermOpticCamo)), @@ -100,6 +101,7 @@ IMPLEMENT_CLIENTCLASS_DT(C_NEO_Player, DT_NEO_Player, CNEO_Player) RecvPropArray(RecvPropInt(RECVINFO(m_rfAttackersScores[0])), m_rfAttackersScores), RecvPropArray(RecvPropFloat(RECVINFO(m_rfAttackersAccumlator[0])), m_rfAttackersAccumlator), RecvPropArray(RecvPropInt(RECVINFO(m_rfAttackersHits[0])), m_rfAttackersHits), + RecvPropArray(RecvPropVector(RECVINFO(m_vLastPingByStar[0])), m_vLastPingByStar), RecvPropInt(RECVINFO(m_NeoFlags)), RecvPropString(RECVINFO(m_szNeoName)), diff --git a/src/game/client/neo/c_neo_player.h b/src/game/client/neo/c_neo_player.h index 0a10b14cda..8f52920761 100644 --- a/src/game/client/neo/c_neo_player.h +++ b/src/game/client/neo/c_neo_player.h @@ -217,6 +217,8 @@ class C_NEO_Player : public C_HL2MP_Player CNetworkVar(float, m_flNextPingTime); + CNetworkArray(Vector, m_vLastPingByStar, STAR__TOTAL); + CNetworkVar(bool, m_bInThermOpticCamo); CNetworkVar(bool, m_bLastTickInThermOpticCamo); CNetworkVar(bool, m_bInVision); @@ -225,6 +227,7 @@ class C_NEO_Player : public C_HL2MP_Player CNetworkVar(bool, m_bCarryingGhost); CNetworkVar(bool, m_bIneligibleForLoadoutPick); CNetworkHandle(CBaseEntity, m_hServerRagdoll); + CNetworkHandle(CBasePlayer, m_hCommandingPlayer); CNetworkVar(int, m_iNeoClass); CNetworkVar(int, m_iNeoSkin); diff --git a/src/game/client/neo/ui/neo_hud_round_state.cpp b/src/game/client/neo/ui/neo_hud_round_state.cpp index 3649b9e23a..6140c9bb33 100644 --- a/src/game/client/neo/ui/neo_hud_round_state.cpp +++ b/src/game/client/neo/ui/neo_hud_round_state.cpp @@ -634,45 +634,15 @@ void CNEOHud_RoundState::DrawPlayerList() const bool hideDueToScoreboard = cl_neo_hud_scoreboard_hide_others.GetBool() && g_pNeoScoreBoard->IsVisible(); // Draw squad mates - if (!localPlayerSpec && g_PR->GetStar(localPlayerIndex) != 0 && !hideDueToScoreboard) - { - bool squadMateFound = false; - - for (int i = 0; i < (MAX_PLAYERS + 1); i++) - { - if (i == localPlayerIndex) - { - continue; - } - if (!g_PR->IsConnected(i)) - { - continue; - } - const int playerTeam = g_PR->GetTeam(i); - if (playerTeam != localPlayerTeam) - { - continue; - } - const bool isSameSquad = g_PR->GetStar(i) == g_PR->GetStar(localPlayerIndex); - if (!isSameSquad) - { - continue; - } - - offset = DrawPlayerRow(i, offset); - squadMateFound = true; - } - - if (squadMateFound) - { - offset += 12; - } - } - + CUtlVector commandedList; + CUtlVector nonCommandedList; + CUtlVector nonSquadList; + bool squadMateFound = false; m_iLeftPlayersAlive = 0; m_iRightPlayersAlive = 0; + const int localStar = g_PR->GetStar(localPlayerIndex); - // Draw other team mates + // Single pass to collect and categorize players for (int i = 0; i < (MAX_PLAYERS + 1); i++) { if (!g_PR->IsConnected(i)) @@ -697,22 +667,62 @@ void CNEOHud_RoundState::DrawPlayerList() { continue; } - if (i == localPlayerIndex || localPlayerSpec || hideDueToScoreboard) + if (i == localPlayerIndex || localPlayerSpec || hideDueToScoreboard || !ArePlayersOnSameTeam(i, localPlayerIndex)) { continue; } - const bool isSameSquad = g_PR->GetStar(i) == g_PR->GetStar(localPlayerIndex); - if (isSameSquad) + + // Only consider players in the same squad star as the local player + const bool isSameSquadStar = (localStar != STAR_NONE) && (g_PR->GetStar(i) == localStar); + if (isSameSquadStar) { - continue; + C_NEO_Player* pPlayer = ToNEOPlayer(UTIL_PlayerByIndex(i)); + bool isCommanded = (pPlayer && pPlayer->m_hCommandingPlayer.Get() == C_NEO_Player::GetLocalNEOPlayer()); + if (isCommanded) + { + commandedList.AddToTail(i); + } + else + { + nonCommandedList.AddToTail(i); + } + squadMateFound = true; + } + else + { + nonSquadList.AddToTail(i); } + } + + // Draw commanded players first + const auto player = static_cast(UTIL_PlayerByIndex(localPlayerIndex)); + TeamLogoColor* pTeamLogoColor = player ? &m_teamLogoColors[player->GetTeamNumber()] : nullptr; + const Color* colorOverride = pTeamLogoColor ? &pTeamLogoColor->color : nullptr; + for (int i = 0; i < commandedList.Count(); ++i) + { + offset = DrawPlayerRow(commandedList[i], offset, false, colorOverride); + } - offset = DrawPlayerRow(i, offset, true); + // Draw non-commanded players (who are also in the same squad star) + for (int i = 0; i < nonCommandedList.Count(); ++i) + { + offset = DrawPlayerRow(nonCommandedList[i], offset, false); + } + + if (squadMateFound) + { + offset += 12; + } + + // Draw other team mates + for (int i = 0; i < nonSquadList.Count(); ++i) + { + offset = DrawPlayerRow(nonSquadList[i], offset, true); } } } -int CNEOHud_RoundState::DrawPlayerRow(int playerIndex, const int yOffset, bool small) +int CNEOHud_RoundState::DrawPlayerRow(int playerIndex, const int yOffset, bool small, const Color* colorOverride) { // Draw player static constexpr int SQUAD_MATE_TEXT_LENGTH = 62; // 31 characters in name without end character max plus 3 in short rank name plus 7 max in class name plus 3 max in health plus other characters @@ -760,7 +770,8 @@ int CNEOHud_RoundState::DrawPlayerRow(int playerIndex, const int yOffset, bool s int fontWidth, fontHeight; surface()->DrawSetTextFont(small ? m_hOCRSmallerFont : m_hOCRSmallFont); surface()->GetTextSize(m_hOCRSmallFont, m_wszPlayersAliveUnicode, fontWidth, fontHeight); - surface()->DrawSetTextColor(isAlive ? COLOR_FADED_WHITE : COLOR_DARK_FADED_WHITE); + + surface()->DrawSetTextColor(colorOverride ? *colorOverride : (isAlive ? COLOR_FADED_WHITE : COLOR_DARK_FADED_WHITE)); surface()->DrawSetTextPos(8, yOffset); surface()->DrawPrintText(wSquadMateText, V_wcslen(wSquadMateText)); @@ -792,6 +803,18 @@ void CNEOHud_RoundState::DrawPlayer(int playerIndex, int teamIndex, const TeamLo surface()->DrawSetColor(COLOR_DARK); surface()->DrawTexturedRect(xOffset, Y_POS + 1, xOffset + m_ilogoSize, Y_POS + m_ilogoSize + 1); + // Draw Command Highlight Border on top of the avatar image + C_NEO_Player* pPlayer = static_cast(UTIL_PlayerByIndex(playerIndex)); + if (pPlayer && pPlayer->m_hCommandingPlayer.Get() == C_NEO_Player::GetLocalNEOPlayer()) + { + surface()->DrawSetColor(COLOR_WHITE); + // Draw a thicker border inwards + for (int borderIndex = 0; borderIndex < 3; ++borderIndex) + { + surface()->DrawOutlinedRect(xOffset + borderIndex, Y_POS + borderIndex, xOffset + m_ilogoSize - borderIndex, Y_POS + m_ilogoSize - borderIndex); + } + } + // Deathmatch only: Draw XP on everyone if (!NEORules()->IsTeamplay()) { diff --git a/src/game/client/neo/ui/neo_hud_round_state.h b/src/game/client/neo/ui/neo_hud_round_state.h index b1aee9e3d9..656149a9d2 100644 --- a/src/game/client/neo/ui/neo_hud_round_state.h +++ b/src/game/client/neo/ui/neo_hud_round_state.h @@ -44,7 +44,7 @@ class CNEOHud_RoundState : public CNEOHud_ChildElement, public CHudElement, publ void CheckActiveStar(); void DrawPlayerList(); - int DrawPlayerRow(int playerIndex, int yOffset, bool small = false); + int DrawPlayerRow(int playerIndex, int yOffset, bool small = false, const Color* highlightColor = nullptr); void DrawPlayer(int playerIndex, int teamIndex, const TeamLogoColor &teamLogoColor, const int xOffset, const bool drawHealthClass); void SetTextureToAvatar(int playerIndex); diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index b7e4b3c741..386a7225e7 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1436,6 +1436,8 @@ target_sources_grouped( neo/bot/behavior/neo_bot_attack.h neo/bot/behavior/neo_bot_behavior.cpp neo/bot/behavior/neo_bot_behavior.h + neo/bot/behavior/neo_bot_command_follow.cpp + neo/bot/behavior/neo_bot_command_follow.h neo/bot/behavior/neo_bot_dead.cpp neo/bot/behavior/neo_bot_dead.h neo/bot/behavior/neo_bot_jgr_capture.cpp @@ -1452,6 +1454,8 @@ target_sources_grouped( neo/bot/behavior/neo_bot_melee_attack.h neo/bot/behavior/neo_bot_move_to_vantage_point.cpp neo/bot/behavior/neo_bot_move_to_vantage_point.h + neo/bot/behavior/neo_bot_pause.cpp + neo/bot/behavior/neo_bot_pause.h neo/bot/behavior/neo_bot_retreat_to_cover.cpp neo/bot/behavior/neo_bot_retreat_to_cover.h neo/bot/behavior/neo_bot_retreat_from_grenade.cpp diff --git a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp new file mode 100644 index 0000000000..b417436222 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp @@ -0,0 +1,374 @@ +#include "cbase.h" +#include "neo_player.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_command_follow.h" +#include "nav_mesh.h" + + +ConVar sv_neo_bot_cmdr_debug_pause_uncommanded("sv_neo_bot_cmdr_debug_pause_uncommanded", "0", + FCVAR_CHEAT, "If true, uncommanded bots in behavior CNEOBotSeekAndDestroy will pause.", false, 0, false, 1); + +// NEOTODO: Figure out a clean way to represent config distances as Hammer units, without needing to square on use +ConVar sv_neo_bot_cmdr_stop_distance_sq("sv_neo_bot_cmdr_stop_distance_sq", "5000", + FCVAR_NONE, "Minimum distance gap between following bots", true, 3000, true, 100000); + +ConVar sv_neo_bot_cmdr_look_weights_friendly_repulsion("sv_neo_bot_cmdr_look_weights_friendly_repulsion", "2", + FCVAR_NONE, "Weight for friendly bot repulsion force", true, 1, true, 9999); +ConVar sv_neo_bot_cmdr_look_weights_wall_repulsion("sv_neo_bot_cmdr_look_weights_wall_repulsion", "3", + FCVAR_NONE, "Weight for wall repulsion force", true, 1, true, 9999); +ConVar sv_neo_bot_cmdr_look_weights_explosives_repulsion("sv_neo_bot_cmdr_look_weights_explosives_repulsion", "4", + FCVAR_NONE, "Weight for explosive repulsion force", true, 1, true, 9999); + +ConVar sv_neo_bot_cmdr_look_weights_friendly_max_dist("sv_neo_bot_cmdr_look_weights_friendly_max_dist", "5000", + FCVAR_NONE, "Distance to compare friendly repulsion forces", true, 1, true, 100000); +ConVar sv_neo_bot_cmdr_look_weights_wall_repulsion_whisker_dist("sv_neo_bot_cmdr_look_weights_wall_repulsion_whisker_dist", "500", + FCVAR_NONE, "Distance to extend whiskers", true, 1, true, 100000); + +//--------------------------------------------------------------------------------------------- +CNEOBotCommandFollow::CNEOBotCommandFollow() +{ +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCommandFollow::OnStart(CNEOBot *me, Action< CNEOBot > *priorAction) +{ + m_path.SetMinLookAheadDistance(me->GetDesiredPathLookAheadRange()); + + if (!FollowCommandChain(me)) + { + return Done("No commander to follow"); + } + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCommandFollow::Update(CNEOBot *me, float interval) +{ + if (!FollowCommandChain(me)) + { + return Done("Lost commander or released"); + } + + m_path.Update(me); + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCommandFollow::OnResume(CNEOBot *me, Action< CNEOBot > *interruptingAction) +{ + if (!FollowCommandChain(me)) + { + return Done("No commander to follow on resume"); + } + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +void CNEOBotCommandFollow::OnEnd(CNEOBot *me, Action< CNEOBot > *nextAction) +{ + me->m_hLeadingPlayer = nullptr; + me->m_hCommandingPlayer = nullptr; +} + + +// --------------------------------------------------------------------------------------------- +// Process commander ping waypoint commands for bots +// true - order has been processed, continue following +// false - no order, stop following +bool CNEOBotCommandFollow::FollowCommandChain(CNEOBot* me) +{ + Assert(me); + CNEO_Player* pCommander = me->m_hCommandingPlayer.Get(); + + if (!pCommander) + { + return false; + } + + // Mirror behavior of leader if we have one + if (me->m_hLeadingPlayer.Get()) + { + CNEO_Player *pPlayerToMirror = me->m_hLeadingPlayer.Get(); + if (pPlayerToMirror) + { + if (pPlayerToMirror->GetInThermOpticCamo()) + { + me->EnableCloak(4.0f); + } + else + { + me->DisableCloak(); + } + + if (pPlayerToMirror->IsDucking()) + { + me->PressCrouchButton(0.5f); + } + else + { + me->ReleaseCrouchButton(); + } + } + } + + // Calibrate dynamic follow distance + float follow_stop_distance_sq = sv_neo_bot_cmdr_stop_distance_sq.GetFloat(); + if (pCommander->m_flBotDynamicFollowDistanceSq > follow_stop_distance_sq) + { + follow_stop_distance_sq = pCommander->m_flBotDynamicFollowDistanceSq; + } + + if (pCommander->IsAlive()) + { + // Follow commander if they are close enough to collect you (and ping cooldown elapsed) + if (pCommander->m_tBotPlayerPingCooldown.IsElapsed() + && (me->GetStar() == pCommander->GetStar()) // only follow when commander is focused on your squad + && (me->GetAbsOrigin().DistToSqr(pCommander->GetAbsOrigin()) < sv_neo_bot_cmdr_stop_distance_sq.GetFloat())) + { + // Use sv_neo_bot_cmdr_stop_distance_sq for consistent bot collection range + // follow_stop_distance_sq would be confusing if player doesn't know about distance tuning + me->m_hLeadingPlayer = pCommander; + m_vGoalPos = vec3_origin; + pCommander->m_vLastPingByStar.GetForModify(me->GetStar()) = vec3_origin; + } + // Go to commander's ping + else if (pCommander->m_vLastPingByStar.Get(me->GetStar()) != vec3_origin) + { + // Check if there's been an update for this star's ping waypoint + if (pCommander->m_vLastPingByStar.Get(me->GetStar()) != me->m_vLastPingByStar.Get(me->GetStar())) + { + me->m_hLeadingPlayer = nullptr; // Stop following and start travelling to ping + m_vGoalPos = pCommander->m_vLastPingByStar.Get(me->GetStar()); + me->m_vLastPingByStar.GetForModify(me->GetStar()) = pCommander->m_vLastPingByStar.Get(me->GetStar()); + + // Force a repath to allow for fine tuned positioning + CNEOBotPathCost cost(me, DEFAULT_ROUTE); + if (m_path.Compute(me, m_vGoalPos, cost, 0.0f, true, true) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH) + { + return true; + } + else + { + me->m_hLeadingPlayer = pCommander; // fallback to following commander + // continue with leader following logic below + } + } + + if (FanOutAndCover(me, m_vGoalPos)) + { + // FanOutAndCover true: arrived at destination and settled, so don't recompute path + return true; + } + + CNEOBotPathCost cost(me, DEFAULT_ROUTE); + if (m_path.Compute(me, m_vGoalPos, cost, 0.0f, true, true) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH) + { + return true; + } + else + { + me->m_hLeadingPlayer = pCommander; // fallback to following commander + // continue with leader following logic below + } + } + } + else + { + // Commander died + return false; // This triggers OnEnd which clears vars + } + + // Didn't have order from commander, so follow in snake formation + CNEO_Player* pLeader = me->m_hLeadingPlayer.Get(); + if (pLeader && pLeader->IsAlive()) + { + // Commander can swap and order around different squads while having followers + // but otherwise bots only follow squadmates in the same star or their commander + if ((pLeader != pCommander) && (pLeader->GetStar() != me->GetStar())) + { + me->m_hLeadingPlayer = nullptr; + return true; // restart logic next tick + } + else if (me->GetAbsOrigin().DistToSqr(pLeader->GetAbsOrigin()) < follow_stop_distance_sq) + { + // Anti-collision: follow neighbor in snake chain + for (int idx = 1; idx <= gpGlobals->maxClients; ++idx) + { + CNEO_Player* pOther = static_cast(UTIL_PlayerByIndex(idx)); + if (!pOther || !pOther->IsBot() || pOther == me + || (pOther->m_hLeadingPlayer.Get() != me->m_hLeadingPlayer.Get())) + { + // Not another bot in the same snake formation + continue; + } + + if (me->GetAbsOrigin().DistToSqr(pOther->GetAbsOrigin()) < follow_stop_distance_sq / 2) + { + // Follow person I bumped into and link up to snake chain of followers + me->m_hLeadingPlayer = pOther; + break; + } + } + + m_hTargetEntity = NULL; + m_bGoingToTargetEntity = false; + m_path.Invalidate(); + Vector tempLeaderOrigin = pLeader->GetAbsOrigin(); // don't want to override m_vGoalPos + FanOutAndCover(me, tempLeaderOrigin, false, follow_stop_distance_sq); + return true; + } + + // Set the bot's goal to the leader's position. + m_vGoalPos = pLeader->GetAbsOrigin(); + CNEOBotPathCost cost(me, DEFAULT_ROUTE); + if (m_path.Compute(me, m_vGoalPos, cost, 0.0f, true, true) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH) + { + // Prioritize following leader. + return true; + } + else if (me->m_hLeadingPlayer.Get() == me->m_hCommandingPlayer.Get()) + { + // Invalid path to leader who is also the commander, so reset both + // Returning false will trigger OnEnd which resets vars + return false; + } + else + { + // check command chain on next tick + me->m_hLeadingPlayer = nullptr; + return true; + } + } + else + { + // Leader is no longer valid or alive + me->m_hLeadingPlayer = nullptr; + return true; + } +} + +// --------------------------------------------------------------------------------------------- +// Process commander ping waypoint commands for bots +// The movementTarget is mutable to accomodate spreading out at goal zone +bool CNEOBotCommandFollow::FanOutAndCover(CNEOBot* me, Vector& movementTarget, bool bMoveToSeparate /*= false*/, float flArrivalZoneSizeSq /*= -1.0f*/) +{ + if (flArrivalZoneSizeSq == -1.0f) + { + flArrivalZoneSizeSq = sv_neo_bot_cmdr_stop_distance_sq.GetFloat(); + } + Vector vBotRepulsion = vec3_origin; + Vector vWallRepulsion = vec3_origin; + Vector vFinalForce = vec3_origin; // Initialize to vec3_origin + bool bTooClose = false; + + if (gpGlobals->curtime > m_flNextFanOutLookCalcTime) + { + // Combined loop for player forces + for (int idx = 1; idx <= gpGlobals->maxClients; ++idx) + { + CBasePlayer* pPlayer = UTIL_PlayerByIndex(idx); + if (!pPlayer || pPlayer == me || !pPlayer->IsAlive()) + continue; + + if (pPlayer->GetTeamNumber() == me->GetTeamNumber()) + { + // Friendly player + CNEO_Player* pOther = static_cast(pPlayer); + if (pOther) + { + // Determine if we are too close to any friendly bot + if (me->GetAbsOrigin().DistToSqr(pOther->GetAbsOrigin()) < sv_neo_bot_cmdr_stop_distance_sq.GetFloat() / 2) + { + bTooClose = true; + } + + // Check for line of sight to other player for repulsion + trace_t tr; + UTIL_TraceLine(me->EyePosition(), pOther->EyePosition(), MASK_PLAYERSOLID, me, COLLISION_GROUP_NONE, &tr); + + if (tr.fraction == 1.0f || tr.m_pEnt == pOther) + { + Vector vToOther = pOther->GetAbsOrigin() - me->GetAbsOrigin(); + float flDistSqr = vToOther.LengthSqr(); + if (flDistSqr > 0.0f) + { + const float flMaxRepulsionDist = sv_neo_bot_cmdr_look_weights_friendly_max_dist.GetFloat(); + float flDist = FastSqrt(flDistSqr); + float flRepulsionScale = 1.0f - (flDist / flMaxRepulsionDist); + flRepulsionScale = Clamp(flRepulsionScale, 0.0f, 1.0f); + float flRepulsion = flRepulsionScale * flRepulsionScale; + + if (flRepulsion > 0.0f) + { + vBotRepulsion -= vToOther.Normalized() * flRepulsion; + } + } + } + } + } + } + + // Next look time + if (bTooClose) + { + m_flNextFanOutLookCalcTime = gpGlobals->curtime + 0.1; // 100 ms + } + else + { + float waitSec = RandomFloat(0.2f, 2.0); + m_flNextFanOutLookCalcTime = gpGlobals->curtime + waitSec; + } + + // Wall Repulsion + trace_t tr; + const int numWhiskers = 12; + for (int i = 0; i < numWhiskers; ++i) + { + QAngle ang = me->GetLocalAngles(); + ang.y += (360.0f / numWhiskers) * i; + Vector vWhiskerDir; + AngleVectors(ang, &vWhiskerDir); + float whiskerDist = sv_neo_bot_cmdr_look_weights_wall_repulsion_whisker_dist.GetFloat(); + vWhiskerDir.z = 0; // look at eye level for windows + UTIL_TraceLine(me->GetBodyInterface()->GetEyePosition(), me->GetBodyInterface()->GetEyePosition() + vWhiskerDir * whiskerDist, MASK_SHOT, me, COLLISION_GROUP_NONE, &tr); + if (tr.DidHit()) + { + float flRepulsion = 1.0f - (tr.fraction); + vWallRepulsion += tr.plane.normal * flRepulsion; + } + } + + // Combine forces and look at final direction + float friendlyRepulsionWeight = sv_neo_bot_cmdr_look_weights_friendly_repulsion.GetFloat(); + float wallRepulsionWeight = sv_neo_bot_cmdr_look_weights_wall_repulsion.GetFloat(); + vFinalForce = (vBotRepulsion * friendlyRepulsionWeight) + (vWallRepulsion * wallRepulsionWeight); + vFinalForce.NormalizeInPlace(); + vFinalForce.z = 0; // avoid tilting awkwardly up or down + if (!vFinalForce.IsZero()) + { + me->GetBodyInterface()->AimHeadTowards(me->GetBodyInterface()->GetEyePosition() + vFinalForce * 500.0f); + } + } + + // Fudge factor to reduce teammates running into each other trying to reach the same point + if (me->GetAbsOrigin().DistToSqr(movementTarget) < flArrivalZoneSizeSq) + { + if (bMoveToSeparate) + { + if (bTooClose) + { + me->PressForwardButton(0.1); + } + } + + movementTarget = me->GetAbsOrigin(); + m_path.Invalidate(); + return true; // Is already at destination + } + + // Still moving to destination, path will be recomputed by the calling context + return false; +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_command_follow.h b/src/game/server/neo/bot/behavior/neo_bot_command_follow.h new file mode 100644 index 0000000000..684866ff3d --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_command_follow.h @@ -0,0 +1,34 @@ +#ifndef NEO_BOT_COMMAND_FOLLOW_H +#define NEO_BOT_COMMAND_FOLLOW_H +#ifdef _WIN32 +#pragma once +#endif + +#include "Path/NextBotChasePath.h" + +class CNEOBotCommandFollow : public Action< CNEOBot > +{ +public: + CNEOBotCommandFollow(); + + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ); + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ); + virtual ActionResult< CNEOBot > OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ); + virtual void OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ); + + virtual const char *GetName( void ) const { return "CommandFollow"; }; + +private: + bool FollowCommandChain( CNEOBot *me ); + bool FanOutAndCover( CNEOBot *me, Vector &movementTarget, bool bMoveToSeparate = true, float flArrivalZoneSizeSq = -1.0f ); + + PathFollower m_path; + CountdownTimer m_repathTimer; + + EHANDLE m_hTargetEntity; + bool m_bGoingToTargetEntity = false; + Vector m_vGoalPos = vec3_origin; + float m_flNextFanOutLookCalcTime = 0.0f; +}; + +#endif // NEO_BOT_COMMAND_FOLLOW_H diff --git a/src/game/server/neo/bot/behavior/neo_bot_pause.cpp b/src/game/server/neo/bot/behavior/neo_bot_pause.cpp new file mode 100644 index 0000000000..800143ce32 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_pause.cpp @@ -0,0 +1,61 @@ +#include "cbase.h" +#include "neo_player.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_pause.h" +#include "bot/behavior/neo_bot_command_follow.h" + +extern ConVar sv_neo_bot_cmdr_debug_pause_uncommanded; + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotPause::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + if ( sv_neo_bot_cmdr_debug_pause_uncommanded.GetBool() ) + { + me->SetAttribute( CNEOBot::IGNORE_ENEMIES ); + me->StopLookingAroundForEnemies(); + } + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotPause::Update( CNEOBot* me, float interval ) +{ + // If the bot gets commanded, transition to CommandFollow immediately + if (me->m_hLeadingPlayer.Get() || me->m_hCommandingPlayer.Get()) + { + return ChangeTo(new CNEOBotCommandFollow, "Commanded while paused"); + } + + if (!sv_neo_bot_cmdr_debug_pause_uncommanded.GetBool()) + { + return Done("Unpaused"); + } + else + { + if ( !me->HasAttribute( CNEOBot::IGNORE_ENEMIES ) ) + { + // Ensure these are set if cvar changed while in state + me->SetAttribute( CNEOBot::IGNORE_ENEMIES ); + me->StopLookingAroundForEnemies(); + } + } + + // Stop moving + const PathFollower *path = me->GetCurrentPath(); + if (path) + { + // Stop existing path + const_cast(path)->Invalidate(); + me->SetCurrentPath(NULL); + } + + me->GetLocomotionInterface()->Reset(); + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +void CNEOBotPause::OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ) +{ + me->ClearAttribute( CNEOBot::IGNORE_ENEMIES ); + me->StartLookingAroundForEnemies(); +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_pause.h b/src/game/server/neo/bot/behavior/neo_bot_pause.h new file mode 100644 index 0000000000..2f16cbf5c6 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_pause.h @@ -0,0 +1,19 @@ +#ifndef NEO_BOT_PAUSE_H +#define NEO_BOT_PAUSE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "bot/neo_bot.h" + +class CNEOBotPause : public Action< CNEOBot > +{ +public: + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ); + virtual ActionResult< CNEOBot > Update( CNEOBot* me, float interval ); + virtual void OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ); + + virtual const char* GetName( void ) const { return "Pause"; }; +}; + +#endif // NEO_BOT_PAUSE_H diff --git a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp index d9bfb55701..2bf3768a63 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp @@ -10,10 +10,12 @@ #include "bot/behavior/neo_bot_tactical_monitor.h" #include "bot/behavior/neo_bot_scenario_monitor.h" +#include "bot/behavior/neo_bot_command_follow.h" #include "bot/behavior/neo_bot_seek_and_destroy.h" #include "bot/behavior/neo_bot_seek_weapon.h" #include "bot/behavior/neo_bot_retreat_to_cover.h" #include "bot/behavior/neo_bot_retreat_from_grenade.h" +#include "bot/behavior/neo_bot_pause.h" #if 0 // NEO TODO (Adam) Fix picking up weapons, search for dropped weapons to pick up ammo #include "bot/behavior/neo_bot_get_ammo.h" #endif @@ -24,6 +26,7 @@ ConVar neo_bot_force_jump( "neo_bot_force_jump", "0", FCVAR_CHEAT, "Force bots to continuously jump" ); ConVar neo_bot_grenade_check_radius( "neo_bot_grenade_check_radius", "500", FCVAR_CHEAT ); +extern ConVar sv_neo_bot_cmdr_debug_pause_uncommanded; //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -275,6 +278,16 @@ ActionResult< CNEOBot > CNEOBotTacticalMonitor::Update( CNEOBot *me, float inter return result; } + if (me->m_hLeadingPlayer.Get() || me->m_hCommandingPlayer.Get()) + { + return SuspendFor(new CNEOBotCommandFollow, "Following commander"); + } + + if (sv_neo_bot_cmdr_debug_pause_uncommanded.GetBool()) + { + return SuspendFor( new CNEOBotPause, "Paused by debug convar sv_neo_bot_cmdr_debug_pause_uncommanded" ); + } + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); // check if we need to get to cover diff --git a/src/game/server/neo/neo_player.cpp b/src/game/server/neo/neo_player.cpp index 2e24d39740..ce610093c3 100644 --- a/src/game/server/neo/neo_player.cpp +++ b/src/game/server/neo/neo_player.cpp @@ -54,6 +54,7 @@ SendPropInt(SENDINFO(m_iLoadoutWepChoice)), SendPropInt(SENDINFO(m_iNextSpawnClassChoice)), SendPropInt(SENDINFO(m_bInLean)), SendPropEHandle(SENDINFO(m_hServerRagdoll)), +SendPropEHandle(SENDINFO(m_hCommandingPlayer)), SendPropBool(SENDINFO(m_bInThermOpticCamo)), SendPropBool(SENDINFO(m_bLastTickInThermOpticCamo)), @@ -75,6 +76,7 @@ SendPropString(SENDINFO(m_pszTestMessage)), SendPropArray(SendPropInt(SENDINFO_ARRAY(m_rfAttackersScores)), m_rfAttackersScores), SendPropArray(SendPropFloat(SENDINFO_ARRAY(m_rfAttackersAccumlator), -1, SPROP_COORD_MP_LOWPRECISION | SPROP_CHANGES_OFTEN, MIN_COORD_FLOAT, MAX_COORD_FLOAT), m_rfAttackersAccumlator), SendPropArray(SendPropInt(SENDINFO_ARRAY(m_rfAttackersHits)), m_rfAttackersHits), +SendPropArray(SendPropVector(SENDINFO_ARRAY(m_vLastPingByStar), -1, SPROP_COORD), m_vLastPingByStar), SendPropInt(SENDINFO(m_NeoFlags), 4, SPROP_UNSIGNED), SendPropString(SENDINFO(m_szNeoName)), @@ -504,6 +506,7 @@ CNEO_Player::CNEO_Player() m_szNameDupePos = 0; m_flNextPingTime = 0; + ResetBotCommandState(); } CNEO_Player::~CNEO_Player( void ) @@ -569,7 +572,6 @@ void CNEO_Player::Spawn(void) m_bIsPendingSpawnForThisRound = false; m_bLastTickInThermOpticCamo = m_bInThermOpticCamo = false; - m_iBotDetectableBleedingInjuryEvents = 0; m_flCamoAuxLastTime = 0; m_bInVision = false; @@ -630,6 +632,9 @@ void CNEO_Player::Spawn(void) m_iLoadoutWepChoice = NEORules()->GetForcedWeapon(); respawn(this, false); } + + ResetBotCommandState(); + m_iBotDetectableBleedingInjuryEvents = 0; } extern ConVar neo_lean_angle; @@ -2037,7 +2042,22 @@ void CNEO_Player::Event_Killed( const CTakeDamageInfo &info ) SetDeadModel(info); } + // If teamkilled by commander, other subordinates stop following commander + CNEO_Player *pAttacker = ToNEOPlayer(info.GetAttacker()); + if (pAttacker && m_hCommandingPlayer.Get() == pAttacker) + { + for (int i = 1; i <= gpGlobals->maxClients; i++) + { + CNEO_Player *pPlayer = ToNEOPlayer(UTIL_PlayerByIndex(i)); + if (pPlayer && pPlayer->m_hCommandingPlayer.Get() == pAttacker) + { + pPlayer->m_hCommandingPlayer.Set(NULL); + } + } + } + SpectatorTakeoverPlayerRevert(false); // soft reset: may still have live impostor + ResetBotCommandState(); } void CNEO_Player::Weapon_DropAllOnDeath( const CTakeDamageInfo &info ) @@ -3181,6 +3201,115 @@ void CNEO_Player::SetTestMessageVisible(bool visible) m_bShowTestMessage = visible; } +void CNEO_Player::ResetBotCommandState() +{ + m_hLeadingPlayer = nullptr; + m_hCommandingPlayer = nullptr; + m_tBotPlayerPingCooldown.Invalidate(); + m_flBotDynamicFollowDistanceSq = 0.0f; + for (int i = 0; i < STAR__TOTAL; ++i) + { + m_vLastPingByStar.GetForModify(i).Init(); + } +} + +void CNEO_Player::SetAllSquadPingWaypoints(const Vector& vec) +{ + for (int i = 0; i < STAR__TOTAL; ++i) + { + m_vLastPingByStar.GetForModify(i) = vec; + } +} + +void CNEO_Player::ToggleBotFollowCommander(CNEO_Player* pCommander) +{ + if (!pCommander) + { + DevMsg("ToggleBotFollowCommander called without valid player handle!\n"); + return; + } + + if (!NEORules()->IsTeamplay()) + { + // Do not allow bot commanding in free for all game modes + return; + } + + if (pCommander->GetTeamNumber() != GetTeamNumber()) + { + // Commander is not on the same team, do not allow toggling follow state. + return; + } + + if (m_hLeadingPlayer.Get() == pCommander) + { + // If already following, check if only star needs updating + if (!pCommander->IsBot() && pCommander->GetStar() != GetStar()) + { + // Commander is a player and stars are different, just update bot's star + RequestSetStar(pCommander->GetStar()); + + // Behavior without resetting pings/commander/leader: + // If this is a new squad star with no waypoint this round, bots will leave waypoint to come follow. + // If this is an existing squad star with an active waypoint, bots will return to that position. + // Might allow for complicated strategems or might be confusing. + // e.g. set a fallback position for a squad star and remote use bots to send them there. + } + else + { + // Bot is already following this player in same star, so toggle off. + // Bot will return to following general uncommanded bot behavior. + m_hLeadingPlayer = nullptr; + m_hCommandingPlayer = nullptr; + } + } + // Without other checks, players can steal others' bots. + else + { + // Bot starts following this player. + m_hLeadingPlayer = pCommander; + if (!pCommander->IsBot()) + { + m_hCommandingPlayer = pCommander; + + // If commander is a player and stars are different, update bot's star + if (pCommander->GetStar() != GetStar()) + { + RequestSetStar(pCommander->GetStar()); + } + } + } +} + +void CNEO_Player::PlayerUse( void ) +{ + BaseClass::PlayerUse(); + + if ( (m_afButtonPressed & IN_USE) && !FindUseEntity() ) + { + // Select bot under cursor to follow/unfollow. + Vector eyePos = EyePosition(); + Vector forward; + EyeVectors( &forward ); + Vector traceEnd = eyePos + forward * MAX_COORD_RANGE; + + trace_t tr; + // MASK_SHOT_HULL to match friendly fire warning trace + UTIL_TraceLine( eyePos, traceEnd, MASK_SHOT_HULL, this, COLLISION_GROUP_NONE, &tr ); + + if ( tr.DidHit() && tr.m_pEnt ) + { + CNEO_Player* pTargetPlayer = ToNEOPlayer(tr.m_pEnt); + if ( pTargetPlayer && pTargetPlayer->IsBot()) + { + // The hit entity is a bot! Now, toggle its follow state. + pTargetPlayer->ToggleBotFollowCommander( this ); + // TODO: Do we want to allow using players for some kind of communication? + } + } + } +} + void CNEO_Player::StartAutoSprint(void) { BaseClass::StartAutoSprint(); diff --git a/src/game/server/neo/neo_player.h b/src/game/server/neo/neo_player.h index 5fc4763056..94007a7a4a 100644 --- a/src/game/server/neo/neo_player.h +++ b/src/game/server/neo/neo_player.h @@ -67,6 +67,7 @@ class CNEO_Player : public CHL2MP_Player virtual void CalculateSpeed(void); virtual void PreThink(void) OVERRIDE; virtual void PlayerDeathThink(void) OVERRIDE; + virtual void PlayerUse(void) OVERRIDE; virtual bool HandleCommand_JoinTeam(int team) OVERRIDE; virtual bool ClientCommand(const CCommand &args) OVERRIDE; virtual void CreateViewModel(int viewmodelindex = 0) OVERRIDE; @@ -308,6 +309,15 @@ class CNEO_Player : public CHL2MP_Player // Bot-only usage float m_flRanOutSprintTime = 0.0f; + CNetworkHandle(CNEO_Player, m_hCommandingPlayer); // The player this bot is commanded by + CHandle m_hLeadingPlayer; // The player this bot is following + CountdownTimer m_tBotPlayerPingCooldown; // The cooldown time for following player ping + float m_flBotDynamicFollowDistanceSq; // The dynamic follow distance interval for bots + CNetworkArray(Vector, m_vLastPingByStar, STAR__TOTAL); // The last ping location from this player for each squad star + // Bot Functions + void ResetBotCommandState(); + void ToggleBotFollowCommander( CNEO_Player *pCommander ); + void SetAllSquadPingWaypoints(const Vector &vec); private: bool m_bFirstDeathTick; diff --git a/src/game/shared/neo/neo_player_shared.cpp b/src/game/shared/neo/neo_player_shared.cpp index 60363e8d41..bd5bb52f6b 100644 --- a/src/game/shared/neo/neo_player_shared.cpp +++ b/src/game/shared/neo/neo_player_shared.cpp @@ -19,6 +19,10 @@ #include "neo_player.h" #endif +#ifdef GAME_DLL +#include "basetypes.h" +#endif + #include "convar.h" #include "neo_weapon_loadout.h" @@ -157,7 +161,40 @@ void CheckPingButton(CNEO_Player* player) { player->m_flNextPingTime = gpGlobals->curtime; } + + UpdatePingCommands(player, tr.endpos); + } +} + +#ifdef GAME_DLL +static ConVar sv_neo_bot_cmdr_enable("sv_neo_bot_cmdr_enable", "1", + FCVAR_NONE, "Allow bots to follow you after you press use on them", true, 0, true, 1); +static ConVar sv_neo_bot_cmdr_stop_distance_sq_max("sv_neo_bot_cmdr_stop_distance_sq_max", "50000", + FCVAR_NONE, "Maximum distance bot following gap interval can be set by player pings", true, 5000, true, 500000); +static ConVar sv_neo_bot_cmdr_ping_ignore_delay_min_sec("sv_neo_bot_cmdr_ping_ignore_delay_min_sec", "3", + FCVAR_NONE, "Minimum time bots ignore pings for new waypoint settings", true, 0, true, 1000); +#endif // GAME_DLL + +void UpdatePingCommands(CNEO_Player* player, const Vector& pingPos) +{ +#ifdef GAME_DLL + if (sv_neo_bot_cmdr_enable.GetBool()) + { + player->m_vLastPingByStar.GetForModify(player->GetStar()) = pingPos; + float distSqrToPing = player->GetAbsOrigin().DistToSqr(pingPos); + if (distSqrToPing < sv_neo_bot_cmdr_stop_distance_sq_max.GetFloat()) + { + // If pinging close to self, calibrate follow distance of commanded bots based on distance to ping + player->m_flBotDynamicFollowDistanceSq = Clamp(distSqrToPing, 5000.0f, sv_neo_bot_cmdr_stop_distance_sq_max.GetFloat()); + } + else + { + player->m_flBotDynamicFollowDistanceSq = 0.0f; + } + + player->m_tBotPlayerPingCooldown.Start(sv_neo_bot_cmdr_ping_ignore_delay_min_sec.GetFloat()); } +#endif } void KillerLineStr(char* killByLine, const int killByLineMax, diff --git a/src/game/shared/neo/neo_player_shared.h b/src/game/shared/neo/neo_player_shared.h index cbbc1d5e97..c7991900ce 100644 --- a/src/game/shared/neo/neo_player_shared.h +++ b/src/game/shared/neo/neo_player_shared.h @@ -253,6 +253,7 @@ enum NeoWeponAimToggleE { }; void CheckPingButton(CNEO_Player* player); +void UpdatePingCommands(CNEO_Player* player, const Vector& pingPos); struct AttackersTotals { From 323345415b430330750169af7bc65bb707a95f72 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sun, 28 Dec 2025 23:04:07 -0800 Subject: [PATCH 2/7] Hide bot command feature behind ConVar flag --- .../client/neo/ui/neo_hud_round_state.cpp | 216 ++++++++++++++++-- src/game/client/neo/ui/neo_hud_round_state.h | 9 +- .../bot/behavior/neo_bot_command_follow.cpp | 12 +- .../bot/behavior/neo_bot_tactical_monitor.cpp | 16 +- src/game/server/neo/neo_player.cpp | 49 ++-- src/game/server/neo/neo_player.h | 1 - src/game/shared/neo/neo_player_shared.cpp | 9 +- 7 files changed, 252 insertions(+), 60 deletions(-) diff --git a/src/game/client/neo/ui/neo_hud_round_state.cpp b/src/game/client/neo/ui/neo_hud_round_state.cpp index 6140c9bb33..4b7924258c 100644 --- a/src/game/client/neo/ui/neo_hud_round_state.cpp +++ b/src/game/client/neo/ui/neo_hud_round_state.cpp @@ -614,6 +614,112 @@ void CNEOHud_RoundState::DrawNeoHudElement() } void CNEOHud_RoundState::DrawPlayerList() +{ + ConVarRef cl_neo_bot_cmdr_enable_ref("sv_neo_bot_cmdr_enable"); + if (cl_neo_bot_cmdr_enable_ref.IsValid() && cl_neo_bot_cmdr_enable_ref.GetBool()) + { + DrawPlayerList_BotCmdr(); + return; + } + + if (g_PR) + { + // Draw members of players squad in an old style list + const int localPlayerTeam = GetLocalPlayerTeam(); + const int localPlayerIndex = GetLocalPlayerIndex(); + const bool localPlayerSpec = !(localPlayerTeam == TEAM_JINRAI || localPlayerTeam == TEAM_NSF); + const int leftTeam = cl_neo_hud_team_swap_sides.GetBool() ? (localPlayerSpec ? TEAM_JINRAI : localPlayerTeam) : TEAM_JINRAI; + + int offset = 52; + if (cl_neo_squad_hud_star_scale.GetFloat() > 0) + { + IntDim res = {}; + surface()->GetScreenSize(res.w, res.h); + offset *= cl_neo_squad_hud_star_scale.GetFloat() * res.h / 1080.0f; + } + + const bool hideDueToScoreboard = cl_neo_hud_scoreboard_hide_others.GetBool() && g_pNeoScoreBoard->IsVisible(); + + // Draw squad mates + if (!localPlayerSpec && g_PR->GetStar(localPlayerIndex) != 0 && !hideDueToScoreboard) + { + bool squadMateFound = false; + + for (int i = 0; i < (MAX_PLAYERS + 1); i++) + { + if (i == localPlayerIndex) + { + continue; + } + if (!g_PR->IsConnected(i)) + { + continue; + } + const int playerTeam = g_PR->GetTeam(i); + if (playerTeam != localPlayerTeam) + { + continue; + } + const bool isSameSquad = g_PR->GetStar(i) == g_PR->GetStar(localPlayerIndex); + if (!isSameSquad) + { + continue; + } + + offset = DrawPlayerRow(i, offset); + squadMateFound = true; + } + + if (squadMateFound) + { + offset += 12; + } + } + + m_iLeftPlayersAlive = 0; + m_iRightPlayersAlive = 0; + + // Draw other team mates + for (int i = 0; i < (MAX_PLAYERS + 1); i++) + { + if (!g_PR->IsConnected(i)) + { + continue; + } + const int playerTeam = g_PR->GetTeam(i); + if (playerTeam != leftTeam) + { + if (g_PR->IsAlive(i)) + { + m_iRightPlayersAlive++; + } + } + else { + if (g_PR->IsAlive(i)) + { + m_iLeftPlayersAlive++; + } + } + if (playerTeam != localPlayerTeam) + { + continue; + } + if (i == localPlayerIndex || localPlayerSpec || hideDueToScoreboard) + { + continue; + } + const bool isSameSquad = g_PR->GetStar(i) == g_PR->GetStar(localPlayerIndex); + if (isSameSquad) + { + continue; + } + + offset = DrawPlayerRow(i, offset, true); + } + } +} + +void CNEOHud_RoundState::DrawPlayerList_BotCmdr() { if (g_PR) { @@ -634,9 +740,10 @@ void CNEOHud_RoundState::DrawPlayerList() const bool hideDueToScoreboard = cl_neo_hud_scoreboard_hide_others.GetBool() && g_pNeoScoreBoard->IsVisible(); // Draw squad mates - CUtlVector commandedList; - CUtlVector nonCommandedList; - CUtlVector nonSquadList; + m_commandedList.RemoveAll(); + m_nonCommandedList.RemoveAll(); + m_nonSquadList.RemoveAll(); + bool squadMateFound = false; m_iLeftPlayersAlive = 0; m_iRightPlayersAlive = 0; @@ -680,33 +787,33 @@ void CNEOHud_RoundState::DrawPlayerList() bool isCommanded = (pPlayer && pPlayer->m_hCommandingPlayer.Get() == C_NEO_Player::GetLocalNEOPlayer()); if (isCommanded) { - commandedList.AddToTail(i); + m_commandedList.AddToTail(i); } else { - nonCommandedList.AddToTail(i); + m_nonCommandedList.AddToTail(i); } squadMateFound = true; } else { - nonSquadList.AddToTail(i); + m_nonSquadList.AddToTail(i); } } // Draw commanded players first - const auto player = static_cast(UTIL_PlayerByIndex(localPlayerIndex)); + const auto player = ToNEOPlayer(UTIL_PlayerByIndex(localPlayerIndex)); TeamLogoColor* pTeamLogoColor = player ? &m_teamLogoColors[player->GetTeamNumber()] : nullptr; const Color* colorOverride = pTeamLogoColor ? &pTeamLogoColor->color : nullptr; - for (int i = 0; i < commandedList.Count(); ++i) + for (int i = 0; i < m_commandedList.Count(); ++i) { - offset = DrawPlayerRow(commandedList[i], offset, false, colorOverride); + offset = DrawPlayerRow_BotCmdr(m_commandedList[i], offset, false, colorOverride); } // Draw non-commanded players (who are also in the same squad star) - for (int i = 0; i < nonCommandedList.Count(); ++i) + for (int i = 0; i < m_nonCommandedList.Count(); ++i) { - offset = DrawPlayerRow(nonCommandedList[i], offset, false); + offset = DrawPlayerRow_BotCmdr(m_nonCommandedList[i], offset, false); } if (squadMateFound) @@ -715,15 +822,21 @@ void CNEOHud_RoundState::DrawPlayerList() } // Draw other team mates - for (int i = 0; i < nonSquadList.Count(); ++i) + for (int i = 0; i < m_nonSquadList.Count(); ++i) { - offset = DrawPlayerRow(nonSquadList[i], offset, true); + offset = DrawPlayerRow_BotCmdr(m_nonSquadList[i], offset, true); } } } -int CNEOHud_RoundState::DrawPlayerRow(int playerIndex, const int yOffset, bool small, const Color* colorOverride) +int CNEOHud_RoundState::DrawPlayerRow(int playerIndex, const int yOffset, bool small) { + ConVarRef cl_neo_bot_cmdr_enable_ref("sv_neo_bot_cmdr_enable"); + if (cl_neo_bot_cmdr_enable_ref.IsValid() && cl_neo_bot_cmdr_enable_ref.GetBool()) + { + return DrawPlayerRow_BotCmdr(playerIndex, yOffset, small); + } + // Draw player static constexpr int SQUAD_MATE_TEXT_LENGTH = 62; // 31 characters in name without end character max plus 3 in short rank name plus 7 max in class name plus 3 max in health plus other characters char squadMateText[SQUAD_MATE_TEXT_LENGTH]; @@ -759,7 +872,62 @@ int CNEOHud_RoundState::DrawPlayerRow(int playerIndex, const int yOffset, bool s auto* pImpersonator = pNeoPlayer ? pNeoPlayer->m_hSpectatorTakeoverPlayerImpersonatingMe.Get() : nullptr; const char* pPlayerDisplayName = pImpersonator ? - pImpersonator->GetPlayerName() + pImpersonator->GetPlayerName() + : g_PR->GetPlayerName(playerIndex); + + const char* displayClass = pImpersonator ? GetNeoClassName(pImpersonator->m_iClassBeforeTakeover) : squadMateClass; + V_snprintf(squadMateText, SQUAD_MATE_TEXT_LENGTH, "%s [%s] DEAD", pPlayerDisplayName, displayClass); + } + g_pVGuiLocalize->ConvertANSIToUnicode(squadMateText, wSquadMateText, sizeof(wSquadMateText)); + + int fontWidth, fontHeight; + surface()->DrawSetTextFont(small ? m_hOCRSmallerFont : m_hOCRSmallFont); + surface()->GetTextSize(m_hOCRSmallFont, m_wszPlayersAliveUnicode, fontWidth, fontHeight); + surface()->DrawSetTextColor(isAlive ? COLOR_FADED_WHITE : COLOR_DARK_FADED_WHITE); + surface()->DrawSetTextPos(8, yOffset); + surface()->DrawPrintText(wSquadMateText, V_wcslen(wSquadMateText)); + + return yOffset + fontHeight; +} + +int CNEOHud_RoundState::DrawPlayerRow_BotCmdr(int playerIndex, const int yOffset, bool small, const Color* colorOverride) +{ + // Draw player + static constexpr int SQUAD_MATE_TEXT_LENGTH = 62; // 31 characters in name without end character max plus 3 in short rank name plus 7 max in class name plus 3 max in health plus other characters + char squadMateText[SQUAD_MATE_TEXT_LENGTH]; + wchar_t wSquadMateText[SQUAD_MATE_TEXT_LENGTH]; + const char* squadMateRankName = GetRankName(g_PR->GetXP(playerIndex), true); + const char* squadMateClass = GetNeoClassName(g_PR->GetClass(playerIndex)); + const bool isAlive = g_PR->IsAlive(playerIndex); + + C_NEO_Player* pNeoPlayer = ToNEOPlayer(UTIL_PlayerByIndex(playerIndex)); + if (isAlive) + { + const char* pPlayerDisplayName = pNeoPlayer ? + pNeoPlayer->GetPlayerNameWithTakeoverContext(playerIndex) + : g_PR->GetPlayerName(playerIndex); + + const char* displayRankName = squadMateRankName; + if (pNeoPlayer) + { + C_NEO_Player* pTakeoverTarget = ToNEOPlayer(pNeoPlayer->m_hSpectatorTakeoverPlayerTarget.Get()); + if (pTakeoverTarget) + { + displayRankName = GetRankName(g_PR->GetXP(pTakeoverTarget->entindex()), true); + } + } + + const int healthMode = cl_neo_hud_health_mode.GetInt(); + char playerHealth[7]; // 4 digits + 2 letters + V_snprintf(playerHealth, sizeof(playerHealth), healthMode ? "%dhp" : "%d%%", g_PR->GetDisplayedHealth(playerIndex, healthMode)); + V_snprintf(squadMateText, SQUAD_MATE_TEXT_LENGTH, "%s %s [%s] %s", pPlayerDisplayName, squadMateRankName, squadMateClass, playerHealth); + } + else + { + auto* pImpersonator = pNeoPlayer ? pNeoPlayer->m_hSpectatorTakeoverPlayerImpersonatingMe.Get() : nullptr; + + const char* pPlayerDisplayName = pImpersonator ? + pImpersonator->GetPlayerName() : g_PR->GetPlayerName(playerIndex); const char* displayClass = pImpersonator ? GetNeoClassName(pImpersonator->m_iClassBeforeTakeover) : squadMateClass; @@ -803,15 +971,19 @@ void CNEOHud_RoundState::DrawPlayer(int playerIndex, int teamIndex, const TeamLo surface()->DrawSetColor(COLOR_DARK); surface()->DrawTexturedRect(xOffset, Y_POS + 1, xOffset + m_ilogoSize, Y_POS + m_ilogoSize + 1); - // Draw Command Highlight Border on top of the avatar image - C_NEO_Player* pPlayer = static_cast(UTIL_PlayerByIndex(playerIndex)); - if (pPlayer && pPlayer->m_hCommandingPlayer.Get() == C_NEO_Player::GetLocalNEOPlayer()) + ConVarRef cl_neo_bot_cmdr_enable_ref("sv_neo_bot_cmdr_enable"); + if (cl_neo_bot_cmdr_enable_ref.IsValid() && cl_neo_bot_cmdr_enable_ref.GetBool()) { - surface()->DrawSetColor(COLOR_WHITE); - // Draw a thicker border inwards - for (int borderIndex = 0; borderIndex < 3; ++borderIndex) + // Draw Command Highlight Border on top of the avatar image + C_NEO_Player* pPlayer = static_cast(UTIL_PlayerByIndex(playerIndex)); + if (pPlayer && pPlayer->m_hCommandingPlayer.Get() == C_NEO_Player::GetLocalNEOPlayer()) { - surface()->DrawOutlinedRect(xOffset + borderIndex, Y_POS + borderIndex, xOffset + m_ilogoSize - borderIndex, Y_POS + m_ilogoSize - borderIndex); + surface()->DrawSetColor(COLOR_WHITE); + // Draw a thicker border inwards + for (int borderIndex = 0; borderIndex < 3; ++borderIndex) + { + surface()->DrawOutlinedRect(xOffset + borderIndex, Y_POS + borderIndex, xOffset + m_ilogoSize - borderIndex, Y_POS + m_ilogoSize - borderIndex); + } } } diff --git a/src/game/client/neo/ui/neo_hud_round_state.h b/src/game/client/neo/ui/neo_hud_round_state.h index 656149a9d2..6f30c8dd3a 100644 --- a/src/game/client/neo/ui/neo_hud_round_state.h +++ b/src/game/client/neo/ui/neo_hud_round_state.h @@ -44,7 +44,9 @@ class CNEOHud_RoundState : public CNEOHud_ChildElement, public CHudElement, publ void CheckActiveStar(); void DrawPlayerList(); - int DrawPlayerRow(int playerIndex, int yOffset, bool small = false, const Color* highlightColor = nullptr); + void DrawPlayerList_BotCmdr(); + int DrawPlayerRow(int playerIndex, int yOffset, bool small = false); + int DrawPlayerRow_BotCmdr(int playerIndex, int yOffset, bool small = false, const Color* highlightColor = nullptr); void DrawPlayer(int playerIndex, int teamIndex, const TeamLogoColor &teamLogoColor, const int xOffset, const bool drawHealthClass); void SetTextureToAvatar(int playerIndex); @@ -98,6 +100,11 @@ class CNEOHud_RoundState : public CNEOHud_ChildElement, public CHudElement, publ int m_iBeepSecsTotal = 0; NeoRoundStatus m_ePrevRoundStatus = NeoRoundStatus::Idle; + // Bot Commander Lists + CUtlVector m_commandedList; + CUtlVector m_nonCommandedList; + CUtlVector m_nonSquadList; + CPanelAnimationVar(Color, box_color, "box_color", "200 200 200 40"); CPanelAnimationVarAliasType(bool, health_monochrome, "health_monochrome", "1", "bool"); CPanelAnimationVarAliasType(bool, top_left_corner, "top_left_corner", "0", "bool"); diff --git a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp index b417436222..bbfb690024 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp @@ -10,19 +10,19 @@ ConVar sv_neo_bot_cmdr_debug_pause_uncommanded("sv_neo_bot_cmdr_debug_pause_unco // NEOTODO: Figure out a clean way to represent config distances as Hammer units, without needing to square on use ConVar sv_neo_bot_cmdr_stop_distance_sq("sv_neo_bot_cmdr_stop_distance_sq", "5000", - FCVAR_NONE, "Minimum distance gap between following bots", true, 3000, true, 100000); + FCVAR_CHEAT, "Minimum distance gap between following bots", true, 3000, true, 100000); ConVar sv_neo_bot_cmdr_look_weights_friendly_repulsion("sv_neo_bot_cmdr_look_weights_friendly_repulsion", "2", - FCVAR_NONE, "Weight for friendly bot repulsion force", true, 1, true, 9999); + FCVAR_CHEAT, "Weight for friendly bot repulsion force", true, 1, true, 9999); ConVar sv_neo_bot_cmdr_look_weights_wall_repulsion("sv_neo_bot_cmdr_look_weights_wall_repulsion", "3", - FCVAR_NONE, "Weight for wall repulsion force", true, 1, true, 9999); + FCVAR_CHEAT, "Weight for wall repulsion force", true, 1, true, 9999); ConVar sv_neo_bot_cmdr_look_weights_explosives_repulsion("sv_neo_bot_cmdr_look_weights_explosives_repulsion", "4", - FCVAR_NONE, "Weight for explosive repulsion force", true, 1, true, 9999); + FCVAR_CHEAT, "Weight for explosive repulsion force", true, 1, true, 9999); ConVar sv_neo_bot_cmdr_look_weights_friendly_max_dist("sv_neo_bot_cmdr_look_weights_friendly_max_dist", "5000", - FCVAR_NONE, "Distance to compare friendly repulsion forces", true, 1, true, 100000); + FCVAR_CHEAT, "Distance to compare friendly repulsion forces", true, 1, true, 100000); ConVar sv_neo_bot_cmdr_look_weights_wall_repulsion_whisker_dist("sv_neo_bot_cmdr_look_weights_wall_repulsion_whisker_dist", "500", - FCVAR_NONE, "Distance to extend whiskers", true, 1, true, 100000); + FCVAR_CHEAT, "Distance to extend whiskers", true, 1, true, 100000); //--------------------------------------------------------------------------------------------- CNEOBotCommandFollow::CNEOBotCommandFollow() diff --git a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp index 2bf3768a63..8674eef5c3 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp @@ -26,6 +26,7 @@ ConVar neo_bot_force_jump( "neo_bot_force_jump", "0", FCVAR_CHEAT, "Force bots to continuously jump" ); ConVar neo_bot_grenade_check_radius( "neo_bot_grenade_check_radius", "500", FCVAR_CHEAT ); +extern ConVar sv_neo_bot_cmdr_enable; extern ConVar sv_neo_bot_cmdr_debug_pause_uncommanded; //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -278,14 +279,17 @@ ActionResult< CNEOBot > CNEOBotTacticalMonitor::Update( CNEOBot *me, float inter return result; } - if (me->m_hLeadingPlayer.Get() || me->m_hCommandingPlayer.Get()) + if (sv_neo_bot_cmdr_enable.GetBool()) { - return SuspendFor(new CNEOBotCommandFollow, "Following commander"); - } + if (me->m_hLeadingPlayer.Get() || me->m_hCommandingPlayer.Get()) + { + return SuspendFor(new CNEOBotCommandFollow, "Following commander"); + } - if (sv_neo_bot_cmdr_debug_pause_uncommanded.GetBool()) - { - return SuspendFor( new CNEOBotPause, "Paused by debug convar sv_neo_bot_cmdr_debug_pause_uncommanded" ); + if (sv_neo_bot_cmdr_debug_pause_uncommanded.GetBool()) + { + return SuspendFor( new CNEOBotPause, "Paused by debug convar sv_neo_bot_cmdr_debug_pause_uncommanded" ); + } } const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); diff --git a/src/game/server/neo/neo_player.cpp b/src/game/server/neo/neo_player.cpp index ce610093c3..e67fb43dbc 100644 --- a/src/game/server/neo/neo_player.cpp +++ b/src/game/server/neo/neo_player.cpp @@ -144,6 +144,7 @@ CBaseEntity *g_pLastJinraiSpawn, *g_pLastNSFSpawn; CNEOGameRulesProxy* neoGameRules; extern CBaseEntity *g_pLastSpawn; +extern ConVar sv_neo_bot_cmdr_enable; extern ConVar sv_neo_ignore_wep_xp_limit; extern ConVar sv_neo_clantag_allow; extern ConVar sv_neo_dev_test_clantag; @@ -2042,22 +2043,25 @@ void CNEO_Player::Event_Killed( const CTakeDamageInfo &info ) SetDeadModel(info); } - // If teamkilled by commander, other subordinates stop following commander - CNEO_Player *pAttacker = ToNEOPlayer(info.GetAttacker()); - if (pAttacker && m_hCommandingPlayer.Get() == pAttacker) + if (sv_neo_bot_cmdr_enable.GetBool()) { - for (int i = 1; i <= gpGlobals->maxClients; i++) + // If teamkilled by commander, other subordinates stop following commander + CNEO_Player *pAttacker = ToNEOPlayer(info.GetAttacker()); + if (pAttacker && m_hCommandingPlayer.Get() == pAttacker) { - CNEO_Player *pPlayer = ToNEOPlayer(UTIL_PlayerByIndex(i)); - if (pPlayer && pPlayer->m_hCommandingPlayer.Get() == pAttacker) + for (int i = 1; i <= gpGlobals->maxClients; i++) { - pPlayer->m_hCommandingPlayer.Set(NULL); + CNEO_Player *pPlayer = ToNEOPlayer(UTIL_PlayerByIndex(i)); + if (pPlayer && pPlayer->m_hCommandingPlayer.Get() == pAttacker) + { + pPlayer->m_hCommandingPlayer.Set(NULL); + } } } } + ResetBotCommandState(); SpectatorTakeoverPlayerRevert(false); // soft reset: may still have live impostor - ResetBotCommandState(); } void CNEO_Player::Weapon_DropAllOnDeath( const CTakeDamageInfo &info ) @@ -3203,26 +3207,26 @@ void CNEO_Player::SetTestMessageVisible(bool visible) void CNEO_Player::ResetBotCommandState() { - m_hLeadingPlayer = nullptr; - m_hCommandingPlayer = nullptr; - m_tBotPlayerPingCooldown.Invalidate(); - m_flBotDynamicFollowDistanceSq = 0.0f; - for (int i = 0; i < STAR__TOTAL; ++i) + if (sv_neo_bot_cmdr_enable.GetBool()) { - m_vLastPingByStar.GetForModify(i).Init(); + m_hLeadingPlayer = nullptr; + m_hCommandingPlayer = nullptr; + m_tBotPlayerPingCooldown.Invalidate(); + m_flBotDynamicFollowDistanceSq = 0.0f; + for (int i = 0; i < STAR__TOTAL; ++i) + { + m_vLastPingByStar.GetForModify(i).Init(); + } } } -void CNEO_Player::SetAllSquadPingWaypoints(const Vector& vec) +void CNEO_Player::ToggleBotFollowCommander(CNEO_Player* pCommander) { - for (int i = 0; i < STAR__TOTAL; ++i) + if (!sv_neo_bot_cmdr_enable.GetBool()) { - m_vLastPingByStar.GetForModify(i) = vec; + return; } -} -void CNEO_Player::ToggleBotFollowCommander(CNEO_Player* pCommander) -{ if (!pCommander) { DevMsg("ToggleBotFollowCommander called without valid player handle!\n"); @@ -3285,6 +3289,11 @@ void CNEO_Player::PlayerUse( void ) { BaseClass::PlayerUse(); + if (!sv_neo_bot_cmdr_enable.GetBool()) + { + return; + } + if ( (m_afButtonPressed & IN_USE) && !FindUseEntity() ) { // Select bot under cursor to follow/unfollow. diff --git a/src/game/server/neo/neo_player.h b/src/game/server/neo/neo_player.h index 94007a7a4a..14ebdefb7e 100644 --- a/src/game/server/neo/neo_player.h +++ b/src/game/server/neo/neo_player.h @@ -317,7 +317,6 @@ class CNEO_Player : public CHL2MP_Player // Bot Functions void ResetBotCommandState(); void ToggleBotFollowCommander( CNEO_Player *pCommander ); - void SetAllSquadPingWaypoints(const Vector &vec); private: bool m_bFirstDeathTick; diff --git a/src/game/shared/neo/neo_player_shared.cpp b/src/game/shared/neo/neo_player_shared.cpp index bd5bb52f6b..89f26c95b8 100644 --- a/src/game/shared/neo/neo_player_shared.cpp +++ b/src/game/shared/neo/neo_player_shared.cpp @@ -166,13 +166,14 @@ void CheckPingButton(CNEO_Player* player) } } +ConVar sv_neo_bot_cmdr_enable("sv_neo_bot_cmdr_enable", "0", + FCVAR_CHEAT | FCVAR_REPLICATED, "Allow bots to follow you after you press use on them", true, 0, true, 1); + #ifdef GAME_DLL -static ConVar sv_neo_bot_cmdr_enable("sv_neo_bot_cmdr_enable", "1", - FCVAR_NONE, "Allow bots to follow you after you press use on them", true, 0, true, 1); static ConVar sv_neo_bot_cmdr_stop_distance_sq_max("sv_neo_bot_cmdr_stop_distance_sq_max", "50000", - FCVAR_NONE, "Maximum distance bot following gap interval can be set by player pings", true, 5000, true, 500000); + FCVAR_CHEAT, "Maximum distance bot following gap interval can be set by player pings", true, 5000, true, 500000); static ConVar sv_neo_bot_cmdr_ping_ignore_delay_min_sec("sv_neo_bot_cmdr_ping_ignore_delay_min_sec", "3", - FCVAR_NONE, "Minimum time bots ignore pings for new waypoint settings", true, 0, true, 1000); + FCVAR_CHEAT, "Minimum time bots ignore pings for new waypoint settings", true, 0, true, 1000); #endif // GAME_DLL void UpdatePingCommands(CNEO_Player* player, const Vector& pingPos) From 925afb644e45341a7762f55d520a95eed16c843f Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Mon, 29 Dec 2025 13:04:54 -0800 Subject: [PATCH 3/7] Implement code review feedback assert convar collapse indent max players loop optimization memdbg header and convar name tweak simplify assignment check Define invalid vector waypoint constant Collapse more indentation Collapse indentation Fix wrong comment re: default value Flatten conditions NextBotBehavior.h override specifier memdbgon Override specifier DevWarning ConVar Min follow distance use GetLocalNEOPlayer --- .../client/neo/ui/neo_hud_round_state.cpp | 175 +++++------ .../bot/behavior/neo_bot_command_follow.cpp | 283 +++++++++--------- .../neo/bot/behavior/neo_bot_command_follow.h | 14 +- .../server/neo/bot/behavior/neo_bot_pause.cpp | 10 +- .../server/neo/bot/behavior/neo_bot_pause.h | 8 +- src/game/server/neo/neo_player.cpp | 6 +- src/game/server/neo/neo_player.h | 1 + src/game/shared/neo/neo_player_shared.cpp | 4 +- 8 files changed, 253 insertions(+), 248 deletions(-) diff --git a/src/game/client/neo/ui/neo_hud_round_state.cpp b/src/game/client/neo/ui/neo_hud_round_state.cpp index 4b7924258c..d43dab56e3 100644 --- a/src/game/client/neo/ui/neo_hud_round_state.cpp +++ b/src/game/client/neo/ui/neo_hud_round_state.cpp @@ -536,7 +536,7 @@ void CNEOHud_RoundState::DrawNeoHudElement() bool bDMRightSide = false; if (NEORules()->IsTeamplay()) { - for (int i = 0; i < (MAX_PLAYERS + 1); i++) + for (int i = 1; i <= gpGlobals->maxClients; i++) { if (g_PR->IsConnected(i)) { @@ -616,6 +616,7 @@ void CNEOHud_RoundState::DrawNeoHudElement() void CNEOHud_RoundState::DrawPlayerList() { ConVarRef cl_neo_bot_cmdr_enable_ref("sv_neo_bot_cmdr_enable"); + Assert(cl_neo_bot_cmdr_enable_ref.IsValid()); if (cl_neo_bot_cmdr_enable_ref.IsValid() && cl_neo_bot_cmdr_enable_ref.GetBool()) { DrawPlayerList_BotCmdr(); @@ -645,7 +646,7 @@ void CNEOHud_RoundState::DrawPlayerList() { bool squadMateFound = false; - for (int i = 0; i < (MAX_PLAYERS + 1); i++) + for (int i = 1; i <= gpGlobals->maxClients; i++) { if (i == localPlayerIndex) { @@ -680,7 +681,7 @@ void CNEOHud_RoundState::DrawPlayerList() m_iRightPlayersAlive = 0; // Draw other team mates - for (int i = 0; i < (MAX_PLAYERS + 1); i++) + for (int i = 1; i <= gpGlobals->maxClients; i++) { if (!g_PR->IsConnected(i)) { @@ -721,117 +722,120 @@ void CNEOHud_RoundState::DrawPlayerList() void CNEOHud_RoundState::DrawPlayerList_BotCmdr() { - if (g_PR) + if (!g_PR) { - // Draw members of players squad in an old style list - const int localPlayerTeam = GetLocalPlayerTeam(); - const int localPlayerIndex = GetLocalPlayerIndex(); - const bool localPlayerSpec = !(localPlayerTeam == TEAM_JINRAI || localPlayerTeam == TEAM_NSF); - const int leftTeam = cl_neo_hud_team_swap_sides.GetBool() ? (localPlayerSpec ? TEAM_JINRAI : localPlayerTeam) : TEAM_JINRAI; + return; + } - int offset = 52; - if (cl_neo_squad_hud_star_scale.GetFloat() > 0) - { - IntDim res = {}; - surface()->GetScreenSize(res.w, res.h); - offset *= cl_neo_squad_hud_star_scale.GetFloat() * res.h / 1080.0f; - } + // Draw members of players squad in an old style list + const int localPlayerTeam = GetLocalPlayerTeam(); + const int localPlayerIndex = GetLocalPlayerIndex(); + const bool localPlayerSpec = !(localPlayerTeam == TEAM_JINRAI || localPlayerTeam == TEAM_NSF); + const int leftTeam = cl_neo_hud_team_swap_sides.GetBool() ? (localPlayerSpec ? TEAM_JINRAI : localPlayerTeam) : TEAM_JINRAI; - const bool hideDueToScoreboard = cl_neo_hud_scoreboard_hide_others.GetBool() && g_pNeoScoreBoard->IsVisible(); + int offset = 52; + if (cl_neo_squad_hud_star_scale.GetFloat() > 0) + { + IntDim res = {}; + surface()->GetScreenSize(res.w, res.h); + offset *= cl_neo_squad_hud_star_scale.GetFloat() * res.h / 1080.0f; + } - // Draw squad mates - m_commandedList.RemoveAll(); - m_nonCommandedList.RemoveAll(); - m_nonSquadList.RemoveAll(); + const bool hideDueToScoreboard = cl_neo_hud_scoreboard_hide_others.GetBool() && g_pNeoScoreBoard->IsVisible(); - bool squadMateFound = false; - m_iLeftPlayersAlive = 0; - m_iRightPlayersAlive = 0; - const int localStar = g_PR->GetStar(localPlayerIndex); + // Draw squad mates + m_commandedList.RemoveAll(); + m_nonCommandedList.RemoveAll(); + m_nonSquadList.RemoveAll(); - // Single pass to collect and categorize players - for (int i = 0; i < (MAX_PLAYERS + 1); i++) + bool squadMateFound = false; + m_iLeftPlayersAlive = 0; + m_iRightPlayersAlive = 0; + const int localStar = g_PR->GetStar(localPlayerIndex); + + // Single pass to collect and categorize players + for (int i = 1; i <= gpGlobals->maxClients; i++) + { + if (!g_PR->IsConnected(i)) { - if (!g_PR->IsConnected(i)) - { - continue; - } - const int playerTeam = g_PR->GetTeam(i); - if (playerTeam != leftTeam) - { - if (g_PR->IsAlive(i)) - { - m_iRightPlayersAlive++; - } - } - else { - if (g_PR->IsAlive(i)) - { - m_iLeftPlayersAlive++; - } - } - if (playerTeam != localPlayerTeam) + continue; + } + const int playerTeam = g_PR->GetTeam(i); + if (playerTeam != leftTeam) + { + if (g_PR->IsAlive(i)) { - continue; + m_iRightPlayersAlive++; } - if (i == localPlayerIndex || localPlayerSpec || hideDueToScoreboard || !ArePlayersOnSameTeam(i, localPlayerIndex)) + } + else { + if (g_PR->IsAlive(i)) { - continue; + m_iLeftPlayersAlive++; } + } + if (playerTeam != localPlayerTeam) + { + continue; + } + if (i == localPlayerIndex || localPlayerSpec || hideDueToScoreboard || !ArePlayersOnSameTeam(i, localPlayerIndex)) + { + continue; + } - // Only consider players in the same squad star as the local player - const bool isSameSquadStar = (localStar != STAR_NONE) && (g_PR->GetStar(i) == localStar); - if (isSameSquadStar) + // Only consider players in the same squad star as the local player + const bool isSameSquadStar = (localStar != STAR_NONE) && (g_PR->GetStar(i) == localStar); + if (isSameSquadStar) + { + C_NEO_Player* pPlayer = ToNEOPlayer(UTIL_PlayerByIndex(i)); + bool isCommanded = (pPlayer && pPlayer->m_hCommandingPlayer.Get() == C_NEO_Player::GetLocalNEOPlayer()); + if (isCommanded) { - C_NEO_Player* pPlayer = ToNEOPlayer(UTIL_PlayerByIndex(i)); - bool isCommanded = (pPlayer && pPlayer->m_hCommandingPlayer.Get() == C_NEO_Player::GetLocalNEOPlayer()); - if (isCommanded) - { - m_commandedList.AddToTail(i); - } - else - { - m_nonCommandedList.AddToTail(i); - } - squadMateFound = true; + m_commandedList.AddToTail(i); } else { - m_nonSquadList.AddToTail(i); + m_nonCommandedList.AddToTail(i); } + squadMateFound = true; } - - // Draw commanded players first - const auto player = ToNEOPlayer(UTIL_PlayerByIndex(localPlayerIndex)); - TeamLogoColor* pTeamLogoColor = player ? &m_teamLogoColors[player->GetTeamNumber()] : nullptr; - const Color* colorOverride = pTeamLogoColor ? &pTeamLogoColor->color : nullptr; - for (int i = 0; i < m_commandedList.Count(); ++i) + else { - offset = DrawPlayerRow_BotCmdr(m_commandedList[i], offset, false, colorOverride); + m_nonSquadList.AddToTail(i); } + } - // Draw non-commanded players (who are also in the same squad star) - for (int i = 0; i < m_nonCommandedList.Count(); ++i) - { - offset = DrawPlayerRow_BotCmdr(m_nonCommandedList[i], offset, false); - } + // Draw commanded players first + const auto player = C_NEO_Player::GetLocalNEOPlayer(); + TeamLogoColor* pTeamLogoColor = player ? &m_teamLogoColors[player->GetTeamNumber()] : nullptr; + const Color* colorOverride = pTeamLogoColor ? &pTeamLogoColor->color : nullptr; + for (int i = 0; i < m_commandedList.Count(); ++i) + { + offset = DrawPlayerRow_BotCmdr(m_commandedList[i], offset, false, colorOverride); + } - if (squadMateFound) - { - offset += 12; - } + // Draw non-commanded players (who are also in the same squad star) + for (int i = 0; i < m_nonCommandedList.Count(); ++i) + { + offset = DrawPlayerRow_BotCmdr(m_nonCommandedList[i], offset, false); + } - // Draw other team mates - for (int i = 0; i < m_nonSquadList.Count(); ++i) - { - offset = DrawPlayerRow_BotCmdr(m_nonSquadList[i], offset, true); - } + if (squadMateFound) + { + offset += 12; + } + + // Draw other team mates + for (int i = 0; i < m_nonSquadList.Count(); ++i) + { + offset = DrawPlayerRow_BotCmdr(m_nonSquadList[i], offset, true); } } int CNEOHud_RoundState::DrawPlayerRow(int playerIndex, const int yOffset, bool small) { ConVarRef cl_neo_bot_cmdr_enable_ref("sv_neo_bot_cmdr_enable"); + Assert(cl_neo_bot_cmdr_enable_ref.IsValid()); if (cl_neo_bot_cmdr_enable_ref.IsValid() && cl_neo_bot_cmdr_enable_ref.GetBool()) { return DrawPlayerRow_BotCmdr(playerIndex, yOffset, small); @@ -972,6 +976,7 @@ void CNEOHud_RoundState::DrawPlayer(int playerIndex, int teamIndex, const TeamLo surface()->DrawTexturedRect(xOffset, Y_POS + 1, xOffset + m_ilogoSize, Y_POS + m_ilogoSize + 1); ConVarRef cl_neo_bot_cmdr_enable_ref("sv_neo_bot_cmdr_enable"); + Assert(cl_neo_bot_cmdr_enable_ref.IsValid()); if (cl_neo_bot_cmdr_enable_ref.IsValid() && cl_neo_bot_cmdr_enable_ref.GetBool()) { // Draw Command Highlight Border on top of the avatar image diff --git a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp index bbfb690024..e05f488ee8 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp @@ -4,6 +4,9 @@ #include "bot/behavior/neo_bot_command_follow.h" #include "nav_mesh.h" +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + ConVar sv_neo_bot_cmdr_debug_pause_uncommanded("sv_neo_bot_cmdr_debug_pause_uncommanded", "0", FCVAR_CHEAT, "If true, uncommanded bots in behavior CNEOBotSeekAndDestroy will pause.", false, 0, false, 1); @@ -19,13 +22,13 @@ ConVar sv_neo_bot_cmdr_look_weights_wall_repulsion("sv_neo_bot_cmdr_look_weights ConVar sv_neo_bot_cmdr_look_weights_explosives_repulsion("sv_neo_bot_cmdr_look_weights_explosives_repulsion", "4", FCVAR_CHEAT, "Weight for explosive repulsion force", true, 1, true, 9999); -ConVar sv_neo_bot_cmdr_look_weights_friendly_max_dist("sv_neo_bot_cmdr_look_weights_friendly_max_dist", "5000", +ConVar sv_neo_bot_cmdr_look_weights_friendly_max_dist_sq("sv_neo_bot_cmdr_look_weights_friendly_max_dist_sq", "5000", FCVAR_CHEAT, "Distance to compare friendly repulsion forces", true, 1, true, 100000); ConVar sv_neo_bot_cmdr_look_weights_wall_repulsion_whisker_dist("sv_neo_bot_cmdr_look_weights_wall_repulsion_whisker_dist", "500", FCVAR_CHEAT, "Distance to extend whiskers", true, 1, true, 100000); //--------------------------------------------------------------------------------------------- -CNEOBotCommandFollow::CNEOBotCommandFollow() +CNEOBotCommandFollow::CNEOBotCommandFollow() : m_vGoalPos( CNEO_Player::VECTOR_INVALID_WAYPOINT ) { } @@ -89,28 +92,24 @@ bool CNEOBotCommandFollow::FollowCommandChain(CNEOBot* me) } // Mirror behavior of leader if we have one - if (me->m_hLeadingPlayer.Get()) + if ( CNEO_Player *pPlayerToMirror = me->m_hLeadingPlayer.Get() ) { - CNEO_Player *pPlayerToMirror = me->m_hLeadingPlayer.Get(); - if (pPlayerToMirror) + if (pPlayerToMirror->GetInThermOpticCamo()) { - if (pPlayerToMirror->GetInThermOpticCamo()) - { - me->EnableCloak(4.0f); - } - else - { - me->DisableCloak(); - } + me->EnableCloak(4.0f); + } + else + { + me->DisableCloak(); + } - if (pPlayerToMirror->IsDucking()) - { - me->PressCrouchButton(0.5f); - } - else - { - me->ReleaseCrouchButton(); - } + if (pPlayerToMirror->IsDucking()) + { + me->PressCrouchButton(0.5f); + } + else + { + me->ReleaseCrouchButton(); } } @@ -121,48 +120,34 @@ bool CNEOBotCommandFollow::FollowCommandChain(CNEOBot* me) follow_stop_distance_sq = pCommander->m_flBotDynamicFollowDistanceSq; } - if (pCommander->IsAlive()) + if (!pCommander->IsAlive()) { - // Follow commander if they are close enough to collect you (and ping cooldown elapsed) - if (pCommander->m_tBotPlayerPingCooldown.IsElapsed() - && (me->GetStar() == pCommander->GetStar()) // only follow when commander is focused on your squad - && (me->GetAbsOrigin().DistToSqr(pCommander->GetAbsOrigin()) < sv_neo_bot_cmdr_stop_distance_sq.GetFloat())) - { - // Use sv_neo_bot_cmdr_stop_distance_sq for consistent bot collection range - // follow_stop_distance_sq would be confusing if player doesn't know about distance tuning - me->m_hLeadingPlayer = pCommander; - m_vGoalPos = vec3_origin; - pCommander->m_vLastPingByStar.GetForModify(me->GetStar()) = vec3_origin; - } - // Go to commander's ping - else if (pCommander->m_vLastPingByStar.Get(me->GetStar()) != vec3_origin) - { - // Check if there's been an update for this star's ping waypoint - if (pCommander->m_vLastPingByStar.Get(me->GetStar()) != me->m_vLastPingByStar.Get(me->GetStar())) - { - me->m_hLeadingPlayer = nullptr; // Stop following and start travelling to ping - m_vGoalPos = pCommander->m_vLastPingByStar.Get(me->GetStar()); - me->m_vLastPingByStar.GetForModify(me->GetStar()) = pCommander->m_vLastPingByStar.Get(me->GetStar()); - - // Force a repath to allow for fine tuned positioning - CNEOBotPathCost cost(me, DEFAULT_ROUTE); - if (m_path.Compute(me, m_vGoalPos, cost, 0.0f, true, true) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH) - { - return true; - } - else - { - me->m_hLeadingPlayer = pCommander; // fallback to following commander - // continue with leader following logic below - } - } + // Commander died + return false; // This triggers OnEnd which clears vars + } - if (FanOutAndCover(me, m_vGoalPos)) - { - // FanOutAndCover true: arrived at destination and settled, so don't recompute path - return true; - } + // Follow commander if they are close enough to collect you (and ping cooldown elapsed) + if (pCommander->m_tBotPlayerPingCooldown.IsElapsed() + && (me->GetStar() == pCommander->GetStar()) // only follow when commander is focused on your squad + && (me->GetAbsOrigin().DistToSqr(pCommander->GetAbsOrigin()) < sv_neo_bot_cmdr_stop_distance_sq.GetFloat())) + { + // Use sv_neo_bot_cmdr_stop_distance_sq for consistent bot collection range + // follow_stop_distance_sq would be confusing if player doesn't know about distance tuning + me->m_hLeadingPlayer = pCommander; + m_vGoalPos = CNEO_Player::VECTOR_INVALID_WAYPOINT; + pCommander->m_vLastPingByStar.GetForModify(me->GetStar()) = CNEO_Player::VECTOR_INVALID_WAYPOINT; + } + // Go to commander's ping + else if (pCommander->m_vLastPingByStar.Get(me->GetStar()) != CNEO_Player::VECTOR_INVALID_WAYPOINT) + { + // Check if there's been an update for this star's ping waypoint + if (pCommander->m_vLastPingByStar.Get(me->GetStar()) != me->m_vLastPingByStar.Get(me->GetStar())) + { + me->m_hLeadingPlayer = nullptr; // Stop following and start travelling to ping + m_vGoalPos = pCommander->m_vLastPingByStar.Get(me->GetStar()); + me->m_vLastPingByStar.GetForModify(me->GetStar()) = pCommander->m_vLastPingByStar.Get(me->GetStar()); + // Force a repath to allow for fine tuned positioning CNEOBotPathCost cost(me, DEFAULT_ROUTE); if (m_path.Compute(me, m_vGoalPos, cost, 0.0f, true, true) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH) { @@ -174,77 +159,87 @@ bool CNEOBotCommandFollow::FollowCommandChain(CNEOBot* me) // continue with leader following logic below } } - } - else - { - // Commander died - return false; // This triggers OnEnd which clears vars - } - // Didn't have order from commander, so follow in snake formation - CNEO_Player* pLeader = me->m_hLeadingPlayer.Get(); - if (pLeader && pLeader->IsAlive()) - { - // Commander can swap and order around different squads while having followers - // but otherwise bots only follow squadmates in the same star or their commander - if ((pLeader != pCommander) && (pLeader->GetStar() != me->GetStar())) - { - me->m_hLeadingPlayer = nullptr; - return true; // restart logic next tick - } - else if (me->GetAbsOrigin().DistToSqr(pLeader->GetAbsOrigin()) < follow_stop_distance_sq) + if (FanOutAndCover(me, m_vGoalPos)) { - // Anti-collision: follow neighbor in snake chain - for (int idx = 1; idx <= gpGlobals->maxClients; ++idx) - { - CNEO_Player* pOther = static_cast(UTIL_PlayerByIndex(idx)); - if (!pOther || !pOther->IsBot() || pOther == me - || (pOther->m_hLeadingPlayer.Get() != me->m_hLeadingPlayer.Get())) - { - // Not another bot in the same snake formation - continue; - } - - if (me->GetAbsOrigin().DistToSqr(pOther->GetAbsOrigin()) < follow_stop_distance_sq / 2) - { - // Follow person I bumped into and link up to snake chain of followers - me->m_hLeadingPlayer = pOther; - break; - } - } - - m_hTargetEntity = NULL; - m_bGoingToTargetEntity = false; - m_path.Invalidate(); - Vector tempLeaderOrigin = pLeader->GetAbsOrigin(); // don't want to override m_vGoalPos - FanOutAndCover(me, tempLeaderOrigin, false, follow_stop_distance_sq); + // FanOutAndCover true: arrived at destination and settled, so don't recompute path return true; } - // Set the bot's goal to the leader's position. - m_vGoalPos = pLeader->GetAbsOrigin(); CNEOBotPathCost cost(me, DEFAULT_ROUTE); if (m_path.Compute(me, m_vGoalPos, cost, 0.0f, true, true) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH) { - // Prioritize following leader. return true; } - else if (me->m_hLeadingPlayer.Get() == me->m_hCommandingPlayer.Get()) + else { - // Invalid path to leader who is also the commander, so reset both - // Returning false will trigger OnEnd which resets vars - return false; + me->m_hLeadingPlayer = pCommander; // fallback to following commander + // continue with leader following logic below } - else + } + + // Didn't have order from commander, so follow in snake formation + CNEO_Player* pLeader = me->m_hLeadingPlayer.Get(); + if (!pLeader || !pLeader->IsAlive()) + { + // Leader is no longer valid or alive + me->m_hLeadingPlayer = nullptr; + return true; + } + + // Commander can swap and order around different squads while having followers + // but otherwise bots only follow squadmates in the same star or their commander + if ((pLeader != pCommander) && (pLeader->GetStar() != me->GetStar())) + { + me->m_hLeadingPlayer = nullptr; + return true; // restart logic next tick + } + else if (me->GetAbsOrigin().DistToSqr(pLeader->GetAbsOrigin()) < follow_stop_distance_sq) + { + // Anti-collision: follow neighbor in snake chain + for (int idx = 1; idx <= gpGlobals->maxClients; ++idx) { - // check command chain on next tick - me->m_hLeadingPlayer = nullptr; - return true; + CNEO_Player* pOther = static_cast(UTIL_PlayerByIndex(idx)); + if (!pOther || !pOther->IsBot() || pOther == me + || (pOther->m_hLeadingPlayer.Get() != me->m_hLeadingPlayer.Get())) + { + // Not another bot in the same snake formation + continue; + } + + if (me->GetAbsOrigin().DistToSqr(pOther->GetAbsOrigin()) < follow_stop_distance_sq / 2) + { + // Follow person I bumped into and link up to snake chain of followers + me->m_hLeadingPlayer = pOther; + break; + } } + + m_hTargetEntity = NULL; + m_bGoingToTargetEntity = false; + m_path.Invalidate(); + Vector tempLeaderOrigin = pLeader->GetAbsOrigin(); // don't want to override m_vGoalPos + FanOutAndCover(me, tempLeaderOrigin, false, follow_stop_distance_sq); + return true; + } + + // Set the bot's goal to the leader's position. + m_vGoalPos = pLeader->GetAbsOrigin(); + CNEOBotPathCost cost(me, DEFAULT_ROUTE); + if (m_path.Compute(me, m_vGoalPos, cost, 0.0f, true, true) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH) + { + // Prioritize following leader. + return true; + } + else if (me->m_hLeadingPlayer.Get() == me->m_hCommandingPlayer.Get()) + { + // Invalid path to leader who is also the commander, so reset both + // Returning false will trigger OnEnd which resets vars + return false; } else { - // Leader is no longer valid or alive + // check command chain on next tick me->m_hLeadingPlayer = nullptr; return true; } @@ -253,7 +248,7 @@ bool CNEOBotCommandFollow::FollowCommandChain(CNEOBot* me) // --------------------------------------------------------------------------------------------- // Process commander ping waypoint commands for bots // The movementTarget is mutable to accomodate spreading out at goal zone -bool CNEOBotCommandFollow::FanOutAndCover(CNEOBot* me, Vector& movementTarget, bool bMoveToSeparate /*= false*/, float flArrivalZoneSizeSq /*= -1.0f*/) +bool CNEOBotCommandFollow::FanOutAndCover(CNEOBot* me, Vector& movementTarget, bool bMoveToSeparate /*= true*/, float flArrivalZoneSizeSq /*= -1.0f*/) { if (flArrivalZoneSizeSq == -1.0f) { @@ -273,39 +268,39 @@ bool CNEOBotCommandFollow::FanOutAndCover(CNEOBot* me, Vector& movementTarget, b if (!pPlayer || pPlayer == me || !pPlayer->IsAlive()) continue; - if (pPlayer->GetTeamNumber() == me->GetTeamNumber()) + // Only consider friendly players + if (pPlayer->GetTeamNumber() != me->GetTeamNumber()) + continue; + + CNEO_Player* pOther = static_cast(pPlayer); + if (!pOther) + continue; + + // Determine if we are too close to any friendly bot + if (me->GetAbsOrigin().DistToSqr(pOther->GetAbsOrigin()) < sv_neo_bot_cmdr_stop_distance_sq.GetFloat() / 2) { - // Friendly player - CNEO_Player* pOther = static_cast(pPlayer); - if (pOther) - { - // Determine if we are too close to any friendly bot - if (me->GetAbsOrigin().DistToSqr(pOther->GetAbsOrigin()) < sv_neo_bot_cmdr_stop_distance_sq.GetFloat() / 2) - { - bTooClose = true; - } + bTooClose = true; + } - // Check for line of sight to other player for repulsion - trace_t tr; - UTIL_TraceLine(me->EyePosition(), pOther->EyePosition(), MASK_PLAYERSOLID, me, COLLISION_GROUP_NONE, &tr); + // Check for line of sight to other player for repulsion + trace_t tr; + UTIL_TraceLine(me->EyePosition(), pOther->EyePosition(), MASK_PLAYERSOLID, me, COLLISION_GROUP_NONE, &tr); + + if (tr.fraction == 1.0f || tr.m_pEnt == pOther) + { + Vector vToOther = pOther->GetAbsOrigin() - me->GetAbsOrigin(); + float flDistSqr = vToOther.LengthSqr(); + if (flDistSqr > 0.0f) + { + const float flMaxRepulsionDist = sv_neo_bot_cmdr_look_weights_friendly_max_dist_sq.GetFloat(); + float flDist = FastSqrt(flDistSqr); + float flRepulsionScale = 1.0f - (flDist / flMaxRepulsionDist); + flRepulsionScale = Clamp(flRepulsionScale, 0.0f, 1.0f); + float flRepulsion = flRepulsionScale * flRepulsionScale; - if (tr.fraction == 1.0f || tr.m_pEnt == pOther) + if (flRepulsion > 0.0f) { - Vector vToOther = pOther->GetAbsOrigin() - me->GetAbsOrigin(); - float flDistSqr = vToOther.LengthSqr(); - if (flDistSqr > 0.0f) - { - const float flMaxRepulsionDist = sv_neo_bot_cmdr_look_weights_friendly_max_dist.GetFloat(); - float flDist = FastSqrt(flDistSqr); - float flRepulsionScale = 1.0f - (flDist / flMaxRepulsionDist); - flRepulsionScale = Clamp(flRepulsionScale, 0.0f, 1.0f); - float flRepulsion = flRepulsionScale * flRepulsionScale; - - if (flRepulsion > 0.0f) - { - vBotRepulsion -= vToOther.Normalized() * flRepulsion; - } - } + vBotRepulsion -= vToOther.Normalized() * flRepulsion; } } } diff --git a/src/game/server/neo/bot/behavior/neo_bot_command_follow.h b/src/game/server/neo/bot/behavior/neo_bot_command_follow.h index 684866ff3d..49035ca12a 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_command_follow.h +++ b/src/game/server/neo/bot/behavior/neo_bot_command_follow.h @@ -4,19 +4,19 @@ #pragma once #endif -#include "Path/NextBotChasePath.h" +#include "NextBotBehavior.h" class CNEOBotCommandFollow : public Action< CNEOBot > { public: CNEOBotCommandFollow(); - virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ); - virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ); - virtual ActionResult< CNEOBot > OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ); - virtual void OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ); + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; + virtual ActionResult< CNEOBot > OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ) override; + virtual void OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ) override; - virtual const char *GetName( void ) const { return "CommandFollow"; }; + virtual const char *GetName( void ) const override { return "CommandFollow"; }; private: bool FollowCommandChain( CNEOBot *me ); @@ -27,7 +27,7 @@ class CNEOBotCommandFollow : public Action< CNEOBot > EHANDLE m_hTargetEntity; bool m_bGoingToTargetEntity = false; - Vector m_vGoalPos = vec3_origin; + Vector m_vGoalPos; float m_flNextFanOutLookCalcTime = 0.0f; }; diff --git a/src/game/server/neo/bot/behavior/neo_bot_pause.cpp b/src/game/server/neo/bot/behavior/neo_bot_pause.cpp index 800143ce32..10b288c80e 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_pause.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_pause.cpp @@ -4,6 +4,9 @@ #include "bot/behavior/neo_bot_pause.h" #include "bot/behavior/neo_bot_command_follow.h" +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + extern ConVar sv_neo_bot_cmdr_debug_pause_uncommanded; //--------------------------------------------------------------------------------------------- @@ -41,12 +44,9 @@ ActionResult< CNEOBot > CNEOBotPause::Update( CNEOBot* me, float interval ) } // Stop moving - const PathFollower *path = me->GetCurrentPath(); - if (path) + if ( me->GetCurrentPath() ) { - // Stop existing path - const_cast(path)->Invalidate(); - me->SetCurrentPath(NULL); + me->SetCurrentPath( NULL ); } me->GetLocomotionInterface()->Reset(); diff --git a/src/game/server/neo/bot/behavior/neo_bot_pause.h b/src/game/server/neo/bot/behavior/neo_bot_pause.h index 2f16cbf5c6..a372dde650 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_pause.h +++ b/src/game/server/neo/bot/behavior/neo_bot_pause.h @@ -9,11 +9,11 @@ class CNEOBotPause : public Action< CNEOBot > { public: - virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ); - virtual ActionResult< CNEOBot > Update( CNEOBot* me, float interval ); - virtual void OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ); + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot* me, float interval ) override; + virtual void OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ) override; - virtual const char* GetName( void ) const { return "Pause"; }; + virtual const char* GetName( void ) const override { return "Pause"; }; }; #endif // NEO_BOT_PAUSE_H diff --git a/src/game/server/neo/neo_player.cpp b/src/game/server/neo/neo_player.cpp index e67fb43dbc..7cbfd896c3 100644 --- a/src/game/server/neo/neo_player.cpp +++ b/src/game/server/neo/neo_player.cpp @@ -140,6 +140,8 @@ END_SCRIPTDESC(); static constexpr int SHOWMENU_STRLIMIT = 512; +const Vector CNEO_Player::VECTOR_INVALID_WAYPOINT = vec3_invalid; + CBaseEntity *g_pLastJinraiSpawn, *g_pLastNSFSpawn; CNEOGameRulesProxy* neoGameRules; extern CBaseEntity *g_pLastSpawn; @@ -3215,7 +3217,7 @@ void CNEO_Player::ResetBotCommandState() m_flBotDynamicFollowDistanceSq = 0.0f; for (int i = 0; i < STAR__TOTAL; ++i) { - m_vLastPingByStar.GetForModify(i).Init(); + m_vLastPingByStar.GetForModify(i) = VECTOR_INVALID_WAYPOINT; } } } @@ -3229,7 +3231,7 @@ void CNEO_Player::ToggleBotFollowCommander(CNEO_Player* pCommander) if (!pCommander) { - DevMsg("ToggleBotFollowCommander called without valid player handle!\n"); + DevWarning("ToggleBotFollowCommander called without valid player handle!\n"); return; } diff --git a/src/game/server/neo/neo_player.h b/src/game/server/neo/neo_player.h index 14ebdefb7e..d963989fa5 100644 --- a/src/game/server/neo/neo_player.h +++ b/src/game/server/neo/neo_player.h @@ -317,6 +317,7 @@ class CNEO_Player : public CHL2MP_Player // Bot Functions void ResetBotCommandState(); void ToggleBotFollowCommander( CNEO_Player *pCommander ); + static const Vector VECTOR_INVALID_WAYPOINT; private: bool m_bFirstDeathTick; diff --git a/src/game/shared/neo/neo_player_shared.cpp b/src/game/shared/neo/neo_player_shared.cpp index 89f26c95b8..b0b8817a95 100644 --- a/src/game/shared/neo/neo_player_shared.cpp +++ b/src/game/shared/neo/neo_player_shared.cpp @@ -186,7 +186,9 @@ void UpdatePingCommands(CNEO_Player* player, const Vector& pingPos) if (distSqrToPing < sv_neo_bot_cmdr_stop_distance_sq_max.GetFloat()) { // If pinging close to self, calibrate follow distance of commanded bots based on distance to ping - player->m_flBotDynamicFollowDistanceSq = Clamp(distSqrToPing, 5000.0f, sv_neo_bot_cmdr_stop_distance_sq_max.GetFloat()); + float minFollowDistanceSq = 0.0f; + sv_neo_bot_cmdr_stop_distance_sq_max.GetMin(minFollowDistanceSq); + player->m_flBotDynamicFollowDistanceSq = Clamp(distSqrToPing, minFollowDistanceSq, sv_neo_bot_cmdr_stop_distance_sq_max.GetFloat()); } else { From 45bce9bd79b5dfe4ef1ca5cb4f148aaeae35c93b Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Tue, 30 Dec 2025 19:19:27 -0800 Subject: [PATCH 4/7] REPLICATED and ARCHIVE flags for sv_neo_bot_cmdr_enable - Removed FCVAR_CHEAT to allow admins to decide if they want to enable this feature without needing to turn on sv_cheats - Replicated flag is needed for some feature flags on the client side related to player list GUI elements - Archive flag in case server has a preference for turning on the feature on or off for future server sessions. --- src/game/shared/neo/neo_player_shared.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/shared/neo/neo_player_shared.cpp b/src/game/shared/neo/neo_player_shared.cpp index b0b8817a95..4a69c01e68 100644 --- a/src/game/shared/neo/neo_player_shared.cpp +++ b/src/game/shared/neo/neo_player_shared.cpp @@ -167,7 +167,7 @@ void CheckPingButton(CNEO_Player* player) } ConVar sv_neo_bot_cmdr_enable("sv_neo_bot_cmdr_enable", "0", - FCVAR_CHEAT | FCVAR_REPLICATED, "Allow bots to follow you after you press use on them", true, 0, true, 1); + FCVAR_REPLICATED | FCVAR_ARCHIVE, "Allow bots to follow you after you press use on them", true, 0, true, 1); #ifdef GAME_DLL static ConVar sv_neo_bot_cmdr_stop_distance_sq_max("sv_neo_bot_cmdr_stop_distance_sq_max", "50000", From 52602302fd39a4b78826607c7b5760a17e1fe8ec Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 1 Jan 2026 14:35:30 -0700 Subject: [PATCH 5/7] Limit trace distance ConVars to MAX_TRACE_LENGTH --- src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp index e05f488ee8..00ea578b4a 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp @@ -23,9 +23,9 @@ ConVar sv_neo_bot_cmdr_look_weights_explosives_repulsion("sv_neo_bot_cmdr_look_w FCVAR_CHEAT, "Weight for explosive repulsion force", true, 1, true, 9999); ConVar sv_neo_bot_cmdr_look_weights_friendly_max_dist_sq("sv_neo_bot_cmdr_look_weights_friendly_max_dist_sq", "5000", - FCVAR_CHEAT, "Distance to compare friendly repulsion forces", true, 1, true, 100000); + FCVAR_CHEAT, "Distance to compare friendly repulsion forces", true, 1, true, MAX_TRACE_LENGTH); ConVar sv_neo_bot_cmdr_look_weights_wall_repulsion_whisker_dist("sv_neo_bot_cmdr_look_weights_wall_repulsion_whisker_dist", "500", - FCVAR_CHEAT, "Distance to extend whiskers", true, 1, true, 100000); + FCVAR_CHEAT, "Distance to extend whiskers", true, 1, true, MAX_TRACE_LENGTH); //--------------------------------------------------------------------------------------------- CNEOBotCommandFollow::CNEOBotCommandFollow() : m_vGoalPos( CNEO_Player::VECTOR_INVALID_WAYPOINT ) From f390c8d715441863aff46177eca07df6dcb9896f Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 2 Jan 2026 14:04:44 -0700 Subject: [PATCH 6/7] Z-axis look horizon reset and player list optimization --- src/game/client/neo/ui/neo_hud_round_state.cpp | 4 ++-- src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/game/client/neo/ui/neo_hud_round_state.cpp b/src/game/client/neo/ui/neo_hud_round_state.cpp index d43dab56e3..8e98da8de9 100644 --- a/src/game/client/neo/ui/neo_hud_round_state.cpp +++ b/src/game/client/neo/ui/neo_hud_round_state.cpp @@ -937,7 +937,7 @@ int CNEOHud_RoundState::DrawPlayerRow_BotCmdr(int playerIndex, const int yOffset const char* displayClass = pImpersonator ? GetNeoClassName(pImpersonator->m_iClassBeforeTakeover) : squadMateClass; V_snprintf(squadMateText, SQUAD_MATE_TEXT_LENGTH, "%s [%s] DEAD", pPlayerDisplayName, displayClass); } - g_pVGuiLocalize->ConvertANSIToUnicode(squadMateText, wSquadMateText, sizeof(wSquadMateText)); + int wSquadMateTextLen = g_pVGuiLocalize->ConvertANSIToUnicode(squadMateText, wSquadMateText, sizeof(wSquadMateText)); int fontWidth, fontHeight; surface()->DrawSetTextFont(small ? m_hOCRSmallerFont : m_hOCRSmallFont); @@ -945,7 +945,7 @@ int CNEOHud_RoundState::DrawPlayerRow_BotCmdr(int playerIndex, const int yOffset surface()->DrawSetTextColor(colorOverride ? *colorOverride : (isAlive ? COLOR_FADED_WHITE : COLOR_DARK_FADED_WHITE)); surface()->DrawSetTextPos(8, yOffset); - surface()->DrawPrintText(wSquadMateText, V_wcslen(wSquadMateText)); + surface()->DrawPrintText(wSquadMateText, wSquadMateTextLen - 1); return yOffset + fontHeight; } diff --git a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp index 00ea578b4a..7efc37046a 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp @@ -340,8 +340,8 @@ bool CNEOBotCommandFollow::FanOutAndCover(CNEOBot* me, Vector& movementTarget, b float friendlyRepulsionWeight = sv_neo_bot_cmdr_look_weights_friendly_repulsion.GetFloat(); float wallRepulsionWeight = sv_neo_bot_cmdr_look_weights_wall_repulsion.GetFloat(); vFinalForce = (vBotRepulsion * friendlyRepulsionWeight) + (vWallRepulsion * wallRepulsionWeight); - vFinalForce.NormalizeInPlace(); vFinalForce.z = 0; // avoid tilting awkwardly up or down + vFinalForce.NormalizeInPlace(); if (!vFinalForce.IsZero()) { me->GetBodyInterface()->AimHeadTowards(me->GetBodyInterface()->GetEyePosition() + vFinalForce * 500.0f); From e6ae4e82e74d1358f359f3b7e1143b0c589f27aa Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 8 Jan 2026 22:48:03 -0700 Subject: [PATCH 7/7] Clarify sv_neo_bot_cmdr_look_weights_friendly_max_dist name to fit MAX_TRACE_LENGTH scale --- src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp index 7efc37046a..adaa7545a1 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp @@ -22,7 +22,7 @@ ConVar sv_neo_bot_cmdr_look_weights_wall_repulsion("sv_neo_bot_cmdr_look_weights ConVar sv_neo_bot_cmdr_look_weights_explosives_repulsion("sv_neo_bot_cmdr_look_weights_explosives_repulsion", "4", FCVAR_CHEAT, "Weight for explosive repulsion force", true, 1, true, 9999); -ConVar sv_neo_bot_cmdr_look_weights_friendly_max_dist_sq("sv_neo_bot_cmdr_look_weights_friendly_max_dist_sq", "5000", +ConVar sv_neo_bot_cmdr_look_weights_friendly_max_dist("sv_neo_bot_cmdr_look_weights_friendly_max_dist", "5000", FCVAR_CHEAT, "Distance to compare friendly repulsion forces", true, 1, true, MAX_TRACE_LENGTH); ConVar sv_neo_bot_cmdr_look_weights_wall_repulsion_whisker_dist("sv_neo_bot_cmdr_look_weights_wall_repulsion_whisker_dist", "500", FCVAR_CHEAT, "Distance to extend whiskers", true, 1, true, MAX_TRACE_LENGTH); @@ -292,7 +292,7 @@ bool CNEOBotCommandFollow::FanOutAndCover(CNEOBot* me, Vector& movementTarget, b float flDistSqr = vToOther.LengthSqr(); if (flDistSqr > 0.0f) { - const float flMaxRepulsionDist = sv_neo_bot_cmdr_look_weights_friendly_max_dist_sq.GetFloat(); + const float flMaxRepulsionDist = sv_neo_bot_cmdr_look_weights_friendly_max_dist.GetFloat(); float flDist = FastSqrt(flDistSqr); float flRepulsionScale = 1.0f - (flDist / flMaxRepulsionDist); flRepulsionScale = Clamp(flRepulsionScale, 0.0f, 1.0f);