Files
sbox-public/engine/Sandbox.Engine/Scene/Networking/NetworkObject.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

798 lines
20 KiB
C#

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;
/// <summary>
/// The unique <see cref="Guid"/> of the underlying <see cref="GameObject"/>.
/// </summary>
internal Guid Id => GameObject.Id;
/// <summary>
/// The <see cref="Guid"/> of the connection that created this.
/// </summary>
public Guid Creator { get; set; }
public bool IsValid => GameObject.IsValid();
/// <summary>
/// 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.
/// </summary>
bool _isNetworkSpawning;
/// <summary>
/// The <see cref="Guid"/> of the connection that owns this.
/// </summary>
public Guid Owner
{
get;
set
{
if ( field == value )
return;
var oldOwner = field;
field = value;
OnOwnerChanged( field, oldOwner );
}
}
/// <summary>
/// Are we the owner of this networked object?
/// </summary>
public bool IsOwner => Owner == Connection.Local.Id;
/// <summary>
/// Is this networked object unowned?
/// </summary>
public bool IsUnowned => Owner == Guid.Empty;
/// <summary>
/// This is this a proxy if we don't own this networked object.
/// </summary>
public bool IsProxy
{
get
{
if ( _isNetworkSpawning ) return false;
if ( IsOwner ) return false;
if ( IsUnowned && Networking.IsHost ) return false;
return true;
}
}
/// <summary>
/// Current snapshot version for this networked object.
/// </summary>
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;
}
/// <summary>
/// Initialize and spawn this networked object with the specified owner <see cref="Connection"/>.
/// </summary>
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 );
}
/// <summary>
/// Call INetworkSpawn hooks
/// </summary>
private void CallNetworkSpawn( Connection owner )
{
foreach ( var target in GameObject.Components.GetAll<INetworkSpawn>( FindMode.EverythingInSelfAndDescendants ).ToArray() )
{
try
{
target.OnNetworkSpawn( owner );
}
catch ( Exception e )
{
Log.Error( e );
}
}
}
/// <summary>
/// Tell everyone that we exist, and spawn any child network objects with the same owner.
/// </summary>
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 );
}
/// <summary>
/// Initialize this networked object from a create message.
/// </summary>
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<Guid> _culledConnections = [];
private readonly Dictionary<Guid, CullState> _cullStates = new();
private readonly SnapshotValueCache _snapshotCache = new();
private TimeUntil _nextUpdateCachedBounds;
private BBox _cachedLocalBounds;
/// <summary>
/// Only cull this object if we've been invisible for this long.
/// </summary>
private const float CullDelay = 2f;
/// <summary>
/// Remove a connection id from any internal data structures.
/// </summary>
/// <param name="id"></param>
internal void RemoveConnection( Guid id )
{
LocalSnapshotState.RemoveConnection( id );
_culledConnections.Remove( id );
_cullStates.Remove( id );
}
/// <summary>
/// Clear all connections associated with the local snapshot state.
/// </summary>
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;
}
/// <summary>
/// Is this network object visible to the provided <see cref="Connection"/>. 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.
/// </summary>
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<bool>( SnapshotInterpolationSlot, out var clearInterpolation );
var didTransformChange = false;
var transform = GameObject.Transform.TargetLocal;
if ( snapshot.TryGetValue<Vector3>( SnapshotPositionSlot, out var position ) )
{
didTransformChange = true;
transform.Position = position;
}
if ( snapshot.TryGetValue<Rotation>( SnapshotRotationSlot, out var rotation ) )
{
didTransformChange = true;
transform.Rotation = rotation;
}
if ( snapshot.TryGetValue<Vector3>( 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<bool>( SnapshotEnabledSlot, out var enabled ) )
{
GameObject.Enabled = enabled;
}
}
dataTable.ReadSnapshot( source, snapshot );
return true;
}
/// <summary>
/// Whether the specified <see cref="Connection"/> 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.
/// </summary>
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();
}
}