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