From 7d21bded9a521e53acc212b11a756d41e1b4218c Mon Sep 17 00:00:00 2001 From: Zac North Date: Sun, 24 Mar 2019 05:18:31 -0400 Subject: [PATCH] feat(syncvar): Add SyncDictionary (#602) * Added basic SyncDictionary support, no support for structs yet * Fixed TryGetValue usage * Removed extraneous hardcoded SyncDictionary type * Added a couple basic tests, more coming * Added 4 more tests * Added two tests and SyncDictionary now bubbles item to Callback on Remove (both Remove cases) * Added the remainder of tests * Added basic documentation about SyncDictionaries on StateSync.md page * Simplify test syntax Co-Authored-By: Katori * Simplify test syntax Co-Authored-By: Katori * Simplify test syntax Co-Authored-By: Katori * Simplify test syntax Co-Authored-By: Katori * Remove null-check when setting value directly (and updated expected test behaviour) * fix: Provide default implementation for SyncDictionary serializers * feat: Add Weaver support for syncdictionary * Fix minor issue with Set code and made test use Weaved serialization instead of manual * Added a new test for bare set (non-overwrite) * Added another test for BareSetNull and cleaned up some tests * Updated SyncDictionary documentation on StateSync.md * Update docs with SyncDictionary info * Update SyncDictionary docs wording * docs: document the types and better example * Add two SyncDictionary constructors * Removed unnecessary initialization * Style fixes * - Merged many operation cases - Fixed Contains method - Added new test to test contains (and flag its earlier improper usage) - Use PackedUInt32 instead of int for Changes and Counts * - Simplify "default" syntax - Use Rodol's remove method (faster) - Don't use var * Removed unnecessary newline, renamed to per vis2k, corrected wording of InvalidOperationException on ReadOnly AddOp * Code simplification, style fixes, docs example style fixes, newly improved implementation for CopyTo that fails gracefully --- .../Processors/SyncDictionaryProcessor.cs | 19 ++ .../SyncDictionaryProcessor.cs.meta | 11 + Assets/Mirror/Editor/Weaver/Weaver.cs | 8 + Assets/Mirror/Runtime/SyncDictionary.cs | 303 ++++++++++++++++++ Assets/Mirror/Runtime/SyncDictionary.cs.meta | 11 + Assets/Mirror/Tests/SyncDictionaryTest.cs | 233 ++++++++++++++ .../Mirror/Tests/SyncDictionaryTest.cs.meta | 11 + docs/Classes/SyncDictionary.md | 54 +++- docs/Concepts/StateSync.md | 56 ++++ 9 files changed, 705 insertions(+), 1 deletion(-) create mode 100644 Assets/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs create mode 100644 Assets/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs.meta create mode 100644 Assets/Mirror/Runtime/SyncDictionary.cs create mode 100644 Assets/Mirror/Runtime/SyncDictionary.cs.meta create mode 100644 Assets/Mirror/Tests/SyncDictionaryTest.cs create mode 100644 Assets/Mirror/Tests/SyncDictionaryTest.cs.meta diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs new file mode 100644 index 000000000..97c8efb9a --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs @@ -0,0 +1,19 @@ +// this class generates OnSerialize/OnDeserialize for SyncLists +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace Mirror.Weaver +{ + static class SyncDictionaryProcessor + { + /// + /// Generates serialization methods for synclists + /// + /// The synclist class + public static void Process(TypeDefinition td) + { + SyncObjectProcessor.GenerateSerialization(td, 0, "SerializeKey", "DeserializeKey"); + SyncObjectProcessor.GenerateSerialization(td, 1, "SerializeItem", "DeserializeItem"); + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs.meta new file mode 100644 index 000000000..0a7c2aa5c --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 29e4a45f69822462ab0b15adda962a29 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Weaver.cs b/Assets/Mirror/Editor/Weaver/Weaver.cs index 5355ba7c2..fb555e960 100644 --- a/Assets/Mirror/Editor/Weaver/Weaver.cs +++ b/Assets/Mirror/Editor/Weaver/Weaver.cs @@ -64,6 +64,7 @@ class Weaver public static TypeReference MessageBaseType; public static TypeReference SyncListType; + public static TypeReference SyncDictionaryType; public static MethodReference NetworkBehaviourDirtyBitsReference; public static TypeReference NetworkClientType; @@ -1125,6 +1126,7 @@ static void SetupTargetTypes() MessageBaseType = NetAssembly.MainModule.GetType("Mirror.MessageBase"); SyncListType = NetAssembly.MainModule.GetType("Mirror.SyncList`1"); + SyncDictionaryType = NetAssembly.MainModule.GetType("Mirror.SyncDictionary`2"); NetworkBehaviourDirtyBitsReference = Resolvers.ResolveProperty(NetworkBehaviourType, CurrentAssembly, "syncVarDirtyBits"); @@ -1364,6 +1366,12 @@ static bool CheckSyncList(TypeDefinition td) didWork = true; break; } + else if (parent.FullName.StartsWith(SyncDictionaryType.FullName)) + { + SyncDictionaryProcessor.Process(td); + didWork = true; + break; + } try { parent = parent.Resolve().BaseType; diff --git a/Assets/Mirror/Runtime/SyncDictionary.cs b/Assets/Mirror/Runtime/SyncDictionary.cs new file mode 100644 index 000000000..dad5f50ce --- /dev/null +++ b/Assets/Mirror/Runtime/SyncDictionary.cs @@ -0,0 +1,303 @@ +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Mirror +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class SyncDictionary : IDictionary, SyncObject + { + public delegate void SyncDictionaryChanged(Operation op, K key, V item); + + readonly Dictionary m_Objects; + + public int Count => m_Objects.Count; + public bool IsReadOnly { get; private set; } + public event SyncDictionaryChanged Callback; + + public enum Operation : byte + { + OP_ADD, + OP_CLEAR, + OP_REMOVE, + OP_SET, + OP_DIRTY + } + + struct Change + { + internal Operation operation; + internal K key; + internal V item; + } + + readonly List Changes = new List(); + // how many changes we need to ignore + // this is needed because when we initialize the list, + // we might later receive changes that have already been applied + // so we need to skip them + int changesAhead = 0; + + protected virtual void SerializeKey(NetworkWriter writer, K item) {} + protected virtual void SerializeItem(NetworkWriter writer, V item) {} + protected virtual K DeserializeKey(NetworkReader reader) => default; + protected virtual V DeserializeItem(NetworkReader reader) => default; + + public bool IsDirty => Changes.Count > 0; + + public ICollection Keys => m_Objects.Keys; + + public ICollection Values => m_Objects.Values; + + // throw away all the changes + // this should be called after a successfull sync + public void Flush() => Changes.Clear(); + + public SyncDictionary() + { + m_Objects = new Dictionary(); + } + + public SyncDictionary(IEqualityComparer eq) + { + m_Objects = new Dictionary(eq); + } + + void AddOperation(Operation op, K key, V item) + { + if (IsReadOnly) + { + throw new System.InvalidOperationException("SyncDictionaries can only be modified by the server"); + } + + Change change = new Change + { + operation = op, + key = key, + item = item + }; + + Changes.Add(change); + + Callback?.Invoke(op, key, item); + } + + public void OnSerializeAll(NetworkWriter writer) + { + // if init, write the full list content + writer.WritePackedUInt32((uint)m_Objects.Count); + + foreach (KeyValuePair syncItem in m_Objects) + { + SerializeKey(writer, syncItem.Key); + SerializeItem(writer, syncItem.Value); + } + + // all changes have been applied already + // thus the client will need to skip all the pending changes + // or they would be applied again. + // So we write how many changes are pending + writer.WritePackedUInt32((uint)Changes.Count); + } + + public void OnSerializeDelta(NetworkWriter writer) + { + // write all the queued up changes + writer.WritePackedUInt32((uint)Changes.Count); + + for (int i = 0; i < Changes.Count; i++) + { + Change change = Changes[i]; + writer.Write((byte)change.operation); + + switch (change.operation) + { + case Operation.OP_ADD: + case Operation.OP_REMOVE: + case Operation.OP_SET: + case Operation.OP_DIRTY: + SerializeKey(writer, change.key); + SerializeItem(writer, change.item); + break; + case Operation.OP_CLEAR: + break; + } + } + } + + public void OnDeserializeAll(NetworkReader reader) + { + // This list can now only be modified by synchronization + IsReadOnly = true; + + // if init, write the full list content + int count = (int)reader.ReadPackedUInt32(); + + m_Objects.Clear(); + Changes.Clear(); + + for (int i = 0; i < count; i++) + { + K key = DeserializeKey(reader); + V obj = DeserializeItem(reader); + m_Objects.Add(key, obj); + } + + // We will need to skip all these changes + // the next time the list is synchronized + // because they have already been applied + changesAhead = (int)reader.ReadPackedUInt32(); + } + + public void OnDeserializeDelta(NetworkReader reader) + { + // This list can now only be modified by synchronization + IsReadOnly = true; + + int changesCount = (int)reader.ReadPackedUInt32(); + + for (int i = 0; i < changesCount; i++) + { + Operation operation = (Operation)reader.ReadByte(); + + // apply the operation only if it is a new change + // that we have not applied yet + bool apply = changesAhead == 0; + K key = default; + V item = default; + + switch (operation) + { + case Operation.OP_ADD: + case Operation.OP_SET: + case Operation.OP_DIRTY: + key = DeserializeKey(reader); + item = DeserializeItem(reader); + if (apply) + { + m_Objects[key] = item; + } + break; + + case Operation.OP_CLEAR: + if (apply) + { + m_Objects.Clear(); + } + break; + + case Operation.OP_REMOVE: + key = DeserializeKey(reader); + item = DeserializeItem(reader); + if (apply) + { + m_Objects.Remove(key); + } + break; + } + + if (apply) + { + Callback?.Invoke(operation, key, item); + } + // we just skipped this change + else + { + changesAhead--; + } + } + } + + public void Clear() + { + m_Objects.Clear(); + AddOperation(Operation.OP_CLEAR, default, default); + } + + public bool ContainsKey(K key) => m_Objects.ContainsKey(key); + + public bool Remove(K key) + { + if (m_Objects.TryGetValue(key, out V item) && m_Objects.Remove(key)) + { + AddOperation(Operation.OP_REMOVE, key, item); + return true; + } + return false; + } + + public void Dirty(K index) + { + AddOperation(Operation.OP_DIRTY, index, m_Objects[index]); + } + + public V this[K i] + { + get => m_Objects[i]; + set + { + if (TryGetValue(i, out V val)) + { + AddOperation(Operation.OP_SET, i, value); + } + else + { + AddOperation(Operation.OP_ADD, i, value); + } + m_Objects[i] = value; + } + } + + public bool TryGetValue(K key, out V value) => m_Objects.TryGetValue(key, out value); + + public void Add(K key, V value) + { + m_Objects.Add(key, value); + AddOperation(Operation.OP_ADD, key, value); + } + + public void Add(KeyValuePair item) => Add(item.Key, item.Value); + + public bool Contains(KeyValuePair item) + { + return TryGetValue(item.Key, out V val) && EqualityComparer.Default.Equals(val, item.Value); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + if (array == null) + { + throw new System.ArgumentNullException("Array Is Null"); + } + if (arrayIndex < 0 || arrayIndex > array.Length) + { + throw new System.ArgumentOutOfRangeException("Array Index Out of Range"); + } + if (array.Length - arrayIndex < Count) + { + throw new System.ArgumentException("The number of items in the SyncDictionary is greater than the available space from arrayIndex to the end of the destination array"); + } + + int i = arrayIndex; + foreach (KeyValuePair item in m_Objects) + { + array[i] = item; + i++; + } + } + + public bool Remove(KeyValuePair item) + { + bool result = m_Objects.Remove(item.Key); + if (result) + { + AddOperation(Operation.OP_REMOVE, item.Key, item.Value); + } + return result; + } + + public IEnumerator> GetEnumerator() => ((IDictionary)m_Objects).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IDictionary)m_Objects).GetEnumerator(); + } +} diff --git a/Assets/Mirror/Runtime/SyncDictionary.cs.meta b/Assets/Mirror/Runtime/SyncDictionary.cs.meta new file mode 100644 index 000000000..9b4ff53db --- /dev/null +++ b/Assets/Mirror/Runtime/SyncDictionary.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4b346c49cfdb668488a364c3023590e2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Tests/SyncDictionaryTest.cs b/Assets/Mirror/Tests/SyncDictionaryTest.cs new file mode 100644 index 000000000..76ee938a5 --- /dev/null +++ b/Assets/Mirror/Tests/SyncDictionaryTest.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; + +namespace Mirror.Tests +{ + [TestFixture] + public class SyncDictionaryTest + { + public class SyncDictionaryIntString : SyncDictionary {} + + SyncDictionaryIntString serverSyncDictionary; + SyncDictionaryIntString clientSyncDictionary; + + private void SerializeAllTo(T fromList, T toList) where T : SyncObject + { + NetworkWriter writer = new NetworkWriter(); + fromList.OnSerializeAll(writer); + NetworkReader reader = new NetworkReader(writer.ToArray()); + toList.OnDeserializeAll(reader); + } + + private void SerializeDeltaTo(T fromList, T toList) where T : SyncObject + { + NetworkWriter writer = new NetworkWriter(); + fromList.OnSerializeDelta(writer); + NetworkReader reader = new NetworkReader(writer.ToArray()); + toList.OnDeserializeDelta(reader); + fromList.Flush(); + } + + [SetUp] + public void SetUp() + { + serverSyncDictionary = new SyncDictionaryIntString(); + clientSyncDictionary = new SyncDictionaryIntString(); + + // add some data to the list + serverSyncDictionary.Add(0, "Hello"); + serverSyncDictionary.Add(1, "World"); + serverSyncDictionary.Add(2, "!"); + SerializeAllTo(serverSyncDictionary, clientSyncDictionary); + } + + [Test] + public void TestInit() + { + Dictionary comparer = new Dictionary + { + [0] = "Hello", + [1] = "World", + [2] = "!" + }; + Assert.That(clientSyncDictionary[0], Is.EqualTo("Hello")); + Assert.That(clientSyncDictionary, Is.EquivalentTo(comparer)); + } + + [Test] + public void TestAdd() + { + serverSyncDictionary.Add(4, "yay"); + SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); + Assert.That(clientSyncDictionary.ContainsKey(4), Is.EqualTo(true)); + Assert.That(clientSyncDictionary[4], Is.EqualTo("yay")); + } + + [Test] + public void TestClear() + { + serverSyncDictionary.Clear(); + SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); + Assert.That(serverSyncDictionary, Is.EquivalentTo(new SyncDictionaryIntString())); + } + + [Test] + public void TestSet() + { + serverSyncDictionary[1] = "yay"; + SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); + Assert.That(clientSyncDictionary.ContainsKey(1)); + Assert.That(clientSyncDictionary[1], Is.EqualTo("yay")); + } + + [Test] + public void TestBareSet() + { + serverSyncDictionary[4] = "yay"; + SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); + Assert.That(clientSyncDictionary.ContainsKey(4)); + Assert.That(clientSyncDictionary[4], Is.EqualTo("yay")); + } + + [Test] + public void TestBareSetNull() + { + serverSyncDictionary[4] = null; + SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); + Assert.That(clientSyncDictionary[4], Is.Null); + Assert.That(clientSyncDictionary.ContainsKey(4)); + } + + [Test] + public void TestConsecutiveSet() + { + serverSyncDictionary[1] = "yay"; + serverSyncDictionary[1] = "world"; + SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); + Assert.That(clientSyncDictionary[1], Is.EqualTo("world")); + } + + [Test] + public void TestNullSet() + { + serverSyncDictionary[1] = null; + SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); + Assert.That(clientSyncDictionary.ContainsKey(1)); + Assert.That(clientSyncDictionary[1], Is.Null); + } + + [Test] + public void TestRemove() + { + serverSyncDictionary.Remove(1); + SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); + Assert.That(clientSyncDictionary.ContainsKey(1), Is.False); + } + + [Test] + public void TestMultSync() + { + serverSyncDictionary.Add(10, "1"); + SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); + // add some delta and see if it applies + serverSyncDictionary.Add(11, "2"); + SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); + Assert.That(clientSyncDictionary.ContainsKey(10)); + Assert.That(clientSyncDictionary[10], Is.EqualTo("1")); + Assert.That(clientSyncDictionary.ContainsKey(11)); + Assert.That(clientSyncDictionary[11], Is.EqualTo("2")); + } + + [Test] + public void TestContains() + { + Assert.That(!clientSyncDictionary.Contains(new KeyValuePair(2, "Hello"))); + serverSyncDictionary[2] = "Hello"; + SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); + Assert.That(clientSyncDictionary.Contains(new KeyValuePair(2, "Hello"))); + } + + [Test] + public void CallbackTest() + { + bool called = false; + clientSyncDictionary.Callback += (op, index, item) => + { + called = true; + + Assert.That(op, Is.EqualTo(SyncDictionaryIntString.Operation.OP_ADD)); + Assert.That(index, Is.EqualTo(3)); + Assert.That(item, Is.EqualTo("yay")); + }; + serverSyncDictionary.Add(3, "yay"); + SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); + Assert.That(called, Is.True); + } + + [Test] + public void CallbackRemoveTest() + { + bool called = false; + clientSyncDictionary.Callback += (op, key, item) => + { + called = true; + Assert.That(op, Is.EqualTo(SyncDictionaryIntString.Operation.OP_REMOVE)); + Assert.That(item, Is.EqualTo("World")); + }; + serverSyncDictionary.Remove(1); + SerializeDeltaTo(serverSyncDictionary, clientSyncDictionary); + Assert.That(called, Is.True); + } + + [Test] + public void CountTest() + { + Assert.That(serverSyncDictionary.Count, Is.EqualTo(3)); + } + + [Test] + public void ReadOnlyTest() + { + Assert.That(serverSyncDictionary.IsReadOnly, Is.False); + } + + [Test] + public void DirtyTest() + { + SyncDictionaryIntString serverList = new SyncDictionaryIntString(); + SyncDictionaryIntString clientList = new SyncDictionaryIntString(); + + // nothing to send + Assert.That(serverList.IsDirty, Is.False); + + // something has changed + serverList.Add(15, "yay"); + Assert.That(serverList.IsDirty, Is.True); + SerializeDeltaTo(serverList, clientList); + + // data has been flushed, should go back to clear + Assert.That(serverList.IsDirty, Is.False); + } + + [Test] + public void ReadonlyTest() + { + SyncDictionaryIntString serverList = new SyncDictionaryIntString(); + SyncDictionaryIntString clientList = new SyncDictionaryIntString(); + + // data has been flushed, should go back to clear + Assert.That(clientList.IsReadOnly, Is.False); + + serverList.Add(20, "yay"); + serverList.Add(30, "hello"); + serverList.Add(35, "world"); + SerializeDeltaTo(serverList, clientList); + + // client list should now lock itself, trying to modify it + // should produce an InvalidOperationException + Assert.That(clientList.IsReadOnly, Is.True); + Assert.Throws(() => clientList.Add(50, "fail")); + } + } +} diff --git a/Assets/Mirror/Tests/SyncDictionaryTest.cs.meta b/Assets/Mirror/Tests/SyncDictionaryTest.cs.meta new file mode 100644 index 000000000..552e96ded --- /dev/null +++ b/Assets/Mirror/Tests/SyncDictionaryTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cadf48c3662efac4181b91f5c9c88774 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/docs/Classes/SyncDictionary.md b/docs/Classes/SyncDictionary.md index 40d511447..538eaf0ec 100644 --- a/docs/Classes/SyncDictionary.md +++ b/docs/Classes/SyncDictionary.md @@ -1,3 +1,55 @@ # SyncDictionary -Need description and code samples for SyncDictionary. +A `SyncDictionary` is an associative array containing an unordered list of key, value pairs. Keys and values can be of the following types: + +- Basic type (byte, int, float, string, UInt64, etc) +- Built-in Unity math type (Vector3, Quaternion, etc) +- NetworkIdentity +- GameObject with a NetworkIdentity component attached. +- Struct with any of the above + +SyncDictionaries work much like [SyncLists](SyncLists): when you make a change on the server the change is propagated to all clients and the Callback is called. + +To use it, create a class that derives from `SyncDictionary` for your specific type. This is necesary because the Weaver will add methods to that class. Then add a field to your NetworkBehaviour class. + +## Simple Example + +```cs +using UnityEngine; +using Mirror; + +public class ExamplePlayer : NetworkBehaviour +{ + public class SyncDictionaryStringItem : SyncDictionary {} + + public struct Item + { + public string name; + public int hitPoints; + public int durability; + } + + public SyncDictionaryStringItem Equipment = new SyncDictionaryStringItem(); + + public void OnStartServer() + { + Equipment.Add("head", new Item { name = "Helmet", hitPoints = 10, durability = 20 }); + Equipment.Add("body", new Item { name = "Epic Armor", hitPoints = 50, durability = 50 }); + Equipment.Add("feet", new Item { name = "Sneakers", hitPoints = 3, durability = 40 }); + Equipment.Add("hands", new Item { name = "Sword", hitPoints = 30, durability = 15 }); + } + + private void OnStartClient() + { + // Equipment is already populated with anything the server set up + // but we can subscribe to the callback in case it is updated later on + Equipment.Callback += OnEquipmentChange; + } + + private void OnEquipmentChange(SyncDictionaryStringItem.Operation op, string key, Item item) + { + // equipment changed, perhaps update the gameobject + Debug.Log(op + " - " + key); + } +} +``` diff --git a/docs/Concepts/StateSync.md b/docs/Concepts/StateSync.md index 87c5e4a63..c1d68308d 100644 --- a/docs/Concepts/StateSync.md +++ b/docs/Concepts/StateSync.md @@ -71,3 +71,59 @@ public class MyScript : NetworkBehaviour } } ``` + +## SyncDictionaries + +A `SyncDictionary` is an associative array containing an unordered list of key, value pairs. Keys and values can be of the following types: + +- Basic type (byte, int, float, string, UInt64, etc) +- Built-in Unity math type (Vector3, Quaternion, etc) +- NetworkIdentity +- GameObject with a NetworkIdentity component attached. +- Struct with any of the above + +SyncDictionaries work much like [SyncLists](SyncLists): when you make a change on the server the change is propagated to all clients and the Callback is called. + +To use it, create a class that derives from `SyncDictionary` for your specific type. This is necesary because the Weaver will add methods to that class. Then add a field to your NetworkBehaviour class. + +### Simple Example + +```cs +using UnityEngine; +using Mirror; + +public class ExamplePlayer : NetworkBehaviour +{ + public class SyncDictionaryStringItem : SyncDictionary {} + + public struct Item + { + public string name; + public int hitPoints; + public int durability; + } + + public SyncDictionaryStringItem Equipment = new SyncDictionaryStringItem(); + + public void OnStartServer() + { + Equipment.Add("head", new Item { name = "Helmet", hitPoints = 10, durability = 20 }); + Equipment.Add("body", new Item { name = "Epic Armor", hitPoints = 50, durability = 50 }); + Equipment.Add("feet", new Item { name = "Sneakers", hitPoints = 3, durability = 40 }); + Equipment.Add("hands", new Item { name = "Sword", hitPoints = 30, durability = 15 }); + } + + private void OnStartClient() + { + // Equipment is already populated with anything the server set up + // but we can subscribe to the callback in case it is updated later on + Equipment.Callback += OnEquipmentChange; + } + + private void OnEquipmentChange(SyncDictionaryStringItem.Operation op, string key, Item item) + { + // equipment changed, perhaps update the gameobject + Debug.Log(op + " - " + key); + } +} +```