From 8919d3b5b5341a8c786ecb51632e010fd15c4bc6 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Mon, 31 Aug 2026 00:36:04 -0600 Subject: [PATCH] Fix bot visibility bugs related to ghost beaconing Primary fix was to prevent ghost carrier bots from getting fixed on a beaconed enemy, even when the beaconed enemy moved out of range and there were no other enemies in range. Also includes fixes for teammate awareness of beaconed enemies called out by the ghost carrier. --- .../server/NextBot/NextBotVisionInterface.cpp | 27 ++++++++++ .../server/NextBot/NextBotVisionInterface.h | 4 ++ .../neo/bot/behavior/neo_bot_ctg_carrier.cpp | 53 +++++++++++++------ .../neo/bot/behavior/neo_bot_ctg_carrier.h | 1 + src/game/server/neo/bot/neo_bot.cpp | 47 ++++++++++++++++ src/game/server/neo/bot/neo_bot.h | 4 ++ src/game/server/neo/bot/neo_bot_vision.cpp | 11 +--- 7 files changed, 121 insertions(+), 26 deletions(-) diff --git a/src/game/server/NextBot/NextBotVisionInterface.cpp b/src/game/server/NextBot/NextBotVisionInterface.cpp index 9523fbdfbe..d6ddc0a3bb 100644 --- a/src/game/server/NextBot/NextBotVisionInterface.cpp +++ b/src/game/server/NextBot/NextBotVisionInterface.cpp @@ -263,6 +263,33 @@ void IVision::ForgetEntity( CBaseEntity *forgetMe ) } +#ifdef NEO +//------------------------------------------------------------------------------------------ +// Update our knowledge of where an entity is, without asserting that we saw it ourselves. +// Needed because AddKnownEntity does not refresh already known entity position data. +void IVision::UpdateKnownEntityPosition( CBaseEntity *entity ) +{ + if ( !entity ) + { + return; + } + + FOR_EACH_VEC( m_knownEntityVector, it ) + { + if ( m_knownEntityVector[ it ].Is( entity ) ) + { + // Found already known entity + m_knownEntityVector[ it ].UpdatePosition(); + return; + } + } + + // We had no idea this entity existed until now + AddKnownEntity( entity ); +} +#endif // NEO + + //------------------------------------------------------------------------------------------ void IVision::ForgetAllKnownEntities( void ) { diff --git a/src/game/server/NextBot/NextBotVisionInterface.h b/src/game/server/NextBot/NextBotVisionInterface.h index c14d3ea440..172f310b6f 100644 --- a/src/game/server/NextBot/NextBotVisionInterface.h +++ b/src/game/server/NextBot/NextBotVisionInterface.h @@ -64,6 +64,10 @@ class IVision : public INextBotComponent virtual void ForgetEntity( CBaseEntity *forgetMe ); // remove the given entity from our awareness (whether we know if it or not) virtual void ForgetAllKnownEntities( void ); +#ifdef NEO + virtual void UpdateKnownEntityPosition( CBaseEntity *entity ); +#endif // NEO + //-- physical vision interface follows ------------------------------------------------------ /** diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.cpp index fef39cd0cd..97673182d2 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.cpp @@ -79,18 +79,21 @@ void CNEOBotGhostEquipmentHandler::Update( CNEOBot *me ) m_enemyUpdateTimer.Start( GetUpdateInterval( me ) ); } + // Check if currently focused enemy has disappeared from beacon range + CBaseEntity *pFocus = m_hCurrentFocusEnemy.Get(); + if ( !IsValidFocusEnemy( me, pFocus ) ) + { + m_hCurrentFocusEnemy = nullptr; + pFocus = nullptr; + } + // Debug: Highlight the location of the enemy a bot ghost carrier is calling out - if ( neo_debug_ghost_carrier.GetBool() ) + if ( neo_debug_ghost_carrier.GetBool() && pFocus && pFocus->IsPlayer() ) { - CBaseEntity *pFocus = m_hCurrentFocusEnemy.Get(); - if ( pFocus && pFocus->IsPlayer() && pFocus->IsAlive() ) - { - NDebugOverlay::Cross3D( pFocus->GetAbsOrigin(), 20.0f, 255, 0, 0, true, 0.1f ); - } + NDebugOverlay::Cross3D( pFocus->GetAbsOrigin(), 20.0f, 255, 0, 0, true, 0.1f ); } - CBaseEntity *pFocus = m_hCurrentFocusEnemy.Get(); - if ( pFocus && pFocus->IsAlive() ) + if ( pFocus ) { if ( bUpdateCallout ) { @@ -125,12 +128,8 @@ void CNEOBotGhostEquipmentHandler::Update( CNEOBot *me ) // NEO Jank: Urge relevant teammate bots look at the enemy pBot->GetBodyInterface()->AimHeadTowards( pFocus, IBody::IMPORTANT, 0.5f, nullptr, "Ghost carrier teammate look override" ); } - else - { - // Force updates to known but not visible entity by forgetting them first - pBot->GetVisionInterface()->ForgetEntity( pFocus ); - } - pBot->GetVisionInterface()->AddKnownEntity( pFocus ); // keep after ForgetEntity + + pBot->GetVisionInterface()->UpdateKnownEntityPosition( pFocus ); } } @@ -177,6 +176,21 @@ void CNEOBotGhostEquipmentHandler::EquipBestWeaponForGhoster( CNEOBot *me ) } } +bool CNEOBotGhostEquipmentHandler::IsValidFocusEnemy( CNEOBot *me, CBaseEntity *pFocus ) const +{ + if ( !pFocus || !pFocus->IsAlive() || pFocus->IsEffectActive( EF_NODRAW ) ) + { + return false; + } + + if ( !me->IsEnemy( pFocus ) ) + { + return false; + } + + return me->GetVisionInterface()->IsAbleToSee( pFocus, IVision::DISREGARD_FOV ); +} + float CNEOBotGhostEquipmentHandler::GetUpdateInterval( CNEOBot *me ) const { switch ( me->GetDifficulty() ) @@ -338,6 +352,11 @@ void CNEOBotGhostEquipmentHandler::UpdateGhostCarrierCallout( CNEOBot *me, const m_enemyLastPos[ idx ] = pBestCallout->GetAbsOrigin(); } } + else + { + // Nobody is in beacon range or in sight + m_hCurrentFocusEnemy = nullptr; + } } @@ -516,8 +535,7 @@ void CNEOBotCtgCarrier::UpdateFollowPath( CNEOBot *me, const CUtlVector( me->Weapon_GetSlot( 0 ) ); - if ( pGhost && pGhost->IsGhost() && pGhost->IsBootupCompleted() ) + if ( me->GetBeaconingGhost() ) { float flDistMeToGoalSq = me->GetAbsOrigin().DistToSqr( vecGoalPos ); @@ -526,7 +544,8 @@ void CNEOBotCtgCarrier::UpdateFollowPath( CNEOBot *me, const CUtlVectormaxClients; i++ ) { CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); - if ( pPlayer && pPlayer->IsAlive() && pPlayer->GetTeamNumber() != me->GetTeamNumber() ) + if ( pPlayer && pPlayer->IsAlive() && pPlayer->GetTeamNumber() != me->GetTeamNumber() + && me->GetVisionInterface()->IsAbleToSee( pPlayer, IVision::DISREGARD_FOV ) ) { float dSq = pPlayer->GetAbsOrigin().DistToSqr( vecGoalPos ); if ( dSq <= flDistMeToGoalSq ) diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.h index 4b70b47ca0..645b56d8af 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.h +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.h @@ -17,6 +17,7 @@ class CNEOBotGhostEquipmentHandler private: void EquipBestWeaponForGhoster( CNEOBot *me ); float GetUpdateInterval( CNEOBot *me ) const; + bool IsValidFocusEnemy( CNEOBot *me, CBaseEntity *pFocus ) const; void UpdateGhostCarrierCallout( CNEOBot *me, const CUtlVector &enemies ); EHANDLE m_hCurrentFocusEnemy{nullptr}; diff --git a/src/game/server/neo/bot/neo_bot.cpp b/src/game/server/neo/bot/neo_bot.cpp index 175eb07c2f..85cf258a6f 100644 --- a/src/game/server/neo/bot/neo_bot.cpp +++ b/src/game/server/neo/bot/neo_bot.cpp @@ -22,6 +22,7 @@ #include "neo_weapon_loadout.h" #include "behavior/neo_bot_behavior.h" #include "neo_crosshair.h" +#include "neo/weapons/weapon_ghost.h" ConVar neo_bot_notice_gunfire_range("neo_bot_notice_gunfire_range", "3000", FCVAR_GAMEDLL); ConVar neo_bot_notice_quiet_gunfire_range("neo_bot_notice_quiet_gunfire_range", "500", FCVAR_GAMEDLL); @@ -41,6 +42,7 @@ extern ConVar neo_bot_difficulty; extern ConVar neo_bot_farthest_visible_theater_sample_count; extern ConVar neo_bot_path_lookahead_range; extern ConVar neo_bot_path_around_friendly_cooldown; +extern ConVar sv_neo_ctg_ghost_beacons_when_inactive; @@ -1625,6 +1627,51 @@ void CNEOBot::EquipBestWeaponForThreat(const CKnownEntity* threat, const bool bN } +//----------------------------------------------------------------------------------------------------- +// Return handle to ghost if it is beaconing for the bot player +// Also useful to get true beacon range from weapon implementation +CWeaponGhost *CNEOBot::GetBeaconingGhost( void ) const +{ + if ( !IsCarryingGhost() ) + { + return nullptr; + } + + CBaseCombatWeapon *pCandidate = sv_neo_ctg_ghost_beacons_when_inactive.GetBool() + ? Weapon_GetSlot( 0 ) + : GetActiveWeapon(); + + CNEOBaseCombatWeapon *pNeoWeapon = dynamic_cast( pCandidate ); + if ( !pNeoWeapon || !pNeoWeapon->IsGhost() ) + { + return nullptr; + } + + CWeaponGhost *pGhost = assert_cast( pNeoWeapon ); + return pGhost->IsBootupCompleted() ? pGhost : nullptr; +} + + +//----------------------------------------------------------------------------------------------------- +// Returns whether the conditions are satisfied for the ghost revealing the subject +bool CNEOBot::IsRevealedByMyGhost( CBaseEntity *subject ) const +{ + if ( !subject ) + { + return false; + } + + const CWeaponGhost *pGhost = GetBeaconingGhost(); + if ( !pGhost ) + { + return false; + } + + float flDistIgnored; + return pGhost->BeaconRange( subject, flDistIgnored ); +} + + //----------------------------------------------------------------------------------------------------- bool CNEOBot::DropGhost() { diff --git a/src/game/server/neo/bot/neo_bot.h b/src/game/server/neo/bot/neo_bot.h index a0a21be817..5f388f5d38 100644 --- a/src/game/server/neo/bot/neo_bot.h +++ b/src/game/server/neo/bot/neo_bot.h @@ -22,6 +22,7 @@ class CNEOBotActionPoint; class CNEOBotGenerator; class CNEOBot; +class CWeaponGhost; extern ConVar hl2_normspeed; @@ -160,7 +161,10 @@ class CNEOBot : public NextBotPlayer< CNEO_Player >, public CGameEventListener bool EquipRequiredWeapon(void); // if we're required to equip a specific weapon, do it. void EquipBestWeaponForThreat(const CKnownEntity* threat, const bool bNotPrimary = false); // equip the best weapon we have to attack the given threat void ReloadIfLowClip(bool bForceReload = false); + bool DropGhost(); + CWeaponGhost *GetBeaconingGhost(void) const; + bool IsRevealedByMyGhost(CBaseEntity *subject) const; void DropPrimaryWeapon(void); diff --git a/src/game/server/neo/bot/neo_bot_vision.cpp b/src/game/server/neo/bot/neo_bot_vision.cpp index 2b3dbd2adb..b07bccd7bb 100644 --- a/src/game/server/neo/bot/neo_bot_vision.cpp +++ b/src/game/server/neo/bot/neo_bot_vision.cpp @@ -174,16 +174,9 @@ bool CNEOBotVision::IsInFieldOfView( CBaseEntity *subject ) const bool CNEOBotVision::IsAbleToSee(CBaseEntity *subject, FieldOfViewCheckType checkFOV, Vector *visibleSpot) const { CNEOBot *me = (CNEOBot *)GetBot()->GetEntity(); - if (me && me->IsCarryingGhost()) + if (me && me->IsRevealedByMyGhost(subject)) { - auto *pGhost = dynamic_cast(me->GetActiveWeapon()); - if (pGhost && pGhost->IsGhost() && pGhost->IsBootupCompleted()) - { - if (me->GetAbsOrigin().DistToSqr(subject->GetAbsOrigin()) < Square(CWeaponGhost::GetGhostRangeInHammerUnits())) - { - return true; - } - } + return true; } return IVision::IsAbleToSee(subject, checkFOV, visibleSpot);