using System; using System.Collections.Generic; using Unity.Collections; using UnityEngine; namespace Unity.Netcode { /// /// Event based NetworkVariable container for syncing Lists /// /// The type for the list public class NetworkList : NetworkVariableBase where T : unmanaged, IEquatable { private NativeList m_List = new NativeList(64, Allocator.Persistent); private NativeList> m_DirtyEvents = new NativeList>(64, Allocator.Persistent); /// /// Delegate type for list changed event /// /// Struct containing information about the change event public delegate void OnListChangedDelegate(NetworkListEvent changeEvent); /// /// The callback to be invoked when the list gets changed /// public event OnListChangedDelegate OnListChanged; /// /// Constructor method for /// public NetworkList() { } /// /// /// /// public NetworkList(IEnumerable values = default, NetworkVariableReadPermission readPerm = DefaultReadPerm, NetworkVariableWritePermission writePerm = DefaultWritePerm) : base(readPerm, writePerm) { // allow null IEnumerable to mean "no values" if (values != null) { foreach (var value in values) { m_List.Add(value); } } } /// public override void ResetDirty() { base.ResetDirty(); if (m_DirtyEvents.Length > 0) { m_DirtyEvents.Clear(); } } /// public override bool IsDirty() { // we call the base class to allow the SetDirty() mechanism to work return base.IsDirty() || m_DirtyEvents.Length > 0; } internal void MarkNetworkObjectDirty() { if (m_NetworkBehaviour == null) { Debug.LogWarning($"NetworkList is written to, but doesn't know its NetworkBehaviour yet. " + "Are you modifying a NetworkList before the NetworkObject is spawned?"); return; } m_NetworkBehaviour.NetworkManager.MarkNetworkObjectDirty(m_NetworkBehaviour.NetworkObject); } /// public override void WriteDelta(FastBufferWriter writer) { if (base.IsDirty()) { writer.WriteValueSafe((ushort)1); writer.WriteValueSafe(NetworkListEvent.EventType.Full); WriteField(writer); return; } writer.WriteValueSafe((ushort)m_DirtyEvents.Length); for (int i = 0; i < m_DirtyEvents.Length; i++) { var element = m_DirtyEvents.ElementAt(i); writer.WriteValueSafe(element.Type); switch (element.Type) { case NetworkListEvent.EventType.Add: { NetworkVariableSerialization.Write(writer, ref element.Value); } break; case NetworkListEvent.EventType.Insert: { writer.WriteValueSafe(element.Index); NetworkVariableSerialization.Write(writer, ref element.Value); } break; case NetworkListEvent.EventType.Remove: { NetworkVariableSerialization.Write(writer, ref element.Value); } break; case NetworkListEvent.EventType.RemoveAt: { writer.WriteValueSafe(element.Index); } break; case NetworkListEvent.EventType.Value: { writer.WriteValueSafe(element.Index); NetworkVariableSerialization.Write(writer, ref element.Value); } break; case NetworkListEvent.EventType.Clear: { //Nothing has to be written } break; } } } /// public override void WriteField(FastBufferWriter writer) { writer.WriteValueSafe((ushort)m_List.Length); for (int i = 0; i < m_List.Length; i++) { NetworkVariableSerialization.Write(writer, ref m_List.ElementAt(i)); } } /// public override void ReadField(FastBufferReader reader) { m_List.Clear(); reader.ReadValueSafe(out ushort count); for (int i = 0; i < count; i++) { var value = new T(); NetworkVariableSerialization.Read(reader, ref value); m_List.Add(value); } } /// public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) { reader.ReadValueSafe(out ushort deltaCount); for (int i = 0; i < deltaCount; i++) { reader.ReadValueSafe(out NetworkListEvent.EventType eventType); switch (eventType) { case NetworkListEvent.EventType.Add: { var value = new T(); NetworkVariableSerialization.Read(reader, ref value); m_List.Add(value); if (OnListChanged != null) { OnListChanged(new NetworkListEvent { Type = eventType, Index = m_List.Length - 1, Value = m_List[m_List.Length - 1] }); } if (keepDirtyDelta) { m_DirtyEvents.Add(new NetworkListEvent() { Type = eventType, Index = m_List.Length - 1, Value = m_List[m_List.Length - 1] }); MarkNetworkObjectDirty(); } } break; case NetworkListEvent.EventType.Insert: { reader.ReadValueSafe(out int index); var value = new T(); NetworkVariableSerialization.Read(reader, ref value); if (index < m_List.Length) { m_List.InsertRangeWithBeginEnd(index, index + 1); m_List[index] = value; } else { m_List.Add(value); } if (OnListChanged != null) { OnListChanged(new NetworkListEvent { Type = eventType, Index = index, Value = m_List[index] }); } if (keepDirtyDelta) { m_DirtyEvents.Add(new NetworkListEvent() { Type = eventType, Index = index, Value = m_List[index] }); MarkNetworkObjectDirty(); } } break; case NetworkListEvent.EventType.Remove: { var value = new T(); NetworkVariableSerialization.Read(reader, ref value); int index = m_List.IndexOf(value); if (index == -1) { break; } m_List.RemoveAt(index); if (OnListChanged != null) { OnListChanged(new NetworkListEvent { Type = eventType, Index = index, Value = value }); } if (keepDirtyDelta) { m_DirtyEvents.Add(new NetworkListEvent() { Type = eventType, Index = index, Value = value }); MarkNetworkObjectDirty(); } } break; case NetworkListEvent.EventType.RemoveAt: { reader.ReadValueSafe(out int index); T value = m_List[index]; m_List.RemoveAt(index); if (OnListChanged != null) { OnListChanged(new NetworkListEvent { Type = eventType, Index = index, Value = value }); } if (keepDirtyDelta) { m_DirtyEvents.Add(new NetworkListEvent() { Type = eventType, Index = index, Value = value }); MarkNetworkObjectDirty(); } } break; case NetworkListEvent.EventType.Value: { reader.ReadValueSafe(out int index); var value = new T(); NetworkVariableSerialization.Read(reader, ref value); if (index >= m_List.Length) { throw new Exception("Shouldn't be here, index is higher than list length"); } var previousValue = m_List[index]; m_List[index] = value; if (OnListChanged != null) { OnListChanged(new NetworkListEvent { Type = eventType, Index = index, Value = value, PreviousValue = previousValue }); } if (keepDirtyDelta) { m_DirtyEvents.Add(new NetworkListEvent() { Type = eventType, Index = index, Value = value, PreviousValue = previousValue }); MarkNetworkObjectDirty(); } } break; case NetworkListEvent.EventType.Clear: { //Read nothing m_List.Clear(); if (OnListChanged != null) { OnListChanged(new NetworkListEvent { Type = eventType, }); } if (keepDirtyDelta) { m_DirtyEvents.Add(new NetworkListEvent() { Type = eventType }); MarkNetworkObjectDirty(); } } break; case NetworkListEvent.EventType.Full: { ReadField(reader); ResetDirty(); } break; } } } /// public IEnumerator GetEnumerator() { return m_List.GetEnumerator(); } /// public void Add(T item) { // check write permissions if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) { throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); } m_List.Add(item); var listEvent = new NetworkListEvent() { Type = NetworkListEvent.EventType.Add, Value = item, Index = m_List.Length - 1 }; HandleAddListEvent(listEvent); } /// public void Clear() { // check write permissions if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) { throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); } m_List.Clear(); var listEvent = new NetworkListEvent() { Type = NetworkListEvent.EventType.Clear }; HandleAddListEvent(listEvent); } /// public bool Contains(T item) { int index = m_List.IndexOf(item); return index != -1; } /// public bool Remove(T item) { // check write permissions if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) { throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); } int index = m_List.IndexOf(item); if (index == -1) { return false; } m_List.RemoveAt(index); var listEvent = new NetworkListEvent() { Type = NetworkListEvent.EventType.Remove, Value = item }; HandleAddListEvent(listEvent); return true; } /// public int Count => m_List.Length; /// public int IndexOf(T item) { return m_List.IndexOf(item); } /// public void Insert(int index, T item) { // check write permissions if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) { throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); } if (index < m_List.Length) { m_List.InsertRangeWithBeginEnd(index, index + 1); m_List[index] = item; } else { m_List.Add(item); } var listEvent = new NetworkListEvent() { Type = NetworkListEvent.EventType.Insert, Index = index, Value = item }; HandleAddListEvent(listEvent); } /// public void RemoveAt(int index) { // check write permissions if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) { throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); } m_List.RemoveAt(index); var listEvent = new NetworkListEvent() { Type = NetworkListEvent.EventType.RemoveAt, Index = index }; HandleAddListEvent(listEvent); } /// public T this[int index] { get => m_List[index]; set { // check write permissions if (!CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) { throw new InvalidOperationException("Client is not allowed to write to this NetworkList"); } var previousValue = m_List[index]; m_List[index] = value; var listEvent = new NetworkListEvent() { Type = NetworkListEvent.EventType.Value, Index = index, Value = value, PreviousValue = previousValue }; HandleAddListEvent(listEvent); } } private void HandleAddListEvent(NetworkListEvent listEvent) { m_DirtyEvents.Add(listEvent); MarkNetworkObjectDirty(); OnListChanged?.Invoke(listEvent); } /// /// This is actually unused left-over from a previous interface /// public int LastModifiedTick { get { // todo: implement proper network tick for NetworkList return NetworkTickSystem.NoTick; } } /// /// Overridden implementation. /// CAUTION: If you derive from this class and override the method, /// you **must** always invoke the base.Dispose() method! /// public override void Dispose() { m_List.Dispose(); m_DirtyEvents.Dispose(); } } /// /// Struct containing event information about changes to a NetworkList. /// /// The type for the list that the event is about public struct NetworkListEvent { /// /// Enum representing the different operations available for triggering an event. /// public enum EventType : byte { /// /// Add /// Add, /// /// Insert /// Insert, /// /// Remove /// Remove, /// /// Remove at /// RemoveAt, /// /// Value changed /// Value, /// /// Clear /// Clear, /// /// Full list refresh /// Full } /// /// Enum representing the operation made to the list. /// public EventType Type; /// /// The value changed, added or removed if available. /// public T Value; /// /// The previous value when "Value" has changed, if available. /// public T PreviousValue; /// /// the index changed, added or removed if available /// public int Index; } }