using FishNet.Documenting; using FishNet.Managing.Logging; using FishNet.Object.Synchronizing.Internal; using FishNet.Serializing; using FishNet.Serializing.Helping; using FishNet.Utility.Performance; using System; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; namespace FishNet.Object.Synchronizing { public class SyncHashSet : SyncBase, ISet { #region Types. /// /// Information needed to invoke a callback. /// private struct CachedOnChange { internal readonly SyncHashSetOperation Operation; internal readonly T Item; public CachedOnChange(SyncHashSetOperation operation, T item) { Operation = operation; Item = item; } } /// /// Information about how the collection has changed. /// private struct ChangeData { internal readonly SyncHashSetOperation Operation; internal readonly T Item; public ChangeData(SyncHashSetOperation operation, T item) { Operation = operation; Item = item; } } #endregion #region Public. /// /// Implementation from List. Not used. /// [APIExclude] public bool IsReadOnly => false; /// /// Delegate signature for when SyncList changes. /// /// Type of change. /// Item which was modified. /// True if callback is occuring on the server. [APIExclude] public delegate void SyncHashSetChanged(SyncHashSetOperation op, T item, bool asServer); /// /// Called when the SyncList changes. /// public event SyncHashSetChanged OnChange; /// /// Collection of objects. /// public readonly ISet Collection; /// /// Copy of objects on client portion when acting as a host. /// public readonly ISet ClientHostCollection = new HashSet(); /// /// Number of objects in the collection. /// public int Count => Collection.Count; #endregion #region Private. /// /// ListCache for comparing. /// private ListCache _listCache; /// /// Values upon initialization. /// private ISet _initialValues = new HashSet(); /// /// Comparer to see if entries change when calling public methods. /// private readonly IEqualityComparer _comparer; /// /// Changed data which will be sent next tick. /// private readonly List _changed = new List(); /// /// Server OnChange events waiting for start callbacks. /// private readonly List _serverOnChanges = new List(); /// /// Client OnChange events waiting for start callbacks. /// private readonly List _clientOnChanges = new List(); /// /// True if values have changed since initialization. /// The only reasonable way to reset this during a Reset call is by duplicating the original list and setting all values to it on reset. /// private bool _valuesChanged; /// /// True to send all values in the next WriteDelta. /// private bool _sendAll; #endregion [APIExclude] public SyncHashSet() : this(new HashSet(), EqualityComparer.Default) { } [APIExclude] public SyncHashSet(IEqualityComparer comparer) : this(new HashSet(), (comparer == null) ? EqualityComparer.Default : comparer) { } [APIExclude] public SyncHashSet(ISet collection, IEqualityComparer comparer = null) { this._comparer = (comparer == null) ? EqualityComparer.Default : comparer; this.Collection = collection; //Add each in collection to clienthostcollection. foreach (T item in collection) ClientHostCollection.Add(item); } /// /// Called when the SyncType has been registered, but not yet initialized over the network. /// protected override void Registered() { base.Registered(); foreach (T item in Collection) _initialValues.Add(item); } /// /// Gets the collection being used within this SyncList. /// /// public HashSet GetCollection(bool asServer) { bool asClientAndHost = (!asServer && base.NetworkManager.IsServer); ISet collection = (asClientAndHost) ? ClientHostCollection : Collection; return (collection as HashSet); } /// /// Adds an operation and invokes locally. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddOperation(SyncHashSetOperation operation, T item) { if (!base.IsRegistered) return; bool asServerInvoke = (!base.IsNetworkInitialized || base.NetworkBehaviour.IsServer); if (asServerInvoke) { _valuesChanged = true; if (base.Dirty()) { ChangeData change = new ChangeData(operation, item); _changed.Add(change); } } InvokeOnChange(operation, item, asServerInvoke); } /// /// Called after OnStartXXXX has occurred. /// /// True if OnStartServer was called, false if OnStartClient. public override void OnStartCallback(bool asServer) { base.OnStartCallback(asServer); List collection = (asServer) ? _serverOnChanges : _clientOnChanges; if (OnChange != null) { foreach (CachedOnChange item in collection) OnChange.Invoke(item.Operation, item.Item, asServer); } collection.Clear(); } /// /// Writes all changed values. /// /// ///True to set the next time data may sync. public override void WriteDelta(PooledWriter writer, bool resetSyncTick = true) { //If sending all then clear changed and write full. if (_sendAll) { _sendAll = false; _changed.Clear(); WriteFull(writer); } else { base.WriteDelta(writer, resetSyncTick); //False for not full write. writer.WriteBoolean(false); writer.WriteInt32(_changed.Count); for (int i = 0; i < _changed.Count; i++) { ChangeData change = _changed[i]; writer.WriteByte((byte)change.Operation); //Clear does not need to write anymore data so it is not included in checks. if (change.Operation == SyncHashSetOperation.Add || change.Operation == SyncHashSetOperation.Remove || change.Operation == SyncHashSetOperation.Update) { writer.Write(change.Item); } } _changed.Clear(); } } /// /// Writes all values if not initial values. /// /// public override void WriteFull(PooledWriter writer) { if (!_valuesChanged) return; base.WriteHeader(writer, false); //True for full write. writer.WriteBoolean(true); int count = Collection.Count; writer.WriteInt32(count); foreach (T item in Collection) { writer.WriteByte((byte)SyncHashSetOperation.Add); writer.Write(item); } } /// /// Reads and sets the current values for server or client. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] [APIExclude] public override void Read(PooledReader reader, bool asServer) { /* When !asServer don't make changes if server is running. * This is because changes would have already been made on * the server side and doing so again would result in duplicates * and potentially overwrite data not yet sent. */ bool asClientAndHost = (!asServer && base.NetworkManager.IsServer); ISet collection = (asClientAndHost) ? ClientHostCollection : Collection; //Clear collection since it's a full write. bool fullWrite = reader.ReadBoolean(); if (fullWrite) collection.Clear(); int changes = reader.ReadInt32(); for (int i = 0; i < changes; i++) { SyncHashSetOperation operation = (SyncHashSetOperation)reader.ReadByte(); T next = default; //Add. if (operation == SyncHashSetOperation.Add) { next = reader.Read(); collection.Add(next); } //Clear. else if (operation == SyncHashSetOperation.Clear) { collection.Clear(); } //Remove. else if (operation == SyncHashSetOperation.Remove) { next = reader.Read(); collection.Remove(next); } //Updated. else if (operation == SyncHashSetOperation.Update) { next = reader.Read(); collection.Remove(next); collection.Add(next); } InvokeOnChange(operation, next, false); } //If changes were made invoke complete after all have been read. if (changes > 0) InvokeOnChange(SyncHashSetOperation.Complete, default, false); } /// /// Invokes OnChanged callback. /// private void InvokeOnChange(SyncHashSetOperation operation, T item, bool asServer) { if (asServer) { if (base.NetworkBehaviour.OnStartServerCalled) OnChange?.Invoke(operation, item, asServer); else _serverOnChanges.Add(new CachedOnChange(operation, item)); } else { if (base.NetworkBehaviour.OnStartClientCalled) OnChange?.Invoke(operation, item, asServer); else _clientOnChanges.Add(new CachedOnChange(operation, item)); } } /// /// Resets to initialized values. /// public override void Reset() { base.Reset(); _sendAll = false; _changed.Clear(); Collection.Clear(); ClientHostCollection.Clear(); foreach (T item in _initialValues) { Collection.Add(item); ClientHostCollection.Add(item); } } /// /// Adds value. /// /// public bool Add(T item) { return Add(item, true); } private bool Add(T item, bool asServer) { if (!base.CanNetworkSetValues(true)) return false; bool result = Collection.Add(item); //Only process if remove was successful. if (result && asServer) { if (base.NetworkManager == null) ClientHostCollection.Add(item); AddOperation(SyncHashSetOperation.Add, item); } return result; } /// /// Adds a range of values. /// /// public void AddRange(IEnumerable range) { foreach (T entry in range) Add(entry, true); } /// /// Clears all values. /// public void Clear() { Clear(true); } private void Clear(bool asServer) { if (!base.CanNetworkSetValues(true)) return; Collection.Clear(); if (asServer) { if (base.NetworkManager == null) ClientHostCollection.Clear(); AddOperation(SyncHashSetOperation.Clear, default); } } /// /// Returns if value exist. /// /// /// public bool Contains(T item) { return Collection.Contains(item); } /// /// Removes a value. /// /// /// public bool Remove(T item) { return Remove(item, true); } private bool Remove(T item, bool asServer) { if (!base.CanNetworkSetValues(true)) return false; bool result = Collection.Remove(item); //Only process if remove was successful. if (result && asServer) { if (base.NetworkManager == null) ClientHostCollection.Remove(item); AddOperation(SyncHashSetOperation.Remove, item); } return result; } /// /// Dirties the entire collection forcing a full send. /// public void DirtyAll() { if (!base.IsRegistered) return; if (!base.CanNetworkSetValues(true)) return; if (base.Dirty()) _sendAll = true; } /// /// Looks up obj in Collection and if found marks it's index as dirty. /// This operation can be very expensive, will cause allocations, and may fail if your value cannot be compared. /// /// Object to lookup. public void Dirty(T obj) { if (!base.IsRegistered) return; if (!base.CanNetworkSetValues(true)) return; foreach (T item in Collection) { if (item.Equals(obj)) { AddOperation(SyncHashSetOperation.Update, obj); return; } } //Not found. base.NetworkManager.LogError($"Could not find object within SyncHashSet, dirty will not be set."); } /// /// Returns Enumerator for collection. /// /// public IEnumerator GetEnumerator() => Collection.GetEnumerator(); [APIExclude] IEnumerator IEnumerable.GetEnumerator() => Collection.GetEnumerator(); [APIExclude] IEnumerator IEnumerable.GetEnumerator() => Collection.GetEnumerator(); public void ExceptWith(IEnumerable other) { //Again, removing from self is a clear. if (other == Collection) { Clear(); } else { foreach (T item in other) Remove(item); } } public void IntersectWith(IEnumerable other) { ISet set; if (other is ISet setA) set = setA; else set = new HashSet(other); IntersectWith(set); } private void IntersectWith(ISet other) { Intersect(Collection); if (base.NetworkManager == null) Intersect(ClientHostCollection); void Intersect(ISet collection) { if (_listCache == null) _listCache = new ListCache(); else _listCache.Reset(); _listCache.AddValues(collection); int count = _listCache.Written; for (int i = 0; i < count; i++) { T entry = _listCache.Collection[i]; if (!other.Contains(entry)) Remove(entry); } } } public bool IsProperSubsetOf(IEnumerable other) { return Collection.IsProperSubsetOf(other); } public bool IsProperSupersetOf(IEnumerable other) { return Collection.IsProperSupersetOf(other); } public bool IsSubsetOf(IEnumerable other) { return Collection.IsSubsetOf(other); } public bool IsSupersetOf(IEnumerable other) { return Collection.IsSupersetOf(other); } public bool Overlaps(IEnumerable other) { bool result = Collection.Overlaps(other); return result; } public bool SetEquals(IEnumerable other) { return Collection.SetEquals(other); } public void SymmetricExceptWith(IEnumerable other) { //If calling except on self then that is the same as a clear. if (other == Collection) { Clear(); } else { foreach (T item in other) Remove(item); } } public void UnionWith(IEnumerable other) { if (other == Collection) return; foreach (T item in other) Add(item); } /// /// Adds an item. /// /// void ICollection.Add(T item) { Add(item, true); } /// /// Copies values to an array. /// /// /// public void CopyTo(T[] array, int index) { Collection.CopyTo(array, index); if (base.NetworkManager == null) ClientHostCollection.CopyTo(array, index); } } }