NetworkManager.cs
using System;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Sandbox.Mapping;

namespace Sandbox;

public sealed class NetworkManager : Component, Component.INetworkListener
{
	public static NetworkManager Instance { get; private set; }

	public readonly List<PlayerSession> PlayerSessions = [];

	[Property] public GameObject PlayerPrefab { get; set; }
	[Property] GameObject VRHandsPrefab { get; set; }

	/// <summary>
	/// Create a server (if we're not joining one)
	/// </summary>
	[Property] public bool StartServer { get; set; } = true;

	/// <summary>When true, VR clients skip the height calibration screen and spawn with the default eye height.</summary>
	[Property] public bool SkipHeightCalibration { get; set; }

	/// <summary>When true, the host will pick a random map from the map pool if no map is specified.</summary>
	[Property] public bool PickRandomMapOnEmptyMapName { get; set; } = true;

	[Sync, Change( nameof( OnMapChange ) )] public string MapName { get; private set; } = LaunchArguments.Map;

	private MapInstance _mapInstance;
	private GameObject _loadingScreenGo;
	private MapLoadingScreen _loadingScreen;

	/// <summary>True when the map is loaded, or no map was requested.</summary>
	public bool IsMapReady =>
		string.IsNullOrEmpty( MapName )
		|| ( _mapInstance is not null && _mapInstance.IsLoaded );

	protected override void OnStart()
	{
		Instance = this;

		if ( string.IsNullOrEmpty( MapName ) && Networking.IsHost && PickRandomMapOnEmptyMapName )
		{
			var randomMap = PickRandomMap();
			if ( !string.IsNullOrEmpty( randomMap ) )
				MapName = randomMap;
		}

		if ( _mapInstance is null )
			CreateMapInstance( MapName );

		Log.Info( $"Map name: {MapName}" );

		if ( Game.IsRunningInVR )
		{
			VRHandsPrefab.Clone( new Transform( Vector3.Zero, Rotation.Identity ) );
		}
	}

	protected override void OnDestroy()
	{
		DestroyMapInstance();

		if ( _loadingScreenGo.IsValid() )
			_loadingScreenGo.Destroy();

		if ( Instance == this )
			Instance = null;
	}

	private string PickRandomMap()
	{
		var deathmatch = DeathmatchGame.Instance ?? Scene.GetAll<DeathmatchGame>().FirstOrDefault();
		var pool = (deathmatch?.MapPool ?? [])
			.Where( m => !string.IsNullOrWhiteSpace( m ) )
			.Select( m => m.Trim() )
			.Distinct( StringComparer.OrdinalIgnoreCase )
			.ToList();

		if ( pool.Count == 0 )
		{
			Log.Warning( "Host has no MapName and the map pool is empty" );
			return null;
		}

		return pool[Game.Random.Int( 0, pool.Count - 1 )];
	}

	private void CreateMapInstance( string mapName )
	{
		var mapGO = new GameObject( "Map" );
		mapGO.NetworkMode = NetworkMode.Never;
		_mapInstance = mapGO.AddComponent<MapInstance>( startEnabled: false );
		_mapInstance.OnMapLoaded += OnMapLoaded;
		_mapInstance.OnMapUnloaded += OnMapUnloaded;
		_mapInstance.Enabled = true;
		_mapInstance.MapName = mapName;
	}

	private void DestroyMapInstance()
	{
		if ( _mapInstance is null )
			return;

		_mapInstance.OnMapLoaded -= OnMapLoaded;
		_mapInstance.OnMapUnloaded -= OnMapUnloaded;

		if ( _mapInstance.GameObject.IsValid() )
			_mapInstance.GameObject.Destroy();

		_mapInstance = null;
	}

	protected override async Task OnLoad()
	{
		if ( Scene.IsEditor )
			return;

		if ( StartServer && !Networking.IsActive )
		{
			LoadingScreen.Title = "Creating Lobby";
			await Task.DelayRealtimeSeconds( 0.1f );
			Networking.CreateLobby( new() );
		}
	}

	public void OnActive( Connection channel )
	{
		AddPlayerSession( channel );
		TrySpawnPlayer( channel );
	}

	private void AddPlayerSession( Connection channel )
	{
		var sessionGO = new GameObject( $"PlayerSession_{channel.Id}" );
		sessionGO.AddComponent<PlayerSession>();
		sessionGO.NetworkSpawn( channel );
	}

	public void RespawnPlayer( Connection channel )
	{
		if ( !Networking.IsHost || channel is null )
			return;

		var session = GetPlayerSession( channel.Id );
		if ( !CanSpawn( session ) )
			return;

		var players = Scene.GetAll<Player>();
		var player = players.FirstOrDefault( p => p.Network.OwnerId == channel.Id );
		player?.GameObject.Destroy();

		SpawnPlayer( channel );
	}

	public void RespawnAllPlayers()
	{
		foreach ( var session in PlayerSessions )
		{
			RespawnPlayer( session.Network.Owner );
		}
	}

