Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/game/server/neo/bot/behavior/neo_bot_behavior.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ ActionResult< CNEOBot > CNEOBotMainAction::Update( CNEOBot *me, float interval )
{
VPROF_BUDGET( "CNEOBotMainAction::Update", "NextBot" );

// If bot is already dead at this point, make sure it's dead.
// This check prevents the main behavior loop from executing on dead bots, which can happen
// if a bot dies during a frame or enters an invalid state like Observer mode.
// Executing main behavior (like looking for enemies or navigating) on a dead/observer bot
// can lead to crashes due to invalid entity state or accessing components that shouldn't be accessed.
if ( !me->IsAlive() )
{
return ChangeTo( new CNEOBotDead, "I'm actually dead" );
}

// TEAM_UNASSIGNED -> deathmatch
if ( me->GetTeamNumber() != TEAM_JINRAI && me->GetTeamNumber() != TEAM_NSF && me->GetTeamNumber() != TEAM_UNASSIGNED )
{
Expand Down
7 changes: 6 additions & 1 deletion src/game/server/neo/bot/behavior/neo_bot_dead.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ ActionResult< CNEOBot > CNEOBotDead::Update( CNEOBot *me, float interval )
{
if ( g_pGameRules->FPlayerCanRespawn( me ) )
{
respawn( me, !me->IsObserver() );
// Defer respawn to outside of the behavior update loop.
// Calling respawn() here triggers CNEOBot::Spawn -> INextBot::Reset -> delete m_behavior.
// If we delete the behavior while we are executing a method of an action belonging to it,
// we cause a use-after-free when the stack unwinds back to Behavior::Update.
me->m_bWantsRespawn = true;
me->m_bRespawnCopyCorpse = !me->IsObserver();
}
}

Expand Down
30 changes: 25 additions & 5 deletions src/game/server/neo/bot/neo_bot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,9 @@ CNEOBot::CNEOBot()
m_maxVisionRangeOverride = -1.0f;
m_squadFormationError = 0.0f;

m_bWantsRespawn = false;
m_bRespawnCopyCorpse = false;

SetAutoJump(0.f, 0.f);

V_memcpy(&m_profile, &FIXED_DEFAULT_PROFILE, sizeof(CNEOBotProfile));
Expand Down Expand Up @@ -657,6 +660,9 @@ void CNEOBot::Spawn()

m_qPrevShouldAim = ANSWER_NO;
m_flLastShouldAimTime = 0.0f;

m_bWantsRespawn = false;
m_bRespawnCopyCorpse = false;
}


Expand All @@ -681,10 +687,21 @@ void CNEOBot::SetMission(MissionType mission, bool resetBehaviorSystem)


//-----------------------------------------------------------------------------------------------------
extern void respawn(CBaseEntity* pEdict, bool fCopyCorpse);
void CNEOBot::PhysicsSimulate(void)
{
BaseClass::PhysicsSimulate();

if (m_bWantsRespawn)
{
m_bWantsRespawn = false;
respawn(this, m_bRespawnCopyCorpse);
// Note: respawn() triggers Reset(), which deletes the behavior.
// Since we are outside BaseClass::PhysicsSimulate() (which calls Update()),
// it is safe to delete the behavior now.
return;
}

if (m_spawnArea == NULL)
{
m_spawnArea = GetLastKnownArea();
Expand Down Expand Up @@ -1023,7 +1040,10 @@ void CNEOBot::DisableCloak(void)

bool CNEOBot::IsDormantWhenDead(void) const
{
return false;
// When a bot becomes an Observer (e.g., in Juggernaut mode after death without respawn), it should become dormant.
// This stops the NextBot update loop (INextBot::Update), preventing it from trying to run behaviors
// on a non-existent or spectator body, which causes crashes (use-after-free/dangling pointers).
return IsObserver();
}


Expand Down Expand Up @@ -2496,10 +2516,10 @@ bool CNEOBot::IsEnemy(const CBaseEntity* them) const
}
else
{
if (them->GetTeamNumber() == TEAM_UNASSIGNED)
return true;

return false;
// Since we are now forcing bots into JINRAI/NSF teams even in DM to avoid the TEAM_UNASSIGNED crash,
// we must explicitly tell them to treat everyone as an enemy, otherwise they will treat their "teammates" as friends and not fight.
// In non-Teamplay modes (like DM), everyone else is an enemy.
return true;
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/game/server/neo/bot/neo_bot.h
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,9 @@ class CNEOBot : public NextBotPlayer< CNEO_Player >, public CGameEventListener
int m_iIntendTeam = 0;
int m_iProfileIdx = -1;

bool m_bWantsRespawn = false;
bool m_bRespawnCopyCorpse = false;

private:
CNEOBotLocomotion *m_locomotor;
CNEOBotBody *m_body;
Expand Down
5 changes: 4 additions & 1 deletion src/game/server/neo/bot/neo_bot_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,10 @@ void CNEOBotManager::MaintainBotQuota()
{
int iTeam = TEAM_UNASSIGNED;

if ( NEORules()->IsTeamplay() && iTeam == TEAM_UNASSIGNED )
// In DeathMatch (DM), bots must still be assigned to valid teams (JINRAI/NSF) instead of TEAM_UNASSIGNED.
// TEAM_UNASSIGNED forces bots into a limbo/Observer state that the bot logic isn't designed to handle, leading to crashes.
// By forcing them onto teams, we ensure they have a valid state and can participate in the DM logic (treating everyone as enemy).
if ( iTeam == TEAM_UNASSIGNED )
{
CTeam* pJinrai = GetGlobalTeam(TEAM_JINRAI);
CTeam* pNSF = GetGlobalTeam(TEAM_NSF);
Expand Down