From a0a0f6303d84a0d5949dcc01fb4775a5f35a9ad9 Mon Sep 17 00:00:00 2001 From: Brajanowski Date: Mon, 21 Jul 2025 09:04:43 +0200 Subject: [PATCH] multiplayer refactor --- .../Code/Development/DebugOverlayView.cs | 4 + .../Engine/Code/Extensions/NativeArrayEx.cs | 19 + .../Code/Extensions/NativeArrayEx.cs.meta | 3 + Runtime/Engine/Code/Main/NetworkSystem.cs | 273 ---------- Runtime/Engine/Code/Main/RR.cs | 70 +-- Runtime/Engine/Code/Network.meta | 3 + .../Code/Network/DataSerializationUtils.cs | 40 ++ .../Network/DataSerializationUtils.cs.meta | 3 + .../Code/Network/ISerializableEntity.cs | 8 + .../Code/Network/ISerializableEntity.cs.meta | 3 + .../Code/Network/NetworkBufferReader.cs | 396 ++++++++++++++ .../Code/Network/NetworkBufferReader.cs.meta | 3 + .../Code/Network/NetworkBufferWriter.cs | 316 +++++++++++ .../Code/Network/NetworkBufferWriter.cs.meta | 3 + .../Engine/Code/Network/NetworkPacketQueue.cs | 281 ++++++++++ .../Code/Network/NetworkPacketQueue.cs.meta | 3 + .../NetworkPlayerController.cs | 35 +- .../NetworkPlayerController.cs.meta | 0 Runtime/Engine/Code/Network/NetworkSystem.cs | 515 ++++++++++++++++++ .../{Main => Network}/NetworkSystem.cs.meta | 0 .../NetworkWorldController.cs | 2 +- .../NetworkWorldController.cs.meta | 0 Runtime/Engine/Code/RConsts.cs | 3 + Runtime/Engine/Code/Simulation/Actor.cs | 202 ++++--- .../Engine/Code/Simulation/ActorsManager.cs | 346 ++++++------ Runtime/Engine/Code/Steam/SteamManager.cs | 14 +- .../Engine/NetworkBufferWriterReaderTests.cs | 204 +++++++ .../NetworkBufferWriterReaderTests.cs.meta | 3 + .../Engine/RebootKit.Engine.Tests.asmdef | 37 +- 29 files changed, 2186 insertions(+), 603 deletions(-) create mode 100644 Runtime/Engine/Code/Extensions/NativeArrayEx.cs create mode 100644 Runtime/Engine/Code/Extensions/NativeArrayEx.cs.meta delete mode 100644 Runtime/Engine/Code/Main/NetworkSystem.cs create mode 100644 Runtime/Engine/Code/Network.meta create mode 100644 Runtime/Engine/Code/Network/DataSerializationUtils.cs create mode 100644 Runtime/Engine/Code/Network/DataSerializationUtils.cs.meta create mode 100644 Runtime/Engine/Code/Network/ISerializableEntity.cs create mode 100644 Runtime/Engine/Code/Network/ISerializableEntity.cs.meta create mode 100644 Runtime/Engine/Code/Network/NetworkBufferReader.cs create mode 100644 Runtime/Engine/Code/Network/NetworkBufferReader.cs.meta create mode 100644 Runtime/Engine/Code/Network/NetworkBufferWriter.cs create mode 100644 Runtime/Engine/Code/Network/NetworkBufferWriter.cs.meta create mode 100644 Runtime/Engine/Code/Network/NetworkPacketQueue.cs create mode 100644 Runtime/Engine/Code/Network/NetworkPacketQueue.cs.meta rename Runtime/Engine/Code/{Main => Network}/NetworkPlayerController.cs (70%) rename Runtime/Engine/Code/{Main => Network}/NetworkPlayerController.cs.meta (100%) create mode 100644 Runtime/Engine/Code/Network/NetworkSystem.cs rename Runtime/Engine/Code/{Main => Network}/NetworkSystem.cs.meta (100%) rename Runtime/Engine/Code/{Main => Network}/NetworkWorldController.cs (72%) rename Runtime/Engine/Code/{Main => Network}/NetworkWorldController.cs.meta (100%) create mode 100644 Tests/Runtime/Engine/NetworkBufferWriterReaderTests.cs create mode 100644 Tests/Runtime/Engine/NetworkBufferWriterReaderTests.cs.meta diff --git a/Runtime/Engine/Code/Development/DebugOverlayView.cs b/Runtime/Engine/Code/Development/DebugOverlayView.cs index 4a025e0..eabd754 100644 --- a/Runtime/Engine/Code/Development/DebugOverlayView.cs +++ b/Runtime/Engine/Code/Development/DebugOverlayView.cs @@ -1,5 +1,6 @@ using System.Text; using RebootKit.Engine.Main; +using RebootKit.Engine.Network; using RebootKit.Engine.Simulation; using RebootKit.Engine.UI; using UnityEngine; @@ -48,6 +49,8 @@ namespace RebootKit.Engine.Development { m_StringBuilder.Append(resolution.height); m_StringBuilder.Append("@"); m_StringBuilder.Append(resolution.refreshRateRatio); + m_StringBuilder.Append("Hz | IsLittleEndian: "); + m_StringBuilder.Append(System.BitConverter.IsLittleEndian ? "true" : "false"); m_StringBuilder.AppendLine(); } @@ -60,6 +63,7 @@ namespace RebootKit.Engine.Development { } m_StringBuilder.Append($"IsServer: {RR.IsServer().ToString()}"); + m_StringBuilder.Append($" | TickRate: {NetworkSystem.TickRate.IndexValue.ToString()}"); m_StringBuilder.Append($" | IsClient: {RR.IsClient().ToString()}"); m_StringBuilder.Append($" | WorldID: {network.WorldID.ToString()}"); m_StringBuilder.Append($" | Clients: {network.Clients.Count.ToString()}"); diff --git a/Runtime/Engine/Code/Extensions/NativeArrayEx.cs b/Runtime/Engine/Code/Extensions/NativeArrayEx.cs new file mode 100644 index 0000000..b9e2278 --- /dev/null +++ b/Runtime/Engine/Code/Extensions/NativeArrayEx.cs @@ -0,0 +1,19 @@ +using System.Text; +using Unity.Collections; + +namespace RebootKit.Engine.Extensions { + public static class NativeArrayEx { + public static string ToHexString(this NativeArray array) { + if (array.IsCreated) { + StringBuilder sb = new StringBuilder(array.Length * 3); + for (int i = 0; i < array.Length; i++) { + sb.AppendFormat("{0:X2} ", array[i]); + } + + return sb.ToString(); + } + return string.Empty; + } + + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Extensions/NativeArrayEx.cs.meta b/Runtime/Engine/Code/Extensions/NativeArrayEx.cs.meta new file mode 100644 index 0000000..9042c6b --- /dev/null +++ b/Runtime/Engine/Code/Extensions/NativeArrayEx.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2921b0badb164eef8dae6f95a6e4c002 +timeCreated: 1753068953 \ No newline at end of file diff --git a/Runtime/Engine/Code/Main/NetworkSystem.cs b/Runtime/Engine/Code/Main/NetworkSystem.cs deleted file mode 100644 index d0fc142..0000000 --- a/Runtime/Engine/Code/Main/NetworkSystem.cs +++ /dev/null @@ -1,273 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using Cysharp.Threading.Tasks; -using NUnit.Framework; -using RebootKit.Engine.Simulation; -using Unity.Collections; -using Unity.Netcode; -using UnityEngine; -using Logger = RebootKit.Engine.Foundation.Logger; - -namespace RebootKit.Engine.Main { - enum NetworkClientSyncState { - NotReady, - LoadingWorld, - PreparingForActorsSync, - SyncingActors, - Ready - } - - struct NetworkClientState : INetworkSerializable { - public ulong ClientID; - public NetworkClientSyncState SyncState; - public int ActorsSyncPacketsLeft; - - public bool IsReady { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get { - return SyncState == NetworkClientSyncState.Ready; - } - } - - public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { - serializer.SerializeValue(ref ClientID); - serializer.SerializeValue(ref SyncState); - serializer.SerializeValue(ref ActorsSyncPacketsLeft); - } - } - - public class NetworkSystem : NetworkBehaviour { - static readonly Logger s_Logger = new Logger(nameof(NetworkSystem)); - - [field: SerializeField] public ActorsManager Actors { get; private set; } - - internal readonly Dictionary Clients = new Dictionary(); - - public FixedString512Bytes WorldID { get; private set; } = new FixedString512Bytes(""); - bool m_IsChangingWorld = false; - - public ulong LocalClientID { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get { - return NetworkManager.Singleton.LocalClientId; - } - } - - // - // @MARK: Unity callbacks - // - void Awake() { - RR.NetworkSystemInstance = this; - } - - // - // @MARK: NetworkBehaviour callbacks - // - public override void OnNetworkSpawn() { - base.OnNetworkSpawn(); - - NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected; - NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnect; - } - - public override void OnNetworkDespawn() { - base.OnNetworkDespawn(); - - NetworkManager.Singleton.OnClientConnectedCallback -= OnClientConnected; - NetworkManager.Singleton.OnClientDisconnectCallback -= OnClientDisconnect; - } - - void OnClientConnected(ulong clientID) { - if (!IsServer) { - return; - } - - s_Logger.Info($"OnClientConnected: {clientID}"); - - NetworkClientState newClientState = new NetworkClientState { - ClientID = clientID, - SyncState = NetworkClientSyncState.NotReady - }; - Clients.Add(clientID, newClientState); - - if (clientID != NetworkManager.Singleton.LocalClientId) { - foreach (NetworkClientState state in Clients.Values) { - UpdateClientStateRpc(state, RpcTarget.Single(clientID, RpcTargetUse.Temp)); - } - } - - if (!WorldID.IsEmpty) { - s_Logger.Info($"Synchronizing world load for client {clientID} with world ID '{WorldID}'"); - ClientLoadWorldRpc(WorldID.ToString(), RpcTarget.Single(clientID, RpcTargetUse.Temp)); - } - } - - void OnClientDisconnect(ulong clientID) { - s_Logger.Info($"OnClientDisconnect: {clientID}"); - Clients.Remove(clientID); - } - - // - // @MARK: Server API - // - public void KickClient(ulong clientID, string reason = "Kicked by server") { - if (!IsServer) { - s_Logger.Error("Only server can kick clients."); - return; - } - - if (NetworkManager.Singleton.ConnectedClients.TryGetValue(clientID, out NetworkClient client)) { - NetworkManager.Singleton.DisconnectClient(clientID, reason); - s_Logger.Info($"Kicked client {clientID}: {reason}"); - } else { - s_Logger.Error($"Client {clientID} not found."); - } - } - - public void SetCurrentWorld(string worldID) { - if (!IsServer) { - s_Logger.Error("Only server can set the current world."); - return; - } - - if (m_IsChangingWorld) { - s_Logger.Error($"Already changing world to '{WorldID}'. Please wait until the current world change is complete."); - return; - } - - WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID); - if (worldConfigAsset is null) { - s_Logger.Error($"Failed to set current world: World config asset for '{worldID}' not found."); - return; - } - - WorldID = worldID; - - foreach ((ulong _, NetworkClientState clientState) in Clients.ToList()) { - NetworkClientState state = clientState; - state.SyncState = NetworkClientSyncState.LoadingWorld; - UpdateClientState(state); - } - - ServerLoadWorldAsync(worldConfigAsset, destroyCancellationToken).Forget(); - } - - async UniTask ServerLoadWorldAsync(WorldConfigAsset asset, CancellationToken cancellationToken) { - s_Logger.Info($"ServerLoadWorldAsync: {asset.Config.name}"); - - m_IsChangingWorld = true; - - RR.World.Unload(); - RR.CloseMainMenu(); - - await RR.World.LoadAsync(asset.Config, cancellationToken); - - m_IsChangingWorld = false; - - if (!TryGetClientState(NetworkManager.Singleton.LocalClientId, out NetworkClientState localClientState)) { - s_Logger.Error($"Local client state not found for client ID {NetworkManager.Singleton.LocalClientId}."); - RR.Disconnect(); - return; - } - - localClientState.SyncState = NetworkClientSyncState.Ready; - UpdateClientState(localClientState); - - RR.GameInstance.PlayerBecameReady(localClientState.ClientID); - - ClientLoadWorldRpc(asset.name, RpcTarget.NotMe); - } - - [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)] - void ClientLoadWorldRpc(string worldID, RpcParams rpcParams) { - WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID); - if (worldConfigAsset is null) { - s_Logger.Error($"World config asset for '{worldID}' not found."); - RR.Disconnect(); - return; - } - - ClientLoadWorldAsync(worldID, destroyCancellationToken).Forget(); - } - - async UniTask ClientLoadWorldAsync(string worldID, CancellationToken cancellationToken) { - s_Logger.Info($"ClientLoadWorldAsync: {worldID}"); - - WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID); - if (worldConfigAsset is null) { - s_Logger.Error($"World config asset for '{worldID}' not found."); - return; - } - - RR.World.Unload(); - RR.CloseMainMenu(); - - await RR.World.LoadAsync(worldConfigAsset.Config, cancellationToken); - - WorldID = worldID; - ClientLoadedWorldRpc(worldID); - } - - [Rpc(SendTo.Server, Delivery = RpcDelivery.Reliable)] - void ClientLoadedWorldRpc(string worldID, RpcParams rpcParams = default) { - ulong clientID = rpcParams.Receive.SenderClientId; - - if (!WorldID.Equals(worldID)) { - s_Logger.Error($"Client {clientID} tried to load world '{worldID}', but server is in world '{WorldID}'."); - NetworkManager.Singleton.DisconnectClient(clientID, "World mismatch!"); - return; - } - - if (Clients.TryGetValue(clientID, out NetworkClientState clientState)) { - Actors.InitializeActorsForClient(clientID); - } else { - NetworkManager.Singleton.DisconnectClient(clientID, "Client is not registered!"); - } - } - - // - // @MARK: Internal - // - internal bool TryGetClientState(ulong clientID, out NetworkClientState clientState) { - return Clients.TryGetValue(clientID, out clientState); - } - - internal void UpdateClientState(NetworkClientState clientState) { - if (!IsServer) { - s_Logger.Error("UpdateClientState can only be called on the server."); - return; - } - - Clients[clientState.ClientID] = clientState; - UpdateClientStateRpc(clientState, RpcTarget.NotServer); - } - - [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)] - void UpdateClientStateRpc(NetworkClientState newState, RpcParams rpcParams) { - Clients[newState.ClientID] = newState; - } - - internal void ClientSynchronizedActors(ulong clientID) { - if (TryGetClientState(clientID, out NetworkClientState state)) { - state.SyncState = NetworkClientSyncState.Ready; - UpdateClientState(state); - - RR.GameInstance.PlayerBecameReady(clientID); - } else { - s_Logger.Error($"Client state for {clientID} not found."); - } - } - - internal int GetReadyClientsCount() { - int count = 0; - foreach (NetworkClientState clientState in Clients.Values) { - if (clientState.IsReady) { - count++; - } - } - return count; - } - } -} \ No newline at end of file diff --git a/Runtime/Engine/Code/Main/RR.cs b/Runtime/Engine/Code/Main/RR.cs index 270fc84..3bc0711 100755 --- a/Runtime/Engine/Code/Main/RR.cs +++ b/Runtime/Engine/Code/Main/RR.cs @@ -7,6 +7,7 @@ using R3; using RebootKit.Engine.Console; using RebootKit.Engine.Foundation; using RebootKit.Engine.Input; +using RebootKit.Engine.Network; using RebootKit.Engine.Simulation; using RebootKit.Engine.Steam; using Unity.Netcode; @@ -26,19 +27,16 @@ namespace RebootKit.Engine.Main { static readonly Logger s_Logger = new Logger("RR"); [ConfigVar("con.write_log", 1, "Enables writing game log to console output")] - static ConfigVar s_writeLogToConsole; - - [ConfigVar("sv.tick_rate", 24, "Server tick rate in Hz")] - public static ConfigVar TickRate; - + static ConfigVar s_WriteLogToConsole; + internal static EngineConfigAsset EngineConfig; - static DisposableBag s_disposableBag; - static DisposableBag s_servicesBag; + static DisposableBag s_DisposableBag; + static DisposableBag s_ServicesBag; - static AsyncOperationHandle s_mainMenuSceneHandle; + static AsyncOperationHandle s_MainMenuSceneHandle; - static NetworkSystem s_networkSystemPrefab; + static NetworkSystem s_NetworkSystemPrefab; internal static NetworkSystem NetworkSystemInstance; internal static ConsoleService Console { get; private set; } @@ -49,10 +47,6 @@ namespace RebootKit.Engine.Main { public static Game GameInstance { get; internal set; } - public static ulong TickCount { get; private set; } - public static event Action ServerTick = delegate { }; - public static event Action ClientTick = delegate { }; - // Lifecycle API // @NOTE: This method is called at the very start of the game, when boot scene loaded. @@ -62,13 +56,13 @@ namespace RebootKit.Engine.Main { EngineConfig = configAsset; s_Logger.Info("Initializing"); - s_servicesBag = new DisposableBag(); - s_disposableBag = new DisposableBag(); + s_ServicesBag = new DisposableBag(); + s_DisposableBag = new DisposableBag(); s_Logger.Info("Registering core services"); Console = CreateService(); Input = new InputService(EngineConfig.inputConfig); - s_servicesBag.Add(Input); + s_ServicesBag.Add(Input); World = CreateService(); await InitializeAssetsAsync(cancellationToken); @@ -80,7 +74,7 @@ namespace RebootKit.Engine.Main { // @NOTE: This method is called after the main scene is loaded. internal static async UniTask RunAsync(CancellationToken cancellationToken) { - s_networkSystemPrefab = + s_NetworkSystemPrefab = Resources.Load(RConsts.k_CoreNetworkGameSystemsResourcesPath); NetworkManager.Singleton.OnConnectionEvent += OnConnectionEvent; @@ -97,7 +91,7 @@ namespace RebootKit.Engine.Main { Observable.EveryUpdate() .Subscribe(_ => Tick()) - .AddTo(ref s_disposableBag); + .AddTo(ref s_DisposableBag); await OpenMainMenuAsync(cancellationToken); @@ -133,8 +127,8 @@ namespace RebootKit.Engine.Main { SteamManager.Shutdown(); #endif - s_servicesBag.Dispose(); - s_disposableBag.Dispose(); + s_ServicesBag.Dispose(); + s_DisposableBag.Dispose(); } // Assets API @@ -176,16 +170,16 @@ namespace RebootKit.Engine.Main { return; } - s_mainMenuSceneHandle = Addressables.LoadSceneAsync(EngineConfig.mainMenuScene, LoadSceneMode.Additive); - await s_mainMenuSceneHandle; + s_MainMenuSceneHandle = Addressables.LoadSceneAsync(EngineConfig.mainMenuScene, LoadSceneMode.Additive); + await s_MainMenuSceneHandle; } internal static void CloseMainMenu() { - if (!s_mainMenuSceneHandle.IsValid()) { + if (!s_MainMenuSceneHandle.IsValid()) { return; } - Addressables.UnloadSceneAsync(s_mainMenuSceneHandle); + Addressables.UnloadSceneAsync(s_MainMenuSceneHandle); } public static void SetServerWorld(string worldID) { @@ -253,13 +247,13 @@ namespace RebootKit.Engine.Main { } TService service = asset.Create(); - s_servicesBag.Add(service); + s_ServicesBag.Add(service); return service; } public static TService CreateService() where TService : class, IService { TService service = Activator.CreateInstance(); - s_servicesBag.Add(service); + s_ServicesBag.Add(service); return service; } @@ -382,28 +376,8 @@ namespace RebootKit.Engine.Main { GameInstance.SendChatMessageRpc(message); } - - static float s_tickTimer; - + static void Tick() { - float deltaTime = Time.deltaTime; - - float minTickTime = 1.0f / TickRate.IndexValue; - s_tickTimer += deltaTime; - - while (s_tickTimer >= minTickTime) { - s_tickTimer -= minTickTime; - - if (IsServer()) { - ServerTick?.Invoke(TickCount); - } - - if (IsClient()) { - ClientTick?.Invoke(); - } - - TickCount++; - } } static void OnConnectionEvent(NetworkManager network, ConnectionEventData data) { @@ -416,7 +390,7 @@ namespace RebootKit.Engine.Main { GameInstance = Object.Instantiate(EngineConfig.gamePrefab); GameInstance.NetworkObject.Spawn(); - NetworkSystemInstance = Object.Instantiate(s_networkSystemPrefab); + NetworkSystemInstance = Object.Instantiate(s_NetworkSystemPrefab); NetworkSystemInstance.NetworkObject.Spawn(); } diff --git a/Runtime/Engine/Code/Network.meta b/Runtime/Engine/Code/Network.meta new file mode 100644 index 0000000..57bfa37 --- /dev/null +++ b/Runtime/Engine/Code/Network.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b01874b8658349da857b43006deab7d1 +timeCreated: 1752855354 \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/DataSerializationUtils.cs b/Runtime/Engine/Code/Network/DataSerializationUtils.cs new file mode 100644 index 0000000..30a2807 --- /dev/null +++ b/Runtime/Engine/Code/Network/DataSerializationUtils.cs @@ -0,0 +1,40 @@ +using System; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Netcode; +using UnityEngine.Assertions; + +namespace RebootKit.Engine.Network { + public static class DataSerializationUtils { + public const int k_DefaultMessageSize = 256; + + public static NativeArray Serialize(TEntity entity, + Allocator allocator = Allocator.Temp) + where TEntity : ISerializableEntity { + int size = entity.GetMaxBytes(); + if (size < 0) { + size = k_DefaultMessageSize; + } + + NativeArray data = new NativeArray(size, allocator); + + using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0); + Assert.IsTrue(writer.WillFit(size)); + + if (writer.WillFit(size)) { + entity.Serialize(writer); + return data; + } + + return default; + } + + public static void Deserialize(NativeArray data, ref TEntity entity) + where TEntity : ISerializableEntity { + using NetworkBufferReader reader = new NetworkBufferReader(data); + if (reader.HasNext(data.Length)) { + entity.Deserialize(reader); + } + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/DataSerializationUtils.cs.meta b/Runtime/Engine/Code/Network/DataSerializationUtils.cs.meta new file mode 100644 index 0000000..8970b34 --- /dev/null +++ b/Runtime/Engine/Code/Network/DataSerializationUtils.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 07f4afdf6bb24860b2524b3250238533 +timeCreated: 1752855533 \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/ISerializableEntity.cs b/Runtime/Engine/Code/Network/ISerializableEntity.cs new file mode 100644 index 0000000..a79a5f9 --- /dev/null +++ b/Runtime/Engine/Code/Network/ISerializableEntity.cs @@ -0,0 +1,8 @@ +namespace RebootKit.Engine.Network { + public interface ISerializableEntity { + void Serialize(NetworkBufferWriter writer); + void Deserialize(NetworkBufferReader reader); + + int GetMaxBytes(); + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/ISerializableEntity.cs.meta b/Runtime/Engine/Code/Network/ISerializableEntity.cs.meta new file mode 100644 index 0000000..67a0f0c --- /dev/null +++ b/Runtime/Engine/Code/Network/ISerializableEntity.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0f726d33d24d45e7b7cadf610566622d +timeCreated: 1752855518 \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/NetworkBufferReader.cs b/Runtime/Engine/Code/Network/NetworkBufferReader.cs new file mode 100644 index 0000000..ac3309e --- /dev/null +++ b/Runtime/Engine/Code/Network/NetworkBufferReader.cs @@ -0,0 +1,396 @@ +using System; +using Unity.Collections; +using UnityEngine; +using UnityEngine.Assertions; +using UnityEngine.Pool; + +namespace RebootKit.Engine.Network { + public struct NetworkBufferReader : IDisposable { + class ReaderHandle { + public NativeArray Data; + public int Position; + public bool IsBigEndian; + } + + static readonly IObjectPool s_ReaderPool = new ObjectPool( + () => new ReaderHandle(), + _ => { }, + handle => { + handle.Data = default; + handle.Position = 0; + handle.IsBigEndian = false; + }, + _ => { }, + true, + 256 + ); + + ReaderHandle m_Handle; + + public NetworkBufferReader(NativeArray data, int position = 0) { + Assert.IsTrue(data.IsCreated, "Trying to create a NetworkBufferReader with uncreated data."); + Assert.IsTrue(position >= 0 && position <= data.Length, + "Position must be within the bounds of the data array."); + + m_Handle = s_ReaderPool.Get(); + m_Handle.Data = data; + m_Handle.Position = position; + m_Handle.IsBigEndian = !BitConverter.IsLittleEndian; + } + + public void Dispose() { + if (m_Handle != null) { + s_ReaderPool.Release(m_Handle); + m_Handle = null; + } + } + + public bool HasNext(int size) { + return m_Handle.Position + size <= m_Handle.Data.Length; + } + + public bool Read(out NativeArray value, int size, Allocator allocator = Allocator.Temp) { + Assert.IsTrue(HasNext(size), + $"Not enough data to read the requested size. Requested: {size}, Available: {m_Handle.Data.Length - m_Handle.Position}"); + + value = new NativeArray(size, allocator); + for (int i = 0; i < size; i++) { + value[i] = m_Handle.Data[m_Handle.Position++]; + } + + return true; + } + + public bool Read(out byte value) { + if (!HasNext(1)) { + value = 0; + return false; + } + + Assert.IsTrue(HasNext(1), "Not enough data to read a byte."); + value = m_Handle.Data[m_Handle.Position++]; + return true; + } + + public bool Read(out bool value) { + if (!HasNext(1)) { + value = false; + return false; + } + + value = m_Handle.Data[m_Handle.Position++] != 0; + return true; + } + + public bool Read(out int value) { + value = 0; + + if (!HasNext(4)) { + return false; + } + + if (m_Handle.IsBigEndian) { + value |= m_Handle.Data[m_Handle.Position++] << 24; + value |= m_Handle.Data[m_Handle.Position++] << 16; + value |= m_Handle.Data[m_Handle.Position++] << 8; + value |= m_Handle.Data[m_Handle.Position++]; + } else { + value |= m_Handle.Data[m_Handle.Position++]; + value |= m_Handle.Data[m_Handle.Position++] << 8; + value |= m_Handle.Data[m_Handle.Position++] << 16; + value |= m_Handle.Data[m_Handle.Position++] << 24; + } + + return true; + } + + public bool Read(out short value) { + value = 0; + + if (!HasNext(2)) { + return false; + } + + if (m_Handle.IsBigEndian) { + value |= (short) (m_Handle.Data[m_Handle.Position++] << 8); + value |= (short) (m_Handle.Data[m_Handle.Position++]); + } else { + value |= (short) (m_Handle.Data[m_Handle.Position++]); + value |= (short) (m_Handle.Data[m_Handle.Position++] << 8); + } + + return true; + } + + public bool Read(out ushort value) { + value = 0; + + if (!HasNext(2)) { + return false; + } + + if (m_Handle.IsBigEndian) { + value |= (ushort) (m_Handle.Data[m_Handle.Position++] << 8); + value |= (ushort) (m_Handle.Data[m_Handle.Position++]); + } else { + value |= (ushort) (m_Handle.Data[m_Handle.Position++]); + value |= (ushort) (m_Handle.Data[m_Handle.Position++] << 8); + } + + return true; + } + + public bool Read(out long value) { + value = 0; + + if (!HasNext(8)) { + return false; + } + + if (m_Handle.IsBigEndian) { + value |= (long) m_Handle.Data[m_Handle.Position++] << 56; + value |= (long) m_Handle.Data[m_Handle.Position++] << 48; + value |= (long) m_Handle.Data[m_Handle.Position++] << 40; + value |= (long) m_Handle.Data[m_Handle.Position++] << 32; + value |= (long) m_Handle.Data[m_Handle.Position++] << 24; + value |= (long) m_Handle.Data[m_Handle.Position++] << 16; + value |= (long) m_Handle.Data[m_Handle.Position++] << 8; + value |= (long) m_Handle.Data[m_Handle.Position++]; + } else { + value |= (long) m_Handle.Data[m_Handle.Position++]; + value |= (long) m_Handle.Data[m_Handle.Position++] << 8; + value |= (long) m_Handle.Data[m_Handle.Position++] << 16; + value |= (long) m_Handle.Data[m_Handle.Position++] << 24; + value |= (long) m_Handle.Data[m_Handle.Position++] << 32; + value |= (long) m_Handle.Data[m_Handle.Position++] << 40; + value |= (long) m_Handle.Data[m_Handle.Position++] << 48; + value |= (long) m_Handle.Data[m_Handle.Position++] << 56; + } + + return true; + } + + public bool Read(out ulong value) { + value = 0; + + if (!HasNext(8)) { + return false; + } + + if (m_Handle.IsBigEndian) { + value |= (ulong) m_Handle.Data[m_Handle.Position++] << 56; + value |= (ulong) m_Handle.Data[m_Handle.Position++] << 48; + value |= (ulong) m_Handle.Data[m_Handle.Position++] << 40; + value |= (ulong) m_Handle.Data[m_Handle.Position++] << 32; + value |= (ulong) m_Handle.Data[m_Handle.Position++] << 24; + value |= (ulong) m_Handle.Data[m_Handle.Position++] << 16; + value |= (ulong) m_Handle.Data[m_Handle.Position++] << 8; + value |= (ulong) m_Handle.Data[m_Handle.Position++]; + } else { + value |= (ulong) m_Handle.Data[m_Handle.Position++]; + value |= (ulong) m_Handle.Data[m_Handle.Position++] << 8; + value |= (ulong) m_Handle.Data[m_Handle.Position++] << 16; + value |= (ulong) m_Handle.Data[m_Handle.Position++] << 24; + value |= (ulong) m_Handle.Data[m_Handle.Position++] << 32; + value |= (ulong) m_Handle.Data[m_Handle.Position++] << 40; + value |= (ulong) m_Handle.Data[m_Handle.Position++] << 48; + value |= (ulong) m_Handle.Data[m_Handle.Position++] << 56; + } + + return true; + } + + public bool Read(out float value) { + if (Read(out int intValue)) { + value = System.BitConverter.Int32BitsToSingle(intValue); + return true; + } + + value = 0.0f; + return false; + } + + public bool Read(out Vector2 value) { + Assert.IsTrue(HasNext(sizeof(float) * 2), "Not enough data to read a Vector2."); + + if (Read(out float x) && Read(out float y)) { + value = new Vector2(x, y); + return true; + } + + value = Vector2.zero; + return false; + } + + public bool Read(out Vector3 value) { + Assert.IsTrue(HasNext(sizeof(float) * 3), "Not enough data to read a Vector3."); + + if (Read(out float x) && Read(out float y) && Read(out float z)) { + value = new Vector3(x, y, z); + return true; + } + + value = Vector3.zero; + return false; + } + + public bool Read(out Vector4 value) { + Assert.IsTrue(HasNext(sizeof(float) * 4), "Not enough data to read a Vector4."); + + if (Read(out float x) && Read(out float y) && Read(out float z) && Read(out float w)) { + value = new Vector4(x, y, z, w); + return true; + } + + value = Vector4.zero; + return false; + } + + public bool Read(out Quaternion value) { + Assert.IsTrue(HasNext(sizeof(float) * 4), "Not enough data to read a Quaternion."); + + if (Read(out float x) && Read(out float y) && Read(out float z) && Read(out float w)) { + value = new Quaternion(x, y, z, w); + return true; + } + + value = Quaternion.identity; + return false; + } + + public bool Read(out FixedString32Bytes value) { + Assert.IsTrue(HasNext(32), "Not enough data to read a FixedString32Bytes."); + + NativeArray tempData = new NativeArray(32, Allocator.Temp); + + value = new FixedString32Bytes(); + int length = 0; + + for (int i = 0; i < 32; i++) { + Read(out byte byteValue); + + tempData[i] = byteValue; + + if (byteValue != 0) { + length++; + } + } + + value.Length = length; + for (int i = 0; i < length; i++) { + value[i] = tempData[i]; + } + + tempData.Dispose(); + return true; + } + + public bool Read(out FixedString64Bytes value) { + Assert.IsTrue(HasNext(64), "Not enough data to read a FixedString64Bytes."); + + NativeArray tempData = new NativeArray(64, Allocator.Temp); + + value = new FixedString64Bytes(); + int length = 0; + + for (int i = 0; i < 64; i++) { + Read(out byte byteValue); + + tempData[i] = byteValue; + + if (byteValue != 0) { + length++; + } + } + + value.Length = length; + for (int i = 0; i < length; i++) { + value[i] = tempData[i]; + } + + tempData.Dispose(); + return true; + } + + public bool Read(out FixedString128Bytes value) { + Assert.IsTrue(HasNext(128), "Not enough data to read a FixedString128Bytes."); + + NativeArray tempData = new NativeArray(128, Allocator.Temp); + + value = new FixedString128Bytes(); + int length = 0; + + for (int i = 0; i < 128; i++) { + Read(out byte byteValue); + + tempData[i] = byteValue; + + if (byteValue != 0) { + length++; + } + } + + value.Length = length; + for (int i = 0; i < length; i++) { + value[i] = tempData[i]; + } + + tempData.Dispose(); + return true; + } + + public bool Read(out FixedString512Bytes value) { + Assert.IsTrue(HasNext(512), "Not enough data to read a FixedString512Bytes."); + + NativeArray tempData = new NativeArray(512, Allocator.Temp); + + value = new FixedString512Bytes(); + int length = 0; + + for (int i = 0; i < 512; i++) { + Read(out byte byteValue); + + tempData[i] = byteValue; + + if (byteValue != 0) { + length++; + } + } + + value.Length = length; + for (int i = 0; i < length; i++) { + value[i] = tempData[i]; + } + + tempData.Dispose(); + return true; + } + + public bool Read(out FixedString4096Bytes value) { + Assert.IsTrue(HasNext(4096), "Not enough data to read a FixedString4096Bytes."); + + NativeArray tempData = new NativeArray(4096, Allocator.Temp); + + value = new FixedString4096Bytes(); + int length = 0; + + for (int i = 0; i < 4096; i++) { + Read(out byte byteValue); + + tempData[i] = byteValue; + + if (byteValue != 0) { + length++; + } + } + + value.Length = length; + for (int i = 0; i < length; i++) { + value[i] = tempData[i]; + } + + tempData.Dispose(); + return true; + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/NetworkBufferReader.cs.meta b/Runtime/Engine/Code/Network/NetworkBufferReader.cs.meta new file mode 100644 index 0000000..78aaa77 --- /dev/null +++ b/Runtime/Engine/Code/Network/NetworkBufferReader.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 15e108857a064cd28bde3e3a8dfe749e +timeCreated: 1752858133 \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/NetworkBufferWriter.cs b/Runtime/Engine/Code/Network/NetworkBufferWriter.cs new file mode 100644 index 0000000..5b0de1c --- /dev/null +++ b/Runtime/Engine/Code/Network/NetworkBufferWriter.cs @@ -0,0 +1,316 @@ +using System; +using Unity.Collections; +using UnityEngine; +using UnityEngine.Assertions; +using UnityEngine.Pool; + +namespace RebootKit.Engine.Network { + // @NOTE: Data is written in a linear fashion, so the position is always at the end of the written data. + // We are writting everything in little-endian format. + public struct NetworkBufferWriter : IDisposable { + class WriterHandle { + public NativeArray Data; + public bool IsOwner; // Indicates if this handle owns the data and should dispose it. + public int Position; + public int Capacity; + } + + static readonly IObjectPool s_WriterPool = new ObjectPool( + () => new WriterHandle(), + _ => { }, + handle => { + if (handle.Data.IsCreated && handle.IsOwner) { + handle.Data.Dispose(); + } + + handle.Data = default; + handle.Position = 0; + handle.Capacity = 0; + }, + handle => { + if (handle.Data.IsCreated && handle.IsOwner) { + handle.Data.Dispose(); + } + }, + true, + 256 + ); + + WriterHandle m_Handle; + + public int Position { + get { + return m_Handle.Position; + } + + set { + Assert.IsTrue(value >= 0 && value <= m_Handle.Capacity, "Position must be within the bounds of the buffer."); + m_Handle.Position = value; + } + } + + public NetworkBufferWriter(int capacity, Allocator allocator) { + m_Handle = s_WriterPool.Get(); + m_Handle.Data = new NativeArray(capacity, allocator); + m_Handle.IsOwner = true; + m_Handle.Capacity = capacity; + m_Handle.Position = 0; + } + + public NetworkBufferWriter(NativeArray buffer, int position) { + m_Handle = s_WriterPool.Get(); + m_Handle.Data = buffer; + m_Handle.IsOwner = false; + m_Handle.Capacity = buffer.Length; + m_Handle.Position = position; + } + + public void Dispose() { + if (m_Handle != null) { + s_WriterPool.Release(m_Handle); + m_Handle = null; + } + } + + public bool WillFit(int size) { + return m_Handle.Position + size <= m_Handle.Capacity; + } + + public void Write(byte value) { + if (m_Handle.Position >= m_Handle.Capacity) { + throw new InvalidOperationException("Buffer overflow: Cannot write beyond capacity."); + } + m_Handle.Data[m_Handle.Position++] = value; + } + + public void Write(byte[] values) { + Assert.IsNotNull(values, "Trying to write null byte array to the buffer."); + Assert.IsTrue(WillFit(values.Length), "Buffer overflow: Cannot write beyond capacity."); + + if (values.Length == 0) { + return; + } + + for (int i = 0; i < values.Length; i++) { + m_Handle.Data[m_Handle.Position++] = values[i]; + } + } + + public void Write(NativeArray values) { + Assert.IsTrue(values.IsCreated, "Trying to write uncreated NativeArray to the buffer."); + Assert.IsTrue(WillFit(values.Length), "Buffer overflow: Cannot write beyond capacity."); + + if (values.Length == 0) { + return; + } + + for (int i = 0; i < values.Length; i++) { + m_Handle.Data[m_Handle.Position++] = values[i]; + } + } + + public void Write(int value) { + Assert.IsTrue(sizeof(int) == 4, "Size of int must be 4 bytes."); + Assert.IsTrue(WillFit(sizeof(int)), "Buffer overflow: Cannot write beyond capacity."); + + if (BitConverter.IsLittleEndian) { + Write((byte) (value & 0xFF)); + Write((byte) ((value >> 8) & 0xFF)); + Write((byte) ((value >> 16) & 0xFF)); + Write((byte) ((value >> 24) & 0xFF)); + } else { + Write((byte) ((value >> 24) & 0xFF)); + Write((byte) ((value >> 16) & 0xFF)); + Write((byte) ((value >> 8) & 0xFF)); + Write((byte) (value & 0xFF)); + } + } + + public void Write(short value) { + Assert.IsTrue(sizeof(short) == 2, "Size of short must be 2 bytes."); + Assert.IsTrue(WillFit(sizeof(short)), "Buffer overflow: Cannot write beyond capacity."); + + if (BitConverter.IsLittleEndian) { + Write((byte) (value & 0xFF)); + Write((byte) ((value >> 8) & 0xFF)); + } else { + Write((byte) ((value >> 8) & 0xFF)); + Write((byte) (value & 0xFF)); + } + } + + public void Write(ushort value) { + Assert.IsTrue(sizeof(ushort) == 2, "Size of ushort must be 2 bytes."); + Assert.IsTrue(WillFit(sizeof(ushort)), "Buffer overflow: Cannot write beyond capacity."); + + if (BitConverter.IsLittleEndian) { + Write((byte) (value & 0xFF)); + Write((byte) ((value >> 8) & 0xFF)); + } else { + Write((byte) ((value >> 8) & 0xFF)); + Write((byte) (value & 0xFF)); + } + } + + public void Write(long value) { + Assert.IsTrue(sizeof(long) == 8, "Size of long must be 8 bytes."); + Assert.IsTrue(WillFit(sizeof(long)), "Buffer overflow: Cannot write beyond capacity."); + + if (BitConverter.IsLittleEndian) { + Write((byte) (value & 0xFF)); + Write((byte) ((value >> 8) & 0xFF)); + Write((byte) ((value >> 16) & 0xFF)); + Write((byte) ((value >> 24) & 0xFF)); + Write((byte) ((value >> 32) & 0xFF)); + Write((byte) ((value >> 40) & 0xFF)); + Write((byte) ((value >> 48) & 0xFF)); + Write((byte) ((value >> 56) & 0xFF)); + } else { + Write((byte) ((value >> 56) & 0xFF)); + Write((byte) ((value >> 48) & 0xFF)); + Write((byte) ((value >> 40) & 0xFF)); + Write((byte) ((value >> 32) & 0xFF)); + Write((byte) ((value >> 24) & 0xFF)); + Write((byte) ((value >> 16) & 0xFF)); + Write((byte) ((value >> 8) & 0xFF)); + Write((byte) (value & 0xFF)); + } + } + + public void Write(ulong value) { + Assert.IsTrue(sizeof(ulong) == 8, "Size of ulong must be 8 bytes."); + Assert.IsTrue(WillFit(sizeof(ulong)), "Buffer overflow: Cannot write beyond capacity."); + + if (BitConverter.IsLittleEndian) { + Write((byte) (value & 0xFF)); + Write((byte) ((value >> 8) & 0xFF)); + Write((byte) ((value >> 16) & 0xFF)); + Write((byte) ((value >> 24) & 0xFF)); + Write((byte) ((value >> 32) & 0xFF)); + Write((byte) ((value >> 40) & 0xFF)); + Write((byte) ((value >> 48) & 0xFF)); + Write((byte) ((value >> 56) & 0xFF)); + } else { + Write((byte) ((value >> 56) & 0xFF)); + Write((byte) ((value >> 48) & 0xFF)); + Write((byte) ((value >> 40) & 0xFF)); + Write((byte) ((value >> 32) & 0xFF)); + Write((byte) ((value >> 24) & 0xFF)); + Write((byte) ((value >> 16) & 0xFF)); + Write((byte) ((value >> 8) & 0xFF)); + Write((byte) (value & 0xFF)); + } + } + + public void Write(float value) { + Assert.IsTrue(sizeof(float) == 4, "Size of float must be 4 bytes."); + Assert.IsTrue(WillFit(sizeof(float)), "Buffer overflow: Cannot write beyond capacity."); + + unsafe { + byte* bytes = (byte*) &value; + Write(bytes[0]); + Write(bytes[1]); + Write(bytes[2]); + Write(bytes[3]); + } + } + + public void Write(bool value) { + Assert.IsTrue(WillFit(1), "Buffer overflow: Cannot write beyond capacity."); + Write((byte) (value ? 1 : 0)); + } + + public void Write(Vector2 value) { + Assert.IsTrue(WillFit(sizeof(float) * 2), "Buffer overflow: Cannot write beyond capacity."); + + Write(value.x); + Write(value.y); + } + + public void Write(Vector3 value) { + Assert.IsTrue(WillFit(sizeof(float) * 3), "Buffer overflow: Cannot write beyond capacity."); + + Write(value.x); + Write(value.y); + Write(value.z); + } + + public void Write(Vector4 value) { + Assert.IsTrue(WillFit(sizeof(float) * 4), "Buffer overflow: Cannot write beyond capacity."); + + Write(value.x); + Write(value.y); + Write(value.z); + Write(value.w); + } + + public void Write(Quaternion value) { + Assert.IsTrue(WillFit(sizeof(float) * 4), "Buffer overflow: Cannot write beyond capacity."); + + Write(value.x); + Write(value.y); + Write(value.z); + Write(value.w); + } + + public void Write(FixedString32Bytes value) { + Assert.IsTrue(WillFit(32)); + + for (int i = 0; i < 32; i++) { + if (i < value.Length) { + Write(value[i]); + } else { + Write((byte) 0); // Fill with zero if the string is shorter than 32 bytes + } + } + } + + public void Write(FixedString64Bytes value) { + Assert.IsTrue(WillFit(64)); + + for (int i = 0; i < 64; i++) { + if (i < value.Length) { + Write(value[i]); + } else { + Write((byte) 0); // Fill with zero if the string is shorter than 64 bytes + } + } + } + + public void Write(FixedString128Bytes value) { + Assert.IsTrue(WillFit(128)); + + for (int i = 0; i < 128; i++) { + if (i < value.Length) { + Write(value[i]); + } else { + Write((byte) 0); // Fill with zero if the string is shorter than 128 bytes + } + } + } + + public void Write(FixedString512Bytes value) { + Assert.IsTrue(WillFit(512)); + + for (int i = 0; i < 512; i++) { + if (i < value.Length) { + Write(value[i]); + } else { + Write((byte) 0); // Fill with zero if the string is shorter than 512 bytes + } + } + } + + public void Write(FixedString4096Bytes value) { + Assert.IsTrue(WillFit(4096)); + + for (int i = 0; i < 4096; i++) { + if (i < value.Length) { + Write(value[i]); + } else { + Write((byte) 0); // Fill with zero if the string is shorter than 4096 bytes + } + } + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/NetworkBufferWriter.cs.meta b/Runtime/Engine/Code/Network/NetworkBufferWriter.cs.meta new file mode 100644 index 0000000..89eeab9 --- /dev/null +++ b/Runtime/Engine/Code/Network/NetworkBufferWriter.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b2f038461ab549e48a5f3c6d59e92f9d +timeCreated: 1752855725 \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/NetworkPacketQueue.cs b/Runtime/Engine/Code/Network/NetworkPacketQueue.cs new file mode 100644 index 0000000..7a0eba4 --- /dev/null +++ b/Runtime/Engine/Code/Network/NetworkPacketQueue.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using RebootKit.Engine.Foundation; +using RebootKit.Engine.Simulation; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine.Assertions; +using UnityEngine.Pool; + +namespace RebootKit.Engine.Network { + struct NetworkPacketHeader : ISerializableEntity { + public int MagicNumber; + public ushort Version; + public ushort EntityCount; + + public static int GetEntityCountOffset() { + return sizeof(int) + sizeof(ushort); + } + + public void Serialize(NetworkBufferWriter writer) { + writer.Write(MagicNumber); + writer.Write(Version); + writer.Write(EntityCount); + } + + public void Deserialize(NetworkBufferReader reader) { + reader.Read(out MagicNumber); + reader.Read(out Version); + reader.Read(out EntityCount); + } + + public int GetMaxBytes() { + return sizeof(int) + sizeof(ushort) * 2; // MagicNumber, Version, EntityCount + } + } + + class NetworkPacket : IDisposable { + public static readonly IObjectPool Pool = new ObjectPool( + () => { + NetworkPacket packet = new NetworkPacket(); + packet.Data = default; + packet.Writer = default; + return packet; + }, + packet => { + // Packet is initialized after being retrieved from the pool + }, + packet => { + packet.Dispose(); + }, + packet => { + packet.Dispose(); + }, + true, + 16 + ); + + public NativeArray Data; + public NetworkBufferWriter Writer; + + public ushort EntityCount { get; private set; } + + public void IncrementEntityCount() { + int originalPosition = Writer.Position; + + EntityCount += 1; + + Writer.Position = NetworkPacketHeader.GetEntityCountOffset(); // Reset position to write the entity count + Writer.Write(EntityCount); + Writer.Position = originalPosition; + } + + public void Dispose() { + Data.Dispose(); + Writer.Dispose(); + + EntityCount = 0; + } + } + + enum NetworkDataType : byte { + None = 0x00, + ActorCoreState = 0x01, + ActorTransformSync = 0x02, + ActorState = 0x03, + ActorEvent = 0x04, + ActorCommand = 0x05, + SynchronizeActor = 0x07, + SpawnActor = 0x08, + } + + struct NetworkDataHeader : ISerializableEntity { + public NetworkDataType Type; + public ulong ActorID; + public byte CommandID; + public byte EventID; + public int DataSize; + + public void Serialize(NetworkBufferWriter writer) { + writer.Write((byte) Type); + writer.Write(ActorID); + writer.Write(CommandID); + writer.Write(EventID); + writer.Write(DataSize); + } + + public void Deserialize(NetworkBufferReader reader) { + reader.Read(out byte typeByte); + Type = (NetworkDataType) typeByte; + reader.Read(out ActorID); + reader.Read(out CommandID); + reader.Read(out EventID); + reader.Read(out DataSize); + } + + public int GetMaxBytes() { + return sizeof(ulong) + sizeof(byte) * 3 + sizeof(int); + } + } + + class NetworkPacketQueue : IDisposable { + static readonly Logger s_Logger = new Logger(nameof(NetworkPacketQueue)); + + readonly int m_PacketMaxSize; + readonly ushort m_Version; + + internal readonly List NetworkPackets = new List(); + + public NetworkPacketQueue(int packetMaxSize, ushort version = 2137) { + m_PacketMaxSize = packetMaxSize; + m_Version = version; + Assert.IsTrue(m_PacketMaxSize > 0, "Packet maximum size must be greater than zero."); + } + + public void Dispose() { + foreach (NetworkPacket packet in NetworkPackets) { + packet.Data.Dispose(); + } + + NetworkPackets.Clear(); + } + + public void Clear() { + foreach (NetworkPacket packet in NetworkPackets) { + packet.Dispose(); + } + + NetworkPackets.Clear(); + } + + public void WriteActorState(ulong actorID, IActorData entity) { + Assert.IsTrue(entity.GetMaxBytes() <= m_PacketMaxSize, + $"Entity size {entity.GetMaxBytes()} exceeds packet max size {m_PacketMaxSize}."); + + NetworkDataHeader header = new NetworkDataHeader { + Type = NetworkDataType.ActorState, + ActorID = actorID, + DataSize = entity.GetMaxBytes() + }; + + int bytesToWrite = header.GetMaxBytes() + entity.GetMaxBytes(); + + NetworkPacket packet = GetPacketToWriteTo(bytesToWrite); + header.Serialize(packet.Writer); + entity.Serialize(packet.Writer); + packet.IncrementEntityCount(); + } + + public void WriteActorTransformState(ulong actorID, ActorTransformSyncData transformData) { + NetworkDataHeader header = new NetworkDataHeader { + Type = NetworkDataType.ActorTransformSync, + ActorID = actorID, + DataSize = transformData.GetMaxBytes() + }; + + int bytesToWrite = header.GetMaxBytes() + transformData.GetMaxBytes(); + + NetworkPacket packet = GetPacketToWriteTo(bytesToWrite); + header.Serialize(packet.Writer); + transformData.Serialize(packet.Writer); + packet.IncrementEntityCount(); + } + + public void WriteActorCoreState(ulong actorID, ActorCoreStateSnapshot coreState) { + NetworkDataHeader header = new NetworkDataHeader { + Type = NetworkDataType.ActorCoreState, + ActorID = actorID, + DataSize = coreState.GetMaxBytes() + }; + + int bytesToWrite = header.GetMaxBytes() + coreState.GetMaxBytes(); + + NetworkPacket packet = GetPacketToWriteTo(bytesToWrite); + header.Serialize(packet.Writer); + coreState.Serialize(packet.Writer); + packet.IncrementEntityCount(); + } + + public void WriteSpawnActor(FixedString64Bytes assetGUID, + ulong actorID, + ActorCoreStateSnapshot coreState, + IActorData actorData) { + NetworkDataHeader header = new NetworkDataHeader { + Type = NetworkDataType.SpawnActor, + ActorID = actorID, + DataSize = 0 + }; + + header.DataSize += sizeof(byte) * 64; // assetGUID + header.DataSize += coreState.GetMaxBytes(); + header.DataSize += sizeof(ushort); + header.DataSize += actorData.GetMaxBytes(); + + NetworkPacket packet = GetPacketToWriteTo(header.GetMaxBytes() + header.DataSize); + header.Serialize(packet.Writer); + + packet.Writer.Write(assetGUID); + coreState.Serialize(packet.Writer); + + packet.Writer.Write((ushort) actorData.GetMaxBytes()); + actorData.Serialize(packet.Writer); + + packet.IncrementEntityCount(); + } + + public void WriteActorSynchronize(ulong actorID, + ActorCoreStateSnapshot coreState, + IActorData actorData) { + NetworkDataHeader header = new NetworkDataHeader { + Type = NetworkDataType.SynchronizeActor, + ActorID = actorID, + DataSize = 0 + }; + + header.DataSize += coreState.GetMaxBytes(); + header.DataSize += sizeof(ushort); + header.DataSize += actorData.GetMaxBytes(); + + NetworkPacket packet = GetPacketToWriteTo(header.GetMaxBytes() + header.DataSize); + header.Serialize(packet.Writer); + + coreState.Serialize(packet.Writer); + + packet.Writer.Write((ushort) actorData.GetMaxBytes()); + actorData.Serialize(packet.Writer); + + packet.IncrementEntityCount(); + } + + NetworkPacket GetPacketToWriteTo(int bytesToWrite) { + foreach (NetworkPacket networkPacket in NetworkPackets) { + if (networkPacket.Writer.WillFit(bytesToWrite)) { + return networkPacket; + } + } + + Assert.IsTrue(bytesToWrite < m_PacketMaxSize, + $"Packet size {bytesToWrite} exceeds maximum allowed size {m_PacketMaxSize}."); + + NetworkPacket packet = NetworkPacket.Pool.Get(); + packet.Data = new NativeArray(m_PacketMaxSize, Allocator.Persistent); + + unsafe { + void* ptr = packet.Data.GetUnsafePtr(); + UnsafeUtility.MemClear(ptr, sizeof(byte) * packet.Data.Length); + } + + packet.Writer = new NetworkBufferWriter(packet.Data, 0); + + NetworkPacketHeader header = new NetworkPacketHeader { + MagicNumber = RConsts.k_NetworkPacketMagicNumber, + Version = m_Version, + EntityCount = 0 // Will be updated later + }; + + header.Serialize(packet.Writer); + NetworkPackets.Add(packet); + return packet; + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/NetworkPacketQueue.cs.meta b/Runtime/Engine/Code/Network/NetworkPacketQueue.cs.meta new file mode 100644 index 0000000..0b8c2e4 --- /dev/null +++ b/Runtime/Engine/Code/Network/NetworkPacketQueue.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8a122573c79c4b3e9ef3bc2da3b09faa +timeCreated: 1752855419 \ No newline at end of file diff --git a/Runtime/Engine/Code/Main/NetworkPlayerController.cs b/Runtime/Engine/Code/Network/NetworkPlayerController.cs similarity index 70% rename from Runtime/Engine/Code/Main/NetworkPlayerController.cs rename to Runtime/Engine/Code/Network/NetworkPlayerController.cs index 2189bb1..cb69a11 100644 --- a/Runtime/Engine/Code/Main/NetworkPlayerController.cs +++ b/Runtime/Engine/Code/Network/NetworkPlayerController.cs @@ -1,13 +1,18 @@ -using RebootKit.Engine.Foundation; +using System; +using System.Threading; +using Cysharp.Threading.Tasks; +using RebootKit.Engine.Foundation; +using RebootKit.Engine.Main; using RebootKit.Engine.Simulation; using Unity.Netcode; -namespace RebootKit.Engine.Main { +namespace RebootKit.Engine.Network { public abstract class NetworkPlayerController : NetworkBehaviour { static readonly Logger s_Logger = new Logger(nameof(NetworkPlayerController)); + ulong m_ActorIDToPossess; public Actor PossessedActor { get; private set; } - + public void PossessActor(Actor actor) { if (!IsServer) { s_Logger.Error("PossessActor can only be called on the server."); @@ -21,19 +26,27 @@ namespace RebootKit.Engine.Main { PossessActorRpc(actor.ActorID, RpcTarget.Everyone); } - + [Rpc(SendTo.SpecifiedInParams)] void PossessActorRpc(ulong actorID, RpcParams rpcParams) { - Actor actor = RR.FindSpawnedActor(actorID); - if (actor == null) { - s_Logger.Error($"Actor with ID {actorID} not found."); - return; - } - if (PossessedActor is not null) { OnUnpossessActor(PossessedActor); } - + + WaitForActorToSpawnThenPossessAsync(actorID, destroyCancellationToken).Forget(); + } + + async UniTask WaitForActorToSpawnThenPossessAsync(ulong actorID, CancellationToken cancellationToken) { + Actor actor = null; + while (actor == null) { + actor = RR.FindSpawnedActor(actorID); + await UniTask.WaitForSeconds(0.5f, cancellationToken: cancellationToken); + + if (cancellationToken.IsCancellationRequested) { + return; + } + } + PossessedActor = actor; OnPossessActor(actor); } diff --git a/Runtime/Engine/Code/Main/NetworkPlayerController.cs.meta b/Runtime/Engine/Code/Network/NetworkPlayerController.cs.meta similarity index 100% rename from Runtime/Engine/Code/Main/NetworkPlayerController.cs.meta rename to Runtime/Engine/Code/Network/NetworkPlayerController.cs.meta diff --git a/Runtime/Engine/Code/Network/NetworkSystem.cs b/Runtime/Engine/Code/Network/NetworkSystem.cs new file mode 100644 index 0000000..f86d63e --- /dev/null +++ b/Runtime/Engine/Code/Network/NetworkSystem.cs @@ -0,0 +1,515 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using Cysharp.Threading.Tasks; +using RebootKit.Engine.Extensions; +using RebootKit.Engine.Foundation; +using RebootKit.Engine.Main; +using RebootKit.Engine.Simulation; +using Unity.Collections; +using Unity.Netcode; +using UnityEngine; +using UnityEngine.Assertions; +using Logger = RebootKit.Engine.Foundation.Logger; + +namespace RebootKit.Engine.Network { + enum NetworkClientSyncState { + NotReady, + LoadingWorld, + PreparingForActorsSync, + SyncingActors, + Ready + } + + struct NetworkClientState : INetworkSerializable { + public ulong ClientID; + public NetworkClientSyncState SyncState; + public int ActorsSyncPacketsLeft; + + public NetworkPacketQueue ReliableQueue; + public NetworkPacketQueue UnreliableQueue; + + public bool IsReady { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { + return SyncState == NetworkClientSyncState.Ready; + } + } + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { + serializer.SerializeValue(ref ClientID); + serializer.SerializeValue(ref SyncState); + serializer.SerializeValue(ref ActorsSyncPacketsLeft); + } + } + + struct NetworkPacketTarget { + public enum Type { + AllClients, + Single + } + + public Type TargetType; + public ulong ClientID; + + public static NetworkPacketTarget AllClients() { + return new NetworkPacketTarget { + TargetType = Type.AllClients, + ClientID = 0 + }; + } + + public static NetworkPacketTarget Single(ulong clientID) { + return new NetworkPacketTarget { + TargetType = Type.Single, + ClientID = clientID + }; + } + } + + public class NetworkSystem : NetworkBehaviour { + [ConfigVar("sv.tick_rate", 32, "Server tick rate in Hz", CVarFlags.Server)] + public static ConfigVar TickRate; + + static readonly Logger s_Logger = new Logger(nameof(NetworkSystem)); + + [field: SerializeField] public ActorsManager Actors { get; private set; } + + internal readonly Dictionary Clients = new Dictionary(); + + public FixedString512Bytes WorldID { get; private set; } = new FixedString512Bytes(""); + bool m_IsChangingWorld = false; + + float m_TickTimer; + + public ulong TickCount { get; private set; } + public event Action ServerTick = delegate { }; + + public ulong LocalClientID { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { + return NetworkManager.Singleton.LocalClientId; + } + } + + NetworkPacketQueue m_ReliablePacketQueue; + NetworkPacketQueue m_UnreliablePacketQueue; + + // + // @MARK: Unity callbacks + // + void Awake() { + RR.NetworkSystemInstance = this; + + m_ReliablePacketQueue = new NetworkPacketQueue(1024 * 4); + m_UnreliablePacketQueue = new NetworkPacketQueue(1024); + } + + void Update() { + float deltaTime = Time.deltaTime; + + float serverDeltaTime = 1.0f / TickRate.IndexValue; + m_TickTimer += deltaTime; + + while (m_TickTimer >= serverDeltaTime) { + m_TickTimer -= serverDeltaTime; + + if (RR.IsServer()) { + Actors.ServerTick(serverDeltaTime); + + ServerTick?.Invoke(TickCount); + TickCount++; + + FlushNetworkPackets(); + } + } + } + + // + // @MARK: NetworkBehaviour callbacks + // + public override void OnNetworkSpawn() { + base.OnNetworkSpawn(); + + NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected; + NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnect; + } + + public override void OnNetworkDespawn() { + base.OnNetworkDespawn(); + + NetworkManager.Singleton.OnClientConnectedCallback -= OnClientConnected; + NetworkManager.Singleton.OnClientDisconnectCallback -= OnClientDisconnect; + } + + void OnClientConnected(ulong clientID) { + if (!IsServer) { + return; + } + + s_Logger.Info($"OnClientConnected: {clientID}"); + + NetworkClientState newClientState = new NetworkClientState { + ClientID = clientID, + SyncState = NetworkClientSyncState.NotReady, + ReliableQueue = new NetworkPacketQueue(1024 * 4), + UnreliableQueue = new NetworkPacketQueue(512) + }; + Clients.Add(clientID, newClientState); + + if (clientID != NetworkManager.Singleton.LocalClientId) { + foreach (NetworkClientState state in Clients.Values) { + UpdateClientStateRpc(state, RpcTarget.Single(clientID, RpcTargetUse.Temp)); + } + } + + if (!WorldID.IsEmpty) { + s_Logger.Info($"Synchronizing world load for client {clientID} with world ID '{WorldID}'"); + ClientLoadWorldRpc(WorldID.ToString(), RpcTarget.Single(clientID, RpcTargetUse.Temp)); + } + } + + void OnClientDisconnect(ulong clientID) { + s_Logger.Info($"OnClientDisconnect: {clientID}"); + Clients.Remove(clientID); + } + + // + // @MARK: Server API + // + public void KickClient(ulong clientID, string reason = "Kicked by server") { + if (!IsServer) { + s_Logger.Error("Only server can kick clients."); + return; + } + + if (NetworkManager.Singleton.ConnectedClients.TryGetValue(clientID, out NetworkClient client)) { + NetworkManager.Singleton.DisconnectClient(clientID, reason); + s_Logger.Info($"Kicked client {clientID}: {reason}"); + } else { + s_Logger.Error($"Client {clientID} not found."); + } + } + + public void SetCurrentWorld(string worldID) { + if (!IsServer) { + s_Logger.Error("Only server can set the current world."); + return; + } + + if (m_IsChangingWorld) { + s_Logger.Error($"Already changing world to '{WorldID}'. Please wait until the current world change is complete."); + return; + } + + WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID); + if (worldConfigAsset is null) { + s_Logger.Error($"Failed to set current world: World config asset for '{worldID}' not found."); + return; + } + + WorldID = worldID; + + foreach ((ulong _, NetworkClientState clientState) in Clients.ToList()) { + NetworkClientState state = clientState; + state.SyncState = NetworkClientSyncState.LoadingWorld; + UpdateClientState(state); + } + + ServerLoadWorldAsync(worldConfigAsset, destroyCancellationToken).Forget(); + } + + async UniTask ServerLoadWorldAsync(WorldConfigAsset asset, CancellationToken cancellationToken) { + s_Logger.Info($"ServerLoadWorldAsync: {asset.Config.name}"); + + m_IsChangingWorld = true; + + RR.World.Unload(); + RR.CloseMainMenu(); + + await RR.World.LoadAsync(asset.Config, cancellationToken); + + m_IsChangingWorld = false; + + if (!TryGetClientState(NetworkManager.Singleton.LocalClientId, out NetworkClientState localClientState)) { + s_Logger.Error($"Local client state not found for client ID {NetworkManager.Singleton.LocalClientId}."); + RR.Disconnect(); + return; + } + + localClientState.SyncState = NetworkClientSyncState.Ready; + UpdateClientState(localClientState); + + RR.GameInstance.PlayerBecameReady(localClientState.ClientID); + + ClientLoadWorldRpc(asset.name, RpcTarget.NotMe); + } + + [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)] + void ClientLoadWorldRpc(string worldID, RpcParams rpcParams) { + WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID); + if (worldConfigAsset is null) { + s_Logger.Error($"World config asset for '{worldID}' not found."); + RR.Disconnect(); + return; + } + + ClientLoadWorldAsync(worldID, destroyCancellationToken).Forget(); + } + + async UniTask ClientLoadWorldAsync(string worldID, CancellationToken cancellationToken) { + s_Logger.Info($"ClientLoadWorldAsync: {worldID}"); + + WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID); + if (worldConfigAsset is null) { + s_Logger.Error($"World config asset for '{worldID}' not found."); + return; + } + + RR.World.Unload(); + RR.CloseMainMenu(); + + await RR.World.LoadAsync(worldConfigAsset.Config, cancellationToken); + + WorldID = worldID; + ClientLoadedWorldRpc(worldID); + } + + [Rpc(SendTo.Server, Delivery = RpcDelivery.Reliable)] + void ClientLoadedWorldRpc(string worldID, RpcParams rpcParams = default) { + ulong clientID = rpcParams.Receive.SenderClientId; + + if (!WorldID.Equals(worldID)) { + s_Logger.Error($"Client {clientID} tried to load world '{worldID}', but server is in world '{WorldID}'."); + NetworkManager.Singleton.DisconnectClient(clientID, "World mismatch!"); + return; + } + + if (Clients.TryGetValue(clientID, out NetworkClientState clientState)) { + Actors.InitializeActorsForClient(clientID); + } else { + NetworkManager.Singleton.DisconnectClient(clientID, "Client is not registered!"); + } + } + + // + // @MARK: Internal + // + internal bool TryGetClientState(ulong clientID, out NetworkClientState clientState) { + return Clients.TryGetValue(clientID, out clientState); + } + + internal void UpdateClientState(NetworkClientState clientState) { + if (!IsServer) { + s_Logger.Error("UpdateClientState can only be called on the server."); + return; + } + + Clients[clientState.ClientID] = clientState; + UpdateClientStateRpc(clientState, RpcTarget.NotServer); + } + + [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)] + void UpdateClientStateRpc(NetworkClientState newState, RpcParams rpcParams) { + Clients[newState.ClientID] = newState; + } + + internal void ClientSynchronizedActors(ulong clientID) { + if (TryGetClientState(clientID, out NetworkClientState state)) { + state.SyncState = NetworkClientSyncState.Ready; + UpdateClientState(state); + + RR.GameInstance.PlayerBecameReady(clientID); + } else { + s_Logger.Error($"Client state for {clientID} not found."); + } + } + + internal int GetReadyClientsCount() { + int count = 0; + foreach (NetworkClientState clientState in Clients.Values) { + if (clientState.IsReady) { + count++; + } + } + return count; + } + + // + // @MARK: Network packets + // + NetworkPacketQueue GetPacketQueue(NetworkPacketTarget target, bool reliable) { + if (target.TargetType == NetworkPacketTarget.Type.AllClients) { + return reliable ? m_ReliablePacketQueue : m_UnreliablePacketQueue; + } + + if (target.TargetType == NetworkPacketTarget.Type.Single) { + if (TryGetClientState(target.ClientID, out NetworkClientState clientState)) { + return reliable ? clientState.ReliableQueue : clientState.UnreliableQueue; + } + + s_Logger.Error($"Client state for {target.ClientID} not found."); + return null; + } + + s_Logger.Error($"Invalid network packet target type: {target.TargetType}"); + return null; + } + + internal void WriteActorState(NetworkPacketTarget target, ulong actorID, IActorData actorData) { + NetworkPacketQueue queue = GetPacketQueue(target, true); + queue.WriteActorState(actorID, actorData); + } + + internal void WriteActorTransformState(NetworkPacketTarget target, + ulong actorID, + ActorTransformSyncData transformData) { + NetworkPacketQueue queue = GetPacketQueue(target, false); + queue.WriteActorTransformState(actorID, transformData); + } + + internal void WriteActorCoreState(NetworkPacketTarget target, + ulong actorID, + ActorCoreStateSnapshot coreData) { + NetworkPacketQueue queue = GetPacketQueue(target, true); + queue.WriteActorCoreState(actorID, coreData); + } + + internal void WriteSpawnActor(NetworkPacketTarget target, + string assetGUID, + ulong actorID, + ActorCoreStateSnapshot coreState, + IActorData actorData) { + NetworkPacketQueue queue = GetPacketQueue(target, true); + queue.WriteSpawnActor(assetGUID, actorID, coreState, actorData); + } + + internal void WriteActorSynchronize(NetworkPacketTarget target, + ulong actorID, + ActorCoreStateSnapshot coreState, + IActorData actorData) { + NetworkPacketQueue queue = GetPacketQueue(target, true); + queue.WriteActorSynchronize(actorID, coreState, actorData); + } + + void FlushNetworkPackets() { + if (!RR.IsServer()) { + return; + } + + foreach (NetworkPacket networkPacket in m_ReliablePacketQueue.NetworkPackets) { + foreach ((ulong clientID, NetworkClientState state) in RR.NetworkSystemInstance.Clients) { + if (clientID == NetworkManager.Singleton.LocalClientId) { + continue; + } + + if (networkPacket.EntityCount == 0) { + continue; + } + + if (state.IsReady) { + ReliableReceiveNetworkPacketRpc(networkPacket.Data, + RpcTarget.Single(clientID, RpcTargetUse.Temp)); + } + } + } + + foreach (NetworkPacket networkPacket in m_UnreliablePacketQueue.NetworkPackets) { + foreach ((ulong clientID, NetworkClientState state) in RR.NetworkSystemInstance.Clients) { + if (clientID == NetworkManager.Singleton.LocalClientId) { + continue; + } + + if (networkPacket.EntityCount == 0) { + continue; + } + + if (state.IsReady) { + UnreliableReceiveNetworkPacketRpc(networkPacket.Data, + RpcTarget.Single(clientID, RpcTargetUse.Temp)); + } + } + } + + m_ReliablePacketQueue.Clear(); + m_UnreliablePacketQueue.Clear(); + + foreach (NetworkClientState clientState in Clients.Values) { + if (clientState.ClientID == NetworkManager.Singleton.LocalClientId) { + continue; + } + + foreach (NetworkPacket networkPacket in clientState.ReliableQueue.NetworkPackets) { + if (networkPacket.EntityCount == 0) { + continue; + } + + ReliableReceiveNetworkPacketRpc(networkPacket.Data, + RpcTarget.Single(clientState.ClientID, RpcTargetUse.Temp)); + } + + foreach (NetworkPacket networkPacket in clientState.UnreliableQueue.NetworkPackets) { + if (networkPacket.EntityCount == 0) { + continue; + } + + UnreliableReceiveNetworkPacketRpc(networkPacket.Data, + RpcTarget.Single(clientState.ClientID, RpcTargetUse.Temp)); + } + + clientState.ReliableQueue.Clear(); + clientState.UnreliableQueue.Clear(); + } + } + + [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)] + void ReliableReceiveNetworkPacketRpc(NativeArray data, RpcParams rpcParams) { + OnReceivedNetworkPacket(data); + } + + [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Unreliable)] + void UnreliableReceiveNetworkPacketRpc(NativeArray data, RpcParams rpcParams) { + OnReceivedNetworkPacket(data); + } + + void OnReceivedNetworkPacket(NativeArray data) { + using NetworkBufferReader reader = new NetworkBufferReader(data); + + NetworkPacketHeader packetHeader = new NetworkPacketHeader(); + packetHeader.Deserialize(reader); + + // s_Logger.Info($"Received packet: MagicNumber={packetHeader.MagicNumber}, Version={packetHeader.Version}, EntityCount={packetHeader.EntityCount}"); + + Assert.IsTrue(packetHeader.MagicNumber == RConsts.k_NetworkPacketMagicNumber, + "Received packet with invalid magic number."); + + if (packetHeader.EntityCount == 0) { + s_Logger.Info("Received packet with no entities.\n" + data.ToHexString()); + } + + for (int i = 0; i < packetHeader.EntityCount; i++) { + NetworkDataHeader dataHeader = new NetworkDataHeader(); + dataHeader.Deserialize(reader); + + // s_Logger.Info($"Received entity: Type={dataHeader.Type}, ActorID={dataHeader.ActorID}, DataSize={dataHeader.DataSize}"); + + if (dataHeader.Type == NetworkDataType.None) { + s_Logger.Info("Data of packet with entry with type None:\n" + data.ToHexString()); + } + + Assert.IsTrue(dataHeader.Type != NetworkDataType.None, "Received packet with invalid data type."); + + reader.Read(out NativeArray entityData, dataHeader.DataSize, Allocator.Temp); + OnReceivedEntity(dataHeader, entityData); + + entityData.Dispose(); + } + } + + void OnReceivedEntity(NetworkDataHeader header, NativeArray data) { + Actors.OnReceivedEntity(header, data); + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Main/NetworkSystem.cs.meta b/Runtime/Engine/Code/Network/NetworkSystem.cs.meta similarity index 100% rename from Runtime/Engine/Code/Main/NetworkSystem.cs.meta rename to Runtime/Engine/Code/Network/NetworkSystem.cs.meta diff --git a/Runtime/Engine/Code/Main/NetworkWorldController.cs b/Runtime/Engine/Code/Network/NetworkWorldController.cs similarity index 72% rename from Runtime/Engine/Code/Main/NetworkWorldController.cs rename to Runtime/Engine/Code/Network/NetworkWorldController.cs index a383c5b..a5182f4 100644 --- a/Runtime/Engine/Code/Main/NetworkWorldController.cs +++ b/Runtime/Engine/Code/Network/NetworkWorldController.cs @@ -1,6 +1,6 @@ using Unity.Netcode; -namespace RebootKit.Engine.Main { +namespace RebootKit.Engine.Network { public abstract class NetworkWorldController : NetworkBehaviour { } } \ No newline at end of file diff --git a/Runtime/Engine/Code/Main/NetworkWorldController.cs.meta b/Runtime/Engine/Code/Network/NetworkWorldController.cs.meta similarity index 100% rename from Runtime/Engine/Code/Main/NetworkWorldController.cs.meta rename to Runtime/Engine/Code/Network/NetworkWorldController.cs.meta diff --git a/Runtime/Engine/Code/RConsts.cs b/Runtime/Engine/Code/RConsts.cs index 7ccdd52..b9482e8 100755 --- a/Runtime/Engine/Code/RConsts.cs +++ b/Runtime/Engine/Code/RConsts.cs @@ -19,5 +19,8 @@ internal const string k_CVarsFilename = k_FilenamePrefix + "cvars.txt"; internal const string k_BuildFlagDebug = "RR_DEBUG"; + internal const string k_BuildFlagSteam = "RR_STEAM"; + + internal const int k_NetworkPacketMagicNumber = 0x52455245; // "RERE" in ASCII } } \ No newline at end of file diff --git a/Runtime/Engine/Code/Simulation/Actor.cs b/Runtime/Engine/Code/Simulation/Actor.cs index 7eab06d..6603bee 100644 --- a/Runtime/Engine/Code/Simulation/Actor.cs +++ b/Runtime/Engine/Code/Simulation/Actor.cs @@ -3,66 +3,23 @@ using System.ComponentModel.DataAnnotations; using System.Globalization; using RebootKit.Engine.Foundation; using RebootKit.Engine.Main; +using RebootKit.Engine.Network; using TriInspector; using Unity.Collections; -using Unity.Collections.LowLevel.Unsafe; using Unity.Netcode; using UnityEngine; using Logger = RebootKit.Engine.Foundation.Logger; namespace RebootKit.Engine.Simulation { - public interface ISerializableEntity { - void Serialize(FastBufferWriter writer); - void Deserialize(FastBufferReader reader); - - // @NOTE: -1 means use the default size and have hope it will fit. - int MinimumSizeInBytes() { return -1; } - } - public interface IActorData : ISerializableEntity { } public class NoActorData : IActorData { - public void Serialize(FastBufferWriter writer) { } + public void Serialize(NetworkBufferWriter writer) { } - public void Deserialize(FastBufferReader reader) { } - } + public void Deserialize(NetworkBufferReader reader) { } - public static class DataSerializationUtils { - public const int k_DefaultMessageSize = 256; - - public static NativeArray Serialize(TEntity entity, - Allocator allocator = Allocator.Temp) - where TEntity : ISerializableEntity { - int size = entity.MinimumSizeInBytes(); - if (size < 0) { - size = k_DefaultMessageSize; - } - - using FastBufferWriter writer = new FastBufferWriter(size, allocator); - if (writer.TryBeginWrite(size)) { - entity.Serialize(writer); - - int length = writer.Length; - NativeArray data = new NativeArray(length, allocator); - - unsafe { - void* dst = data.GetUnsafePtr(); - void* src = writer.GetUnsafePtr(); - Buffer.MemoryCopy(src, dst, length, length); - } - - return data; - } - - return default; - } - - public static void Deserialize(NativeArray data, ref TEntity entity) - where TEntity : ISerializableEntity { - using FastBufferReader reader = new FastBufferReader(data, Allocator.Temp); - if (reader.TryBeginRead(data.Length)) { - entity.Deserialize(reader); - } + public int GetMaxBytes() { + return 0; } } @@ -95,6 +52,8 @@ namespace RebootKit.Engine.Simulation { } } + // @NOTE: ActorEvent is used to send events from the server to clients and only clients. + // Server should not receive ActorEvents. public struct ActorEvent : INetworkSerializable { public ulong ActorID; public ulong ClientID; @@ -131,7 +90,7 @@ namespace RebootKit.Engine.Simulation { DisableColliders = 1 << 1, } - struct ActorCoreStateSnapshot : INetworkSerializable { + struct ActorCoreStateSnapshot : ISerializableEntity { public DateTime Timestamp; // @NOTE: Position, Rotation, and Scale are in local space. @@ -145,15 +104,39 @@ namespace RebootKit.Engine.Simulation { public ulong MasterActorID; public FixedString32Bytes MasterSocketName; - public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { - serializer.SerializeValue(ref Timestamp); - serializer.SerializeValue(ref Position); - serializer.SerializeValue(ref Rotation); - serializer.SerializeValue(ref Scale); - serializer.SerializeValue(ref IsHidden); - serializer.SerializeValue(ref Flags); - serializer.SerializeValue(ref MasterActorID); - serializer.SerializeValue(ref MasterSocketName); + public void Serialize(NetworkBufferWriter writer) { + writer.Write(Timestamp.Ticks); + writer.Write(Position); + writer.Write(Rotation); + writer.Write(Scale); + writer.Write(IsHidden); + writer.Write((byte) Flags); + writer.Write(MasterActorID); + writer.Write(MasterSocketName); + } + + public void Deserialize(NetworkBufferReader reader) { + reader.Read(out long ticks); + Timestamp = new DateTime(ticks, DateTimeKind.Utc); + reader.Read(out Position); + reader.Read(out Rotation); + reader.Read(out Scale); + reader.Read(out IsHidden); + reader.Read(out byte flagsByte); + Flags = (ActorPhysicsFlags) flagsByte; + reader.Read(out MasterActorID); + reader.Read(out MasterSocketName); + } + + public int GetMaxBytes() { + return sizeof(long) + // Timestamp + sizeof(float) * 3 + // Position + sizeof(float) * 4 + // Rotation (Quaternion) + sizeof(float) * 3 + // Scale + sizeof(bool) + // IsHidden + sizeof(byte) + // Flags + sizeof(ulong) + // MasterActorID + sizeof(byte) * 32; // MasterSocketName } } @@ -165,19 +148,19 @@ namespace RebootKit.Engine.Simulation { /// - Velocity and AngularVelocity are only used if UsingRigidbody is set. /// - When Actor is mounted to another actor, sync won't happen. /// - [Flags] public enum ActorTransformSyncMode : byte { None = 0, Position = 1 << 0, Rotation = 1 << 1, Scale = 1 << 2, - UsingRigidbody = 1 << 3, // @NOTE: If this is set, Position and Rotation will be synced using Rigidbody's position and rotation. + // @NOTE: If this is set, Position and Rotation will be synced using Rigidbody's position and rotation. + UsingRigidbody = 1 << 3, Velocity = 1 << 4, // @NOTE: Velocity is only used if UsingRigidbody is set. AngularVelocity = 1 << 5 // @NOTE: AngularVelocity is only used if UsingRigidbody is set. } - public struct ActorTransformSyncData : INetworkSerializable { + public struct ActorTransformSyncData : ISerializableEntity { public ActorTransformSyncMode SyncMode; public Vector3 Position; @@ -209,6 +192,81 @@ namespace RebootKit.Engine.Simulation { serializer.SerializeValue(ref AngularVelocity); } } + + public void Serialize(NetworkBufferWriter writer) { + writer.Write((byte) SyncMode); + + if ((SyncMode & ActorTransformSyncMode.Position) != 0) { + writer.Write(Position); + } + + if ((SyncMode & ActorTransformSyncMode.Rotation) != 0) { + writer.Write(Rotation); + } + + if ((SyncMode & ActorTransformSyncMode.Scale) != 0) { + writer.Write(Scale); + } + + if ((SyncMode & ActorTransformSyncMode.Velocity) != 0) { + writer.Write(Velocity); + } + + if ((SyncMode & ActorTransformSyncMode.AngularVelocity) != 0) { + writer.Write(AngularVelocity); + } + } + + public void Deserialize(NetworkBufferReader reader) { + reader.Read(out byte syncModeByte); + SyncMode = (ActorTransformSyncMode) syncModeByte; + + if ((SyncMode & ActorTransformSyncMode.Position) != 0) { + reader.Read(out Position); + } + + if ((SyncMode & ActorTransformSyncMode.Rotation) != 0) { + reader.Read(out Rotation); + } + + if ((SyncMode & ActorTransformSyncMode.Scale) != 0) { + reader.Read(out Scale); + } + + if ((SyncMode & ActorTransformSyncMode.Velocity) != 0) { + reader.Read(out Velocity); + } + + if ((SyncMode & ActorTransformSyncMode.AngularVelocity) != 0) { + reader.Read(out AngularVelocity); + } + } + + public int GetMaxBytes() { + int size = sizeof(byte); // SyncMode + + if ((SyncMode & ActorTransformSyncMode.Position) != 0) { + size += sizeof(float) * 3; // Vector3 + } + + if ((SyncMode & ActorTransformSyncMode.Rotation) != 0) { + size += sizeof(float) * 4; // Quaternion + } + + if ((SyncMode & ActorTransformSyncMode.Scale) != 0) { + size += sizeof(float) * 3; // Vector3 + } + + if ((SyncMode & ActorTransformSyncMode.Velocity) != 0) { + size += sizeof(float) * 3; // Vector3 + } + + if ((SyncMode & ActorTransformSyncMode.AngularVelocity) != 0) { + size += sizeof(float) * 3; // Vector3 + } + + return size; + } } public abstract class Actor : MonoBehaviour { @@ -256,11 +314,12 @@ namespace RebootKit.Engine.Simulation { internal Actor MasterActor; internal FixedString32Bytes MasterSocketName; + internal bool IsCoreStateDirty; public bool IsDataDirty { get; protected internal set; } internal ActorsManager Manager; internal DateTime LastCoreStateSyncTime = DateTime.MinValue; - + // // @MARK: Unity callbacks // @@ -269,11 +328,11 @@ namespace RebootKit.Engine.Simulation { ActorID = UniqueID.NewULongFromGuid(); } } - + // // @MARK: Callbacks to override in derived classes // - + protected abstract IActorData CreateActorData(); // @MARK: Server side @@ -302,7 +361,7 @@ namespace RebootKit.Engine.Simulation { } gameObject.SetActive(shouldBeActive); - Manager.SynchronizeActorCoreStateWithOther(this); + IsCoreStateDirty = true; } public void MountTo(Actor actor, string slotName) { @@ -326,7 +385,7 @@ namespace RebootKit.Engine.Simulation { UpdateLocalPhysicsState(PhysicsFlags); UpdateMountedTransform(); - Manager.SynchronizeActorCoreStateWithOther(this); + IsCoreStateDirty = true; } } @@ -348,7 +407,7 @@ namespace RebootKit.Engine.Simulation { PhysicsFlags = PhysicsFlagsBeforeMount; UpdateLocalPhysicsState(PhysicsFlags); - Manager.SynchronizeActorCoreStateWithOther(this); + IsCoreStateDirty = true; } public void SetCollidersEnabled(bool enableColliders) { @@ -369,7 +428,7 @@ namespace RebootKit.Engine.Simulation { } UpdateLocalCollidersState(enableColliders); - Manager.SynchronizeActorCoreStateWithOther(this); + IsCoreStateDirty = true; } public void SetKinematic(bool isKinematic) { @@ -390,7 +449,7 @@ namespace RebootKit.Engine.Simulation { } actorRigidbody.isKinematic = isKinematic; - Manager.SynchronizeActorCoreStateWithOther(this); + IsCoreStateDirty = true; } // @@ -403,6 +462,7 @@ namespace RebootKit.Engine.Simulation { protected void SendActorCommand(ushort commandID, ref TCmdData commandData) where TCmdData : struct, ISerializableEntity { NativeArray data = DataSerializationUtils.Serialize(commandData); + SendActorCommand(commandID, data); } @@ -651,11 +711,11 @@ namespace RebootKit.Engine.Simulation { internal IActorData InternalCreateActorData() { return CreateActorData(); } - + internal void InitialSyncFinished() { OnClientFinishedInitialSync(); } - + internal void HandleActorCommand(ActorCommand actorCommand) { if (!RR.IsServer()) { s_ActorLogger.Error($"Only the server can handle actor commands. Actor: {name} (ID: {ActorID})"); diff --git a/Runtime/Engine/Code/Simulation/ActorsManager.cs b/Runtime/Engine/Code/Simulation/ActorsManager.cs index 90f48b2..c7d4727 100644 --- a/Runtime/Engine/Code/Simulation/ActorsManager.cs +++ b/Runtime/Engine/Code/Simulation/ActorsManager.cs @@ -1,7 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using NUnit.Framework; using RebootKit.Engine.Extensions; using RebootKit.Engine.Foundation; using RebootKit.Engine.Main; +using RebootKit.Engine.Network; using Unity.Collections; using Unity.Netcode; using UnityEngine; @@ -27,12 +31,10 @@ namespace RebootKit.Engine.Simulation { // public override void OnNetworkSpawn() { base.OnNetworkSpawn(); - RR.ServerTick += OnServerTick; } public override void OnNetworkDespawn() { base.OnNetworkDespawn(); - RR.ServerTick -= OnServerTick; } // @@ -51,13 +53,11 @@ namespace RebootKit.Engine.Simulation { // // @MARK: Server-side logic // - void OnServerTick(ulong tick) { + public void ServerTick(float dt) { if (!IsServer) { return; } - float dt = 1.0f / RR.TickRate.IndexValue; - TickActorsList(m_InSceneActors, dt); TickActorsList(m_SpawnedActors, dt); } @@ -69,78 +69,28 @@ namespace RebootKit.Engine.Simulation { if (actor.IsDataDirty) { actor.IsDataDirty = false; - NativeArray data = SerializeActorState(actor); - if (data.IsCreated) { - SendActorStateToClients(actor.ActorID, data); - } else { - s_Logger.Error($"Failed to serialize actor data for {actor.name}"); + if (actor.Data.GetMaxBytes() > 0) { + RR.NetworkSystemInstance.WriteActorState(NetworkPacketTarget.AllClients(), actor.ActorID, actor.Data); } } + if (actor.IsCoreStateDirty) { + actor.IsCoreStateDirty = false; + + RR.NetworkSystemInstance.WriteActorCoreState(NetworkPacketTarget.AllClients(), + actor.ActorID, + actor.GetCoreStateSnapshot()); + } + if (actor.transformSyncMode != ActorTransformSyncMode.None && actor.MasterActor == null) { ActorTransformSyncData syncData = actor.GetTransformSyncData(); - - foreach ((ulong _, NetworkClientState state) in RR.NetworkSystemInstance.Clients) { - if (state.IsReady) { - SynchronizeActorTransformStateRpc(actor.ActorID, syncData, RpcTarget.NotMe); - } - } + RR.NetworkSystemInstance.WriteActorTransformState(NetworkPacketTarget.AllClients(), + actor.ActorID, + syncData); } } } - internal void SynchronizeActorCoreStateWithOther(Actor actor) { - if (!RR.IsServer()) { - s_Logger.Error("Only the server can synchronize actor core states."); - return; - } - - SynchronizeCoreActorStateRpc(actor.ActorID, actor.GetCoreStateSnapshot(), RpcTarget.NotMe); - } - - void SendActorStateToClients(ulong actorID, NativeArray data) { - if (!RR.IsServer()) { - s_Logger.Error("Only the server can synchronize actor states with clients."); - return; - } - - foreach ((ulong clientID, NetworkClientState state) in RR.NetworkSystemInstance.Clients) { - if (state.IsReady) { - SynchronizeActorStateRpc(actorID, data, RpcTarget.Single(clientID, RpcTargetUse.Temp)); - } - } - } - - [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Unreliable)] - void SynchronizeActorStateRpc(ulong actorID, NativeArray data, RpcParams rpcParams) { - Actor actor = FindActorByID(actorID); - if (actor is null) { - return; - } - - s_Logger.Info($"Synchronizing actor state for {actor.name} with ID {actorID}"); - DeserializeActorState(actor, data); - } - - [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Unreliable)] - void SynchronizeActorTransformStateRpc(ulong actorID, ActorTransformSyncData syncData, RpcParams rpcParams) { - Actor actor = FindActorByID(actorID); - if (actor is null) { - s_Logger.Error($"Actor with ID {actorID} not found for transform synchronization."); - return; - } - - actor.RestoreTransformState(syncData); - } - - NativeArray SerializeActorState(Actor actor) { - return DataSerializationUtils.Serialize(actor.Data); - } - - void DeserializeActorState(Actor actor, NativeArray data) { - DataSerializationUtils.Deserialize(data, ref actor.Data); - } - // // @MARK: Server API // @@ -172,73 +122,14 @@ namespace RebootKit.Engine.Simulation { m_SpawnedActors.Add(actor); - NativeArray stateData = SerializeActorState(actor); - SpawnActorRpc(assetReference.AssetGUID, - actor.ActorID, - actor.GetCoreStateSnapshot(), - stateData, - RpcTarget.NotMe); + RR.NetworkSystemInstance.WriteSpawnActor(NetworkPacketTarget.AllClients(), + assetReference.AssetGUID, + actor.ActorID, + actor.GetCoreStateSnapshot(), + actor.Data); return actor; } - // @NOTE: This RPC is used to spawn actors on clients. - [Rpc(SendTo.SpecifiedInParams)] - void SpawnActorRpc(string guid, - ulong actorID, - ActorCoreStateSnapshot coreStateSnapshot, - NativeArray stateData, - RpcParams rpcParams) { - AssetReferenceGameObject assetReference = new AssetReferenceGameObject(guid); - if (!assetReference.RuntimeKeyIsValid()) { - s_Logger.Error($"Invalid asset reference for actor with GUID {guid}"); - return; - } - - GameObject actorObject = assetReference - .InstantiateAsync(coreStateSnapshot.Position, coreStateSnapshot.Rotation) - .WaitForCompletion(); - if (actorObject == null) { - s_Logger.Error($"Failed to instantiate actor with GUID {guid}"); - return; - } - - Actor actor = actorObject.GetComponent(); - if (actor is null) { - s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component."); - Destroy(actorObject); - return; - } - - actor.Manager = this; - actor.SourceActorPath = guid; - actor.ActorID = actorID; - actor.Data = actor.InternalCreateActorData(); - - actor.RestoreCoreState(coreStateSnapshot); - DeserializeActorState(actor, stateData); - m_SpawnedActors.Add(actor); - } - - public void KillActor(Actor actor) { - if (!IsServer) { - s_Logger.Error("Only the server can kill actors."); - return; - } - - if (actor is null) { - s_Logger.Error("Trying to kill a null actor."); - return; - } - - if (!m_SpawnedActors.Remove(actor)) { - s_Logger.Error($"Trying to kill an actor that is not registered: {actor.name}. " + - "Remember you can only kill actors spawned that are dynamically created"); - return; - } - - Destroy(actor.gameObject); - } - public void CleanUp() { if (IsServer) { CleanUpRpc(); @@ -259,7 +150,7 @@ namespace RebootKit.Engine.Simulation { void CleanUpRpc() { CleanUp(); } - + // // @MARK: Common API // @@ -325,60 +216,27 @@ namespace RebootKit.Engine.Simulation { clientState.ActorsSyncPacketsLeft = m_InSceneActors.Count; RR.NetworkSystemInstance.UpdateClientState(clientState); - RpcSendParams sendParams = RpcTarget.Single(clientID, RpcTargetUse.Temp); + s_Logger.Info($"Starting actor synchronization for client {clientID}.\n" + + $"InScene Actors to sync: {m_InSceneActors.Count}\n" + + $"Actors to spawn: {m_SpawnedActors.Count}"); foreach (Actor actor in m_InSceneActors) { - NativeArray data = SerializeActorState(actor); - if (!data.IsCreated) { - s_Logger.Error($"Failed to serialize actor data for {actor.name}"); - continue; - } - - SynchronizeActorStateForClientRpc(actor.ActorID, actor.GetCoreStateSnapshot(), data, sendParams); + RR.NetworkSystemInstance.WriteActorSynchronize(NetworkPacketTarget.Single(clientID), + actor.ActorID, + actor.GetCoreStateSnapshot(), + actor.Data); } foreach (Actor actor in m_SpawnedActors) { - NativeArray data = SerializeActorState(actor); - if (!data.IsCreated) { - s_Logger.Error($"Failed to serialize actor data for {actor.name}"); - continue; - } - - ActorCoreStateSnapshot coreStateSnapshot = actor.GetCoreStateSnapshot(); - SpawnActorRpc(actor.SourceActorPath, - actor.ActorID, - coreStateSnapshot, - data, - sendParams); + s_Logger.Info("Spawning actor for client synchronization: " + actor.SourceActorPath); + RR.NetworkSystemInstance.WriteSpawnActor(NetworkPacketTarget.Single(clientID), + actor.SourceActorPath, + actor.ActorID, + actor.GetCoreStateSnapshot(), + actor.Data); } } - [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)] - void SynchronizeActorStateForClientRpc(ulong actorID, - ActorCoreStateSnapshot coreStateSnapshot, - NativeArray data, - RpcParams rpcParams) { - Actor actor = FindActorByID(actorID); - if (actor is null) { - return; - } - - actor.RestoreCoreState(coreStateSnapshot); - DeserializeActorState(actor, data); - ClientSynchronizedActorRpc(); - } - - [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)] - void SynchronizeCoreActorStateRpc(ulong actorID, ActorCoreStateSnapshot snapshot, RpcParams rpcParams) { - Actor actor = FindActorByID(actorID); - if (actor is null) { - s_Logger.Error($"Actor with ID {actorID} not found for core state synchronization."); - return; - } - - actor.RestoreCoreState(snapshot); - } - [Rpc(SendTo.Server, Delivery = RpcDelivery.Reliable)] void ClientSynchronizedActorRpc(RpcParams rpcParams = default) { ulong clientID = rpcParams.Receive.SenderClientId; @@ -389,6 +247,7 @@ namespace RebootKit.Engine.Simulation { } clientState.ActorsSyncPacketsLeft--; + s_Logger.Info($"Synchronized actor for client {clientID}. Packets left: {clientState.ActorsSyncPacketsLeft}"); RR.NetworkSystemInstance.UpdateClientState(clientState); if (clientState.ActorsSyncPacketsLeft == 0) { @@ -396,6 +255,129 @@ namespace RebootKit.Engine.Simulation { } } + /// + /// @MARK: Network Data Handling + /// + internal void OnReceivedEntity(NetworkDataHeader header, NativeArray data) { + if (header.Type == NetworkDataType.ActorCoreState) { + Actor actor = FindActorByID(header.ActorID); + if (actor == null) { + s_Logger.Error($"Failed to find actor with ID {header.ActorID} for core state update."); + return; + } + + using NetworkBufferReader reader = new NetworkBufferReader(data); + ActorCoreStateSnapshot coreState = new ActorCoreStateSnapshot(); + coreState.Deserialize(reader); + actor.RestoreCoreState(coreState); + } else if (header.Type == NetworkDataType.ActorTransformSync) { + Actor actor = FindActorByID(header.ActorID); + if (actor == null) { + s_Logger.Error($"Failed to find actor with ID {header.ActorID} for transform state update."); + return; + } + + using NetworkBufferReader reader = new NetworkBufferReader(data); + ActorTransformSyncData transformSyncData = new ActorTransformSyncData(); + transformSyncData.Deserialize(reader); + actor.RestoreTransformState(transformSyncData); + } else if (header.Type == NetworkDataType.ActorState) { + Actor actor = FindActorByID(header.ActorID); + if (actor == null) { + s_Logger.Error($"Failed to find actor with ID {header.ActorID} for state update."); + return; + } + + DataSerializationUtils.Deserialize(data, ref actor.Data); + } else if (header.Type == NetworkDataType.ActorEvent) { + Actor actor = FindActorByID(header.ActorID); + if (actor == null) { + s_Logger.Error($"Failed to find actor with ID {header.ActorID} for event handling."); + return; + } + + throw new NotImplementedException(); + } else if (header.Type == NetworkDataType.ActorCommand) { + Actor actor = FindActorByID(header.ActorID); + if (actor == null) { + s_Logger.Error($"Failed to find actor with ID {header.ActorID} for command handling."); + return; + } + + throw new NotImplementedException(); + } else if (header.Type == NetworkDataType.SynchronizeActor) { + Actor actor = FindActorByID(header.ActorID); + if (actor == null) { + s_Logger.Error($"Failed to find actor with ID {header.ActorID} for synchronization."); + return; + } + + using NetworkBufferReader reader = new NetworkBufferReader(data); + + ActorCoreStateSnapshot coreState = new ActorCoreStateSnapshot(); + coreState.Deserialize(reader); + + reader.Read(out ushort actorDataSize); + reader.Read(out NativeArray stateData, actorDataSize); + + actor.RestoreCoreState(coreState); + DataSerializationUtils.Deserialize(stateData, ref actor.Data); + + ClientSynchronizedActorRpc(); + } else if (header.Type == NetworkDataType.SpawnActor) { + using NetworkBufferReader reader = new NetworkBufferReader(data); + + reader.Read(out FixedString64Bytes value); + string guid = value.ToString(); + + ActorCoreStateSnapshot coreState = new ActorCoreStateSnapshot(); + coreState.Deserialize(reader); + + reader.Read(out ushort actorDataSize); + reader.Read(out NativeArray stateData, actorDataSize); + + SpawnLocalActor(guid, + header.ActorID, + coreState, + stateData); + } + } + + void SpawnLocalActor(string guid, + ulong actorID, + ActorCoreStateSnapshot coreStateSnapshot, + NativeArray stateData) { + AssetReferenceGameObject assetReference = new AssetReferenceGameObject(guid); + if (!assetReference.RuntimeKeyIsValid()) { + s_Logger.Error($"Invalid asset reference for actor with GUID {guid}"); + return; + } + + GameObject actorObject = assetReference + .InstantiateAsync(coreStateSnapshot.Position, coreStateSnapshot.Rotation) + .WaitForCompletion(); + if (actorObject == null) { + s_Logger.Error($"Failed to instantiate actor with GUID {guid}"); + return; + } + + Actor actor = actorObject.GetComponent(); + if (actor is null) { + s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component."); + Destroy(actorObject); + return; + } + + actor.Manager = this; + actor.SourceActorPath = guid; + actor.ActorID = actorID; + actor.Data = actor.InternalCreateActorData(); + + actor.RestoreCoreState(coreStateSnapshot); + DataSerializationUtils.Deserialize(stateData, ref actor.Data); + m_SpawnedActors.Add(actor); + } + // // @MARK: Actor Commands and Events // @@ -422,6 +404,10 @@ namespace RebootKit.Engine.Simulation { } foreach ((ulong clientID, NetworkClientState state) in RR.NetworkSystemInstance.Clients) { + if (NetworkManager.Singleton.LocalClientId == clientID) { + continue; + } + if (state.IsReady) { SendActorEventRpc(actorEvent, RpcTarget.Single(clientID, RpcTargetUse.Temp)); } diff --git a/Runtime/Engine/Code/Steam/SteamManager.cs b/Runtime/Engine/Code/Steam/SteamManager.cs index 97cb1c6..2b2c96a 100644 --- a/Runtime/Engine/Code/Steam/SteamManager.cs +++ b/Runtime/Engine/Code/Steam/SteamManager.cs @@ -24,8 +24,10 @@ namespace RebootKit.Engine.Steam { } IsInitialized = true; - + await UniTask.Yield(cancellationToken); + + SteamFriends.OnGameRichPresenceJoinRequested += OnJoinRequested; } internal static void Shutdown() { @@ -36,9 +38,19 @@ namespace RebootKit.Engine.Steam { s_Logger.Info("Shutting down Steam Manager..."); + SteamFriends.OnGameRichPresenceJoinRequested -= OnJoinRequested; SteamClient.Shutdown(); IsInitialized = false; } + + static void OnJoinRequested(Friend friend, string key) { + s_Logger.Info($"Join request received from {friend.Name} with key: {key}"); + + if (string.IsNullOrEmpty(key)) { + s_Logger.Warning("Join request key is empty. Cannot process join request."); + return; + } + } } } \ No newline at end of file diff --git a/Tests/Runtime/Engine/NetworkBufferWriterReaderTests.cs b/Tests/Runtime/Engine/NetworkBufferWriterReaderTests.cs new file mode 100644 index 0000000..f46f905 --- /dev/null +++ b/Tests/Runtime/Engine/NetworkBufferWriterReaderTests.cs @@ -0,0 +1,204 @@ +using NUnit.Framework; +using RebootKit.Engine.Extensions; +using RebootKit.Engine.Foundation; +using RebootKit.Engine.Network; +using Unity.Collections; +using UnityEngine; + +namespace Tests.Runtime.Engine { + public class NetworkBufferWriterReaderTests { + [Test] + public void NetworkBuffer_Int() { + const int k_Value = 12345; + + using NativeArray data = new NativeArray(4, Allocator.Temp); + + using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0); + writer.Write(k_Value); + + using NetworkBufferReader reader = new NetworkBufferReader(data); + Assert.IsTrue(reader.Read(out int value)); + Assert.AreEqual(k_Value, value); + } + + [Test] + public void NetworkBuffer_Two_Ints() { + const int k_Value1 = 12345; + const int k_Value2 = 67890; + + using NativeArray data = new NativeArray(8, Allocator.Temp); + + using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0); + writer.Write(k_Value1); + writer.Write(k_Value2); + + using NetworkBufferReader reader = new NetworkBufferReader(data); + Assert.IsTrue(reader.Read(out int value1)); + Assert.AreEqual(k_Value1, value1); + Assert.IsTrue(reader.Read(out int value2)); + Assert.AreEqual(k_Value2, value2); + } + + [Test] + public void NetworkBuffer_Short() { + const short k_Value = 123; + + using NativeArray data = new NativeArray(2, Allocator.Temp); + + using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0); + writer.Write(k_Value); + + using NetworkBufferReader reader = new NetworkBufferReader(data); + Assert.IsTrue(reader.Read(out short value)); + Assert.AreEqual(k_Value, value); + } + + [Test] + public void NetworkBuffer_UShort() { + const ushort k_Value = 123; + using NativeArray data = new NativeArray(2, Allocator.Temp); + + using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0); + writer.Write(k_Value); + + using NetworkBufferReader reader = new NetworkBufferReader(data); + Assert.IsTrue(reader.Read(out ushort value)); + Assert.AreEqual(k_Value, value); + } + + [Test] + public void NetworkBuffer_Long() { + const long k_Value = 123456789L; + + using NativeArray data = new NativeArray(8, Allocator.Temp); + + using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0); + writer.Write(k_Value); + + using NetworkBufferReader reader = new NetworkBufferReader(data); + Assert.IsTrue(reader.Read(out long value)); + Assert.AreEqual(k_Value, value); + } + + [Test] + public void NetworkBuffer_ULong() { + const ulong k_Value = 123456789UL; + + using NativeArray data = new NativeArray(8, Allocator.Temp); + + using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0); + writer.Write(k_Value); + + using NetworkBufferReader reader = new NetworkBufferReader(data); + Assert.IsTrue(reader.Read(out ulong value)); + Assert.AreEqual(k_Value, value); + } + + [Test] + public void NetworkBuffer_Bool() { + const bool k_Value = true; + + using NativeArray data = new NativeArray(1, Allocator.Temp); + + using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0); + writer.Write(k_Value); + + using NetworkBufferReader reader = new NetworkBufferReader(data); + Assert.IsTrue(reader.Read(out bool value)); + Assert.AreEqual(k_Value, value); + } + + [Test] + public void NetworkBuffer_Byte() { + const byte k_Value = 0xAB; + + using NativeArray data = new NativeArray(1, Allocator.Temp); + + using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0); + writer.Write(k_Value); + + using NetworkBufferReader reader = new NetworkBufferReader(data); + Assert.IsTrue(reader.Read(out byte value)); + Assert.AreEqual(k_Value, value); + } + + [Test] + public void NetworkBuffer_Float() { + const float k_Value = 123.45f; + using NativeArray data = new NativeArray(4, Allocator.Temp); + using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0); + writer.Write(k_Value); + + using NetworkBufferReader reader = new NetworkBufferReader(data); + Assert.IsTrue(reader.Read(out float value)); + Assert.AreEqual(k_Value, value, 0.0001f); + } + + [Test] + public void NetworkBuffer_ReadBeyond() { + using NativeArray data = new NativeArray(4, Allocator.Temp); + + using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0); + writer.Write(12345); + + using NetworkBufferReader reader = new NetworkBufferReader(data); + Assert.IsTrue(reader.Read(out int value)); + Assert.AreEqual(12345, value); + + Assert.IsFalse(reader.Read(out int _)); + } + + [Test] + public void NetworkBuffer_FixedString32Bytes() { + FixedString32Bytes value = new FixedString32Bytes("henlo_world"); + + using NativeArray data = new NativeArray(32, Allocator.Temp); + using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0); + writer.Write(value); + + using NetworkBufferReader reader = new NetworkBufferReader(data); + Assert.IsTrue(reader.Read(out FixedString32Bytes readValue), "Failed to read FixedString32Bytes"); + Assert.IsTrue(value == readValue, "Value should be equal to read value"); + } + + [Test] + public void NetworkBuffer_Empty() { + using NativeArray data = new NativeArray(0, Allocator.Temp); + + using NetworkBufferReader reader = new NetworkBufferReader(data); + Assert.IsFalse(reader.Read(out int value)); + Assert.AreEqual(0, value); + } + + [Test] + public void NetworkBuffer_Size_NativeArray() { + NativeArray value = new NativeArray(new byte[] { + 1, + 2, + 3, + 4, + 5 + }, Allocator.Temp); + + using NativeArray data = new NativeArray(value.Length + sizeof(ushort), Allocator.Temp); + using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0); + writer.Write((ushort) value.Length); + writer.Write(value); + + using NetworkBufferReader reader = new NetworkBufferReader(data); + reader.Read(out ushort readLength); + Assert.IsTrue(readLength == value.Length, + $"Read Length({readLength }) should be equal to an actual value.length({value.Length})"); + Assert.IsTrue(reader.Read(out NativeArray readValue, readLength), "Failed to read NativeArray"); + Assert.AreEqual(value.Length, readValue.Length, + "Length of read NativeArray should match written length"); + + for (int i = 0; i < value.Length; i++) { + Assert.AreEqual(value[i], readValue[i], $"Value at index {i} should match"); + } + + value.Dispose(); + readValue.Dispose(); + } + } +} \ No newline at end of file diff --git a/Tests/Runtime/Engine/NetworkBufferWriterReaderTests.cs.meta b/Tests/Runtime/Engine/NetworkBufferWriterReaderTests.cs.meta new file mode 100644 index 0000000..19e0940 --- /dev/null +++ b/Tests/Runtime/Engine/NetworkBufferWriterReaderTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8f01bbcc978f4b9993c29d389987bc9c +timeCreated: 1752874442 \ No newline at end of file diff --git a/Tests/Runtime/Engine/RebootKit.Engine.Tests.asmdef b/Tests/Runtime/Engine/RebootKit.Engine.Tests.asmdef index 3670c49..9b72f2d 100755 --- a/Tests/Runtime/Engine/RebootKit.Engine.Tests.asmdef +++ b/Tests/Runtime/Engine/RebootKit.Engine.Tests.asmdef @@ -1,20 +1,21 @@ { - "name": "RebootKit.Engine.Tests", - "rootNamespace": "", - "references": [ - "GUID:284059c7949783646b281a1b815580e6", - "GUID:0acc523941302664db1f4e527237feb3", - "GUID:27619889b8ba8c24980f49ee34dbb44a" - ], - "includePlatforms": [ - "Editor" - ], - "excludePlatforms": [], - "allowUnsafeCode": false, - "overrideReferences": false, - "precompiledReferences": [], - "autoReferenced": true, - "defineConstraints": [], - "versionDefines": [], - "noEngineReferences": false + "name": "RebootKit.Engine.Tests", + "rootNamespace": "", + "references": [ + "GUID:284059c7949783646b281a1b815580e6", + "GUID:0acc523941302664db1f4e527237feb3", + "GUID:e0cd26848372d4e5c891c569017e11f1", + "GUID:27619889b8ba8c24980f49ee34dbb44a" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false } \ No newline at end of file