Crew.cs
using Sandbox;
using System;
using System.Collections.Generic;
// Who's still in the fight. "Active" means a player who hasn't escaped and
// hasn't been eliminated - the same test several systems need, so it lives
// in one place rather than being re-written per caller.
//
// This gets asked a LOT: the spawner reads it twice a frame, and every cop
// reads it several times a second to pick a target. So it does two things
// to stay cheap:
//
// 1. Uses Scene.FindAllWithTag, which is an index the engine keeps as
// tags are added and removed, rather than Scene.GetAllObjects, which
// walks the entire hierarchy. On a real map that's thousands of
// GameObjects visited per call.
//
// 2. Caches the result for the rest of the frame. Every caller in a
// frame gets the same answer anyway, so recomputing it per caller is
// pure waste.
public static class Crew
{
// The cached answer, plus enough to know when it went stale. Static
// rather than per-component because the question isn't about any one
// object - and it has to survive scene loads, which _cachedScene
// handles by forcing a rebuild when the scene changes underneath us.
static readonly List<GameObject> _active = new();
static Scene _cachedScene;
static float _cachedAt = float.NegativeInfinity;
/// <summary>
/// Every player still in the fight. The returned list is shared and
/// rebuilt each frame - read it, don't hold onto it.
/// </summary>
public static IReadOnlyList<GameObject> ActivePlayers( Scene scene )
{
if ( scene is null )
return Array.Empty<GameObject>();
// Time.Now is constant for the whole of a frame, so comparing
// against it is "have we already answered this frame?". The scene
// check catches a mission load, where the old scene's players are
// gone but no time may have passed yet.
if ( _cachedScene == scene && _cachedAt == Time.Now )
return _active;
_cachedScene = scene;
_cachedAt = Time.Now;
_active.Clear();
foreach ( var go in scene.FindAllWithTag( "player" ) )
{
// Out of play, not out of existence: MissionPlayerState leaves
// eliminated and escaped players enabled so their camera keeps
// running for spectating, and marks them with a tag instead.
if ( !go.IsValid() || !go.Enabled )
continue;
if ( go.Tags.Has( "escaped" ) || go.Tags.Has( "eliminated" ) )
continue;
_active.Add( go );
}
return _active;
}
/// <summary>
/// How many players are still in the fight, never below 1.
/// </summary>
public static int ActiveCount( Scene scene )
{
// Floored at 1 so anything scaling off this can divide or multiply
// safely. A count of zero only happens the instant the job ends,
// at which point nothing is reading it anyway.
return Math.Max( ActivePlayers( scene ).Count, 1 );
}
/// <summary>
/// The number of players difficulty should scale off: how many are
/// still fighting, but never more than there are people connected.
/// </summary>
public static int ScalingSize( Scene scene )
{
// The tag count is the honest answer to "who's still fighting",
// and it's what makes pressure drop as the crew gets whittled
// down. But it's a count of GameObjects, and anything that leaves
// a stray player-tagged object behind - a duplicate spawned across
// a scene change, a corpse that didn't get cleaned up - would read
// as extra players and multiply the entire police response.
//
// The connection count can't drift like that: it's one per human,
// straight from the network layer. Clamping to it means a bug of
// that shape costs us accuracy at worst, instead of flooding the
// map with cops until the game stops responding.
var connected = Connection.All?.Count ?? 1;
return Math.Clamp( ActiveCount( scene ), 1, Math.Max( connected, 1 ) );
}
}