From fd151a689d0eb95f980a894b731857d15bad7a7a Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 15 Aug 2026 23:45:08 -0600 Subject: [PATCH] Bots choose another bot buddy to pair with during freezetime --- src/game/server/CMakeLists.txt | 2 + .../neo/bot/behavior/neo_bot_attack.cpp | 22 +++ .../server/neo/bot/behavior/neo_bot_attack.h | 3 +- .../neo/bot/behavior/neo_bot_freezetime.cpp | 166 ++++++++++++++++++ .../neo/bot/behavior/neo_bot_freezetime.h | 18 ++ .../bot/behavior/neo_bot_retreat_to_cover.cpp | 22 +++ .../bot/behavior/neo_bot_retreat_to_cover.h | 5 +- .../bot/behavior/neo_bot_tactical_monitor.cpp | 19 ++ .../bot/behavior/neo_bot_tactical_monitor.h | 1 + 9 files changed, 255 insertions(+), 3 deletions(-) create mode 100644 src/game/server/neo/bot/behavior/neo_bot_freezetime.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_freezetime.h diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index eb9a890881..335a8055a5 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1530,6 +1530,7 @@ set(UNITY_SOURCE_NEO_BOT neo/bot/behavior/neo_bot_ctg_lone_wolf.cpp neo/bot/behavior/neo_bot_ctg_seek.cpp neo/bot/behavior/neo_bot_dead.cpp + neo/bot/behavior/neo_bot_freezetime.cpp neo/bot/behavior/neo_bot_grenade_dispatch.cpp neo/bot/behavior/neo_bot_grenade_throw.cpp neo/bot/behavior/neo_bot_grenade_throw_frag.cpp @@ -1595,6 +1596,7 @@ target_sources_grouped( neo/bot/behavior/neo_bot_ctg_seek.cpp neo/bot/behavior/neo_bot_ctg_seek.h neo/bot/behavior/neo_bot_dead.h + neo/bot/behavior/neo_bot_freezetime.h neo/bot/behavior/neo_bot_grenade_dispatch.h neo/bot/behavior/neo_bot_grenade_throw.h neo/bot/behavior/neo_bot_grenade_throw_frag.h diff --git a/src/game/server/neo/bot/behavior/neo_bot_attack.cpp b/src/game/server/neo/bot/behavior/neo_bot_attack.cpp index 0d0dab5270..52cc61af02 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_attack.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_attack.cpp @@ -46,10 +46,32 @@ CNEOBotAttack::CNEOBotAttack( const Vector &goalPosition ) : m_chasePath( ChaseP ActionResult< CNEOBot > CNEOBotAttack::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) { m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + // NEO Jank: Rudimentary communication between paired buddy bots regarding threat + // If this bot was following another bot buddy, + // becoming the commander recalls their buddy to regroup + // and pings when shooting direct the buddy to the enemy's position + if ( CNEO_Player* commander = me->m_hCommandingPlayer.Get() ) + { + if ( commander->IsBot() ) + { + me->m_hCommandingPlayer.Set( nullptr ); + commander->m_hCommandingPlayer.Set( me ); + } + } + return Continue(); } +//--------------------------------------------------------------------------------------------- +void CNEOBotAttack::OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ) +{ + // Clear out any target callouts when disengaged from threat + me->m_vLastPingByStar.GetForModify(me->GetStar()) = CNEO_Player::VECTOR_INVALID_WAYPOINT; +} + + //--------------------------------------------------------------------------------------------- // for finding cover closer to our attack destination class CSearchForAttackCover : public ISearchSurroundingAreasFunctor diff --git a/src/game/server/neo/bot/behavior/neo_bot_attack.h b/src/game/server/neo/bot/behavior/neo_bot_attack.h index 52ec61eeb7..d6d33afaca 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_attack.h +++ b/src/game/server/neo/bot/behavior/neo_bot_attack.h @@ -14,7 +14,8 @@ class CNEOBotAttack : public Action< CNEOBot > CNEOBotAttack( const Vector &goalPosition ); virtual ~CNEOBotAttack() { } - virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ); + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual void OnEnd( CNEOBot *me, Action *nextAction ) override; virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ); virtual EventDesiredResult< CNEOBot > OnStuck( CNEOBot *me ); diff --git a/src/game/server/neo/bot/behavior/neo_bot_freezetime.cpp b/src/game/server/neo/bot/behavior/neo_bot_freezetime.cpp new file mode 100644 index 0000000000..da21aa90b4 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_freezetime.cpp @@ -0,0 +1,166 @@ +#include "cbase.h" +#include "neo_player.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_freezetime.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// sv_neo_bot_freezetime_planning_start_time_buffer should be longer than sv_neo_bot_freezetime_planning_end_time_buffer +// ideally with enough ticks left to plan next round paths +// used to calibrate throttle timer for checking if a bot is in freezetime in tactical monitor +ConVar sv_neo_bot_freezetime_planning_start_time_buffer( "sv_neo_bot_freezetime_planning_start_time_buffer", "2", FCVAR_NONE, + "Seconds of freezetime that must be available before bots will start planning.", true, 0.0f, false, 0 ); + +// sv_neo_bot_freezetime_planning_end_time_buffer should leave enough ticks for path planning before the round starts +ConVar sv_neo_bot_freezetime_planning_end_time_buffer( "sv_neo_bot_freezetime_planning_end_time_buffer", "1", FCVAR_NONE, + "Seconds of freezetime left for bots to stop planning prior to round start.", true, 0.0f, false, 0 ); + +ActionResult< CNEOBot > CNEOBotFreezeTime::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + m_bBuddyPairingDecisionMade = false; + return Continue(); +} + +ActionResult< CNEOBot > CNEOBotFreezeTime::Update( CNEOBot* me, float interval ) +{ + if ( !me || !me->IsAlive() ) + { + return Done("Bot is not in valid state for freezetime planning"); + } + + const float freezeTimeLeft = NEORules()->GetRemainingPreRoundFreezeTime( true ); + if ( freezeTimeLeft < sv_neo_bot_freezetime_planning_end_time_buffer.GetFloat() ) + { + return Done("Done planning, waiting for freeze time to end"); + } + + if ( CNEO_Player* buddy = me->m_hCommandingPlayer.Get() ) + { + // Check if buddy was reassigned to another partner + if ( buddy->m_hCommandingPlayer.Get() != nullptr ) + { + // Retry looking for another buddy + m_bBuddyPairingDecisionMade = false; + me->m_hCommandingPlayer = nullptr; + } + } + + if ( !m_bBuddyPairingDecisionMade ) + { + const bool foundBuddy = TryFindBuddy( me ); + if ( foundBuddy ) + { + return ChangeTo( new CNEOBotCommandFollow, "Freeze-time buddy found" ); + } + } + + return Continue(); +} + +bool CNEOBotFreezeTime::TryFindBuddy( CNEOBot *me ) +{ + if ( m_bBuddyPairingDecisionMade ) + { + return true; + } + + // Check if I already have a commander + // This could happen if a human selects a bot as a follower + if ( me->m_hCommandingPlayer.Get() != nullptr ) + { + return false; + } + + m_bBuddyPairingDecisionMade = true; + + // We shouldn't be pairing up in free for all matches + if ( !NEORules()->IsTeamplay() ) + { + return false; + } + + CUtlVector friendlyPlayers; + CollectPlayers( &friendlyPlayers, me->GetTeamNumber(), COLLECT_ONLY_LIVING_PLAYERS ); + + CNEO_Player *bestBuddy = nullptr; + float bestDistanceSq = FLT_MAX; + + for ( int i = 0; i < friendlyPlayers.Count(); ++i ) + { + CNEO_Player *friendly = friendlyPlayers[i]; + if ( !friendly || !WantsToPairWithBuddyCandidate( me, friendly ) ) + { + continue; + } + + const float distSq = ( friendly->GetAbsOrigin() - me->GetAbsOrigin() ).LengthSqr(); + if ( distSq < bestDistanceSq ) + { + bestDistanceSq = distSq; + bestBuddy = friendly; + } + } + + if ( !bestBuddy ) + { + return false; + } + + me->m_hCommandingPlayer = bestBuddy; + me->m_hLeadingPlayer = bestBuddy; + return true; +} + +bool CNEOBotFreezeTime::WantsToPairWithBuddyCandidate( const CNEO_Player *me, const CNEO_Player *buddy ) +{ + if ( !me || !buddy || me == buddy ) + { + return false; + } + + // CollectPlayers should only return teammates + Assert ( buddy->GetTeamNumber() == me->GetTeamNumber() ); + + // It might confuse human players if bots decide to follow them unprompted + if ( !buddy->IsBot() ) + { + return false; + } + + // This bot is already paired with a leader + if ( buddy->m_hCommandingPlayer.Get() != nullptr ) + { + return false; + } + + // Bots in different squad stars should not link with each other + // Humans can rearrange bot squad star membership to influence valid pairings + if ( buddy->GetStar() == me->GetStar()) + { + return false; + } + + const int myClass = me->GetClass(); + const int buddyClass = buddy->GetClass(); + + if ( myClass == NEO_CLASS_RECON ) + { + // Recons only pair with other recons + return buddyClass == NEO_CLASS_RECON; + } + if ( myClass == NEO_CLASS_ASSAULT ) + { + // Assaults avoid pairing with Supports and VIPS + return buddyClass == NEO_CLASS_RECON || buddyClass == NEO_CLASS_ASSAULT; + } + if ( myClass == NEO_CLASS_SUPPORT ) + { + // Supports avoid following VIPs + return buddyClass != NEO_CLASS_VIP; + } + + // VIPs can pair with anyone + // JGR does not exist as a player in freezetime + return true; +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_freezetime.h b/src/game/server/neo/bot/behavior/neo_bot_freezetime.h new file mode 100644 index 0000000000..b8ec473a26 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_freezetime.h @@ -0,0 +1,18 @@ +#pragma once + +#include "bot/neo_bot.h" + +class CNEOBotFreezeTime : public Action< CNEOBot > +{ +public: + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; + + virtual const char* GetName( void ) const override { return "FreezeTime"; } + +private: + bool m_bBuddyPairingDecisionMade = false; + + bool TryFindBuddy( CNEOBot *me ); + bool WantsToPairWithBuddyCandidate( const CNEO_Player *me, const CNEO_Player *buddy ); +}; diff --git a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp index b31b8a5ae4..ae5af67e14 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp @@ -197,10 +197,32 @@ ActionResult< CNEOBot > CNEOBotRetreatToCover::OnStart( CNEOBot *me, Action< CNE m_waitInCoverTimer.Start( m_hideDuration ); + + // NEO Jank: Rudimentary communication between paired buddy bots regarding threat + // If this bot was following another bot buddy, + // becoming the commander recalls their buddy to regroup + // and pings when shooting direct the buddy to the enemy's position + if ( CNEO_Player* commander = me->m_hCommandingPlayer.Get() ) + { + if ( commander->IsBot() ) + { + me->m_hCommandingPlayer.Set( nullptr ); + commander->m_hCommandingPlayer.Set( me ); + } + } + return Continue(); } +//--------------------------------------------------------------------------------------------- +void CNEOBotRetreatToCover::OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ) +{ + // Clear out any target callouts when disengaged from threat + me->m_vLastPingByStar.GetForModify(me->GetStar()) = CNEO_Player::VECTOR_INVALID_WAYPOINT; +} + + //--------------------------------------------------------------------------------------------- ActionResult< CNEOBot > CNEOBotRetreatToCover::Update( CNEOBot *me, float interval ) { diff --git a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.h b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.h index 580b7f7790..e4cfc3ee1e 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.h +++ b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.h @@ -6,8 +6,9 @@ class CNEOBotRetreatToCover : public Action< CNEOBot > CNEOBotRetreatToCover( float hideDuration = -1.0f ); CNEOBotRetreatToCover( Action< CNEOBot > *actionToChangeToOnceCoverReached ); - virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ); - virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ); + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual void OnEnd( CNEOBot *me, Action *nextAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; virtual EventDesiredResult< CNEOBot > OnStuck( CNEOBot *me ); virtual EventDesiredResult< CNEOBot > OnMoveToSuccess( CNEOBot *me, const Path *path ); 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 78e8be610a..8c6c68ce96 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 @@ -11,6 +11,7 @@ #include "bot/behavior/neo_bot_tactical_monitor.h" #include "bot/behavior/neo_bot_scenario_monitor.h" +#include "bot/behavior/neo_bot_freezetime.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" @@ -33,6 +34,11 @@ ConVar neo_bot_force_jump( "neo_bot_force_jump", "0", FCVAR_CHEAT, "Force bots t ConVar neo_bot_scavenge_upgrade_delay( "neo_bot_scavenge_upgrade_delay", "4", FCVAR_GAMEDLL, "Delay in seconds between checking for a better weapon if the bot already has a primary weapon.", true, 1, false, 0 ); +extern ConVar mp_chattime; +extern ConVar sv_neo_bot_freezetime_planning_start_time_buffer; +extern ConVar sv_neo_bot_freezetime_planning_end_time_buffer; +extern ConVar sv_neo_preround_freeze_time; + //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -327,6 +333,19 @@ ActionResult< CNEOBot > CNEOBotTacticalMonitor::Update( CNEOBot *me, float inter me->ReloadIfLowClip(); } + // if we are in freezetime, make plans before next round + if ( !m_freezeTimeDecisionTimer.HasStarted() || m_freezeTimeDecisionTimer.IsElapsed() ) + { + const float freezeCheckDelay = mp_chattime.GetFloat() + Max( 0.0f, sv_neo_preround_freeze_time.GetFloat() - sv_neo_bot_freezetime_planning_start_time_buffer.GetFloat() ); + m_freezeTimeDecisionTimer.Start( freezeCheckDelay ); + + const float freezeTimeLeft = NEORules()->GetRemainingPreRoundFreezeTime( true ); + if ( freezeTimeLeft > sv_neo_bot_freezetime_planning_end_time_buffer.GetFloat() ) + { + return SuspendFor( new CNEOBotFreezeTime, "Planning next round during freezetime" ); + } + } + me->UpdateDelayedThreatNotices(); return Continue(); diff --git a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.h b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.h index 83634acfcf..77f7088311 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.h +++ b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.h @@ -27,6 +27,7 @@ class CNEOBotTacticalMonitor : public Action< CNEOBot > private: CountdownTimer m_maintainTimer; + CountdownTimer m_freezeTimeDecisionTimer; CountdownTimer m_acknowledgeAttentionTimer; CountdownTimer m_acknowledgeRetryTimer;