776 lines
26 KiB
C#
776 lines
26 KiB
C#
using Cysharp.Threading.Tasks.Triggers;
|
|
using JetBrains.Annotations;
|
|
using System;
|
|
using System.Drawing;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using TMPro;
|
|
using UdonSharp;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
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.
|
|
|
|
// Each event contains a header. The first part contains the size of the event as a ushort, allowing a maximum size of 65,536 bytes.
|
|
// After that timestamp is transferred in seconds since the owner started the game as a float, matching Unity's time representation.
|
|
// Afterwards is a message id, a single byte which increments (with overflow) for every event to confirm data completeness.
|
|
// Lastly, a single byte indicates the type of event. 0 is a full sync, which is reserved for syncing up from any unknown state.
|
|
|
|
// 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]: (float) Time in seconds at which event occured.
|
|
// [6]: (byte) Message id, increments by one for each message with the same timestamp.
|
|
// [7]: (byte) Type of event. 0 = Full Sync, which is used to sync up from an undefinted state.
|
|
// [+]: Event-specific data.
|
|
|
|
#region Constants
|
|
/// <summary>
|
|
/// The maximum size of the buffer in bytes.
|
|
/// </summary>
|
|
private const int BufferMaxTotalEvents = 255;
|
|
|
|
/// <summary>
|
|
/// The index in an event where the event size is stored.
|
|
/// </summary>
|
|
private const ushort HeaderEventSizeIndex = 0;
|
|
/// <summary>
|
|
/// The index in an event where the timestamp is stored.
|
|
/// </summary>
|
|
private const ushort HeaderTimestampIndex = 2;
|
|
/// <summary>
|
|
/// The index in an event where the event id is stored.
|
|
/// </summary>
|
|
private const ushort HeaderEventIdIndex = 6;
|
|
/// <summary>
|
|
/// The index in an event where the event type is stored.
|
|
/// </summary>
|
|
private const ushort HeaderEventTypeIndex = 7;
|
|
/// <summary>
|
|
/// The total length of the header of an event, in bytes.
|
|
/// </summary>
|
|
private const ushort HeaderLength = 8;
|
|
/// <summary>
|
|
/// The amount of parts of which the header consists.
|
|
/// </summary>
|
|
private const ushort HeaderPartsCount = 4;
|
|
|
|
/// <summary>
|
|
/// The delay at which the receiving side replays events.
|
|
/// </summary>
|
|
private const float Delay = 1f;
|
|
#endregion
|
|
|
|
#region Private attributes
|
|
/// <summary>
|
|
/// Objects which are controlled by this <see cref="NetworkManager"/>.
|
|
/// </summary>
|
|
[SerializeField] private SyncedObject[] syncedObjects;
|
|
|
|
/// <summary>
|
|
/// Whether the current perspective is the transmitting side.
|
|
/// </summary>
|
|
private bool isOwner;
|
|
/// <summary>
|
|
/// Whether the current perspective is synced with the owner. (Always true if current perspective is owner.)
|
|
/// </summary>
|
|
private bool isSynced;
|
|
|
|
|
|
/// <summary>
|
|
/// Offset from system time to network time, including delay.
|
|
/// </summary>
|
|
private float offsetTime;
|
|
/// <summary>
|
|
/// Time since last full sync, captured when this FixedUpdate started, with network delay applied.
|
|
/// </summary>
|
|
private float internalTime;
|
|
|
|
/// <summary>
|
|
/// Time at which next received event occured.
|
|
/// </summary>
|
|
private float nextEventTime;
|
|
|
|
/// <summary>
|
|
/// The timestamp of the most recent event created or received.
|
|
/// </summary>
|
|
private float lastEventTimestamp;
|
|
/// <summary>
|
|
/// The message id of the most recent event created or received.
|
|
/// </summary>
|
|
private byte lastEventId;
|
|
|
|
/// <summary>
|
|
/// Amounot of retries in a row without a successful sync.
|
|
/// </summary>
|
|
private int retriesWithoutSuccess;
|
|
|
|
/// <summary>
|
|
/// Main buffer of data to be transmitted or processed
|
|
/// </summary>
|
|
private byte[][] buffer;
|
|
/// <summary>
|
|
/// Index of <see cref="buffer"/>.
|
|
/// </summary>
|
|
private int bufferIndex;
|
|
|
|
/// <summary>
|
|
/// Data which is currently available on the network.
|
|
/// </summary>
|
|
[UdonSynced] private byte[] networkedData = new byte[0];
|
|
#endregion
|
|
|
|
#region Public fields
|
|
/// <summary>
|
|
/// Whether this <see cref="NetworkManager"/> is ready to transmit or receive data.
|
|
/// If false, networking is disabled and this <see cref="NetworkManager"/> acts as a pass-through.
|
|
/// </summary>
|
|
public bool Ready { get; private set; } = false;
|
|
|
|
/// <summary>
|
|
/// The time since last full sync which is currently being simulated.
|
|
/// </summary>
|
|
public float SyncedTime { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Time since the last simulation, in seconds.
|
|
/// </summary>
|
|
public float Dt { get; private set; }
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
public bool IsEventUpdate { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Is the local user owner?
|
|
/// </summary>
|
|
public bool IsOwner => isOwner;
|
|
#endregion
|
|
|
|
#region General
|
|
public void Awake()
|
|
{
|
|
offsetTime = Time.fixedTime;
|
|
}
|
|
|
|
public void Initialize()
|
|
{
|
|
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[BufferMaxTotalEvents][];
|
|
bufferIndex = 0;
|
|
isSynced = isOwner; // Owner is always synced
|
|
retriesWithoutSuccess = 0;
|
|
lastEventTimestamp = 0;
|
|
lastEventId = 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(bool clearBuffer)
|
|
{
|
|
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.");
|
|
|
|
if (clearBuffer)
|
|
{
|
|
ClearBuffer();
|
|
}
|
|
|
|
isSynced = false;
|
|
|
|
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 timestamp = SyncedTime;
|
|
|
|
var eventId = GetNextEventId(lastEventId);
|
|
|
|
InitializeEvent(eventType, timestamp, eventId, BufferMaxTotalEvents, out byte[][] data, out var index);
|
|
|
|
foreach (var obj in syncedObjects)
|
|
{
|
|
obj.AppendSyncedData(data, ref index, eventType);
|
|
}
|
|
|
|
var oldIndex = this.bufferIndex;
|
|
|
|
var result = Flatten(data, 0, index);
|
|
|
|
// Validate and fill in event size
|
|
var eventSize = result.Length;
|
|
|
|
if (eventSize > ushort.MaxValue || eventSize < 0)
|
|
{
|
|
Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) New event is too large or negative! Size is {eventSize}, maximum allowed is {ushort.MaxValue}");
|
|
HandleError(false);
|
|
return;
|
|
}
|
|
|
|
var eventSizeBytes = BitConverter.GetBytes((ushort)eventSize);
|
|
Array.Copy(eventSizeBytes, 0, result, HeaderEventSizeIndex, eventSizeBytes.Length);
|
|
|
|
AppendEventToBuffer(result);
|
|
|
|
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Prepared event with {eventSize} bytes and timestamp {timestamp} for serialization, index went from {oldIndex} to {this.bufferIndex}");
|
|
|
|
RequestSerialization();
|
|
|
|
lastEventTimestamp = timestamp;
|
|
lastEventId = eventId;
|
|
|
|
retriesWithoutSuccess = 0; // We had success!
|
|
}
|
|
|
|
private static void InitializeEvent(NetworkEventType eventType, float timestamp, byte eventId, int maxSize, out byte[][] data, out int index)
|
|
{
|
|
// Create header
|
|
var timestampBytes = BitConverter.GetBytes(timestamp);
|
|
|
|
var header = new byte[HeaderLength];
|
|
|
|
// Event size is added later
|
|
Array.Copy(timestampBytes, 0, header, HeaderTimestampIndex, timestampBytes.Length);
|
|
header[HeaderEventIdIndex] = eventId;
|
|
header[HeaderEventTypeIndex] = Int32ToByte((int)eventType);
|
|
|
|
// Initialize event container
|
|
data = new byte[maxSize][];
|
|
data[0] = header;
|
|
index = 1;
|
|
}
|
|
|
|
[NetworkCallable]
|
|
public void RequestEventReceived(NetworkEventType eventType)
|
|
{
|
|
SendEvent(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 StoreIncomingData()
|
|
{
|
|
if (networkedData == null || networkedData.Length == 0)
|
|
{
|
|
return; // Nothing to store
|
|
}
|
|
|
|
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Received {networkedData.Length} bytes!\nBytes received:\n{BytesToString(networkedData)}");
|
|
|
|
var length = networkedData.Length;
|
|
int index = 0;
|
|
int eventSize = 0; // Store event size here so we can increment the index no matter how we increment the loop
|
|
while (true)
|
|
{
|
|
index += eventSize;
|
|
|
|
if (index >= length)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (length - index < HeaderLength)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) {nameof(StoreIncomingData)}: Remaining data in networkedData is not long enough to form a complete event! remaining: {length - index}.");
|
|
HandleError(false);
|
|
return;
|
|
}
|
|
|
|
eventSize = GetEventSizeFromHeader(networkedData, index);
|
|
|
|
if (length - index < eventSize)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) {nameof(StoreIncomingData)}: Event size is larger than total remaining data! {nameof(eventSize)}: {eventSize}, remaining: {length - index}.");
|
|
HandleError(false);
|
|
return;
|
|
}
|
|
|
|
if (length - index < HeaderLength)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) {nameof(StoreIncomingData)}: Event size is not long enough to form a complete event! {nameof(eventSize)}: {eventSize}, minimum needed: {HeaderLength}.");
|
|
HandleError(false);
|
|
return;
|
|
}
|
|
|
|
var @event = GetArrayPart(networkedData, index, eventSize);
|
|
|
|
var eventType = GetEventTypeFromHeader(@event);
|
|
|
|
if (eventType == NetworkEventType.FullSync)
|
|
{
|
|
ProcessIncomingFullSync(@event); // Immediately process full sync
|
|
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.");
|
|
continue;
|
|
}
|
|
|
|
var timestamp = GetTimestampFromHeader(@event);
|
|
var eventId = GetEventIdFromHeader(@event);
|
|
|
|
if (timestamp == lastEventTimestamp
|
|
&& eventId == lastEventId)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Duplicate message of type {eventType}, timestamp: {timestamp}, messageId: {eventId}.");
|
|
continue;
|
|
}
|
|
|
|
if (eventId != GetNextEventId(lastEventId))
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) EventIds were not sequential! Did we miss a serialization? Timestamp: {timestamp}, eventId: {eventId}, lastEventId: {lastEventId}.");
|
|
HandleError(false);
|
|
return;
|
|
}
|
|
|
|
AppendEventToBuffer(@event);
|
|
|
|
lastEventTimestamp = timestamp;
|
|
lastEventId = eventId;
|
|
}
|
|
|
|
UpdateNextEventTime();
|
|
}
|
|
|
|
private void ProcessIncomingFullSync(byte[] @event)
|
|
{
|
|
// Intentionally not doing a buffer size check here, since this is not appended to the buffer
|
|
// (and there is no good way to continue if this event is too large)
|
|
|
|
// Clear buffer and put the full sync into it
|
|
ClearBuffer();
|
|
AppendEventToBuffer(@event);
|
|
|
|
// Sync up to the time in the full sync
|
|
var timestamp = GetTimestampFromHeader(@event);
|
|
var eventId = GetEventIdFromHeader(@event);
|
|
SyncToTimestamp(timestamp, eventId);
|
|
|
|
// Immediately apply the full sync
|
|
UpdateNextEventTime(ignoreOrder: true);
|
|
isSynced = true;
|
|
}
|
|
|
|
private void ProgressEventTime()
|
|
{
|
|
IsEventUpdate = true;
|
|
|
|
while (bufferIndex != 0 && nextEventTime <= internalTime)
|
|
{
|
|
ProcessIncomingEvent();
|
|
UpdateNextEventTime();
|
|
}
|
|
}
|
|
|
|
private void ProcessIncomingEvent()
|
|
{
|
|
var @event = NextEvent;
|
|
|
|
var timestamp = GetTimestampFromHeader(@event);
|
|
var eventType = GetEventTypeFromHeader(@event);
|
|
var index = (int)HeaderLength; // Skip header
|
|
|
|
ProgressSyncedTime(timestamp);
|
|
|
|
foreach (var obj in syncedObjects)
|
|
{
|
|
obj.SyncedUpdate();
|
|
}
|
|
|
|
foreach (var obj in syncedObjects)
|
|
{
|
|
var success = obj.SetSyncedData(@event, ref index, eventType);
|
|
|
|
if (!success)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Malformed data reported by {obj.name} during event type {eventType}!");
|
|
HandleError(true);
|
|
return;
|
|
}
|
|
}
|
|
|
|
var eventSize = GetEventSizeFromHeader(@event);
|
|
if (index != eventSize)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Amount of data read does not match event size! Expected {eventSize}, read {index}.");
|
|
HandleError(true);
|
|
return;
|
|
}
|
|
|
|
RemoveProcessedDataFromBuffer(1);
|
|
|
|
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[BufferMaxTotalEvents][];
|
|
bufferIndex = 0;
|
|
}
|
|
|
|
private void RemoveProcessedDataFromBuffer(int amountProcessed)
|
|
{
|
|
var oldBuffer = buffer;
|
|
bufferIndex -= amountProcessed;
|
|
buffer = new byte[BufferMaxTotalEvents][];
|
|
Array.Copy(oldBuffer, amountProcessed, buffer, 0, bufferIndex);
|
|
}
|
|
|
|
private bool AppendEventToBuffer(byte[] @event)
|
|
{
|
|
if (bufferIndex >= BufferMaxTotalEvents)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Buffer not large enough to store event! Maximum event count: {BufferMaxTotalEvents}.");
|
|
HandleError(true);
|
|
return false;
|
|
}
|
|
|
|
buffer[bufferIndex++] = @event;
|
|
return true;
|
|
}
|
|
|
|
private byte[] NextEvent =>
|
|
buffer[0];
|
|
|
|
#endregion
|
|
|
|
#region Time
|
|
private void ProgressSyncedTime(float newTime)
|
|
{
|
|
//Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) updating SyncedTime from {SyncedTime} to {newTime}");
|
|
Dt = newTime - SyncedTime;
|
|
|
|
if (Dt < 0)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Negative Dt: {Dt}! Going from {SyncedTime} to {newTime}, IsEventUpdate: {IsEventUpdate}");
|
|
}
|
|
|
|
SyncedTime = newTime;
|
|
}
|
|
|
|
private void SyncToTimestamp(float timestamp, byte eventId)
|
|
{
|
|
var oldOffset = offsetTime;
|
|
var timeToSyncTo = timestamp - Delay;
|
|
offsetTime = Time.fixedTime - timeToSyncTo;
|
|
|
|
var delta = offsetTime - oldOffset;
|
|
internalTime = internalTime - delta;
|
|
SyncedTime = SyncedTime - delta;
|
|
|
|
lastEventTimestamp = timestamp;
|
|
lastEventId = eventId;
|
|
|
|
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(bool ignoreOrder = false)
|
|
{
|
|
if (bufferIndex == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var nextEventTime = GetTimestampFromHeader(NextEvent);
|
|
if (ignoreOrder || nextEventTime >= this.nextEventTime)
|
|
{
|
|
this.nextEventTime = nextEventTime;
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) New event is earlier than previous event!");
|
|
HandleError(true);
|
|
return;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Header
|
|
public static byte GetNextEventId(byte currentEventId)
|
|
{
|
|
if (currentEventId == byte.MaxValue) // Udon forces overflow checks
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
currentEventId += 1;
|
|
|
|
return currentEventId;
|
|
}
|
|
|
|
public static ushort GetEventSizeFromHeader(byte[] @event, int eventIndex = 0)
|
|
=> BitConverter.ToUInt16(@event, eventIndex + HeaderEventSizeIndex);
|
|
|
|
public static NetworkEventType GetEventTypeFromHeader(byte[] @event, int eventIndex = 0) =>
|
|
(NetworkEventType)@event[eventIndex + HeaderEventTypeIndex];
|
|
|
|
public static float GetTimestampFromHeader(byte[] @event, int eventIndex = 0) =>
|
|
BitConverter.ToSingle(@event, eventIndex + HeaderTimestampIndex);
|
|
|
|
public static byte GetEventIdFromHeader(byte[] @event, int eventIndex = 0) =>
|
|
@event[eventIndex + HeaderEventIdIndex];
|
|
#endregion
|
|
|
|
#region VRC events
|
|
public override void OnOwnershipTransferred(VRCPlayerApi newOwner)
|
|
{
|
|
SetOwner(newOwner == Networking.LocalPlayer);
|
|
}
|
|
|
|
private int indexAtLastSerialization = 0;
|
|
|
|
public override void OnPreSerialization()
|
|
{
|
|
if (!Ready)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (isOwner)
|
|
{
|
|
if (buffer == null || bufferIndex == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
networkedData = Flatten(buffer, 0, bufferIndex);
|
|
indexAtLastSerialization = bufferIndex;
|
|
}
|
|
else
|
|
{
|
|
networkedData = new byte[0]; // Prevent exception loop in VRChat SDK
|
|
}
|
|
}
|
|
|
|
public override void OnPostSerialization(SerializationResult result)
|
|
{
|
|
if (!Ready)
|
|
{
|
|
return;
|
|
}
|
|
|
|
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(indexAtLastSerialization);
|
|
networkedData = new byte[0];
|
|
}
|
|
|
|
public override void OnDeserialization()
|
|
{
|
|
if (!Ready || isOwner)
|
|
{
|
|
return;
|
|
}
|
|
|
|
StoreIncomingData();
|
|
}
|
|
#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 int GetFlattenedSize(byte[][] data, int start, int length)
|
|
{
|
|
var size = 0;
|
|
for (int i = start; i < start + length; i++)
|
|
{
|
|
size += data[i].Length;
|
|
}
|
|
return size;
|
|
}
|
|
|
|
private static byte[] Flatten(byte[][] data, int start, int length)
|
|
{
|
|
var finalLength = GetFlattenedSize(data, start, length);
|
|
|
|
var result = new byte[finalLength];
|
|
int resultIndex = 0;
|
|
|
|
for (int sourceIndex = start; sourceIndex < start + length; sourceIndex++)
|
|
{
|
|
var array = data[sourceIndex];
|
|
Array.Copy(array, 0, result, resultIndex, array.Length);
|
|
resultIndex += array.Length;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static byte[] GetArrayPart(byte[] data, int start, int length)
|
|
{
|
|
var result = new byte[length];
|
|
Array.Copy(data, start, result, 0, length);
|
|
return result;
|
|
}
|
|
|
|
public static byte Int32ToByte(int value) =>
|
|
(byte)value; // Doing this inline causes an error...?
|
|
#endregion
|
|
|
|
#region Debug
|
|
public void SimulateSyncToTimestamp(float timestamp)
|
|
{
|
|
SyncToTimestamp(timestamp, 0);
|
|
}
|
|
|
|
public void WriteDebugOutput(TMP_InputField debugOutput)
|
|
{
|
|
debugOutput.text += $"{nameof(NetworkManager)}:\n" +
|
|
$"IsOwner: {isOwner}\n" +
|
|
$"Ready: {Ready}\n" +
|
|
$"Time.fixedTime: {Time.fixedTime}\n" +
|
|
$"offsetTime: {offsetTime}\n" +
|
|
$"internalTime: {internalTime}\n" +
|
|
$"SyncedTime: {SyncedTime}\n" +
|
|
$"Dt: {Dt}\n" +
|
|
$"\n";
|
|
}
|
|
|
|
/// <summary>
|
|
/// An animator which visualizes whether the current perspective is the owner.
|
|
/// </summary>
|
|
[SerializeField] private Animator DebugImageToIndicateOwner;
|
|
#endregion
|
|
}
|
|
}
|