using FishNet.Documenting; using FishNet.Managing.Logging; using FishNet.Object.Synchronizing.Internal; using FishNet.Serializing; using System; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; namespace FishNet.Object.Synchronizing { 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; } } /// /// Custom enumerator to prevent garbage collection. /// [APIExclude] public struct Enumerator : IEnumerator { public T Current { get; private set; } private readonly SyncList _list; private int _index; public Enumerator(SyncList list) { this._list = list; _index = -1; Current = default; } public bool MoveNext() { _index++; if (_index >= _list.Count) return false; Current = _list[_index]; return true; } public void Reset() => _index = -1; object IEnumerator.Current => Current; public void Dispose() { } } #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 readonly IList Collection; /// /// Copy of objects on client portion when acting as a host. /// public readonly IList ClientHostCollection = new List(); /// /// Number of objects in the collection. /// public int Count => Collection.Count; #endregion #region Private. /// /// Values upon initialization. /// private IList _initialValues = new List(); /// /// 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 SyncList() : this(new List(), EqualityComparer.Default) { } [APIExclude] public SyncList(IEqualityComparer comparer) : this(new List(), (comparer == null) ? EqualityComparer.Default : comparer) { } [APIExclude] public SyncList(IList 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) this.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. /// /// 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.IsServer); IList collection = (asClientAndHost) ? ClientHostCollection : Collection; return (collection as List); } /// /// Adds an operation and invokes locally. /// /// /// /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddOperation(SyncListOperation operation, int index, T prev, T next) { if (!base.IsRegistered) 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.IsServer); /* 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 ChangeData(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. 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.Index, item.Previous, item.Next, 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 == 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. /// /// public override void WriteFull(PooledWriter writer) { if (!_valuesChanged) return; base.WriteHeader(writer, false); //True for full write. writer.WriteBoolean(true); writer.WriteInt32(Collection.Count); for (int i = 0; i < Collection.Count; i++) { writer.WriteByte((byte)SyncListOperation.Add); writer.Write(Collection[i]); } } /// /// 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); IList 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++) { SyncListOperation operation = (SyncListOperation)reader.ReadByte(); int index = -1; T prev = default; T next = default; //Add. if (operation == SyncListOperation.Add) { next = reader.Read(); index = collection.Count; collection.Add(next); } //Clear. else if (operation == SyncListOperation.Clear) { collection.Clear(); } //Insert. else if (operation == SyncListOperation.Insert) { index = reader.ReadInt32(); next = reader.Read(); collection.Insert(index, next); } //RemoveAt. else if (operation == SyncListOperation.RemoveAt) { index = reader.ReadInt32(); prev = collection[index]; collection.RemoveAt(index); } //Set else if (operation == SyncListOperation.Set) { index = reader.ReadInt32(); next = reader.Read(); prev = collection[index]; collection[index] = next; } InvokeOnChange(operation, index, prev, next, false); } //If changes were made invoke complete after all have been read. if (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 CachedOnChange(operation, index, prev, next)); } else { if (base.NetworkBehaviour.OnStartClientCalled) OnChange?.Invoke(operation, index, prev, next, asServer); else _clientOnChanges.Add(new CachedOnChange(operation, index, prev, next)); } } /// /// Resets to initialized values. /// public override void Reset() { base.Reset(); _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 List(); 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 List(); 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.IsRegistered) return; if (base.NetworkManager != null && !base.NetworkBehaviour.IsServer) { base.NetworkManager.LogWarning($"Cannot complete operation as server when server is not active."); 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 Enumerator GetEnumerator() => new Enumerator(this); [APIExclude] IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); [APIExclude] IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); } }