123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 |
- using System;
- using System.Collections;
- using System.Collections.Generic;
- namespace Mirror
- {
- // Deprecated 2020-10-02
- [Obsolete("Use SyncList<string> instead")]
- public class SyncListString : SyncList<string> {}
- // Deprecated 2020-10-02
- [Obsolete("Use SyncList<float> instead")]
- public class SyncListFloat : SyncList<float> {}
- // Deprecated 2020-10-02
- [Obsolete("Use SyncList<int> instead")]
- public class SyncListInt : SyncList<int> {}
- // Deprecated 2020-10-02
- [Obsolete("Use SyncList<uint> instead")]
- public class SyncListUInt : SyncList<uint> {}
- // Deprecated 2020-10-02
- [Obsolete("Use SyncList<bool> instead")]
- public class SyncListBool : SyncList<bool> {}
- public class SyncList<T> : IList<T>, IReadOnlyList<T>, SyncObject
- {
- public delegate void SyncListChanged(Operation op, int itemIndex, T oldItem, T newItem);
- readonly IList<T> objects;
- readonly IEqualityComparer<T> comparer;
- public int Count => objects.Count;
- public bool IsReadOnly { get; private set; }
- public event SyncListChanged Callback;
- public enum Operation : byte
- {
- OP_ADD,
- OP_CLEAR,
- OP_INSERT,
- OP_REMOVEAT,
- OP_SET
- }
- struct Change
- {
- internal Operation operation;
- internal int index;
- 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;
- public SyncList() : this(EqualityComparer<T>.Default) {}
- public SyncList(IEqualityComparer<T> comparer)
- {
- this.comparer = comparer ?? EqualityComparer<T>.Default;
- objects = new List<T>();
- }
- public SyncList(IList<T> objects, IEqualityComparer<T> comparer = null)
- {
- this.comparer = comparer ?? EqualityComparer<T>.Default;
- this.objects = objects;
- }
- public bool IsDirty => changes.Count > 0;
- // throw away all the changes
- // this should be called after a successful sync
- public void Flush() => changes.Clear();
- public void Reset()
- {
- IsReadOnly = false;
- changes.Clear();
- changesAhead = 0;
- objects.Clear();
- }
- void AddOperation(Operation op, int itemIndex, T oldItem, T newItem)
- {
- if (IsReadOnly)
- {
- throw new InvalidOperationException("Synclists can only be modified at the server");
- }
- Change change = new Change
- {
- operation = op,
- index = itemIndex,
- item = newItem
- };
- changes.Add(change);
- Callback?.Invoke(op, itemIndex, oldItem, newItem);
- }
- public void OnSerializeAll(NetworkWriter writer)
- {
- // if init, write the full list content
- writer.WriteUInt((uint)objects.Count);
- for (int i = 0; i < objects.Count; i++)
- {
- T obj = objects[i];
- writer.Write(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.WriteUInt((uint)changes.Count);
- }
- public void OnSerializeDelta(NetworkWriter writer)
- {
- // write all the queued up changes
- writer.WriteUInt((uint)changes.Count);
- for (int i = 0; i < changes.Count; i++)
- {
- Change change = changes[i];
- writer.WriteByte((byte)change.operation);
- switch (change.operation)
- {
- case Operation.OP_ADD:
- writer.Write(change.item);
- break;
- case Operation.OP_CLEAR:
- break;
- case Operation.OP_REMOVEAT:
- writer.WriteUInt((uint)change.index);
- break;
- case Operation.OP_INSERT:
- case Operation.OP_SET:
- writer.WriteUInt((uint)change.index);
- writer.Write(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.ReadUInt();
- objects.Clear();
- changes.Clear();
- for (int i = 0; i < count; i++)
- {
- T obj = reader.Read<T>();
- 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.ReadUInt();
- }
- public void OnDeserializeDelta(NetworkReader reader)
- {
- // This list can now only be modified by synchronization
- IsReadOnly = true;
- int changesCount = (int)reader.ReadUInt();
- 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;
- int index = 0;
- T oldItem = default;
- T newItem = default;
- switch (operation)
- {
- case Operation.OP_ADD:
- newItem = reader.Read<T>();
- if (apply)
- {
- index = objects.Count;
- objects.Add(newItem);
- }
- break;
- case Operation.OP_CLEAR:
- if (apply)
- {
- objects.Clear();
- }
- break;
- case Operation.OP_INSERT:
- index = (int)reader.ReadUInt();
- newItem = reader.Read<T>();
- if (apply)
- {
- objects.Insert(index, newItem);
- }
- break;
- case Operation.OP_REMOVEAT:
- index = (int)reader.ReadUInt();
- if (apply)
- {
- oldItem = objects[index];
- objects.RemoveAt(index);
- }
- break;
- case Operation.OP_SET:
- index = (int)reader.ReadUInt();
- newItem = reader.Read<T>();
- if (apply)
- {
- oldItem = objects[index];
- objects[index] = newItem;
- }
- break;
- }
- if (apply)
- {
- Callback?.Invoke(operation, index, oldItem, newItem);
- }
- // we just skipped this change
- else
- {
- changesAhead--;
- }
- }
- }
- public void Add(T item)
- {
- objects.Add(item);
- AddOperation(Operation.OP_ADD, objects.Count - 1, default, item);
- }
- public void AddRange(IEnumerable<T> range)
- {
- foreach (T entry in range)
- {
- Add(entry);
- }
- }
- public void Clear()
- {
- objects.Clear();
- AddOperation(Operation.OP_CLEAR, 0, default, default);
- }
- public bool Contains(T item) => IndexOf(item) >= 0;
- public void CopyTo(T[] array, int index) => objects.CopyTo(array, index);
- public int IndexOf(T item)
- {
- for (int i = 0; i < objects.Count; ++i)
- if (comparer.Equals(item, objects[i]))
- return i;
- return -1;
- }
- public int FindIndex(Predicate<T> match)
- {
- for (int i = 0; i < objects.Count; ++i)
- if (match(objects[i]))
- return i;
- return -1;
- }
- public T Find(Predicate<T> match)
- {
- int i = FindIndex(match);
- return (i != -1) ? objects[i] : default;
- }
- public List<T> FindAll(Predicate<T> match)
- {
- List<T> results = new List<T>();
- for (int i = 0; i < objects.Count; ++i)
- if (match(objects[i]))
- results.Add(objects[i]);
- return results;
- }
- public void Insert(int index, T item)
- {
- objects.Insert(index, item);
- AddOperation(Operation.OP_INSERT, index, default, item);
- }
- public void InsertRange(int index, IEnumerable<T> range)
- {
- foreach (T entry in range)
- {
- Insert(index, entry);
- index++;
- }
- }
- public bool Remove(T item)
- {
- int index = IndexOf(item);
- bool result = index >= 0;
- if (result)
- {
- RemoveAt(index);
- }
- return result;
- }
- public void RemoveAt(int index)
- {
- T oldItem = objects[index];
- objects.RemoveAt(index);
- AddOperation(Operation.OP_REMOVEAT, index, oldItem, default);
- }
- public int RemoveAll(Predicate<T> match)
- {
- List<T> toRemove = new List<T>();
- for (int i = 0; i < objects.Count; ++i)
- if (match(objects[i]))
- toRemove.Add(objects[i]);
- foreach (T entry in toRemove)
- {
- Remove(entry);
- }
- return toRemove.Count;
- }
- public T this[int i]
- {
- get => objects[i];
- set
- {
- if (!comparer.Equals(objects[i], value))
- {
- T oldItem = objects[i];
- objects[i] = value;
- AddOperation(Operation.OP_SET, i, oldItem, value);
- }
- }
- }
- public Enumerator GetEnumerator() => new Enumerator(this);
- IEnumerator<T> IEnumerable<T>.GetEnumerator() => new Enumerator(this);
- IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this);
- // default Enumerator allocates. we need a custom struct Enumerator to
- // not allocate on the heap.
- // (System.Collections.Generic.List<T> source code does the same)
- //
- // benchmark:
- // uMMORPG with 800 monsters, Skills.GetHealthBonus() which runs a
- // foreach on skills SyncList:
- // before: 81.2KB GC per frame
- // after: 0KB GC per frame
- // => this is extremely important for MMO scale networking
- public struct Enumerator : IEnumerator<T>
- {
- readonly SyncList<T> list;
- int index;
- public T Current { get; private set; }
- public Enumerator(SyncList<T> list)
- {
- this.list = list;
- index = -1;
- Current = default;
- }
- public bool MoveNext()
- {
- if (++index >= list.Count)
- {
- return false;
- }
- Current = list[index];
- return true;
- }
- public void Reset() => index = -1;
- object IEnumerator.Current => Current;
- public void Dispose() {}
- }
- }
- }
|