using Sandbox.Network; using System.Runtime.InteropServices; using System.Text.Json.Nodes; using static Sandbox.Component; namespace Sandbox; internal sealed partial class NetworkObject : IValid, IDeltaSnapshot { internal NetworkObject RootNetworkObject => GameObject.RootNetwork.RootGameObject._net; internal GameObject GameObject { get; set; } Guid IDeltaSnapshot.Id => Id; /// /// The unique of the underlying . /// internal Guid Id => GameObject.Id; /// /// The of the connection that created this. /// public Guid Creator { get; set; } public bool IsValid => GameObject.IsValid(); /// /// If true then this object is spawning on the host, on behalf of another client. While it's /// doing this we're going to act like the host is the owner.. so that anything that is called in /// OnAwake will think we're not a proxy - until we've fully handed it off. /// bool _isNetworkSpawning; /// /// The of the connection that owns this. /// public Guid Owner { get; set { if ( field == value ) return; var oldOwner = field; field = value; OnOwnerChanged( field, oldOwner ); } } /// /// Are we the owner of this networked object? /// public bool IsOwner => Owner == Connection.Local.Id; /// /// Is this networked object unowned? /// public bool IsUnowned => Owner == Guid.Empty; /// /// This is this a proxy if we don't own this networked object. /// public bool IsProxy { get { if ( _isNetworkSpawning ) return false; if ( IsOwner ) return false; if ( IsUnowned && Networking.IsHost ) return false; return true; } } /// /// Current snapshot version for this networked object. /// public ushort SnapshotVersion => LocalSnapshotState.Version; bool _clearInterpolationFlag; bool _hasNetworkDestroyed; bool _initialized; internal void OnHotload() { // Build the network table again as properties may have changed. CreateDataTable(); } internal NetworkObject( GameObject source ) { GameObject = source; } /// /// Initialize and spawn this networked object with the specified owner . /// internal void InitializeForConnection( Connection owner, bool enable ) { if ( _initialized ) throw new( "NetworkObject already initialized" ); using var _ = PerformanceStats.Timings.Network.Scope(); _initialized = true; if ( owner is not null ) { Creator = owner.Id; Owner = owner.Id; } else { Creator = Guid.Empty; Owner = Guid.Empty; } CreateDataTable(); // Keep track of us GameObject.Scene.RegisterNetworkedObject( this ); // Call OnAwake on everything. We allow you to enable here because if you're the // host spawning this object for other connections, then you probably want to act // like the owner of it while OnAwake/OnEnabled is being called. _isNetworkSpawning = true; GameObject.Enabled = enable; CallNetworkSpawn( owner ); _isNetworkSpawning = false; // Tell the world that we're here BroadcastNetworkSpawn( owner ); } /// /// Call INetworkSpawn hooks /// private void CallNetworkSpawn( Connection owner ) { foreach ( var target in GameObject.Components.GetAll( FindMode.EverythingInSelfAndDescendants ).ToArray() ) { try { target.OnNetworkSpawn( owner ); } catch ( Exception e ) { Log.Error( e ); } } } /// /// Tell everyone that we exist, and spawn any child network objects with the same owner. /// void BroadcastNetworkSpawn( Connection owner ) { // Tell everyone that we exist SceneNetworkSystem.Instance?.NetworkSpawnBroadcast( this ); // If we have any child network objects, then spawn them with the same owner GameObject.NetworkSpawnRecursive( owner ); } /// /// Initialize this networked object from a create message. /// internal void Initialize( ObjectCreateMsg msg ) { if ( _initialized ) throw new( "NetworkObject already initialized" ); using var _ = PerformanceStats.Timings.Network.Scope(); _initialized = true; Creator = msg.Creator; Owner = msg.Owner; CreateDataTable(); OnCreateMessage( msg ); GameObject.Scene.RegisterNetworkedObject( this ); } internal void Dispose() { GameObject.Scene.UnregisterNetworkObject( this ); GameObject = default; } internal void ClearInterpolation() { if ( IsProxy ) return; _clearInterpolationFlag = true; } internal bool CanDropOwnership( Connection source ) { // Conna: accept all requests for now. In future, maybe we can do something with INetworkListener? return true; } internal bool CanAssignOwnership( Connection source, Guid target ) { // Conna: accept all requests for now. In future, maybe we can do something with INetworkListener? return true; } internal bool CanTakeOwnership( Connection source ) { // Conna: accept all requests for now. In future, maybe we can do something with INetworkListener? return true; } internal void OnNetworkDestroy() { _hasNetworkDestroyed = true; GameObject.Destroy(); } internal void SendNetworkDestroy() { if ( SceneNetworkSystem.Instance is null ) return; if ( Networking.IsDisconnecting ) return; if ( _hasNetworkDestroyed ) return; if ( IsProxy && !Networking.IsHost ) return; var msg = new ObjectDestroyMsg { Guid = GameObject.Id }; SceneNetworkSystem.Instance.Broadcast( msg ); } internal ObjectRefreshMsg GetRefreshMessage() { var system = SceneNetworkSystem.Instance; if ( system is null ) throw new Exception( "SceneNetworkSystem is null" ); var snapshot = ((IDeltaSnapshot)this).WriteSnapshotState(); var o = new GameObject.SerializeOptions { SingleNetworkObject = true }; var msg = new ObjectRefreshMsg { Guid = GameObject.Id, Parent = GameObject.Parent.Id, JsonData = GameObject.Serialize( o ).ToJsonString(), TableData = WriteReliableData(), Snapshot = system.DeltaSnapshots.GetFullSnapshotData( snapshot ) }; return msg; } internal void SendNetworkRefresh( GameObject go ) { var system = SceneNetworkSystem.Instance; if ( system is null ) return; if ( go is null ) return; if ( go == GameObject ) { // If the object passed isn't actually a descendant, then refresh // the entire tree. SendNetworkRefresh(); return; } if ( go.IsDestroyed ) { // We want to tell clients that this component has been destroyed... var msg = new ObjectDestroyDescendantMsg { Guid = go.Id }; system.Broadcast( msg ); return; } if ( !go.IsAncestor( GameObject ) ) return; { var snapshot = ((IDeltaSnapshot)this).WriteSnapshotState(); // Only this one object... var options = new GameObject.SerializeOptions { IgnoreChildren = true }; var msg = new ObjectRefreshDescendantMsg { GameObjectId = GameObject.Id, ParentId = go.Parent.Id, JsonData = go.Serialize( options ).ToJsonString(), TableData = WriteReliableData(), Snapshot = system.DeltaSnapshots.GetFullSnapshotData( snapshot ) }; system.Broadcast( msg ); } } internal void SendNetworkRefresh( Component component ) { var system = SceneNetworkSystem.Instance; if ( system is null ) return; if ( component is null ) return; if ( !component.IsValid() ) { // We want to tell clients that this component has been destroyed... var msg = new ObjectDestroyComponentMsg { Guid = component.Id }; system.Broadcast( msg ); return; } { var snapshot = ((IDeltaSnapshot)this).WriteSnapshotState(); var msg = new ObjectRefreshComponentMsg { JsonData = component.Serialize().ToJsonString(), GameObjectId = component.GameObject.Id, TableData = WriteReliableData(), Snapshot = system.DeltaSnapshots.GetFullSnapshotData( snapshot ) }; system.Broadcast( msg ); } } internal void SendNetworkRefresh() { var system = SceneNetworkSystem.Instance; if ( system is null ) return; var msg = GetRefreshMessage(); system.Broadcast( msg ); } private struct CullState { public bool Culled; public float LastVisibleAt; } internal readonly LocalSnapshotState LocalSnapshotState = new(); private readonly HashSet _culledConnections = []; private readonly Dictionary _cullStates = new(); private readonly SnapshotValueCache _snapshotCache = new(); private TimeUntil _nextUpdateCachedBounds; private BBox _cachedLocalBounds; /// /// Only cull this object if we've been invisible for this long. /// private const float CullDelay = 2f; /// /// Remove a connection id from any internal data structures. /// /// internal void RemoveConnection( Guid id ) { LocalSnapshotState.RemoveConnection( id ); _culledConnections.Remove( id ); _cullStates.Remove( id ); } /// /// Clear all connections associated with the local snapshot state. /// internal void ClearConnections() { LocalSnapshotState.ClearConnections(); } bool IDeltaSnapshot.ShouldTransmit( Connection target ) { return GameObject.Network.AlwaysTransmit || !_culledConnections.Contains( target.Id ); } bool IDeltaSnapshot.UpdateTransmitState( Connection[] targets ) { if ( GameObject.Network.AlwaysTransmit ) { for ( var i = 0; i < targets.Length; i++ ) { var target = targets[i]; if ( !_culledConnections.Remove( target.Id ) ) continue; GameObject.Network.SetCullState( target, false ); } return true; } if ( _nextUpdateCachedBounds ) { // Let's update the cached local bounds every half a second. _nextUpdateCachedBounds = 0.5f; _cachedLocalBounds = GameObject.GetLocalBounds(); } var shouldTransmitToAny = false; var worldBounds = _cachedLocalBounds + GameObject.WorldPosition; var timeNow = Time.Now; var rootNetworkObject = RootNetworkObject; IDeltaSnapshot root = rootNetworkObject != this ? rootNetworkObject : null; for ( var i = 0; i < targets.Length; i++ ) { var target = targets[i]; ref var state = ref CollectionsMarshal.GetValueRefOrAddDefault( _cullStates, target.Id, out var exists ); if ( !exists ) { state = new CullState { Culled = false, LastVisibleAt = timeNow }; } if ( !state.Culled ) shouldTransmitToAny = true; if ( (root?.ShouldTransmit( target ) ?? false) || IsVisible( target, worldBounds ) ) { state.LastVisibleAt = timeNow; if ( !state.Culled ) continue; if ( !_culledConnections.Remove( target.Id ) ) continue; LocalSnapshotState.RemoveConnection( target.Id ); GameObject.Network.SetCullState( target, false ); shouldTransmitToAny = true; state.Culled = false; } else { var timeSinceVisible = timeNow - state.LastVisibleAt; if ( state.Culled || timeSinceVisible < CullDelay ) continue; if ( !_culledConnections.Add( target.Id ) ) continue; GameObject.Network.SetCullState( target, true ); state.Culled = true; } } return shouldTransmitToAny; } /// /// Is this network object visible to the provided . We'll check if we /// have a culler component and use that, but we'll also use our bounds to determine if we're /// visible. /// private bool IsVisible( Connection target, BBox worldBounds ) { // Do we have a INetworkVisible? We're going to let that take priority. var go = GameObject; if ( go.IsValid() && go.Enabled && go.NetworkVisibility is not null ) { return go.NetworkVisibility.IsVisibleToConnection( target, worldBounds ); } // Global culling return GameObject.Scene.IsBBoxVisibleToConnection( target, worldBounds ); } void IDeltaSnapshot.OnSnapshotAck( Connection source, DeltaSnapshot snapshot, RemoteSnapshotState state ) { IDeltaSnapshot snapshotter = this; if ( !snapshotter.ShouldTransmit( source ) ) return; var hasFullSnapshotState = true; foreach ( var entry in LocalSnapshotState.Entries ) { if ( state.IsValueHashEqual( entry.Slot, entry.Hash, snapshot.SnapshotId ) ) { entry.Connections.Add( source.Id ); } else { entry.Connections.Remove( source.Id ); hasFullSnapshotState = false; } } if ( hasFullSnapshotState ) LocalSnapshotState.UpdatedConnections.Add( source.Id ); else LocalSnapshotState.UpdatedConnections.Remove( source.Id ); } private const int SnapshotPositionSlot = 1; private const int SnapshotRotationSlot = 2; private const int SnapshotScaleSlot = 3; private const int SnapshotInterpolationSlot = 4; private const int SnapshotEnabledSlot = 5; LocalSnapshotState IDeltaSnapshot.WriteSnapshotState() { var system = SceneNetworkSystem.Instance; if ( system is null ) return null; LocalSnapshotState.SnapshotId = system.DeltaSnapshots.CreateSnapshotId( Id ); LocalSnapshotState.ObjectId = Id; if ( !IsProxy ) { var tx = GameObject.Transform.TargetLocal; LocalSnapshotState.AddCached( _snapshotCache, SnapshotPositionSlot, tx.Position ); LocalSnapshotState.AddCached( _snapshotCache, SnapshotRotationSlot, tx.Rotation ); LocalSnapshotState.AddCached( _snapshotCache, SnapshotScaleSlot, tx.Scale ); LocalSnapshotState.AddCached( _snapshotCache, SnapshotInterpolationSlot, _clearInterpolationFlag ); LocalSnapshotState.AddCached( _snapshotCache, SnapshotEnabledSlot, GameObject.Enabled ); } dataTable.QueryValues(); dataTable.WriteSnapshotState( LocalSnapshotState ); _clearInterpolationFlag = false; return LocalSnapshotState; } void IDeltaSnapshot.SendNetworkUpdate( bool queryValues ) { if ( queryValues ) dataTable.QueryValues( true ); if ( !dataTable.HasReliableChanges() ) return; var msg = new ObjectNetworkTableMsg { Guid = GameObject.Id }; var data = ByteStream.Create( 4096 ); dataTable.WriteReliableChanged( ref data ); msg.TableData = data.ToArray(); data.Dispose(); SceneNetworkSystem.Instance.Broadcast( msg ); } internal void TransmitStateChanged() { LocalSnapshotState.ClearConnections(); } internal ObjectCreateMsg GetCreateMessage() { var o = new GameObject.SerializeOptions { SingleNetworkObject = true }; if ( GameObject.Parent is null ) { throw new( $"GameObject {GameObject.Id} ({GameObject.Name} has invalid parent" ); } var jsonData = GameObject.Serialize( o ); if ( jsonData is null ) { throw new( $"Unable to serialize {GameObject.Id} ({GameObject.Name})" ); } var create = new ObjectCreateMsg { Guid = GameObject.Id, SnapshotVersion = GameObject._net.LocalSnapshotState.Version, Transform = GameObject.Transform.TargetLocal, JsonData = jsonData.ToJsonString(), Creator = Creator, Parent = GameObject.Parent.Id, Owner = Owner, TableData = WriteDataTable( true ), Enabled = GameObject.Enabled }; return create; } internal void DoOrphanedAction() { var action = GameObject.Network.NetworkOrphaned; if ( action == NetworkOrphaned.Destroy ) { GameObject.Destroy(); } else if ( action == NetworkOrphaned.ClearOwner ) { if ( Networking.IsHost ) GameObject.Network.AssignOwnership( Guid.Empty ); else Owner = Guid.Empty; } else if ( action == NetworkOrphaned.Random ) { // Only the host can assign ownership to a random connection. Because they'll need to broadcast // the random selection to everyone else. if ( Networking.IsHost ) { var connections = Connection.All.ToArray(); var randomIndex = Game.Random.Int( 0, connections.Length - 1 ); var connection = connections[randomIndex]; GameObject.Network.AssignOwnership( connection ); } else { // We're not the host so let's just clear the owner until we get the new randomly // selected owner from the host. Owner = Guid.Empty; } } else if ( action == NetworkOrphaned.Host ) { if ( Networking.IsHost ) GameObject.Network.AssignOwnership( Connection.Host?.Id ?? Guid.Empty ); else Owner = Guid.Empty; } } internal void OnNetworkTableMessage( ObjectNetworkTableMsg message ) { ReadDataTable( message.TableData ); } internal void OnRefreshMessage( Connection source, ObjectRefreshMsg message ) { var scene = Game.ActiveScene; if ( !scene.IsValid() ) return; var jsonObj = JsonNode.Parse( message.JsonData ).AsObject(); GameObject.SetParentFromNetwork( scene.Directory.FindByGuid( message.Parent ) ); GameObject.NetworkRefresh( jsonObj ); UpdateFromRefresh( source, message.TableData, message.Snapshot ); } internal void UpdateFromRefresh( Connection source, byte[] tableData, byte[] snapshotData ) { RegisterPropertiesRecursive(); var system = SceneNetworkSystem.Instance; if ( system is null ) return; ReadDataTable( tableData ); var bs = ByteStream.CreateReader( snapshotData ); system.DeltaSnapshots.OnDeltaSnapshot( source, bs ); bs.Dispose(); } internal void OnCreateMessage( ObjectCreateMsg msg ) { LocalSnapshotState.Version = msg.SnapshotVersion; var parent = GameObject.Scene.Directory.FindByGuid( msg.Parent ); GameObject.Transform.SetLocalTransformFast( msg.Transform ); GameObject.SetParentFromNetwork( parent ); GameObject.Enabled = msg.Enabled; ReadDataTable( msg.TableData ); } bool IDeltaSnapshot.OnSnapshot( Connection source, DeltaSnapshot snapshot ) { // Don't process this if the source connection does not have control, and they // are not the host. if ( !HasControl( source ) && !source.IsHost ) return false; // Conna: only what we regard as the owner can modify this shit. if ( HasControl( source ) ) { snapshot.TryGetValue( SnapshotInterpolationSlot, out var clearInterpolation ); var didTransformChange = false; var transform = GameObject.Transform.TargetLocal; if ( snapshot.TryGetValue( SnapshotPositionSlot, out var position ) ) { didTransformChange = true; transform.Position = position; } if ( snapshot.TryGetValue( SnapshotRotationSlot, out var rotation ) ) { didTransformChange = true; transform.Rotation = rotation; } if ( snapshot.TryGetValue( SnapshotScaleSlot, out var scale ) ) { didTransformChange = true; transform.Scale = scale; } if ( didTransformChange ) { GameObject.Transform.FromNetwork( transform, clearInterpolation ); } else if ( clearInterpolation ) { GameObject.Transform.ClearLocalInterpolation(); } if ( snapshot.TryGetValue( SnapshotEnabledSlot, out var enabled ) ) { GameObject.Enabled = enabled; } } dataTable.ReadSnapshot( source, snapshot ); return true; } /// /// Whether the specified has control over this networked object. A connection /// has control if the object is unowned and they are the host, or if they own it directly. /// internal bool HasControl( Connection c ) { if ( IsUnowned ) return c.IsHost; return c.Id == Owner; } void OnOwnerChanged( Guid newOwner, Guid prevOwner ) { var wasOwner = (prevOwner == Connection.Local.Id) || (prevOwner == Guid.Empty && Networking.IsHost); var isOwner = (newOwner == Connection.Local.Id) || (newOwner == Guid.Empty && Networking.IsHost); var newConnection = Connection.Find( newOwner ); var oldConnection = Connection.Find( prevOwner ); // Conna: clear interpolation when ownership changes. GameObject.Transform.ClearLocalInterpolation(); IGameObjectNetworkEvents.PostToGameObject( GameObject, x => x.NetworkOwnerChanged( newConnection, oldConnection ) ); if ( wasOwner && !isOwner ) { IGameObjectNetworkEvents.PostToGameObject( GameObject, x => x.StopControl() ); } if ( isOwner && !wasOwner ) { IGameObjectNetworkEvents.PostToGameObject( GameObject, x => x.StartControl() ); } var system = SceneNetworkSystem.Instance; system?.DeltaSnapshots.ClearNetworkObject( this ); LocalSnapshotState.ClearConnections(); if ( !isOwner ) return; GameObject.IsNetworkCulled = false; GameObject.UpdateNetworkCulledState(); } }