Successful sync test

This commit is contained in:
2026-01-01 20:44:34 +01:00
parent 97fe8cd69f
commit 69a0a752be
19 changed files with 3173 additions and 740 deletions

View File

@@ -1,21 +1,19 @@
using Assets.Scripts;
using JetBrains.Annotations;
using System;
using System.Linq;
using System;
using System.Drawing;
using System.Text;
using UdonSharp;
using UnityEngine;
using UnityEngine.UIElements;
using VRC.SDK3.Data;
using VRC.SDK3.UdonNetworkCalling;
using VRC.SDKBase;
using VRC.Udon.ClientBindings.Interfaces;
using VRC.Udon.Common;
using static VRC.SDKBase.Networking;
namespace Marro.PacManUdon
{
enum NetworkEventType
public enum NetworkEventType
{
FullSync = 0,
PacManTurn = 1,
}
public class NetworkManager : UdonSharpBehaviour
{
@@ -32,33 +30,58 @@ namespace Marro.PacManUdon
// 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
// [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
const int BufferSizeBytes = 1000;
private const int BufferMaxSizeBytes = 10000;
private const int BufferIncrementSizeBytes = 1000;
[SerializeField]
private SyncedObject[] syncedObjects;
[SerializeField] private SyncedObject[] syncedObjects;
[SerializeField] private Animator DebugImageToIndicateOwner;
private bool isOwner;
private bool isSynced;
private long startTimeTicks;
private long startTimeTicks = DateTime.UtcNow.Ticks; // Initialize to prevent errors
private uint nextEventTime;
private int retriesWithoutSuccess;
// Main buffer of events
private byte[] buffer;
private int index;
[UdonSynced] private byte[] networkedData;
private const ushort HeaderEventSizeIndex = 0;
private const ushort HeaderTimestampIndex = 2;
private const ushort HeaderEventTypeIndex = 6;
private const ushort HeaderLength = 7;
public void Initialize(bool isOwner)
private const int Delay = 250;
[UdonSynced] private byte[] networkedData = new byte[0];
public void Initialize()
{
buffer = new byte[BufferSizeBytes];
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;
}
buffer = new byte[BufferIncrementSizeBytes];
index = 0;
startTimeTicks = DateTime.UtcNow.Ticks;
this.isOwner = isOwner;
isSynced = false;
retriesWithoutSuccess = 0;
SetOwner(Networking.IsOwner(gameObject));
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Initialized, startTimeTicks: {startTimeTicks}");
}
public void Update()
@@ -69,65 +92,141 @@ namespace Marro.PacManUdon
}
}
[NetworkCallable]
public void DoFullSync()
public void SendEvent(NetworkEventType eventType)
{
if (!isOwner)
{
Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(DoFullSync)} while not the owner!");
Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(SendEvent)} while not the owner!");
return;
}
InitializeEvent(NetworkEventType.FullSync, 1000, out byte[][] data, out var index);
var eventTime = CurrentTime;
InitializeEvent(eventType, eventTime, BufferMaxSizeBytes, out byte[][] data, out var index);
foreach (var obj in syncedObjects)
{
obj.AppendSyncedData(data, ref index);
obj.AppendSyncedData(data, ref index, eventType, eventTime);
}
var eventSize = 0;
// Get event size, skipping over the event size which is not yet included
ushort eventSize = 0;
for (int i = 0; i < index; i++)
{
eventSize += data[i].Length;
eventSize += (ushort)data[i].Length;
}
if (!EnsureSpaceToStorePreparedEvent(eventSize))
if (!EnsureSpaceToStoreEvent(eventSize))
{
return;
}
data[0] = BitConverter.GetBytes(eventSize);
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}");
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Prepared event with {eventSize} bytes and timestamp {eventTime} for serialization, index went from {oldIndex} to {this.index}");
RequestSerialization();
retriesWithoutSuccess = 0; // We had success!
}
public void RequestFullSync()
public void RequestEvent(NetworkEventType eventType)
{
if (isOwner)
{
Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(RequestFullSync)} while we are the owner!");
Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(RequestEvent)} while we are the owner!");
return;
}
SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.Owner, "DoFullSync");
SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.Owner, "RequestEventReceived", eventType);
}
[NetworkCallable]
public void RequestEventReceived(NetworkEventType eventType)
{
if (!isOwner)
{
Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(RequestEventReceived)} while we are not the owner!");
return;
}
SendEvent(eventType);
}
private void ProcessIncomingData()
{
var syncType = (NetworkEventType)networkedData[4];
var size = networkedData.Length;
if (networkedData.Length == 0)
{
return; // Nothing to process
}
if (!EnsureSpaceToStoreReceivedEvent(size, syncType == NetworkEventType.FullSync))
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;
}
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Received {networkedData.Length} bytes!\nBytes received:\n{BytesToString(networkedData)}");
}
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.index = size;
// Sync up to the time in the full sync
var time = BitConverter.ToUInt32(networkedData, index + HeaderTimestampIndex);
SyncToTime(time);
// Immediately apply the full sync
nextEventTime = time;
isSynced = true;
}
private void AppendEventToBuffer(int index, int size)
{
if (!EnsureSpaceToStoreEvent(size))
{
return;
}
Array.Copy(networkedData, 0, buffer, index, size);
index += size;
Array.Copy(networkedData, index, buffer, this.index, size);
this.index += size;
UpdateNextEventTime();
}
@@ -137,120 +236,140 @@ namespace Marro.PacManUdon
var currentTime = CurrentTime;
while (index != 0 && nextEventTime <= currentTime)
{
ProcessNextEvent();
ProcessIncomingEvent();
UpdateNextEventTime();
}
}
private void UpdateNextEventTime()
{
if (index > 0)
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;
}
return;
}
var nextEventTime = 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;
}
}
private void ProcessNextEvent()
private void ProcessIncomingEvent()
{
var eventType = (NetworkEventType)buffer[4];
var eventTime = BitConverter.ToUInt32(buffer, HeaderTimestampIndex);
var eventType = (NetworkEventType)buffer[HeaderEventTypeIndex];
var index = (int)HeaderLength; // Skip header
switch (eventType)
if (false) // TODO: check event type
{
default:
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Invalid sync type for incoming data! Buffer will be cleared.");
ClearBuffer();
return;
case NetworkEventType.FullSync:
ProcessFullSync();
return;
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Invalid event type for incoming data! Buffer will be cleared.");
HandleError();
return;
}
}
private void ProcessFullSync()
{
var index = 5; // Skip header
foreach (var obj in syncedObjects)
{
var success = obj.SetSyncedData(buffer, ref index);
var success = obj.SetSyncedData(buffer, ref index, eventType, eventTime);
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();
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Malformed data reported by {obj.name} during event type {eventType}! Resetting state.");
HandleError();
return;
}
if (index > this.index)
{
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Buffer overflow during SetSyncedData for {obj.name} in event type {eventType}! Resetting state.");
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($"Processed full sync! Total {index} bytes.");
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Processed incoming event! Total {index} bytes.");
retriesWithoutSuccess = 0; // We had success!
}
private bool EnsureSpaceToStorePreparedEvent(int eventSize)
private void SyncToTime(uint newTime)
{
var timeToSyncTo = newTime - Delay;
startTimeTicks = DateTime.UtcNow.Ticks - timeToSyncTo * TimeSpan.TicksPerMillisecond;
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Synced to time {newTime}, startTimeTicks is now {startTimeTicks}");
}
private bool EnsureSpaceToStoreEvent(int eventSize)
{
if (index + eventSize <= buffer.Length)
{
return true; // Enough space!
}
var newBufferSize = ((index + eventSize) / BufferIncrementSizeBytes + 1) * BufferIncrementSizeBytes;
var success = IncreaseBufferSize(newBufferSize);
if (success)
{
return true;
}
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.
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!");
}
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.
HandleError(); // We can store event now that we cleared the buffer.
return false;
}
private bool EnsureSpaceToStoreReceivedEvent(int eventSize, bool isFullSync)
private bool IncreaseBufferSize(int newSize)
{
if (index + eventSize <= buffer.Length)
if (newSize < buffer.Length)
{
return true; // Enough space!
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Cannot decrease the size of the buffer!");
return false;
}
if (isFullSync && index == 0)
if (newSize > BufferMaxSizeBytes)
{
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.
return false;
}
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.
var oldBuffer = buffer;
buffer = new byte[newSize];
oldBuffer.CopyTo(buffer, 0);
return true;
}
private void InitializeEvent(NetworkEventType eventType, int maxSize, out byte[][] data, out int index)
private void InitializeEvent(NetworkEventType eventType, uint eventTime, int maxSize, out byte[][] data, out int index)
{
data = new byte[maxSize][];
index = 2;
index = 3;
data[0] = BitConverter.GetBytes(CurrentTime);
data[1] = new byte[] { GameManager.Int32ToByte((int)eventType) };
data[0] = new byte[2]; // Placeholder for event size
data[1] = BitConverter.GetBytes(eventTime);
data[2] = new byte[] { GameManager.Int32ToByte((int)eventType) };
}
private void FlattenAndCopy(byte[][] data, int length, byte[] target, ref int index)
@@ -267,16 +386,62 @@ namespace Marro.PacManUdon
{
var oldBuffer = buffer;
index -= amountProcessed;
buffer = new byte[BufferSizeBytes];
buffer = new byte[BufferMaxSizeBytes];
Array.Copy(oldBuffer, amountProcessed, buffer, 0, index);
}
private void ClearBuffer()
{
buffer = new byte[BufferSizeBytes];
buffer = new byte[BufferMaxSizeBytes];
index = 0;
}
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);
}
}
public override void OnOwnershipTransferred(VRCPlayerApi newOwner)
{
SetOwner(newOwner == Networking.LocalPlayer);
if(isOwner)
{
HandleError();
}
}
public override void OnPreSerialization()
{
if (isOwner)
@@ -284,24 +449,30 @@ namespace Marro.PacManUdon
networkedData = new byte[index];
Array.Copy(buffer, networkedData, index);
}
else
{
networkedData = new byte[0]; // Prevent exception loop in VRChat SDK
}
}
public override void OnPostSerialization(SerializationResult result)
{
if (!result.success)
{
Debug.LogWarning($"Serialization failed! Tried to send {result.byteCount} bytes.");
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Serialization failed! Tried to send {result.byteCount} bytes.");
return;
}
Debug.Log($"Serialized with {result.byteCount} bytes!");
if (isOwner)
if (!isOwner || networkedData.Length == 0)
{
// Remove all transferred data from the buffer, leaving data that came in after serialization
RemoveProcessedDataFromBuffer(networkedData.Length);
networkedData = null;
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()
@@ -312,8 +483,24 @@ namespace Marro.PacManUdon
}
}
public float GetDtInSeconds(uint currentTime, uint lastUpdate)
{
return (CurrentTime - lastUpdate) / 1000f;
}
public uint CurrentTime => (uint)((DateTime.UtcNow.Ticks - startTimeTicks) / TimeSpan.TicksPerMillisecond);
public bool IsOwner => isOwner;
public string BytesToString(byte[] bytes)
{
var sb = new StringBuilder("new byte[] { ");
foreach (var b in bytes)
{
sb.Append(b + ", ");
}
sb.Append("}");
return sb.ToString();
}
}
}