// // /*=============================================================================== // // Copyright (C) 2024 PhantomsXR Ltd. All Rights Reserved. // // // // This file is part of the Phantom.XRMOD.PhotonModule.Runtime. // // // // The AVPPlatform cannot be copied, distributed, or made available to // // third-parties for commercial purposes without written permission of PhantomsXR Ltd. // // // // Contact info@phantomsxr.com for licensing requests. // // ===============================================================================*/ #if FUSION2 using System; using System.Collections.Generic; using Fusion; using UnityEngine; namespace Phantom.XRMOD.PhotonModule.Runtime { public interface INetworkEvent : INetworkStruct { } /// /// The TickAlignedEventRelay is a networked object that gives each peer in shared mode a list /// of outgoing events meant for the State Authority of other peers. /// /// Each peer will process all lists for all other peers looking for events destined for itself, /// and then execute that event. /// /// The primary reason to use this over a regular RPC is that it allow synchronous execution of /// state changes for multiple State Authorities. /// /// For example, in Tanknarok, when peer A fires a bullet on peer B, it is A that detects the collision, /// removes the bullet, triggers an explosion FX and decreases ammo count if relevant. /// /// However, A cannot alter the visual state of B or reduce its HP since it does not have /// StateAuthority over it, so instead it sends an event to tell B to do it. /// /// Because the event is part of A's state, it arrives at B in the same tick as the bullet /// destruction and the explosion FX, and everything will occur simultaneously as seen from B's perspective. /// (Note however that B will probably not be in the same state it was when A registered the event) /// /// This is not a silver bullet, and does not generally replace RPCs. Specifically, the /// need to pre-allocate the event structure at build-time means there are certain limitations: /// /// * You need to have a reasonable limit on the number of events that may be sent per tick /// * The event structure uses memory based on the largest possible event you have, so need to keep event size down, or use multiple event relays. /// * There's a risk of loosing events because the buffer is cyclic and will re-use slots as soon as they have been sent. /// public class TickAlignedEventRelay : NetworkBehaviour { // Theoretical maximum number of events you'll ever send with each relay in a single tick. // In reality this needs to be a couple of times larger than that to avoid loosing events due to package drops. const int _CONST_MAX_EVENTS = 10; // The maximum size of any event sent with the relay (in bytes). const int _CONST_MAX_EVENT_SIZE = 24; // Each event has a header which identifies the event and its intended target authority as well as a byte array payload. private struct EventHeader : INetworkStruct { public int ID { get; set; } public int Type { get; set; } public NetworkId Target { get; set; } } [Networked, Capacity(_CONST_MAX_EVENTS)] private NetworkArray EventHeaders => default; [Networked, Capacity(_CONST_MAX_EVENTS * _CONST_MAX_EVENT_SIZE)] private NetworkArray EventBuffer => default; private int nextEventIndex = 1; private int handledEventIndex; private unsafe delegate void ITypeWrapper(int _typeIndex, byte* _data); private readonly List registeredTypes = new(); private readonly List listeners = new(); /// /// Register an event listener for a specific type of event. When you call this method a map of event type and type IDs /// is built dynamically, so it is crucial that calls to this method for any given relay is always done in the same order /// on all peers. /// /// Preferably, call this only from Spawned() of a single NB, and register all your listeners unconditionally. /// /// Note that the callback will trigger on both the source of the event (immediately for predictive updates), /// as well as on State Authority and proxies. Deal with that how you like. /// /// The callback that will receive the event /// Type of event struct public void RegisterEventListener(Action _listener) where T : unmanaged, INetworkEvent { int tmp_MonitoredTypeIndex = registeredTypes.IndexOf(typeof(T)); if (tmp_MonitoredTypeIndex < 0) { tmp_MonitoredTypeIndex = registeredTypes.Count; registeredTypes.Add(typeof(T)); } unsafe { listeners.Add((_typeIndex, _data) => { if (_typeIndex == tmp_MonitoredTypeIndex) { _listener(*(T*) _data); } }); } } /// /// Send event to be executed on the State Authority of another peer. /// /// This will trigger on the local peer immediately, regardless of whether it is StateAuthority or not. /// /// A relay owned by the target StateAuthority (May be *this*, but generally isn't) /// The event struct to send /// The type of the event struct public void RaiseEventFor(TickAlignedEventRelay _target, T _evt) where T : unmanaged, INetworkEvent { unsafe { Assert.Check(sizeof(T) < _CONST_MAX_EVENT_SIZE, $"Event of type {typeof(T)} is larger ({sizeof(T)} bytes) than MAX_EVENT_SIZE ({_CONST_MAX_EVENT_SIZE} bytes)"); } byte[] tmp_Bytes = SerializeValueType(_evt); // Predict it locally int tmp_TypeIndex = registeredTypes.IndexOf(typeof(T)); _target.OnTickAlignedEvent(tmp_TypeIndex, tmp_Bytes); // Do nothing in hosted mode - we're either authority over everything or nothing at all, nothing more to do here. if (Runner == null || Runner.Topology != Topologies.Shared) return; // If we don't have StateAuthority we're going to have to let SA know so it can change it properly. if (!_target.HasStateAuthority) { EventHeader tmp_Head = new() { Target = _target.Object.Id, ID = nextEventIndex, Type = tmp_TypeIndex }; int tmp_Index = nextEventIndex % EventHeaders.Length; EventHeaders.Set(tmp_Index, tmp_Head); for (int tmp_Idx = 0; tmp_Idx < tmp_Bytes.Length; tmp_Idx++) { EventBuffer.Set(tmp_Index * _CONST_MAX_EVENT_SIZE + tmp_Idx, tmp_Bytes[tmp_Idx]); } nextEventIndex++; } } private unsafe void OnTickAlignedEvent(int _typeIndex, byte[] _evt) { fixed (byte* tmp_Buffer = _evt) { foreach (ITypeWrapper tmp_Listener in listeners) { tmp_Listener(_typeIndex, tmp_Buffer); } } } public override void Render() { if (HasStateAuthority) return; // If we have State Authority then these are our outgoing messages and none of them are for us! if (TryGetSnapshotsBuffers(out var tmp_FromBuffer, out _, out _)) { var tmp_HeadersReader = GetArrayReader(nameof(EventHeaders)); var tmp_Headers = tmp_HeadersReader.Read(tmp_FromBuffer); var tmp_ByteReader = GetArrayReader(nameof(EventBuffer)); var tmp_Bytes = tmp_ByteReader.Read(tmp_FromBuffer); int tmp_HandledId = handledEventIndex; for (int tmp_Idx = 0; tmp_Idx < tmp_Headers.Length; tmp_Idx++) { EventHeader tmp_Head = tmp_Headers[tmp_Idx]; if (tmp_Head.ID > handledEventIndex) { tmp_HandledId = Mathf.Max(tmp_HandledId, tmp_Head.ID); if (Runner.TryFindObject(tmp_Head.Target, out NetworkObject tmp_No)) { TickAlignedEventRelay tmp_Behaviour = tmp_No.GetComponent(); byte[] tmp_Buffer = new byte[_CONST_MAX_EVENT_SIZE]; for (int tmp_Bdx = 0; tmp_Bdx < tmp_Buffer.Length; tmp_Bdx++) tmp_Buffer[tmp_Bdx] = tmp_Bytes[tmp_Idx * _CONST_MAX_EVENT_SIZE + tmp_Bdx]; tmp_Behaviour.OnTickAlignedEvent(tmp_Head.Type, tmp_Buffer); } } } handledEventIndex = tmp_HandledId; } } public static unsafe byte[] SerializeValueType(in T _value) where T : unmanaged { byte[] tmp_Result = new byte[sizeof(T)]; fixed (byte* tmp_Dst = tmp_Result) *(T*) tmp_Dst = _value; return tmp_Result; } } } #endif