using FishNet.Documenting; using FishNet.Managing.Logging; using FishNet.Object.Synchronizing.Internal; using FishNet.Serializing; using FishNet.Utility.Extension; using JetBrains.Annotations; using System; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; namespace FishNet.Object.Synchronizing { public class SyncIDictionary : SyncBase, IDictionary, IReadOnlyDictionary { #region Types. /// /// Information needed to invoke a callback. /// private struct CachedOnChange { internal readonly SyncDictionaryOperation Operation; internal readonly TKey Key; internal readonly TValue Value; public CachedOnChange(SyncDictionaryOperation operation, TKey key, TValue value) { Operation = operation; Key = key; Value = value; } } /// /// Information about how the collection has changed. /// private struct ChangeData { internal readonly SyncDictionaryOperation Operation; internal readonly TKey Key; internal readonly TValue Value; public ChangeData(SyncDictionaryOperation operation, TKey key, TValue value) { this.Operation = operation; this.Key = key; this.Value = value; } } #endregion #region Public. /// /// Implementation from Dictionary. Not used. /// [APIExclude] public bool IsReadOnly => false; /// /// Delegate signature for when SyncDictionary changes. /// /// Operation being completed, such as Add, Set, Remove. /// Key being modified. /// Value of operation. /// True if callback is on the server side. False is on the client side. [APIExclude] public delegate void SyncDictionaryChanged(SyncDictionaryOperation op, TKey key, TValue value, bool asServer); /// /// Called when the SyncDictionary changes. /// public event SyncDictionaryChanged OnChange; /// /// Collection of objects. /// public readonly IDictionary Collection; /// /// Copy of objects on client portion when acting as a host. /// public readonly IDictionary ClientHostCollection = new Dictionary(); /// /// Number of objects in the collection. /// public int Count => Collection.Count; /// /// Keys within the collection. /// public ICollection Keys => Collection.Keys; [APIExclude] IEnumerable IReadOnlyDictionary.Keys => Collection.Keys; /// /// Values within the collection. /// public ICollection Values => Collection.Values; [APIExclude] IEnumerable IReadOnlyDictionary.Values => Collection.Values; #endregion #region Private. /// /// Initial values for the dictionary. /// private IDictionary _initialValues = new Dictionary(); /// /// 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 SyncIDictionary(IDictionary objects) { this.Collection = objects; //Add to clienthostcollection. foreach (KeyValuePair item in objects) this.ClientHostCollection[item.Key] = item.Value; } /// /// 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. /// The used collection. public Dictionary GetCollection(bool asServer) { bool asClientAndHost = (!asServer && base.NetworkManager.IsServer); IDictionary collection = (asClientAndHost) ? ClientHostCollection : Collection; return (collection as Dictionary); } /// /// Called when the SyncType has been registered, but not yet initialized over the network. /// protected override void Registered() { base.Registered(); foreach (KeyValuePair item in Collection) _initialValues[item.Key] = item.Value; } /// /// Adds an operation and invokes callback locally. /// Internal use. /// May be used for custom SyncObjects. /// /// /// /// [APIExclude] [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddOperation(SyncDictionaryOperation operation, TKey key, TValue value) { 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); if (asServerInvoke) { _valuesChanged = true; if (base.Dirty()) { ChangeData change = new ChangeData(operation, key, value); _changed.Add(change); } } InvokeOnChange(operation, key, value, 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.Key, item.Value, asServer); } collection.Clear(); } /// /// Writes all changed values. /// Internal use. /// May be used for custom SyncObjects. /// /// ///True to set the next time data may sync. [APIExclude] public override void WriteDelta(PooledWriter writer, bool resetSyncTick = true) { base.WriteDelta(writer, resetSyncTick); //If sending all then clear changed and write full. if (_sendAll) { _sendAll = false; _changed.Clear(); WriteFull(writer); } else { //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 == SyncDictionaryOperation.Add || change.Operation == SyncDictionaryOperation.Set) { writer.Write(change.Key); writer.Write(change.Value); } else if (change.Operation == SyncDictionaryOperation.Remove) { writer.Write(change.Key); } } _changed.Clear(); } } /// /// Writers all values if not initial values. /// Internal use. /// May be used for custom SyncObjects. /// /// [APIExclude] public override void WriteFull(PooledWriter writer) { if (!_valuesChanged) return; base.WriteHeader(writer, false); //True for full write. writer.WriteBoolean(true); writer.WriteInt32(Collection.Count); foreach (KeyValuePair item in Collection) { writer.WriteByte((byte)SyncDictionaryOperation.Add); writer.Write(item.Key); writer.Write(item.Value); } } /// /// Reads and sets the current values for server or client. /// [APIExclude] [MethodImpl(MethodImplOptions.AggressiveInlining)] 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.NetworkBehaviour.IsServer); IDictionary 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++) { SyncDictionaryOperation operation = (SyncDictionaryOperation)reader.ReadByte(); TKey key = default; TValue value = default; /* Add, Set. * Use the Set code for add and set, * especially so collection doesn't throw * if entry has already been added. */ if (operation == SyncDictionaryOperation.Add || operation == SyncDictionaryOperation.Set) { key = reader.Read(); value = reader.Read(); collection[key] = value; } //Clear. else if (operation == SyncDictionaryOperation.Clear) { collection.Clear(); } //Remove. else if (operation == SyncDictionaryOperation.Remove) { key = reader.Read(); collection.Remove(key); } InvokeOnChange(operation, key, value, false); } //If changes were made invoke complete after all have been read. if (changes > 0) InvokeOnChange(SyncDictionaryOperation.Complete, default, default, false); } /// /// Invokes OnChanged callback. /// private void InvokeOnChange(SyncDictionaryOperation operation, TKey key, TValue value, bool asServer) { if (asServer) { if (base.NetworkBehaviour.OnStartServerCalled) OnChange?.Invoke(operation, key, value, asServer); else _serverOnChanges.Add(new CachedOnChange(operation, key, value)); } else { if (base.NetworkBehaviour.OnStartClientCalled) OnChange?.Invoke(operation, key, value, asServer); else _clientOnChanges.Add(new CachedOnChange(operation, key, value)); } } /// /// Resets to initialized values. /// [APIExclude] public override void Reset() { base.Reset(); _sendAll = false; _changed.Clear(); Collection.Clear(); ClientHostCollection.Clear(); _valuesChanged = false; foreach (KeyValuePair item in _initialValues) { Collection[item.Key] = item.Value; ClientHostCollection[item.Key] = item.Value; } } /// /// Adds item. /// /// Item to add. public void Add(KeyValuePair item) { Add(item.Key, item.Value); } /// /// Adds key and value. /// /// Key to add. /// Value for key. public void Add(TKey key, TValue value) { Add(key, value, true); } private void Add(TKey key, TValue value, bool asServer) { if (!base.CanNetworkSetValues(true)) return; Collection.Add(key, value); if (asServer) AddOperation(SyncDictionaryOperation.Add, key, value); } /// /// Clears all values. /// public void Clear() { Clear(true); } private void Clear(bool asServer) { if (!base.CanNetworkSetValues(true)) return; Collection.Clear(); if (asServer) AddOperation(SyncDictionaryOperation.Clear, default, default); } /// /// Returns if key exist. /// /// Key to use. /// True if found. public bool ContainsKey(TKey key) { return Collection.ContainsKey(key); } /// /// Returns if item exist. /// /// Item to use. /// True if found. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Contains(KeyValuePair item) { return TryGetValue(item.Key, out TValue value) && EqualityComparer.Default.Equals(value, item.Value); } /// /// Copies collection to an array. /// /// Array to copy to. /// Offset of array data is copied to. public void CopyTo([NotNull] KeyValuePair[] array, int offset) { if (offset <= -1 || offset >= array.Length) { base.NetworkManager.LogError($"Index is out of range."); return; } int remaining = array.Length - offset; if (remaining < Count) { base.NetworkManager.LogError($"Array is not large enough to copy data. Array is of length {array.Length}, index is {offset}, and number of values to be copied is {Count.ToString()}."); return; } int i = offset; foreach (KeyValuePair item in Collection) { array[i] = item; i++; } } /// /// Removes a key. /// /// Key to remove. /// True if removed. public bool Remove(TKey key) { if (!base.CanNetworkSetValues(true)) return false; if (Collection.Remove(key)) { AddOperation(SyncDictionaryOperation.Remove, key, default); return true; } return false; } /// /// Removes an item. /// /// Item to remove. /// True if removed. public bool Remove(KeyValuePair item) { return Remove(item.Key); } /// /// Tries to get value from key. /// /// Key to use. /// Variable to output to. /// True if able to output value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetValue(TKey key, out TValue value) { return Collection.TryGetValueIL2CPP(key, out value); } /// /// Gets or sets value for a key. /// /// Key to use. /// Value when using as Get. public TValue this[TKey key] { get => Collection[key]; set { if (!base.CanNetworkSetValues(true)) return; Collection[key] = value; AddOperation(SyncDictionaryOperation.Set, key, value); } } /// /// 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; } /// /// Dirties an entry by key. /// /// Key to dirty. public void Dirty(TKey key) { if (!base.IsRegistered) return; if (!base.CanNetworkSetValues(true)) return; if (Collection.TryGetValueIL2CPP(key, out TValue value)) AddOperation(SyncDictionaryOperation.Set, key, value); } /// /// Dirties an entry by value. /// This operation can be very expensive, will cause allocations, and may fail if your value cannot be compared. /// /// Value to dirty. /// True if value was found and marked dirty. public bool Dirty(TValue value, EqualityComparer comparer = null) { if (!base.IsRegistered) return false; if (!base.CanNetworkSetValues(true)) return false; if (comparer == null) comparer = EqualityComparer.Default; foreach (KeyValuePair item in Collection) { if (comparer.Equals(item.Value, value)) { AddOperation(SyncDictionaryOperation.Set, item.Key, value); return true; } } //Not found. return false; } /// /// Gets the IEnumerator for the collection. /// /// public IEnumerator> GetEnumerator() => Collection.GetEnumerator(); /// /// Gets the IEnumerator for the collection. /// /// IEnumerator IEnumerable.GetEnumerator() => Collection.GetEnumerator(); } [APIExclude] public class SyncDictionary : SyncIDictionary { [APIExclude] public SyncDictionary() : base(new Dictionary()) { } [APIExclude] public SyncDictionary(IEqualityComparer eq) : base(new Dictionary(eq)) { } [APIExclude] public new Dictionary.ValueCollection Values => ((Dictionary)Collection).Values; [APIExclude] public new Dictionary.KeyCollection Keys => ((Dictionary)Collection).Keys; [APIExclude] public new Dictionary.Enumerator GetEnumerator() => ((Dictionary)Collection).GetEnumerator(); } }