using Cysharp.Threading.Tasks.Triggers; using JetBrains.Annotations; using System; using System.Drawing; using System.Text; using UdonSharp; using UnityEngine; using VRC.SDK3.UdonNetworkCalling; using VRC.SDKBase; using VRC.Udon.ClientBindings.Interfaces; using VRC.Udon.Common; namespace Marro.PacManUdon { public enum NetworkEventType { FullSync = 0, PacManTurn = 1, } public class NetworkManager : UdonSharpBehaviour { // The network manager works by serializing event and state data into a byte array, including a timestamp for each event. // If user is owner, this data is created and stored in a buffer which is cleared upon transmission. // If user is not owner, this data is read into the same buffer and replayed based on the included timestamp. // Data replay is delayed with an offset on the timestamp to hide inconsistency in latency. // The timestamp is transferred in ms as a 32 bit uint, which gives a maximum time of about 49 days. // The maximum allowed age of a VRChat instance is 7 days, so this should not cause issues. // A byte array is used as a DataList or DataDictionary can only be transmitted as JSON which is much less efficient. // As Udon does not support instantiating objects, I have not created classes to represent the data being sent. // Correct parsing is dependend upon everything being read out identially to how it was created. // An event has the following structure: // [0-1]: (ushort) Size of event. // [2-5]: (uint) Time in seconds at which event occured. // [6]: (byte) Type of event. 0 = Full Sync, which is used to sync up from an undefinted state. // [7+]: Event-specific data #region Constants /// /// The maximum size of the buffer in bytes. /// private const int BufferMaxSizeBytes = 10000; /// /// How many bytes to increase the buffer size by if the current one is not enough. /// private const int BufferIncrementSizeBytes = 1000; /// /// The index in an event where the event size is stored. /// private const ushort HeaderEventSizeIndex = 0; /// /// The index in an event where the timestamp is stored. /// private const ushort HeaderTimestampIndex = 2; /// /// The index in an event where the event type is stored. /// private const ushort HeaderEventTypeIndex = 6; /// /// The total length of the header of an event, in bytes. /// private const ushort HeaderLength = 7; /// /// The multiplier from Unity time to a timestamp. /// private const int TimestampMultiplier = 1000; /// /// The zero value of a timestamp. Anything below this value is negative. /// private const uint TimestampZeroValue = 1000; /// /// The delay at which the receiving side replays events. /// private const float Delay = 0.250f; #endregion #region Private attributes /// /// Objects which are controlled by this . /// [SerializeField] private SyncedObject[] syncedObjects; /// /// An animator which visualizes whether the current perspective is the owner. /// [SerializeField] private Animator DebugImageToIndicateOwner; /// /// Whether the current perspective is the transmitting side. /// private bool isOwner; /// /// Whether the current perspective is synced with the owner. (Always true if current perspective is owner.) /// private bool isSynced; /// /// Offset from system time to network time, including delay. /// private float offsetTime; /// /// Time since last full sync, captured when this FixedUpdate started, with network delay applied. /// private float internalTime; /// /// Time at which next received event occured. /// private float nextEventTime; /// /// Amounot of retries in a row without a successful sync. /// private int retriesWithoutSuccess; /// /// Main buffer of data to be transmitted or processed /// private byte[] buffer; /// /// Index of . /// private int bufferIndex; /// /// Data which is currently available on the network. /// [UdonSynced] private byte[] networkedData = new byte[0]; #endregion #region Public fields /// /// Whether this is ready to transmit or receive data. /// If false, networking is disabled and this acts as a pass-through. /// public bool Ready { get; private set; } = false; /// /// The time since last full sync which is currently being simulated. /// public float SyncedTime { get; private set; } /// /// Time since the last simulation, in seconds. /// public float Dt { get; private set; } /// /// Is the current simulation to prepare for applying a network event? /// True = Yes, This update is preparing for a network update. /// False = No, this update is after the network update or there was no /// public bool IsEventUpdate { get; private set; } /// /// Is the local user owner? /// public bool IsOwner => isOwner; #endregion #region General public void Awake() { offsetTime = Time.fixedTime; } public void Initialize() { if (Ready) { Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Already initialized, rejecting repeat call to {nameof(Initialize)}"); return; } if (!BitConverter.IsLittleEndian) { Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Fatal: NetworkManager only supports little endian! Network sync will not be possible."); var zero = 0; Debug.Log(1 / zero); // Intentionally crash return; } SetOwner(Networking.IsOwner(gameObject)); buffer = new byte[BufferIncrementSizeBytes]; bufferIndex = 0; isSynced = isOwner; // Owner is always synced retriesWithoutSuccess = 0; offsetTime = Time.fixedTime; internalTime = 0; SyncedTime = 0; Dt = Time.fixedDeltaTime; Ready = true; Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Initialized, time offset: {offsetTime}"); } public void FixedUpdate() { UpdateInternalTime(); if (!isOwner) { ProgressEventTime(); } PerformFixedSyncedUpdate(); } public void UpdateInternalTime() { internalTime = Time.fixedTime - offsetTime; } private void PerformFixedSyncedUpdate() { IsEventUpdate = false; ProgressSyncedTime(internalTime); foreach (var obj in syncedObjects) { obj.SyncedUpdate(); } } private void HandleError() { retriesWithoutSuccess++; if (retriesWithoutSuccess > 3) { Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Fatal: Retried 3 times without success."); var zero = 0; Debug.Log(1 / zero); // Intentionally crash return; } Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Encountered data error, attempting to recover via full sync."); ClearBuffer(); if (!isOwner) { RequestEvent(NetworkEventType.FullSync); } else { SendEvent(NetworkEventType.FullSync); } } private void SetOwner(bool isOwner) { this.isOwner = isOwner; if (DebugImageToIndicateOwner != null) { DebugImageToIndicateOwner.SetFloat("Color", isOwner ? 1 : 0); } } #endregion #region Sender public void SendEvent(NetworkEventType eventType) { if (!isOwner) { Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(SendEvent)} while not the owner!"); return; } var eventTime = TimeToTimestamp(SyncedTime); InitializeEvent(eventType, eventTime, BufferMaxSizeBytes, out byte[][] data, out var index); foreach (var obj in syncedObjects) { obj.AppendSyncedData(data, ref index, eventType); } // Get event size, skipping over the event size which is not yet included ushort eventSize = 0; for (int i = 0; i < index; i++) { eventSize += (ushort)data[i].Length; } if (!EnsureSpaceToStoreEvent(eventSize)) { return; } data[0] = BitConverter.GetBytes(eventSize); var oldIndex = this.bufferIndex; FlattenAndCopy(data, index, buffer, ref this.bufferIndex); Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Prepared event with {eventSize} bytes and timestamp {eventTime} for serialization, index went from {oldIndex} to {this.bufferIndex}"); RequestSerialization(); retriesWithoutSuccess = 0; // We had success! } private static void InitializeEvent(NetworkEventType eventType, uint eventTime, int maxSize, out byte[][] data, out int index) { data = new byte[maxSize][]; index = 3; data[0] = new byte[2]; // Placeholder for event size data[1] = BitConverter.GetBytes(eventTime); data[2] = new byte[] { GameManager.Int32ToByte((int)eventType) }; } #endregion #region Receiver public void RequestEvent(NetworkEventType eventType) { if (isOwner) { Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(RequestEvent)} while we are the owner!"); return; } SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.Owner, "RequestEventReceived", eventType); } private void ProcessIncomingData() { if (networkedData.Length == 0) { return; // Nothing to process } Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Received {networkedData.Length} bytes!\nBytes received:\n{BytesToString(networkedData)}"); var length = networkedData.Length; int index = 0; while (index < length) { if (length - index < HeaderLength) { Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) {nameof(ProcessIncomingData)}: Remaining data in networkedData is not long enough to form a complete event!"); HandleError(); return; } var eventSize = networkedData[index + HeaderEventSizeIndex]; var eventType = (NetworkEventType)networkedData[index + HeaderEventTypeIndex]; if (eventType == NetworkEventType.FullSync) { ProcessIncomingFullSync(index, eventSize); // Immediately process full sync index += eventSize; continue; } if (!isSynced) { Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Received event of type {eventType} while we are not yet synced to the remote time, ignoring event."); index += eventSize; continue; } AppendEventToBuffer(index, eventSize); index += eventSize; } } private void ProcessIncomingFullSync(int index, int size) { // Intentionally not doing a buffer size check here, since this is not appending to the buffer // (and there is no good way to continue if event is too large) // Clear buffer and copy the full sync into it buffer = new byte[size]; Array.Copy(networkedData, index, buffer, 0, size); this.bufferIndex = size; // Sync up to the time in the full sync var timestamp = BitConverter.ToUInt32(networkedData, index + HeaderTimestampIndex); SyncToTimestamp(timestamp); // Immediately apply the full sync UpdateNextEventTime(); isSynced = true; } private void ProgressEventTime() { IsEventUpdate = true; while (bufferIndex != 0 && nextEventTime <= internalTime) { ProcessIncomingEvent(); UpdateNextEventTime(); } } private void ProcessIncomingEvent() { var eventTime = TimestampToTime(BitConverter.ToUInt32(buffer, HeaderTimestampIndex)); var eventType = (NetworkEventType)buffer[HeaderEventTypeIndex]; var index = (int)HeaderLength; // Skip header ProgressSyncedTime(eventTime); foreach (var obj in syncedObjects) { obj.SyncedUpdate(); } foreach (var obj in syncedObjects) { var success = obj.SetSyncedData(buffer, ref index, eventType); if (!success) { Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Malformed data reported by {obj.name} during event type {eventType}!"); HandleError(); return; } if (index > this.bufferIndex) { Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Buffer overflow during {nameof(SyncedObject.SetSyncedData)} for {obj.name} in event type {eventType}!"); HandleError(); return; } } var eventSize = BitConverter.ToUInt16(buffer, HeaderEventSizeIndex); if (index != eventSize) { Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Amount of data read does not match event size! Expected {eventSize}, read {index}."); HandleError(); return; } RemoveProcessedDataFromBuffer(index); Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Processed incoming event! Total {index} bytes."); retriesWithoutSuccess = 0; // We had success! } #endregion #region Buffer private void ClearBuffer() { buffer = new byte[BufferMaxSizeBytes]; bufferIndex = 0; } private void RemoveProcessedDataFromBuffer(int amountProcessed) { var oldBuffer = buffer; bufferIndex -= amountProcessed; buffer = new byte[BufferMaxSizeBytes]; Array.Copy(oldBuffer, amountProcessed, buffer, 0, bufferIndex); } private bool IncreaseBufferSize(int newSize) { if (newSize < buffer.Length) { Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Cannot decrease the size of the buffer!"); return false; } if (newSize > BufferMaxSizeBytes) { return false; } var oldBuffer = buffer; buffer = new byte[newSize]; oldBuffer.CopyTo(buffer, 0); return true; } private bool EnsureSpaceToStoreEvent(int eventSize) { if (bufferIndex + eventSize <= buffer.Length) { return true; // Enough space! } var newBufferSize = ((bufferIndex + eventSize) / BufferIncrementSizeBytes + 1) * BufferIncrementSizeBytes; var success = IncreaseBufferSize(newBufferSize); if (success) { return true; } if (bufferIndex == 0) { Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Buffer is not large enough to store event!"); } else { Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Too much data in buffer to store event!"); } HandleError(); // We can store event now that we cleared the buffer. return false; } private void AppendEventToBuffer(int index, int size) { if (!EnsureSpaceToStoreEvent(size)) { return; } Array.Copy(networkedData, index, buffer, this.bufferIndex, size); this.bufferIndex += size; UpdateNextEventTime(); } #endregion #region Time private void ProgressSyncedTime(float newTime) { //Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) updating SyncedTime from {SyncedTime} to {newTime}"); Dt = newTime - SyncedTime; SyncedTime = newTime; } private void SyncToTimestamp(uint timestamp) { var oldOffset = offsetTime; var timeToSyncTo = timestamp / (float)TimestampMultiplier - Delay; offsetTime = Time.fixedTime - timeToSyncTo; var delta = offsetTime - oldOffset; internalTime = internalTime - delta; SyncedTime = SyncedTime - delta; Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Synced to timestamp {timestamp}, current time is {Time.fixedTime}, timeToSyncTo is {timeToSyncTo}, offsetTime is now {offsetTime}, internalTime is now {internalTime}, SyncedTime is now {SyncedTime}"); } private void UpdateNextEventTime() { if (bufferIndex == 0) { return; } var nextEventTime = TimestampToTime(BitConverter.ToUInt32(buffer, HeaderTimestampIndex)); if (nextEventTime >= this.nextEventTime) { this.nextEventTime = nextEventTime; } else { Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) New event is earlier than previous event!"); HandleError(); return; } } public static uint TimeToTimestamp(float time) { return (uint)((time * TimestampMultiplier) + TimestampZeroValue); } public static float TimestampToTime(uint timeStamp) { return (timeStamp - (long)TimestampZeroValue) / (float)TimestampMultiplier; // Use a long here to prevent an underflow } #endregion #region VRC events public override void OnOwnershipTransferred(VRCPlayerApi newOwner) { SetOwner(newOwner == Networking.LocalPlayer); if(isOwner) { HandleError(); } } public override void OnPreSerialization() { if (isOwner) { networkedData = new byte[bufferIndex]; Array.Copy(buffer, networkedData, bufferIndex); } else { networkedData = new byte[0]; // Prevent exception loop in VRChat SDK } } public override void OnPostSerialization(SerializationResult result) { if (!result.success) { Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Serialization failed! Tried to send {result.byteCount} bytes."); return; } if (!isOwner || networkedData.Length == 0) { return; } Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Serialized with {networkedData.Length} bytes!\nBytes sent:\n{BytesToString(networkedData)}"); // Remove all transferred data from the buffer, leaving data that came in after serialization RemoveProcessedDataFromBuffer(networkedData.Length); networkedData = null; } public override void OnDeserialization() { if (!isOwner) { ProcessIncomingData(); } } #endregion #region Utils public string BytesToString(byte[] bytes) { var sb = new StringBuilder("new byte[] { "); foreach (var b in bytes) { sb.Append(b + ", "); } sb.Append("}"); return sb.ToString(); } private static void FlattenAndCopy(byte[][] data, int length, byte[] target, ref int index) { for (int i = 0; i < length; i++) { var values = data[i]; Array.Copy(values, 0, target, index, values.Length); index += values.Length; } } #endregion #region Debug public void SimulateSyncToTimestamp(uint timestamp) { SyncToTimestamp(timestamp); } #endregion } }