Files
PacManUdon/Assets/Scripts/NetworkManager.cs
2025-12-29 21:02:05 +01:00

320 lines
10 KiB
C#

using Assets.Scripts;
using JetBrains.Annotations;
using System;
using System.Linq;
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);
}
var eventSize = 0;
for (int i = 0; i < index; i++)
{
eventSize += data[i].Length;
}
if (!EnsureSpaceToStorePreparedEvent(eventSize))
{
return;
}
var oldIndex = this.index;
FlattenAndCopy(data, index, buffer, ref this.index);
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Prepared event with {eventSize} bytes for serialization, index went from {oldIndex} to {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 (!EnsureSpaceToStoreReceivedEvent(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.LogWarning($"({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.LogWarning($"({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 = 5; // Skip header
foreach (var obj in syncedObjects)
{
var success = obj.SetSyncedData(buffer, ref index);
if (!success)
{
Debug.LogWarning($"({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 EnsureSpaceToStorePreparedEvent(int eventSize)
{
if (index + eventSize <= buffer.Length)
{
return true; // Enough space!
}
if (index == 0)
{
Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Buffer is not large enough to store event! Viewing remote play is not possible.");
return false; // Unable to store event, networking features will not function.
}
ClearBuffer();
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Too much data in buffer to store event! Old events will be discarded.");
return true; // We can store event now that we cleared the buffer.
}
private bool EnsureSpaceToStoreReceivedEvent(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.LogWarning($"({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.LogWarning($"({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);
data[1] = new byte[] { GameManager.Int32ToByte((int)eventType) };
}
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;
}
}