// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.DataStructure.Adapters { using System; using System.Collections; using System.Collections.Generic; using System.Runtime.Serialization; using System.Text.Json.Serialization; using ProtoBuf; using UnityEngine; using WallstopStudios.UnityHelpers.Utils; #if UNITY_EDITOR using UnityEditor; #endif /// /// Base implementation for Unity-friendly sorted dictionaries that preserves ordering across Unity, JSON, and ProtoBuf serialization. /// Coordinates the serialized key/value arrays with an underlying and lets derived types decide how values are cached. /// /// /// /// { /// } /// /// [Serializable] /// public sealed class QuestDictionary /// : SerializableSortedDictionaryBase /// { /// public QuestDictionary() /// : base(new SortedDictionary(StringComparer.OrdinalIgnoreCase)) /// { /// } /// /// protected override QuestDefinition GetValue(QuestCache[] cache, int index) /// { /// return cache[index].Data; /// } /// /// protected override void SetValue(QuestCache[] cache, int index, QuestDefinition value) /// { /// cache[index] = new QuestCache { Data = value }; /// } /// } /// ]]> /// [Serializable] [ProtoContract(IgnoreListHandling = true)] public abstract class SerializableSortedDictionaryBase : IDictionary, IDictionary, IReadOnlyDictionary, ISerializationCallbackReceiver, IDeserializationCallback, ISerializable where TKey : IComparable { private const string KeysSerializationName = "Keys"; private const string ValuesSerializationName = "Values"; internal bool HasDuplicatesOrNulls => _hasDuplicatesOrNulls; internal bool PreserveSerializedEntries => _preserveSerializedEntries; internal TKey[] SerializedKeys => _keys; internal TValueCache[] SerializedValues => _values; internal bool SerializationArraysDirty => _arraysDirty; [SerializeField] [ProtoMember(1, OverwriteList = true)] [JsonInclude] protected internal TKey[] _keys; [SerializeField] [ProtoMember(2, OverwriteList = true)] [JsonInclude] protected internal TValueCache[] _values; [ProtoIgnore] [JsonIgnore] protected internal SortedDictionary _dictionary; [NonSerialized] protected internal bool _preserveSerializedEntries; [NonSerialized] protected internal bool _arraysDirty = true; [NonSerialized] protected internal bool _hasDuplicatesOrNulls; protected SerializableSortedDictionaryBase() { _dictionary = new SortedDictionary(); } protected SerializableSortedDictionaryBase(IDictionary dictionary) { if (dictionary == null) { throw new ArgumentNullException(nameof(dictionary)); } _dictionary = new SortedDictionary(); foreach (KeyValuePair pair in dictionary) { _dictionary[pair.Key] = pair.Value; } } protected SerializableSortedDictionaryBase(SerializationInfo info, StreamingContext context) { if (info == null) { throw new ArgumentNullException(nameof(info)); } _dictionary = new SortedDictionary(); _keys = (TKey[])info.GetValue(KeysSerializationName, typeof(TKey[])); _values = (TValueCache[])info.GetValue(ValuesSerializationName, typeof(TValueCache[])); OnAfterDeserialize(); } protected abstract TValue GetValue(TValueCache[] cache, int index); protected abstract void SetValue(TValueCache[] cache, int index, TValue value); public int Count => _dictionary.Count; public bool IsReadOnly => ((IDictionary)_dictionary).IsReadOnly; public ICollection Keys => _dictionary.Keys; IEnumerable IReadOnlyDictionary.Keys => _dictionary.Keys; public ICollection Values => _dictionary.Values; IEnumerable IReadOnlyDictionary.Values => _dictionary.Values; /// /// Gets or sets the value associated with the provided key while preserving sorted order. /// /// The key to access. /// The stored value. /// /// scoreboard = new SerializableSortedDictionary(StringComparer.Ordinal); /// scoreboard["Alice"] = 1200; /// int score = scoreboard["Alice"]; /// ]]> /// public TValue this[TKey key] { get => _dictionary[key]; set { _dictionary[key] = value; MarkSerializationCacheDirty(); } } /// /// Adds a new entry to the sorted dictionary and invalidates the serialized cache. /// /// The key to insert. /// The value associated with the key. /// /// scoreboard = new SerializableSortedDictionary(StringComparer.Ordinal); /// scoreboard.Add("Alice", 1200); /// ]]> /// public void Add(TKey key, TValue value) { _dictionary.Add(key, value); MarkSerializationCacheDirty(); } /// /// Attempts to add a new entry without throwing when the key already exists. /// /// The key to insert. /// The value associated with the key. /// true when the entry was added. /// /// scoreboard = new SerializableSortedDictionary(StringComparer.Ordinal); /// bool added = scoreboard.TryAdd("Alice", 1200); /// ]]> /// public bool TryAdd(TKey key, TValue value) { bool added = _dictionary.TryAdd(key, value); if (added) { MarkSerializationCacheDirty(); } return added; } /// /// Determines whether the dictionary already contains the specified key. /// /// The key to look up. /// true when the key exists. /// /// scoreboard = new SerializableSortedDictionary(StringComparer.Ordinal); /// bool hasAlice = scoreboard.ContainsKey("Alice"); /// ]]> /// public bool ContainsKey(TKey key) { return _dictionary.ContainsKey(key); } /// /// Removes an entry by key and marks the serialized cache as dirty when the key existed. /// /// The key to remove. /// true when the entry was removed. /// /// scoreboard = new SerializableSortedDictionary(StringComparer.Ordinal); /// bool removed = scoreboard.Remove("Alice"); /// ]]> /// public bool Remove(TKey key) { bool removed = _dictionary.Remove(key); if (removed) { MarkSerializationCacheDirty(); } return removed; } /// /// Removes an entry and outputs the value that was previously stored. /// /// The key to remove. /// Receives the removed value. /// true when the key existed. /// /// scoreboard = new SerializableSortedDictionary(StringComparer.Ordinal); /// int score; /// bool removed = scoreboard.Remove("Alice", out score); /// ]]> /// public bool Remove(TKey key, out TValue value) { bool removed = _dictionary.Remove(key, out value); if (removed) { MarkSerializationCacheDirty(); } return removed; } /// /// Retrieves a value without throwing when the key is missing. /// /// The key to locate. /// Outputs the value when found. /// true when the key exists. /// /// scoreboard = new SerializableSortedDictionary(StringComparer.Ordinal); /// int score; /// if (scoreboard.TryGetValue("Alice", out score)) /// { /// Debug.Log(score); /// } /// ]]> /// public bool TryGetValue(TKey key, out TValue value) { return _dictionary.TryGetValue(key, out value); } /// /// Removes every entry from the dictionary and clears the serialized cache. /// /// /// scoreboard = new SerializableSortedDictionary(StringComparer.Ordinal); /// scoreboard.Clear(); /// ]]> /// public void Clear() { _dictionary.Clear(); MarkSerializationCacheDirty(); } /// /// Replaces the dictionary contents with the entries from another dictionary. /// /// The source map to copy. /// /// seed = new SortedDictionary(); /// SerializableSortedDictionary scoreboard = new SerializableSortedDictionary(StringComparer.Ordinal); /// scoreboard.CopyFrom(seed); /// ]]> /// public void CopyFrom(IDictionary dictionary) { if (dictionary == null) { throw new ArgumentNullException(nameof(dictionary)); } _dictionary.Clear(); foreach (KeyValuePair pair in dictionary) { _dictionary[pair.Key] = pair.Value; } // Clear cached arrays since we're replacing all content _keys = null; _values = null; MarkSerializationCacheDirty(); } /// /// Creates a new populated with this dictionary's contents. /// /// A copy of the sorted dictionary's current state. public SortedDictionary ToSortedDictionary() { SortedDictionary copy = new(_dictionary, _dictionary.Comparer); return copy; } /// /// Creates a new array containing all keys in sorted order. /// /// /// /// Returns keys in the natural sorted order determined by the key's implementation. /// This matches the behavior of and standard sorted collection semantics. /// /// /// The returned array is always a defensive copy - modifications to it do not affect the dictionary. /// For empty dictionaries, is returned. /// /// /// To retrieve keys in their user-defined serialization order (as shown in the Unity inspector), /// use instead. /// /// /// A new array containing all keys in sorted order. /// /// scores = new SerializableSortedDictionary(); /// scores["Charlie"] = 75; /// scores["Alice"] = 100; /// scores["Bob"] = 85; /// string[] keyArray = scores.ToKeysArray(); // Returns ["Alice", "Bob", "Charlie"] /// ]]> /// public TKey[] ToKeysArray() { int count = _dictionary.Count; if (count == 0) { return Array.Empty(); } // Return keys in sorted order (from the underlying SortedDictionary) TKey[] result = new TKey[count]; _dictionary.Keys.CopyTo(result, 0); return result; } /// /// Creates a new array containing all keys in their user-defined serialization order. /// /// /// /// Returns keys in the order they appear in the serialized backing array, which reflects /// the user-defined order from the Unity inspector. This order is preserved across domain /// reloads and serialization cycles. /// /// /// The returned array is always a defensive copy - modifications to it do not affect the dictionary. /// For empty dictionaries, is returned. /// /// /// To retrieve keys in their natural sorted order, use instead. /// /// /// A new array containing all keys in serialization order. /// /// scores = new SerializableSortedDictionary(); /// // After inspector reordering, keys might be in a custom order /// string[] keyArray = scores.ToPersistedOrderKeysArray(); // Returns keys in inspector order /// ]]> /// public TKey[] ToPersistedOrderKeysArray() { int count = _dictionary.Count; if (count == 0) { return Array.Empty(); } // Ensure serialized state is current before reading from _keys. // Check both array structure validity AND that no mutations have occurred since last serialize. bool arraysValid = _preserveSerializedEntries && !_arraysDirty && _keys != null && _values != null && _keys.Length == count; if (!arraysValid) { OnBeforeSerialize(); } // Return a defensive copy preserving user-defined order TKey[] result = new TKey[count]; Array.Copy(_keys, result, count); return result; } /// /// Creates a new array containing all values in sorted key order. /// /// /// /// Returns values in the order determined by the sorted key order. The value at index i /// corresponds to the key at index i from . /// This matches the behavior of and standard sorted collection semantics. /// /// /// The returned array is always a defensive copy - modifications to it do not affect the dictionary. /// For empty dictionaries, is returned. /// /// /// To retrieve values in their user-defined serialization order (as shown in the Unity inspector), /// use instead. /// /// /// A new array containing all values in sorted key order. /// /// scores = new SerializableSortedDictionary(); /// scores["Charlie"] = 75; /// scores["Alice"] = 100; /// scores["Bob"] = 85; /// int[] valueArray = scores.ToValuesArray(); // Returns [100, 85, 75] (sorted by key) /// ]]> /// public TValue[] ToValuesArray() { int count = _dictionary.Count; if (count == 0) { return Array.Empty(); } // Return values in sorted key order (from the underlying SortedDictionary) TValue[] result = new TValue[count]; _dictionary.Values.CopyTo(result, 0); return result; } /// /// Creates a new array containing all values in their user-defined serialization order. /// /// /// /// Returns values in the order they appear in the serialized backing array, which reflects /// the user-defined order from the Unity inspector. Values are aligned with keys - the value /// at index i corresponds to the key at index i from . /// /// /// The returned array is always a defensive copy - modifications to it do not affect the dictionary. /// For empty dictionaries, is returned. /// /// /// To retrieve values in their natural sorted key order, use instead. /// /// /// A new array containing all values in serialization order. /// /// scores = new SerializableSortedDictionary(); /// // After inspector reordering, values are aligned with their persisted key order /// int[] valueArray = scores.ToPersistedOrderValuesArray(); // Returns values in inspector order /// ]]> /// public TValue[] ToPersistedOrderValuesArray() { int count = _dictionary.Count; if (count == 0) { return Array.Empty(); } // Ensure serialized state is current before reading from _values. // Check both array structure validity AND that no mutations have occurred since last serialize. bool arraysValid = _preserveSerializedEntries && !_arraysDirty && _keys != null && _values != null && _values.Length == count; if (!arraysValid) { OnBeforeSerialize(); } // Return a defensive copy preserving user-defined order TValue[] result = new TValue[count]; for (int i = 0; i < count; i++) { result[i] = GetValue(_values, i); } return result; } /// /// Creates a new array containing all key-value pairs in sorted key order. /// /// /// /// Returns pairs in the natural sorted order determined by the key's implementation. /// This matches the behavior of enumerating a and standard sorted collection semantics. /// /// /// The returned array is always a defensive copy - modifications to it do not affect the dictionary. /// For empty dictionaries, is returned. /// /// /// To retrieve key-value pairs in their user-defined serialization order (as shown in the Unity inspector), /// use instead. /// /// /// A new array containing all key-value pairs in sorted key order. /// /// scores = new SerializableSortedDictionary(); /// scores["Charlie"] = 75; /// scores["Alice"] = 100; /// scores["Bob"] = 85; /// KeyValuePair[] pairArray = scores.ToArray(); /// // Returns [("Alice", 100), ("Bob", 85), ("Charlie", 75)] in sorted key order /// ]]> /// public KeyValuePair[] ToArray() { int count = _dictionary.Count; if (count == 0) { return Array.Empty>(); } // Return pairs in sorted key order (from the underlying SortedDictionary) KeyValuePair[] result = new KeyValuePair[count]; int index = 0; foreach (KeyValuePair pair in _dictionary) { result[index++] = pair; } return result; } /// /// Creates a new array containing all key-value pairs in their user-defined serialization order. /// /// /// /// Returns pairs in the order they appear in the serialized backing arrays, which reflects /// the user-defined order from the Unity inspector. This order is preserved across domain /// reloads and serialization cycles. /// /// /// The returned array is always a defensive copy - modifications to it do not affect the dictionary. /// For empty dictionaries, is returned. /// /// /// To retrieve key-value pairs in their natural sorted key order, use instead. /// /// /// A new array containing all key-value pairs in serialization order. /// /// scores = new SerializableSortedDictionary(); /// // After inspector reordering, pairs are in a custom order /// KeyValuePair[] pairArray = scores.ToPersistedOrderArray(); // Returns pairs in inspector order /// ]]> /// public KeyValuePair[] ToPersistedOrderArray() { int count = _dictionary.Count; if (count == 0) { return Array.Empty>(); } // Ensure serialized state is current before reading from arrays. // Check both array structure validity AND that no mutations have occurred since last serialize. bool arraysValid = _preserveSerializedEntries && !_arraysDirty && _keys != null && _values != null && _keys.Length == count && _values.Length == count; if (!arraysValid) { OnBeforeSerialize(); } // Return a defensive copy preserving user-defined order KeyValuePair[] result = new KeyValuePair[count]; for (int i = 0; i < count; i++) { result[i] = new KeyValuePair(_keys[i], GetValue(_values, i)); } return result; } /// /// Flushes the in-memory dictionary into the serialized key/value arrays prior to Unity or ProtoBuf serialization. /// /// /// /// When serialized arrays already exist from a previous deserialization, this method preserves their /// order while synchronizing values. This ensures that the user-defined order of entries as shown /// in the Unity inspector is maintained across domain reloads and serialization cycles. /// /// /// The synchronization process: /// /// Existing keys: Values are updated in-place to reflect runtime changes /// Removed keys: Entries are filtered out from the arrays /// New keys: Appended to the end of the arrays /// /// /// /// /// /// sortedDictionary.OnBeforeSerialize(); /// var keys = sortedDictionary.SerializedKeys; /// /// public void OnBeforeSerialize() { bool arraysIntact = _keys != null && _values != null && _keys.Length == _values.Length; // If we have valid arrays with duplicates/nulls and should preserve them, // skip sync entirely to maintain the inspector's view of problematic data. if ( arraysIntact && _preserveSerializedEntries && !_arraysDirty && _hasDuplicatesOrNulls ) { return; } // If we have valid arrays and should preserve order, sync values while maintaining key order if (arraysIntact && _preserveSerializedEntries && !_arraysDirty) { SyncSerializedArraysPreservingOrder(); return; } // If arrays exist but are dirty, try to preserve order while applying changes if (arraysIntact && _arraysDirty) { SyncSerializedArraysPreservingOrder(); _arraysDirty = false; _preserveSerializedEntries = true; return; } // No existing arrays or they're inconsistent - build from scratch (sorted order) int count = _dictionary.Count; _keys = new TKey[count]; _values = new TValueCache[count]; int index = 0; foreach (KeyValuePair pair in _dictionary) { _keys[index] = pair.Key; SetValue(_values, index, pair.Value); index++; } _preserveSerializedEntries = true; _arraysDirty = false; } /// /// Synchronizes the serialized arrays with the dictionary while preserving the existing key order. /// New keys are appended, removed keys are filtered out, and existing keys have their values updated. /// private void SyncSerializedArraysPreservingOrder() { int dictionaryCount = _dictionary.Count; int arrayLength = _keys.Length; // Fast path: if counts match and all keys exist, just update values in place if (dictionaryCount == arrayLength) { bool allKeysMatch = true; for (int i = 0; i < arrayLength; i++) { TKey key = _keys[i]; if (key == null || !_dictionary.ContainsKey(key)) { allKeysMatch = false; break; } } if (allKeysMatch) { // Just update values in place, preserving key order for (int i = 0; i < arrayLength; i++) { TKey key = _keys[i]; TValue value = _dictionary[key]; SetValue(_values, i, value); } return; } } // Need to rebuild arrays while preserving order of existing keys using PooledResource> keysResource = Buffers.List.Get( out List newKeys ); using PooledResource> valuesResource = Buffers.List.Get( out List newValues ); using PooledResource> seenResource = Buffers.HashSet.Get( out HashSet seenKeys ); // First pass: keep existing keys that still exist in the dictionary, in their original order for (int i = 0; i < arrayLength; i++) { TKey key = _keys[i]; if (key != null && _dictionary.TryGetValue(key, out TValue value)) { if (seenKeys.Add(key)) { newKeys.Add(key); newValues.Add(value); } } } // Second pass: append new keys that weren't in the original arrays foreach (KeyValuePair pair in _dictionary) { if (seenKeys.Add(pair.Key)) { newKeys.Add(pair.Key); newValues.Add(pair.Value); } } // Rebuild arrays int newCount = newKeys.Count; _keys = new TKey[newCount]; _values = new TValueCache[newCount]; for (int i = 0; i < newCount; i++) { _keys[i] = newKeys[i]; SetValue(_values, i, newValues[i]); } } /// /// Rehydrates the sorted dictionary from the serialized key/value arrays after Unity or ProtoBuf deserialization. /// /// /// /// The serialized arrays represent the user-defined order of entries as they appear in the Unity inspector. /// This order is preserved across domain reloads and serialization cycles. The internal /// maintains sorted iteration order for efficient lookups, /// but the serialized arrays always reflect the user's intended order. /// /// /// /// /// sortedDictionary.OnAfterDeserialize(); /// TValue value = sortedDictionary[key]; /// /// public void OnAfterDeserialize() { bool keysAndValuesPresent = _keys != null && _values != null && _keys.Length == _values.Length; if (!keysAndValuesPresent) { _keys = null; _values = null; _preserveSerializedEntries = false; _arraysDirty = true; return; } _dictionary.Clear(); bool hasDuplicateKeys = false; bool encounteredNullReference = false; bool keySupportsNullCheck = TypeSupportsNullReferences(typeof(TKey)); int length = _keys.Length; for (int index = 0; index < length; index++) { TKey key = _keys[index]; TValue value = GetValue(_values, index); if (keySupportsNullCheck && ReferenceEquals(key, null)) { encounteredNullReference = true; LogNullReferenceSkip("key", index); continue; } if (!hasDuplicateKeys && _dictionary.ContainsKey(key)) { hasDuplicateKeys = true; } _dictionary[key] = value; } // Always preserve the serialized arrays after deserialization to maintain user-defined order. // The arrays represent the order as it appears in the Unity inspector, which should not // change due to domain reloads. Only runtime modifications via Add/Remove/Clear should // trigger array rebuilding (handled by MarkSerializationCacheDirty). _preserveSerializedEntries = true; _arraysDirty = false; // Track if we have duplicates/nulls that require special handling in the editor _hasDuplicatesOrNulls = hasDuplicateKeys || encounteredNullReference; } private static bool TypeSupportsNullReferences(Type type) { return type != null && (!type.IsValueType || typeof(UnityEngine.Object).IsAssignableFrom(type)); } private static void LogNullReferenceSkip(string component, int index) { #if UNITY_EDITOR if (!EditorShouldLog()) { return; } #endif Debug.LogError( $"SerializableSortedDictionary<{typeof(TKey).FullName}, {typeof(TValue).FullName}> skipped serialized entry at index {index} because the {component} reference was null." ); } #if UNITY_EDITOR private static bool EditorShouldLog() { try { return EditorApplication.isPlayingOrWillChangePlaymode; } catch (UnityException) { return false; } } #endif [ProtoBeforeSerialization] protected internal void OnProtoBeforeSerialization() { OnBeforeSerialize(); } [ProtoAfterSerialization] protected internal void OnProtoAfterSerialization() { if (_preserveSerializedEntries) { return; } _keys = null; _values = null; } [ProtoAfterDeserialization] protected internal void OnProtoAfterDeserialization() { OnAfterDeserialize(); } /// /// Writes the serialized representation of the dictionary into a instance. /// /// The serialization store to populate. /// Context for the serialization process. public void GetObjectData(SerializationInfo info, StreamingContext context) { if (info == null) { throw new ArgumentNullException(nameof(info)); } OnBeforeSerialize(); info.AddValue(KeysSerializationName, _keys, typeof(TKey[])); info.AddValue(ValuesSerializationName, _values, typeof(TValueCache[])); } /// /// Finalizes deserialization after data has been applied. /// /// Reserved for future use. public void OnDeserialization(object sender) { // No additional action required. The serialization constructor already // reconstructed the sorted dictionary from the serialized key/value arrays. } private void MarkSerializationCacheDirty() { _preserveSerializedEntries = false; _arraysDirty = true; // Note: We intentionally do NOT null out _keys and _values here to preserve order information // for SyncSerializedArraysPreservingOrder() during the next OnBeforeSerialize() call. } /// /// Returns a struct enumerator that iterates over entries in sorted order without allocations. /// /// An enumerator positioned before the first entry. /// /// scoreboard = new SerializableSortedDictionary(StringComparer.Ordinal); /// foreach (KeyValuePair entry in scoreboard) /// { /// Debug.Log(entry.Key); /// } /// ]]> /// public Enumerator GetEnumerator() { return new Enumerator(_dictionary.GetEnumerator()); } /// IEnumerator> IEnumerable< KeyValuePair >.GetEnumerator() { return _dictionary.GetEnumerator(); } /// IEnumerator IEnumerable.GetEnumerator() { return _dictionary.GetEnumerator(); } /// /// Adds an entry via the interface. /// /// The entry to add. /// /// scoreboard = new SerializableSortedDictionary(StringComparer.Ordinal); /// KeyValuePair entry = new KeyValuePair("Alice", 1200); /// scoreboard.Add(entry); /// ]]> /// public void Add(KeyValuePair item) { ((IDictionary)_dictionary).Add(item); MarkSerializationCacheDirty(); } /// /// Determines whether the dictionary contains the provided entry. /// /// The entry to locate. /// true when both the key and value match. /// /// scoreboard = new SerializableSortedDictionary(StringComparer.Ordinal); /// KeyValuePair entry = new KeyValuePair("Alice", 1200); /// bool present = scoreboard.Contains(entry); /// ]]> /// public bool Contains(KeyValuePair item) { return ((IDictionary)_dictionary).Contains(item); } /// /// Copies the dictionary contents into the provided array. /// /// Destination array. /// Index to begin writing at. /// /// scoreboard = new SerializableSortedDictionary(StringComparer.Ordinal); /// KeyValuePair[] snapshot = new KeyValuePair[scoreboard.Count]; /// scoreboard.CopyTo(snapshot, 0); /// ]]> /// public void CopyTo(KeyValuePair[] array, int arrayIndex) { ((IDictionary)_dictionary).CopyTo(array, arrayIndex); } /// /// Removes the specified entry when both the key and value match. /// /// The entry to remove. /// true when the entry existed. /// /// scoreboard = new SerializableSortedDictionary(StringComparer.Ordinal); /// KeyValuePair entry = new KeyValuePair("Alice", 1200); /// bool removed = scoreboard.Remove(entry); /// ]]> /// public bool Remove(KeyValuePair item) { bool removed = ((IDictionary)_dictionary).Remove(item); if (removed) { MarkSerializationCacheDirty(); } return removed; } /// /// Indicates whether the non-generic wrapper has a fixed size. /// public bool IsFixedSize => ((IDictionary)_dictionary).IsFixedSize; ICollection IDictionary.Keys => _dictionary.Keys; ICollection IDictionary.Values => _dictionary.Values; /// /// Indicates whether access to the dictionary is synchronized. /// public bool IsSynchronized => ((IDictionary)_dictionary).IsSynchronized; /// /// Provides an object that callers can lock on when coordinating access from multiple threads. /// public object SyncRoot => ((IDictionary)_dictionary).SyncRoot; /// /// Gets or sets entries through the non-generic interface. /// /// The boxed key. /// The boxed value. public object this[object key] { get => ((IDictionary)_dictionary)[key]; set { ((IDictionary)_dictionary)[key] = value; MarkSerializationCacheDirty(); } } /// /// Adds a boxed entry via the non-generic interface. /// /// The boxed key. /// The boxed value. /// /// scoreboard = new SerializableSortedDictionary(StringComparer.Ordinal); /// IDictionary boxed = scoreboard; /// boxed.Add((object)"Alice", 1200); /// ]]> /// public void Add(object key, object value) { ((IDictionary)_dictionary).Add(key, value); MarkSerializationCacheDirty(); } /// /// Determines whether the dictionary contains the specified boxed key. /// /// The key to locate. /// true when the key exists. public bool Contains(object key) { return ((IDictionary)_dictionary).Contains(key); } /// IDictionaryEnumerator IDictionary.GetEnumerator() { return _dictionary.GetEnumerator(); } /// /// Removes a boxed entry and invalidates the serialized cache when the element existed. /// /// The boxed key to remove. public void Remove(object key) { IDictionary dictionary = _dictionary; bool existed = dictionary.Contains(key); dictionary.Remove(key); if (existed) { MarkSerializationCacheDirty(); } } /// /// Copies entries into a array to satisfy . /// /// Destination array. /// Destination index. public void CopyTo(Array array, int index) { ((IDictionary)_dictionary).CopyTo(array, index); } /// /// Allocation-free enumerator returned by . /// public struct Enumerator : IEnumerator> { private SortedDictionary.Enumerator _enumerator; internal Enumerator(SortedDictionary.Enumerator enumerator) { _enumerator = enumerator; } public KeyValuePair Current => _enumerator.Current; object IEnumerator.Current => _enumerator.Current; /// /// Advances the enumerator to the next entry. /// /// true when another element is available. public bool MoveNext() { return _enumerator.MoveNext(); } /// /// Releases the underlying sorted dictionary enumerator. /// public void Dispose() { _enumerator.Dispose(); } /// /// Reset is not supported; enumerators follow semantics. /// void IEnumerator.Reset() { throw new NotSupportedException("Reset is not supported."); } } } /// /// Concrete sorted dictionary implementation that saves both keys and values through Unity, ProtoBuf, and JSON serialization. /// Use this when you need deterministic iteration order plus inspector support for key/value data. /// /// /// scoreboard = new SerializableSortedDictionary(StringComparer.Ordinal); /// scoreboard.Add("Alice", 1200); /// scoreboard.Add("Bob", 900); /// foreach (KeyValuePair entry in scoreboard) /// { /// Debug.Log($"{entry.Key}: {entry.Value}"); /// } /// ]]> /// [Serializable] public class SerializableSortedDictionary : SerializableSortedDictionaryBase where TKey : IComparable { /// /// Initializes an empty sorted dictionary compatible with Unity and ProtoBuf serialization. /// public SerializableSortedDictionary() { } /// /// Initializes the dictionary by copying entries from an existing map. /// /// Source dictionary whose contents are copied into the new instance. public SerializableSortedDictionary(IDictionary dictionary) : base(dictionary) { } /// /// Initializes the dictionary during custom serialization scenarios. /// protected SerializableSortedDictionary(SerializationInfo info, StreamingContext context) : base(info, context) { } protected override TValue GetValue(TValue[] cache, int index) { return cache[index]; } protected override void SetValue(TValue[] cache, int index, TValue value) { cache[index] = value; } } /// /// Sorted dictionary variant that stores each value in a cache object so complex data can be serialized safely. /// Extend this when values require bespoke serialization, such as types containing Unity objects or unmanaged resources. /// /// /// /// { /// } /// /// SerializableSortedDictionary catalog = /// new SerializableSortedDictionary(); /// catalog[1] = new RichValue { Name = "HealthPotion", Cost = 50 }; /// ]]> /// [Serializable] public class SerializableSortedDictionary : SerializableSortedDictionaryBase where TKey : IComparable where TValueCache : SerializableDictionary.Cache, new() { /// /// Initializes an empty sorted dictionary whose values are stored through cache entries. /// public SerializableSortedDictionary() { } /// /// Initializes the dictionary by copying entries from an existing map. /// /// Source dictionary whose contents are copied into the new instance. public SerializableSortedDictionary(IDictionary dictionary) : base(dictionary) { } /// /// Initializes the dictionary during custom serialization scenarios. /// protected SerializableSortedDictionary(SerializationInfo info, StreamingContext context) : base(info, context) { } protected override TValue GetValue(TValueCache[] cache, int index) { return cache[index].Data; } protected override void SetValue(TValueCache[] cache, int index, TValue value) { cache[index] = new TValueCache { Data = value }; } } }