Session manager component for the Mansion game. It handles lobby creation, player spawn prefab management, per-connection player GameObject bookkeeping, spawn point selection and seating on floor for the Bathrooms level, and connection lifecycle (OnActive/OnDisconnected).
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using Sandbox;
namespace BrickJam;
/// <summary>
/// Core game/session manager. Scene-System port of the legacy <c>MansionGame : GameManager</c>.
///
/// In the Entity System this derived from <c>GameManager</c> and used <c>ClientJoined</c>/
/// <c>ClientDisconnect</c>. In the Scene System a session manager is a <see cref="Component"/>
/// that implements <see cref="Component.INetworkListener"/> and spawns a player prefab per
/// <see cref="Connection"/>.
/// </summary>
[Title( "Mansion Game" )]
[Category( "Game" )]
[Icon( "casino" )]
public sealed partial class MansionGame : Component, Component.INetworkListener
{
public static MansionGame Instance { get; private set; }
/// <summary>
/// Create a server (lobby) if we're not already in a network session.
/// </summary>
[Property] public bool StartServer { get; set; } = true;
/// <summary>
/// The prefab cloned and network-spawned for each connected player.
/// </summary>
[Property] public GameObject PlayerPrefab { get; set; }
/// <summary>
/// Which level the session is currently in. Replaces the old <c>CurrentLevel.Type</c> lookup
/// while the full Level subsystem is still being ported. The Level subsystem will own setting this.
/// </summary>
// FromHost: the GameManager is an unowned snapshot object and this is written host-side only (SetLevel),
// so plain [Sync] wouldn't reliably reach clients - which also drives client-side music/level visuals.
[Property, Sync( SyncFlags.FromHost )] public LevelType CurrentLevelType { get; set; } = LevelType.None;
public static int Seed { get; set; } = 0;
public static Random Random { get; set; } = new Random();
// Server-side bookkeeping of the GameObject spawned for each connection, so we can clean it
// up on disconnect. Not networked - only meaningful on the host.
private readonly Dictionary<Guid, GameObject> spawnedPlayers = new();
protected override void OnAwake()
{
Instance = this;
Upgrading.Creator.Build();
}
protected override void OnDestroy()
{
if ( Instance == this )
Instance = 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 static void ResetRandomSeed()
{
if ( !Networking.IsHost ) return;
Seed = DateTime.UtcNow.Ticks.GetHashCode();
Random = new Random( Seed );
}
/// <summary>
/// A client has finished loading and entered the game. Host-only.
/// Replaces the legacy <c>ClientJoined</c>.
/// </summary>
public void OnActive( Connection channel )
{
Log.Info( $"Player '{channel.DisplayName}' has joined the game" );
GiveSlot( channel );
// Joined mid-round past the grace window: spectate until the next level (legacy ClientJoined).
if ( ShouldSpectateOnJoin )
SpawnSpectator( channel, GetSpawnPoint() );
else
SpawnPlayer( channel );
}
/// <summary>
/// A client has disconnected. Host-only. Replaces the legacy <c>ClientDisconnect</c>.
/// </summary>
public void OnDisconnected( Connection channel )
{
ReleaseSlot( channel );
RemoveSpectator( channel );
if ( spawnedPlayers.Remove( channel.Id, out var go ) && go.IsValid() )
go.Destroy();
}
private void SpawnPlayer( Connection channel )
{
if ( !PlayerPrefab.IsValid() )
{
Log.Warning( "MansionGame.PlayerPrefab is not set - cannot spawn player." );
return;
}
var spawn = GetSpawnPoint();
var go = PlayerPrefab.Clone( spawn, name: $"Player - {channel.DisplayName}" );
go.NetworkSpawn( channel );
spawnedPlayers[channel.Id] = go;
}
public Transform GetSpawnPoint() => GetSpawnPoint( CurrentLevelType );
public Transform GetSpawnPoint( LevelType level )
{
var spawnPoints = Scene.GetAllComponents<PlayerSpawn>()
.Where( x => x.LevelType == level )
.ToList();
var marker = Random.Shared.FromList( spawnPoints, null );
if ( marker is not null )
{
var t = marker.WorldTransform.WithScale( 1f );
// Bathrooms ONLY: its single PlayerSpawn marker is authored up near the ceiling in bathrooms.vmap,
// so spawning on it verbatim drops everyone into the roof. Until the marker is moved in Hammer,
// seat bathroom spawns on the floor by tracing straight DOWN from the marker. Every other level
// keeps its verbatim marker (placed correctly) - so this can't relocate a good spawn (which is what
// broke the dungeon last time we floor-snapped globally).
if ( level == LevelType.Bathrooms )
t = SeatOnFloor( t );
return t;
}
// Only when a level has NO marker at all: drop into the centre of the level's region rather than the
// game-manager origin (which is up in the sky for a deep level). Add PlayerSpawn entities for the level.
Log.Warning( $"[MansionGame] No PlayerSpawn for level '{level}'; using the level's WorldBox centre. " +
$"Add PlayerSpawn entities with LevelType={level} to the map." );
var box = CurrentLevel?.WorldBox ?? new BBox( WorldPosition - 128f, WorldPosition + 128f );
return new Transform( box.Center.WithZ( box.Mins.z ), WorldRotation ).WithScale( 1f );
}
/// <summary>
/// Seat a spawn transform on solid floor by tracing straight down from just above it. A strictly-downward
/// ray can only hit a top surface, so it never lands the player "inside the roof"; if nothing solid is
/// below (within the level's WorldBox), the original transform is returned untouched. Used for the
/// Bathrooms level whose marker sits near the ceiling.
/// </summary>
private Transform SeatOnFloor( Transform spawn )
{
// Start AT the marker (it sits in open air up near the ceiling) and trace straight down. Starting any
// higher risks beginning above the ceiling slab and landing the player on top of it.
var from = spawn.Position;
var bottomZ = (CurrentLevel?.WorldBox.Mins.z ?? spawn.Position.z - 1024f) - 64f;
var to = spawn.Position.WithZ( bottomZ );
var trace = Scene.Trace.Ray( from, to )
.WithoutTags( "player", "npc", "loot", "nocollide", "trigger", "usable", "door" )
.Run();
return trace.Hit
? spawn.WithPosition( trace.HitPosition + Vector3.Up * 8f )
: spawn;
}
}