using System.Collections; using System.Collections.ObjectModel; using System.Collections.Specialized; namespace Sandbox; /// /// A networkable dictionary for use with the and . Only changes will be /// networked instead of sending the whole dictionary every time, so it's more efficient. ///
/// /// Example usage: /// /// public class MyComponent : Component /// { /// [Sync] public NetDictionary<string,bool> MyBoolTable { get; set; } = new(); ///
/// public void SetBoolState( string key, bool state ) /// { /// if ( IsProxy ) return; /// MyBoolTable[key] = state; /// } /// } ///
///
///
public sealed class NetDictionary : INetworkSerializer, INetworkReliable, INetworkProperty, IDisposable, IDictionary, IDictionary, IReadOnlyDictionary { /// /// Represents a change in the dictionary. /// private struct Change { public NotifyCollectionChangedAction Type { get; set; } public TKey Key { get; set; } public TValue Value { get; set; } } private readonly ObservableDictionary dictionary = new(); private readonly List changes = new(); bool ICollection>.IsReadOnly => false; bool IDictionary.IsReadOnly => false; bool IDictionary.IsFixedSize => false; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; ICollection IDictionary.Values => (ICollection)dictionary.Values; ICollection IDictionary.Keys => (ICollection)dictionary.Keys; IEnumerable IReadOnlyDictionary.Values => dictionary.Values; IEnumerable IReadOnlyDictionary.Keys => dictionary.Keys; /// /// /// public ICollection Values => dictionary.Values; public NetDictionary() { dictionary.CollectionChanged += OnCollectionChanged; AddResetChange(); } public void Dispose() { changes.Clear(); } /// /// /// void ICollection.CopyTo( Array array, int index ) { (dictionary as ICollection).CopyTo( array, index ); } /// /// /// void IDictionary.Add( object key, object value ) { Add( (TKey)key, (TValue)value ); } /// /// /// bool IDictionary.Contains( object key ) { return ContainsKey( (TKey)key ); } /// /// /// void IDictionary.Remove( object key ) { Remove( (TKey)key ); } /// /// /// public void Add( TKey key, TValue value ) { if ( !CanWriteChanges() ) return; dictionary.Add( key, value ); } /// /// /// public void Add( KeyValuePair item ) { if ( !CanWriteChanges() ) return; dictionary.Add( item ); } /// /// /// public void Clear() { if ( !CanWriteChanges() ) return; dictionary.Clear(); } /// /// /// public bool ContainsKey( TKey key ) { return dictionary.ContainsKey( key ); } /// /// /// public bool Contains( KeyValuePair item ) { return dictionary.Contains( item ); } /// /// /// public void CopyTo( KeyValuePair[] array, int arrayIndex ) { dictionary.CopyTo( array, arrayIndex ); } public bool Remove( KeyValuePair item ) { return CanWriteChanges() && dictionary.Remove( item ); } /// /// /// public ICollection Keys { get { return dictionary.Keys; } } /// /// /// public bool Remove( TKey key ) { if ( !CanWriteChanges() ) return false; return dictionary.Remove( key ); } /// /// /// public bool TryGetValue( TKey key, out TValue value ) => dictionary.TryGetValue( key, out value ); /// /// /// public int Count => dictionary.Count; public TValue this[TKey key] { get { return dictionary[key]; } set { if ( !CanWriteChanges() ) return; dictionary[key] = value; } } object IDictionary.this[object key] { get => this[(TKey)key]; set => this[(TKey)key] = (TValue)value; } /// /// /// IDictionaryEnumerator IDictionary.GetEnumerator() { return ((IDictionary)dictionary).GetEnumerator(); } /// /// /// public IEnumerator> GetEnumerator() { return dictionary.GetEnumerator(); } /// /// /// IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)dictionary).GetEnumerator(); } private INetworkProxy Parent { get; set; } void INetworkProperty.Init( int slot, INetworkProxy parent ) { Parent = parent; } /// /// Do we have any pending changes? /// bool INetworkSerializer.HasChanges => changes.Count > 0; /// /// Write any changed items to a . /// void INetworkSerializer.WriteChanged( ref ByteStream data ) { try { // We are sending changes, not a full update. This flag indicates that. data.Write( false ); data.Write( changes.Count ); foreach ( var change in changes ) { data.Write( change.Type ); WriteValue( change.Key, ref data ); WriteValue( change.Value, ref data ); } } catch ( Exception e ) { Log.Warning( e, $"Error when writing NetDictionary changes - {e.Message}" ); } changes.Clear(); } /// /// Read a network update from a . /// void INetworkSerializer.Read( ref ByteStream data ) { try { var isFullUpdate = data.Read(); if ( isFullUpdate ) ReadAll( ref data ); else ReadChanged( ref data ); } catch ( Exception e ) { Log.Warning( e, $"Error when reading NetDictionary - {e.Message}" ); } // Clear changes whenever we read data. We don't want to keep local changes. changes.Clear(); } /// /// Write all items to a . /// void INetworkSerializer.WriteAll( ref ByteStream data ) { try { // We are sending a full update. This flag indicates that. data.Write( true ); data.Write( dictionary.Count ); foreach ( var (k, v) in dictionary ) { WriteValue( k, ref data ); WriteValue( v, ref data ); } } catch ( Exception e ) { Log.Warning( e, $"Error when writing NetDictionary - {e.Message}" ); } } /// /// Read all changes in the dictionary as if we're building it for the first time. /// private void ReadAll( ref ByteStream data ) { dictionary.Clear(); var count = data.Read(); for ( var i = 0; i < count; i++ ) { var key = ReadValue( ref data ); var value = ReadValue( ref data ); if ( key is null ) continue; dictionary[key] = value; } } /// /// Read any changed items from a . /// private void ReadChanged( ref ByteStream data ) { var count = data.Read(); for ( var i = 0; i < count; i++ ) { var type = data.Read(); var key = ReadValue( ref data ); var value = ReadValue( ref data ); if ( type == NotifyCollectionChangedAction.Reset ) { dictionary.Clear(); } else if ( key is null ) { continue; } else if ( type == NotifyCollectionChangedAction.Add ) { dictionary.Add( key, value ); } else if ( type == NotifyCollectionChangedAction.Remove ) { dictionary.Remove( key ); } else if ( type == NotifyCollectionChangedAction.Replace ) { dictionary[key] = value; } } } private void OnCollectionChanged( object sender, NotifyCollectionChangedEventArgs e ) { if ( !CanWriteChanges() ) return; if ( e.Action == NotifyCollectionChangedAction.Add ) { var (k, v) = (KeyValuePair)e.NewItems[0]; var change = new Change { Key = k, Value = v, Type = e.Action }; changes.Add( change ); } else if ( e.Action == NotifyCollectionChangedAction.Remove ) { var (k, v) = (KeyValuePair)e.NewItems[0]; var change = new Change { Key = k, Type = e.Action }; changes.Add( change ); } else if ( e.Action == NotifyCollectionChangedAction.Reset ) { AddResetChange(); } else if ( e.Action == NotifyCollectionChangedAction.Replace ) { var (k, v) = (KeyValuePair)e.NewItems[0]; var change = new Change { Key = k, Type = e.Action, Value = v }; changes.Add( change ); } } private T ReadValue( ref ByteStream data ) { var value = Game.TypeLibrary.FromBytes( ref data ); return (T)value; } private void WriteValue( object value, ref ByteStream data ) { Game.TypeLibrary.ToBytes( value, ref data ); } private bool CanWriteChanges() => !Parent?.IsProxy ?? true; private void AddResetChange() { var change = new Change { Type = NotifyCollectionChangedAction.Reset }; changes.Add( change ); foreach ( var (k, v) in dictionary ) { // If a key is no longer valid, don't send it as a change, it'll be a null key on read. if ( k is IValid valid && !valid.IsValid() ) continue; change = new Change { Key = k, Value = v, Type = NotifyCollectionChangedAction.Add }; changes.Add( change ); } } }