using Assets.Scripts; using JetBrains.Annotations; using System; using System.Text; using UdonSharp; using UnityEngine; using UnityEngine.UIElements; using VRC.SDK3.Data; using VRC.SDK3.UdonNetworkCalling; using VRC.Udon.Common; using static VRC.SDKBase.Networking; namespace Marro.PacManUdon { enum NetworkEventType { FullSync = 0, } 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-3]: (uint) Time in seconds at which event occured. // [4]: (byte) Type of event: 0 = Full Sync, others will be defined later. // [5+]: Event-specific data const int BufferSizeBytes = 1000; [SerializeField] private SyncedObject[] syncedObjects; private bool isOwner; private long startTimeTicks; private uint nextEventTime; // Main buffer of events private byte[] buffer; private int index; [UdonSynced] private byte[] networkedData; public void Initialize(bool isOwner) { buffer = new byte[BufferSizeBytes]; index = 0; startTimeTicks = DateTime.UtcNow.Ticks; this.isOwner = isOwner; } public void Update() { if (!isOwner) { ProgressReplayTime(); } } [NetworkCallable] public void DoFullSync() { if (!isOwner) { Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(DoFullSync)} while not the owner!"); return; } InitializeEvent(NetworkEventType.FullSync, 1000, out byte[][] data, out var index); foreach (var obj in syncedObjects) { obj.AppendSyncedData(data, ref index); } FlattenAndCopy(data, index, buffer, ref this.index); RequestSerialization(); } public void RequestFullSync() { if (isOwner) { Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(RequestFullSync)} while we are the owner!"); return; } SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.Owner, "DoFullSync"); } private void ProcessIncomingData() { var syncType = (NetworkEventType)networkedData[4]; var size = networkedData.Length; if (!EnsureSpaceToStoreEvent(size, syncType == NetworkEventType.FullSync)) { return; } Array.Copy(networkedData, 0, buffer, index, size); index += size; UpdateNextEventTime(); } private void ProgressReplayTime() { var currentTime = CurrentTime; while (index != 0 && nextEventTime <= currentTime) { ProcessNextEvent(); UpdateNextEventTime(); } } private void UpdateNextEventTime() { if (index > 0) { var nextEventTime = BitConverter.ToUInt32(buffer, 0); if (nextEventTime >= this.nextEventTime) { this.nextEventTime = nextEventTime; } else { Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Events are in invalid order! Clearing buffer."); ClearBuffer(); return; } } } private void ProcessNextEvent() { var eventType = (NetworkEventType)buffer[4]; switch (eventType) { default: Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Invalid sync type for incoming data! Buffer will be cleared."); ClearBuffer(); return; case NetworkEventType.FullSync: ProcessFullSync(); return; } } private void ProcessFullSync() { var index = 0; foreach (var obj in syncedObjects) { var success = obj.SetSyncedData(buffer, ref index); if (!success) { Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Malformed data reported by {obj.name} during full sync! Clearing buffer and requesting new full sync."); ClearBuffer(); RequestFullSync(); return; } } RemoveProcessedDataFromBuffer(index); Debug.Log($"Processed full sync! Total {index} bytes."); } private bool EnsureSpaceToStoreEvent(int eventSize, bool isFullSync) { if (index + eventSize <= buffer.Length) { return true; // Enough space! } if (isFullSync && index == 0) { Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Buffer is not large enough to store full sync! Viewing remote play is not possible."); return false; // Unable to store full sync, networking features will not function. } ClearBuffer(); if (!isFullSync) { Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Too much data in buffer to store event! Old events will be discarded, and full sync will be performed."); RequestFullSync(); return false; // No use storing this event, we're going to wait for the full sync. } Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Too much data in buffer to store full sync! Old events will be discarded."); return true; // We can store event now that we cleared the buffer. } private void InitializeEvent(NetworkEventType eventType, int maxSize, out byte[][] data, out int index) { data = new byte[maxSize][]; index = 2; data[0] = BitConverter.GetBytes(CurrentTime); byte eventTypeByte = byte.Parse(eventType.ToString()); data[1] = new byte[] { eventTypeByte }; } private 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; } } private void RemoveProcessedDataFromBuffer(int amountProcessed) { var oldBuffer = buffer; index -= amountProcessed; buffer = new byte[BufferSizeBytes]; Array.Copy(oldBuffer, amountProcessed, buffer, 0, index); } private void ClearBuffer() { buffer = new byte[BufferSizeBytes]; index = 0; } public override void OnPreSerialization() { if (isOwner) { networkedData = new byte[index]; Array.Copy(buffer, networkedData, index); } } public override void OnPostSerialization(SerializationResult result) { if (!result.success) { Debug.LogWarning($"Serialization failed! Tried to send {result.byteCount} bytes."); return; } Debug.Log($"Serialized with {result.byteCount} bytes!"); if (isOwner) { // 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(); } } public uint CurrentTime => (uint)((DateTime.UtcNow.Ticks - startTimeTicks) / TimeSpan.TicksPerMillisecond); public bool IsOwner => isOwner; } }