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