using Sandbox.Network; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Text.Json.Nodes; namespace Sandbox; public partial class GameObject { internal NetworkObject _net { get; private set; } /// /// True if this is a networked object and is owned by another client. This means that we're /// not controlling this object, so shouldn't try to move it or anything. /// public bool IsProxy => Network.IsProxy; /// /// If true then this object is the root of a networked object. /// public bool IsNetworkRoot => _net is not null; /// /// OBSOLETE: Use NetworkMode instead. /// [Obsolete( "Use GameObject.NetworkMode" )] public bool Networked { get => NetworkMode == NetworkMode.Object; set { NetworkMode = value ? NetworkMode.Object : NetworkMode.Snapshot; } } /// /// How should this object be networked to other clients? By default, a will be /// networked as part of the snapshot. /// public NetworkMode NetworkMode { get => _networkMode; set { if ( _net is not null ) { // We must always be `NetworkMode.Object` if we're a networked object. _networkMode = NetworkMode.Object; return; } _networkMode = value; } } private NetworkMode _networkMode = NetworkMode.Snapshot; /// /// A component that can control our network visibility to a specific . /// internal Component.INetworkVisible NetworkVisibility; /// /// If this object is networked, who can control ownership of it? This property will only /// be synchronized for a root network object. /// [Sync, Expose] private OwnerTransfer OwnerTransfer { get; set; } = OwnerTransfer.Fixed; /// /// Determines what happens when the owner disconnects. This property will only /// be synchronized for a root network object. /// [Sync, Expose] private NetworkOrphaned NetworkOrphaned { get; set; } = NetworkOrphaned.Destroy; /// /// Determines whether updates for this networked object are always transmitted to clients. Otherwise, /// they are only transmitted when the object is determined as visible to each client. /// [Property, Expose] private bool AlwaysTransmit { get; set; } = true; /// /// Whether our networked transform will be interpolated. This property will only /// be synchronized for a root network object. /// [Property, Sync, Expose] public bool NetworkInterpolation { get; set; } = true; /// /// Spawn on the network. If you have permission to spawn entities, this will spawn on /// everyone else's clients, and you will be the owner. /// public bool NetworkSpawn() => NetworkSpawn( Connection.Local ); /// /// Spawn on the network with the specified options. If you have permission to spawn /// entities, this will spawn on everyone else's clients. /// public bool NetworkSpawn( NetworkSpawnOptions options ) { // We don't network spawn prefab scene files if ( Scene is PrefabScene ) return false; if ( Scene.IsEditor ) return false; // Already is spawned if ( _net is not null ) return false; var connection = Connection.Local; if ( !connection.CanSpawnObjects ) { Log.Warning( $"{this} is trying to spawn - but we don't have permission!" ); return false; } if ( IsPrefabInstanceRoot ) { PrefabInstanceData.BreakAllPrefabInstanceInHierarchy( this ); } // We may contain other networked children. In which case we want to send // them all in a singular message to keep any references using ( SceneNetworkSystem.Instance?.NetworkSpawnBatch() ) { NetworkMode = NetworkMode.Object; if ( options.OwnerTransfer.HasValue ) OwnerTransfer = options.OwnerTransfer.Value; if ( options.OrphanedMode.HasValue ) NetworkOrphaned = options.OrphanedMode.Value; if ( options.AlwaysTransmit.HasValue ) AlwaysTransmit = options.AlwaysTransmit.Value; // Give us a network object _net = new NetworkObject( this ); // Tell all children that we're the network root UpdateNetworkRoot(); // Make this connection the owner _net.InitializeForConnection( options.Owner, options.StartEnabled ); } return true; } /// /// Spawn on the network. If you have permission to spawn entities, this will spawn on /// everyone else's clients and the owner will be the connection provided. /// public bool NetworkSpawn( bool enabled, Connection owner ) { var options = new NetworkSpawnOptions { StartEnabled = enabled, Owner = owner }; return NetworkSpawn( options ); } /// /// Spawn on the network. If you have permission to spawn entities, this will spawn on /// everyone else's clients and the owner will be the connection provided. /// public bool NetworkSpawn( Connection owner ) => NetworkSpawn( true, owner ); /// /// Initialize this object from the network /// internal void NetworkSpawnRemote( ObjectCreateMsg msg ) { if ( _net is not null ) { _net.OnCreateMessage( msg ); return; } _net = new NetworkObject( this ); _net.Initialize( msg ); UpdateNetworkRoot(); } /// /// Always transmit has been changed by the owner. We can't use Sync Vars for this, because /// they don't get sent if they shouldn't be transmitted. /// [Rpc.Broadcast( NetFlags.OwnerOnly ), Expose] void Msg_UpdateAlwaysTransmit( bool alwaysTransmit ) { AlwaysTransmit = alwaysTransmit; _net?.TransmitStateChanged(); } internal bool IsNetworkCulled { get; set; } void ClearNetworking() { if ( _net is null ) return; _net.Dispose(); _net = null; } /// /// Make a request from the host to stop being the network owner of this game object. /// [Rpc.Broadcast] void Msg_RequestDropOwnership( ushort snapshotVersion ) { if ( _net is null ) return; if ( !Networking.IsHost ) return; if ( OwnerTransfer != OwnerTransfer.Request ) return; var caller = Rpc.Caller; // Does the source connection even own this object? if ( _net.Owner != caller.Id ) return; // Can this caller drop ownership? if ( _net.CanDropOwnership( caller ) ) { Msg_DropOwnership( snapshotVersion ); } } /// /// Stop being the network owner of this game object, or clear ownership if you're the host. /// [Rpc.Broadcast] void Msg_DropOwnership( ushort snapshotVersion ) { if ( _net is null ) return; var caller = Rpc.Caller; // Is it the host telling us to drop ownership of this object? if ( caller.IsHost ) { _net.Owner = Guid.Empty; return; } if ( OwnerTransfer == OwnerTransfer.Request ) return; // Does the source connection even own this object? if ( _net.Owner != caller.Id ) return; _net.LocalSnapshotState.Version = (ushort)(snapshotVersion + 1); _net.Owner = Guid.Empty; } /// /// Make a request from the host to become the network owner of this game object. /// [Rpc.Broadcast] void Msg_RequestTakeOwnership( ushort snapshotVersion ) { if ( _net is null ) return; if ( !Networking.IsHost ) return; if ( OwnerTransfer != OwnerTransfer.Request ) return; // Can this caller take ownership? if ( _net.CanTakeOwnership( Rpc.Caller ) ) { Msg_AssignOwnership( Rpc.CallerId, snapshotVersion ); } } /// /// Set the parent of this networked object. /// [Rpc.Broadcast] void Msg_SetParent( Guid id, Transform transform, ushort snapshotVersion ) { if ( _net is null ) return; var caller = Rpc.Caller; if ( caller == Connection.Local ) return; // Can this caller set the parent? if ( !caller.IsHost && !_net.HasControl( caller ) ) return; var parentObject = Scene.Directory.FindByGuid( id ); if ( !parentObject.IsValid() ) parentObject = Scene; if ( parentObject._net is not null ) { // Does the caller own the parent object too? if ( !caller.IsHost && !parentObject._net.HasControl( caller ) ) return; } _net.LocalSnapshotState.Version = snapshotVersion; SetParentFromNetwork( parentObject, transform ); } /// /// Become the network owner of this game object. /// [Rpc.Broadcast] void Msg_TakeOwnership( ushort snapshotVersion ) { if ( _net is null ) return; var caller = Rpc.Caller; // Can only the host control ownership? if ( OwnerTransfer == OwnerTransfer.Fixed && !caller.IsHost ) return; // Only the host can give ownership if we have to request it. if ( OwnerTransfer == OwnerTransfer.Request && !caller.IsHost ) return; _net.LocalSnapshotState.Version = (ushort)(snapshotVersion + 1); _net.Owner = Rpc.CallerId; } /// /// Make a request from the host to assign ownership of this game object to the specified connection . /// [Rpc.Broadcast] void Msg_RequestAssignOwnership( Guid guid, ushort snapshotVersion ) { if ( _net is null ) return; if ( !Networking.IsHost ) return; if ( OwnerTransfer != OwnerTransfer.Request ) return; // Can this caller assign ownership? if ( _net.CanAssignOwnership( Rpc.Caller, guid ) ) { Msg_AssignOwnership( guid, snapshotVersion ); } } /// /// Assign ownership of this game object to the specified connection . /// /// /// [Rpc.Broadcast] void Msg_AssignOwnership( Guid guid, ushort snapshotVersion ) { if ( _net is null ) return; var caller = Rpc.Caller; // Can only the host control ownership? if ( OwnerTransfer == OwnerTransfer.Fixed && !caller.IsHost ) return; // Only the host can assign ownership if we have to request it. if ( OwnerTransfer == OwnerTransfer.Request && !caller.IsHost ) return; _net.LocalSnapshotState.Version = (ushort)(snapshotVersion + 1); _net.Owner = guid; } [EditorBrowsable( EditorBrowsableState.Never )] protected void __sync_SetValue( in WrappedPropertySet p ) { var root = FindNetworkRoot(); var slot = NetworkObject.GetPropertySlot( p.MemberIdent, Id ); if ( root is null ) { p.Setter( p.Value ); return; } var net = root._net; if ( !net.dataTable.IsRegistered( slot ) ) { p.Setter( p.Value ); return; } if ( !net.dataTable.HasControl( slot ) ) { if ( NetworkTable.IsReadingChanges ) p.Setter( p.Value ); return; } net.dataTable.UpdateSlotHash( slot, p.Value ); p.Setter( p.Value ); } [EditorBrowsable( EditorBrowsableState.Never )] protected T __sync_GetValue( WrappedPropertyGet p ) { // We might want to implement lerp or something later on // so keeping this open in case. return p.Value; } [EditorBrowsable( EditorBrowsableState.Never )] [MethodImpl( MethodImplOptions.AggressiveInlining )] protected void __rpc_Wrapper( in WrappedMethod m, params object[] argumentList ) { Rpc.OnCallInstanceRpc( this, default, m, argumentList ); } /// /// The network root is the first networked GameObject above this. /// This gets set from the parent's NetworkSpawn and invalidated when the parent changes. /// internal GameObject NetworkRoot { get; private set; } internal void UpdateNetworkRoot() { NetworkRoot = FindNetworkRoot(); ForEachChild( "UpdateNetworkRoot", true, go => go.UpdateNetworkRoot() ); } internal GameObject FindNetworkRoot() { if ( _net is not null ) return this; if ( Parent is null || Parent is Scene ) return null; return Parent.FindNetworkRoot(); } /// /// Update hierarchy from a network refresh. /// internal void NetworkRefresh( JsonObject jso ) { Deserialize( jso, new DeserializeOptions { IsRefreshing = true, IsNetworkRefresh = true } ); } /// /// Loop all of our children, and any with networking enabled, we should spawn them /// with the same creator + owner as this. /// internal void NetworkSpawnRecursive( Connection connection ) { if ( Scene is PrefabScene ) return; foreach ( var go in Children ) { // not this child, maybe one of its children if ( go.NetworkMode != NetworkMode.Object ) { go.NetworkSpawnRecursive( connection ); continue; } go.NetworkSpawn( go.Enabled, connection ); } } NetworkAccessor __networkAccess; /// /// Access network information for this GameObject. /// [ActionGraphInclude, Icon( "wifi" )] public NetworkAccessor Network { get { var root = NetworkRoot; if ( root is not null && root != this ) return root.Network; __networkAccess ??= new( this ); return __networkAccess; } } public NetworkAccessor RootNetwork { get { var accessor = Network; if ( IsRoot ) return accessor; var parentAccessor = Parent?.RootNetwork; return (parentAccessor?.Active ?? false) ? parentAccessor : accessor; } } [Expose, ActionGraphIgnore] public class NetworkAccessor { readonly GameObject go; public NetworkAccessor( GameObject o ) { go = o; } /// /// Is this object networked /// public bool Active => go._net is not null; /// /// Get the GameObject that is the root of this network object /// public GameObject RootGameObject => go; /// /// Are we the owner of this network object /// [ActionGraphInclude] public bool IsOwner => OwnerId == Connection.Local.Id; /// /// The Id of the owner of this object /// public Guid OwnerId => go._net?.Owner ?? Guid.Empty; /// /// Are we the creator of this network object /// [ActionGraphInclude] public bool IsCreator => CreatorId == Connection.Local.Id; /// /// The Id of the create of this object /// public Guid CreatorId => go._net?.Creator ?? Guid.Empty; /// /// Is this object a network proxy. A network proxy is a network object that is not being simulated on the local pc. /// This means it's either owned by no-one and is being simulated by the host, or owned by another client. /// [ActionGraphInclude] public bool IsProxy => go._net?.IsProxy ?? false; /// /// Try to get the connection that owns this object. This can and will return null /// if we don't have information for this connection. /// [ActionGraphInclude, Obsolete( "Moved to Owner" )] public Connection OwnerConnection => Owner; /// /// Try to get the connection that owns this object. This can and will return null /// if we don't have information for this connection. /// [ActionGraphInclude] public Connection Owner => Connection.Find( OwnerId ); /// /// Who can control ownership of this networked object? /// public OwnerTransfer OwnerTransfer => go.OwnerTransfer; /// /// Determines what happens when the owner disconnects. /// public NetworkOrphaned NetworkOrphaned => go.NetworkOrphaned; /// /// Current snapshot version. This usually changes when the owner changes. /// internal ushort SnapshotVersion => go._net?.SnapshotVersion ?? 0; /// /// Determines whether updates for this networked object are always transmitted to clients. Otherwise, /// they are only transmitted when the object is determined as visible to each client. /// public bool AlwaysTransmit { get => go.AlwaysTransmit; set { if ( IsProxy ) return; if ( go.AlwaysTransmit == value ) return; go.Msg_UpdateAlwaysTransmit( value ); } } /// /// Whether the networked object's transform is interpolated. /// public bool Interpolation => go.NetworkInterpolation; /// /// Enable interpolation for the networked object's transform. /// public bool EnableInterpolation() { if ( IsProxy ) return false; go.NetworkInterpolation = true; return true; } /// /// Disable interpolation for the networked object's transform. /// public bool DisableInterpolation() { if ( IsProxy ) return false; go.NetworkInterpolation = false; return true; } /// /// /// public bool ClearInterpolation() { go.Transform.ClearInterpolation(); return true; } /// /// Set what happens to this networked object when the owner disconnects. /// public bool SetOrphanedMode( NetworkOrphaned action ) { if ( IsProxy ) return false; go.NetworkOrphaned = action; return true; } /// /// Set who can control ownership of this networked object. Only the current owner can change this. /// public bool SetOwnerTransfer( OwnerTransfer option ) { if ( IsProxy ) return false; go.OwnerTransfer = option; return true; } /// /// Send a complete refresh snapshot of this networked object to other clients. This is useful if you have /// made vast changes to components or children. /// public void Refresh() { if ( IsProxy && !Networking.IsHost ) return; var connection = Connection.Local; if ( !connection.CanRefreshObjects ) { Log.Warning( $"{go} is trying to refresh - but we don't have permission!" ); return; } go._net?.RegisterPropertiesRecursive(); go._net?.SendNetworkRefresh(); } /// /// Send a refresh for a specific in the hierarchy of this networked object to other clients. /// This is useful if you've destroyed or added a new descendent and don't want to refresh /// the entire networked object. /// public void Refresh( GameObject descendent ) { if ( IsProxy && !Networking.IsHost ) return; var connection = Connection.Local; if ( !connection.CanRefreshObjects ) { Log.Warning( $"{go} is trying to refresh - but we don't have permission!" ); return; } go._net?.RegisterPropertiesRecursive(); go._net?.SendNetworkRefresh( descendent ); } /// /// Send a refresh for a specific in the hierarchy of this networked object to other clients. /// This is useful if you've destroyed or added a new and don't want to refresh the entire object. /// public void Refresh( Component component ) { if ( IsProxy && !Networking.IsHost ) return; var connection = Connection.Local; if ( !connection.CanRefreshObjects ) { Log.Warning( $"{go} is trying to refresh - but we don't have permission!" ); return; } go._net?.RegisterPropertiesRecursive(); go._net?.SendNetworkRefresh( component ); } /// /// Become the network owner of this object. ///
///
/// Note: whether you can take ownership of this object depends on the /// of this networked object. ///
[ActionGraphInclude] public bool TakeOwnership() { if ( !Active ) return false; if ( IsOwner ) return false; var snapshotVersion = go.Network.SnapshotVersion; if ( !Networking.IsHost && go.OwnerTransfer == OwnerTransfer.Request ) { go.Msg_RequestTakeOwnership( snapshotVersion ); return true; } go.Msg_TakeOwnership( snapshotVersion ); return true; } /// /// Set the owner of this object to the specified . ///
///
/// Note: whether you can assign ownership of this object depends on the /// of this networked object. ///
/// cannot be null. To clear owner, use instead. [ActionGraphInclude] public bool AssignOwnership( Connection channel ) { ArgumentNullException.ThrowIfNull( channel ); if ( !Active ) return false; var snapshotVersion = go.Network.SnapshotVersion; if ( !IsProxy ) { // Clear interpolation and set that flag here. go.Transform.ClearInterpolation(); // Force a delta snapshot for this object since we changed owner. var system = SceneNetworkSystem.Instance; system?.DeltaSnapshots?.Send( go._net, NetFlags.Reliable, true ); } if ( !Networking.IsHost && go.OwnerTransfer == OwnerTransfer.Request ) { go.Msg_RequestAssignOwnership( channel.Id, snapshotVersion ); return true; } go.Msg_AssignOwnership( channel.Id, snapshotVersion ); return true; } /// /// Assign ownership to the specific connection id. This should only be used internally /// when we want to force an ownership change, such as for a action. /// /// /// internal bool AssignOwnership( Guid connectionId ) { if ( !Active ) return false; if ( !IsProxy ) { // Clear interpolation and set that flag here. go.Transform.ClearInterpolation(); // Force a delta snapshot for this object since we changed owner. var system = SceneNetworkSystem.Instance; system?.DeltaSnapshots?.Send( go._net, NetFlags.Reliable, true ); } go.Msg_AssignOwnership( connectionId, go.Network.SnapshotVersion ); return true; } /// /// Change the cull state of this GameObject on the network for the specified . /// This is for internal use only. /// internal void SetCullState( Connection target, bool isCulled ) { if ( IsProxy ) return; using var bs = ByteStream.Create( 32 ); bs.Write( InternalMessageType.SetCullState ); bs.Write( RootGameObject.Id ); bs.Write( isCulled ); target.SendRawMessage( bs ); } /// /// Stop being the owner of this object. Will clear the owner so the object becomes /// controlled by the server, and owned by no-one. ///
///
/// Note: whether you can drop ownership of this object depends on the /// of this networked object. ///
[ActionGraphInclude] public bool DropOwnership() { if ( !Active ) return false; var snapshotVersion = go.Network.SnapshotVersion; if ( !IsProxy ) { // Clear interpolation and set that flag here. go.Transform.ClearInterpolation(); // Force a delta snapshot for this object since we changed owner. var system = SceneNetworkSystem.Instance; system?.DeltaSnapshots?.Send( go._net, NetFlags.Reliable, true ); } if ( Networking.IsHost ) { // The host can always drop ownership of a networked object. go.Msg_DropOwnership( snapshotVersion ); return true; } if ( !IsOwner ) return false; if ( go.OwnerTransfer == OwnerTransfer.Request ) { go.Msg_RequestDropOwnership( snapshotVersion ); return true; } go.Msg_DropOwnership( snapshotVersion ); return true; } /// /// /// [Obsolete( "Use GameObject.NetworkSpawn" )] public bool Spawn() { return go.NetworkSpawn(); } /// /// /// [Obsolete( "Use GameObject.NetworkSpawn" )] public bool Spawn( Connection owner ) { return go.NetworkSpawn( owner ); } } }