// 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 };
}
}
}