	/// <summary>Spawns the connection's pawn when map and calibration/ready gates pass.</summary>
	public void TrySpawnPlayer( Connection channel )
	{
		if ( !Networking.IsHost || channel is null )
			return;

		if ( !IsMapReady )
			return;

		var session = GetPlayerSession( channel.Id );
		if ( !CanSpawn( session ) )
			return;

		var existing = Scene.GetAll<Player>().FirstOrDefault( p => p.Network.OwnerId == channel.Id );
		if ( existing.IsValid() )
			return;

		SpawnPlayer( channel );
	}

	private static bool CanSpawn( PlayerSession session ) =>
		session.IsValid() && session.IsReadyToSpawn;

	private void SpawnPlayer( Connection channel )
	{
		var clothing = new ClothingContainer();
		clothing.Deserialize( channel.GetUserData( "avatar" ) );

		var session = GetPlayerSession( channel.Id );
		var spawnTransform = GetSpawnTransform( session );

		var player = PlayerPrefab.Clone( spawnTransform );

		// Assume that if they have a skinned model renderer, it's the citizen's body
		var avatarObject = player.Children.Find( c => c.Name == "Avatar" );
		if ( avatarObject.IsValid() )
		{
			var skinnedModelRenderer = avatarObject.GetComponent<SkinnedModelRenderer>();
			if ( skinnedModelRenderer is not null )
			{
				clothing.Apply( skinnedModelRenderer );
			}
		}

		player.NetworkSpawn( channel );
	}

	private Transform GetSpawnTransform( PlayerSession session )
	{
		if ( session.IsValid() && session.HasReservedSpawn )
			return new Transform( session.ReservedSpawnPosition, session.ReservedSpawnRotation );

		var spawnPoints = Scene.GetAllComponents<SpawnPoint>().ToArray();
		if ( spawnPoints.Length == 0 )
			return new Transform();

		return spawnPoints[Game.Random.Int( 0, spawnPoints.Length - 1 )].Transform.World;
	}

	private void OnMapUnloaded()
	{
		Log.Info( "Map unloaded" );
	}

	private void OnMapLoaded()
	{
		Log.Info( "Map loaded" );
		HideMapLoadingScreen();

		if ( Networking.IsHost )
		{
			TerrorTownDoorCompat.ConvertAll( _mapInstance.GameObject );

			var doors = Scene.GetAll<Door>();
			foreach ( var door in doors )
			{
				door.GameObject.NetworkSpawn();
			}

			DeathmatchGame.Instance.StartMatch();
		}
	}

	private void OnMapChange( string oldValue, string newValue )
	{
		Log.Info( $"Changing map to {newValue}" );
		DestroyMapInstance();
		ShowMapLoadingScreen( newValue );
		CreateMapInstance( newValue );
		var session = GetPlayerSession( Connection.Local.Id );
		session?.ClearStats();
	}

	private void ShowMapLoadingScreen( string mapIdent )
	{
		EnsureMapLoadingScreen();
		_loadingScreenGo.Enabled = true;
		_loadingScreen.Show( mapIdent );
	}

	private void HideMapLoadingScreen()
	{
		if ( _loadingScreenGo.IsValid() )
			_loadingScreenGo.Enabled = false;
	}

	private void EnsureMapLoadingScreen()
	{
		if ( _loadingScreenGo.IsValid() )
			return;

		_loadingScreenGo = new GameObject( true, "MapLoadingScreen" )
		{
			NetworkMode = NetworkMode.Never
		};

		var screenPanel = _loadingScreenGo.AddComponent<ScreenPanel>();
		screenPanel.ZIndex = 1000;
		_loadingScreen = _loadingScreenGo.AddComponent<MapLoadingScreen>();
	}

	public PlayerSession GetPlayerSession( Guid ConnectionId )
	{
		return PlayerSessions.FirstOrDefault( ps => ps.Network.Owner.Id == ConnectionId );
	}

	/// <summary>Fetches the map package and syncs <see cref="MapName"/> so every peer loads it.</summary>
	public async Task<bool> SetMap( string map )
	{
		if ( string.IsNullOrWhiteSpace( map ) )
			return false;

		var mapPackage = await Package.FetchAsync( map, false );
		if ( mapPackage is null )
		{
			Log.Error( $"Map {map} not found" );
			return false;
		}

		Log.Info( $"Setting map to {map}" );
		MapName = map;
		return true;
	}

	[ConCmd( "map", ConVarFlags.Server )]
	public static async Task SetMapCommand( Connection caller, string map )
	{
		if ( !caller.IsHost )
			return;

		await Instance.SetMap( map );
	}

	[ConCmd( "maps" )]
	public static void ShowMapsCommand()
	{
		Log.Info( "Available maps:" );
		foreach ( var mapName in DeathmatchGame.Instance.MapPool )
		{
			Log.Info( $"- {mapName}" );
		}
	}
}