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