1277 lines
42 KiB
C#
1277 lines
42 KiB
C#
using Newtonsoft.Json.Linq;
|
|
using System;
|
|
using System.Numerics;
|
|
using System.Text;
|
|
using TMPro;
|
|
using UdonSharp;
|
|
using UnityEngine;
|
|
using VRC.SDK3.UdonNetworkCalling;
|
|
using VRC.SDKBase;
|
|
using VRC.Udon.Common;
|
|
|
|
namespace Marro.PacManUdon
|
|
{
|
|
public enum NetworkEventType
|
|
{
|
|
FullSyncForced = 0,
|
|
FullSync = 1,
|
|
InputChange = 2,
|
|
StartGameButtonPressed = 3,
|
|
Pause = 7,
|
|
Resume = 8,
|
|
Step = 9,
|
|
}
|
|
|
|
public class NetworkManager : SyncedObject
|
|
{
|
|
// 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 Settings
|
|
/// <summary>
|
|
/// The root from which this <see cref="NetworkManager"/> will look for <see cref="SyncedObject"/> to control.
|
|
/// </summary>
|
|
[SerializeField] private GameObject root;
|
|
/// <summary>
|
|
/// The delay in ticks at which the receiving side replays events.
|
|
/// </summary>
|
|
[SerializeField] private int delay = 50;
|
|
/// <summary>
|
|
/// The maximum amount of times a message is sent.
|
|
/// </summary>
|
|
[SerializeField] private int maxEventSendTries = 3;
|
|
/// <summary>
|
|
/// How long to wait since last message to send next ping.
|
|
/// </summary>
|
|
[SerializeField] private int pingDelay = 15;
|
|
/// <summary>
|
|
/// The time delta at which updates occur.
|
|
/// </summary>
|
|
[SerializeField] private float tickDelta = 0.0165f;
|
|
#endregion
|
|
|
|
#region Constants
|
|
/// <summary>
|
|
/// The maximum amount of events in the buffer.
|
|
/// </summary>
|
|
private const int BufferMaxTotalEvents = 255;
|
|
/// <summary>
|
|
/// The maximum amount of events in the buffer.
|
|
/// </summary>
|
|
private const int MaxEventSize = ushort.MaxValue;
|
|
|
|
/// <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;
|
|
#endregion
|
|
|
|
#region Private attributes
|
|
/// <summary>
|
|
/// Whether <see cref="Initialize"/> has been called successfully.
|
|
/// </summary>
|
|
private bool initialized = false;
|
|
|
|
/// <summary>
|
|
/// Subscribers to each <see cref="NetworkEventType"/>.
|
|
/// </summary>
|
|
private SyncedObject[][] networkEventSubscribers;
|
|
|
|
/// <summary>
|
|
/// Indices for <see cref="networkEventSubscribers"/>.
|
|
/// </summary>
|
|
private int[] networkEventSubscribersIndices;
|
|
|
|
/// <summary>
|
|
/// Subscribers for <see cref="SyncedObject.SyncedUpdate"/>.
|
|
/// </summary>
|
|
private SyncedObject[] syncedUpdateSubscribers;
|
|
|
|
/// <summary>
|
|
/// The <see cref="Time.fixedTime"/> that corresponds to tick 0.
|
|
/// </summary>
|
|
private float startTime;
|
|
|
|
/// <summary>
|
|
/// Time in ticks since start of game.
|
|
/// </summary>
|
|
private int targetTicks;
|
|
|
|
/// <summary>
|
|
/// True if time is paused
|
|
/// </summary>
|
|
private bool paused;
|
|
|
|
/// <summary>
|
|
/// True if a step should happen next update
|
|
/// </summary>
|
|
private bool stepNext;
|
|
|
|
/// <summary>
|
|
/// Time at which next received event occured, in ticks.
|
|
/// </summary>
|
|
private int nextEventTime;
|
|
|
|
/// <summary>
|
|
/// Amounot of retries in a row without a successful sync.
|
|
/// </summary>
|
|
private int retriesWithoutSuccess;
|
|
|
|
/// <summary>
|
|
/// For receiver: True if there's a full sync in the queue and we are not synced, otherwise false.
|
|
/// For transmitter: True if there's a full sync in the queue which has not yet been transmitted, otherwise false.
|
|
/// </summary>
|
|
private bool hasFullSyncReady;
|
|
/// <summary>
|
|
/// True if serialization has been requestsed
|
|
/// </summary>
|
|
private bool serializationRequested;
|
|
|
|
/// <summary>
|
|
/// Events to send at the end of SyncedUpdate cycle.
|
|
/// </summary>
|
|
private NetworkEventType[] eventsToSendEarly;
|
|
/// <summary>
|
|
/// Index for <see cref="eventsToSendEarly"/>.
|
|
/// </summary>
|
|
private int eventsToSendEarlyIndex;
|
|
/// <summary>
|
|
/// Events to send at the end of SyncedUpdate cycle, delayed by a tick.
|
|
/// </summary>
|
|
private NetworkEventType[] eventsToSendLate;
|
|
/// <summary>
|
|
/// Index for <see cref="eventsToSendLate"/>.
|
|
/// </summary>
|
|
private int eventsToSendLateIndex;
|
|
|
|
/// <summary>
|
|
/// Queue of events to be transmitted or processed.
|
|
/// </summary>
|
|
private byte[][] eventsQueue;
|
|
/// <summary>
|
|
/// Index of <see cref="eventsQueue"/>.
|
|
/// </summary>
|
|
private int eventsQueueIndex;
|
|
|
|
/// <summary>
|
|
/// The value of <see cref="eventsQueueIndex"/> at the last transmission.
|
|
/// </summary>
|
|
private int eventsQueueIndexAtLastTransmission;
|
|
/// <summary>
|
|
/// Counts of new events at recent transmissions.
|
|
/// </summary>
|
|
private int[] eventTransmissionHistory;
|
|
/// <summary>
|
|
/// Index of <see cref="eventTransmissionHistoryIndex"/>.
|
|
/// </summary>
|
|
private int eventTransmissionHistoryIndex;
|
|
/// <summary>
|
|
/// Time of last event transmission, in ticks.
|
|
/// </summary>
|
|
private int lastEventTransmissionTime;
|
|
|
|
|
|
/// <summary>
|
|
/// The message id of the most recent event created or received.
|
|
/// </summary>
|
|
private byte lastEventId;
|
|
|
|
|
|
/// <summary>
|
|
/// Data which is currently available on the network.
|
|
/// </summary>
|
|
[UdonSynced] private byte[] networkedData = new byte[0];
|
|
#endregion
|
|
|
|
#region Public fields
|
|
private bool ready = false;
|
|
/// <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 => ready;
|
|
private set
|
|
{
|
|
ready = value;
|
|
if (DebugImageToIndicateReady != null)
|
|
{
|
|
DebugImageToIndicateReady.SetFloat("Color", value ? 1 : 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Whether the current perspective is synced with the owner. (Always true if current perspective is owner.)
|
|
/// </summary>
|
|
public bool Synced
|
|
{
|
|
get => synced;
|
|
private set
|
|
{
|
|
synced = value;
|
|
if (DebugImageToIndicateSynced != null)
|
|
{
|
|
DebugImageToIndicateSynced.SetFloat("Color", value ? 1 : 0);
|
|
}
|
|
}
|
|
}
|
|
private bool synced = false;
|
|
|
|
/// <summary>
|
|
/// The time since start of game, in ticks.
|
|
/// </summary>
|
|
public int SyncedTimeTicks { get; private set; }
|
|
/// <summary>
|
|
/// The time since start of game which is currently being simulated.
|
|
/// </summary>
|
|
public float SyncedTime { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Time since the last simulation, in seconds.
|
|
/// </summary>
|
|
public float SyncedDeltaTime => tickDelta;
|
|
|
|
/// <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 network update this cycle.
|
|
/// </summary>
|
|
public bool IsEventUpdate { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Is the local user owner?
|
|
/// </summary>
|
|
public bool IsOwner
|
|
{
|
|
get => isOwner;
|
|
private set
|
|
{
|
|
isOwner = value;
|
|
if (DebugImageToIndicateOwner != null)
|
|
{
|
|
DebugImageToIndicateOwner.SetFloat("Color", value ? 1 : 0);
|
|
}
|
|
}
|
|
}
|
|
private bool isOwner = false;
|
|
#endregion
|
|
|
|
#region General
|
|
/// <summary>
|
|
/// Initializes the <see cref="NetworkManager"/>. Call <see cref="Reset"/> afterwards to activate networking.
|
|
/// </summary>
|
|
public void Initialize()
|
|
{
|
|
if (initialized)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Tried to call {nameof(Initialize)} when already initialized!");
|
|
return;
|
|
}
|
|
|
|
if (!BitConverter.IsLittleEndian)
|
|
{
|
|
Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Fatal: NetworkManager only supports little endian! Network sync will not be possible.");
|
|
return;
|
|
}
|
|
|
|
if (root == null)
|
|
{
|
|
root = transform.parent.gameObject;
|
|
}
|
|
|
|
InitializeSubscribers();
|
|
|
|
initialized = true;
|
|
|
|
Reset();
|
|
}
|
|
|
|
private void InitializeSubscribers()
|
|
{
|
|
syncedUpdateSubscribers = root.GetComponentsInChildren<SyncedObject>(includeInactive: true);
|
|
|
|
foreach (var obj in syncedUpdateSubscribers)
|
|
{
|
|
obj.networkManager = this;
|
|
}
|
|
|
|
//Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Found {syncedUpdateSubscribers.Length} {nameof(SyncedObject)} in children of {root.name}.");
|
|
|
|
const int eventTypeCount = byte.MaxValue + 1;
|
|
|
|
networkEventSubscribers = new SyncedObject[eventTypeCount][];
|
|
networkEventSubscribersIndices = new int[eventTypeCount];
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
if (!initialized)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Tried to call {nameof(Reset)} while not initialized. Call {nameof(Initialize)} first.");
|
|
return;
|
|
}
|
|
|
|
if (tester == null)
|
|
{
|
|
IsOwner = Networking.IsOwner(gameObject);
|
|
}
|
|
else
|
|
{
|
|
IsOwner = tester.ShouldBeOwner(this);
|
|
}
|
|
|
|
ClearBuffer();
|
|
|
|
Synced = IsOwner; // Owner is always synced
|
|
retriesWithoutSuccess = 0;
|
|
hasFullSyncReady = false;
|
|
|
|
targetTicks = 0;
|
|
startTime = Time.fixedTime;
|
|
SyncedTime = 0;
|
|
SyncedTimeTicks = 0;
|
|
|
|
Ready = true;
|
|
|
|
// Sync up
|
|
if (IsOwner)
|
|
{
|
|
SendEventSoon(NetworkEventType.FullSyncForced);
|
|
}
|
|
else
|
|
{
|
|
RequestEvent(NetworkEventType.FullSync);
|
|
}
|
|
|
|
//Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Initialized");
|
|
}
|
|
|
|
public void Update()
|
|
{
|
|
if (debugOutput != null)
|
|
{
|
|
WriteDebugOutput(debugOutput);
|
|
}
|
|
|
|
if (!initialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get our target time
|
|
UpdateInternalTime();
|
|
|
|
// Forwards simulated time by updateDelta until we're caught up
|
|
while (SyncedTimeTicks <= targetTicks)
|
|
{
|
|
PerformFixedSyncedUpdate();
|
|
}
|
|
}
|
|
|
|
private void PerformFixedSyncedUpdate()
|
|
{
|
|
if (Ready)
|
|
{
|
|
if (IsOwner)
|
|
{
|
|
ProcessEventsToSend(); // Prepare events from last cycle
|
|
ProgressPingTime(); // See if we need to send a ping
|
|
}
|
|
if (!isOwner)
|
|
{
|
|
ApplyReceivedEvents(); // See if there's events after last update that need to be replayed
|
|
}
|
|
}
|
|
|
|
ProgressSyncedTime();
|
|
CallSyncedUpdate();
|
|
}
|
|
|
|
private void CallSyncedUpdate()
|
|
{
|
|
IsEventUpdate = false;
|
|
|
|
for (int i = 0; i < syncedUpdateSubscribers.Length; i++)
|
|
{
|
|
var obj = syncedUpdateSubscribers[i];
|
|
|
|
if (obj.gameObject.activeInHierarchy)
|
|
{
|
|
obj.SyncedUpdate();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void HandleError(bool clearBuffer)
|
|
{
|
|
retriesWithoutSuccess++;
|
|
|
|
if (retriesWithoutSuccess > 3)
|
|
{
|
|
Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Fatal: Retried 3 times without success.");
|
|
Ready = false;
|
|
return;
|
|
}
|
|
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Encountered data error, attempting to recover via full sync.");
|
|
|
|
if (clearBuffer)
|
|
{
|
|
ClearBuffer();
|
|
}
|
|
|
|
Synced = false;
|
|
|
|
if (!IsOwner)
|
|
{
|
|
RequestEvent(NetworkEventType.FullSync);
|
|
}
|
|
else
|
|
{
|
|
SendEventSoon(NetworkEventType.FullSyncForced);
|
|
}
|
|
}
|
|
|
|
private SyncedObject[] GetEventSubscribers(NetworkEventType eventType) => networkEventSubscribers[(int)eventType];
|
|
|
|
public void SubscribeToEvent(SyncedObject syncedObject, NetworkEventType eventType)
|
|
{
|
|
// This is inefficient, but I'd rather initialize slowly than perform bounds checks in often called code
|
|
|
|
var eventTypeIndex = (int)eventType;
|
|
|
|
var subscribers = networkEventSubscribers[eventTypeIndex];
|
|
int subscribersIndex = networkEventSubscribersIndices[eventTypeIndex];
|
|
if (subscribers == null)
|
|
{
|
|
subscribers = new SyncedObject[1];
|
|
}
|
|
else
|
|
{
|
|
subscribers = new SyncedObject[subscribersIndex+1];
|
|
Array.Copy(networkEventSubscribers[eventTypeIndex], subscribers, subscribersIndex);
|
|
}
|
|
|
|
subscribers[subscribersIndex] = syncedObject;
|
|
|
|
networkEventSubscribers[eventTypeIndex] = subscribers;
|
|
networkEventSubscribersIndices[eventTypeIndex] = subscribersIndex;
|
|
}
|
|
#endregion
|
|
|
|
#region Sender
|
|
public void SendEventSoon(NetworkEventType eventType, bool early = false)
|
|
{
|
|
if (!Ready)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!IsOwner)
|
|
{
|
|
//Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(SendEventSoon)} while not the owner!");
|
|
return;
|
|
}
|
|
|
|
if (early)
|
|
{
|
|
if (eventsToSendEarlyIndex >= eventsToSendEarly.Length)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) {nameof(eventsToSendEarly)} overflow!");
|
|
HandleError(false);
|
|
return;
|
|
}
|
|
|
|
eventsToSendEarly[eventsToSendEarlyIndex++] = eventType;
|
|
}
|
|
else
|
|
{
|
|
if (eventsToSendLateIndex >= eventsToSendLate.Length)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) {nameof(eventsToSendLate)} overflow!");
|
|
HandleError(false);
|
|
return;
|
|
}
|
|
|
|
eventsToSendLate[eventsToSendLateIndex++] = eventType;
|
|
}
|
|
}
|
|
|
|
private void ProcessEventsToSend()
|
|
{
|
|
for (int i = 0; i < eventsToSendEarlyIndex; i++)
|
|
{
|
|
SendEventNow(eventsToSendEarly[i], true);
|
|
}
|
|
|
|
eventsToSendEarlyIndex = 0;
|
|
|
|
for (int i = 0; i < eventsToSendLateIndex; i++)
|
|
{
|
|
SendEventNow(eventsToSendLate[i], false);
|
|
}
|
|
|
|
eventsToSendLateIndex = 0;
|
|
}
|
|
|
|
public void SendEventNow(NetworkEventType eventType, bool early)
|
|
{
|
|
if (!Ready)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!IsOwner)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(SendEventNow)} while not the owner!");
|
|
return;
|
|
}
|
|
|
|
var timestamp = SyncedTimeTicks;
|
|
|
|
if (early)
|
|
{
|
|
timestamp--;
|
|
}
|
|
|
|
var eventId = GetNextEventId(lastEventId);
|
|
|
|
InitializeEvent(eventType, timestamp, eventId, out byte[] data, out var index);
|
|
|
|
var effectiveEventType = eventType == NetworkEventType.FullSyncForced ? NetworkEventType.FullSync : eventType;
|
|
|
|
var subscibers = GetEventSubscribers(effectiveEventType);
|
|
|
|
if (subscibers != null)
|
|
{
|
|
foreach (var obj in subscibers)
|
|
{
|
|
obj.CollectSyncedData(data, ref index, effectiveEventType);
|
|
}
|
|
}
|
|
|
|
// Validate and fill in event size
|
|
var eventSizeBytes = BitConverter.GetBytes((ushort)index);
|
|
Array.Copy(eventSizeBytes, 0, data, HeaderEventSizeIndex, eventSizeBytes.Length);
|
|
|
|
if (IsFullSync(eventType))
|
|
{
|
|
hasFullSyncReady = true;
|
|
}
|
|
|
|
data = GetArrayPart(data, 0, index);
|
|
|
|
QueueEventInBuffer(data);
|
|
|
|
//Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Prepared event of type {eventType} with {data.Length} bytes, timestamp {timestamp} and id {eventId} for serialization.");
|
|
|
|
RequestSerializationForEvents();
|
|
|
|
lastEventId = eventId;
|
|
|
|
retriesWithoutSuccess = 0; // We had success!
|
|
}
|
|
|
|
private static void InitializeEvent(NetworkEventType eventType, int timestamp, byte eventId, out byte[] data, out int index)
|
|
{
|
|
data = new byte[MaxEventSize];
|
|
|
|
// Create header (note: event size is added later)
|
|
var timestampBytes = BitConverter.GetBytes(timestamp);
|
|
Array.Copy(timestampBytes, 0, data, HeaderTimestampIndex, timestampBytes.Length);
|
|
data[HeaderEventIdIndex] = eventId;
|
|
data[HeaderEventTypeIndex] = ((int)eventType).ToByte();
|
|
index = HeaderLength;
|
|
}
|
|
|
|
private void RequestSerializationForEvents()
|
|
{
|
|
RequestSerialization();
|
|
serializationRequested = true;
|
|
if (tester != null)
|
|
{
|
|
tester.RequestSerializationTest();
|
|
}
|
|
}
|
|
|
|
[NetworkCallable]
|
|
public void RequestEventReceived(NetworkEventType eventType)
|
|
{
|
|
if (!Ready)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (IsFullSync(eventType) && hasFullSyncReady)
|
|
{
|
|
return; // Don't send another full sync if we're already preparing to send one
|
|
}
|
|
|
|
if (eventType == NetworkEventType.FullSyncForced)
|
|
{
|
|
SendEventSoon(NetworkEventType.FullSync); // Remote is not allowed to request a forced full sync
|
|
return;
|
|
}
|
|
|
|
SendEventSoon(eventType);
|
|
}
|
|
|
|
private void ProgressPingTime()
|
|
{
|
|
if (eventsQueueIndex > 0 && !serializationRequested
|
|
&& targetTicks - lastEventTransmissionTime >= pingDelay)
|
|
{
|
|
RequestSerializationForEvents();
|
|
}
|
|
}
|
|
|
|
public override void OnPreSerialization()
|
|
{
|
|
if (!Ready || !IsOwner || !serializationRequested || eventsQueue == null || eventsQueueIndex == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
networkedData = Flatten(eventsQueue, 0, eventsQueueIndex);
|
|
|
|
eventTransmissionHistory[eventTransmissionHistoryIndex] = eventsQueueIndex - eventsQueueIndexAtLastTransmission;
|
|
eventTransmissionHistoryIndex += 1;
|
|
if (eventTransmissionHistoryIndex >= eventTransmissionHistory.Length)
|
|
{
|
|
eventTransmissionHistoryIndex = 0;
|
|
}
|
|
|
|
serializationRequested = false;
|
|
|
|
//Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Serializing {eventsQueueIndex} event(s), eventTransmissionHistory is now {ArrayToString(eventTransmissionHistory)} with index {eventTransmissionHistoryIndex}");
|
|
}
|
|
|
|
public override void OnPostSerialization(SerializationResult result) => OnPostSerializationInternal(result.success, result.byteCount);
|
|
|
|
// Version of OnPostSerialization which does not require instantiating SerializationResult, so that it can be called for debugging purposes.
|
|
public void OnPostSerializationInternal(bool success, int byteCount)
|
|
{
|
|
if (!Ready || !IsOwner || networkedData.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!success)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Serialization failed! Tried to send {byteCount} bytes.");
|
|
return;
|
|
}
|
|
|
|
//Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Serialized with {networkedData.Length} bytes.\nBytes sent:\n{ArrayToString(networkedData)}");
|
|
|
|
// Remove data from the buffer that no longer needs to be (re)transmitted
|
|
DequeueEventsFromBuffer(eventTransmissionHistory[eventTransmissionHistoryIndex]);
|
|
eventTransmissionHistory[eventTransmissionHistoryIndex] = 0; // Prevent trying to remove the same events again
|
|
eventsQueueIndexAtLastTransmission = eventsQueueIndex;
|
|
|
|
networkedData = new byte[0];
|
|
|
|
// If there was a full sync in the queue, it has now been transmitted at least once
|
|
hasFullSyncReady = false;
|
|
|
|
lastEventTransmissionTime = targetTicks;
|
|
}
|
|
#endregion
|
|
|
|
#region Receiver
|
|
public void RequestEvent(NetworkEventType eventType)
|
|
{
|
|
if (!Ready)
|
|
{
|
|
return;
|
|
}
|
|
|
|
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);
|
|
|
|
if (tester != null)
|
|
{
|
|
tester.RequestEvent(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{ArrayToString(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 ((index += eventSize) < length)
|
|
{
|
|
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 timestamp = GetTimestampFromHeader(@event);
|
|
var eventId = GetEventIdFromHeader(@event);
|
|
var eventType = GetEventTypeFromHeader(@event);
|
|
|
|
if (eventType != NetworkEventType.FullSyncForced && (Synced || hasFullSyncReady))
|
|
{
|
|
// Check if event id is sequential
|
|
if (eventId != GetNextEventId(lastEventId))
|
|
{
|
|
if (index + eventSize >= length // If this is the last event of the batch
|
|
&& eventId != lastEventId) // Unless the eventId is the same, then this is probably just a duplicate
|
|
{
|
|
// The last event of the batch has to be synced up with our current event id count, else we've missed an event
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) EventIds were not sequential! Did we miss an event? Timestamp: {timestamp}, eventId: {eventId}, lastEventId: {lastEventId}.");
|
|
HandleError(false);
|
|
return;
|
|
}
|
|
|
|
// We've likely already processed this event, ignore it
|
|
continue;
|
|
}
|
|
|
|
QueueEventInBuffer(@event);
|
|
|
|
//Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)} Queued event with id {eventId}");
|
|
}
|
|
else
|
|
{
|
|
// If we're not yet synced, we only care about full sync events.
|
|
if (IsFullSync(eventType))
|
|
{
|
|
QueueFullSyncForReplay(@event); // Immediately process full sync
|
|
}
|
|
}
|
|
|
|
lastEventId = eventId;
|
|
}
|
|
|
|
if (Synced)
|
|
{
|
|
UpdateNextEventTime();
|
|
}
|
|
}
|
|
|
|
private void QueueFullSyncForReplay(byte[] @event)
|
|
{
|
|
// Clear buffer and put the full sync into it
|
|
ClearBuffer();
|
|
QueueEventInBuffer(@event);
|
|
|
|
// Set this event to play after the default delay
|
|
nextEventTime = targetTicks + delay;
|
|
|
|
hasFullSyncReady = true;
|
|
|
|
//Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Queued full sync in buffer, should execute at {nextEventTime}.");
|
|
}
|
|
|
|
private void ApplyReceivedEvents()
|
|
{
|
|
IsEventUpdate = true;
|
|
|
|
while (eventsQueueIndex > 0 && nextEventTime <= SyncedTimeTicks)
|
|
{
|
|
var success = ApplyEvent(eventsQueue[0]);
|
|
|
|
if (!success)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DequeueEventsFromBuffer(1);
|
|
UpdateNextEventTime();
|
|
}
|
|
}
|
|
|
|
private bool ApplyEvent(byte[] @event)
|
|
{
|
|
var timestamp = GetTimestampFromHeader(@event);
|
|
var eventType = GetEventTypeFromHeader(@event);
|
|
|
|
var isFullSync = IsFullSync(eventType);
|
|
|
|
if (!Synced || eventType == NetworkEventType.FullSyncForced)
|
|
{
|
|
SyncToTimestamp(timestamp);
|
|
}
|
|
else if (timestamp < SyncedTimeTicks)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Next received event is in the past! " +
|
|
$"{nameof(nextEventTime)}: {nextEventTime} < {nameof(SyncedTimeTicks)}: {SyncedTimeTicks}.");
|
|
HandleError(true);
|
|
return false;
|
|
}
|
|
|
|
if (eventType == NetworkEventType.FullSyncForced)
|
|
{
|
|
eventType = NetworkEventType.FullSync;
|
|
}
|
|
|
|
var index = (int)HeaderLength; // Skip header
|
|
|
|
var subscribers = GetEventSubscribers(eventType);
|
|
|
|
//Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) ApplyEvent with dt {SyncedDeltaTime}");
|
|
|
|
if (subscribers != null)
|
|
{
|
|
foreach (var obj in subscribers)
|
|
{
|
|
obj.SyncedUpdate();
|
|
}
|
|
|
|
foreach (var obj in subscribers)
|
|
{
|
|
var success = obj.WriteSyncedData(@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 false;
|
|
}
|
|
}
|
|
}
|
|
|
|
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 false;
|
|
}
|
|
|
|
if (!Synced && isFullSync)
|
|
{
|
|
hasFullSyncReady = false;
|
|
Synced = true;
|
|
}
|
|
|
|
//Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Performed incoming event of type {eventType}! Total {index} bytes.");
|
|
|
|
retriesWithoutSuccess = 0; // We had success!
|
|
|
|
return true;
|
|
}
|
|
|
|
public override void OnDeserialization()
|
|
{
|
|
if (!Ready || IsOwner)
|
|
{
|
|
return;
|
|
}
|
|
|
|
StoreIncomingData();
|
|
}
|
|
#endregion
|
|
|
|
#region Buffer
|
|
private void ClearBuffer()
|
|
{
|
|
eventsQueue = new byte[BufferMaxTotalEvents][];
|
|
eventsQueueIndex = 0;
|
|
lastEventId = 0;
|
|
hasFullSyncReady = false;
|
|
|
|
eventTransmissionHistory = new int[maxEventSendTries];
|
|
eventTransmissionHistoryIndex = 0;
|
|
eventsQueueIndexAtLastTransmission = 0;
|
|
|
|
eventsToSendEarly = new NetworkEventType[BufferMaxTotalEvents];
|
|
eventsToSendEarlyIndex = 0;
|
|
eventsToSendLate = new NetworkEventType[BufferMaxTotalEvents];
|
|
eventsToSendLateIndex = 0;
|
|
}
|
|
|
|
private void DequeueEventsFromBuffer(int eventCount)
|
|
{
|
|
if (eventCount > eventsQueueIndex) // Never remove more events than are in the queue
|
|
{
|
|
eventCount = eventsQueueIndex;
|
|
}
|
|
|
|
var oldBuffer = eventsQueue;
|
|
eventsQueueIndex -= eventCount;
|
|
eventsQueue = new byte[BufferMaxTotalEvents][];
|
|
Array.Copy(oldBuffer, eventCount, eventsQueue, 0, eventsQueueIndex);
|
|
}
|
|
|
|
private bool QueueEventInBuffer(byte[] @event)
|
|
{
|
|
if (eventsQueueIndex >= BufferMaxTotalEvents)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Buffer not large enough to store event! Maximum event count: {BufferMaxTotalEvents}.");
|
|
HandleError(true);
|
|
return false;
|
|
}
|
|
|
|
eventsQueue[eventsQueueIndex++] = @event;
|
|
return true;
|
|
}
|
|
#endregion
|
|
|
|
#region Time
|
|
private void UpdateInternalTime()
|
|
{
|
|
if (paused && !stepNext)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (paused && stepNext)
|
|
{
|
|
targetTicks++;
|
|
stepNext = false;
|
|
return;
|
|
}
|
|
|
|
targetTicks = (int)((Time.fixedTime - startTime) / tickDelta);
|
|
}
|
|
|
|
private void ProgressSyncedTime()
|
|
{
|
|
SyncedTimeTicks++;
|
|
SyncedTime = SyncedTimeTicks * tickDelta;
|
|
}
|
|
|
|
private void SyncToTimestamp(int timestamp)
|
|
{
|
|
startTime = Time.fixedTime - timestamp * tickDelta;
|
|
targetTicks = timestamp;
|
|
SyncedTimeTicks = timestamp;
|
|
SyncedTime = SyncedTimeTicks * tickDelta;
|
|
nextEventTime = timestamp;
|
|
|
|
//Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Synced to timestamp {timestamp}, internalTime is now {targetTicks}, SyncedTime is now {SyncedTime}, nextEventTime is now {nextEventTime}");
|
|
}
|
|
|
|
private void UpdateNextEventTime()
|
|
{
|
|
if (eventsQueueIndex == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var nextEventTime = GetTimestampFromHeader(eventsQueue[0]);
|
|
|
|
if (nextEventTime < this.nextEventTime)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) New event is earlier than previous event!");
|
|
HandleError(true);
|
|
return;
|
|
}
|
|
|
|
if (nextEventTime < SyncedTimeTicks)
|
|
{
|
|
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) New event timestamp is earlier than our current synced time by {SyncedTime - nextEventTime} seconds! nextEventTime: {nextEventTime} SyncedTime: {SyncedTime}, internalTime: {targetTicks}");
|
|
HandleError(true);
|
|
return;
|
|
}
|
|
|
|
this.nextEventTime = nextEventTime;
|
|
}
|
|
#endregion
|
|
|
|
#region Header
|
|
private static byte GetNextEventId(byte currentEventId)
|
|
{
|
|
if (currentEventId == byte.MaxValue) // Udon forces overflow checks
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
currentEventId += 1;
|
|
|
|
return currentEventId;
|
|
}
|
|
|
|
private static bool IsFullSync(NetworkEventType eventType)
|
|
=> eventType == NetworkEventType.FullSync || eventType == NetworkEventType.FullSyncForced;
|
|
|
|
private static ushort GetEventSizeFromHeader(byte[] @event, int eventIndex = 0)
|
|
=> BitConverter.ToUInt16(@event, eventIndex + HeaderEventSizeIndex);
|
|
|
|
private static NetworkEventType GetEventTypeFromHeader(byte[] @event, int eventIndex = 0) =>
|
|
(NetworkEventType)@event[eventIndex + HeaderEventTypeIndex];
|
|
|
|
private static int GetTimestampFromHeader(byte[] @event, int eventIndex = 0) =>
|
|
BitConverter.ToInt32(@event, eventIndex + HeaderTimestampIndex);
|
|
|
|
private static byte GetEventIdFromHeader(byte[] @event, int eventIndex = 0) =>
|
|
@event[eventIndex + HeaderEventIdIndex];
|
|
#endregion
|
|
|
|
#region VRC events
|
|
public override void OnOwnershipTransferred(VRCPlayerApi newOwner)
|
|
{
|
|
if (!Ready)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool newOwnerIsLocalPlayer = newOwner == Networking.LocalPlayer;
|
|
IsOwner = newOwnerIsLocalPlayer;
|
|
|
|
if (newOwnerIsLocalPlayer)
|
|
{
|
|
SendEventSoon(NetworkEventType.FullSyncForced);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Utils
|
|
public string ArrayToString(byte[] bytes)
|
|
{
|
|
var sb = new StringBuilder("[ ");
|
|
foreach (var b in bytes)
|
|
{
|
|
sb.Append(b + " ");
|
|
}
|
|
sb.Append("]");
|
|
return sb.ToString();
|
|
}
|
|
|
|
public string ArrayToString(int[] bytes)
|
|
{
|
|
var sb = new StringBuilder("[ ");
|
|
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;
|
|
}
|
|
#endregion
|
|
|
|
#region SyncedData
|
|
public override void CollectSyncedData(byte[] data, ref int index, NetworkEventType eventType)
|
|
{
|
|
// Nothing
|
|
}
|
|
|
|
public override bool WriteSyncedData(byte[] data, ref int index, NetworkEventType eventType)
|
|
{
|
|
switch (eventType)
|
|
{
|
|
case NetworkEventType.Pause:
|
|
Pause();
|
|
break;
|
|
case NetworkEventType.Resume:
|
|
Resume();
|
|
break;
|
|
case NetworkEventType.Step:
|
|
Step();
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
#endregion
|
|
|
|
#region Debug
|
|
public void SimulateSyncToTimestamp(float timestamp)
|
|
{
|
|
//SyncToTimestamp(timestamp);
|
|
}
|
|
|
|
public void WriteDebugOutput(TMP_InputField debugOutput)
|
|
{
|
|
debugOutput.text = $"{nameof(NetworkManager)}:\n" +
|
|
$"IsOwner: {IsOwner}\n" +
|
|
$"Ready: {Ready}\n" +
|
|
$"Synced: {Synced}\n" +
|
|
$"hasFullSyncReady: {hasFullSyncReady}\n" +
|
|
$"lastEventId: {lastEventId}\n" +
|
|
$"Time.fixedTime: {Time.fixedTime}\n" +
|
|
$"startTime: {startTime}\n" +
|
|
$"targetTicks: {targetTicks}\n" +
|
|
$"SyncedTime: {SyncedTime}\n" +
|
|
$"Dt: {SyncedDeltaTime}\n" +
|
|
$"BufferIndex: {eventsQueueIndex}\n" +
|
|
$"BufferIndexHistory: {ArrayToString(eventTransmissionHistory)}\n" +
|
|
$"\n";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Text field to display debug info in.
|
|
/// </summary>
|
|
[SerializeField] private TMP_InputField debugOutput;
|
|
|
|
/// <summary>
|
|
/// An animator which visualizes whether the current perspective is the owner.
|
|
/// </summary>
|
|
[SerializeField] private Animator DebugImageToIndicateOwner;
|
|
|
|
/// <summary>
|
|
/// An animator which visualizes whether the NetworkManager is synced.
|
|
/// </summary>
|
|
[SerializeField] private Animator DebugImageToIndicateSynced;
|
|
|
|
/// <summary>
|
|
/// An animator which visualizes whether the NetworkManager is synced.
|
|
/// </summary>
|
|
[SerializeField] private Animator DebugImageToIndicateReady;
|
|
|
|
private NetworkManagerTester tester;
|
|
public void SetNetworkManagerTester(NetworkManagerTester tester)
|
|
{
|
|
this.tester = tester;
|
|
}
|
|
|
|
public byte[] NetworkedData
|
|
{
|
|
get => networkedData;
|
|
set => networkedData = value;
|
|
}
|
|
|
|
public bool SerializationRequested => serializationRequested;
|
|
|
|
public void SetIsOwner(bool isOwner) => IsOwner = isOwner;
|
|
|
|
public void DoFullSync()
|
|
{
|
|
SendEventSoon(NetworkEventType.FullSyncForced);
|
|
}
|
|
|
|
public void Pause()
|
|
{
|
|
paused = true;
|
|
stepNext = false;
|
|
}
|
|
|
|
private void Resume()
|
|
{
|
|
paused = false;
|
|
stepNext = false;
|
|
}
|
|
|
|
private void Step()
|
|
{
|
|
paused = true;
|
|
stepNext = true;
|
|
}
|
|
|
|
public void PauseButtonPressed()
|
|
{
|
|
Pause();
|
|
SendEventSoon(NetworkEventType.Pause);
|
|
}
|
|
|
|
public void ResumeButtonPressed()
|
|
{
|
|
Resume();
|
|
SendEventSoon(NetworkEventType.Resume);
|
|
}
|
|
|
|
public void StepButtonPressed()
|
|
{
|
|
Step();
|
|
SendEventSoon(NetworkEventType.Step);
|
|
}
|
|
#endregion
|
|
}
|
|
}
|