using System; using System.Collections; using System.Collections.Generic; namespace Mirror { public class SyncSet : SyncObject, ISet { public delegate void SyncSetChanged(Operation op, T item); protected readonly ISet objects; public int Count => objects.Count; public bool IsReadOnly { get; private set; } public event SyncSetChanged Callback; public enum Operation : byte { OP_ADD, OP_CLEAR, OP_REMOVE } struct Change { internal Operation operation; internal T item; } // list of changes. // -> insert/delete/clear is only ONE change // -> changing the same slot 10x caues 10 changes. // -> note that this grows until next sync(!) // TODO Dictionary to avoid ever growing changes / redundant changes! readonly List changes = new List(); // 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 SyncSet(ISet objects) { this.objects = objects; } public override void Reset() { IsReadOnly = false; changes.Clear(); changesAhead = 0; objects.Clear(); } // throw away all the changes // this should be called after a successful sync public override void ClearChanges() => changes.Clear(); void AddOperation(Operation op, T item) { if (IsReadOnly) { throw new InvalidOperationException("SyncSets can only be modified at the server"); } Change change = new Change { operation = op, item = item }; if (IsRecording()) { changes.Add(change); OnDirty?.Invoke(); } Callback?.Invoke(op, item); } void AddOperation(Operation op) => AddOperation(op, default); public override void OnSerializeAll(NetworkWriter writer) { // if init, write the full list content writer.WriteUInt((uint)objects.Count); foreach (T obj in objects) { writer.Write(obj); } // 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: writer.Write(change.item); break; case Operation.OP_CLEAR: break; case Operation.OP_REMOVE: writer.Write(change.item); break; } } } public override void OnDeserializeAll(NetworkReader reader) { // This list can now only be modified by synchronization IsReadOnly = true; // if init, write the full list content int count = (int)reader.ReadUInt(); objects.Clear(); changes.Clear(); for (int i = 0; i < count; i++) { T obj = reader.Read(); objects.Add(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) { // This list can now only be modified by synchronization IsReadOnly = true; 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; T item = default; switch (operation) { case Operation.OP_ADD: item = reader.Read(); if (apply) { objects.Add(item); } break; case Operation.OP_CLEAR: if (apply) { objects.Clear(); } break; case Operation.OP_REMOVE: item = reader.Read(); if (apply) { objects.Remove(item); } break; } if (apply) { Callback?.Invoke(operation, item); } // we just skipped this change else { changesAhead--; } } } public bool Add(T item) { if (objects.Add(item)) { AddOperation(Operation.OP_ADD, item); return true; } return false; } void ICollection.Add(T item) { if (objects.Add(item)) { AddOperation(Operation.OP_ADD, item); } } public void Clear() { objects.Clear(); AddOperation(Operation.OP_CLEAR); } public bool Contains(T item) => objects.Contains(item); public void CopyTo(T[] array, int index) => objects.CopyTo(array, index); public bool Remove(T item) { if (objects.Remove(item)) { AddOperation(Operation.OP_REMOVE, item); return true; } return false; } public IEnumerator GetEnumerator() => objects.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public void ExceptWith(IEnumerable other) { if (other == this) { Clear(); return; } // remove every element in other from this foreach (T element in other) { Remove(element); } } public void IntersectWith(IEnumerable other) { if (other is ISet otherSet) { IntersectWithSet(otherSet); } else { HashSet otherAsSet = new HashSet(other); IntersectWithSet(otherAsSet); } } void IntersectWithSet(ISet otherSet) { List elements = new List(objects); foreach (T element in elements) { if (!otherSet.Contains(element)) { Remove(element); } } } public bool IsProperSubsetOf(IEnumerable other) => objects.IsProperSubsetOf(other); public bool IsProperSupersetOf(IEnumerable other) => objects.IsProperSupersetOf(other); public bool IsSubsetOf(IEnumerable other) => objects.IsSubsetOf(other); public bool IsSupersetOf(IEnumerable other) => objects.IsSupersetOf(other); public bool Overlaps(IEnumerable other) => objects.Overlaps(other); public bool SetEquals(IEnumerable other) => objects.SetEquals(other); // custom implementation so we can do our own Clear/Add/Remove for delta public void SymmetricExceptWith(IEnumerable other) { if (other == this) { Clear(); } else { foreach (T element in other) { if (!Remove(element)) { Add(element); } } } } // custom implementation so we can do our own Clear/Add/Remove for delta public void UnionWith(IEnumerable other) { if (other != this) { foreach (T element in other) { Add(element); } } } } public class SyncHashSet : SyncSet { public SyncHashSet() : this(EqualityComparer.Default) {} public SyncHashSet(IEqualityComparer comparer) : base(new HashSet(comparer ?? EqualityComparer.Default)) {} // allocation free enumerator public new HashSet.Enumerator GetEnumerator() => ((HashSet)objects).GetEnumerator(); } public class SyncSortedSet : SyncSet { public SyncSortedSet() : this(Comparer.Default) {} public SyncSortedSet(IComparer comparer) : base(new SortedSet(comparer ?? Comparer.Default)) {} // allocation free enumerator public new SortedSet.Enumerator GetEnumerator() => ((SortedSet)objects).GetEnumerator(); } }