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 ) );
	}
}