mirror of
https://github.com/Facepunch/sbox-public.git
synced 2025-12-23 22:48:07 -05:00
* Stop generating solutions via -test flag add -generatesolution * Add TestAppSystem remove Application.InitUnitTest Avoids some hacks and also makes sure our tests are as close to a real AppSystem as possible. * Add shutdown unit test shuts down an re-inits the engine * Properly dispose native resources hold by managed during shutdown Should fix a bunch of crashes * Fix filesystem and networking tests * StandaloneTest does proper Game Close * Make sure package tests clean up properly * Make sure menu scene and resources are released on shutdown * Report leaked scenes on shutdown * Ensure DestroyImmediate is not used on scenes * Fix unmounting in unit tests not clearing native refs * Force destroy native resource on ResourceLib Clear
1241 lines
31 KiB
C#
1241 lines
31 KiB
C#
using Sandbox.Network;
|
|
using Sandbox.Utility;
|
|
using System.IO;
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace Sandbox;
|
|
|
|
/// <summary>
|
|
/// This is created and referenced by the network system, as a way to route.
|
|
/// </summary>
|
|
[Expose]
|
|
public partial class SceneNetworkSystem : GameNetworkSystem
|
|
{
|
|
internal static SceneNetworkSystem Instance { get; set; }
|
|
internal DeltaSnapshotSystem DeltaSnapshots { get; private set; }
|
|
|
|
private List<NetworkObject> BatchSpawnList { get; set; } = [];
|
|
private static bool IsSupressingSpawnMessages { get; set; }
|
|
private bool IsBatchNetworkSpawning { get; set; }
|
|
private int BatchNetworkSpawnCount { get; set; }
|
|
|
|
internal override bool IsHostBusy => !Game.ActiveScene?.IsLoading ?? true;
|
|
|
|
internal bool IsDisconnecting { get; set; }
|
|
|
|
internal SceneNetworkSystem( Internal.TypeLibrary typeLibrary, NetworkSystem system )
|
|
{
|
|
Instance = this;
|
|
DeltaSnapshots = new( this );
|
|
|
|
Library = typeLibrary;
|
|
NetworkSystem = system;
|
|
|
|
AddHandler<ObjectCreateBatchMsg>( OnObjectCreateBatch );
|
|
AddHandler<ObjectCreateMsg>( OnObjectCreate );
|
|
AddHandler<ObjectRefreshMsg>( OnObjectRefresh );
|
|
AddHandler<ObjectDestroyComponentMsg>( OnObjectDestroyComponent );
|
|
AddHandler<ObjectDestroyDescendantMsg>( OnObjectDestroyDescendant );
|
|
AddHandler<ObjectRefreshComponentMsg>( OnObjectRefreshComponent );
|
|
AddHandler<ObjectRefreshDescendantMsg>( OnObjectRefreshDescendant );
|
|
AddHandler<ObjectRefreshMsgAck>( OnObjectRefreshAck );
|
|
AddHandler<ObjectDestroyMsg>( OnObjectDestroy );
|
|
AddHandler<ObjectRpcMsg>( OnObjectMessage );
|
|
AddHandler<ObjectNetworkTableMsg>( OnNetworkTableChanges );
|
|
AddHandler<SceneNetworkTableMsg>( OnNetworkTableChanges );
|
|
AddHandler<SceneRpcMsg>( OnSceneRpc );
|
|
AddHandler<StaticRpcMsg>( OnStaticRpc );
|
|
AddHandler<LoadSceneBeginMsg>( OnLoadSceneMsg );
|
|
AddHandler<LoadSceneSnapshotMsg>( OnLoadSceneSnapshotMsg );
|
|
AddHandler<LoadSceneRequestSnapshotMsg>( OnLoadSceneRequestSnapshotMsg );
|
|
AddHandler<SceneLoadedMsg>( OnSceneLoadedMsg );
|
|
}
|
|
|
|
internal void OnHotload()
|
|
{
|
|
DeltaSnapshots.Reset();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Any <see cref="GameObject">GameObjects</see> created within this scope will not send spawn messages to other clients.
|
|
/// </summary>
|
|
internal static IDisposable SuppressSpawnMessages()
|
|
{
|
|
IsSupressingSpawnMessages = true;
|
|
|
|
return new DisposeAction( () =>
|
|
{
|
|
IsSupressingSpawnMessages = false;
|
|
} );
|
|
}
|
|
|
|
private readonly Dictionary<Guid, Guid> PendingSceneLoads = new();
|
|
|
|
/// <summary>
|
|
/// Load a scene for all other clients. This can only be called by the host.
|
|
/// </summary>
|
|
internal void LoadSceneBroadcast( SceneLoadOptions options )
|
|
{
|
|
if ( !Networking.IsActive || !Networking.IsHost )
|
|
return;
|
|
|
|
var loadMsg = new LoadSceneBeginMsg
|
|
{
|
|
ShowLoadingScreen = options.ShowLoadingScreen,
|
|
MountedVPKs = Game.ActiveScene.GetAllComponents<MapInstance>().Select( x => x.MapName ).ToList(),
|
|
Id = Guid.NewGuid()
|
|
};
|
|
|
|
var msg = ByteStream.Create( 256 );
|
|
msg.Write( InternalMessageType.Packed );
|
|
|
|
Networking.System.Serialize( loadMsg, ref msg );
|
|
|
|
foreach ( var c in Connection.All )
|
|
{
|
|
if ( c.State < Connection.ChannelState.Snapshot )
|
|
continue;
|
|
|
|
PendingSceneLoads[c.Id] = loadMsg.Id;
|
|
c.SendRawMessage( msg );
|
|
}
|
|
|
|
msg.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start a network spawn batch. Any networked objects created within this scope
|
|
/// will be sent with one spawn message. This makes sure that any references are
|
|
/// kept to child networked objects when the objects are spawned on the other side.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
internal IDisposable NetworkSpawnBatch()
|
|
{
|
|
IsBatchNetworkSpawning = true;
|
|
BatchNetworkSpawnCount++;
|
|
|
|
return new DisposeAction( () =>
|
|
{
|
|
BatchNetworkSpawnCount--;
|
|
|
|
if ( BatchNetworkSpawnCount > 0 )
|
|
return;
|
|
|
|
IsBatchNetworkSpawning = false;
|
|
SendNetworkSpawnBatch();
|
|
} );
|
|
}
|
|
|
|
private void SendNetworkSpawnBatch()
|
|
{
|
|
// If we only have one, just send a normal message.
|
|
if ( BatchSpawnList.Count == 1 )
|
|
{
|
|
var networkObject = BatchSpawnList.FirstOrDefault();
|
|
|
|
if ( !(networkObject.GameObject?.IsDestroyed ?? true) )
|
|
Broadcast( networkObject.GetCreateMessage() );
|
|
|
|
BatchSpawnList.Clear();
|
|
return;
|
|
}
|
|
|
|
var msg = new ObjectCreateBatchMsg();
|
|
var list = new List<ObjectCreateMsg>();
|
|
|
|
foreach ( var networkObject in BatchSpawnList )
|
|
{
|
|
if ( networkObject.GameObject?.IsDestroyed ?? true )
|
|
continue;
|
|
|
|
list.Add( networkObject.GetCreateMessage() );
|
|
}
|
|
|
|
msg.CreateMsgs = list.ToArray();
|
|
BatchSpawnList.Clear();
|
|
|
|
Broadcast( msg );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Broadcast the spawning of a networked object. This will add the networked object
|
|
/// to batch list if we're spawning as part of a batch, and will ignore the spawn message
|
|
/// entirely if we're supposed to be suppressing spawn messages.
|
|
/// </summary>
|
|
/// <param name="networkObject"></param>
|
|
internal void NetworkSpawnBroadcast( NetworkObject networkObject )
|
|
{
|
|
// We're not supposed to send spawn messages right now.
|
|
if ( IsSupressingSpawnMessages )
|
|
return;
|
|
|
|
if ( IsBatchNetworkSpawning )
|
|
{
|
|
BatchSpawnList.Add( networkObject );
|
|
return;
|
|
}
|
|
|
|
Broadcast( networkObject.GetCreateMessage() );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the host has provided us with a snapshot for a newly loaded scene.
|
|
/// </summary>
|
|
private async Task OnLoadSceneSnapshotMsg( LoadSceneSnapshotMsg msg, Connection connection, Guid msgId )
|
|
{
|
|
NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.Snapshot, msg );
|
|
|
|
await SetSnapshotAsync( msg.Snapshot );
|
|
|
|
// Let them know we have now loaded this scene.
|
|
var loadedMsg = new SceneLoadedMsg { Id = msg.Id };
|
|
connection.SendMessage( loadedMsg, NetFlags.Reliable );
|
|
|
|
LoadingScreen.IsVisible = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when a client has requested the snapshot for a newly loaded scene. This is usually
|
|
/// once they've done any preloading that they need to do.
|
|
/// </summary>
|
|
private void OnLoadSceneRequestSnapshotMsg( LoadSceneRequestSnapshotMsg msg, Connection connection, Guid msgId )
|
|
{
|
|
// If this connection doesn't have a pending scene load with this id then bail.
|
|
if ( !PendingSceneLoads.TryGetValue( connection.Id, out var id ) || id != msg.Id )
|
|
return;
|
|
|
|
var output = new LoadSceneSnapshotMsg { Id = msg.Id };
|
|
var snapshot = new SnapshotMsg
|
|
{
|
|
GameObjectSystems = [],
|
|
NetworkObjects = new( 64 )
|
|
};
|
|
|
|
GetSnapshot( default, ref snapshot );
|
|
output.Snapshot = snapshot;
|
|
|
|
var bs = ByteStream.Create( 256 );
|
|
bs.Write( InternalMessageType.Packed );
|
|
|
|
Networking.System.Serialize( output, ref bs );
|
|
connection.SendRawMessage( bs );
|
|
|
|
bs.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the host has told us to load a new scene.
|
|
/// </summary>
|
|
private async Task OnLoadSceneMsg( LoadSceneBeginMsg msg, Connection connection, Guid msgId )
|
|
{
|
|
if ( !Game.IsEditor && msg.ShowLoadingScreen )
|
|
{
|
|
LoadingScreen.IsVisible = true;
|
|
LoadingScreen.Title = "Loading Scene";
|
|
}
|
|
|
|
// Go ahead and destroy the scene
|
|
if ( Game.ActiveScene is not null )
|
|
{
|
|
Game.ActiveScene?.Destroy();
|
|
Game.ActiveScene = null;
|
|
}
|
|
|
|
MountedVPKs?.Dispose();
|
|
MountedVPKs = await MountMaps( msg.MountedVPKs );
|
|
|
|
// Let them know we would like a snapshot now.
|
|
var loadedMsg = new LoadSceneRequestSnapshotMsg { Id = msg.Id };
|
|
connection.SendMessage( loadedMsg, NetFlags.Reliable );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by clients to confirm they have finished loading the new scene.
|
|
/// </summary>
|
|
private void OnSceneLoadedMsg( SceneLoadedMsg msg, Connection connection, Guid msgId )
|
|
{
|
|
// If this connection doesn't have a pending scene load with this id then bail.
|
|
if ( !PendingSceneLoads.TryGetValue( connection.Id, out var id ) || id != msg.Id )
|
|
return;
|
|
|
|
PendingSceneLoads.Remove( connection.Id );
|
|
Instance?.OnJoined( connection );
|
|
}
|
|
|
|
/// <summary>
|
|
/// A client has joined and wants to know what VPKs to preload.
|
|
/// </summary>
|
|
public override void GetMountedVPKs( Connection source, ref MountedVPKsResponse msg )
|
|
{
|
|
msg.MountedVPKs = Game.ActiveScene.GetAllComponents<MapInstance>().Select( x => x.MapName ).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asynchronously load and mount any VPKs from the provided server response.
|
|
/// </summary>
|
|
public override async Task MountVPKs( Connection source, MountedVPKsResponse msg )
|
|
{
|
|
// Mount any vpks early because snapshotted or networked objects can use resources within
|
|
// This removes it's refcount at the end because the MapInstance should take over
|
|
MountedVPKs?.Dispose();
|
|
MountedVPKs = await MountMaps( msg.MountedVPKs );
|
|
}
|
|
|
|
private static readonly GameObject.SerializeOptions _snapshotSerializeOptions = new() { SceneForNetwork = true };
|
|
|
|
/// <summary>
|
|
/// A client has joined and wants a snapshot of the world.
|
|
/// </summary>
|
|
public override void GetSnapshot( Connection source, ref SnapshotMsg msg )
|
|
{
|
|
ThreadSafe.AssertIsMainThread();
|
|
using var _ = PerformanceStats.Timings.Network.Scope();
|
|
|
|
msg.Time = Time.Now;
|
|
|
|
var analytic = new Api.Events.EventRecord( "SceneNetworkSystem.GetSnapshot" );
|
|
|
|
using ( analytic.ScopeTimer( "SceneTime" ) )
|
|
{
|
|
msg.SceneData = Game.ActiveScene.Serialize( _snapshotSerializeOptions ).ToJsonString();
|
|
}
|
|
|
|
using ( analytic.ScopeTimer( "NetworkObjectTime" ) )
|
|
{
|
|
Game.ActiveScene.SerializeNetworkObjects( msg.NetworkObjects );
|
|
}
|
|
|
|
var systems = Game.ActiveScene.GetSystems();
|
|
|
|
foreach ( var system in systems )
|
|
{
|
|
var type = new SnapshotMsg.GameObjectSystemData
|
|
{
|
|
TableData = system.WriteDataTable( true ),
|
|
Type = Game.TypeLibrary.GetType( system.GetType() ).Identity,
|
|
Id = system.Id
|
|
};
|
|
|
|
msg.GameObjectSystems.Add( type );
|
|
}
|
|
|
|
analytic.SetValue( "SceneDataLength", msg.SceneData?.Length ?? 0 );
|
|
analytic.SetValue( "NetworkObjectCount", msg.NetworkObjects?.Count ?? 0 );
|
|
analytic.SetValue( "GameObjectCount", Game.ActiveScene.Directory.GameObjectCount );
|
|
analytic.SetValue( "ComponentCount", Game.ActiveScene.Directory.ComponentCount );
|
|
analytic.SetValue( "Machine", Environment.MachineName );
|
|
|
|
analytic.Submit();
|
|
}
|
|
|
|
public override void Dispose()
|
|
{
|
|
base.Dispose();
|
|
|
|
MountedVPKs?.Dispose();
|
|
MountedVPKs = null;
|
|
|
|
if ( Instance == this )
|
|
Instance = null;
|
|
}
|
|
|
|
protected string WorkoutMapName()
|
|
{
|
|
if ( Game.ActiveScene is null ) return "<empty>";
|
|
|
|
foreach ( var map in Game.ActiveScene.GetAllComponents<MapInstance>() )
|
|
{
|
|
if ( !map.Active ) continue;
|
|
if ( !map.IsLoaded ) continue;
|
|
|
|
return map.MapName;
|
|
}
|
|
|
|
return Game.ActiveScene.Name;
|
|
}
|
|
|
|
protected override void Tick()
|
|
{
|
|
if ( !Networking.IsHost )
|
|
return;
|
|
|
|
Networking.MapName = WorkoutMapName();
|
|
}
|
|
|
|
private IDisposable MountedVPKs { get; set; }
|
|
private async Task<IDisposable> MountMaps( List<string> maps )
|
|
{
|
|
// Lets see if any are cloud maps and mount those first
|
|
List<string> vpks = new();
|
|
foreach ( var map in maps )
|
|
{
|
|
if ( map.EndsWith( ".vpk" ) )
|
|
{
|
|
vpks.Add( map );
|
|
continue;
|
|
}
|
|
|
|
if ( !Package.TryParseIdent( map, out var parts ) )
|
|
continue;
|
|
|
|
var package = await Package.Fetch( map, false );
|
|
if ( package is null )
|
|
continue;
|
|
|
|
var fs = await package.MountAsync();
|
|
if ( fs is null ) continue;
|
|
|
|
var mapFileName = package.PrimaryAsset;
|
|
vpks.Add( mapFileName );
|
|
}
|
|
|
|
foreach ( var vpk in vpks )
|
|
g_pWorldRendererMgr.MountWorldVPK( Path.GetFileNameWithoutExtension( vpk ), Path.ChangeExtension( vpk, ".vpk" ) );
|
|
|
|
return new DisposeAction( () =>
|
|
{
|
|
foreach ( var vpk in vpks )
|
|
g_pWorldRendererMgr.UnmountWorldVPK( Path.GetFileNameWithoutExtension( vpk ) );
|
|
} );
|
|
}
|
|
|
|
/// <summary>
|
|
/// We have recieved a snapshot of the world.
|
|
/// </summary>
|
|
public override async Task SetSnapshotAsync( SnapshotMsg msg )
|
|
{
|
|
ThreadSafe.AssertIsMainThread();
|
|
|
|
if ( Game.ActiveScene is not null )
|
|
{
|
|
Game.ActiveScene?.Destroy();
|
|
Game.ActiveScene = null;
|
|
}
|
|
|
|
Game.ActiveScene = new();
|
|
Game.ActiveScene.StartLoading();
|
|
|
|
Time.Now = (float)msg.Time;
|
|
Game.ActiveScene.UpdateTimeFromHost( msg.Time );
|
|
|
|
foreach ( var s in msg.GameObjectSystems )
|
|
{
|
|
var type = Game.TypeLibrary.GetTypeByIdent( s.Type );
|
|
var system = Game.ActiveScene.GetSystemByType( type );
|
|
|
|
if ( system is null )
|
|
continue;
|
|
|
|
system.Id = s.Id;
|
|
system.ReadDataTable( s.TableData );
|
|
}
|
|
|
|
{
|
|
using var batchGroup = CallbackBatch.Batch();
|
|
|
|
if ( !string.IsNullOrWhiteSpace( msg.SceneData ) )
|
|
{
|
|
var sceneData = JsonNode.Parse( msg.SceneData ).AsObject();
|
|
Game.ActiveScene.Deserialize( sceneData );
|
|
}
|
|
|
|
var createdNetworkObjects = new List<Tuple<GameObject, ObjectCreateMsg>>();
|
|
|
|
foreach ( var nwo in msg.NetworkObjects )
|
|
{
|
|
if ( nwo is not ObjectCreateMsg oc )
|
|
continue;
|
|
|
|
var go = new GameObject();
|
|
go.Deserialize( JsonNode.Parse( oc.JsonData ).AsObject() );
|
|
createdNetworkObjects.Add( new( go, oc ) );
|
|
}
|
|
|
|
foreach ( var (go, oc) in createdNetworkObjects )
|
|
{
|
|
go.NetworkSpawnRemote( oc );
|
|
}
|
|
}
|
|
|
|
MountedVPKs?.Dispose();
|
|
MountedVPKs = null;
|
|
|
|
// Wait for loading to finish
|
|
if ( Game.ActiveScene is not null )
|
|
{
|
|
await Game.ActiveScene.WaitForLoading();
|
|
}
|
|
|
|
if ( Game.ActiveScene.IsValid() )
|
|
{
|
|
Game.ActiveScene.RunEvent<ISceneStartup>( x => x.OnClientInitialize() );
|
|
}
|
|
|
|
Game.IsPlaying = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called on the host to decide whether to accept a <see cref="Connection"/>. If any <see cref="Component"/>
|
|
/// that implements this returns false, the connection will be denied.
|
|
/// </summary>
|
|
/// <param name="channel"></param>
|
|
/// <param name="reason">The reason to display to the client.</param>
|
|
public override bool AcceptConnection( Connection channel, ref string reason )
|
|
{
|
|
foreach ( var c in Game.ActiveScene.GetAll<Component.INetworkListener>() )
|
|
{
|
|
if ( !c.AcceptConnection( channel, ref reason ) )
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public override void OnConnected( Connection client )
|
|
{
|
|
Action queue = default;
|
|
|
|
foreach ( var c in Game.ActiveScene.GetAll<Component.INetworkListener>() )
|
|
{
|
|
queue += () => c.OnConnected( client );
|
|
}
|
|
|
|
try
|
|
{
|
|
queue?.Invoke();
|
|
}
|
|
catch ( Exception e )
|
|
{
|
|
Log.Error( e, "Exception when calling INetworkListener.OnConnected" );
|
|
}
|
|
}
|
|
|
|
public override void OnInitialize()
|
|
{
|
|
if ( !Networking.IsHost )
|
|
return;
|
|
|
|
var scene = Game.ActiveScene;
|
|
|
|
if ( !scene.IsValid() || scene.IsLoading )
|
|
return;
|
|
|
|
var sceneInformation = scene.Components.Get<SceneInformation>();
|
|
OnLoadedScene( sceneInformation?.Title );
|
|
}
|
|
|
|
public override void OnJoined( Connection client )
|
|
{
|
|
Action queue = default;
|
|
|
|
foreach ( var c in Game.ActiveScene.GetAll<Component.INetworkListener>() )
|
|
{
|
|
queue += () => c.OnActive( client );
|
|
}
|
|
|
|
try
|
|
{
|
|
queue?.Invoke();
|
|
}
|
|
catch ( Exception e )
|
|
{
|
|
Log.Error( e, "Exception when calling INetworkListener.OnActive" );
|
|
}
|
|
}
|
|
|
|
public override void OnLeave( Connection client )
|
|
{
|
|
DeltaSnapshots.RemoveConnection( client );
|
|
|
|
if ( Game.ActiveScene is not null )
|
|
{
|
|
foreach ( var no in Game.ActiveScene.networkedObjects )
|
|
{
|
|
no.RemoveConnection( client.Id );
|
|
}
|
|
|
|
foreach ( var system in Game.ActiveScene.GetSystems() )
|
|
{
|
|
system.LocalSnapshotState.RemoveConnection( client.Id );
|
|
}
|
|
|
|
Action queue = default;
|
|
|
|
foreach ( var c in Game.ActiveScene.GetAll<Component.INetworkListener>() )
|
|
{
|
|
queue += () => c.OnDisconnected( client );
|
|
}
|
|
|
|
try
|
|
{
|
|
queue?.Invoke();
|
|
}
|
|
catch ( Exception e )
|
|
{
|
|
Log.Error( e, "Exception when calling INetworkListener.OnDisconnected" );
|
|
}
|
|
}
|
|
|
|
if ( client.Id == Guid.Empty )
|
|
return;
|
|
|
|
DoOrphanedActions( client );
|
|
}
|
|
|
|
public override void OnHostChanged( Connection previousHost, Connection newHost )
|
|
{
|
|
var scene = Game.ActiveScene;
|
|
|
|
if ( scene.IsValid() )
|
|
{
|
|
foreach ( var system in scene.GetSystems() )
|
|
{
|
|
system.LocalSnapshotState.ClearConnections();
|
|
}
|
|
|
|
foreach ( var no in scene.networkedObjects )
|
|
{
|
|
no.ClearConnections();
|
|
}
|
|
}
|
|
|
|
foreach ( var connection in Connection.All )
|
|
{
|
|
connection.Input.Clear();
|
|
}
|
|
|
|
DeltaSnapshots?.Reset();
|
|
UserCommand.Reset();
|
|
}
|
|
|
|
public override void OnBecameHost( Connection previousHost )
|
|
{
|
|
// Was the host at startup, so this call isn't needed
|
|
if ( previousHost is null || previousHost.Id == Guid.Empty )
|
|
return;
|
|
|
|
Log.Info( $"Became the host (previous host was {previousHost})" );
|
|
var scene = Game.ActiveScene;
|
|
if ( !scene.IsValid() ) return;
|
|
|
|
Action queue = default;
|
|
foreach ( var c in scene.GetAll<Component.INetworkListener>() )
|
|
{
|
|
queue += () => c.OnBecameHost( previousHost );
|
|
}
|
|
|
|
try
|
|
{
|
|
queue?.Invoke();
|
|
}
|
|
catch ( Exception e )
|
|
{
|
|
Log.Error( e, "Exception when calling INetworkListener.OnBecameHost" );
|
|
}
|
|
|
|
// Don't run orphaned actions if the previous host is still connected.
|
|
if ( previousHost.IsActive )
|
|
return;
|
|
|
|
DoOrphanedActions( previousHost );
|
|
}
|
|
|
|
internal void DoOrphanedActions( Connection connection )
|
|
{
|
|
Game.ActiveScene?.DoOrphanedActions( connection );
|
|
}
|
|
|
|
public override IDisposable Push()
|
|
{
|
|
return Game.ActiveScene is null ? null : Game.ActiveScene.Push();
|
|
}
|
|
|
|
private void OnObjectDestroyDescendant( ObjectDestroyDescendantMsg message, Connection source )
|
|
{
|
|
var scene = Game.ActiveScene;
|
|
if ( !scene.IsValid() )
|
|
return;
|
|
|
|
var go = scene.Directory.FindByGuid( message.Guid );
|
|
if ( !go.IsValid() ) return;
|
|
|
|
var root = go.Network.RootGameObject;
|
|
if ( !root.IsValid() ) return;
|
|
|
|
if ( root._net is null )
|
|
{
|
|
Log.Warning( $"ObjectDestroyDescendant: Object {root} is not networked" );
|
|
return;
|
|
}
|
|
|
|
// Only the owner or the host can do this.
|
|
if ( !root._net.HasControl( source ) && !source.IsHost )
|
|
return;
|
|
|
|
go.Destroy();
|
|
}
|
|
|
|
private void OnObjectDestroyComponent( ObjectDestroyComponentMsg message, Connection source )
|
|
{
|
|
var scene = Game.ActiveScene;
|
|
if ( !scene.IsValid() )
|
|
return;
|
|
|
|
var component = scene.Directory.FindComponentByGuid( message.Guid );
|
|
if ( component == null ) return;
|
|
|
|
var root = component.Network.RootGameObject;
|
|
if ( !root.IsValid() ) return;
|
|
|
|
if ( root._net is null )
|
|
{
|
|
Log.Warning( $"ObjectDestroyComponent: Object {root} is not networked" );
|
|
return;
|
|
}
|
|
|
|
// Only the owner or the host can do this.
|
|
if ( !root._net.HasControl( source ) && !source.IsHost )
|
|
return;
|
|
|
|
component.Destroy();
|
|
}
|
|
|
|
private void OnObjectRefreshAck( ObjectRefreshMsgAck message, Connection source )
|
|
{
|
|
var scene = Game.ActiveScene;
|
|
if ( !scene.IsValid() )
|
|
return;
|
|
|
|
var obj = scene.Directory.FindByGuid( message.Guid );
|
|
if ( obj is null ) return;
|
|
|
|
if ( obj._net is null )
|
|
{
|
|
Log.Warning( $"ObjectRefreshAck: Object {obj} is not networked" );
|
|
return;
|
|
}
|
|
|
|
DeltaSnapshots.ClearNetworkObject( obj._net );
|
|
}
|
|
|
|
private void OnObjectRefreshDescendant( ObjectRefreshDescendantMsg message, Connection source )
|
|
{
|
|
var scene = Game.ActiveScene;
|
|
if ( !scene.IsValid() )
|
|
return;
|
|
|
|
var parentObject = scene.Directory.FindByGuid( message.ParentId );
|
|
if ( !parentObject.IsValid() )
|
|
return;
|
|
|
|
var root = parentObject.Network.RootGameObject;
|
|
if ( !root.IsValid() ) return;
|
|
|
|
if ( root._net is null )
|
|
{
|
|
Log.Warning( $"ObjectRefreshDescendant: Object {root} is not networked" );
|
|
return;
|
|
}
|
|
|
|
// Only the owner or the host can do this.
|
|
if ( !root._net.HasControl( source ) && !source.IsHost )
|
|
return;
|
|
|
|
var gameObjectJson = JsonNode.Parse( message.JsonData ).AsObject();
|
|
|
|
if ( !gameObjectJson.TryGetPropertyValue( GameObject.JsonKeys.Id, out var childId ) )
|
|
return;
|
|
|
|
var gameObject = scene.Directory.FindByGuid( childId.GetValue<Guid>() );
|
|
|
|
if ( !gameObject.IsValid() )
|
|
{
|
|
gameObject = new GameObject( parentObject, false );
|
|
}
|
|
else if ( gameObject != parentObject )
|
|
{
|
|
gameObject.SetParentFromNetwork( parentObject );
|
|
}
|
|
|
|
using ( var _ = CallbackBatch.Batch() )
|
|
{
|
|
gameObject?.Deserialize( gameObjectJson, new GameObject.DeserializeOptions
|
|
{
|
|
IsNetworkRefresh = true,
|
|
IsRefreshing = true
|
|
} );
|
|
}
|
|
|
|
root._net.UpdateFromRefresh( source, message.TableData, message.Snapshot );
|
|
}
|
|
|
|
private void OnObjectRefreshComponent( ObjectRefreshComponentMsg message, Connection source )
|
|
{
|
|
var scene = Game.ActiveScene;
|
|
if ( !scene.IsValid() )
|
|
return;
|
|
|
|
var gameObject = scene.Directory.FindByGuid( message.GameObjectId );
|
|
|
|
if ( !gameObject.IsValid() )
|
|
return;
|
|
|
|
var root = gameObject.Network.RootGameObject;
|
|
if ( !root.IsValid() ) return;
|
|
|
|
if ( root._net is null )
|
|
{
|
|
Log.Warning( $"ObjectRefreshComponent: Object {root} is not networked" );
|
|
return;
|
|
}
|
|
|
|
// Only the owner or the host can do this.
|
|
if ( !root._net.HasControl( source ) && !source.IsHost )
|
|
return;
|
|
|
|
var componentJson = JsonNode.Parse( message.JsonData ).AsObject();
|
|
|
|
if ( !componentJson.TryGetPropertyValue( Component.JsonKeys.Id, out var componentId ) )
|
|
return;
|
|
|
|
var component = scene.Directory.FindComponentByGuid( componentId.GetValue<Guid>() );
|
|
|
|
if ( !component.IsValid() )
|
|
{
|
|
var componentTypeName = componentJson.GetPropertyValue( Component.JsonKeys.Type, "" );
|
|
var componentType = Game.TypeLibrary.GetType<Component>( componentTypeName, true );
|
|
|
|
if ( componentType is null || componentType.TargetType.IsAbstract )
|
|
{
|
|
Log.Warning( $"TypeLibrary couldn't find {nameof( Component )} type {componentTypeName}" );
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
component = gameObject.Components.Create( componentType, false );
|
|
}
|
|
catch ( Exception e )
|
|
{
|
|
Log.Error( e );
|
|
}
|
|
}
|
|
else if ( component.GameObject != gameObject )
|
|
{
|
|
return;
|
|
}
|
|
|
|
using ( var _ = CallbackBatch.Batch() )
|
|
{
|
|
component?.Deserialize( componentJson );
|
|
}
|
|
|
|
root._net.UpdateFromRefresh( source, message.TableData, message.Snapshot );
|
|
}
|
|
|
|
|
|
private void OnObjectRefresh( ObjectRefreshMsg message, Connection source )
|
|
{
|
|
NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.Refresh, message );
|
|
NetworkDebugSystem.Current?.Track( "OnObjectRefresh", message );
|
|
|
|
// Is this a request from someone?
|
|
if ( source is not null && !source.CanRefreshObjects )
|
|
return;
|
|
|
|
var scene = Game.ActiveScene;
|
|
if ( !scene.IsValid() ) return;
|
|
|
|
var obj = scene.Directory.FindByGuid( message.Guid );
|
|
if ( obj is null ) return;
|
|
|
|
if ( obj._net is null )
|
|
{
|
|
Log.Warning( $"ObjectRefresh: Object {obj} is not networked" );
|
|
return;
|
|
}
|
|
|
|
if ( obj._net.IsUnowned )
|
|
{
|
|
// If we're unowned and the source is not the host, we can't refresh.
|
|
if ( !source.IsHost )
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
// If the source is not the owner and not the host, we can't refresh.
|
|
if ( !source.IsHost && obj._net.Owner != source.Id )
|
|
return;
|
|
}
|
|
|
|
obj._net.OnRefreshMessage( source, message );
|
|
}
|
|
|
|
private void OnObjectCreateBatch( ObjectCreateBatchMsg message, Connection source, Guid msgId )
|
|
{
|
|
NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.Spawn, message );
|
|
NetworkDebugSystem.Current?.Track( "OnObjectCreateBatch", message );
|
|
|
|
// If we haven't even loaded a scene yet, this message was not sent in order (we don't even have the snapshot yet.)
|
|
var scene = Game.ActiveScene;
|
|
if ( !scene.IsValid() )
|
|
return;
|
|
|
|
// Is this a request from someone?
|
|
if ( source is not null && !source.CanSpawnObjects )
|
|
return;
|
|
|
|
using ( CallbackBatch.Batch() )
|
|
{
|
|
foreach ( var msg in message.CreateMsgs )
|
|
{
|
|
var go = new GameObject();
|
|
go.Deserialize( JsonNode.Parse( msg.JsonData ).AsObject() );
|
|
go.NetworkSpawnRemote( msg );
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnObjectCreate( ObjectCreateMsg message, Connection source, Guid msgId )
|
|
{
|
|
NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.Spawn, message );
|
|
NetworkDebugSystem.Current?.Track( "OnObjectCreate", message );
|
|
|
|
// If we haven't even loaded a scene yet, this message was not sent in order (we don't even have the snapshot yet.)
|
|
var scene = Game.ActiveScene;
|
|
if ( !scene.IsValid() )
|
|
return;
|
|
|
|
// Is this a request from someone?
|
|
if ( source is not null && !source.CanSpawnObjects )
|
|
return;
|
|
|
|
var go = new GameObject();
|
|
|
|
using ( CallbackBatch.Batch() )
|
|
{
|
|
go.Deserialize( JsonNode.Parse( message.JsonData ).AsObject() );
|
|
go.NetworkSpawnRemote( message );
|
|
}
|
|
}
|
|
|
|
private void OnNetworkTableChanges( SceneNetworkTableMsg message, Connection source, Guid msgId )
|
|
{
|
|
NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.SyncVars, message );
|
|
|
|
var scene = Game.ActiveScene;
|
|
if ( !scene.IsValid() )
|
|
return;
|
|
|
|
var system = scene.Directory.FindSystemByGuid( message.Guid );
|
|
if ( system is null )
|
|
return;
|
|
|
|
// Can we receive network table changes from this source?
|
|
if ( !source.IsHost )
|
|
return;
|
|
|
|
system.ReadDataTable( message.TableData );
|
|
}
|
|
|
|
private void OnNetworkTableChanges( ObjectNetworkTableMsg message, Connection source, Guid msgId )
|
|
{
|
|
NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.SyncVars, message );
|
|
|
|
var scene = Game.ActiveScene;
|
|
if ( !scene.IsValid() )
|
|
return;
|
|
|
|
var obj = scene.Directory.FindByGuid( message.Guid );
|
|
if ( obj is null )
|
|
return;
|
|
|
|
if ( obj._net is null )
|
|
{
|
|
Log.Warning( $"ObjectNetworkTable: Object {obj} is not networked" );
|
|
return;
|
|
}
|
|
|
|
// Can we receive network table changes from this source?
|
|
if ( !source.IsHost && source.Id != obj._net.Owner )
|
|
return;
|
|
|
|
obj._net.OnNetworkTableMessage( message );
|
|
}
|
|
|
|
private void OnObjectDestroy( ObjectDestroyMsg message, Connection source, Guid msgId )
|
|
{
|
|
NetworkDebugSystem.Current?.Track( "OnObjectDestroy", message );
|
|
|
|
var scene = Game.ActiveScene;
|
|
if ( !scene.IsValid() )
|
|
return;
|
|
|
|
var obj = scene.Directory.FindByGuid( message.Guid );
|
|
if ( obj is null )
|
|
return;
|
|
|
|
if ( obj._net is null )
|
|
{
|
|
// We can't just destroy arbitrary game objects.
|
|
Log.Warning( $"ObjectDestroy: Object {obj} is not networked" );
|
|
return;
|
|
}
|
|
|
|
if ( obj._net.IsUnowned )
|
|
{
|
|
// If we're unowned and the source is not the host, we can't destroy.
|
|
if ( !source.IsHost )
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
// If the source is not the owner and not the host, we can't destroy.
|
|
if ( !source.IsHost && obj._net.Owner != source.Id )
|
|
return;
|
|
}
|
|
|
|
obj._net.OnNetworkDestroy();
|
|
}
|
|
|
|
private void OnObjectMessage( ObjectRpcMsg rpc, Connection source, Guid msgId )
|
|
{
|
|
Rpc.IncomingInstanceRpcMsg( rpc, source );
|
|
}
|
|
|
|
private void OnSceneRpc( SceneRpcMsg message, Connection source, Guid msgId )
|
|
{
|
|
Rpc.IncomingInstanceRpcMsg( message, source );
|
|
}
|
|
|
|
private void OnStaticRpc( StaticRpcMsg message, Connection source, Guid msgId )
|
|
{
|
|
Rpc.IncomingStaticRpcMsg( message, source );
|
|
}
|
|
|
|
/// <summary>
|
|
/// A heartbeat has been received from the host. We should make sure our times are in sync.
|
|
/// </summary>
|
|
internal override void OnHeartbeat( float serverGameTime )
|
|
{
|
|
Game.ActiveScene?.UpdateTimeFromHost( serverGameTime );
|
|
}
|
|
|
|
/// <summary>
|
|
/// We've received a cull state change for a networked object.
|
|
/// </summary>
|
|
internal override void OnCullStateChangeMessage( ByteStream bs, Connection source )
|
|
{
|
|
var scene = Game.ActiveScene;
|
|
if ( !scene.IsValid() ) return;
|
|
|
|
NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.Culling, bs.Length );
|
|
|
|
var objectId = bs.Read<Guid>();
|
|
var isCulled = bs.Read<bool>();
|
|
|
|
var go = scene.Directory.FindByGuid( objectId );
|
|
if ( !go.IsValid() ) return;
|
|
|
|
if ( go.IsNetworkCulled == isCulled )
|
|
return;
|
|
|
|
var ownerId = go.Network.OwnerId;
|
|
var isOwner = (source.Id == ownerId) || (ownerId == Guid.Empty && source.IsHost);
|
|
|
|
if ( !isOwner )
|
|
return;
|
|
|
|
go.IsNetworkCulled = isCulled;
|
|
go.UpdateNetworkCulledState();
|
|
}
|
|
|
|
/// <summary>
|
|
/// A delta snapshot message has been received from another connection.
|
|
/// </summary>
|
|
internal override void OnDeltaSnapshotMessage( InternalMessageType type, ByteStream bs, Connection source )
|
|
{
|
|
if ( type == InternalMessageType.DeltaSnapshot )
|
|
{
|
|
NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.SyncVars, bs.Length );
|
|
DeltaSnapshots.OnDeltaSnapshot( source, bs );
|
|
}
|
|
else if ( type == InternalMessageType.DeltaSnapshotAck )
|
|
{
|
|
DeltaSnapshots.OnDeltaSnapshotAck( source, bs );
|
|
}
|
|
else if ( type == InternalMessageType.DeltaSnapshotCluster )
|
|
{
|
|
NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.SyncVars, bs.Length );
|
|
DeltaSnapshots.OnDeltaSnapshotCluster( source, bs );
|
|
}
|
|
else if ( type == InternalMessageType.DeltaSnapshotClusterAck )
|
|
{
|
|
DeltaSnapshots.OnDeltaSnapshotClusterAck( source, bs );
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When a client has sent an acknowledgement that they've received a refresh message for
|
|
/// a networked object.
|
|
/// </summary>
|
|
[Expose]
|
|
struct ObjectRefreshMsgAck
|
|
{
|
|
public Guid Guid { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// When a <see cref="Component"/> in the hierarchy of a networked object has
|
|
/// been destroyed.
|
|
/// </summary>
|
|
[Expose]
|
|
struct ObjectDestroyComponentMsg
|
|
{
|
|
public Guid Guid { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// When a <see cref="GameObject"/> in the hierarchy of a networked object has
|
|
/// been destroyed.
|
|
/// </summary>
|
|
[Expose]
|
|
struct ObjectDestroyDescendantMsg
|
|
{
|
|
public Guid Guid { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// When a <see cref="GameObject"/> in the hierarchy of a networked object has
|
|
/// been added or changed.
|
|
/// </summary>
|
|
[Expose]
|
|
struct ObjectRefreshDescendantMsg
|
|
{
|
|
public string JsonData { get; set; }
|
|
public byte[] TableData { get; set; }
|
|
public byte[] Snapshot { get; set; }
|
|
public Guid ParentId { get; set; }
|
|
public Guid GameObjectId { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// When a <see cref="Component"/> in the hierarchy of a networked object has
|
|
/// been added or changed.
|
|
/// </summary>
|
|
[Expose]
|
|
struct ObjectRefreshComponentMsg
|
|
{
|
|
public string JsonData { get; set; }
|
|
public byte[] TableData { get; set; }
|
|
public byte[] Snapshot { get; set; }
|
|
public Guid GameObjectId { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// When a networked object has been refreshed. This is a full update message for that
|
|
/// networked object. Any new GameObjects or Components in the hierarchy will be
|
|
/// created and existing ones will be updated.
|
|
/// </summary>
|
|
[Expose]
|
|
struct ObjectRefreshMsg
|
|
{
|
|
public string JsonData { get; set; }
|
|
public byte[] TableData { get; set; }
|
|
public byte[] Snapshot { get; set; }
|
|
public Guid Parent { get; set; }
|
|
public Guid Guid { get; set; }
|
|
}
|
|
|
|
[Expose]
|
|
struct LoadSceneBeginMsg
|
|
{
|
|
public List<string> MountedVPKs { get; set; }
|
|
public bool ShowLoadingScreen { get; set; }
|
|
public Guid Id { get; set; }
|
|
}
|
|
|
|
[Expose]
|
|
struct LoadSceneRequestSnapshotMsg
|
|
{
|
|
public Guid Id { get; set; }
|
|
}
|
|
|
|
[Expose]
|
|
struct LoadSceneSnapshotMsg
|
|
{
|
|
public SnapshotMsg Snapshot { get; set; }
|
|
public Guid Id { get; set; }
|
|
}
|
|
|
|
[Expose]
|
|
struct SceneLoadedMsg
|
|
{
|
|
public Guid Id { get; set; }
|
|
}
|
|
|
|
[Expose]
|
|
struct ObjectCreateBatchMsg
|
|
{
|
|
public ObjectCreateMsg[] CreateMsgs { get; set; }
|
|
}
|
|
|
|
[Expose]
|
|
struct ObjectCreateMsg
|
|
{
|
|
public ushort SnapshotVersion { get; set; }
|
|
public string JsonData { get; set; }
|
|
public Transform Transform { get; set; }
|
|
public Guid Guid { get; set; }
|
|
public Guid Creator { get; set; }
|
|
public Guid Parent { get; set; }
|
|
public Guid Owner { get; set; }
|
|
public byte[] TableData { get; set; }
|
|
public bool Enabled { get; set; }
|
|
}
|
|
|
|
[Expose]
|
|
struct ObjectNetworkTableMsg
|
|
{
|
|
public Guid Guid { get; set; }
|
|
public byte[] TableData { get; set; }
|
|
}
|
|
|
|
[Expose]
|
|
struct SceneNetworkTableMsg
|
|
{
|
|
public Guid Guid { get; set; }
|
|
public byte[] TableData { get; set; }
|
|
}
|
|
|
|
[Expose]
|
|
struct ObjectDestroyMsg
|
|
{
|
|
public Guid Guid { get; set; }
|
|
}
|
|
|
|
[Expose]
|
|
struct SceneRpcMsg
|
|
{
|
|
public Guid Guid { get; set; }
|
|
public int MethodIdentity { get; set; }
|
|
public object[] Arguments { get; set; }
|
|
}
|
|
|
|
[Expose]
|
|
struct ObjectRpcMsg
|
|
{
|
|
public Guid Guid { get; set; }
|
|
public Guid ComponentId { get; set; }
|
|
public int MethodIdentity { get; set; }
|
|
public object[] Arguments { get; set; }
|
|
}
|
|
|
|
[Expose]
|
|
struct StaticRpcMsg
|
|
{
|
|
public int MethodIdentity { get; set; }
|
|
public object[] Arguments { get; set; }
|
|
}
|