#if !FISHNET_STABLE_SYNCTYPES 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; 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 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) { if (!base.IsInitialized) return; bool asServerInvoke = (!base.IsNetworkInitialized || base.NetworkBehaviour.IsServerStarted); if (asServerInvoke) { _valuesChanged = true; if (base.Dirty()) { ChangeData change = new(operation, item); _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 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]; writer.WriteUInt8Unpacked((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. /// /// 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); foreach (T item in Collection) { writer.WriteUInt8Unpacked((byte)SyncHashSetOperation.Add); writer.Write(item); } } /// /// Reads and sets the current values for server or client. /// [APIExclude] protected internal override void Read(PooledReader reader, bool asServer) { base.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 && !base.OnStartServerCalled); if (deinitialized) base.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++) { SyncHashSetOperation operation = (SyncHashSetOperation)reader.ReadUInt8Unpacked(); T next = default; //Add. if (operation == SyncHashSetOperation.Add) { next = reader.Read(); if (canModifyValues) collection.Add(next); } //Clear. else if (operation == SyncHashSetOperation.Clear) { if (canModifyValues) collection.Clear(); } //Remove. else if (operation == SyncHashSetOperation.Remove) { next = reader.Read(); if (canModifyValues) collection.Remove(next); } //Updated. else if (operation == SyncHashSetOperation.Update) { next = reader.Read(); if (canModifyValues) { 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 (base.NetworkBehaviour.OnStartServerCalled) OnChange?.Invoke(operation, item, asServer); else _serverOnChanges.Add(new(operation, item)); } else { if (base.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 (base.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 (!base.CanNetworkSetValues(true)) return false; bool result = Collection.Add(item); //Only process if remove was successful. if (result && asServer) 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) 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) AddOperation(SyncHashSetOperation.Remove, item); return result; } /// /// Dirties the entire collection forcing a full send. /// public void DirtyAll() { if (!base.IsInitialized) return; if (!base.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 (!base.IsInitialized) 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) { _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); } } } #endif