From 131027f3ca62ada9c44d4a4851a0b4cf7f3ae9b9 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 efc0e45826..3d16fac898 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; + } } @@ -521,8 +540,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 ); @@ -531,7 +549,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 70f5ed4d41..0d0ec60a5f 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; @@ -1633,6 +1635,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);