284 lines
9.1 KiB
C#
284 lines
9.1 KiB
C#
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, buffer, ref 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);
|
|
data[1] = new byte[] { (byte)eventType };
|
|
}
|
|
|
|
private void FlattenAndCopy(byte[][] data, byte[] target, ref int index)
|
|
{
|
|
foreach (byte[] values in data)
|
|
{
|
|
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;
|
|
}
|
|
}
|