diff --git a/engine/Sandbox.Engine/Scene/Networking/Containers/NetDictionary.cs b/engine/Sandbox.Engine/Scene/Networking/Containers/NetDictionary.cs index 663fd8db..9c688e62 100644 --- a/engine/Sandbox.Engine/Scene/Networking/Containers/NetDictionary.cs +++ b/engine/Sandbox.Engine/Scene/Networking/Containers/NetDictionary.cs @@ -4,6 +4,18 @@ using System.Collections.Specialized; namespace Sandbox; +/// +/// Describes a change to a which is passed to +/// whenever its contents change. +/// +public struct NetDictionaryChangeEvent +{ + public NotifyCollectionChangedAction Type { get; set; } + public TKey Key { get; set; } + public TValue NewValue { get; set; } + public TValue OldValue { get; set; } +} + /// /// 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. @@ -36,34 +48,39 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe public TValue Value { get; set; } } - private readonly ObservableDictionary dictionary = new(); - private readonly List changes = new(); + /// + /// Get notified when the dictionary is changed. + /// + public Action> OnChanged; + + 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; + ICollection IDictionary.Values => (ICollection)_dictionary.Values; + ICollection IDictionary.Keys => (ICollection)_dictionary.Keys; - IEnumerable IReadOnlyDictionary.Values => dictionary.Values; - IEnumerable IReadOnlyDictionary.Keys => dictionary.Keys; + IEnumerable IReadOnlyDictionary.Values => _dictionary.Values; + IEnumerable IReadOnlyDictionary.Keys => _dictionary.Keys; /// /// /// - public ICollection Values => dictionary.Values; + public ICollection Values => _dictionary.Values; public NetDictionary() { - dictionary.CollectionChanged += OnCollectionChanged; + _dictionary.CollectionChanged += OnCollectionChanged; AddResetChange(); } public void Dispose() { - changes.Clear(); + _changes.Clear(); } /// @@ -71,7 +88,7 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe /// void ICollection.CopyTo( Array array, int index ) { - (dictionary as ICollection).CopyTo( array, index ); + (_dictionary as ICollection).CopyTo( array, index ); } /// @@ -106,7 +123,7 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe if ( !CanWriteChanges() ) return; - dictionary.Add( key, value ); + _dictionary.Add( key, value ); } /// @@ -117,7 +134,7 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe if ( !CanWriteChanges() ) return; - dictionary.Add( item ); + _dictionary.Add( item ); } /// @@ -128,7 +145,7 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe if ( !CanWriteChanges() ) return; - dictionary.Clear(); + _dictionary.Clear(); } /// @@ -136,7 +153,7 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe /// public bool ContainsKey( TKey key ) { - return dictionary.ContainsKey( key ); + return _dictionary.ContainsKey( key ); } /// @@ -144,7 +161,7 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe /// public bool Contains( KeyValuePair item ) { - return dictionary.Contains( item ); + return _dictionary.Contains( item ); } /// @@ -152,12 +169,12 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe /// public void CopyTo( KeyValuePair[] array, int arrayIndex ) { - dictionary.CopyTo( array, arrayIndex ); + _dictionary.CopyTo( array, arrayIndex ); } public bool Remove( KeyValuePair item ) { - return CanWriteChanges() && dictionary.Remove( item ); + return CanWriteChanges() && _dictionary.Remove( item ); } /// @@ -165,7 +182,7 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe /// public ICollection Keys { - get { return dictionary.Keys; } + get { return _dictionary.Keys; } } /// @@ -176,31 +193,31 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe if ( !CanWriteChanges() ) return false; - return dictionary.Remove( key ); + return _dictionary.Remove( key ); } /// /// /// - public bool TryGetValue( TKey key, out TValue value ) => dictionary.TryGetValue( key, out value ); + public bool TryGetValue( TKey key, out TValue value ) => _dictionary.TryGetValue( key, out value ); /// /// /// - public int Count => dictionary.Count; + public int Count => _dictionary.Count; public TValue this[TKey key] { get { - return dictionary[key]; + return _dictionary[key]; } set { if ( !CanWriteChanges() ) return; - dictionary[key] = value; + _dictionary[key] = value; } } @@ -215,7 +232,7 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe /// IDictionaryEnumerator IDictionary.GetEnumerator() { - return ((IDictionary)dictionary).GetEnumerator(); + return ((IDictionary)_dictionary).GetEnumerator(); } /// @@ -223,7 +240,7 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe /// public IEnumerator> GetEnumerator() { - return dictionary.GetEnumerator(); + return _dictionary.GetEnumerator(); } /// @@ -231,7 +248,7 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe /// IEnumerator IEnumerable.GetEnumerator() { - return ((IEnumerable)dictionary).GetEnumerator(); + return ((IEnumerable)_dictionary).GetEnumerator(); } private INetworkProxy Parent { get; set; } @@ -244,7 +261,7 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe /// /// Do we have any pending changes? /// - bool INetworkSerializer.HasChanges => changes.Count > 0; + bool INetworkSerializer.HasChanges => _changes.Count > 0; /// /// Write any changed items to a . @@ -255,9 +272,9 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe { // We are sending changes, not a full update. This flag indicates that. data.Write( false ); - data.Write( changes.Count ); + data.Write( _changes.Count ); - foreach ( var change in changes ) + foreach ( var change in _changes ) { data.Write( change.Type ); WriteValue( change.Key, ref data ); @@ -269,7 +286,7 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe Log.Warning( e, $"Error when writing NetDictionary changes - {e.Message}" ); } - changes.Clear(); + _changes.Clear(); } /// @@ -292,7 +309,7 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe } // Clear changes whenever we read data. We don't want to keep local changes. - changes.Clear(); + _changes.Clear(); } /// @@ -304,9 +321,9 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe { // We are sending a full update. This flag indicates that. data.Write( true ); - data.Write( dictionary.Count ); + data.Write( _dictionary.Count ); - foreach ( var (k, v) in dictionary ) + foreach ( var (k, v) in _dictionary ) { WriteValue( k, ref data ); WriteValue( v, ref data ); @@ -323,7 +340,7 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe /// private void ReadAll( ref ByteStream data ) { - dictionary.Clear(); + _dictionary.Clear(); var count = data.Read(); @@ -334,7 +351,7 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe if ( key is null ) continue; - dictionary[key] = value; + _dictionary[key] = value; } } @@ -353,7 +370,7 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe if ( type == NotifyCollectionChangedAction.Reset ) { - dictionary.Clear(); + _dictionary.Clear(); } else if ( key is null ) { @@ -361,21 +378,42 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe } else if ( type == NotifyCollectionChangedAction.Add ) { - dictionary.Add( key, value ); + _dictionary.Add( key, value ); } else if ( type == NotifyCollectionChangedAction.Remove ) { - dictionary.Remove( key ); + _dictionary.Remove( key ); } else if ( type == NotifyCollectionChangedAction.Replace ) { - dictionary[key] = value; + _dictionary[key] = value; } } } private void OnCollectionChanged( object sender, NotifyCollectionChangedEventArgs e ) { + var changeEvent = new NetDictionaryChangeEvent + { + Type = e.Action + }; + + if ( e.OldItems is not null && e.OldItems.Count > 0 ) + { + var (k, oldValue) = (KeyValuePair)e.OldItems[0]; + changeEvent.OldValue = oldValue; + changeEvent.Key = k; + } + + if ( e.NewItems is not null && e.NewItems.Count > 0 ) + { + var (k, newValue) = (KeyValuePair)e.NewItems[0]; + changeEvent.NewValue = newValue; + changeEvent.Key = k; + } + + OnChanged?.InvokeWithWarning( changeEvent ); + if ( !CanWriteChanges() ) return; @@ -383,13 +421,13 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe { var (k, v) = (KeyValuePair)e.NewItems[0]; var change = new Change { Key = k, Value = v, Type = e.Action }; - changes.Add( change ); + _changes.Add( change ); } else if ( e.Action == NotifyCollectionChangedAction.Remove ) { - var (k, v) = (KeyValuePair)e.NewItems[0]; + var (k, v) = (KeyValuePair)e.OldItems[0]; var change = new Change { Key = k, Type = e.Action }; - changes.Add( change ); + _changes.Add( change ); } else if ( e.Action == NotifyCollectionChangedAction.Reset ) { @@ -397,19 +435,19 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe } 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 ); + var (k, newValue) = (KeyValuePair)e.NewItems[0]; + var change = new Change { Key = k, Type = e.Action, Value = newValue }; + _changes.Add( change ); } } - private T ReadValue( ref ByteStream data ) + private static T ReadValue( ref ByteStream data ) { var value = Game.TypeLibrary.FromBytes( ref data ); return (T)value; } - private void WriteValue( object value, ref ByteStream data ) + private static void WriteValue( object value, ref ByteStream data ) { Game.TypeLibrary.ToBytes( value, ref data ); } @@ -419,16 +457,16 @@ public sealed class NetDictionary : INetworkSerializer, INetworkRe private void AddResetChange() { var change = new Change { Type = NotifyCollectionChangedAction.Reset }; - changes.Add( change ); + _changes.Add( change ); - foreach ( var (k, v) in dictionary ) + 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 ); + _changes.Add( change ); } } } diff --git a/engine/Sandbox.Engine/Scene/Networking/Containers/NetList.cs b/engine/Sandbox.Engine/Scene/Networking/Containers/NetList.cs index 7fec719b..43612336 100644 --- a/engine/Sandbox.Engine/Scene/Networking/Containers/NetList.cs +++ b/engine/Sandbox.Engine/Scene/Networking/Containers/NetList.cs @@ -4,6 +4,19 @@ using System.Collections.Specialized; namespace Sandbox; +/// +/// Describes a change to a which is passed to +/// whenever its contents change. +/// +public struct NetListChangeEvent +{ + public NotifyCollectionChangedAction Type { get; set; } + public int Index { get; set; } + public int MovedIndex { get; set; } + public T NewValue { get; set; } + public T OldValue { get; set; } +} + /// /// A networkable list for use with the and . Only changes will be /// networked instead of sending the whole list every time, so it's more efficient. @@ -37,18 +50,23 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP public T Value { get; set; } } - private readonly ObservableCollection list = new(); - private readonly List changes = new(); + /// + /// Get notified when the list has changed. + /// + public Action> OnChanged; + + private readonly ObservableCollection _list = new(); + private readonly List _changes = new(); public NetList() { - list.CollectionChanged += OnCollectionChanged; + _list.CollectionChanged += OnCollectionChanged; AddResetChange(); } public void Dispose() { - changes.Clear(); + _changes.Clear(); } bool ICollection.IsReadOnly => false; @@ -85,7 +103,7 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP /// void ICollection.CopyTo( Array array, int index ) { - (list as ICollection).CopyTo( array, index ); + (_list as ICollection).CopyTo( array, index ); } /// @@ -93,7 +111,7 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP /// bool IList.Contains( object value ) { - return list.Contains( (T)value ); + return _list.Contains( (T)value ); } /// @@ -101,7 +119,7 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP /// int IList.IndexOf( object value ) { - return list.IndexOf( (T)value ); + return _list.IndexOf( (T)value ); } /// @@ -128,7 +146,7 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP if ( !CanWriteChanges() ) return; - list.Clear(); + _list.Clear(); } /// @@ -136,7 +154,7 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP /// public bool Contains( T item ) { - return list.Contains( item ); + return _list.Contains( item ); } /// @@ -144,7 +162,7 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP /// public void CopyTo( T[] array, int arrayIndex ) { - list.CopyTo( array, arrayIndex ); + _list.CopyTo( array, arrayIndex ); } /// @@ -155,7 +173,7 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP if ( !CanWriteChanges() ) return; - list.Add( value ); + _list.Add( value ); } /// @@ -168,7 +186,7 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP foreach ( var value in collection ) { - list.Add( value ); + _list.Add( value ); } } @@ -177,7 +195,7 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP /// public bool Remove( T value ) { - return CanWriteChanges() && list.Remove( value ); + return CanWriteChanges() && _list.Remove( value ); } /// @@ -185,7 +203,7 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP /// public int IndexOf( T item ) { - return list.IndexOf( item ); + return _list.IndexOf( item ); } /// @@ -196,7 +214,7 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP if ( !CanWriteChanges() ) return; - list.Insert( index, value ); + _list.Insert( index, value ); } /// @@ -207,26 +225,26 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP if ( !CanWriteChanges() ) return; - list.RemoveAt( index ); + _list.RemoveAt( index ); } /// /// /// - public int Count => list.Count; + public int Count => _list.Count; public T this[int key] { get { - return list[key]; + return _list[key]; } set { if ( !CanWriteChanges() ) return; - list[key] = value; + _list[key] = value; } } @@ -235,7 +253,7 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP /// public IEnumerator GetEnumerator() { - return list.GetEnumerator(); + return _list.GetEnumerator(); } /// @@ -243,7 +261,7 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP /// IEnumerator IEnumerable.GetEnumerator() { - return ((IEnumerable)list).GetEnumerator(); + return ((IEnumerable)_list).GetEnumerator(); } private INetworkProxy Parent { get; set; } @@ -256,7 +274,7 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP /// /// Do we have any pending changes? /// - bool INetworkSerializer.HasChanges => changes.Count > 0; + bool INetworkSerializer.HasChanges => _changes.Count > 0; /// /// Write any changed items to a . @@ -267,9 +285,9 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP { // We are sending changes, not a full update. This flag indicates that. data.Write( false ); - data.Write( changes.Count ); + data.Write( _changes.Count ); - foreach ( var change in changes ) + foreach ( var change in _changes ) { data.Write( change.Type ); data.Write( change.Index ); @@ -282,7 +300,7 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP Log.Warning( e, $"Error when writing NetList changes - {e.Message}" ); } - changes.Clear(); + _changes.Clear(); } /// @@ -305,7 +323,7 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP } // Clear changes whenever we read data. We don't want to keep local changes. - changes.Clear(); + _changes.Clear(); } /// @@ -317,9 +335,9 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP { // We are sending a full update. This flag indicates that. data.Write( true ); - data.Write( list.Count ); + data.Write( _list.Count ); - foreach ( var item in list ) + foreach ( var item in _list ) { WriteValue( item, ref data ); } @@ -336,14 +354,14 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP /// private void ReadAll( ref ByteStream data ) { - list.Clear(); + _list.Clear(); var count = data.Read(); for ( var i = 0; i < count; i++ ) { var value = ReadValue( ref data ); - list.Add( value ); + _list.Add( value ); } } @@ -363,45 +381,64 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP if ( type == NotifyCollectionChangedAction.Add ) { - if ( index >= 0 && index <= list.Count ) - list.Insert( index, value ); + if ( index >= 0 && index <= _list.Count ) + _list.Insert( index, value ); else - list.Add( value ); + _list.Add( value ); } else if ( type == NotifyCollectionChangedAction.Remove ) { - if ( index >= 0 && index < list.Count ) - list.RemoveAt( index ); + if ( index >= 0 && index < _list.Count ) + { + var element = _list.ElementAt( index ); + _list.RemoveAt( index ); + } } else if ( type == NotifyCollectionChangedAction.Reset ) { - list.Clear(); + _list.Clear(); } else if ( type == NotifyCollectionChangedAction.Replace ) { - list[index] = value; + if ( index >= 0 && index < _list.Count ) + { + var element = _list.ElementAt( index ); + } + + _list[index] = value; } else if ( type == NotifyCollectionChangedAction.Move ) { - list.Move( index, movedIndex ); + _list.Move( index, movedIndex ); } } } private void OnCollectionChanged( object sender, NotifyCollectionChangedEventArgs e ) { + var changeEvent = new NetListChangeEvent + { + Type = e.Action, + Index = e.Action == NotifyCollectionChangedAction.Add ? e.NewStartingIndex : e.OldStartingIndex, + MovedIndex = e.NewStartingIndex, + OldValue = (e.OldItems is not null && e.OldItems.Count > 0) ? (T)e.OldItems[0] : default, + NewValue = (e.NewItems is not null && e.NewItems.Count > 0) ? (T)e.NewItems[0] : default + }; + + OnChanged?.InvokeWithWarning( changeEvent ); + if ( !CanWriteChanges() ) return; if ( e.Action == NotifyCollectionChangedAction.Add ) { var change = new Change { Index = e.NewStartingIndex, Value = (T)e.NewItems[0], Type = e.Action }; - changes.Add( change ); + _changes.Add( change ); } else if ( e.Action == NotifyCollectionChangedAction.Remove ) { var change = new Change { Index = e.OldStartingIndex, Type = e.Action }; - changes.Add( change ); + _changes.Add( change ); } else if ( e.Action == NotifyCollectionChangedAction.Reset ) { @@ -410,22 +447,22 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP else if ( e.Action == NotifyCollectionChangedAction.Replace ) { var change = new Change { Index = e.OldStartingIndex, Type = e.Action, Value = (T)e.NewItems[0] }; - changes.Add( change ); + _changes.Add( change ); } else if ( e.Action == NotifyCollectionChangedAction.Move ) { var change = new Change { Index = e.OldStartingIndex, MovedIndex = e.NewStartingIndex, Type = e.Action }; - changes.Add( change ); + _changes.Add( change ); } } - private T ReadValue( ref ByteStream data ) + private static T ReadValue( ref ByteStream data ) { var value = Game.TypeLibrary.FromBytes( ref data ); return (T)value; } - private void WriteValue( T value, ref ByteStream data ) + private static void WriteValue( T value, ref ByteStream data ) { Game.TypeLibrary.ToBytes( value, ref data ); } @@ -435,13 +472,13 @@ public sealed class NetList : INetworkSerializer, INetworkReliable, INetworkP private void AddResetChange() { var change = new Change { Type = NotifyCollectionChangedAction.Reset }; - changes.Add( change ); + _changes.Add( change ); - for ( var i = 0; i < list.Count; i++ ) + for ( var i = 0; i < _list.Count; i++ ) { - var item = list[i]; - change = new() { Index = -1, Value = item, Type = NotifyCollectionChangedAction.Add }; - changes.Add( change ); + var item = _list[i]; + change = new Change { Index = -1, Value = item, Type = NotifyCollectionChangedAction.Add }; + _changes.Add( change ); } } } diff --git a/engine/Sandbox.System/Collections/ObservableDictionary.cs b/engine/Sandbox.System/Collections/ObservableDictionary.cs index 2810f913..3eeca0a8 100644 --- a/engine/Sandbox.System/Collections/ObservableDictionary.cs +++ b/engine/Sandbox.System/Collections/ObservableDictionary.cs @@ -76,8 +76,7 @@ public class ObservableDictionary : IDictionary, INo Dictionary.TryGetValue( key, out value ); var removed = Dictionary.Remove( key ); if ( removed ) - //OnCollectionChanged(NotifyCollectionChangedAction.Remove, new KeyValuePair(key, value)); - OnCollectionChanged(); + OnCollectionChanged( NotifyCollectionChangedAction.Remove, new KeyValuePair( key, value ) ); return removed; } diff --git a/engine/Sandbox.Test.Unit/Network/NetDictionary.cs b/engine/Sandbox.Test.Unit/Network/NetDictionary.cs index 48c6665d..2c5909d1 100644 --- a/engine/Sandbox.Test.Unit/Network/NetDictionary.cs +++ b/engine/Sandbox.Test.Unit/Network/NetDictionary.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Specialized; namespace Networking; @@ -54,6 +55,107 @@ public class NetDictionary Assert.AreEqual( 3, dictionary["c"] ); } + [TestMethod] + public void OnChangedIsInvokedWhenItemIsAdded() + { + var dict = new NetDictionary(); + + var callCount = 0; + NetDictionaryChangeEvent receivedEvent = default; + + dict.OnChanged = ev => + { + callCount++; + receivedEvent = ev; + }; + + dict.Add( "foo", 42 ); + + Assert.AreEqual( 1, callCount ); + Assert.AreEqual( NotifyCollectionChangedAction.Add, receivedEvent.Type ); + Assert.AreEqual( "foo", receivedEvent.Key ); + Assert.AreEqual( 42, receivedEvent.NewValue ); + } + + [TestMethod] + public void OnChangedIsInvokedWhenItemIsRemoved() + { + var dict = new NetDictionary(); + + dict.Add( "foo", 10 ); + dict.Add( "bar", 20 ); + + var callCount = 0; + NetDictionaryChangeEvent receivedEvent = default; + + dict.OnChanged = ev => + { + callCount++; + receivedEvent = ev; + }; + + dict.Remove( "foo" ); + + Assert.AreEqual( 1, callCount ); + Assert.AreEqual( NotifyCollectionChangedAction.Remove, receivedEvent.Type ); + Assert.AreEqual( "foo", receivedEvent.Key ); + Assert.AreEqual( 10, receivedEvent.OldValue ); + Assert.IsFalse( dict.ContainsKey( "foo" ) ); + } + + [TestMethod] + public void OnChangedIsInvokedWhenDictionaryIsCleared() + { + var dict = new NetDictionary(); + + dict.Add( "foo", 1 ); + dict.Add( "bar", 2 ); + + var callCount = 0; + NetDictionaryChangeEvent receivedEvent = default; + + dict.OnChanged = ev => + { + callCount++; + receivedEvent = ev; + }; + + dict.Clear(); + + Assert.AreEqual( 1, callCount ); + Assert.AreEqual( NotifyCollectionChangedAction.Reset, receivedEvent.Type ); + Assert.AreEqual( 0, dict.Count ); + } + + [TestMethod] + public void ReplaceInvokesWithCorrectValues() + { + var dict = new NetDictionary(); + + dict.Add( "foo", 10 ); + + var callCount = 0; + NetDictionaryChangeEvent receivedEvent = default; + + dict.OnChanged = ev => + { + callCount++; + receivedEvent = ev; + }; + + // This should represent a Replace: old 10 -> new 99 + dict["foo"] = 99; + + Assert.AreEqual( 1, callCount ); + + Assert.AreEqual( NotifyCollectionChangedAction.Replace, receivedEvent.Type ); + Assert.AreEqual( "foo", receivedEvent.Key ); + Assert.AreEqual( 10, receivedEvent.OldValue ); + Assert.AreEqual( 99, receivedEvent.NewValue ); + + Assert.AreEqual( 99, dict["foo"] ); + } + [TestMethod] public void ValidAccess() { diff --git a/engine/Sandbox.Test.Unit/Network/NetList.cs b/engine/Sandbox.Test.Unit/Network/NetList.cs index 61fb78ec..7195bdfa 100644 --- a/engine/Sandbox.Test.Unit/Network/NetList.cs +++ b/engine/Sandbox.Test.Unit/Network/NetList.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Specialized; namespace Networking; @@ -40,6 +41,96 @@ public class NetList Assert.AreEqual( 3, list[2] ); } + [TestMethod] + public void OnChangedIsInvokedWhenItemIsAdded() + { + var list = new NetList(); + + var callCount = 0; + NetListChangeEvent receivedEvent = default; + + list.OnChanged = change => + { + callCount++; + receivedEvent = change; + }; + + list.Add( 42 ); + + Assert.AreEqual( 1, callCount ); + Assert.AreEqual( NotifyCollectionChangedAction.Add, receivedEvent.Type ); + Assert.AreEqual( 0, receivedEvent.Index ); + Assert.AreEqual( 42, receivedEvent.NewValue ); + } + + [TestMethod] + public void OnChangedIsInvokedWhenItemIsRemoved() + { + var list = new NetList(); + + list.Add( 10 ); + list.Add( 20 ); + + var callCount = 0; + NetListChangeEvent receivedEvent = default; + + list.OnChanged = change => + { + callCount++; + receivedEvent = change; + }; + + list.Remove( 10 ); + + Assert.AreEqual( 1, callCount ); + Assert.AreEqual( NotifyCollectionChangedAction.Remove, receivedEvent.Type ); + Assert.AreEqual( 0, receivedEvent.Index ); // 10 was at index 0 + Assert.AreEqual( 10, receivedEvent.OldValue ); // removed value + } + + [TestMethod] + public void OnChangedIsInvokedWhenListIsCleared() + { + var list = new NetList(); + + list.Add( 1 ); + list.Add( 2 ); + + var callCount = 0; + NetListChangeEvent receivedEvent = default; + + list.OnChanged = change => + { + callCount++; + receivedEvent = change; + }; + + list.Clear(); + + Assert.AreEqual( 1, callCount ); + Assert.AreEqual( NotifyCollectionChangedAction.Reset, receivedEvent.Type ); + Assert.AreEqual( 0, list.Count ); + } + + [TestMethod] + public void OnChangedIsNotInvokedWhenNoChangeOccurs() + { + var list = new NetList(); + var callCount = 0; + + list.OnChanged = _ => + { + callCount++; + }; + + list.Add( 5 ); + + // Removing an item that does not exist should not trigger a change + list.Remove( 999 ); + + Assert.AreEqual( 1, callCount ); + } + [TestMethod] public void ValidAccess() {