GameLoop/GameManager.cs
using Sandbox.UI;
public sealed partial class GameManager : GameObjectSystem<GameManager>, Component.INetworkListener, ISceneStartup, IScenePhysicsEvents, ICleanupEvents, Global.ISaveEvents
{
public GameManager( Scene scene ) : base( scene )
{
}
void ISceneStartup.OnHostInitialize()
{
if ( !Networking.IsActive )
{
Networking.CreateLobby( new Sandbox.Network.LobbyConfig() { Privacy = Sandbox.Network.LobbyPrivacy.Public, MaxPlayers = 32, Name = "Sandbox", DestroyWhenHostLeaves = true } );
}
}
internal void Notify( string text )
{
Assert.True( Networking.IsHost, "Only the host can send notifications" );
NotifyRpc( text );
}
[Rpc.Broadcast( NetFlags.HostOnly )]
private void NotifyRpc( string text )
{
Sandbox.Platform.Chat.AddText( text );
}
void Component.INetworkListener.OnActive( Connection channel )
{
channel.CanSpawnObjects = false;
var playerData = CreatePlayerInfo( channel );
SpawnPlayer( playerData );
CheckConnectionAchievement( channel );
CheckFriendsOnlineStat();
}
/// <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();
}
UndoSystem.Current?.RemovePlayer( channel.SteamId );
}
private PlayerData CreatePlayerInfo( Connection channel )
{
var existingPlayerInfo = PlayerData.For( channel );
if ( existingPlayerInfo.IsValid() )
return existingPlayerInfo;
var go = new GameObject( true, $"PlayerInfo - {channel.DisplayName}" );
var data = go.AddComponent<PlayerData>();
go.NetworkSpawn( channel );
go.Network.SetOwnerTransfer( OwnerTransfer.Fixed );
return data;
}
internal void SpawnPlayer( Connection connection ) => SpawnPlayer( PlayerData.For( connection ) );
internal void SpawnPlayer( PlayerData playerData )
{
Assert.NotNull( playerData, "PlayerData is null" );
Assert.True( Networking.IsHost, $"Client tried to SpawnPlayer: {playerData.Network.Owner?.DisplayName}" );
// does this connection already have a player?
if ( Scene.GetAll<Player>().Any( x => x.Network.Owner == playerData.Network.Owner ) )
return;
// Find a spawn location for this player
var startLocation = FindSpawnLocation().WithScale( 1 );
// Fire pre-respawn event — listeners can modify spawn location
var respawnEvent = new PlayerRespawnEvent { PlayerData = playerData, SpawnLocation = startLocation };
Global.IPlayerEvents.Post( x => x.OnPlayerRespawning( respawnEvent ) );
startLocation = respawnEvent.SpawnLocation;
// Spawn this object and make the client the owner
var playerGo = GameObject.Clone( "/prefabs/engine/player.prefab", new CloneConfig { Name = playerData.Network.Owner?.DisplayName, StartEnabled = false, Transform = startLocation } );
var player = playerGo.Components.Get<Player>( true );
player.PlayerData = playerData;
var owner = playerData.Network.Owner;
playerGo.NetworkSpawn( owner );
Local.IPlayerEvents.PostToGameObject( player.GameObject, x => x.OnSpawned() );
Global.IPlayerEvents.Post( x => x.OnPlayerSpawned( player ) );
}
void Global.ISaveEvents.AfterLoad( string filename )
{
if ( !Networking.IsHost ) return;
// Make sure we spawn any players that weren't included in the loaded save
foreach ( var connection in Connection.All )
{
var playerData = CreatePlayerInfo( connection );
SpawnPlayer( playerData );
}
}
/// <summary>
/// Called by the client (via PlayerObserver) when they want to respawn.
/// </summary>
[Rpc.Host]
internal void RequestRespawn()
{
var connection = Rpc.Caller;
// Clean up any lingering observers for this connection.
foreach ( var observer in Scene.GetAllComponents<PlayerObserver>().Where( x => x.Network.Owner == connection ).ToArray() )
{
observer.GameObject.Destroy();
}
SpawnPlayer( connection );
}
/// <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 )
{
return Transform.Zero;
}
return Random.Shared.FromArray( spawnPoints ).Transform.World;
}
[Rpc.Broadcast( NetFlags.HostOnly )]
private static void NotifyConsole( string msg )
{
Log.Info( msg );
}
/// <summary>
/// Called on the host when a played is killed
/// </summary>
internal 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 source = dmg.Attacker?.GetComponentInParent<IKillSource>( true );
if ( source == null ) return;
var isSuicide = source is Player p && p == player;
if ( !isSuicide )
source.OnKill( player.GameObject );
// Fire kill event on the killer if they're a player
if ( !isSuicide && source is Player killer )
{
var killEvent = new PlayerKillEvent { Player = killer, Victim = player.GameObject, DamageInfo = dmg };
Local.IPlayerEvents.PostToGameObject( killer.GameObject, x => x.OnKill( killEvent ) );
Global.IPlayerEvents.Post( x => x.OnPlayerKill( killEvent ) );
}
player.PlayerData.Deaths++;
var weapon = dmg.Weapon;
var w = weapon.IsValid() ? weapon.GetComponentInChildren<IKillIcon>() : null;
var damageTags = dmg.Tags.ToString() + ( isSuicide ? " suicide" : "" );
var attackerTags = isSuicide ? "" : source.Tags;
var attackerName = isSuicide ? null : source.DisplayName;
var attackerSteamId = isSuicide ? 0L : source.SteamId;
var playerName = player.Network.Owner?.DisplayName ?? "Unknown";
Scene.RunEvent<Feed>( x => x.NotifyKill( playerName, attackerName, attackerSteamId, damageTags, attackerTags, "", w?.DisplayIcon ) );
if ( string.IsNullOrEmpty( attackerName ) )
{
NotifyConsole( $"{playerName} died (tags: {dmg.Tags})" );
}
else if ( weapon.IsValid() )
{
NotifyConsole( $"{attackerName} killed {(isSuicide ? "self" : playerName)} with {weapon.Name} (tags: {dmg.Tags})" );
}
else
{
NotifyConsole( $"{attackerName} killed {(isSuicide ? "self" : playerName)} (tags: {dmg.Tags})" );
}
}
/// <summary>
/// Called on the host when an NPC is killed. Credits the attacker and adds a kill feed entry.
/// </summary>
internal void OnNpcDeath( string npcName, DamageInfo dmg )
{
Assert.True( Networking.IsHost );
var source = dmg.Attacker?.GetComponent<IKillSource>();
source?.OnKill( dmg.Attacker );
var w = dmg.Weapon.IsValid() ? dmg.Weapon.GetComponentInChildren<IKillIcon>() : null;
var attackerName = source?.DisplayName;
var attackerSteamId = source?.SteamId ?? 0L;
var attackerTags = source?.Tags ?? "";
Scene.RunEvent<Feed>( x => x.NotifyKill( npcName, attackerName, attackerSteamId, dmg.Tags.ToString(), attackerTags, "npc", w?.DisplayIcon ) );
}
/// <summary>
/// Change a property, remotely
/// </summary>
[Rpc.Host]
internal static void ChangeProperty( Component c, string propertyName, object value )
{
if ( !c.IsValid() ) return;
if ( !c.GameObject.HasAccess( Rpc.Caller ) ) return;
var tl = TypeLibrary.GetType( c.GetType() );
if ( tl is null ) return;
var prop = tl.GetProperty( propertyName );
if ( prop is null ) return;
prop.SetValue( c, value );
// Broadcast the change to everyone
// BUG - this is optimal I think, but doesn't work??
// c.GameObject.Network.Refresh( c );
c.GameObject.Network?.Refresh();
}
/// <summary>
/// Apply a debounced batch of morph changes to a <see cref="SkinnedModelRenderer"/>,
/// replicated to all clients. Only the morphs present in the batch are modified.
/// </summary>
[Rpc.Host]
internal static void ApplyMorphBatch( SkinnedModelRenderer smr, string morphsJson )
{
if ( !smr.IsValid() ) return;
if ( !smr.GameObject.HasAccess( Rpc.Caller ) ) return;
smr.GameObject.GetOrAddComponent<MorphState>().ApplyBatch( morphsJson );
}
/// <summary>
/// Apply a full morph preset (as json), and captures with <see cref="MorphState"/> which replicates changes to other clients
/// </summary>
[Rpc.Host]
internal static void ApplyFacePosePreset( SkinnedModelRenderer smr, string morphsJson )
{
if ( !smr.IsValid() ) return;
if ( !smr.GameObject.HasAccess( Rpc.Caller ) ) return;
smr.GameObject.GetOrAddComponent<MorphState>().ApplyPreset( morphsJson );
}
[Rpc.Host]
internal static async void ChangeMaterialOverride( ModelRenderer renderer, int materialIndex, string materialPath )
{
if ( !renderer.IsValid() ) return;
if ( !renderer.GameObject.HasAccess( Rpc.Caller ) ) return;
Material material = null;
if ( !string.IsNullOrEmpty( materialPath ) )
{
material = Material.Load( materialPath );
material ??= await Cloud.Load<Material>( materialPath );
}
if ( !renderer.IsValid() ) return;
renderer.Materials.SetOverride( materialIndex, material );
renderer.GameObject.Network?.Refresh();
}
/// <summary>
/// Delete an object from the Inspector context menu.
/// </summary>
[Rpc.Host]
internal static void DeleteInspectedObject( GameObject go )
{
if ( !go.IsValid() || go.IsProxy ) return;
if ( go.Tags.Has( "player" ) ) return;
// Check ownership if the object has an Ownable component
if ( !go.HasAccess( Rpc.Caller ) ) return;
go.Destroy();
}
/// <summary>
/// Break (gib) a prop from the Inspector context menu.
/// </summary>
[Rpc.Host]
internal static void BreakInspectedProp( Prop prop )
{
if ( !prop.IsValid() || prop.IsProxy ) return;
// Check ownership if the object has an Ownable component
if ( !prop.GameObject.HasAccess( Rpc.Caller ) ) return;
var damageable = prop.GetComponent<Component.IDamageable>();
if ( damageable is null ) return;
var dmg = new DamageInfo( 999999, null, null );
dmg.Tags.Add( DamageTags.GibAlways );
damageable.OnDamage( in dmg );
}
[Rpc.Host]
internal static void GiveSpawnerWeaponAt( string type, string path, int slot, string data = null, string icon = null, string title = null )
{
var player = Player.FindForConnection( Rpc.Caller );
if ( player is null ) return;
var inventory = player.GetComponent<PlayerInventory>();
if ( !inventory.IsValid() ) return;
if ( slot < 0 || slot >= inventory.MaxSlots ) return;
ISpawner s = type switch
{
"prop" or "mount" => new PropSpawner( path ),
"entity" or "sent" => new EntitySpawner( path ),
"dupe" when data is not null => DuplicatorSpawner.FromJson( data, title, icon ),
_ => null
};
if ( s is null ) return;
var loadout = player.GetComponent<PlayerLoadout>();
// If there's already a spawner weapon in this slot, just update
if ( inventory.GetSlot( slot ) is SpawnerWeapon existingSpawner )
{
existingSpawner.SetSpawner( s );
inventory.SwitchWeapon( existingSpawner );
loadout?.SaveLoadout();
return;
}
// Slot is occupied by something else — don't replace it
if ( inventory.GetSlot( slot ).IsValid() ) return;
inventory.Pickup( "weapons/spawner/spawner.prefab", slot, false );
var spawner = inventory.GetSlot( slot ) as SpawnerWeapon;
if ( !spawner.IsValid() ) return;
spawner.SetSpawner( s );
inventory.SwitchWeapon( spawner );
loadout?.SaveLoadout();
}
void IScenePhysicsEvents.OnOutOfBounds( Rigidbody body )
{
body.DestroyGameObject();
}
void ICleanupEvents.OnCleanup( int removedObjects, int restoredObjects )
{
Notices.AddNotice( "cleaning_services", Color.Green, $"Cleanup! Removed {removedObjects} objects, restored {restoredObjects} objects." );
}
}