Last active
May 5, 2025 17:08
-
-
Save luttje/f54109d9753293cd1b059c603fada5df to your computer and use it in GitHub Desktop.
A copy of the SyncDictionary in Mirror Networking (Unity), except this will update a synced instance when an INotifyPropertyChanged event is raised
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Mirror; | |
using System; | |
using System.ComponentModel; | |
using UnityEngine; | |
public class CastableSkill : INotifyPropertyChanged | |
{ | |
public SkillData skillData; | |
public double nextCastTime; | |
public uint currentPoints; | |
public byte slotId; | |
public event PropertyChangedEventHandler PropertyChanged; | |
internal bool IsOnCooldown() | |
{ | |
return NetworkTime.time < nextCastTime; | |
} | |
internal float GetRemainingCooldown() | |
{ | |
var remainingCooldown = nextCastTime - NetworkTime.time; | |
return (float)(remainingCooldown > 0 ? remainingCooldown : 0f); | |
} | |
internal void StartCooldown() | |
{ | |
nextCastTime = NetworkTime.time + skillData.cooldownTime; | |
OnPropertyChanged(nameof(nextCastTime)); | |
} | |
private void OnPropertyChanged(string propertyName) | |
{ | |
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Mirror; | |
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.ComponentModel; | |
/// <summary> | |
/// This is a copy and modification of SyncIDictionary in Mirror. It adds updating instances of items. | |
/// </summary> | |
/// <typeparam name="TKey"></typeparam> | |
/// <typeparam name="TValue"></typeparam> | |
public class ExtendedSyncIDictionary<TKey, TValue> : SyncObject, IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue> where TValue : INotifyPropertyChanged | |
{ | |
/// <summary>This is called after the item is added with TKey</summary> | |
public Action<TKey> OnAdd; | |
/// <summary>This is called after the item is changed with TKey.</summary> | |
public Action<TKey, TValue, TValue> OnSet; | |
/// <summary>This is called after the item's properties are changed with TKey.</summary> | |
public Action<TKey, TValue> OnUpdate; | |
/// <summary>This is called after the item is removed with TKey.</summary> | |
public Action<TKey, TValue, TValue> OnRemove; | |
/// <summary>This is called before the data is cleared</summary> | |
public Action OnClear; | |
public enum Operation : byte | |
{ | |
OP_ADD, | |
OP_SET, | |
OP_UPDATE, | |
OP_REMOVE, | |
OP_CLEAR | |
} | |
/// <summary> | |
/// This is called for all changes to the Dictionary. | |
/// <para>For OP_ADD, TValue is the NEW value of the entry.</para> | |
/// <para>For OP_SET and OP_REMOVE, TValue is the OLD value of the entry.</para> | |
/// <para>For OP_CLEAR, both TKey and TValue are default.</para> | |
/// </summary> | |
public Action<Operation, TKey, TValue> OnChange; | |
protected readonly IDictionary<TKey, TValue> objects; | |
public ExtendedSyncIDictionary(IDictionary<TKey, TValue> objects) | |
{ | |
this.objects = objects; | |
AttachListeners(this.objects); | |
} | |
public int Count => objects.Count; | |
public bool IsReadOnly => !IsWritable(); | |
struct Change | |
{ | |
internal Operation operation; | |
internal TKey key; | |
internal TValue item; | |
} | |
// list of changes. | |
// -> insert/delete/clear is only ONE change | |
// -> changing the same slot 10x causes 10 changes. | |
// -> note that this grows until next sync(!) | |
// TODO Dictionary<key, change> to avoid ever growing changes / redundant changes! | |
readonly List<Change> changes = new(); | |
// how many changes we need to ignore | |
// this is needed because when we initialize the list, | |
// we might later receive changes that have already been applied | |
// so we need to skip them | |
int changesAhead; | |
public ICollection<TKey> Keys => objects.Keys; | |
public ICollection<TValue> Values => objects.Values; | |
IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => objects.Keys; | |
IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => objects.Values; | |
public override void OnSerializeAll(NetworkWriter writer) | |
{ | |
// if init, write the full list content | |
writer.WriteUInt((uint)objects.Count); | |
foreach (KeyValuePair<TKey, TValue> syncItem in objects) | |
{ | |
writer.Write(syncItem.Key); | |
writer.Write(syncItem.Value); | |
} | |
// all changes have been applied already | |
// thus the client will need to skip all the pending changes | |
// or they would be applied again. | |
// So we write how many changes are pending | |
writer.WriteUInt((uint)changes.Count); | |
} | |
public override void OnSerializeDelta(NetworkWriter writer) | |
{ | |
// write all the queued up changes | |
writer.WriteUInt((uint)changes.Count); | |
for (int i = 0; i < changes.Count; i++) | |
{ | |
Change change = changes[i]; | |
writer.WriteByte((byte)change.operation); | |
switch (change.operation) | |
{ | |
case Operation.OP_ADD: | |
case Operation.OP_SET: | |
case Operation.OP_UPDATE: | |
writer.Write(change.key); | |
writer.Write(change.item); | |
break; | |
case Operation.OP_REMOVE: | |
writer.Write(change.key); | |
break; | |
case Operation.OP_CLEAR: | |
break; | |
} | |
} | |
} | |
public override void OnDeserializeAll(NetworkReader reader) | |
{ | |
// if init, write the full list content | |
int count = (int)reader.ReadUInt(); | |
objects.Clear(); | |
changes.Clear(); | |
for (int i = 0; i < count; i++) | |
{ | |
TKey key = reader.Read<TKey>(); | |
TValue obj = reader.Read<TValue>(); | |
objects.Add(key, obj); | |
} | |
// We will need to skip all these changes | |
// the next time the list is synchronized | |
// because they have already been applied | |
changesAhead = (int)reader.ReadUInt(); | |
} | |
public override void OnDeserializeDelta(NetworkReader reader) | |
{ | |
int changesCount = (int)reader.ReadUInt(); | |
for (int i = 0; i < changesCount; i++) | |
{ | |
Operation operation = (Operation)reader.ReadByte(); | |
// apply the operation only if it is a new change | |
// that we have not applied yet | |
bool apply = changesAhead == 0; | |
TKey key = default; | |
TValue item = default; | |
switch (operation) | |
{ | |
case Operation.OP_ADD: | |
case Operation.OP_SET: | |
case Operation.OP_UPDATE: | |
key = reader.Read<TKey>(); | |
item = reader.Read<TValue>(); | |
if (apply) | |
{ | |
// add dirty + changes. | |
// ClientToServer needs to set dirty in server OnDeserialize. | |
// no access check: server OnDeserialize can always | |
// write, even for ClientToServer (for broadcasting). | |
if (objects.TryGetValue(key, out TValue oldItem)) | |
{ | |
if (operation == Operation.OP_SET) | |
{ | |
objects[key] = item; // assign after TryGetValue | |
AddOperation(Operation.OP_SET, key, item, oldItem, false); | |
} | |
else if (operation == Operation.OP_UPDATE) | |
{ | |
// Change the properties and fields of the item so the reference | |
// stays the same. | |
var itemType = typeof(TValue); | |
var properties = itemType.GetProperties(); | |
foreach (var property in properties) | |
{ | |
if (property.CanWrite) | |
{ | |
var newValue = property.GetValue(item); | |
property.SetValue(oldItem, newValue); | |
} | |
} | |
var fields = itemType.GetFields(); | |
foreach (var field in fields) | |
{ | |
var newValue = field.GetValue(item); | |
field.SetValue(oldItem, newValue); | |
} | |
AddOperation(Operation.OP_UPDATE, key, oldItem, oldItem, false); | |
} | |
} | |
else | |
{ | |
objects[key] = item; // assign after TryGetValue | |
AddOperation(Operation.OP_ADD, key, item, default, false); | |
} | |
} | |
break; | |
case Operation.OP_CLEAR: | |
if (apply) | |
{ | |
// add dirty + changes. | |
// ClientToServer needs to set dirty in server OnDeserialize. | |
// no access check: server OnDeserialize can always | |
// write, even for ClientToServer (for broadcasting). | |
AddOperation(Operation.OP_CLEAR, default, default, default, false); | |
// clear after invoking the callback so users can iterate the dictionary | |
// and take appropriate action on the items before they are wiped. | |
objects.Clear(); | |
} | |
break; | |
case Operation.OP_REMOVE: | |
key = reader.Read<TKey>(); | |
if (apply) | |
{ | |
if (objects.TryGetValue(key, out TValue oldItem)) | |
{ | |
// add dirty + changes. | |
// ClientToServer needs to set dirty in server OnDeserialize. | |
// no access check: server OnDeserialize can always | |
// write, even for ClientToServer (for broadcasting). | |
objects.Remove(key); | |
AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, false); | |
} | |
} | |
break; | |
} | |
if (!apply) | |
{ | |
// we just skipped this change | |
changesAhead--; | |
} | |
} | |
} | |
// throw away all the changes | |
// this should be called after a successful sync | |
public override void ClearChanges() => changes.Clear(); | |
public override void Reset() | |
{ | |
changes.Clear(); | |
changesAhead = 0; | |
objects.Clear(); | |
} | |
public TValue this[TKey i] | |
{ | |
get => objects[i]; | |
set | |
{ | |
if (ContainsKey(i)) | |
{ | |
TValue oldItem = objects[i]; | |
if (oldItem != null) | |
DetachListener(oldItem); | |
objects[i] = value; | |
AddOperation(Operation.OP_SET, i, value, oldItem, true); | |
} | |
else | |
{ | |
objects[i] = value; | |
AddOperation(Operation.OP_ADD, i, value, default, true); | |
} | |
AttachListener(value); | |
} | |
} | |
protected void AttachListener(TValue item) | |
{ | |
item.PropertyChanged += OnItemPropertyChanged; | |
} | |
protected void AttachListeners(IDictionary<TKey, TValue> items) | |
{ | |
foreach (var item in items.Values) | |
{ | |
AttachListener(item); | |
} | |
} | |
protected void DetachListener(TValue item) | |
{ | |
item.PropertyChanged -= OnItemPropertyChanged; | |
} | |
protected void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e) | |
{ | |
if (sender is not TValue item) | |
return; | |
foreach (var kvp in objects) | |
{ | |
if (kvp.Value.Equals(item)) | |
{ | |
// TODO: Only update the property that changed? | |
AddOperation(Operation.OP_UPDATE, kvp.Key, item, item, true); | |
break; | |
} | |
} | |
} | |
public bool TryGetValue(TKey key, out TValue value) => objects.TryGetValue(key, out value); | |
public bool ContainsKey(TKey key) => objects.ContainsKey(key); | |
public bool Contains(KeyValuePair<TKey, TValue> item) => TryGetValue(item.Key, out TValue val) && EqualityComparer<TValue>.Default.Equals(val, item.Value); | |
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) | |
{ | |
if (arrayIndex < 0 || arrayIndex > array.Length) | |
throw new ArgumentOutOfRangeException(nameof(arrayIndex), "Array Index Out of Range"); | |
if (array.Length - arrayIndex < Count) | |
throw new ArgumentException("The number of items in the SyncDictionary is greater than the available space from arrayIndex to the end of the destination array"); | |
int i = arrayIndex; | |
foreach (KeyValuePair<TKey, TValue> item in objects) | |
{ | |
array[i] = item; | |
i++; | |
} | |
} | |
public void Add(KeyValuePair<TKey, TValue> item) => Add(item.Key, item.Value); | |
public void Add(TKey key, TValue value) | |
{ | |
objects.Add(key, value); | |
AddOperation(Operation.OP_ADD, key, value, default, true); | |
AttachListener(value); | |
} | |
public bool Remove(TKey key) | |
{ | |
if (objects.TryGetValue(key, out TValue oldItem) && objects.Remove(key)) | |
{ | |
AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, true); | |
DetachListener(oldItem); | |
return true; | |
} | |
return false; | |
} | |
public bool Remove(KeyValuePair<TKey, TValue> item) | |
{ | |
bool result = objects.Remove(item.Key); | |
if (result) | |
{ | |
AddOperation(Operation.OP_REMOVE, item.Key, item.Value, item.Value, true); | |
DetachListener(item.Value); | |
} | |
return result; | |
} | |
public void Clear() | |
{ | |
AddOperation(Operation.OP_CLEAR, default, default, default, true); | |
// clear after invoking the callback so users can iterate the dictionary | |
// and take appropriate action on the items before they are wiped. | |
foreach (var item in objects.Values) | |
{ | |
DetachListener(item); | |
} | |
objects.Clear(); | |
} | |
void AddOperation(Operation op, TKey key, TValue item, TValue oldItem, bool checkAccess) | |
{ | |
if (checkAccess && IsReadOnly) | |
throw new InvalidOperationException("SyncDictionaries can only be modified by the owner."); | |
Change change = new() | |
{ | |
operation = op, | |
key = key, | |
item = item | |
}; | |
if (IsRecording()) | |
{ | |
changes.Add(change); | |
OnDirty?.Invoke(); | |
} | |
switch (op) | |
{ | |
case Operation.OP_ADD: | |
OnAdd?.Invoke(key); | |
OnChange?.Invoke(op, key, item); | |
break; | |
case Operation.OP_SET: | |
OnSet?.Invoke(key, oldItem, item); | |
OnChange?.Invoke(op, key, oldItem); | |
break; | |
case Operation.OP_UPDATE: | |
OnUpdate?.Invoke(key, oldItem); | |
OnChange?.Invoke(op, key, oldItem); | |
break; | |
case Operation.OP_REMOVE: | |
OnRemove?.Invoke(key, oldItem, item); | |
OnChange?.Invoke(op, key, oldItem); | |
break; | |
case Operation.OP_CLEAR: | |
OnClear?.Invoke(); | |
OnChange?.Invoke(op, default, default); | |
break; | |
} | |
} | |
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => objects.GetEnumerator(); | |
IEnumerator IEnumerable.GetEnumerator() => objects.GetEnumerator(); | |
} | |
public class ExtendedSyncDictionary<TKey, TValue> : ExtendedSyncIDictionary<TKey, TValue> where TValue : INotifyPropertyChanged | |
{ | |
public ExtendedSyncDictionary() : base(new Dictionary<TKey, TValue>()) { } | |
public ExtendedSyncDictionary(IEqualityComparer<TKey> eq) : base(new Dictionary<TKey, TValue>(eq)) { } | |
public ExtendedSyncDictionary(IDictionary<TKey, TValue> d) : base(new Dictionary<TKey, TValue>(d)) { } | |
public new Dictionary<TKey, TValue>.ValueCollection Values => ((Dictionary<TKey, TValue>)objects).Values; | |
public new Dictionary<TKey, TValue>.KeyCollection Keys => ((Dictionary<TKey, TValue>)objects).Keys; | |
public new Dictionary<TKey, TValue>.Enumerator GetEnumerator() => ((Dictionary<TKey, TValue>)objects).GetEnumerator(); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// | |
/// This example shows a variant of the ExtendedSyncDictionary that doesn't use | |
/// reflection and gives you more control over how you apply the updates to your | |
/// object. | |
/// | |
// CastableSkill.cs | |
using Mirror; | |
using System; | |
using UnityEngine; | |
public class CastableSkill : ISyncObjectUpdates<CastableSkill> | |
{ | |
public SkillData skillData; | |
public double nextCastTime; | |
public uint currentPoints; | |
public byte slotId; | |
public event SyncObjectChangedEventHandler SyncObjectChanged; | |
internal bool IsOnCooldown() | |
{ | |
return NetworkTime.time < nextCastTime; | |
} | |
internal float GetRemainingCooldown() | |
{ | |
var remainingCooldown = nextCastTime - NetworkTime.time; | |
return (float)(remainingCooldown > 0 ? remainingCooldown : 0f); | |
} | |
internal void StartCooldown() | |
{ | |
nextCastTime = NetworkTime.time + skillData.cooldownTime; | |
OnSyncObjectChanged(); | |
} | |
private void OnSyncObjectChanged() | |
{ | |
SyncObjectChanged?.Invoke(this, new SyncObjectChangedEventArgs()); | |
} | |
public void ApplyUpdate(CastableSkill newValuesToAssume) | |
{ | |
skillData = newValuesToAssume.skillData; | |
nextCastTime = newValuesToAssume.nextCastTime; | |
currentPoints = newValuesToAssume.currentPoints; | |
slotId = newValuesToAssume.slotId; | |
} | |
} | |
// ExtendedSyncDictionary.cs | |
using Mirror; | |
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
public delegate void SyncObjectChangedEventHandler(object sender, SyncObjectChangedEventArgs e); | |
public class SyncObjectChangedEventArgs : EventArgs | |
{ | |
public SyncObjectChangedEventArgs() { } | |
} | |
/// <summary> | |
/// Apply this interface to any object you want to sync with ExtendedSyncDictionary. | |
/// </summary> | |
/// <typeparam name="TValue"></typeparam> | |
public interface ISyncObjectUpdates<TValue> | |
{ | |
/// <summary> | |
/// Call this when the object changes. | |
/// </summary> | |
event SyncObjectChangedEventHandler SyncObjectChanged; | |
/// <summary> | |
/// This is called when a new update is received. Copy the values from | |
/// the new object to 'this' instance. | |
/// </summary> | |
/// <param name="newValuesToAssume"></param> | |
void ApplyUpdate(TValue newValuesToAssume); | |
} | |
/// <summary> | |
/// This is a copy and modification of SyncIDictionary in Mirror. It adds updating instances of items. | |
/// </summary> | |
/// <typeparam name="TKey"></typeparam> | |
/// <typeparam name="TValue"></typeparam> | |
public class ExtendedSyncIDictionary<TKey, TValue> : SyncObject, IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue> where TValue : ISyncObjectUpdates<TValue> | |
{ | |
/// <summary>This is called after the item is added with TKey</summary> | |
public Action<TKey> OnAdd; | |
/// <summary>This is called after the item is changed with TKey.</summary> | |
public Action<TKey, TValue, TValue> OnSet; | |
/// <summary>This is called after the item's properties are changed with TKey.</summary> | |
public Action<TKey, TValue> OnUpdate; | |
/// <summary>This is called after the item is removed with TKey.</summary> | |
public Action<TKey, TValue, TValue> OnRemove; | |
/// <summary>This is called before the data is cleared</summary> | |
public Action OnClear; | |
public enum Operation : byte | |
{ | |
OP_ADD, | |
OP_SET, | |
OP_UPDATE, | |
OP_REMOVE, | |
OP_CLEAR | |
} | |
/// <summary> | |
/// This is called for all changes to the Dictionary. | |
/// <para>For OP_ADD, TValue is the NEW value of the entry.</para> | |
/// <para>For OP_SET and OP_REMOVE, TValue is the OLD value of the entry.</para> | |
/// <para>For OP_CLEAR, both TKey and TValue are default.</para> | |
/// </summary> | |
public Action<Operation, TKey, TValue> OnChange; | |
protected readonly IDictionary<TKey, TValue> objects; | |
public ExtendedSyncIDictionary(IDictionary<TKey, TValue> objects) | |
{ | |
this.objects = objects; | |
AttachListeners(this.objects); | |
} | |
public int Count => objects.Count; | |
public bool IsReadOnly => !IsWritable(); | |
struct Change | |
{ | |
internal Operation operation; | |
internal TKey key; | |
internal TValue item; | |
} | |
// list of changes. | |
// -> insert/delete/clear is only ONE change | |
// -> changing the same slot 10x causes 10 changes. | |
// -> note that this grows until next sync(!) | |
// TODO Dictionary<key, change> to avoid ever growing changes / redundant changes! | |
readonly List<Change> changes = new(); | |
// how many changes we need to ignore | |
// this is needed because when we initialize the list, | |
// we might later receive changes that have already been applied | |
// so we need to skip them | |
int changesAhead; | |
public ICollection<TKey> Keys => objects.Keys; | |
public ICollection<TValue> Values => objects.Values; | |
IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => objects.Keys; | |
IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => objects.Values; | |
public override void OnSerializeAll(NetworkWriter writer) | |
{ | |
// if init, write the full list content | |
writer.WriteUInt((uint)objects.Count); | |
foreach (KeyValuePair<TKey, TValue> syncItem in objects) | |
{ | |
writer.Write(syncItem.Key); | |
writer.Write(syncItem.Value); | |
} | |
// all changes have been applied already | |
// thus the client will need to skip all the pending changes | |
// or they would be applied again. | |
// So we write how many changes are pending | |
writer.WriteUInt((uint)changes.Count); | |
} | |
public override void OnSerializeDelta(NetworkWriter writer) | |
{ | |
// write all the queued up changes | |
writer.WriteUInt((uint)changes.Count); | |
for (int i = 0; i < changes.Count; i++) | |
{ | |
Change change = changes[i]; | |
writer.WriteByte((byte)change.operation); | |
switch (change.operation) | |
{ | |
case Operation.OP_ADD: | |
case Operation.OP_SET: | |
case Operation.OP_UPDATE: | |
writer.Write(change.key); | |
writer.Write(change.item); | |
break; | |
case Operation.OP_REMOVE: | |
writer.Write(change.key); | |
break; | |
case Operation.OP_CLEAR: | |
break; | |
} | |
} | |
} | |
public override void OnDeserializeAll(NetworkReader reader) | |
{ | |
// if init, write the full list content | |
int count = (int)reader.ReadUInt(); | |
objects.Clear(); | |
changes.Clear(); | |
for (int i = 0; i < count; i++) | |
{ | |
TKey key = reader.Read<TKey>(); | |
TValue obj = reader.Read<TValue>(); | |
objects.Add(key, obj); | |
} | |
// We will need to skip all these changes | |
// the next time the list is synchronized | |
// because they have already been applied | |
changesAhead = (int)reader.ReadUInt(); | |
} | |
public override void OnDeserializeDelta(NetworkReader reader) | |
{ | |
int changesCount = (int)reader.ReadUInt(); | |
for (int i = 0; i < changesCount; i++) | |
{ | |
Operation operation = (Operation)reader.ReadByte(); | |
// apply the operation only if it is a new change | |
// that we have not applied yet | |
bool apply = changesAhead == 0; | |
TKey key = default; | |
TValue item = default; | |
switch (operation) | |
{ | |
case Operation.OP_ADD: | |
case Operation.OP_SET: | |
case Operation.OP_UPDATE: | |
key = reader.Read<TKey>(); | |
item = reader.Read<TValue>(); | |
if (apply) | |
{ | |
// add dirty + changes. | |
// ClientToServer needs to set dirty in server OnDeserialize. | |
// no access check: server OnDeserialize can always | |
// write, even for ClientToServer (for broadcasting). | |
if (objects.TryGetValue(key, out TValue oldItem)) | |
{ | |
if (operation == Operation.OP_SET) | |
{ | |
objects[key] = item; // assign after TryGetValue | |
AddOperation(Operation.OP_SET, key, item, oldItem, false); | |
} | |
else if (operation == Operation.OP_UPDATE) | |
{ | |
// Change the properties and fields of the item so the reference | |
// stays the same. | |
oldItem.ApplyUpdate(item); | |
AddOperation(Operation.OP_UPDATE, key, oldItem, oldItem, false); | |
} | |
} | |
else | |
{ | |
objects[key] = item; // assign after TryGetValue | |
AddOperation(Operation.OP_ADD, key, item, default, false); | |
} | |
} | |
break; | |
case Operation.OP_CLEAR: | |
if (apply) | |
{ | |
// add dirty + changes. | |
// ClientToServer needs to set dirty in server OnDeserialize. | |
// no access check: server OnDeserialize can always | |
// write, even for ClientToServer (for broadcasting). | |
AddOperation(Operation.OP_CLEAR, default, default, default, false); | |
// clear after invoking the callback so users can iterate the dictionary | |
// and take appropriate action on the items before they are wiped. | |
objects.Clear(); | |
} | |
break; | |
case Operation.OP_REMOVE: | |
key = reader.Read<TKey>(); | |
if (apply) | |
{ | |
if (objects.TryGetValue(key, out TValue oldItem)) | |
{ | |
// add dirty + changes. | |
// ClientToServer needs to set dirty in server OnDeserialize. | |
// no access check: server OnDeserialize can always | |
// write, even for ClientToServer (for broadcasting). | |
objects.Remove(key); | |
AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, false); | |
} | |
} | |
break; | |
} | |
if (!apply) | |
{ | |
// we just skipped this change | |
changesAhead--; | |
} | |
} | |
} | |
// throw away all the changes | |
// this should be called after a successful sync | |
public override void ClearChanges() => changes.Clear(); | |
public override void Reset() | |
{ | |
changes.Clear(); | |
changesAhead = 0; | |
objects.Clear(); | |
} | |
public TValue this[TKey i] | |
{ | |
get => objects[i]; | |
set | |
{ | |
if (ContainsKey(i)) | |
{ | |
TValue oldItem = objects[i]; | |
if (oldItem != null) | |
DetachListener(oldItem); | |
objects[i] = value; | |
AddOperation(Operation.OP_SET, i, value, oldItem, true); | |
} | |
else | |
{ | |
objects[i] = value; | |
AddOperation(Operation.OP_ADD, i, value, default, true); | |
} | |
AttachListener(value); | |
} | |
} | |
protected void AttachListener(TValue item) | |
{ | |
item.SyncObjectChanged += OnItemChanged; | |
} | |
protected void AttachListeners(IDictionary<TKey, TValue> items) | |
{ | |
foreach (var item in items.Values) | |
{ | |
AttachListener(item); | |
} | |
} | |
protected void DetachListener(TValue item) | |
{ | |
item.SyncObjectChanged -= OnItemChanged; | |
} | |
protected void OnItemChanged(object sender, SyncObjectChangedEventArgs e) | |
{ | |
if (sender is not TValue item) | |
return; | |
foreach (var kvp in objects) | |
{ | |
if (kvp.Value.Equals(item)) | |
{ | |
AddOperation(Operation.OP_UPDATE, kvp.Key, item, item, true); | |
break; | |
} | |
} | |
} | |
public bool TryGetValue(TKey key, out TValue value) => objects.TryGetValue(key, out value); | |
public bool ContainsKey(TKey key) => objects.ContainsKey(key); | |
public bool Contains(KeyValuePair<TKey, TValue> item) => TryGetValue(item.Key, out TValue val) && EqualityComparer<TValue>.Default.Equals(val, item.Value); | |
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) | |
{ | |
if (arrayIndex < 0 || arrayIndex > array.Length) | |
throw new ArgumentOutOfRangeException(nameof(arrayIndex), "Array Index Out of Range"); | |
if (array.Length - arrayIndex < Count) | |
throw new ArgumentException("The number of items in the SyncDictionary is greater than the available space from arrayIndex to the end of the destination array"); | |
int i = arrayIndex; | |
foreach (KeyValuePair<TKey, TValue> item in objects) | |
{ | |
array[i] = item; | |
i++; | |
} | |
} | |
public void Add(KeyValuePair<TKey, TValue> item) => Add(item.Key, item.Value); | |
public void Add(TKey key, TValue value) | |
{ | |
objects.Add(key, value); | |
AddOperation(Operation.OP_ADD, key, value, default, true); | |
AttachListener(value); | |
} | |
public bool Remove(TKey key) | |
{ | |
if (objects.TryGetValue(key, out TValue oldItem) && objects.Remove(key)) | |
{ | |
AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, true); | |
DetachListener(oldItem); | |
return true; | |
} | |
return false; | |
} | |
public bool Remove(KeyValuePair<TKey, TValue> item) | |
{ | |
bool result = objects.Remove(item.Key); | |
if (result) | |
{ | |
AddOperation(Operation.OP_REMOVE, item.Key, item.Value, item.Value, true); | |
DetachListener(item.Value); | |
} | |
return result; | |
} | |
public void Clear() | |
{ | |
AddOperation(Operation.OP_CLEAR, default, default, default, true); | |
// clear after invoking the callback so users can iterate the dictionary | |
// and take appropriate action on the items before they are wiped. | |
foreach (var item in objects.Values) | |
{ | |
DetachListener(item); | |
} | |
objects.Clear(); | |
} | |
void AddOperation(Operation op, TKey key, TValue item, TValue oldItem, bool checkAccess) | |
{ | |
if (checkAccess && IsReadOnly) | |
throw new InvalidOperationException("SyncDictionaries can only be modified by the owner."); | |
Change change = new() | |
{ | |
operation = op, | |
key = key, | |
item = item | |
}; | |
if (IsRecording()) | |
{ | |
changes.Add(change); | |
OnDirty?.Invoke(); | |
} | |
switch (op) | |
{ | |
case Operation.OP_ADD: | |
OnAdd?.Invoke(key); | |
OnChange?.Invoke(op, key, item); | |
break; | |
case Operation.OP_SET: | |
OnSet?.Invoke(key, oldItem, item); | |
OnChange?.Invoke(op, key, oldItem); | |
break; | |
case Operation.OP_UPDATE: | |
OnUpdate?.Invoke(key, oldItem); | |
OnChange?.Invoke(op, key, oldItem); | |
break; | |
case Operation.OP_REMOVE: | |
OnRemove?.Invoke(key, oldItem, item); | |
OnChange?.Invoke(op, key, oldItem); | |
break; | |
case Operation.OP_CLEAR: | |
OnClear?.Invoke(); | |
OnChange?.Invoke(op, default, default); | |
break; | |
} | |
} | |
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => objects.GetEnumerator(); | |
IEnumerator IEnumerable.GetEnumerator() => objects.GetEnumerator(); | |
} | |
public class ExtendedSyncDictionary<TKey, TValue> : ExtendedSyncIDictionary<TKey, TValue> where TValue : ISyncObjectUpdates<TValue> | |
{ | |
public ExtendedSyncDictionary() : base(new Dictionary<TKey, TValue>()) { } | |
public ExtendedSyncDictionary(IEqualityComparer<TKey> eq) : base(new Dictionary<TKey, TValue>(eq)) { } | |
public ExtendedSyncDictionary(IDictionary<TKey, TValue> d) : base(new Dictionary<TKey, TValue>(d)) { } | |
public new Dictionary<TKey, TValue>.ValueCollection Values => ((Dictionary<TKey, TValue>)objects).Values; | |
public new Dictionary<TKey, TValue>.KeyCollection Keys => ((Dictionary<TKey, TValue>)objects).Keys; | |
public new Dictionary<TKey, TValue>.Enumerator GetEnumerator() => ((Dictionary<TKey, TValue>)objects).GetEnumerator(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment