using System.Collections; namespace Sandbox.Network; internal class NetworkTable : IDisposable { /// /// Internal flag set while reading changes. Useful when you want to force /// something to be set when we otherwise wouldn't have permission to. /// internal static bool IsReadingChanges { get; private set; } public class Entry : INetworkProxy { public Type TargetType { get; init; } public string DebugName { get; init; } public bool NeedsQuery { get; set; } public Func ControlCondition { get; init; } = c => true; public Func GetValue { get; init; } public Action SetValue { get; init; } public Action OnDirty { get; set; } public int HashValue { get; set; } public bool IsSerializerType { get; private set; } public bool IsDeltaSnapshotType { get; private set; } public bool IsReliableType { get; set; } public byte[] Serialized { get; set; } public bool Initialized { get; set; } public int Slot { get; private set; } private bool InternalIsDirty { get; set; } public bool IsDirty { get => InternalIsDirty; set { if ( InternalIsDirty == value ) return; InternalIsDirty = value; OnDirty?.Invoke( this ); } } bool INetworkProxy.IsProxy => !HasControl( Connection.Local ); /// /// Whether the specified has control of this entry. /// public bool HasControl( Connection c ) { return ControlCondition?.Invoke( c ) ?? true; } /// /// Whether we (our local ) have control of this entry. /// /// public bool HasControl() { return HasControl( Connection.Local ); } internal void Init( int slot ) { if ( TargetType is null ) return; var isListType = TargetType.IsAssignableTo( typeof( IList ) ); var isDictionaryType = TargetType.IsAssignableTo( typeof( IDictionary ) ); IsDeltaSnapshotType = TargetType.IsAssignableTo( typeof( INetworkDeltaSnapshot ) ); IsReliableType = !IsDeltaSnapshotType && TargetType.IsAssignableTo( typeof( INetworkReliable ) ); IsSerializerType = TargetType.IsAssignableTo( typeof( INetworkSerializer ) ); NeedsQuery |= (isListType || isDictionaryType || IsSerializerType); Slot = slot; } } private readonly Dictionary _entries = new(); private readonly List _reliableEntries = []; private readonly List _snapshotEntries = []; private readonly List _queryEntries = []; /// /// Do we have any pending changes for entries we control? /// public bool HasAnyChanges => _entries.Values.Any( entry => entry.HasControl() && entry.IsDirty ); /// /// Do we have any pending reliable changes for entries we control? /// public bool HasReliableChanges() { for ( var i = 0; i < _reliableEntries.Count; i++ ) { var entry = _reliableEntries[i]; if ( !entry.IsDirty ) continue; if ( entry.HasControl() ) return true; } return false; } public void Dispose() { _reliableEntries.Clear(); _snapshotEntries.Clear(); _queryEntries.Clear(); _entries.Clear(); } /// /// Unregister a variable assigned to a slot id. /// /// public void Unregister( int slot ) { _snapshotEntries.RemoveAll( e => e.Slot == slot ); _reliableEntries.RemoveAll( e => e.Slot == slot ); _queryEntries.RemoveAll( e => e.Slot == slot ); _entries.Remove( slot ); } /// /// Register a variable assigned to a slot id. /// public void Register( int slot, Entry entry ) { _entries[slot] = entry; var value = GetValue( slot ); UpdateSlotHash( slot, value ); entry.Init( slot ); entry.IsDirty = true; if ( entry.IsReliableType ) _reliableEntries.Add( entry ); else _snapshotEntries.Add( entry ); if ( entry.NeedsQuery ) _queryEntries.Add( entry ); } /// /// Get a variable from a slot id. /// public object GetValue( int slot ) { return !_entries.TryGetValue( slot, out var v ) ? default : v.GetValue(); } /// /// Does a variable with the specified slot exist? /// public bool IsRegistered( int slot ) { return _entries.ContainsKey( slot ); } /// /// Do we have control over the value for a specific slot id? /// public bool HasControl( int slot ) { return _entries.TryGetValue( slot, out var v ) && v.HasControl(); } /// /// Update the hash for a specific entry. /// private void UpdateSlotHash( Entry entry, object value ) { if ( value is INetworkProperty property && !entry.Initialized ) { property.Init( entry.Slot, entry ); entry.Initialized = true; } if ( value is INetworkSerializer serializer ) { if ( !serializer.HasChanges ) return; entry.Serialized = null; entry.IsDirty = true; return; } var hashValue = GenerateHash( value ); if ( entry.HashValue == hashValue ) return; entry.HashValue = hashValue; entry.Serialized = null; entry.IsDirty = true; } /// /// Update the hash for a specific slot id. /// public void UpdateSlotHash( int slot, object value ) { if ( !_entries.TryGetValue( slot, out var v ) ) return; UpdateSlotHash( v, value ); } private int GenerateHash( object value ) { if ( value is IList list ) { HashCode hc = default; hc.Add( list.Count ); for ( var i = 0; i < list.Count; i++ ) { hc.Add( list[i] ); } return hc.ToHashCode(); } if ( value is IDictionary dictionary ) { HashCode hc = default; hc.Add( dictionary.Count ); foreach ( DictionaryEntry item in dictionary ) { hc.Add( HashCode.Combine( item.Key, item.Value ) ); } return hc.ToHashCode(); } return HashCode.Combine( value ); } /// /// Set a variable from a slot id. /// public void SetValue( int slot, object value ) { if ( !_entries.TryGetValue( slot, out var entry ) ) return; try { var oldValue = entry.GetValue(); if ( Equals( oldValue, value ) ) return; entry.Initialized = false; UpdateSlotHash( slot, value ); entry.SetValue( value ); } catch ( Exception e ) { Log.Warning( e, $"Error when setting value {entry.DebugName} - {e.Message}" ); } } /// /// Write supported snapshot variables serialized to the specified dictionary. /// /// internal void WriteSnapshotState( LocalSnapshotState snapshot ) { for ( var i = 0; i < _snapshotEntries.Count; i++ ) { var entry = _snapshotEntries[i]; if ( !entry.HasControl() ) continue; if ( entry.IsDeltaSnapshotType ) { var value = entry.GetValue() as INetworkDeltaSnapshot; value?.WriteSnapshotState( entry.Slot, snapshot ); continue; } try { if ( entry.Serialized is null ) { var bs = ByteStream.Create( 4096 ); WriteEntryToStream( entry, ref bs ); entry.Serialized = bs.ToArray(); bs.Dispose(); } snapshot.AddSerialized( entry.Slot, entry.Serialized ); } catch ( Exception e ) { Log.Warning( e, $"Error when getting value {entry.DebugName} - {e.Message}" ); } } } /// /// Read and apply any variables from the provided snapshot. /// /// /// internal void ReadSnapshot( Connection source, DeltaSnapshot snapshot ) { foreach ( var entry in _snapshotEntries ) { if ( !entry.IsDeltaSnapshotType ) continue; // The connection sending us this can't modify it! if ( !entry.HasControl( source ) ) continue; var value = entry.GetValue() as INetworkDeltaSnapshot; value?.ReadSnapshot( entry.Slot, snapshot ); } foreach ( var kv in snapshot.Entries ) { var slot = kv.Slot; var serialized = kv.Value; if ( !_entries.TryGetValue( slot, out var entry ) ) continue; // The connection sending us this can't modify it! if ( !entry.HasControl( source ) ) continue; if ( entry.IsReliableType || entry.IsDeltaSnapshotType ) continue; var bs = ByteStream.CreateReader( serialized ); try { IsReadingChanges = true; ReadEntryFromStream( slot, entry, ref bs ); } catch ( Exception e ) { Log.Warning( e, $"Error when reading value {entry.DebugName} - {e.Message}" ); } finally { IsReadingChanges = false; // We're never dirty if we just had our value read. entry.IsDirty = false; bs.Dispose(); } } } /// /// Write all reliable variables to the provided . /// /// public void WriteAllReliable( ref ByteStream data ) { var container = ByteStream.Create( 32 ); var count = 0; foreach ( var entry in _reliableEntries ) { var bs = ByteStream.Create( 32 ); try { WriteEntryToStream( entry, ref bs ); container.Write( entry.Slot ); container.Write( bs.Length ); if ( bs.Length > 0 ) container.Write( bs ); count++; } catch ( Exception e ) { Log.Warning( e, $"Error when getting value {entry.DebugName} - {e.Message}" ); } finally { bs.Dispose(); } } data.Write( count ); if ( count > 0 ) { data.Write( container.Length ); data.Write( container ); } container.Dispose(); } /// /// Write all variables to the provided . /// /// public void WriteAll( ref ByteStream data ) { var container = ByteStream.Create( 32 ); var count = 0; foreach ( var (slot, entry) in _entries ) { var bs = ByteStream.Create( 32 ); try { WriteEntryToStream( entry, ref bs ); } catch ( Exception e ) { Log.Warning( e, $"Error when getting value {entry.DebugName} - {e.Message}" ); } finally { container.Write( slot ); container.Write( bs.Length ); if ( bs.Length > 0 ) container.Write( bs ); bs.Dispose(); count++; } } data.Write( count ); if ( count > 0 ) { data.Write( container.Length ); data.Write( container ); } container.Dispose(); } /// /// Write an entry to the specified . /// /// /// /// private void WriteEntryToStream( Entry entry, ref ByteStream bs, bool onlyWriteChanges = false ) { var value = entry.GetValue(); if ( entry.IsSerializerType ) { if ( value is INetworkSerializer custom ) { bs.Write( true ); if ( onlyWriteChanges ) custom.WriteChanged( ref bs ); else custom.WriteAll( ref bs ); } else { bs.Write( false ); } } else { Game.TypeLibrary.ToBytes( entry.GetValue(), ref bs ); } } /// /// Write any changes to the provided for entries that must be sent reliably. Calling this will clear the changes. /// /// public void WriteReliableChanged( ref ByteStream data ) { var container = ByteStream.Create( 2048 ); var count = 0; foreach ( var entry in _reliableEntries ) { if ( !entry.IsDirty || !entry.HasControl() ) continue; var bs = ByteStream.Create( 2048 ); try { WriteEntryToStream( entry, ref bs, true ); container.Write( entry.Slot ); container.Write( bs.Length ); if ( bs.Length > 0 ) container.Write( bs ); count++; } catch ( Exception e ) { Log.Warning( e, $"Error when getting value {entry.DebugName} - {e.Message}" ); } finally { entry.IsDirty = false; bs.Dispose(); } } data.Write( count ); if ( count > 0 ) { data.Write( container.Length ); data.Write( container ); } container.Dispose(); } /// /// Write any changes to the provided . Calling this will clear the changes. /// /// public void WriteChanged( ref ByteStream data ) { var container = ByteStream.Create( 32 ); var count = 0; foreach ( var (slot, entry) in _entries ) { if ( !entry.IsDirty || !entry.HasControl() ) continue; var bs = ByteStream.Create( 32 ); try { WriteEntryToStream( entry, ref bs, true ); container.Write( slot ); container.Write( bs.Length ); if ( bs.Length > 0 ) container.Write( bs ); count++; } catch ( Exception e ) { Log.Warning( e, $"Error when getting value {entry.DebugName} - {e.Message}" ); } finally { entry.IsDirty = false; bs.Dispose(); } } data.Write( count ); if ( count > 0 ) { data.Write( container.Length ); data.Write( container ); } container.Dispose(); } public delegate bool ReadFilter( int slot, Entry entry ); /// /// Read and apply any variables from the provided . /// public void Read( ref ByteStream reader, ReadFilter filter = null ) { var count = reader.Read(); if ( count <= 0 ) return; var containerCount = reader.Read(); if ( containerCount <= 0 ) return; var container = reader.ReadByteStream( containerCount ); for ( var i = 0; i < count; i++ ) { var slot = container.Read(); var length = container.Read(); if ( length <= 0 ) continue; var bs = container.ReadByteStream( length ); if ( !_entries.TryGetValue( slot, out var entry ) ) { // It might be valid that we don't have a variable in this slot (we haven't had a network refresh yet.) bs.Dispose(); continue; } if ( filter is not null && !filter.Invoke( slot, entry ) ) { // We aren't allowed to make changes to the value in this slot right now. bs.Dispose(); continue; } try { IsReadingChanges = true; ReadEntryFromStream( slot, entry, ref bs ); } catch ( Exception e ) { Log.Warning( e, $"Error when reading value {entry.DebugName} - {e.Message}" ); } finally { IsReadingChanges = false; // We're never dirty if we just had our value read. entry.IsDirty = false; bs.Dispose(); } } container.Dispose(); } private void ReadEntryFromStream( int slot, Entry entry, ref ByteStream bs ) { if ( entry.IsSerializerType ) { var isValid = bs.Read(); if ( isValid ) { var value = entry.GetValue(); if ( value is not INetworkSerializer custom ) { custom = Activator.CreateInstance( entry.TargetType ) as INetworkSerializer; SetValue( slot, custom ); } custom?.Read( ref bs ); } else { SetValue( slot, null ); } } else { var value = Game.TypeLibrary.FromBytes( ref bs ); SetValue( slot, value ); } } /// /// If any properties are "query" types, we'll copy the new values to ourselves /// and mark as changed, if changed. /// public void QueryValues( bool onlyReliableEntries = false ) { for ( var i = 0; i < _queryEntries.Count; i++ ) { var entry = _queryEntries[i]; if ( onlyReliableEntries && !entry.IsReliableType ) continue; if ( !entry.HasControl() ) continue; try { UpdateSlotHash( entry, entry.GetValue() ); } catch ( Exception e ) { Log.Warning( e, $"Error when getting value {entry.DebugName} - {e.Message}" ); } } } }