GameLoop/GameManager.cs
public sealed partial class GameManager : GameObjectSystem<GameManager>, Component.INetworkListener, ISceneStartup
{
	public GameManager( Scene scene ) : base( scene )
	{
		Listen( Stage.FinishUpdate, 0, Update, "Update" );
	}

	void ISceneStartup.OnHostInitialize()
	{
		Scene.NavMesh.AgentRadius = 20;
		Scene.NavMesh.AgentHeight = 72;
		Scene.NavMesh.IsEnabled = true;
		BotIdCounter = 0;

		var lobbyPrivacy = GameSettings.IsBenchmark ? Sandbox.Network.LobbyPrivacy.Private : Sandbox.Network.LobbyPrivacy.Public;

		// If we're not hosting a lobby, start hosting one
		// so that people can join this game.
		Networking.CreateLobby( new Sandbox.Network.LobbyConfig() { Privacy = lobbyPrivacy, MaxPlayers = 32, Name = "Sandbox Deathmatch", DestroyWhenHostLeaves = true } );
	}

	public void Update()
	{
		if ( Scene.IsEditor ) return;

		if ( GameSettings.Debug > 0 )
		{
			var debugString = $"""
				Time: {Time.Now:n0}
				CurrentGameStage: {CurrentGameStage}
				Game Time Remaining: {GameTimeRemaining:n0}
				""";

			DebugOverlaySystem.Current.ScreenText( 30, debugString, 13, flags: TextFlag.LeftTop );
		}

		StageUpdate();
	}

	void Component.INetworkListener.OnActive( Connection channel )
	{
		channel.CanSpawnObjects = false;

		var playerData = CreatePlayerInfo( channel );
		SpawnPlayer( playerData );

		// First player? Restart the game loop
		if ( Connection.All.Count < 2 )
		{
			RestartGame();
		}

		Notify( $"{channel.DisplayName} has joined the game" );
	}

	/// <summary>
	/// Called when someone leaves the server. This will only be called for the host.
	/// </summary>
	void Component.INetworkListener.OnDisconnected( Connection channel )
	{
		var pd = PlayerData.For( channel );
		if ( pd is not null )
		{
			pd.GameObject.Destroy();
		}

		// If we have no more players, restart the game loop
		if ( !Connection.All.Any() )
		{
			RestartGame();
		}

		Notify( $"{channel.DisplayName} has left the game" );
	}

	private PlayerData CreatePlayerInfo( Connection channel )
	{
		var go = new GameObject( true, $"PlayerInfo - {channel.DisplayName}" );
		var data = go.AddComponent<PlayerData>();
		data.SteamId = (long)channel.SteamId;
		data.PlayerId = channel.Id;
		data.DisplayName = channel.DisplayName;

		go.NetworkSpawn( null );
		go.Network.SetOwnerTransfer( OwnerTransfer.Fixed );

		return data;
	}

	public void SpawnPlayer( Connection connection ) => SpawnPlayer( PlayerData.For( connection ) );

	public void SpawnPlayer( PlayerData playerData )
	{
		Assert.NotNull( playerData, "PlayerData is null" );
		Assert.True( Networking.IsHost, $"Client tried to SpawnPlayer: {playerData.DisplayName}" );

		if ( CurrentGameStage != GameManager.GameStage.Game )
		{
			// spawn spectator?
			return;
		}

		// does this connection already have a player?
		if ( Scene.GetAll<Player>().Where( x => x.Network.Owner?.Id == playerData.PlayerId ).Any() )
			return;

		// Find a spawn location for this player
		var startLocation = FindSpawnLocation().WithScale( 1 );

		// Spawn this object and make the client the owner
		var playerGo = GameObject.Clone( "/player.prefab", new CloneConfig { Name = playerData.DisplayName, StartEnabled = false, Transform = startLocation } );
		var player = playerGo.GetComponent<Player>( true );
		player.PlayerData = playerData;

		if ( playerData.IsBot )
		{
			var playerBot = playerGo.AddComponent<PlayerBotController>();
			playerGo.NetworkSpawn( null );

			playerGo.GetComponent<PlayerStats>().Enabled = false;

			player.Controller.UseInputControls = false;
			player.Controller.UseCameraControls = false;

			var dresser = playerGo.GetComponent<Dresser>();
			if ( dresser.IsValid() )
			{
				dresser.Source = Dresser.ClothingSource.Manual;
				dresser.Randomize();
			}
		}
		else
		{
			var owner = Connection.Find( playerData.PlayerId );
			// Main player will get a bot controller too if we're benchmarking
			if ( GameSettings.IsBenchmark )
			{
				var playerBot = playerGo.AddComponent<PlayerBotController>();
				player.Controller.UseInputControls = false;
			}
			playerGo.NetworkSpawn( owner );
		}

		IPlayerEvent.PostToGameObject( player.GameObject, x => x.OnSpawned() );
		player.EquipBestWeapon();

		PlayerSpawnEffects( playerGo.WorldPosition + Vector3.Up * 40 );
	}

