Crew.cs
using Sandbox;
using SWB.Player;
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. Looks up the PlayerBase component rather than walking the whole
// scene hierarchy looking for a tag. One component per player, and
// no way for a bone or a bit of clothing to be mistaken for one.
//
// 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();
// Found by COMPONENT, not by tag.
//
// Tags are inherited down a hierarchy, and the player model has
// CreateBoneObjects switched on - so every bone, every piece of
// clothing and every camera under the player root also answers to
// the "player" tag. A tag search returned 174 objects for a single
// player, and anything picking "the nearest player" out of that
// got a finger bone.
//
// A PlayerBase component exists exactly once per player, on the
// root. Filtering out bots excludes the cops, which are PlayerBase
// too - that's how they get health and weapons for free.
foreach ( var player in scene.GetAllComponents<PlayerBase>() )
{
if ( !player.IsValid() || player.IsBot )
continue;
var go = player.GameObject;
// 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>
/// Drop everything cached from the current scene.
/// </summary>
///
/// Call this immediately BEFORE a scene change, not after.
///
/// These statics exist to survive scene loads, which is what makes
/// them useful - and also what makes them dangerous. _cachedScene is a
/// reference to a Scene and _active holds GameObjects inside it, so
/// for as long as they sit here the engine cannot let go of the scene
/// we just left: its objects, its physics bodies and its network
/// registrations all stay reachable. Two jobs in a row then behave
/// differently from the first, because the first one is still around.
///
/// Comparing _cachedScene against the live scene (as ActivePlayers
/// does) is enough to keep the ANSWERS correct, but it does nothing
/// about the reference itself - that only clears the next time someone
/// asks a question, which may be long after the damage is done.
public static void Reset()
{
_active.Clear();
_cachedScene = null;
_cachedAt = float.NegativeInfinity;
}
/// <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 ) );
}
}