Skip to content

Commit

Permalink
feat: Add SyncHashSet and SyncSortedSet (#685)
Browse files Browse the repository at this point in the history
* feat: Add SyncHashSet and SyncSorted set
  • Loading branch information
paulpach committed Mar 30, 2019
1 parent 6ed7bde commit 695979e
Show file tree
Hide file tree
Showing 9 changed files with 779 additions and 6 deletions.
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 SyncSetType;
public static TypeReference SyncDictionaryType;

public static MethodReference NetworkBehaviourDirtyBitsReference;
Expand Down Expand Up @@ -1130,6 +1131,7 @@ static void SetupTargetTypes()

MessageBaseType = NetAssembly.MainModule.GetType("Mirror.MessageBase");
SyncListType = NetAssembly.MainModule.GetType("Mirror.SyncList`1");
SyncSetType = NetAssembly.MainModule.GetType("Mirror.SyncSet`1");
SyncDictionaryType = NetAssembly.MainModule.GetType("Mirror.SyncDictionary`2");

NetworkBehaviourDirtyBitsReference = Resolvers.ResolveProperty(NetworkBehaviourType, CurrentAssembly, "syncVarDirtyBits");
Expand Down Expand Up @@ -1374,6 +1376,12 @@ static bool CheckSyncList(TypeDefinition td)
didWork = true;
break;
}
else if (parent.FullName.StartsWith(SyncSetType.FullName))
{
SyncListProcessor.Process(td);
didWork = true;
break;
}
else if (parent.FullName.StartsWith(SyncDictionaryType.FullName))
{
SyncDictionaryProcessor.Process(td);
Expand Down
330 changes: 330 additions & 0 deletions Assets/Mirror/Runtime/SyncSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;

namespace Mirror
{

[EditorBrowsable(EditorBrowsableState.Never)]
public abstract class SyncSet<T> : ISet<T>, SyncObject
{
public delegate void SyncSetChanged(Operation op, T item);

readonly ISet<T> objects;

public int Count => objects.Count;
public bool IsReadOnly { get; private set; }
public event SyncSetChanged Callback;

public enum Operation : byte
{
OP_ADD,
OP_CLEAR,
OP_REMOVE
}

struct Change
{
internal Operation operation;
internal T 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 SyncSet(ISet<T> objects)
{
this.objects = objects;
}

protected virtual void SerializeItem(NetworkWriter writer, T item) {}
protected virtual T DeserializeItem(NetworkReader reader) => default;

public bool IsDirty => changes.Count > 0;

// throw away all the changes
// this should be called after a successfull sync
public void Flush() => changes.Clear();

void AddOperation(Operation op, T item)
{
if (IsReadOnly)
{
throw new InvalidOperationException("SyncSets can only be modified at the server");
}

Change change = new Change
{
operation = op,
item = item
};

changes.Add(change);

Callback?.Invoke(op, item);
}

void AddOperation(Operation op) => AddOperation(op, default);

public void OnSerializeAll(NetworkWriter writer)
{
// if init, write the full list content
writer.WritePackedUInt32((uint)objects.Count);

foreach (T obj in objects)
{
SerializeItem(writer, obj);
}

// 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:
SerializeItem(writer, change.item);
break;

case Operation.OP_CLEAR:
break;

case Operation.OP_REMOVE:
SerializeItem(writer, change.item);
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();

objects.Clear();
changes.Clear();

for (int i = 0; i < count; i++)
{
T obj = DeserializeItem(reader);
objects.Add(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;
T item = default;

switch (operation)
{
case Operation.OP_ADD:
item = DeserializeItem(reader);
if (apply)
{
objects.Add(item);
}
break;

case Operation.OP_CLEAR:
if (apply)
{
objects.Clear();
}
break;

case Operation.OP_REMOVE:
item = DeserializeItem(reader);
if (apply)
{
objects.Remove(item);
}
break;
}

if (apply)
{
Callback?.Invoke(operation, item);
}
// we just skipped this change
else
{
changesAhead--;
}
}
}

public bool Add(T item)
{
if (objects.Add(item))
{
AddOperation(Operation.OP_ADD, item);
return true;
}
return false;
}

void ICollection<T>.Add(T item)
{
if (objects.Add(item))
{
AddOperation(Operation.OP_ADD, item);
}
}

public void Clear()
{
objects.Clear();
AddOperation(Operation.OP_CLEAR);
}

public bool Contains(T item) => objects.Contains(item);

public void CopyTo(T[] array, int index) => objects.CopyTo(array, index);

public bool Remove(T item)
{
if (objects.Remove(item))
{
AddOperation(Operation.OP_REMOVE, item);
return true;
}
return false;
}

public IEnumerator<T> GetEnumerator() => objects.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

public void ExceptWith(IEnumerable<T> other)
{
if (other == this)
{
Clear();
return;
}

// remove every element in other from this
foreach (T element in other)
{
Remove(element);
}
}

public void IntersectWith(IEnumerable<T> other)
{
if (other is ISet<T> otherSet)
{
IntersectWithSet(otherSet);
}
else
{
HashSet<T> otherAsSet = new HashSet<T>(other);
IntersectWithSet(otherAsSet);
}
}

void IntersectWithSet(ISet<T> otherSet)
{
List<T> elements = new List<T>(objects);

foreach (T element in elements)
{
if (!otherSet.Contains(element))
{
Remove(element);
}
}
}

public bool IsProperSubsetOf(IEnumerable<T> other) => objects.IsProperSubsetOf(other);

public bool IsProperSupersetOf(IEnumerable<T> other) => objects.IsProperSupersetOf(other);

public bool IsSubsetOf(IEnumerable<T> other) => objects.IsSubsetOf(other);

public bool IsSupersetOf(IEnumerable<T> other) => objects.IsSupersetOf(other);

public bool Overlaps(IEnumerable<T> other) => objects.Overlaps(other);

public bool SetEquals(IEnumerable<T> other) => objects.SetEquals(other);

public void SymmetricExceptWith(IEnumerable<T> other)
{
if (other == this)
{
Clear();
}
else
{
foreach (T element in other)
{
if (!Remove(element))
{
Add(element);
}
}
}
}

public void UnionWith(IEnumerable<T> other)
{
if (other != this)
{
foreach (T element in other)
{
Add(element);
}
}
}
}

public abstract class SyncHashSet<T> : SyncSet<T>
{
protected SyncHashSet() : base(new HashSet<T>()) {}
}

public abstract class SyncSortedSet<T> : SyncSet<T>
{
protected SyncSortedSet() : base(new SortedSet<T>()) {}

protected SyncSortedSet(IComparer<T> comparer) : base (new SortedSet<T>(comparer)) {}
}
}
11 changes: 11 additions & 0 deletions Assets/Mirror/Runtime/SyncSet.cs.meta

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

0 comments on commit 695979e

Please sign in to comment.