Game/Entity/EntitySpawnerEntity.cs
using Sandbox.UI;

/// <summary>
/// A world-placed SENT that spawns another SENT at its location.
/// Can be triggered manually via player input or automatically on a timer.
/// </summary>
[Alias( "entity_spawner" )]
public sealed class EntitySpawnerEntity : Component, IPlayerControllable
{
	/// <summary>
	/// The SENT to spawn.
	/// </summary>
	[Property, ClientEditable]
	public ScriptedEntity Entity { get; set; }

	/// <summary>
	/// Input binding that triggers a manual spawn when the player uses this entity.
	/// </summary>
	[Property, Sync, ClientEditable, Group( "Input" )]
	public ClientInput SpawnInput { get; set; }

	/// <summary>
	/// When enabled, spawns the entity automatically every <see cref="SpawnInterval"/> seconds.
	/// </summary>
	[Property, ClientEditable, Group( "Auto Spawn" )]
	public bool AutoSpawn { get; set; } = false;

	/// <summary>
	/// Seconds between automatic spawns.
	/// </summary>
	[Property, ClientEditable, Range( 1f, 300f ), Step( 1 ), Group( "Auto Spawn" )]
	public float SpawnInterval { get; set; } = 5f;

	/// <summary>
	/// Maximum number of entities spawned by this spawner allowed to exist at once.
	/// New spawns are suppressed until existing ones are destroyed.
	/// </summary>
	[Property, ClientEditable, Range( 1, 50 ), Step( 1 ), Group( "Auto Spawn" )]
	public float MaxEntities { get; set; } = 5;

	private TimeSince _timeSinceLastSpawn;
	private readonly List<WeakReference<GameObject>> _spawnedEntities = new();

	protected override void OnUpdate()
	{
		if ( IsProxy ) return;
		if ( !AutoSpawn ) return;
		if ( _timeSinceLastSpawn < SpawnInterval ) return;

		DoSpawn();
	}

	void IPlayerControllable.OnControl()
	{
		if ( SpawnInput.Pressed() )
			DoSpawn();
	}

	void IPlayerControllable.OnStartControl() { }
	void IPlayerControllable.OnEndControl() { }

	[Rpc.Host]
	private void DoSpawn()
	{
		if ( !Entity.IsValid() || Entity.Prefab is null ) return;

		// Prune destroyed entities from the tracking list
		_spawnedEntities.RemoveAll( wr => !wr.TryGetTarget( out var go ) || !go.IsValid() );

		if ( _spawnedEntities.Count >= MaxEntities ) return;

		_timeSinceLastSpawn = 0;

		var spawned = GameObject.Clone( Entity.Prefab, new CloneConfig
		{
			Transform = WorldTransform,
			StartEnabled = false,
		} );

		spawned.Tags.Add( "removable" );

		var caller = Rpc.Caller ?? GameObject.Network.Owner;
		var player = Player.FindForConnection( caller );

		Ownable.Set( spawned, caller );
		spawned.NetworkSpawn( true, null );
		spawned.Enabled = true;

		_spawnedEntities.Add( new WeakReference<GameObject>( spawned ) );

		if ( player is not null )
		{
			var undo = player.Undo.Create();
			undo.Name = $"Spawn {Entity.Title ?? Entity.ResourceName}";
			undo.Add( spawned );
		}
	}
}