Files
sbox-public/engine/Sandbox.Engine/Scene/GameObject/GameObject.Network.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

881 lines
23 KiB
C#

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; }
/// <summary>
/// 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.
/// </summary>
public bool IsProxy => Network.IsProxy;
/// <summary>
/// If true then this object is the root of a networked object.
/// </summary>
public bool IsNetworkRoot => _net is not null;
/// <summary>
/// OBSOLETE: Use NetworkMode instead.
/// </summary>
[Obsolete( "Use GameObject.NetworkMode" )]
public bool Networked
{
get => NetworkMode == NetworkMode.Object;
set
{
NetworkMode = value ? NetworkMode.Object : NetworkMode.Snapshot;
}
}
/// <summary>
/// How should this object be networked to other clients? By default, a <see cref="GameObject"/> will be
/// networked as part of the <see cref="Scene"/> snapshot.
/// </summary>
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;
/// <summary>
/// A component that can control our network visibility to a specific <see cref="Connection"/>.
/// </summary>
internal Component.INetworkVisible NetworkVisibility;
/// <summary>
/// If this object is networked, who can control ownership of it? This property will only
/// be synchronized for a root network object.
/// </summary>
[Sync, Expose] private OwnerTransfer OwnerTransfer { get; set; } = OwnerTransfer.Fixed;
/// <summary>
/// Determines what happens when the owner disconnects. This property will only
/// be synchronized for a root network object.
/// </summary>
[Sync, Expose] private NetworkOrphaned NetworkOrphaned { get; set; } = NetworkOrphaned.Destroy;
/// <summary>
/// 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.
/// </summary>
[Property, Expose] private bool AlwaysTransmit { get; set; } = true;
/// <summary>
/// Whether our networked transform will be interpolated. This property will only
/// be synchronized for a root network object.
/// </summary>
[Property, Sync, Expose] public bool NetworkInterpolation { get; set; } = true;
/// <summary>
/// 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.
/// </summary>
public bool NetworkSpawn() => NetworkSpawn( Connection.Local );
/// <summary>
/// Spawn on the network with the specified options. If you have permission to spawn
/// entities, this will spawn on everyone else's clients.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
public bool NetworkSpawn( bool enabled, Connection owner )
{
var options = new NetworkSpawnOptions
{
StartEnabled = enabled,
Owner = owner
};
return NetworkSpawn( options );
}
/// <summary>
/// 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.
/// </summary>
public bool NetworkSpawn( Connection owner ) => NetworkSpawn( true, owner );
/// <summary>
/// Initialize this object from the network
/// </summary>
internal void NetworkSpawnRemote( ObjectCreateMsg msg )
{
if ( _net is not null )
{
_net.OnCreateMessage( msg );
return;
}
_net = new NetworkObject( this );
_net.Initialize( msg );
UpdateNetworkRoot();
}
/// <summary>
/// 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.
/// </summary>
[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;
}
/// <summary>
/// Make a request from the host to stop being the network owner of this game object.
/// </summary>
[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 );
}
}
/// <summary>
/// Stop being the network owner of this game object, or clear ownership if you're the host.
/// </summary>
[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;
}
/// <summary>
/// Make a request from the host to become the network owner of this game object.
/// </summary>
[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 );
}
}
/// <summary>
/// Set the parent of this networked object.
/// </summary>
[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 );
}
/// <summary>
/// Become the network owner of this game object.
/// </summary>
[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;
}
/// <summary>
/// Make a request from the host to assign ownership of this game object to the specified connection <see cref="Guid"/>.
/// </summary>
[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 );
}
}
/// <summary>
/// Assign ownership of this game object to the specified connection <see cref="Guid"/>.
/// </summary>
/// <param name="guid"></param>
/// <param name="snapshotVersion"></param>
[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<T>( in WrappedPropertySet<T> 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<T>( WrappedPropertyGet<T> 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 );
}
/// <summary>
/// The network root is the first networked GameObject above this.
/// This gets set from the parent's NetworkSpawn and invalidated when the parent changes.
/// </summary>
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();
}
/// <summary>
/// Update hierarchy from a network refresh.
/// </summary>
internal void NetworkRefresh( JsonObject jso )
{
Deserialize( jso, new DeserializeOptions { IsRefreshing = true, IsNetworkRefresh = true } );
}
/// <summary>
/// Loop all of our children, and any with networking enabled, we should spawn them
/// with the same creator + owner as this.
/// </summary>
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;
/// <summary>
/// Access network information for this GameObject.
/// </summary>
[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;
}
/// <summary>
/// Is this object networked
/// </summary>
public bool Active => go._net is not null;
/// <summary>
/// Get the GameObject that is the root of this network object
/// </summary>
public GameObject RootGameObject => go;
/// <summary>
/// Are we the owner of this network object
/// </summary>
[ActionGraphInclude]
public bool IsOwner => OwnerId == Connection.Local.Id;
/// <summary>
/// The Id of the owner of this object
/// </summary>
public Guid OwnerId => go._net?.Owner ?? Guid.Empty;
/// <summary>
/// Are we the creator of this network object
/// </summary>
[ActionGraphInclude]
public bool IsCreator => CreatorId == Connection.Local.Id;
/// <summary>
/// The Id of the create of this object
/// </summary>
public Guid CreatorId => go._net?.Creator ?? Guid.Empty;
/// <summary>
/// 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.
/// </summary>
[ActionGraphInclude]
public bool IsProxy => go._net?.IsProxy ?? false;
/// <summary>
/// Try to get the connection that owns this object. This can and will return null
/// if we don't have information for this connection.
/// </summary>
[ActionGraphInclude, Obsolete( "Moved to Owner" )]
public Connection OwnerConnection => Owner;
/// <summary>
/// Try to get the connection that owns this object. This can and will return null
/// if we don't have information for this connection.
/// </summary>
[ActionGraphInclude]
public Connection Owner => Connection.Find( OwnerId );
/// <summary>
/// Who can control ownership of this networked object?
/// </summary>
public OwnerTransfer OwnerTransfer => go.OwnerTransfer;
/// <summary>
/// Determines what happens when the owner disconnects.
/// </summary>
public NetworkOrphaned NetworkOrphaned => go.NetworkOrphaned;
/// <summary>
/// Current snapshot version. This usually changes when the owner changes.
/// </summary>
internal ushort SnapshotVersion => go._net?.SnapshotVersion ?? 0;
/// <summary>
/// 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.
/// </summary>
public bool AlwaysTransmit
{
get => go.AlwaysTransmit;
set
{
if ( IsProxy )
return;
if ( go.AlwaysTransmit == value )
return;
go.Msg_UpdateAlwaysTransmit( value );
}
}
/// <summary>
/// Whether the networked object's transform is interpolated.
/// </summary>
public bool Interpolation => go.NetworkInterpolation;
/// <summary>
/// Enable interpolation for the networked object's transform.
/// </summary>
public bool EnableInterpolation()
{
if ( IsProxy ) return false;
go.NetworkInterpolation = true;
return true;
}
/// <summary>
/// Disable interpolation for the networked object's transform.
/// </summary>
public bool DisableInterpolation()
{
if ( IsProxy ) return false;
go.NetworkInterpolation = false;
return true;
}
/// <summary>
/// <inheritdoc cref="GameTransform.ClearInterpolation()"/>
/// </summary>
public bool ClearInterpolation()
{
go.Transform.ClearInterpolation();
return true;
}
/// <summary>
/// Set what happens to this networked object when the owner disconnects.
/// </summary>
public bool SetOrphanedMode( NetworkOrphaned action )
{
if ( IsProxy ) return false;
go.NetworkOrphaned = action;
return true;
}
/// <summary>
/// Set who can control ownership of this networked object. Only the current owner can change this.
/// </summary>
public bool SetOwnerTransfer( OwnerTransfer option )
{
if ( IsProxy ) return false;
go.OwnerTransfer = option;
return true;
}
/// <summary>
/// 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.
/// </summary>
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();
}
/// <summary>
/// Send a refresh for a specific <see cref="GameObject"/> in the hierarchy of this networked object to other clients.
/// This is useful if you've destroyed or added a new <see cref="GameObject"/> descendent and don't want to refresh
/// the entire networked object.
/// </summary>
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 );
}
/// <summary>
/// Send a refresh for a specific <see cref="Component"/> in the hierarchy of this networked object to other clients.
/// This is useful if you've destroyed or added a new <see cref="Component"/> and don't want to refresh the entire object.
/// </summary>
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 );
}
/// <summary>
/// Become the network owner of this object.
/// <br/>
/// <br/>
/// Note: whether you can take ownership of this object depends on the
/// <see cref="OwnerTransfer"/> of this networked object.
/// </summary>
[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;
}
/// <summary>
/// Set the owner of this object to the specified <see cref="Connection"/>.
/// <br/>
/// <br/>
/// Note: whether you can assign ownership of this object depends on the
/// <see cref="OwnerTransfer"/> of this networked object.
/// </summary>
/// <exception cref="ArgumentNullException"><paramref name="channel"/> cannot be null. To clear owner, use <see cref="DropOwnership"/> instead.</exception>
[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;
}
/// <summary>
/// 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 <see cref="NetworkOrphaned"/> action.
/// </summary>
/// <param name="connectionId"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Change the cull state of this GameObject on the network for the specified <see cref="Connection"/>.
/// This is for internal use only.
/// </summary>
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 );
}
/// <summary>
/// Stop being the owner of this object. Will clear the owner so the object becomes
/// controlled by the server, and owned by no-one.
/// <br/>
/// <br/>
/// Note: whether you can drop ownership of this object depends on the
/// <see cref="OwnerTransfer"/> of this networked object.
/// </summary>
[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;
}
/// <summary>
/// <inheritdoc cref="GameObject.NetworkSpawn()"/>
/// </summary>
[Obsolete( "Use GameObject.NetworkSpawn" )]
public bool Spawn()
{
return go.NetworkSpawn();
}
/// <summary>
/// <inheritdoc cref="GameObject.NetworkSpawn( Connection )"/>
/// </summary>
[Obsolete( "Use GameObject.NetworkSpawn" )]
public bool Spawn( Connection owner )
{
return go.NetworkSpawn( owner );
}
}
}