using Sandbox.Network; namespace Sandbox; /// /// A network table that can be used with [Sync]. It will be serialized /// fully for clients when they first need it, and then it will be sent as delta snapshots when individual /// entries change. /// internal class NetworkTable : INetworkDeltaSnapshot, INetworkProperty, INetworkSerializer { protected Dictionary Serialized { get; set; } = new(); protected Dictionary Table { get; set; } = new(); protected byte[] SerializedKeys { get; set; } protected HashSet Keys { get; set; } = new(); private bool CanWriteChanges() => !Parent?.IsProxy ?? true; private readonly Dictionary _subSlotHashes = new(); private HashSet KeysToRemove { get; set; } = []; protected int _parentSlot; protected int _keysHash; /// /// Read-only access for all entries in the network table. /// public IReadOnlyDictionary Entries => Table; /// /// Clear the network table. /// public void Clear() { if ( !CanWriteChanges() ) return; InternalClear(); } void InternalClear() { _subSlotHashes.Clear(); SerializedKeys = []; Table.Clear(); Serialized.Clear(); Keys.Clear(); OnCleared(); } /// /// Set the value of an entry in the specified slot. /// public void Set( int slot, T value ) { if ( EqualityComparer.Default.Equals( Get( slot ), value ) ) return; if ( !CanWriteChanges() ) return; Serialized[slot] = Game.TypeLibrary.ToBytes( value ); Table[slot] = value; OnValueChanged( slot, value ); if ( !Keys.Add( slot ) ) return; WriteSerializedKeys(); UpdateSubSlotHash( slot ); } /// /// Get the value of an entry in the specified slot. /// public T Get( int slot ) { return Table.GetValueOrDefault( slot ); } void WriteSerializedKeys() { var bs = ByteStream.Create( 1024 ); bs.Write( Keys.Count ); foreach ( var key in Keys ) { bs.Write( key ); } SerializedKeys = bs.ToArray(); } void UpdateSubSlotHash( int slot ) { if ( _parentSlot == 0 ) return; _subSlotHashes[slot] = $"{_parentSlot}.{slot}".FastHash(); } void ReadSerializedKeys( byte[] data ) { if ( data.Length == 0 ) { SerializedKeys = data; Keys.Clear(); return; } var reader = ByteStream.CreateReader( data ); var count = reader.Read(); Keys.Clear(); for ( var i = 0; i < count; i++ ) { Keys.Add( reader.Read() ); } SerializedKeys = data; } private INetworkProxy Parent { get; set; } void INetworkProperty.Init( int slot, INetworkProxy parent ) { Parent = parent; _parentSlot = slot; _keysHash = $"{slot}.keys".FastHash(); foreach ( var key in Keys ) { _subSlotHashes[key] = $"{slot}.{key}".FastHash(); } OnInit( slot ); } protected virtual void WriteSnapshot( int slot, LocalSnapshotState snapshot ) { foreach ( var entry in Serialized ) { var subSlot = _subSlotHashes.GetValueOrDefault( entry.Key ); if ( subSlot == 0 ) { subSlot = $"{_parentSlot}.{entry.Key}".FastHash(); _subSlotHashes[entry.Key] = subSlot; } snapshot.AddSerialized( subSlot, entry.Value ); } } void INetworkDeltaSnapshot.WriteSnapshotState( int slot, LocalSnapshotState snapshot ) { if ( SerializedKeys is not null ) { snapshot.AddSerialized( _keysHash, SerializedKeys ); } WriteSnapshot( slot, snapshot ); } protected virtual void ReadSnapshot( int slot, DeltaSnapshot snapshot ) { foreach ( var key in Keys ) { var subSlot = _subSlotHashes.GetValueOrDefault( key ); if ( subSlot == 0 ) { subSlot = $"{_parentSlot}.{key}".FastHash(); _subSlotHashes[key] = subSlot; } if ( !snapshot.TryGetValue( subSlot, out var value ) ) continue; Serialized[key] = snapshot.Lookup[subSlot].Value; Table[key] = value; } } void INetworkDeltaSnapshot.ReadSnapshot( int slot, DeltaSnapshot snapshot ) { if ( snapshot.Lookup.TryGetValue( _keysHash, out var keys ) ) { ReadSerializedKeys( keys.Value ); } ReadSnapshot( slot, snapshot ); PruneRemovedEntries(); } void PruneRemovedEntries() { foreach ( var entry in Table.Where( entry => !Keys.Contains( entry.Key ) ) ) { KeysToRemove.Add( entry.Key ); } if ( KeysToRemove.Count == 0 ) return; foreach ( var key in KeysToRemove ) { OnKeyRemoved( key ); _subSlotHashes.Remove( key ); Serialized.Remove( key ); Table.Remove( key ); } KeysToRemove.Clear(); } void INetworkSerializer.WriteChanged( ref ByteStream data ) { } void INetworkSerializer.WriteAll( ref ByteStream data ) { data.Write( Serialized.Count ); foreach ( var entry in Serialized ) { data.Write( entry.Key ); data.Write( entry.Value.Length ); data.Write( entry.Value ); } } void INetworkSerializer.Read( ref ByteStream data ) { var count = data.Read(); if ( count <= 0 ) { InternalClear(); return; } for ( var i = 0; i < count; i++ ) { var key = data.Read(); var byteCount = data.Read(); var bytes = new byte[byteCount]; data.Read( bytes, 0, byteCount ); var value = Game.TypeLibrary.FromBytes( bytes ); Serialized[key] = bytes; Table[key] = value; OnValueChanged( key, value ); if ( !Keys.Add( key ) ) continue; UpdateSubSlotHash( key ); } WriteSerializedKeys(); } /// /// Called when initialized from a parent network table. /// /// protected virtual void OnInit( int parentSlot ) { } /// /// Called whenever a key is removed from the table. /// protected virtual void OnKeyRemoved( int key ) { } /// /// Called whenever a value changes in a specific slot. /// protected virtual void OnValueChanged( int slot, T value ) { } /// /// Called when the network table is cleared. /// protected virtual void OnCleared() { } bool INetworkSerializer.HasChanges => false; }