Skip to content

Commit

Permalink
feat(syncvar): Add SyncDictionary (#602)
Browse files Browse the repository at this point in the history
* 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 <znorth@gmail.com>

* Simplify test syntax

Co-Authored-By: Katori <znorth@gmail.com>

* Simplify test syntax

Co-Authored-By: Katori <znorth@gmail.com>

* Simplify test syntax

Co-Authored-By: Katori <znorth@gmail.com>

* 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 <B, T> to <K, V> 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
  • Loading branch information
Katori authored and miwarnec committed Mar 24, 2019
1 parent fea46b8 commit 7d21bde
Show file tree
Hide file tree
Showing 9 changed files with 705 additions and 1 deletion.
19 changes: 19 additions & 0 deletions Assets/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// this class generates OnSerialize/OnDeserialize for SyncLists
using Mono.Cecil;
using Mono.Cecil.Cil;

namespace Mirror.Weaver
{
static class SyncDictionaryProcessor
{
/// <summary>
/// Generates serialization methods for synclists
/// </summary>
/// <param name="td">The synclist class</param>
public static void Process(TypeDefinition td)
{
SyncObjectProcessor.GenerateSerialization(td, 0, "SerializeKey", "DeserializeKey");
SyncObjectProcessor.GenerateSerialization(td, 1, "SerializeItem", "DeserializeItem");
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Assets/Mirror/Editor/Weaver/Weaver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -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;
Expand Down
303 changes: 303 additions & 0 deletions Assets/Mirror/Runtime/SyncDictionary.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;

namespace Mirror
{
[EditorBrowsable(EditorBrowsableState.Never)]
public abstract class SyncDictionary<K, V> : IDictionary<K, V>, SyncObject
{
public delegate void SyncDictionaryChanged(Operation op, K key, V item);

readonly Dictionary<K, V> 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<Change> Changes = new List<Change>();
// 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<K> Keys => m_Objects.Keys;

public ICollection<V> 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<K, V>();
}

public SyncDictionary(IEqualityComparer<K> eq)
{
m_Objects = new Dictionary<K, V>(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<K, V> 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<K, V> item) => Add(item.Key, item.Value);

public bool Contains(KeyValuePair<K, V> item)
{
return TryGetValue(item.Key, out V val) && EqualityComparer<V>.Default.Equals(val, item.Value);
}

public void CopyTo(KeyValuePair<K, V>[] 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<K,V> item in m_Objects)
{
array[i] = item;
i++;
}
}

public bool Remove(KeyValuePair<K, V> item)
{
bool result = m_Objects.Remove(item.Key);
if (result)
{
AddOperation(Operation.OP_REMOVE, item.Key, item.Value);
}
return result;
}

public IEnumerator<KeyValuePair<K, V>> GetEnumerator() => ((IDictionary<K, V>)m_Objects).GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => ((IDictionary<K, V>)m_Objects).GetEnumerator();
}
}
11 changes: 11 additions & 0 deletions Assets/Mirror/Runtime/SyncDictionary.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 7d21bde

Please sign in to comment.