#if FISHNET_STABLE_SYNCTYPES using FishNet.Documenting; using FishNet.Managing; using FishNet.Object.Synchronizing.Internal; using FishNet.Serializing; using GameKit.Dependencies.Utilities; using System; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; namespace FishNet.Object.Synchronizing { [System.Serializable] public class SyncList : SyncBase, IList, IReadOnlyList { #region Types. /// /// Information needed to invoke a callback. /// private struct CachedOnChange { internal readonly SyncListOperation Operation; internal readonly int Index; internal readonly T Previous; internal readonly T Next; public CachedOnChange(SyncListOperation operation, int index, T previous, T next) { Operation = operation; Index = index; Previous = previous; Next = next; } } /// /// Information about how the collection has changed. /// private struct ChangeData { internal readonly SyncListOperation Operation; internal readonly int Index; internal readonly T Item; public ChangeData(SyncListOperation operation, int index, T item) { Operation = operation; Index = index; Item = item; } } #endregion #region Public. /// /// Implementation from List. Not used. /// [APIExclude] public bool IsReadOnly => false; /// /// Delegate signature for when SyncList changes. /// /// /// /// /// [APIExclude] public delegate void SyncListChanged(SyncListOperation op, int index, T oldItem, T newItem, bool asServer); /// /// Called when the SyncList changes. /// public event SyncListChanged OnChange; /// /// Collection of objects. /// public List Collection; /// /// Copy of objects on client portion when acting as a host. /// [HideInInspector] public List ClientHostCollection; /// /// Number of objects in the collection. /// public int Count => Collection.Count; #endregion #region Private. /// /// Values upon initialization. /// private List _initialValues; /// /// Comparer to see if entries change when calling public methods. /// private readonly IEqualityComparer _comparer; /// /// 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; /// /// 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 SyncList(SyncTypeSettings settings = new()) : this(CollectionCaches.RetrieveList(), EqualityComparer.Default, settings) { } public SyncList(IEqualityComparer comparer, SyncTypeSettings settings = new()) : this(new(), (comparer == null) ? EqualityComparer.Default : comparer, settings) { } public SyncList(List collection, IEqualityComparer comparer = null, SyncTypeSettings settings = new()) : base(settings) { _comparer = (comparer == null) ? EqualityComparer.Default : comparer; Collection = (collection == null) ? CollectionCaches.RetrieveList() : collection; ClientHostCollection = CollectionCaches.RetrieveList(); _initialValues = CollectionCaches.RetrieveList(); _changed = CollectionCaches.RetrieveList(); _serverOnChanges = CollectionCaches.RetrieveList(); _clientOnChanges = CollectionCaches.RetrieveList(); //Add each in collection to clienthostcollection. foreach (T item in collection) ClientHostCollection.Add(item); } #endregion #region Deconstructor. ~SyncList() { CollectionCaches.StoreAndDefault(ref Collection); CollectionCaches.StoreAndDefault(ref ClientHostCollection); 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. /// /// True if returning the server value, false if client value. The values will only differ when running as host. While asServer is true the most current values on server will be returned, and while false the latest values received by client will be returned. /// public List GetCollection(bool asServer) { bool asClientAndHost = (!asServer && base.NetworkManager.IsServerStarted); List collection = (asClientAndHost) ? ClientHostCollection : Collection; return (collection as List); } /// /// Adds an operation and invokes locally. /// /// /// /// /// private void AddOperation(SyncListOperation operation, int index, T prev, T next) { if (!base.IsInitialized) return; /* asServer might be true if the client is setting the value * through user code. Typically synctypes can only be set * by the server, that's why it is assumed asServer via user code. * However, when excluding owner for the synctype the client should * have permission to update the value locally for use with * prediction. */ bool asServerInvoke = (!base.IsNetworkInitialized || base.NetworkBehaviour.IsServerStarted); /* Only the adds asServer may set * this synctype as dirty and add * to pending changes. However, the event may still * invoke for clientside. */ if (asServerInvoke) { /* Set as changed even if cannot dirty. * Dirty is only set when there are observers, * but even if there are not observers * values must be marked as changed so when * there are observers, new values are sent. */ _valuesChanged = true; /* If unable to dirty then do not add to changed. * A dirty may fail if the server is not started * or if there's no observers. Changed doesn't need * to be populated in this situations because clients * will get the full collection on spawn. If we * were to also add to changed clients would get the full * collection as well the changed, which would double results. */ if (base.Dirty()) { ChangeData change = new(operation, index, next); _changed.Add(change); } } InvokeOnChange(operation, index, prev, next, 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.Index, item.Previous, item.Next, 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); //Number of entries expected. 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 == SyncListOperation.Add) { writer.Write(change.Item); } else if (change.Operation == SyncListOperation.RemoveAt) { writer.WriteInt32(change.Index); } else if (change.Operation == SyncListOperation.Insert || change.Operation == SyncListOperation.Set) { writer.WriteInt32(change.Index); 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); for (int i = 0; i < count; i++) { writer.WriteUInt8Unpacked((byte)SyncListOperation.Add); writer.Write(Collection[i]); } } /// /// 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."); List collection = (asClientHost) ? 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++) { SyncListOperation operation = (SyncListOperation)reader.ReadUInt8Unpacked(); int index = -1; T prev = default; T next = default; //Add. if (operation == SyncListOperation.Add) { next = reader.Read(); if (newChangeId) { index = collection.Count; collection.Add(next); } } //Clear. else if (operation == SyncListOperation.Clear) { if (newChangeId) collection.Clear(); } //Insert. else if (operation == SyncListOperation.Insert) { index = reader.ReadInt32(); next = reader.Read(); if (newChangeId) collection.Insert(index, next); } //RemoveAt. else if (operation == SyncListOperation.RemoveAt) { index = reader.ReadInt32(); if (newChangeId) { prev = collection[index]; collection.RemoveAt(index); } } //Set else if (operation == SyncListOperation.Set) { index = reader.ReadInt32(); next = reader.Read(); if (newChangeId) { prev = collection[index]; collection[index] = next; } } if (newChangeId) InvokeOnChange(operation, index, prev, next, false); } //If changes were made invoke complete after all have been read. if (newChangeId && changes > 0) InvokeOnChange(SyncListOperation.Complete, -1, default, default, false); } /// /// Invokes OnChanged callback. /// private void InvokeOnChange(SyncListOperation operation, int index, T prev, T next, bool asServer) { if (asServer) { if (base.NetworkBehaviour.OnStartServerCalled) OnChange?.Invoke(operation, index, prev, next, asServer); else _serverOnChanges.Add(new(operation, index, prev, next)); } else { if (base.NetworkBehaviour.OnStartClientCalled) OnChange?.Invoke(operation, index, prev, next, asServer); else _clientOnChanges.Add(new(operation, index, prev, next)); } } /// /// Resets to initialized values. /// protected internal override void ResetState(bool asServer) { base.ResetState(asServer); _sendAll = false; _changed.Clear(); ClientHostCollection.Clear(); Collection.Clear(); foreach (T item in _initialValues) { Collection.Add(item); ClientHostCollection.Add(item); } } /// /// Adds value. /// /// public void Add(T item) { Add(item, true); } private void Add(T item, bool asServer) { if (!base.CanNetworkSetValues(true)) return; Collection.Add(item); if (asServer) { if (base.NetworkManager == null) ClientHostCollection.Add(item); AddOperation(SyncListOperation.Add, Collection.Count - 1, default, item); } } /// /// 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(SyncListOperation.Clear, -1, default, default); } } /// /// Returns if value exist. /// /// /// public bool Contains(T item) { return (IndexOf(item) >= 0); } /// /// Copies values to an array. /// /// /// public void CopyTo(T[] array, int index) { Collection.CopyTo(array, index); } /// /// Gets the index of value. /// /// /// public int IndexOf(T item) { for (int i = 0; i < Collection.Count; ++i) { if (_comparer.Equals(item, Collection[i])) return i; } return -1; } /// /// Finds index using match. /// /// /// public int FindIndex(Predicate match) { for (int i = 0; i < Collection.Count; ++i) { if (match(Collection[i])) return i; } return -1; } /// /// Finds value using match. /// /// /// public T Find(Predicate match) { int i = FindIndex(match); return (i != -1) ? Collection[i] : default; } /// /// Finds all values using match. /// /// /// public List FindAll(Predicate match) { List results = new(); for (int i = 0; i < Collection.Count; ++i) { if (match(Collection[i])) results.Add(Collection[i]); } return results; } /// /// Inserts value at index. /// /// /// public void Insert(int index, T item) { Insert(index, item, true); } private void Insert(int index, T item, bool asServer) { if (!base.CanNetworkSetValues(true)) return; Collection.Insert(index, item); if (asServer) { if (base.NetworkManager == null) ClientHostCollection.Insert(index, item); AddOperation(SyncListOperation.Insert, index, default, item); } } /// /// Inserts a range of values. /// /// /// public void InsertRange(int index, IEnumerable range) { foreach (T entry in range) { Insert(index, entry); index++; } } /// /// Removes a value. /// /// /// public bool Remove(T item) { int index = IndexOf(item); bool result = index >= 0; if (result) RemoveAt(index); return result; } /// /// Removes value at index. /// /// /// public void RemoveAt(int index) { RemoveAt(index, true); } private void RemoveAt(int index, bool asServer) { if (!base.CanNetworkSetValues(true)) return; T oldItem = Collection[index]; Collection.RemoveAt(index); if (asServer) { if (base.NetworkManager == null) ClientHostCollection.RemoveAt(index); AddOperation(SyncListOperation.RemoveAt, index, oldItem, default); } } /// /// Removes all values within the collection. /// /// /// public int RemoveAll(Predicate match) { List toRemove = new(); for (int i = 0; i < Collection.Count; ++i) { if (match(Collection[i])) toRemove.Add(Collection[i]); } foreach (T entry in toRemove) Remove(entry); return toRemove.Count; } /// /// Gets or sets value at an index. /// /// /// public T this[int i] { get => Collection[i]; set => Set(i, value, true, true); } /// /// Dirties the entire collection forcing a full send. /// This will not invoke the callback on server. /// public void DirtyAll() { if (!base.IsInitialized) 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. /// While using this operation previous value will be the same as next. /// This operation can be very expensive, and may fail if your value cannot be compared. /// /// Object to lookup. public void Dirty(T obj) { int index = Collection.IndexOf(obj); if (index != -1) Dirty(index); else base.NetworkManager.LogError($"Could not find object within SyncList, dirty will not be set."); } /// /// Marks an index as dirty. /// While using this operation previous value will be the same as next. /// /// public void Dirty(int index) { if (!base.CanNetworkSetValues(true)) return; // bool asServer = true; T value = Collection[index]; // if (asServer) AddOperation(SyncListOperation.Set, index, value, value); } /// /// Sets value at index. /// /// /// public void Set(int index, T value, bool force = true) { Set(index, value, true, force); } private void Set(int index, T value, bool asServer, bool force) { if (!base.CanNetworkSetValues(true)) return; bool sameValue = (!force && _comparer.Equals(Collection[index], value)); if (!sameValue) { T prev = Collection[index]; Collection[index] = value; if (asServer) { if (base.NetworkManager == null) ClientHostCollection[index] = value; AddOperation(SyncListOperation.Set, index, prev, value); } } } /// /// Returns Enumerator for collection. /// /// public IEnumerator GetEnumerator() => Collection.GetEnumerator(); [APIExclude] IEnumerator IEnumerable.GetEnumerator() => Collection.GetEnumerator(); [APIExclude] IEnumerator IEnumerable.GetEnumerator() => Collection.GetEnumerator(); } } #endif