	public void SpawnPlayerDelayed( PlayerData playerData )
	{
		GameTask.RunInThreadAsync( async () =>
		{
			await Task.Delay( 4000 );
			await GameTask.MainThread();
			if ( Current is not null )
				Current.SpawnPlayer( playerData );
		} );
	}

	[Rpc.Broadcast]
	public static void PlayerSpawnEffects( Vector3 center )
	{
		if ( Application.IsDedicatedServer ) return;

		Sound.Play( "audio/sounds/player_spawn.sound", center );
	}

	/// <summary>
	/// In the editor, spawn the player where they're looking
	/// </summary>
	public static Transform EditorSpawnLocation { get; set; }

	/// <summary>
	/// Find the most appropriate place to respawn
	/// </summary>
	Transform FindSpawnLocation()
	{

		//
		// If we have any SpawnPoint components in the scene, then use those
		//
		var spawnPoints = Scene.GetAllComponents<SpawnPoint>().ToArray();

		if ( spawnPoints.Length == 0 )
		{
			if ( Application.IsEditor && !EditorSpawnLocation.Position.IsNearlyZero() )
			{
				return EditorSpawnLocation;
			}

			return Transform.Zero;
		}

		var players = Scene.GetAll<Player>();

		if ( !players.Any() )
		{
			return Random.Shared.FromArray( spawnPoints ).Transform.World;
		}

		//
		// Find spawnpoint furthest away from any players
		// TODO: in the future we may want a different logic, as spawning far away is not necessarily good.
		// But good enough for now and also reduces chances of players from spawning on top of  or inside each other.
		//
		SpawnPoint spawnPointFurthestAway = null;
		float spawnPointFurthestAwayDistanceSqr = float.MinValue;

		foreach ( var spawnPoint in spawnPoints )
		{
			float closestPlayerDistanceToSpawnpointSqr = float.MaxValue;

			foreach ( var player in players )
			{
				float playerDistanceToSpawnPointSqr = (spawnPoint.Transform.World.Position - player.Transform.World.Position).LengthSquared;

				if ( playerDistanceToSpawnPointSqr < closestPlayerDistanceToSpawnpointSqr )
				{
					closestPlayerDistanceToSpawnpointSqr = playerDistanceToSpawnPointSqr;
				}
			}

			if ( closestPlayerDistanceToSpawnpointSqr > spawnPointFurthestAwayDistanceSqr )
			{
				spawnPointFurthestAwayDistanceSqr = closestPlayerDistanceToSpawnpointSqr;
				spawnPointFurthestAway = spawnPoint;
			}
		}

		return spawnPointFurthestAway.Transform.World;
	}

	[Rpc.Broadcast]
	private static void SendMessage( string msg )
	{
		Log.Info( msg );
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	private void Notify( string text )
	{
		Sandbox.Platform.Chat.AddText( text );
	}

	/// <summary>
	/// Called on the host when a player is killed
	/// </summary>
	public void OnDeath( Player player, DamageInfo dmg )
	{
		Assert.True( Networking.IsHost );

		Assert.True( player.IsValid(), "Player invalid" );
		Assert.True( player.PlayerData.IsValid(), $"{player.GameObject.Name}'s PlayerData invalid" );

		var weapon = dmg.Weapon;

		var attacker = dmg.Attacker?.GetComponent<Player>();
		bool isSuicide = attacker == player;

		if ( !isSuicide && attacker.IsValid() )
		{
			attacker.PlayerData.Kills++;
			attacker.PlayerData.AddStat( $"kills" );

			if ( weapon.IsValid() )
			{
				attacker.PlayerData.AddStat( $"kills.{weapon.Name}" );
			}

			Scene.RunEvent<KillTracker>( x => x.OnPlayerKilled( attacker.PlayerData, player.PlayerData, dmg ) );
		}

		player.PlayerData.Deaths++;

		var w = weapon.IsValid() ? weapon.GetComponentInChildren<IKillIcon>() : null;
		Scene.RunEvent<Feed>( x => x.NotifyDeath( player.PlayerData, attacker?.PlayerData, w?.DisplayIcon, dmg.Tags ) );

		string attackerName = attacker.IsValid() ? attacker.DisplayName : dmg.Attacker?.Name ?? "unknown";
		if ( string.IsNullOrEmpty( attackerName ) )
			SendMessage( $"{player.DisplayName} died (tags: {dmg.Tags})" );
		else if ( weapon.IsValid() )
			SendMessage( $"{attackerName} killed {(isSuicide ? "self" : player.DisplayName)} with {weapon.Name} (tags: {dmg.Tags})" );
		else
			SendMessage( $"{attackerName} killed {(isSuicide ? "self" : player.DisplayName)} (tags: {dmg.Tags})" );
	}
}