-
-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <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
Showing
9 changed files
with
705 additions
and
1 deletion.
There are no files selected for viewing
19 changes: 19 additions & 0 deletions
19
Assets/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
Assets/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs.meta
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.