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, PacManTurn = 2, StartGameButtonPressed = 3, SyncPellets = 4, GhostUpdate = 5, TimeSequenceSync = 6, 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 /// /// The root from which this will look for to control. /// [SerializeField] private GameObject root; /// /// The delay in ticks at which the receiving side replays events. /// [SerializeField] private int delay = 50; /// /// The maximum amount of times a message is sent. /// [SerializeField] private int maxEventSendTries = 3; /// /// How long to wait since last message to send next ping. /// [SerializeField] private int pingDelay = 15; /// /// The time delta at which updates occur. /// [SerializeField] private float tickDelta = 0.0166666667f; #endregion #region Constants /// /// The maximum amount of events in the buffer. /// private const int BufferMaxTotalEvents = 255; /// /// The maximum amount of events in the buffer. /// private const int MaxEventSize = ushort.MaxValue; /// /// The index in an event where the event size is stored. /// private const ushort HeaderEventSizeIndex = 0; /// /// The index in an event where the timestamp is stored. /// private const ushort HeaderTimestampIndex = 2; /// /// The index in an event where the event id is stored. /// private const ushort HeaderEventIdIndex = 6; /// /// The index in an event where the event type is stored. /// private const ushort HeaderEventTypeIndex = 7; /// /// The total length of the header of an event, in bytes. /// private const ushort HeaderLength = 8; #endregion #region Private attributes /// /// Whether has been called successfully. /// private bool initialized = false; /// /// Subscribers to each . /// private SyncedObject[][] networkEventSubscribers; /// /// Indices for . /// private int[] networkEventSubscribersIndices; /// /// Subscribers for . /// private SyncedObject[] syncedUpdateSubscribers; /// /// The that corresponds to tick 0. /// private float startTime; /// /// Time in ticks since start of game. /// private int targetTicks; /// /// True if time is paused /// private bool paused; /// /// True if a step should happen next update /// private bool stepNext; /// /// Time at which next received event occured, in ticks. /// private int nextEventTime; /// /// Amounot of retries in a row without a successful sync. /// private int retriesWithoutSuccess; /// /// 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. /// private bool hasFullSyncReady; /// /// True if serialization has been requestsed /// private bool serializationRequested; /// /// Events to send at the end of SyncedUpdate cycle /// private NetworkEventType[] eventsToSend; /// /// Index for /// private int eventsToSendIndex; /// /// Queue of events to be transmitted or processed. /// private byte[][] eventsQueue; /// /// Index of . /// private int eventsQueueIndex; /// /// The value of at the last transmission. /// private int eventsQueueIndexAtLastTransmission; /// /// Counts of new events at recent transmissions. /// private int[] eventTransmissionHistory; /// /// Index of . /// private int eventTransmissionHistoryIndex; /// /// Time of last event transmission, in ticks. /// private int lastEventTransmissionTime; /// /// The message id of the most recent event created or received. /// private byte lastEventId; /// /// Data which is currently available on the network. /// [UdonSynced] private byte[] networkedData = new byte[0]; #endregion #region Public fields private bool ready = false; /// /// Whether this is ready to transmit or receive data. /// If false, networking is disabled and this acts as a pass-through. /// public bool Ready { get => ready; private set { ready = value; if (DebugImageToIndicateReady != null) { DebugImageToIndicateReady.SetFloat("Color", value ? 1 : 0); } } } /// /// Whether the current perspective is synced with the owner. (Always true if current perspective is owner.) /// public bool Synced { get => synced; private set { synced = value; if (DebugImageToIndicateSynced != null) { DebugImageToIndicateSynced.SetFloat("Color", value ? 1 : 0); } } } private bool synced = false; /// /// The time since start of game, in ticks. /// public int SyncedTimeTicks { get; private set; } /// /// The time since start of game which is currently being simulated. /// public float SyncedTime { get; private set; } /// /// Time since the last simulation, in seconds. /// public float SyncedDeltaTime => tickDelta; /// /// 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. /// public bool IsEventUpdate { get; private set; } /// /// Is the local user owner? /// 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 /// /// Initializes the . Call afterwards to activate networking. /// 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(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 } else { 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) { if (!Ready) { return; } if (!IsOwner) { //Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(SendEventSoon)} while not the owner!"); return; } if (eventsQueueIndex > eventsToSend.Length) { Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) {nameof(eventsToSend)} overflow!"); HandleError(false); return; } eventsToSend[eventsToSendIndex++] = eventType; } private void ProcessEventsToSend() { for (int i = 0; i < eventsToSendIndex; i++) { SendEventNow(eventsToSend[0]); } eventsToSendIndex = 0; } public void SendEventNow(NetworkEventType eventType) { if (!Ready) { return; } if (!IsOwner) { Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(SendEventNow)} while not the owner!"); return; } var timestamp = SyncedTimeTicks; var eventId = GetNextEventId(lastEventId); InitializeEvent(eventType, timestamp, eventId, out byte[] data, out var index); var subscibers = GetEventSubscribers(eventType); if (subscibers != null) { foreach (var obj in subscibers) { obj.CollectSyncedData(data, ref index, eventType); } } // 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; } 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; eventsToSend = new NetworkEventType[BufferMaxTotalEvents]; eventsToSendIndex = 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"; } /// /// Text field to display debug info in. /// [SerializeField] private TMP_InputField debugOutput; /// /// An animator which visualizes whether the current perspective is the owner. /// [SerializeField] private Animator DebugImageToIndicateOwner; /// /// An animator which visualizes whether the NetworkManager is synced. /// [SerializeField] private Animator DebugImageToIndicateSynced; /// /// An animator which visualizes whether the NetworkManager is synced. /// [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.FullSync); } public void DoPelletSync() { SendEventSoon(NetworkEventType.SyncPellets); } public void DoGhostSync() { SendEventSoon(NetworkEventType.GhostUpdate); } public void DoTimeSequenceSync() { SendEventSoon(NetworkEventType.TimeSequenceSync); } 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 } }