// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE // Portions of this file are adapted from JDSherbert's Unity-Serializable-Dictionary (MIT License): // https://github.com/JDSherbert/Unity-Serializable-Dictionary 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.Core.Extension; using WallstopStudios.UnityHelpers.Utils; #if UNITY_EDITOR using UnityEditor; #endif /// /// Provides the shared infrastructure for Unity-friendly serializable dictionary implementations. /// Manages the synchronized key and value arrays that Unity, ProtoBuf, and JSON rely on, /// while exposing a runtime dictionary for fast lookups and mutations. /// Derive from the generic base to create strongly typed dictionaries that stay editable in the inspector. /// /// /// /// { /// } /// [Serializable] /// public sealed class WeaponDictionary /// : SerializableDictionaryBase /// { /// protected override WeaponDefinition GetValue(WeaponDefinitionCache[] cache, int index) /// { /// return cache[index].Data; /// } /// protected override void SetValue(WeaponDefinitionCache[] cache, int index, WeaponDefinition value) /// { /// cache[index] = new WeaponDefinitionCache { Data = value }; /// } /// } /// ]]> /// [Serializable] public abstract class SerializableDictionaryBase { protected internal class Dictionary : System.Collections.Generic.Dictionary { /// /// Creates an empty runtime dictionary that uses the default equality comparer. /// /// /// /// { /// public AbilityDictionary() /// : base(new Dictionary()) /// { /// } /// } /// ]]> /// public Dictionary() { } /// /// Creates a runtime dictionary pre-populated with entries from another dictionary. /// /// The source collection to copy. /// /// seed = new Dictionary(); /// Dictionary runtimeDictionary = /// new Dictionary(seed); /// ]]> /// public Dictionary(IDictionary dictionary) : base(dictionary) { } /// /// Rehydrates the dictionary from a payload. /// /// Serialized data describing the dictionary. /// Context about the serialization source or destination. /// /// ), new FormatterConverter()); /// StreamingContext context = new StreamingContext(StreamingContextStates.File); /// Dictionary runtimeDictionary = new Dictionary(info, context); /// ]]> /// public Dictionary( SerializationInfo serializationInfo, StreamingContext streamingContext ) : base(serializationInfo, streamingContext) { } } [Serializable] public abstract class Cache { } /// /// Produces a JSON string that mirrors the serialized key and value arrays, which is useful for debugging. /// /// A JSON representation of the dictionary contents. /// /// /// public override string ToString() { return this.ToJson(); } internal abstract void EditorAfterDeserialize(); /// /// Syncs the runtime dictionary state to the serialized arrays (_keys and _values). /// This is the inverse of EditorAfterDeserialize - it writes runtime state to serialized state. /// Used by editor code when directly modifying the dictionary and needing to persist changes. /// internal abstract void EditorSyncSerializedArrays(); } /// /// Shared implementation for Unity serializable dictionaries that keeps serialized key/value arrays synchronized with the runtime . /// Override and to control how values move between serialized caches and the live map. /// /// /// /// { /// } /// /// [Serializable] /// public sealed class AbilityDictionary /// : SerializableDictionaryBase /// { /// public AbilityDictionary() /// : base(new Dictionary(StringComparer.OrdinalIgnoreCase)) /// { /// } /// /// protected override AbilityDefinition GetValue(AbilityCache[] cache, int index) /// { /// return cache[index].Data; /// } /// /// protected override void SetValue(AbilityCache[] cache, int index, AbilityDefinition value) /// { /// cache[index] = new AbilityCache { Data = value }; /// } /// } /// ]]> /// /// Dictionary key type. /// Dictionary value type. /// Serialized value cache type. [Serializable] [ProtoContract(IgnoreListHandling = true)] public abstract class SerializableDictionaryBase : SerializableDictionaryBase, IDictionary, IDictionary, ISerializationCallbackReceiver, IDeserializationCallback, ISerializable, IReadOnlyDictionary { [ProtoIgnore] [JsonIgnore] protected internal Dictionary _dictionary; [SerializeField] [ProtoMember(1, OverwriteList = true)] protected internal TKey[] _keys; [SerializeField] [ProtoMember(2, OverwriteList = true)] protected internal TValueCache[] _values; [NonSerialized] protected internal bool _preserveSerializedEntries; [NonSerialized] protected internal bool _hasDuplicatesOrNulls; /// /// Tracks keys added since the last serialization cycle, in insertion order. /// This is used to preserve the order in which entries were added during the next serialization. /// [NonSerialized] protected internal List _newKeysOrder; protected internal bool PreserveSerializedEntries => _preserveSerializedEntries; protected internal bool HasDuplicatesOrNulls => _hasDuplicatesOrNulls; protected internal TKey[] SerializedKeys => _keys; protected internal TValueCache[] SerializedValues => _values; protected SerializableDictionaryBase() { _dictionary = new Dictionary(); } protected SerializableDictionaryBase(IDictionary dictionary) { _dictionary = new Dictionary(dictionary); } protected SerializableDictionaryBase( SerializationInfo serializationInfo, StreamingContext streamingContext ) { _dictionary = new Dictionary(serializationInfo, streamingContext); } internal static class SerializedPropertyNames { private sealed class NameHolder : SerializableDictionary { public const string KeysName = nameof(_keys); public const string ValuesName = nameof(_values); } internal const string KeysNameInternal = NameHolder.KeysName; internal const string ValuesNameInternal = NameHolder.ValuesName; } /// /// Rebuilds the runtime dictionary from the serialized key/value arrays after Unity or ProtoBuf deserialization. /// /// /// Invoked automatically by Unity; call it manually only when deserializing outside of Unity's pipeline. /// /// /// restored = dictionary; /// ]]> /// public void OnAfterDeserialize() { OnAfterDeserializeInternal(suppressWarnings: false); } internal override void EditorAfterDeserialize() { OnAfterDeserializeInternal(suppressWarnings: true); } internal override void EditorSyncSerializedArrays() { // Force sync from runtime dictionary to serialized arrays _preserveSerializedEntries = false; OnBeforeSerialize(); } private void OnAfterDeserializeInternal(bool suppressWarnings) { bool keysAndValuesPresent = _keys != null && _values != null && _keys.Length == _values.Length; if (!keysAndValuesPresent) { _keys = null; _values = null; _preserveSerializedEntries = false; _hasDuplicatesOrNulls = false; return; } _dictionary.Clear(); HashSet observedKeys = new(); 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; if (!suppressWarnings) { LogNullReferenceSkip("key", index); } continue; } if (!hasDuplicateKeys && !observedKeys.Add(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; // 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( $"SerializableDictionary<{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 /// /// Packs the runtime dictionary contents into the serialized key/value arrays prior to Unity or ProtoBuf serialization. /// /// /// /// When a serialized array already exists from a previous deserialization, this method preserves its /// order while synchronizing with the runtime dictionary. This ensures that the user-defined order of elements /// as shown in the Unity inspector is maintained across domain reloads and serialization cycles. /// /// /// The synchronization process: /// /// Existing entries: Kept in their original order if still in the dictionary /// Removed entries: Filtered out from the arrays /// New entries: Appended to the end of the arrays in insertion order /// /// /// /// /// /// dictionary.OnBeforeSerialize(); /// var keys = dictionary.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 (_preserveSerializedEntries && arraysIntact && _hasDuplicatesOrNulls) { return; } // If we have valid arrays and should preserve order, sync while maintaining order if (_preserveSerializedEntries && arraysIntact) { SyncSerializedArraysPreservingOrder(); return; } // If arrays exist but are not being preserved (dirty), try to preserve order if (arraysIntact) { SyncSerializedArraysPreservingOrder(); _preserveSerializedEntries = true; return; } // No existing arrays - build from scratch (dictionary's natural iteration 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; _newKeysOrder?.Clear(); } /// /// Synchronizes the serialized arrays with the dictionary while preserving the existing order. /// Existing keys are kept in their original positions, removed keys are filtered out, /// and new keys are appended in insertion order. /// private void SyncSerializedArraysPreservingOrder() { int dictCount = _dictionary.Count; int arrayLength = _keys.Length; // Fast path: if counts match, all array keys are unique, and all keys still exist in the dictionary, no changes needed. // We must check for uniqueness because duplicate keys in the array can make counts match by coincidence // (e.g., array has {3, 3} with dictCount=2 after adding key 4, but the array should become {3, 4}). if (dictCount == arrayLength) { using PooledResource> fastPathSeenResource = Buffers.HashSet.Get(out HashSet fastPathSeenKeys); bool allEntriesMatchAndUnique = true; for (int i = 0; i < arrayLength; i++) { TKey key = _keys[i]; // Check both that the key exists in the dictionary AND that it's not a duplicate in the array if ( !_dictionary.TryGetValue(key, out TValue dictValue) || !fastPathSeenKeys.Add(key) ) { allEntriesMatchAndUnique = false; break; } } if (allEntriesMatchAndUnique) { // Update values in case they changed, but keep order for (int i = 0; i < arrayLength; i++) { TKey key = _keys[i]; SetValue(_values, i, _dictionary[key]); } _newKeysOrder?.Clear(); 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 (_dictionary.TryGetValue(key, out TValue value) && seenKeys.Add(key)) { newKeys.Add(key); newValues.Add(value); } } // Second pass: append new keys in the order they were added (if tracked) if (_newKeysOrder is { Count: > 0 }) { foreach (TKey key in _newKeysOrder) { // Only add if it still exists in the dictionary and wasn't already seen if (_dictionary.TryGetValue(key, out TValue value) && seenKeys.Add(key)) { newKeys.Add(key); newValues.Add(value); } } } else { // Fallback: iterate over the dictionary for keys not in the original array // (order may not match insertion order) 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]); } // Clear the tracked new keys since they're now in the serialized arrays _newKeysOrder?.Clear(); } /// /// Tracks a newly added key for order preservation during serialization. /// private void TrackNewKey(TKey key) { _newKeysOrder ??= new List(); _newKeysOrder.Add(key); } [ProtoBeforeSerialization] protected internal void OnProtoBeforeSerialization() { OnBeforeSerialize(); } [ProtoAfterSerialization] protected internal void OnProtoAfterSerialization() { if (_preserveSerializedEntries) { return; } _keys = null; _values = null; } [ProtoAfterDeserialization] protected internal void OnProtoAfterDeserialization() { OnAfterDeserializeInternal(suppressWarnings: false); } protected abstract void SetValue(TValueCache[] cache, int index, TValue value); protected abstract TValue GetValue(TValueCache[] cache, int index); /// /// Replaces this dictionary's contents with another dictionary. /// /// Source dictionary. public void CopyFrom(IDictionary dictionary) { _dictionary ??= new Dictionary(); _dictionary.Clear(); foreach (KeyValuePair pair in dictionary) { _dictionary[pair.Key] = pair.Value; } // Clear the order tracking since we're replacing all content _newKeysOrder?.Clear(); _keys = null; _values = null; MarkSerializationCacheDirty(); } /// /// Creates a new populated with this dictionary's contents. /// /// A copy of the dictionary's current state. public global::System.Collections.Generic.Dictionary ToDictionary() { global::System.Collections.Generic.Dictionary copy = new( _dictionary, _dictionary.Comparer ); return copy; } /// /// Creates a new array containing all keys in the dictionary's natural iteration order. /// /// /// /// Returns keys in the order determined by the underlying 's iteration order. /// This matches the behavior of and standard dictionary 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 dictionary iteration order. /// /// scores = new SerializableDictionary(); /// scores["Alice"] = 100; /// scores["Bob"] = 85; /// string[] keyArray = scores.ToKeysArray(); /// ]]> /// public TKey[] ToKeysArray() { int count = _dictionary.Count; if (count == 0) { return Array.Empty(); } // Return keys in dictionary iteration order (from the underlying Dictionary) 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 the dictionary's natural iteration order, use instead. /// /// /// A new array containing all keys in serialization order. /// /// scores = new SerializableDictionary(); /// // 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 && _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 the dictionary's natural iteration order. /// /// /// /// Returns values in the order determined by the underlying 's iteration order. /// The value at index i corresponds to the key at index i from . /// This matches the behavior of and standard dictionary 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 dictionary iteration order. /// /// scores = new SerializableDictionary(); /// scores["Alice"] = 100; /// scores["Bob"] = 85; /// int[] valueArray = scores.ToValuesArray(); /// ]]> /// public TValue[] ToValuesArray() { int count = _dictionary.Count; if (count == 0) { return Array.Empty(); } // Return values in dictionary iteration order (from the underlying Dictionary) 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 the dictionary's natural iteration order, use instead. /// /// /// A new array containing all values in serialization order. /// /// scores = new SerializableDictionary(); /// // 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 && _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 the dictionary's natural iteration order. /// /// /// /// Returns pairs in the order determined by the underlying 's iteration order. /// This matches the behavior of enumerating a and standard dictionary 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 dictionary iteration order. /// /// scores = new SerializableDictionary(); /// scores["Alice"] = 100; /// scores["Bob"] = 85; /// KeyValuePair[] pairArray = scores.ToArray(); /// ]]> /// public KeyValuePair[] ToArray() { int count = _dictionary.Count; if (count == 0) { return Array.Empty>(); } // Return pairs in dictionary iteration order (from the underlying Dictionary) 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 the dictionary's natural iteration order, use instead. /// /// /// A new array containing all key-value pairs in serialization order. /// /// scores = new SerializableDictionary(); /// // 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 && _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; } private void MarkSerializationCacheDirty() { _preserveSerializedEntries = false; _hasDuplicatesOrNulls = false; // Note: We intentionally do NOT null out _keys and _values here. // They are preserved so that SyncSerializedArraysPreservingOrder can maintain // the existing order of keys while applying mutations. } public ICollection Keys => _dictionary.Keys; IEnumerable IReadOnlyDictionary.Values => Values; IEnumerable IReadOnlyDictionary.Keys => Keys; public ICollection Values => _dictionary.Values; public int Count => _dictionary.Count; public bool IsReadOnly => ((IDictionary)_dictionary).IsReadOnly; /// /// Gets or sets a value associated with the provided key in the runtime dictionary. /// Updates invalidate the serialized cache so Unity and ProtoBuf can pick up the changes. /// /// The key of the entry to access. /// The stored value. /// /// /// public TValue this[TKey key] { get => _dictionary[key]; set { bool isNewKey = !_dictionary.ContainsKey(key); _dictionary[key] = value; if (isNewKey) { TrackNewKey(key); } MarkSerializationCacheDirty(); } } /// /// Adds a new key/value pair to the runtime dictionary and marks the serialized cache as dirty so Unity can persist the change. /// /// The key to insert. /// The value associated with the key. /// /// /// public void Add(TKey key, TValue value) { _dictionary.Add(key, value); TrackNewKey(key); 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. /// /// /// public bool TryAdd(TKey key, TValue value) { bool added = _dictionary.TryAdd(key, value); if (added) { TrackNewKey(key); MarkSerializationCacheDirty(); } return added; } /// /// Determines whether the runtime dictionary already contains the specified key. /// /// The key to locate. /// true when the key exists. /// /// /// public bool ContainsKey(TKey key) { return _dictionary.ContainsKey(key); } /// /// Removes an entry by key and invalidates the serialized cache if the key existed. /// /// The key to remove. /// true when an entry was removed. /// /// /// public bool Remove(TKey key) { bool removed = _dictionary.Remove(key); if (removed) { // Remove from tracked new keys if present _newKeysOrder?.Remove(key); MarkSerializationCacheDirty(); } return removed; } /// /// Removes an entry and outputs the value that was previously stored. /// /// The key to remove. /// Receives the removed value when successful. /// true when the key was found. /// /// /// public bool Remove(TKey key, out TValue value) { bool removed = _dictionary.Remove(key, out value); if (removed) { // Remove from tracked new keys if present _newKeysOrder?.Remove(key); MarkSerializationCacheDirty(); } return removed; } /// /// Retrieves a value without throwing when the key is missing. /// /// The key to locate. /// Outputs the located value. /// true when the key exists. /// /// /// public bool TryGetValue(TKey key, out TValue value) { return _dictionary.TryGetValue(key, out value); } /// /// Adds a to the dictionary via the interface. /// /// The entry to add. /// /// entry = new KeyValuePair("Dash", dashDefinition); /// abilities.Add(entry); /// ]]> /// public void Add(KeyValuePair item) { ((IDictionary)_dictionary).Add(item); TrackNewKey(item.Key); MarkSerializationCacheDirty(); } /// /// Removes all entries from the dictionary and clears the serialized arrays. /// /// /// /// public void Clear() { _dictionary.Clear(); _newKeysOrder?.Clear(); // Clear the arrays completely since we're removing all entries _keys = null; _values = null; MarkSerializationCacheDirty(); } /// /// Determines whether the dictionary contains the provided key/value pair. /// /// The entry to look for. /// true when both the key and value match. /// /// entry = new KeyValuePair("Dash", dashDefinition); /// bool present = abilities.Contains(entry); /// ]]> /// public bool Contains(KeyValuePair item) { return ((IDictionary)_dictionary).Contains(item); } /// /// Copies the contents of the dictionary into the provided array, which is useful when interoperating with legacy APIs. /// /// The destination array. /// The index to start copying into. /// /// [] snapshot = new KeyValuePair[abilities.Count]; /// abilities.CopyTo(snapshot, 0); /// ]]> /// public void CopyTo(KeyValuePair[] array, int arrayIndex) { ((IDictionary)_dictionary).CopyTo(array, arrayIndex); } /// /// Removes the specified key/value pair only when both components match the stored entry. /// /// The entry to remove. /// true when the pair existed. /// /// entry = new KeyValuePair("Dash", dashDefinition); /// bool removed = abilities.Remove(entry); /// ]]> /// public bool Remove(KeyValuePair item) { bool removed = ((IDictionary)_dictionary).Remove(item); if (removed) { // Remove from tracked new keys if present _newKeysOrder?.Remove(item.Key); MarkSerializationCacheDirty(); } return removed; } /// /// Returns a struct enumerator that iterates over key/value pairs without allocations. /// /// An enumerator positioned before the first entry. /// /// entry in abilities) /// { /// 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(); } /// /// Indicates whether the non-generic wrapper has a fixed size (it does not). /// public bool IsFixedSize => ((IDictionary)_dictionary).IsFixedSize; ICollection IDictionary.Keys => _dictionary.Keys; ICollection IDictionary.Values => _dictionary.Values; /// /// Indicates whether access to the dictionary is synchronized (thread-safe). /// public bool IsSynchronized => ((IDictionary)_dictionary).IsSynchronized; /// /// Provides an object that can be used to synchronize access when required by legacy APIs. /// public object SyncRoot => ((IDictionary)_dictionary).SyncRoot; /// /// Gets or sets entries through the non-generic interface. /// /// The boxed key. /// The boxed value associated with the key. public object this[object key] { get => ((IDictionary)_dictionary)[key]; set { bool isNewKey = !((IDictionary)_dictionary).Contains(key); ((IDictionary)_dictionary)[key] = value; if (isNewKey && key is TKey typedKey) { TrackNewKey(typedKey); } MarkSerializationCacheDirty(); } } /// /// Adds a boxed key/value pair through the non-generic interface. /// /// The boxed key. /// The boxed value. /// /// /// public void Add(object key, object value) { ((IDictionary)_dictionary).Add(key, value); if (key is TKey typedKey) { TrackNewKey(typedKey); } MarkSerializationCacheDirty(); } /// /// Determines whether the dictionary contains the provided 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 marks the serialized cache as dirty when something is deleted. /// /// The boxed key to remove. /// /// /// public void Remove(object key) { IDictionary dictionary = _dictionary; bool existed = dictionary.Contains(key); dictionary.Remove(key); if (existed) { // Remove from tracked new keys if present if (key is TKey typedKey) { _newKeysOrder?.Remove(typedKey); } MarkSerializationCacheDirty(); } } /// /// Copies the dictionary contents into a non-generic array, matching . /// /// The destination array. /// The starting index inside . /// /// /// public void CopyTo(Array array, int index) { ((IDictionary)_dictionary).CopyTo(array, index); } /// /// Completes deserialization after data has been applied. /// /// Reserved for future use. public void OnDeserialization(object sender) { ((IDeserializationCallback)_dictionary).OnDeserialization(sender); } /// /// 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) { _dictionary.GetObjectData(info, context); } /// /// Allocation-free enumerator used by . /// /// /// .Enumerator enumerator = abilities.GetEnumerator(); /// while (enumerator.MoveNext()) /// { /// KeyValuePair entry = enumerator.Current; /// } /// ]]> /// public struct Enumerator : IEnumerator> { private Dictionary.Enumerator _enumerator; internal Enumerator(Dictionary.Enumerator enumerator) { _enumerator = enumerator; } public KeyValuePair Current => _enumerator.Current; object IEnumerator.Current => _enumerator.Current; /// /// Advances the enumerator to the next entry. /// /// true when another item is available. public bool MoveNext() { return _enumerator.MoveNext(); } /// /// Disposes of the underlying dictionary enumerator. /// public void Dispose() { _enumerator.Dispose(); } /// /// Reset is not supported because Unity serializable dictionaries mirror semantics. /// void IEnumerator.Reset() { throw new NotSupportedException("Reset is not supported."); } } } /// /// Factory and cache helpers for serializable dictionaries. /// public static class SerializableDictionary { [Serializable] [ProtoContract] public class Cache : SerializableDictionaryBase.Cache { [ProtoMember(1)] public T Data; } } /// /// Unity-friendly dictionary that keeps keys and values serialized for editor, ProtoBuf, and JSON pipelines. /// Use this when you need deterministic order, inspector editing, and runtime dictionary semantics without custom wrappers. /// /// /// weights = new SerializableDictionary(); /// /// private void Awake() /// { /// weights["Common"] = 80; /// weights["Rare"] = 15; /// weights["Legendary"] = 5; /// } /// } /// ]]> /// /// Dictionary key type. /// Dictionary value type. [Serializable] public class SerializableDictionary : SerializableDictionaryBase { /// /// Initializes an empty serializable dictionary whose values can be written directly to Unity serialization. /// /// /// weights = new SerializableDictionary(); /// weights["Common"] = 42; /// ]]> /// public SerializableDictionary() { } /// /// Initializes the serializable dictionary with items copied from an existing dictionary. /// /// The source entries to clone. /// /// seed = new Dictionary(); /// seed["Common"] = 42; /// SerializableDictionary weights = new SerializableDictionary(seed); /// ]]> /// public SerializableDictionary(IDictionary dictionary) : base(dictionary) { } /// /// Deserialization constructor used by pipelines. /// /// Serialized key/value data. /// Information about the serialization source. protected SerializableDictionary( SerializationInfo serializationInfo, StreamingContext streamingContext ) : base(serializationInfo, streamingContext) { } protected override TValue GetValue(TValue[] cache, int index) { return cache[index]; } protected override void SetValue(TValue[] cache, int index, TValue value) { cache[index] = value; } } internal static class SerializableDictionarySerializedPropertyNames { internal const string Keys = SerializableDictionary .SerializedPropertyNames .KeysNameInternal; internal const string Values = SerializableDictionary .SerializedPropertyNames .ValuesNameInternal; } /// /// Serializable dictionary that stores value data inside cache objects so complex or non-serializable runtime types can participate in Unity serialization. /// /// /// /// { /// } /// /// [Serializable] /// public sealed class ComplexValueDictionary /// : SerializableDictionary /// { /// } /// ]]> /// /// Dictionary key type. /// Dictionary value type. /// Serialized value cache type. [Serializable] public class SerializableDictionary : SerializableDictionaryBase where TValueCache : SerializableDictionary.Cache, new() { /// /// Initializes an empty serializable dictionary whose values are stored in cache objects. /// /// /// cache = /// new SerializableDictionary(); /// ]]> /// public SerializableDictionary() { } /// /// Initializes the dictionary with entries copied from an existing runtime dictionary. /// /// Entries to seed the serializable dictionary with. /// /// seed = new Dictionary(); /// SerializableDictionary cache = /// new SerializableDictionary(seed); /// ]]> /// public SerializableDictionary(IDictionary dictionary) : base(dictionary) { } /// /// Deserialization constructor required by the contract. /// /// Serialized representation of the dictionary. /// Context describing the serialization environment. protected SerializableDictionary( SerializationInfo serializationInfo, StreamingContext streamingContext ) : base(serializationInfo, streamingContext) { } 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 }; } } }