using FishNet.Documenting; using FishNet.Managing; using FishNet.Object.Synchronizing.Internal; using FishNet.Serializing; using GameKit.Dependencies.Utilities; using System.Collections; using System.Collections.Generic; namespace FishNet.Object.Synchronizing { [System.Serializable] 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; internal readonly int CollectionCountAfterChange; public ChangeData(SyncHashSetOperation operation, T item, int collectionCountAfterChange) { Operation = operation; Item = item; CollectionCountAfterChange = collectionCountAfterChange; } } #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 HashSet Collection; /// /// Number of objects in the collection. /// public int Count => Collection.Count; #endregion #region Private. /// /// ListCache for comparing. /// private static List _cache = new(); /// /// Values upon initialization. /// private HashSet _initialValues; /// /// Changed data which will be sent next tick. /// private List _changed; /// /// Server OnChange events waiting for start callbacks. /// private List _serverOnChanges; /// /// Client OnChange events waiting for start callbacks. /// private List _clientOnChanges; /// /// Comparer to see if entries change when calling public methods. /// // Not used right now. /// private readonly IEqualityComparer _comparer; /// /// 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 #region Constructors. public SyncHashSet(SyncTypeSettings settings = new()) : this(CollectionCaches.RetrieveHashSet(), EqualityComparer.Default, settings) { } public SyncHashSet(IEqualityComparer comparer, SyncTypeSettings settings = new()) : this(CollectionCaches.RetrieveHashSet(), comparer == null ? EqualityComparer.Default : comparer, settings) { } public SyncHashSet(HashSet collection, IEqualityComparer comparer = null, SyncTypeSettings settings = new()) : base(settings) { _comparer = comparer == null ? EqualityComparer.Default : comparer; Collection = collection == null ? CollectionCaches.RetrieveHashSet() : collection; _initialValues = CollectionCaches.RetrieveHashSet(); _changed = CollectionCaches.RetrieveList(); _serverOnChanges = CollectionCaches.RetrieveList(); _clientOnChanges = CollectionCaches.RetrieveList(); } #endregion #region Deconstructor. ~SyncHashSet() { CollectionCaches.StoreAndDefault(ref Collection); CollectionCaches.StoreAndDefault(ref _initialValues); CollectionCaches.StoreAndDefault(ref _changed); CollectionCaches.StoreAndDefault(ref _serverOnChanges); CollectionCaches.StoreAndDefault(ref _clientOnChanges); } #endregion /// /// Called when the SyncType has been registered, but not yet initialized over the network. /// protected override void Initialized() { base.Initialized(); // Initialize collections if needed. OdinInspector can cause them to become deinitialized. #if ODIN_INSPECTOR if (_initialValues == null) _initialValues = new(); if (_changed == null) _changed = new(); if (_serverOnChanges == null) _serverOnChanges = new(); if (_clientOnChanges == null) _clientOnChanges = new(); #endif foreach (T item in Collection) _initialValues.Add(item); } /// /// Gets the collection being used within this SyncList. /// /// public HashSet GetCollection(bool asServer) { return Collection; } /// /// Adds an operation and invokes locally. /// private void AddOperation(SyncHashSetOperation operation, T item, int collectionCountAfterChange) { if (!IsInitialized) return; bool asServerInvoke = !IsNetworkInitialized || NetworkBehaviour.IsServerStarted; if (asServerInvoke) { _valuesChanged = true; if (base.Dirty()) { ChangeData change = new(operation, item, collectionCountAfterChange); _changed.Add(change); } } InvokeOnChange(operation, item, asServerInvoke); } /// /// Called after OnStartXXXX has occurred. /// /// True if OnStartServer was called, false if OnStartClient. protected internal 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 an operation and data required by all operations. /// private void WriteOperationHeader(PooledWriter writer, SyncHashSetOperation operation, int collectionCountAfterChange) { writer.WriteUInt8Unpacked((byte)operation); writer.WriteInt32(collectionCountAfterChange); } /// /// Reads an operation and data required by all operations. /// private void ReadOperationHeader(PooledReader reader, out SyncHashSetOperation operation, out int collectionCountAfterChange) { operation = (SyncHashSetOperation)reader.ReadUInt8Unpacked(); collectionCountAfterChange = reader.ReadInt32(); } /// /// Writes all changed values. /// /// /// True to set the next time data may sync. protected internal 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]; WriteOperationHeader(writer, change.Operation, change.CollectionCountAfterChange); // 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.Set) writer.Write(change.Item); } _changed.Clear(); } } /// /// Writes all values if not initial values. /// /// protected internal 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); int iteration = 0; foreach (T item in Collection) { WriteOperationHeader(writer, SyncHashSetOperation.Add, collectionCountAfterChange: iteration + 1); writer.Write(item); iteration++; } } /// /// Reads and sets the current values for server or client. /// [APIExclude] protected internal override void Read(PooledReader reader, bool asServer) { SetReadArguments(reader, asServer, out bool newChangeId, out bool asClientHost, out bool canModifyValues); // True to warn if this object was deinitialized on the server. bool deinitialized = asClientHost && !OnStartServerCalled; if (deinitialized) NetworkManager.LogWarning($"SyncType {GetType().Name} received a Read but was deinitialized on the server. Client callback values may be incorrect. This is a ClientHost limitation."); ISet collection = Collection; bool fullWrite = reader.ReadBoolean(); // Clear collection since it's a full write. if (canModifyValues && fullWrite) collection.Clear(); int changes = reader.ReadInt32(); for (int i = 0; i < changes; i++) { ReadOperationHeader(reader, out SyncHashSetOperation operation, out int collectionCountAfterChange); T next = default; // Add. if (operation == SyncHashSetOperation.Add) { next = reader.Read(); if (canModifyValues) { // Integrity validation. if (collection.Count + 1 == collectionCountAfterChange) collection.Add(next); } } // Clear. else if (operation == SyncHashSetOperation.Clear) { if (canModifyValues) { // No integrity validation needed. collection.Clear(); } } // Remove. else if (operation == SyncHashSetOperation.Remove) { next = reader.Read(); if (canModifyValues) { // Integrity validation. if (collection.Count - 1 == collectionCountAfterChange) collection.Remove(next); } } // Set. else if (operation == SyncHashSetOperation.Set) { next = reader.Read(); if (canModifyValues) { // Integrity validation. if (collection.Count == collectionCountAfterChange) { collection.Remove(next); collection.Add(next); } } } if (newChangeId) InvokeOnChange(operation, next, false); } // If changes were made invoke complete after all have been read. if (newChangeId && changes > 0) InvokeOnChange(SyncHashSetOperation.Complete, default, false); } /// /// Invokes OnChanged callback. /// private void InvokeOnChange(SyncHashSetOperation operation, T item, bool asServer) { if (asServer) { if (NetworkBehaviour.OnStartServerCalled) OnChange?.Invoke(operation, item, asServer); else _serverOnChanges.Add(new(operation, item)); } else { if (NetworkBehaviour.OnStartClientCalled) OnChange?.Invoke(operation, item, asServer); else _clientOnChanges.Add(new(operation, item)); } } /// /// Resets to initialized values. /// protected internal override void ResetState(bool asServer) { base.ResetState(asServer); if (CanReset(asServer)) { _sendAll = false; _changed.Clear(); Collection.Clear(); foreach (T item in _initialValues) Collection.Add(item); } } /// /// Adds value. /// /// public bool Add(T item) { return Add(item, true); } private bool Add(T item, bool asServer) { if (!CanNetworkSetValues(true)) return false; bool result = Collection.Add(item); // Only process if add was successful. if (result && asServer) AddOperation(SyncHashSetOperation.Add, item, Collection.Count); 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 (!CanNetworkSetValues(true)) return; Collection.Clear(); if (asServer) AddOperation(SyncHashSetOperation.Clear, default, Collection.Count); } /// /// 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 (!CanNetworkSetValues(true)) return false; bool result = Collection.Remove(item); // Only process if remove was successful. if (result && asServer) AddOperation(SyncHashSetOperation.Remove, item, Collection.Count); return result; } /// /// Dirties the entire collection forcing a full send. /// public void DirtyAll() { if (!IsInitialized) return; if (!CanNetworkSetValues(log: 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 (!IsInitialized) return; if (!CanNetworkSetValues(true)) return; foreach (T item in Collection) { if (item.Equals(obj)) { AddOperation(SyncHashSetOperation.Set, obj, Collection.Count); return; } } // Not found. 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) { _cache.AddRange(Collection); int count = _cache.Count; for (int i = 0; i < count; i++) { T entry = _cache[i]; if (!other.Contains(entry)) Remove(entry); } _cache.Clear(); } 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); } } }