diff --git a/Editor/Background/EnsureUniqueActorID.cs b/Editor/Background/EnsureUniqueActorID.cs index e075383..0fdd1f8 100644 --- a/Editor/Background/EnsureUniqueActorID.cs +++ b/Editor/Background/EnsureUniqueActorID.cs @@ -21,7 +21,7 @@ namespace RebootKit.Editor.Background { EditorUtility.InstanceIDToObject(createEvent.instanceId) as GameObject; if (gameObjectChanged != null && gameObjectChanged.TryGetComponent(out Actor actor)) { - actor.ActorID = UniqueID.NewULongFromGuid(); + actor.ActorStaticID = UniqueID.NewULongFromGuid(); EditorUtility.SetDirty(actor); } diff --git a/Editor/Build/BuildVersionBumper.cs b/Editor/Build/BuildVersionBumper.cs index 6f09edb..2f6b19a 100755 --- a/Editor/Build/BuildVersionBumper.cs +++ b/Editor/Build/BuildVersionBumper.cs @@ -1,9 +1,9 @@ -using UnityEngine; -using UnityEditor; +using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; +using UnityEngine; -namespace RebootKitEditor.Build { +namespace RebootKit.Editor.Build { class BuildVersionBumper : IPostprocessBuildWithReport { public int callbackOrder => 0; diff --git a/Editor/CommonEditorActions.cs b/Editor/CommonEditorActions.cs index 901fe8b..febba7c 100755 --- a/Editor/CommonEditorActions.cs +++ b/Editor/CommonEditorActions.cs @@ -1,9 +1,9 @@ using System.IO; -using RebootKitEditor.Build; +using RebootKit.Editor.Build; using UnityEditor; using UnityEngine; -namespace RebootKitEditor { +namespace RebootKit.Editor { static class CommonEditorActions { [MenuItem(REditorConsts.k_EditorMenu + "Bump minor version", false, 0)] static void BumpMinorVersion() { diff --git a/Editor/PropertyDrawers/CVarDrawer.cs b/Editor/PropertyDrawers/CVarDrawer.cs index 4d42a41..fb0fbb0 100755 --- a/Editor/PropertyDrawers/CVarDrawer.cs +++ b/Editor/PropertyDrawers/CVarDrawer.cs @@ -1,9 +1,9 @@ -using RebootKit.Engine.Foundation; -using RebootKitEditor.Utils; +using RebootKit.Editor.Utils; +using RebootKit.Engine.Foundation; using UnityEditor; using UnityEngine; -namespace RebootKitEditor.PropertyDrawers { +namespace RebootKit.Editor.PropertyDrawers { [CustomPropertyDrawer(typeof(ConfigVar))] public class CVarDrawer : PropertyDrawer { bool m_Expand; diff --git a/Editor/PropertyDrawers/ConstsPropertyDrawer.cs b/Editor/PropertyDrawers/ConstsPropertyDrawer.cs index eaf713e..82aa473 100755 --- a/Editor/PropertyDrawers/ConstsPropertyDrawer.cs +++ b/Editor/PropertyDrawers/ConstsPropertyDrawer.cs @@ -2,7 +2,7 @@ using UnityEditor; using UnityEngine; -namespace RebootKitEditor.PropertyDrawers { +namespace RebootKit.Editor.PropertyDrawers { [CustomPropertyDrawer(typeof(ConstsProperty<>))] public class ConstsPropertyDrawer : PropertyDrawer { const string k_InlineValue = "m_InlineValue"; diff --git a/Editor/PropertyDrawers/SerializableGuidDrawer.cs b/Editor/PropertyDrawers/SerializableGuidDrawer.cs index 39580ed..3d77c83 100755 --- a/Editor/PropertyDrawers/SerializableGuidDrawer.cs +++ b/Editor/PropertyDrawers/SerializableGuidDrawer.cs @@ -3,7 +3,7 @@ using RebootKit.Engine.Foundation; using UnityEditor; using UnityEngine; -namespace RebootKitEditor.PropertyDrawers { +namespace RebootKit.Editor.PropertyDrawers { [CustomPropertyDrawer(typeof(SerializableGuid))] public class SerializableGuidDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { diff --git a/Editor/PropertyDrawers/ValueRangeDrawer.cs b/Editor/PropertyDrawers/ValueRangeDrawer.cs index 35cea46..8cb982a 100755 --- a/Editor/PropertyDrawers/ValueRangeDrawer.cs +++ b/Editor/PropertyDrawers/ValueRangeDrawer.cs @@ -2,7 +2,7 @@ using UnityEditor; using UnityEngine; -namespace RebootKitEditor.PropertyDrawers { +namespace RebootKit.Editor.PropertyDrawers { [CustomPropertyDrawer(typeof(FloatRange))] public class ValueRangeDrawer : PropertyDrawer { const string k_minPropertyName = "min"; diff --git a/Editor/REditorConsts.cs b/Editor/REditorConsts.cs index 788449c..f519a84 100755 --- a/Editor/REditorConsts.cs +++ b/Editor/REditorConsts.cs @@ -1,4 +1,4 @@ -namespace RebootKitEditor { +namespace RebootKit.Editor { static class REditorConsts { internal const string k_EditorMenu = "Reboot Reality/"; diff --git a/Editor/RebootWindow/ConfigVarsView.cs b/Editor/RebootWindow/ConfigVarsView.cs index 37a450a..3311c8c 100755 --- a/Editor/RebootWindow/ConfigVarsView.cs +++ b/Editor/RebootWindow/ConfigVarsView.cs @@ -3,7 +3,7 @@ using RebootKit.Engine.UI; using UnityEngine; using UnityEngine.UIElements; -namespace RebootKitEditor.RebootWindow { +namespace RebootKit.Editor.RebootWindow { public class ConfigVarsView : IView { public void Dispose() { } diff --git a/Editor/RebootWindow/HomeView.cs b/Editor/RebootWindow/HomeView.cs index 199bc65..af489c2 100755 --- a/Editor/RebootWindow/HomeView.cs +++ b/Editor/RebootWindow/HomeView.cs @@ -1,11 +1,10 @@ using RebootKit.Engine.Console; -using RebootKit.Engine.Main; using RebootKit.Engine.UI; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; -namespace RebootKitEditor.RebootWindow { +namespace RebootKit.Editor.RebootWindow { public class HomeView : IView { public void Dispose() { } diff --git a/Editor/RebootWindow/RebootEditorWindow.cs b/Editor/RebootWindow/RebootEditorWindow.cs index 863d562..13979d4 100755 --- a/Editor/RebootWindow/RebootEditorWindow.cs +++ b/Editor/RebootWindow/RebootEditorWindow.cs @@ -1,13 +1,10 @@ -using RebootKit.Editor.RebootWindow; -using RebootKit.Engine; -using RebootKit.Engine.Foundation; +using RebootKit.Engine; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; using Logger = RebootKit.Engine.Foundation.Logger; -using TabView = RebootKit.Editor.RebootWindow.TabView; -namespace RebootKitEditor.RebootWindow { +namespace RebootKit.Editor.RebootWindow { static class RTheme { public static readonly Color s_FirstColor = ColorFromHex("#B9B8B9"); public static readonly Color s_SecondColor = ColorFromHex("#6B6B6B"); diff --git a/Editor/RebootWindow/TabView.cs b/Editor/RebootWindow/TabView.cs index a2f0f45..c00a03e 100755 --- a/Editor/RebootWindow/TabView.cs +++ b/Editor/RebootWindow/TabView.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using RebootKit.Engine.UI; -using RebootKitEditor.RebootWindow; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.UIElements; diff --git a/Editor/RebootWindow/WorldsView.cs b/Editor/RebootWindow/WorldsView.cs index 3628332..890c843 100755 --- a/Editor/RebootWindow/WorldsView.cs +++ b/Editor/RebootWindow/WorldsView.cs @@ -1,14 +1,14 @@ -using RebootKit.Engine; +using RebootKit.Editor.Utils; +using RebootKit.Engine; using RebootKit.Engine.Simulation; using RebootKit.Engine.UI; -using RebootKitEditor.Utils; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UIElements; -namespace RebootKitEditor.RebootWindow { +namespace RebootKit.Editor.RebootWindow { public class WorldsView : IView { public void Dispose() { } diff --git a/Editor/Utils/AssetDatabaseEx.cs b/Editor/Utils/AssetDatabaseEx.cs index d6dc22c..32b3b6a 100755 --- a/Editor/Utils/AssetDatabaseEx.cs +++ b/Editor/Utils/AssetDatabaseEx.cs @@ -1,7 +1,7 @@ using UnityEditor; using UnityEngine; -namespace RebootKitEditor.Utils { +namespace RebootKit.Editor.Utils { public static class AssetDatabaseEx { public static T[] LoadAllAssets() where T : Object { string[] guids = AssetDatabase.FindAssets($"t:{typeof(T).Name}"); diff --git a/Editor/Utils/CVarSerializedPropertiesFinder.cs b/Editor/Utils/CVarSerializedPropertiesFinder.cs index 75a3b47..8ab0f1c 100755 --- a/Editor/Utils/CVarSerializedPropertiesFinder.cs +++ b/Editor/Utils/CVarSerializedPropertiesFinder.cs @@ -1,6 +1,6 @@ using UnityEditor; -namespace RebootKitEditor.Utils { +namespace RebootKit.Editor.Utils { public struct CVarSerializedProperties { public SerializedProperty flags; public SerializedProperty name; diff --git a/Editor/Utils/DomainReloader.cs b/Editor/Utils/DomainReloader.cs new file mode 100644 index 0000000..d04f2c3 --- /dev/null +++ b/Editor/Utils/DomainReloader.cs @@ -0,0 +1,17 @@ +using UnityEditor; +using UnityEngine; + +namespace RebootKit.Editor.Utils { + public static class DomainReloader { + [MenuItem(REditorConsts.k_EditorMenu + "Reload Domain", priority = 1000)] + public static void ReloadDomain() { + if (EditorApplication.isCompiling) { + Debug.LogError("Cannot reload domain while compiling."); + EditorUtility.DisplayDialog("Reload Domain", "Cannot reload domain while compiling.", "OK"); + return; + } + + EditorUtility.RequestScriptReload(); + } + } +} \ No newline at end of file diff --git a/Editor/Utils/DomainReloader.cs.meta b/Editor/Utils/DomainReloader.cs.meta new file mode 100644 index 0000000..37160da --- /dev/null +++ b/Editor/Utils/DomainReloader.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 38260f05c1a7436cbe1813733e82ef56 +timeCreated: 1753762882 \ No newline at end of file diff --git a/Editor/VisualElements/CVarPropertyField.cs b/Editor/VisualElements/CVarPropertyField.cs index ffe90c6..05b3789 100755 --- a/Editor/VisualElements/CVarPropertyField.cs +++ b/Editor/VisualElements/CVarPropertyField.cs @@ -3,7 +3,7 @@ using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; -namespace SzafaKitEditor.VisualElements { +namespace RebootKit.Editor.VisualElements { public class CVarPropertyField : VisualElement { readonly SerializedProperty _cvarProperty; readonly SerializedProperty _defaultValueKindProperty; diff --git a/Runtime/Engine/Code/Development/DebugOverlayView.cs b/Runtime/Engine/Code/Development/DebugOverlayView.cs index fc6cdd7..613a6fa 100644 --- a/Runtime/Engine/Code/Development/DebugOverlayView.cs +++ b/Runtime/Engine/Code/Development/DebugOverlayView.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Globalization; +using System.Text; using RebootKit.Engine.Main; using RebootKit.Engine.Network; using RebootKit.Engine.Simulation; @@ -55,7 +56,7 @@ namespace RebootKit.Engine.Development { } void AppendNetworkStateInfo() { - NetworkSystem network = RR.NetworkSystemInstance; + NetworkSystem network = RR.Network; if (network == null) { m_StringBuilder.AppendLine("NetworkSystem not initialized"); @@ -74,7 +75,6 @@ namespace RebootKit.Engine.Development { if (network.TryGetClientState(network.LocalClientID, out NetworkClientState clientState)) { m_StringBuilder.Append($"LocalClientID: {clientState.ClientID.ToString()}"); m_StringBuilder.Append($" | SyncState: {clientState.SyncState.ToString()}"); - m_StringBuilder.Append($" | ActorsSyncPacketsLeft: {clientState.ActorsSyncPacketsLeft.ToString()}"); m_StringBuilder.Append($" | IsReady: {clientState.IsReady.ToString()}"); } else { m_StringBuilder.Append("ClientState not found for LocalClientID: "); @@ -82,10 +82,35 @@ namespace RebootKit.Engine.Development { } m_StringBuilder.AppendLine(); + + if (network.Manager.Stats != null) { + m_StringBuilder.Append("Stats: "); + m_StringBuilder.Append($"Send Reliable: {FormatToLargestUnit(network.Manager.Stats.ReliableBytesSentPerSecond)}/s"); + m_StringBuilder.Append($" | Send Unreliable: {FormatToLargestUnit(network.Manager.Stats.UnreliableBytesSentPerSecond)}/s"); + m_StringBuilder.Append($" | Receive: {FormatToLargestUnit(network.Manager.Stats.BytesReceivedPerSecond)}/s"); + m_StringBuilder.Append($" | Ping: {network.Manager.Stats.Ping.ToString()}ms"); + m_StringBuilder.AppendLine(); + } + } + + string FormatToLargestUnit(double value) { + if (value < 1024) { + return $"{value:F2} B"; + } + + if (value < 1024 * 1024) { + return $"{(value / 1024):F2} KB"; + } + + if (value < 1024 * 1024 * 1024) { + return $"{(value / (1024 * 1024)):F2} MB"; + } + + return $"{(value / (1024 * 1024 * 1024)):F2} GB"; } void AppendActorsStateInfo() { - NetworkSystem network = RR.NetworkSystemInstance; + NetworkSystem network = RR.Network; if (network == null) { return; } diff --git a/Runtime/Engine/Code/Development/DevToolsService.cs b/Runtime/Engine/Code/Development/DevToolsService.cs index 2c9f4fb..8b6155a 100644 --- a/Runtime/Engine/Code/Development/DevToolsService.cs +++ b/Runtime/Engine/Code/Development/DevToolsService.cs @@ -1,6 +1,5 @@ using System; using RebootKit.Engine.Foundation; -using Unity.Multiplayer.Tools.NetStatsMonitor; using UnityEngine; using UnityEngine.InputSystem; @@ -11,15 +10,11 @@ namespace RebootKit.Engine.Development { [ConfigVar("debug.game_version", 1, "Controls game version overlay visibility. 0 - hidden, 1 - visible")] public static ConfigVar ShowGameVersion; - - [ConfigVar("debug.network_stats", 1, "Controls network stats overlay visibility. 0 - hidden, 1 - visible")] - public static ConfigVar ShowNetworkStats; } public class DevToolsService : ServiceMonoBehaviour { [SerializeField] DebugOverlayView m_DebugOverlayView; [SerializeField] GameVersionOverlay m_GameVersionOverlay; - [SerializeField] RuntimeNetStatsMonitor m_NetworkStatsOverlay; IDisposable m_CVarChangedListener; @@ -27,7 +22,6 @@ namespace RebootKit.Engine.Development { ConfigVar.StateChanged += OnCVarChanged; OnCVarChanged(DebugCVars.OverlayMode); OnCVarChanged(DebugCVars.ShowGameVersion); - OnCVarChanged(DebugCVars.ShowNetworkStats); } void OnDisable() { @@ -42,10 +36,6 @@ namespace RebootKit.Engine.Development { if (InputSystem.GetDevice().f3Key.wasReleasedThisFrame) { DebugCVars.OverlayMode.Set(DebugCVars.OverlayMode.IndexValue == 1 ? 0 : 1); } - - if (InputSystem.GetDevice().f4Key.wasReleasedThisFrame) { - DebugCVars.ShowNetworkStats.Set(DebugCVars.ShowNetworkStats.IndexValue == 1 ? 0 : 1); - } } void OnOverlayModeChanged(int mode) { @@ -61,8 +51,6 @@ namespace RebootKit.Engine.Development { OnOverlayModeChanged(cvar.IndexValue); } else if (cvar == DebugCVars.ShowGameVersion) { m_GameVersionOverlay.gameObject.SetActive(cvar.IndexValue > 0); - } else if (cvar == DebugCVars.ShowNetworkStats) { - m_NetworkStatsOverlay.Visible = cvar.IndexValue > 0; } } } diff --git a/Runtime/Engine/Code/EngineConfigAsset.cs b/Runtime/Engine/Code/EngineConfigAsset.cs index f63736c..1f9ff0d 100755 --- a/Runtime/Engine/Code/EngineConfigAsset.cs +++ b/Runtime/Engine/Code/EngineConfigAsset.cs @@ -17,5 +17,8 @@ namespace RebootKit.Engine { // @NOTE: Spacewar, change as needed [Header("Steam")] public uint steamAppID = 480; + + [Header("Network")] + public byte protocolVersion = 1; } } \ No newline at end of file diff --git a/Runtime/Engine/Code/Extensions/NativeArrayEx.cs b/Runtime/Engine/Code/Extensions/NativeArrayEx.cs index b9e2278..2b34f06 100644 --- a/Runtime/Engine/Code/Extensions/NativeArrayEx.cs +++ b/Runtime/Engine/Code/Extensions/NativeArrayEx.cs @@ -14,6 +14,14 @@ namespace RebootKit.Engine.Extensions { } return string.Empty; } - + + public static string ToHexString(this NativeSlice slice) { + StringBuilder sb = new StringBuilder(slice.Length * 3); + for (int i = 0; i < slice.Length; i++) { + sb.AppendFormat("{0:X2} ", slice[i]); + } + + return sb.ToString(); + } } } \ No newline at end of file diff --git a/Runtime/Engine/Code/Foundation/QuantizationUtility.cs b/Runtime/Engine/Code/Foundation/QuantizationUtility.cs new file mode 100644 index 0000000..6b840b0 --- /dev/null +++ b/Runtime/Engine/Code/Foundation/QuantizationUtility.cs @@ -0,0 +1,25 @@ +using System.Runtime.CompilerServices; + +namespace RebootKit.Engine.Foundation { + public static class QuantizationUtility { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort FloatToUShort(float value, float min, float max) { + return (ushort)((value - min) / (max - min) * ushort.MaxValue); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float UShortToFloat(ushort value, float min, float max) { + return min + (value / (float)ushort.MaxValue) * (max - min); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte FloatToByte(float value, float min, float max) { + return (byte)((value - min) / (max - min) * byte.MaxValue); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ByteToFloat(byte value, float min, float max) { + return min + (value / (float)byte.MaxValue) * (max - min); + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Foundation/QuantizationUtility.cs.meta b/Runtime/Engine/Code/Foundation/QuantizationUtility.cs.meta new file mode 100644 index 0000000..00caaec --- /dev/null +++ b/Runtime/Engine/Code/Foundation/QuantizationUtility.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a1c1f40f52a64f4ab55436cc00e2ef20 +timeCreated: 1753473209 \ No newline at end of file diff --git a/Runtime/Engine/Code/Main/Game.cs b/Runtime/Engine/Code/Main/Game.cs index 1b09e29..e7ba237 100644 --- a/Runtime/Engine/Code/Main/Game.cs +++ b/Runtime/Engine/Code/Main/Game.cs @@ -1,47 +1,46 @@ using System; -using Cysharp.Threading.Tasks; -using Unity.Collections; -using Unity.Netcode; +using RebootKit.Engine.Network; using UnityEngine; using Logger = RebootKit.Engine.Foundation.Logger; namespace RebootKit.Engine.Main { - public abstract class Game : NetworkBehaviour { + public abstract class Game : MonoBehaviour { static readonly Logger s_GameLogger = new Logger(nameof(Game)); + + [SerializeField] NetworkPlayerController m_PlayerControllerPrefab; + + public NetworkPlayerController LocalPlayerController { get; internal set; } + + protected virtual void Awake() { + LocalPlayerController = Instantiate(m_PlayerControllerPrefab, transform); + } - // Server only callbacks + // + // @MARK: Server + // protected virtual void OnPlayerBecameReady(ulong clientID) { } - // Event callbacks + public virtual void OnClientConnected(ulong clientID) { + s_GameLogger.Info($"Client {clientID} connected"); + } + + public virtual void OnClientDisconnected(ulong clientID) { + s_GameLogger.Info($"Client {clientID} disconnected"); + } + + // + // @MARK: Chat + // protected virtual void OnChatMessage(string message) { s_GameLogger.Info($"Chat: {message}"); } - // NGO callbacks - public override void OnNetworkSpawn() { - base.OnNetworkSpawn(); - RR.GameInstance = this; - } - - public override void OnNetworkDespawn() { - base.OnNetworkDespawn(); - RR.GameInstance = null; - } - - // Chat - [Rpc(SendTo.Server)] - public void SendChatMessageRpc(string message) { - PrintChatMessageClientRpc(message); - } - - [ClientRpc(Delivery = RpcDelivery.Reliable)] - void PrintChatMessageClientRpc(string message) { - OnChatMessage(message); - } - + // + // @MARK: Player Ready State + // internal void PlayerBecameReady(ulong clientID) { - if (!IsServer) { + if (!RR.IsServer()) { s_GameLogger.Error("PlayerBecameReady called on client, but this should only be called on the server."); return; } diff --git a/Runtime/Engine/Code/Main/RR.cs b/Runtime/Engine/Code/Main/RR.cs index 3bc0711..e787179 100755 --- a/Runtime/Engine/Code/Main/RR.cs +++ b/Runtime/Engine/Code/Main/RR.cs @@ -9,8 +9,9 @@ using RebootKit.Engine.Foundation; using RebootKit.Engine.Input; using RebootKit.Engine.Network; using RebootKit.Engine.Simulation; +#if RR_STEAM using RebootKit.Engine.Steam; -using Unity.Netcode; +#endif using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; @@ -28,7 +29,7 @@ namespace RebootKit.Engine.Main { [ConfigVar("con.write_log", 1, "Enables writing game log to console output")] static ConfigVar s_WriteLogToConsole; - + internal static EngineConfigAsset EngineConfig; static DisposableBag s_DisposableBag; @@ -36,8 +37,13 @@ namespace RebootKit.Engine.Main { static AsyncOperationHandle s_MainMenuSceneHandle; - static NetworkSystem s_NetworkSystemPrefab; - internal static NetworkSystem NetworkSystemInstance; + internal static NetworkSystem Network; + internal static byte NetworkProtocolVersion { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { + return EngineConfig.protocolVersion; + } + } internal static ConsoleService Console { get; private set; } public static InputService Input { get; private set; } @@ -65,30 +71,18 @@ namespace RebootKit.Engine.Main { s_ServicesBag.Add(Input); World = CreateService(); - await InitializeAssetsAsync(cancellationToken); - #if RR_STEAM await SteamManager.InitializeAsync(cancellationToken); + Network = new NetworkSystem(new SteamNetworkManager()); +#else + Network = new NetworkSystem(new UnityNetworkManager()); #endif + + await InitializeAssetsAsync(cancellationToken); } // @NOTE: This method is called after the main scene is loaded. internal static async UniTask RunAsync(CancellationToken cancellationToken) { - s_NetworkSystemPrefab = - Resources.Load(RConsts.k_CoreNetworkGameSystemsResourcesPath); - - NetworkManager.Singleton.OnConnectionEvent += OnConnectionEvent; - NetworkManager.Singleton.OnServerStarted += OnServerStarted; - NetworkManager.Singleton.OnServerStopped += OnServerStopped; - -#if RR_STEAM - if (NetworkManager.Singleton.TryGetComponent(out FacepunchTransport facepunchTransport)) { - NetworkManager.Singleton.NetworkConfig.NetworkTransport = facepunchTransport; - } else { - s_Logger.Error("Steam integration is enabled but FacepunchTransport is not found in NetworkManager."); - } -#endif - Observable.EveryUpdate() .Subscribe(_ => Tick()) .AddTo(ref s_DisposableBag); @@ -112,16 +106,12 @@ namespace RebootKit.Engine.Main { s_Logger.Info("Shutting down"); if (GameInstance is not null) { - GameInstance.NetworkObject.Despawn(); Object.Destroy(GameInstance); GameInstance = null; } - if (NetworkManager.Singleton is not null) { - NetworkManager.Singleton.OnConnectionEvent -= OnConnectionEvent; - NetworkManager.Singleton.OnServerStarted -= OnServerStarted; - NetworkManager.Singleton.OnServerStopped -= OnServerStopped; - } + Network.Dispose(); + Network = null; #if RR_STEAM SteamManager.Shutdown(); @@ -159,8 +149,10 @@ namespace RebootKit.Engine.Main { return worldConfig; } - // Game API - public static async UniTask OpenMainMenuAsync(CancellationToken cancellationToken) { + // + // @MARK: Game + // + internal static async UniTask OpenMainMenuAsync(CancellationToken cancellationToken) { s_Logger.Info("Opening main menu"); World.Unload(); @@ -201,7 +193,7 @@ namespace RebootKit.Engine.Main { return; } - NetworkSystemInstance.SetCurrentWorld(worldID); + Network.SetCurrentWorld(worldID); } public static Actor SpawnActor(AssetReferenceGameObject assetReference, @@ -212,7 +204,7 @@ namespace RebootKit.Engine.Main { return null; } - if (NetworkSystemInstance is null) { + if (Network is null) { s_Logger.Error("NetworkSystemInstance is not initialized. Cannot spawn actor."); return null; } @@ -223,16 +215,16 @@ namespace RebootKit.Engine.Main { } s_Logger.Info($"Spawning actor from asset reference: {assetReference.RuntimeKey}"); - return NetworkSystemInstance.Actors.SpawnActor(assetReference, position, rotation); + return Network.Actors.SpawnActor(assetReference, position, rotation); } - public static Actor FindSpawnedActor(ulong actorID) { - if (NetworkSystemInstance is null) { + public static Actor FindSpawnedActor(ushort actorID) { + if (Network is null) { s_Logger.Error("NetworkSystemInstance is not initialized. Cannot find actor."); return null; } - Actor actor = NetworkSystemInstance.Actors.FindActorByID(actorID); + Actor actor = Network.Actors.FindActorByID(actorID); if (actor is null) { s_Logger.Error($"Actor with ID {actorID} not found"); } @@ -240,7 +232,24 @@ namespace RebootKit.Engine.Main { return actor; } - // Service API + public static void PossessActor(ulong clientID, ushort actorID) { + if (!IsServer()) { + s_Logger.Error("Only server can possess actors for clients."); + return; + } + + if (Network == null) { + s_Logger.Error("Network is not initialized. Cannot possess actor."); + return; + } + + Network.SendPossessedActor(clientID, actorID); + } + + // + // @MARK: Service API + // Services seems to be useless in the current architecture. Consider removing this API in the future. + // public static TService CreateService(ServiceAsset asset) where TService : class, IService { if (asset is null) { throw new ArgumentNullException($"Null asset of type {typeof(TService)}"); @@ -257,7 +266,9 @@ namespace RebootKit.Engine.Main { return service; } - // Logging API + // + // @MARK: Logging API + // public static void Log(string message) { Debug.Log(message); Console?.WriteToOutput(message); @@ -277,7 +288,9 @@ namespace RebootKit.Engine.Main { Console?.WriteToOutput(message); } - // CVar API + // + // @MARK: Config Variables + // public static ConfigVar CVarIndex(string name, int defaultValue = -1) { ConfigVar cvar = ConfigVarsContainer.Get(name); if (cvar != null) { @@ -311,49 +324,57 @@ namespace RebootKit.Engine.Main { return cvar; } - // Network API + // + // @MARK: Network API + // public static bool IsServer() { - return NetworkManager.Singleton.IsServer; + return Network != null && Network.Manager.IsServer(); } public static bool IsClient() { - return NetworkManager.Singleton.IsClient; + return Network != null && Network.Manager.IsClient(); } public static void StartHost() { - if (NetworkManager.Singleton.IsHost) { - s_Logger.Error("Already hosting a server"); + if (IsServer() || IsClient()) { + s_Logger.Error("Already hosting a server or connected as a client"); return; } + // @TODO: Handle failures + s_Logger.Info("Starting host"); - NetworkManager.Singleton.StartHost(); + if (!Network.Manager.StartHost()) { + s_Logger.Error("Failed to start host."); + return; + } } - public static void StopServer() { } + public static void StopHost() { + Network.Manager.StopHost(); + } public static void Connect() { - if (NetworkManager.Singleton.IsClient) { + if (IsClient()) { s_Logger.Error("Already connected to a server"); return; } - s_Logger.Info($"Connecting to server."); - NetworkManager.Singleton.StartClient(); + s_Logger.Info("Connecting to server."); + Network.Manager.StartClient(); } public static void ConnectWithSteamID(ulong steamId) { #if RR_STEAM - - if (NetworkManager.Singleton.IsClient) { + if (IsClient()) { s_Logger.Error("Already connected to a server"); return; } s_Logger.Info($"Connecting to server with Steam ID: {steamId}"); - if (NetworkManager.Singleton.NetworkConfig.NetworkTransport is FacepunchTransport facepunchTransport) { - facepunchTransport.targetSteamId = steamId; - NetworkManager.Singleton.StartClient(); + if (Network.Manager is SteamNetworkManager steamNetworkManager) { + steamNetworkManager.TargetSteamID = steamId; + Network.Manager.StartClient(); } else { s_Logger.Error("Network transport is not FacepunchTransport. Cannot connect with Steam ID."); } @@ -362,7 +383,9 @@ namespace RebootKit.Engine.Main { #endif } - public static void Disconnect() { } + public static void Disconnect() { + Network.Manager.Disconnect(); + } public static void SendChatMessage(string message) { if (!IsClient()) { @@ -374,40 +397,48 @@ namespace RebootKit.Engine.Main { return; } - GameInstance.SendChatMessageRpc(message); + throw new NotSupportedException("Cannot send chat message. Not connected to a server."); } - + static void Tick() { + float deltaTime = Time.deltaTime; + Network.Tick(deltaTime); } - static void OnConnectionEvent(NetworkManager network, ConnectionEventData data) { - s_Logger.Info("Connection event: " + data.EventType); - } - - static void OnServerStarted() { + internal static void OnServerStarted() { s_Logger.Info("Server started"); GameInstance = Object.Instantiate(EngineConfig.gamePrefab); - GameInstance.NetworkObject.Spawn(); - - NetworkSystemInstance = Object.Instantiate(s_NetworkSystemPrefab); - NetworkSystemInstance.NetworkObject.Spawn(); } - static void OnServerStopped(bool obj) { + internal static void OnServerStopped() { s_Logger.Info("Server stopped"); if (GameInstance is not null) { - GameInstance.NetworkObject.Despawn(); + Object.Destroy(GameInstance.gameObject); GameInstance = null; } + } - if (NetworkSystemInstance is not null) { - if (NetworkSystemInstance.NetworkObject is not null && NetworkSystemInstance.NetworkObject.IsSpawned) { - NetworkSystemInstance.NetworkObject.Despawn(); - } + internal static void OnClientStarted() { + if (IsServer()) { + return; + } - NetworkSystemInstance = null; + GameInstance = Object.Instantiate(EngineConfig.gamePrefab); + } + + internal static void OnClientStopped() { + if (IsServer()) { + return; + } + + World.Unload(); + OpenMainMenuAsync(CancellationToken.None).Forget(); + + if (GameInstance is not null) { + Object.Destroy(GameInstance.gameObject); + GameInstance = null; } } diff --git a/Runtime/Engine/Code/Network/DataSerializationUtils.cs b/Runtime/Engine/Code/Network/DataSerializationUtils.cs index 30a2807..c84bf7b 100644 --- a/Runtime/Engine/Code/Network/DataSerializationUtils.cs +++ b/Runtime/Engine/Code/Network/DataSerializationUtils.cs @@ -1,7 +1,4 @@ -using System; -using Unity.Collections; -using Unity.Collections.LowLevel.Unsafe; -using Unity.Netcode; +using Unity.Collections; using UnityEngine.Assertions; namespace RebootKit.Engine.Network { @@ -29,7 +26,7 @@ namespace RebootKit.Engine.Network { return default; } - public static void Deserialize(NativeArray data, ref TEntity entity) + public static void Deserialize(NativeSlice data, ref TEntity entity) where TEntity : ISerializableEntity { using NetworkBufferReader reader = new NetworkBufferReader(data); if (reader.HasNext(data.Length)) { diff --git a/Runtime/Engine/Code/Network/INetworkManager.cs b/Runtime/Engine/Code/Network/INetworkManager.cs new file mode 100644 index 0000000..f9d3ea2 --- /dev/null +++ b/Runtime/Engine/Code/Network/INetworkManager.cs @@ -0,0 +1,62 @@ +using System; +using Unity.Collections; + +namespace RebootKit.Engine.Network { + public enum SendMode { + Reliable, + Unreliable + } + + public interface INetworkManagerDelegate { + void OnServerStarted(); + void OnServerStopped(); + void OnClientStarted(); + void OnClientStopped(); + void OnClientConnected(ulong clientID); + void OnClientDisconnected(ulong clientID); + void OnMessageReceived(ulong senderID, NativeArray data); + } + + public class NetworkManagerStats { + public ulong ReliableBytesSent { get; set; } + public ulong UnreliableBytesSent { get; set; } + public ulong BytesReceived { get; set; } + + public double ReliableBytesSentPerSecond { get; set; } + public double UnreliableBytesSentPerSecond { get; set; } + public double BytesReceivedPerSecond { get; set; } + + public int Ping { get; set; } + + public void Reset() { + ReliableBytesSent = 0; + UnreliableBytesSent = 0; + BytesReceived = 0; + ReliableBytesSentPerSecond = 0.0; + UnreliableBytesSentPerSecond = 0.0; + BytesReceivedPerSecond = 0.0; + Ping = 0; + } + } + + public interface INetworkManager : IDisposable { + INetworkManagerDelegate Delegate { get; set; } + + ulong LocalClientID { get; } + + NetworkManagerStats Stats { get; } + + bool IsServer(); + bool StartHost(); + void StopHost(); + + bool IsClient(); + bool StartClient(); + void Disconnect(); + + void Send(ulong clientID, NativeSlice data, SendMode mode); + unsafe void Send(ulong clientID, byte* data, int length, SendMode mode); + + void Tick(); + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/INetworkManager.cs.meta b/Runtime/Engine/Code/Network/INetworkManager.cs.meta new file mode 100644 index 0000000..84a4378 --- /dev/null +++ b/Runtime/Engine/Code/Network/INetworkManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 26612cc58cd249cf8d567501e253eab1 +timeCreated: 1753138601 \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/NetworkBufferReader.cs b/Runtime/Engine/Code/Network/NetworkBufferReader.cs index ac3309e..0ca66da 100644 --- a/Runtime/Engine/Code/Network/NetworkBufferReader.cs +++ b/Runtime/Engine/Code/Network/NetworkBufferReader.cs @@ -7,7 +7,7 @@ using UnityEngine.Pool; namespace RebootKit.Engine.Network { public struct NetworkBufferReader : IDisposable { class ReaderHandle { - public NativeArray Data; + public NativeSlice Data; public int Position; public bool IsBigEndian; } @@ -27,8 +27,7 @@ namespace RebootKit.Engine.Network { ReaderHandle m_Handle; - public NetworkBufferReader(NativeArray data, int position = 0) { - Assert.IsTrue(data.IsCreated, "Trying to create a NetworkBufferReader with uncreated data."); + public NetworkBufferReader(NativeSlice data, int position = 0) { Assert.IsTrue(position >= 0 && position <= data.Length, "Position must be within the bounds of the data array."); @@ -53,6 +52,11 @@ namespace RebootKit.Engine.Network { Assert.IsTrue(HasNext(size), $"Not enough data to read the requested size. Requested: {size}, Available: {m_Handle.Data.Length - m_Handle.Position}"); + if (size <= 0) { + value = default; + return false; + } + value = new NativeArray(size, allocator); for (int i = 0; i < size; i++) { value[i] = m_Handle.Data[m_Handle.Position++]; @@ -61,6 +65,17 @@ namespace RebootKit.Engine.Network { return true; } + public bool Read(out NativeSlice value, int size) { + if (!HasNext(size)) { + value = default; + return false; + } + + value = m_Handle.Data.Slice(m_Handle.Position, size); + m_Handle.Position += size; + return true; + } + public bool Read(out byte value) { if (!HasNext(1)) { value = 0; diff --git a/Runtime/Engine/Code/Network/NetworkBufferWriter.cs b/Runtime/Engine/Code/Network/NetworkBufferWriter.cs index 5b0de1c..0e25c5f 100644 --- a/Runtime/Engine/Code/Network/NetworkBufferWriter.cs +++ b/Runtime/Engine/Code/Network/NetworkBufferWriter.cs @@ -1,4 +1,5 @@ using System; +using System.Text; using Unity.Collections; using UnityEngine; using UnityEngine.Assertions; @@ -179,7 +180,7 @@ namespace RebootKit.Engine.Network { 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."); + Assert.IsTrue(WillFit(sizeof(ulong)), $"Buffer overflow: Cannot write beyond capacity. Current position: {m_Handle.Position}, Capacity: {m_Handle.Capacity}"); if (BitConverter.IsLittleEndian) { Write((byte) (value & 0xFF)); @@ -312,5 +313,43 @@ namespace RebootKit.Engine.Network { } } } + + // @NOTE: Writes ascii characters as bytes, and prepends the length as a ushort. + public static int GetStringWriteLength(string value) { + if (value.Length > ushort.MaxValue) { + return sizeof(ushort); + } + + if (string.IsNullOrEmpty(value)) { + return sizeof(ushort); + } + + // Length prefix + int length = sizeof(ushort); + + for (int i = 0; i < value.Length; i++) { + length += sizeof(char); + } + + return length; + } + + public void Write(string value) { + if (value.Length > ushort.MaxValue) { + Write((ushort) 0); + return; + } + + if (string.IsNullOrEmpty(value)) { + Write((ushort) 0); + return; + } + + Write((ushort)value.Length); + + for (int i = 0; i < value.Length; i++) { + Write((byte)value[i]); + } + } } } \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/NetworkMessageBuffer.cs b/Runtime/Engine/Code/Network/NetworkMessageBuffer.cs new file mode 100644 index 0000000..39cec6a --- /dev/null +++ b/Runtime/Engine/Code/Network/NetworkMessageBuffer.cs @@ -0,0 +1,109 @@ +using System; +using RebootKit.Engine; +using RebootKit.Engine.Network; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine.Assertions; + +namespace RebootKit.Engine.Network { + enum NetworkMessageType : byte { + None = 0x00, + // @MARK: Server to client messages + SpawnActor = 0x01, + PossessActor = 0x02, + ActorsEventsList = 0x03, + ActorsCoreStatesUpdateList = 0x04, + ActorsStatesUpdateList = 0x05, + ActorsTransformUpdateList = 0x06, + SynchronizeGameState = 0x07, + + // @MARK: Client to server messages + ClientSynchronizedGameState = 0x80, + ActorsCommandsList = 0x81, + } + + struct NetworkMessageHeader : ISerializableEntity { + public ushort MagicNumber; + public byte Version; + public NetworkMessageType MessageType; + + public const int k_HeaderSize = sizeof(ushort) + sizeof(byte) + sizeof(byte); + + public int GetMaxBytes() { + return k_HeaderSize; + } + + public void Serialize(NetworkBufferWriter writer) { + writer.Write(MagicNumber); + writer.Write(Version); + writer.Write((byte) MessageType); + } + + public void Deserialize(NetworkBufferReader reader) { + reader.Read(out MagicNumber); + reader.Read(out Version); + reader.Read(out byte type); + MessageType = (NetworkMessageType) type; + } + } + + class NetworkMessageBuffer : IDisposable { + NativeArray m_Data; + public NetworkBufferWriter Writer; + + public NetworkMessageBuffer(Allocator allocator) { + m_Data = new NativeArray(RConsts.k_NetworkMessageMaxSize, allocator); + Writer = new NetworkBufferWriter(m_Data, 0); + } + + public void Dispose() { + Writer.Dispose(); + m_Data.Dispose(); + } + + public void Reset() { + Writer.Position = 0; + + if (m_Data.IsCreated) { + unsafe { + UnsafeUtility.MemClear(m_Data.GetUnsafePtr(), m_Data.Length); + } + } + } + + public NativeSlice GetDataSlice() { + return m_Data.Slice(0, Writer.Position); + } + + public void WriteHeader(NetworkMessageType type, byte version) { + NetworkMessageBufferUtility.WriteHeader(Writer, type, version); + } + + public NativeArray GetDataCopy(Allocator allocator) { + NativeSlice dataSlice = GetDataSlice(); + NativeArray dataCopy = new NativeArray(dataSlice.Length, allocator); + dataSlice.CopyTo(dataCopy); + return dataCopy; + } + + public int GetMessageContentSize() { + Assert.IsTrue(Writer.Position >= NetworkMessageHeader.k_HeaderSize, + "Writer position must be greater than or equal to header size."); + return Writer.Position - NetworkMessageHeader.k_HeaderSize; + } + } +} + +static class NetworkMessageBufferUtility { + public static void WriteHeader(NetworkBufferWriter writer, + NetworkMessageType type, + byte version) { + NetworkMessageHeader header = new NetworkMessageHeader { + MagicNumber = RConsts.k_NetworkMessageMagic, + Version = version, + MessageType = type, + }; + + header.Serialize(writer); + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/NetworkMessageBuffer.cs.meta b/Runtime/Engine/Code/Network/NetworkMessageBuffer.cs.meta new file mode 100644 index 0000000..05298c2 --- /dev/null +++ b/Runtime/Engine/Code/Network/NetworkMessageBuffer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e9d2fa991d31446db7fe1a4fef441a1d +timeCreated: 1753152667 \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/NetworkPacketQueue.cs b/Runtime/Engine/Code/Network/NetworkPacketQueue.cs deleted file mode 100644 index 68784a3..0000000 --- a/Runtime/Engine/Code/Network/NetworkPacketQueue.cs +++ /dev/null @@ -1,303 +0,0 @@ -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 int DataSize; - - public int GetMaxBytes() { - return sizeof(ulong) + sizeof(byte) + sizeof(int); - } - - public void Serialize(NetworkBufferWriter writer) { - writer.Write((byte) Type); - writer.Write(ActorID); - writer.Write(DataSize); - } - - public void Deserialize(NetworkBufferReader reader) { - reader.Read(out byte typeByte); - Type = (NetworkDataType) typeByte; - reader.Read(out ActorID); - reader.Read(out DataSize); - } - } - - 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 = 1) { - 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(); - } - - public void WriteActorEvent(ActorEvent actorEvent) { - NetworkDataHeader header = new NetworkDataHeader { - Type = NetworkDataType.ActorEvent, - ActorID = actorEvent.ActorID, - DataSize = actorEvent.GetMaxBytes() - }; - - NetworkPacket packet = GetPacketToWriteTo(header.GetMaxBytes() + header.DataSize); - header.Serialize(packet.Writer); - actorEvent.Serialize(packet.Writer); - - packet.IncrementEntityCount(); - } - - public void WriteActorCommand(ActorCommand actorCommand) { - NetworkDataHeader header = new NetworkDataHeader { - Type = NetworkDataType.ActorCommand, - ActorID = actorCommand.ActorID, - DataSize = actorCommand.GetMaxBytes() - }; - - NetworkPacket packet = GetPacketToWriteTo(header.GetMaxBytes() + header.DataSize); - header.Serialize(packet.Writer); - actorCommand.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 deleted file mode 100644 index 0b8c2e4..0000000 --- a/Runtime/Engine/Code/Network/NetworkPacketQueue.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 8a122573c79c4b3e9ef3bc2da3b09faa -timeCreated: 1752855419 \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/NetworkPlayerController.cs b/Runtime/Engine/Code/Network/NetworkPlayerController.cs index cb69a11..b762720 100644 --- a/Runtime/Engine/Code/Network/NetworkPlayerController.cs +++ b/Runtime/Engine/Code/Network/NetworkPlayerController.cs @@ -1,42 +1,39 @@ -using System; -using System.Threading; +using System.Threading; using Cysharp.Threading.Tasks; -using RebootKit.Engine.Foundation; using RebootKit.Engine.Main; using RebootKit.Engine.Simulation; -using Unity.Netcode; +using UnityEngine; +using Logger = RebootKit.Engine.Foundation.Logger; namespace RebootKit.Engine.Network { - public abstract class NetworkPlayerController : NetworkBehaviour { + public abstract class NetworkPlayerController : MonoBehaviour { static readonly Logger s_Logger = new Logger(nameof(NetworkPlayerController)); - ulong m_ActorIDToPossess; + ushort m_ActorIDToPossess; + CancellationTokenSource m_PossessionCancellationTokenSource = new CancellationTokenSource(); + public Actor PossessedActor { get; private set; } - public void PossessActor(Actor actor) { - if (!IsServer) { - s_Logger.Error("PossessActor can only be called on the server."); - return; - } + internal void SetPossessedActor(ushort actorID) { + s_Logger.Info("Setting possessed actor to " + actorID); + + if (actorID == 0) { + m_ActorIDToPossess = 0; - if (actor == null) { - s_Logger.Error("Cannot possess a null actor."); - return; - } + if (PossessedActor != null) { + PossessedActor = null; + OnUnpossessActor(PossessedActor); + } + } else { + m_PossessionCancellationTokenSource.Cancel(); + m_PossessionCancellationTokenSource.Dispose(); + m_PossessionCancellationTokenSource = new CancellationTokenSource(); - PossessActorRpc(actor.ActorID, RpcTarget.Everyone); + WaitForActorToSpawnThenPossessAsync(actorID, m_PossessionCancellationTokenSource.Token).Forget(); + } } - [Rpc(SendTo.SpecifiedInParams)] - void PossessActorRpc(ulong actorID, RpcParams rpcParams) { - if (PossessedActor is not null) { - OnUnpossessActor(PossessedActor); - } - - WaitForActorToSpawnThenPossessAsync(actorID, destroyCancellationToken).Forget(); - } - - async UniTask WaitForActorToSpawnThenPossessAsync(ulong actorID, CancellationToken cancellationToken) { + async UniTask WaitForActorToSpawnThenPossessAsync(ushort actorID, CancellationToken cancellationToken) { Actor actor = null; while (actor == null) { actor = RR.FindSpawnedActor(actorID); @@ -51,27 +48,6 @@ namespace RebootKit.Engine.Network { OnPossessActor(actor); } - public void UnPossessActor() { - if (!IsServer) { - s_Logger.Error("UnPossessActor can only be called on the server."); - return; - } - - if (PossessedActor == null) { - return; - } - - UnPossessActorRpc(RpcTarget.Everyone); - } - - [Rpc(SendTo.SpecifiedInParams)] - void UnPossessActorRpc(RpcParams rpcParams) { - if (PossessedActor is not null) { - OnUnpossessActor(PossessedActor); - PossessedActor = null; - } - } - protected virtual void OnPossessActor(Actor actor) { } diff --git a/Runtime/Engine/Code/Network/NetworkSystem.cs b/Runtime/Engine/Code/Network/NetworkSystem.cs index 083de56..45e9c30 100644 --- a/Runtime/Engine/Code/Network/NetworkSystem.cs +++ b/Runtime/Engine/Code/Network/NetworkSystem.cs @@ -9,27 +9,30 @@ using RebootKit.Engine.Foundation; using RebootKit.Engine.Main; using RebootKit.Engine.Simulation; using Unity.Collections; -using Unity.Netcode; -using UnityEngine; +using Unity.Collections.LowLevel.Unsafe; using UnityEngine.Assertions; using Logger = RebootKit.Engine.Foundation.Logger; namespace RebootKit.Engine.Network { enum NetworkClientSyncState { NotReady, - LoadingWorld, - PreparingForActorsSync, - SyncingActors, + Synchronizing, Ready } - struct NetworkClientState : INetworkSerializable { + class NetworkClientState : IDisposable { + public NetworkSystem NetworkSystem; + public ulong ClientID; public NetworkClientSyncState SyncState; - public int ActorsSyncPacketsLeft; - public NetworkPacketQueue ReliableQueue; - public NetworkPacketQueue UnreliableQueue; + public NetworkMessageBuffer ActorsEvents; + public NetworkMessageBuffer ActorsCoreStatesUpdates; + public NetworkMessageBuffer ActorsStatesUpdates; + public NetworkMessageBuffer ActorsTransformUpdates; + + public Queue ReliableMessagesQueue = new Queue(64); + public Queue UnreliableMessagesQueue = new Queue(64); public bool IsReady { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -38,96 +41,183 @@ namespace RebootKit.Engine.Network { } } - public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { - serializer.SerializeValue(ref ClientID); - serializer.SerializeValue(ref SyncState); - serializer.SerializeValue(ref ActorsSyncPacketsLeft); + public bool IsServer { get; private set; } + + public NetworkClientState(NetworkSystem networkSystem, ulong clientID, bool isServer) { + NetworkSystem = networkSystem; + ClientID = clientID; + SyncState = NetworkClientSyncState.NotReady; + IsServer = isServer; + + ResetMessageBuffers(); + } + + public void Dispose() { + while (ReliableMessagesQueue.Count > 0) { + NetworkMessageBuffer message = ReliableMessagesQueue.Dequeue(); + NetworkSystem.ReturnMessageBufferToFreeList(message); + } + + while (UnreliableMessagesQueue.Count > 0) { + NetworkMessageBuffer message = UnreliableMessagesQueue.Dequeue(); + NetworkSystem.ReturnMessageBufferToFreeList(message); + } + + if (ActorsEvents != null) { + NetworkSystem.ReturnMessageBufferToFreeList(ActorsEvents); + } + + if (ActorsCoreStatesUpdates != null) { + NetworkSystem.ReturnMessageBufferToFreeList(ActorsCoreStatesUpdates); + } + + if (ActorsStatesUpdates != null) { + NetworkSystem.ReturnMessageBufferToFreeList(ActorsStatesUpdates); + } + + if (ActorsTransformUpdates != null) { + NetworkSystem.ReturnMessageBufferToFreeList(ActorsTransformUpdates); + } + + ActorsEvents = null; + ActorsCoreStatesUpdates = null; + ActorsStatesUpdates = null; + ActorsTransformUpdates = null; + } + + public void ResetMessageBuffers() { + if (ActorsEvents == null) { + ActorsEvents = NetworkSystem.GetFreeMessageBuffer(); + } + ActorsEvents.Reset(); + ActorsEvents.WriteHeader(NetworkMessageType.ActorsEventsList, RR.NetworkProtocolVersion); + ActorsEvents.Writer.Write((byte) 0); + + if (ActorsCoreStatesUpdates == null) { + ActorsCoreStatesUpdates = NetworkSystem.GetFreeMessageBuffer(); + } + ActorsCoreStatesUpdates.Reset(); + ActorsCoreStatesUpdates.WriteHeader(NetworkMessageType.ActorsCoreStatesUpdateList, + RR.NetworkProtocolVersion); + ActorsCoreStatesUpdates.Writer.Write((byte) 0); + + if (ActorsStatesUpdates == null) { + ActorsStatesUpdates = NetworkSystem.GetFreeMessageBuffer(); + } + ActorsStatesUpdates.Reset(); + ActorsStatesUpdates.WriteHeader(NetworkMessageType.ActorsStatesUpdateList, RR.NetworkProtocolVersion); + ActorsStatesUpdates.Writer.Write((byte) 0); + + if (ActorsTransformUpdates == null) { + ActorsTransformUpdates = NetworkSystem.GetFreeMessageBuffer(); + } + ActorsTransformUpdates.Reset(); + ActorsTransformUpdates.WriteHeader(NetworkMessageType.ActorsTransformUpdateList, RR.NetworkProtocolVersion); + ActorsTransformUpdates.Writer.Write((byte) 0); + } + + public void ScheduleMessages() { + if (ActorsEvents.GetMessageContentSize() > sizeof(byte)) { + ReliableMessagesQueue.Enqueue(ActorsEvents); + ActorsEvents = null; + } + + if (ActorsCoreStatesUpdates.GetMessageContentSize() > sizeof(byte)) { + ReliableMessagesQueue.Enqueue(ActorsCoreStatesUpdates); + ActorsCoreStatesUpdates = null; + } + + if (ActorsStatesUpdates.GetMessageContentSize() > sizeof(byte)) { + ReliableMessagesQueue.Enqueue(ActorsStatesUpdates); + ActorsStatesUpdates = null; + } + + if (ActorsTransformUpdates.GetMessageContentSize() > sizeof(byte)) { + UnreliableMessagesQueue.Enqueue(ActorsTransformUpdates); + ActorsTransformUpdates = null; + } + + ResetMessageBuffers(); } } - struct NetworkPacketTarget { - public enum Type { - AllClients, - Single, - Server - } - - 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 static NetworkPacketTarget Server() { - return new NetworkPacketTarget { - TargetType = Type.Server, - ClientID = 0 - }; - } - } - - public class NetworkSystem : NetworkBehaviour { - [ConfigVar("sv.tick_rate", 32, "Server tick rate in Hz", CVarFlags.Server)] + public class NetworkSystem : INetworkManagerDelegate, IDisposable { + [ConfigVar("sv.tick_rate", 30, "Server tick rate in Hz", CVarFlags.Server)] public static ConfigVar TickRate; static readonly Logger s_Logger = new Logger(nameof(NetworkSystem)); - const int k_ReliablePacketQueueSize = 1024; - const int k_UnreliablePacketQueueSize = 512; - - [field: SerializeField] public ActorsManager Actors { get; private set; } + public ActorsManager Actors { get; private set; } internal readonly Dictionary Clients = new Dictionary(); - public FixedString512Bytes WorldID { get; private set; } = new FixedString512Bytes(""); + NetworkMessageBuffer m_ActorsCommandsMessage; + + List m_MessagesFreeList = new List(); + + FixedString64Bytes m_WorldID = new FixedString64Bytes(""); + public FixedString64Bytes WorldID { + get { + return m_WorldID; + } + } + bool m_IsChangingWorld = false; + CancellationTokenSource m_WorldChangeCancellationTokenSource = new CancellationTokenSource(); + float m_TickTimer; + public INetworkManager Manager { get; private set; } + public ulong TickCount { get; private set; } public event Action ServerTick = delegate { }; public ulong LocalClientID { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - return NetworkManager.Singleton.LocalClientId; + return Manager.LocalClientID; } } public int LastTickPacketsSentCount { get; private set; } - NetworkPacketQueue m_ReliablePacketQueue; - NetworkPacketQueue m_UnreliablePacketQueue; + public NetworkSystem(INetworkManager manager) { + Manager = manager; + Manager.Delegate = this; + Actors = new ActorsManager(this); - NetworkPacketQueue m_ReliablePacketQueueToServer; - NetworkPacketQueue m_UnreliablePacketQueueToServer; - - // - // @MARK: Unity callbacks - // - void Awake() { - RR.NetworkSystemInstance = this; - - m_ReliablePacketQueue = new NetworkPacketQueue(k_ReliablePacketQueueSize); - m_UnreliablePacketQueue = new NetworkPacketQueue(k_UnreliablePacketQueueSize); - - m_ReliablePacketQueueToServer = new NetworkPacketQueue(k_ReliablePacketQueueSize); - m_UnreliablePacketQueueToServer = new NetworkPacketQueue(k_UnreliablePacketQueueSize); + m_ActorsCommandsMessage = new NetworkMessageBuffer(Allocator.Persistent); + m_ActorsCommandsMessage.WriteHeader(NetworkMessageType.ActorsCommandsList, RR.EngineConfig.protocolVersion); + m_ActorsCommandsMessage.Writer.Write((byte) 0); } - void Update() { - float deltaTime = Time.deltaTime; + public void Dispose() { + foreach (NetworkClientState clientState in Clients.Values) { + clientState.Dispose(); + } + Clients.Clear(); + + Manager.Dispose(); + Manager = null; + + m_ActorsCommandsMessage.Dispose(); + m_ActorsCommandsMessage = null; + + foreach (NetworkMessageBuffer networkMessageBuffer in m_MessagesFreeList) { + networkMessageBuffer.Dispose(); + } + m_MessagesFreeList.Clear(); + } + + public void Tick(float deltaTime) { + Manager.Tick(); + + if (!Manager.IsServer() && !Manager.IsClient()) { + return; + } + + Actors.Tick(deltaTime); float tickDeltaTime = 1.0f / TickRate.IndexValue; m_TickTimer += deltaTime; @@ -147,79 +237,77 @@ namespace RebootKit.Engine.Network { } if (RR.IsClient()) { + if (!RR.IsServer()) { + TickCount++; + } + FlushClientPackets(); } } } // - // @MARK: NetworkBehaviour callbacks + // @MARK: INetworkManagerDelegate // - public override void OnNetworkSpawn() { - base.OnNetworkSpawn(); + public void OnServerStarted() { + TickCount = 0; - NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected; - NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnect; + RR.OnServerStarted(); } - public override void OnNetworkDespawn() { - base.OnNetworkDespawn(); - - NetworkManager.Singleton.OnClientConnectedCallback -= OnClientConnected; - NetworkManager.Singleton.OnClientDisconnectCallback -= OnClientDisconnect; + public void OnServerStopped() { + RR.OnServerStopped(); } - void OnClientConnected(ulong clientID) { - if (!IsServer) { - return; - } + public void OnClientStarted() { + RR.OnClientStarted(); + } + public void OnClientStopped() { + RR.OnClientStopped(); + } + + public void OnClientConnected(ulong clientID) { s_Logger.Info($"OnClientConnected: {clientID}"); - NetworkClientState newClientState = new NetworkClientState { - ClientID = clientID, - SyncState = NetworkClientSyncState.NotReady, - ReliableQueue = new NetworkPacketQueue(k_ReliablePacketQueueSize), - UnreliableQueue = new NetworkPacketQueue(k_UnreliablePacketQueueSize) - }; - Clients.Add(clientID, newClientState); + if (RR.IsServer()) { + NetworkClientState newClientState = new NetworkClientState(this, + clientID, + clientID == Manager.LocalClientID); + 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 && !newClientState.IsServer) { + s_Logger.Info($"Synchronizing world load for client {clientID} with world ID '{WorldID}'"); + SendSynchronizeGameState(clientID); } } - if (!WorldID.IsEmpty) { - s_Logger.Info($"Synchronizing world load for client {clientID} with world ID '{WorldID}'"); - ClientLoadWorldRpc(WorldID.ToString(), RpcTarget.Single(clientID, RpcTargetUse.Temp)); + if (RR.GameInstance != null) { + RR.GameInstance.OnClientConnected(clientID); } } - void OnClientDisconnect(ulong clientID) { - s_Logger.Info($"OnClientDisconnect: {clientID}"); - Clients.Remove(clientID); + public void OnClientDisconnected(ulong clientID) { + s_Logger.Info($"OnClientDisconnected: {clientID}"); + + if (RR.IsServer()) { + Clients.Remove(clientID); + } + + if (RR.GameInstance != null) { + RR.GameInstance.OnClientDisconnected(clientID); + } + } + + public void OnMessageReceived(ulong senderID, NativeArray data) { + OnReceivedNetworkMessage(senderID, data); } // // @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) { + if (!Manager.IsServer()) { s_Logger.Error("Only server can set the current world."); return; } @@ -235,15 +323,17 @@ namespace RebootKit.Engine.Network { return; } - WorldID = worldID; + m_WorldID = worldID; foreach ((ulong _, NetworkClientState clientState) in Clients.ToList()) { - NetworkClientState state = clientState; - state.SyncState = NetworkClientSyncState.LoadingWorld; - UpdateClientState(state); + clientState.SyncState = NetworkClientSyncState.Synchronizing; } - ServerLoadWorldAsync(worldConfigAsset, destroyCancellationToken).Forget(); + m_WorldChangeCancellationTokenSource.Cancel(); + m_WorldChangeCancellationTokenSource.Dispose(); + m_WorldChangeCancellationTokenSource = new CancellationTokenSource(); + + ServerLoadWorldAsync(worldConfigAsset, m_WorldChangeCancellationTokenSource.Token).Forget(); } async UniTask ServerLoadWorldAsync(WorldConfigAsset asset, CancellationToken cancellationToken) { @@ -255,67 +345,45 @@ namespace RebootKit.Engine.Network { 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}."); + await UniTask.Yield(cancellationToken); + Actors.AssignActorsIDs(); + + if (!TryGetClientState(LocalClientID, out NetworkClientState localClientState)) { + s_Logger.Error($"Local client state not found for client ID {LocalClientID.ToString()}."); RR.Disconnect(); return; } localClientState.SyncState = NetworkClientSyncState.Ready; - UpdateClientState(localClientState); - RR.GameInstance.PlayerBecameReady(localClientState.ClientID); - ClientLoadWorldRpc(asset.name, RpcTarget.NotMe); + foreach (ulong clientID in Clients.Keys) { + if (clientID == LocalClientID) { + continue; + } + + SendSynchronizeGameState(clientID); + } } - [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(); + void SendSynchronizeGameState(ulong clientID) { + if (!RR.IsServer()) { + s_Logger.Error("Only server can send synchronize game state."); return; } - ClientLoadWorldAsync(worldID, destroyCancellationToken).Forget(); - } + using NetworkMessageBuffer message = new NetworkMessageBuffer(Allocator.Temp); + message.WriteHeader(NetworkMessageType.SynchronizeGameState, RR.EngineConfig.protocolVersion); + message.Writer.Write(WorldID); + Actors.WriteInSceneActorsStates(message.Writer); - 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!"); + unsafe { + Manager.Send(clientID, + (byte*) message.GetDataSlice().GetUnsafePtr(), + message.Writer.Position, + SendMode.Reliable); } } @@ -326,26 +394,12 @@ namespace RebootKit.Engine.Network { 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) { + internal void ClientSynchronized(ulong clientID) { if (TryGetClientState(clientID, out NetworkClientState state)) { state.SyncState = NetworkClientSyncState.Ready; - UpdateClientState(state); + s_Logger.Info($"Client synchronized: {clientID.ToString()}"); + Actors.SpawnDynamicActorsForClient(clientID); RR.GameInstance.PlayerBecameReady(clientID); } else { s_Logger.Error($"Client state for {clientID} not found."); @@ -365,102 +419,249 @@ namespace RebootKit.Engine.Network { // // @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.Server) { - return reliable ? m_ReliablePacketQueueToServer : m_UnreliablePacketQueueToServer; + internal void SendPossessedActor(ulong clientID, ushort actorID) { + if (!RR.IsServer()) { + s_Logger.Error("Only server can send possessed actor message."); + return; } - if (target.TargetType == NetworkPacketTarget.Type.Single) { - if (TryGetClientState(target.ClientID, out NetworkClientState clientState)) { - return reliable ? clientState.ReliableQueue : clientState.UnreliableQueue; - } + if (TryGetClientState(clientID, out NetworkClientState clientState)) { + using NetworkMessageBuffer messageBuffer = new NetworkMessageBuffer(Allocator.Temp); + messageBuffer.WriteHeader(NetworkMessageType.PossessActor, RR.EngineConfig.protocolVersion); + messageBuffer.Writer.Write(actorID); - s_Logger.Error($"Client state for {target.ClientID} not found."); - return null; + LastTickPacketsSentCount += 1; + Manager.Send(clientID, messageBuffer.GetDataSlice(), SendMode.Reliable); + return; } - s_Logger.Error($"Invalid network packet target type: {target.TargetType}"); - return null; + s_Logger.Error($"Failed to find client state for client ID {clientID}. Cannot send possess actor message."); } - internal void WriteActorState(NetworkPacketTarget target, ulong actorID, IActorData actorData) { + internal bool WriteActorState(ulong clientID, ushort actorID, IActorData actorData) { if (!RR.IsServer()) { s_Logger.Error("WriteActorState can only be called on the server."); - return; + return false; + } + Assert.IsTrue(actorData.GetMaxBytes() <= byte.MaxValue, "Actor data must not exceed 255 bytes."); + + if (TryGetClientState(clientID, out NetworkClientState clientState)) { + NetworkMessageBuffer messageBuffer = clientState.ActorsStatesUpdates; + + int dataSizeToFit = actorData.GetMaxBytes() + sizeof(ushort) + sizeof(byte); + if (!messageBuffer.Writer.WillFit(dataSizeToFit)) { + return false; + } + + // Increment count of entities in the message + int savedPosition = messageBuffer.Writer.Position; + using NetworkBufferReader reader = new NetworkBufferReader(messageBuffer.GetDataSlice(), + NetworkMessageHeader.k_HeaderSize); + reader.Read(out byte count); + if (count >= byte.MaxValue) { + return false; + } + messageBuffer.Writer.Position = NetworkMessageHeader.k_HeaderSize; + messageBuffer.Writer.Write((byte) (count + 1)); + messageBuffer.Writer.Position = savedPosition; + + // Actual data writing + messageBuffer.Writer.Write(actorID); + messageBuffer.Writer.Write((byte) actorData.GetMaxBytes()); + actorData.Serialize(messageBuffer.Writer); + return true; } - NetworkPacketQueue queue = GetPacketQueue(target, true); - queue.WriteActorState(actorID, actorData); + return false; + } + + // @NOTE: We are assuming that message buffer is already initialized with header and count of entities. + static bool CanFitNextEntityInMessage(NetworkMessageBuffer messageBuffer, int dataSizeToFit) { + if (!messageBuffer.Writer.WillFit(dataSizeToFit)) { + return false; + } + + // Increment count of entities in the message + using NetworkBufferReader reader = new NetworkBufferReader(messageBuffer.GetDataSlice(), + NetworkMessageHeader.k_HeaderSize); + reader.Read(out byte count); + + if (count >= byte.MaxValue) { + return false; + } + + return true; } - internal void WriteActorTransformState(NetworkPacketTarget target, - ulong actorID, - ActorTransformSyncData transformData) { + internal bool WriteActorTransformState(ulong clientID, ActorTransformSyncData transformData) { if (!RR.IsServer()) { s_Logger.Error("WriteActorTransformState can only be called on the server."); - return; + return false; } - NetworkPacketQueue queue = GetPacketQueue(target, false); - queue.WriteActorTransformState(actorID, transformData); + if (TryGetClientState(clientID, out NetworkClientState clientState)) { + int dataSizeToFit = transformData.GetMaxBytes(); + NetworkMessageBuffer messageBuffer = clientState.ActorsTransformUpdates; + + if (!CanFitNextEntityInMessage(messageBuffer, dataSizeToFit)) { + return false; + } + + // Increment count of entities in the message + int savedPosition = messageBuffer.Writer.Position; + using NetworkBufferReader reader = new NetworkBufferReader(messageBuffer.GetDataSlice(), + NetworkMessageHeader.k_HeaderSize); + reader.Read(out byte count); + if (count >= byte.MaxValue) { + return false; + } + messageBuffer.Writer.Position = NetworkMessageHeader.k_HeaderSize; + messageBuffer.Writer.Write((byte) (count + 1)); + messageBuffer.Writer.Position = savedPosition; + + // Actual data writing + transformData.Serialize(messageBuffer.Writer); + return true; + } + + return false; } - internal void WriteActorCoreState(NetworkPacketTarget target, - ulong actorID, - ActorCoreStateSnapshot coreData) { + internal bool WriteActorCoreState(ulong clientID, ActorCoreStateSnapshot coreData) { if (!RR.IsServer()) { s_Logger.Error("WriteActorCoreState can only be called on the server."); - return; + return false; } - NetworkPacketQueue queue = GetPacketQueue(target, true); - queue.WriteActorCoreState(actorID, coreData); + if (TryGetClientState(clientID, out NetworkClientState clientState)) { + int dataSizeToFit = coreData.GetMaxBytes(); + NetworkMessageBuffer messageBuffer = clientState.ActorsCoreStatesUpdates; + + if (!messageBuffer.Writer.WillFit(dataSizeToFit)) { + return false; + } + + // Increment count of entities in the message + int savedPosition = messageBuffer.Writer.Position; + using NetworkBufferReader reader = new NetworkBufferReader(messageBuffer.GetDataSlice(), + NetworkMessageHeader.k_HeaderSize); + reader.Read(out byte count); + if (count >= byte.MaxValue) { + return false; + } + messageBuffer.Writer.Position = NetworkMessageHeader.k_HeaderSize; + messageBuffer.Writer.Write((byte) (count + 1)); + messageBuffer.Writer.Position = savedPosition; + + // Actual data writing + coreData.Serialize(messageBuffer.Writer); + return true; + } + + return false; } - internal void WriteSpawnActor(NetworkPacketTarget target, - string assetGUID, - ulong actorID, - ActorCoreStateSnapshot coreState, - IActorData actorData) { + internal bool SendSpawnActor(ulong clientID, + FixedString64Bytes assetGUID, + ActorCoreStateSnapshot coreState, + IActorData actorData) { if (!RR.IsServer()) { s_Logger.Error("WriteSpawnActor can only be called on the server."); - return; - } - NetworkPacketQueue queue = GetPacketQueue(target, true); - queue.WriteSpawnActor(assetGUID, actorID, coreState, actorData); - } - - internal void WriteActorSynchronize(NetworkPacketTarget target, - ulong actorID, - ActorCoreStateSnapshot coreState, - IActorData actorData) { - if (!RR.IsServer()) { - s_Logger.Error("WriteActorSynchronize can only be called on the server."); - return; + return false; } - NetworkPacketQueue queue = GetPacketQueue(target, true); - queue.WriteActorSynchronize(actorID, coreState, actorData); + if (TryGetClientState(clientID, out NetworkClientState clientState)) { + int dataSizeToFit = sizeof(byte) * 64 + // assetGUID + coreState.GetMaxBytes() + + sizeof(byte) + // actor data size + actorData.GetMaxBytes(); + using NetworkMessageBuffer messageBuffer = new NetworkMessageBuffer(Allocator.Temp); + messageBuffer.WriteHeader(NetworkMessageType.SpawnActor, RR.NetworkProtocolVersion); + + if (!messageBuffer.Writer.WillFit(dataSizeToFit)) { + s_Logger.Error($"WriteSpawnActor failed: message buffer for client {clientID} is full. " + + $"Data size to fit: {dataSizeToFit}"); + return false; + } + + messageBuffer.Writer.Write(assetGUID); + coreState.Serialize(messageBuffer.Writer); + messageBuffer.Writer.Write((byte) actorData.GetMaxBytes()); + actorData.Serialize(messageBuffer.Writer); + + LastTickPacketsSentCount += 1; + Manager.Send(clientID, messageBuffer.GetDataSlice(), SendMode.Reliable); + + return true; + } + + return false; } - internal void WriteActorEvent(NetworkPacketTarget target, + internal bool WriteActorEvent(ulong clientID, ActorEvent actorEvent) { if (!RR.IsServer()) { s_Logger.Error("WriteActorEvent can only be called on the server."); - return; + return false; } - NetworkPacketQueue queue = GetPacketQueue(target, true); - queue.WriteActorEvent(actorEvent); + if (TryGetClientState(clientID, out NetworkClientState clientState)) { + int dataSizeToFit = actorEvent.GetMaxBytes(); + NetworkMessageBuffer messageBuffer = clientState.ActorsEvents; + + if (!messageBuffer.Writer.WillFit(dataSizeToFit)) { + return false; + } + + // Increment count of entities in the message + int savedPosition = messageBuffer.Writer.Position; + using NetworkBufferReader reader = new NetworkBufferReader(messageBuffer.GetDataSlice(), + NetworkMessageHeader.k_HeaderSize); + reader.Read(out byte count); + if (count >= byte.MaxValue) { + return false; + } + + messageBuffer.Writer.Position = NetworkMessageHeader.k_HeaderSize; + messageBuffer.Writer.Write((byte) (count + 1)); + messageBuffer.Writer.Position = savedPosition; + + // Actual data writing + actorEvent.Serialize(messageBuffer.Writer); + return true; + } + + return false; } - internal void WriteActorCommand(ActorCommand actorCommand) { - NetworkPacketQueue queue = GetPacketQueue(NetworkPacketTarget.Server(), true); - queue.WriteActorCommand(actorCommand); + // @TODO: When buffer is overloaded we might want to flush it immediately instead of returning false. + internal bool WriteActorCommand(ActorCommand actorCommand) { + if (actorCommand.ActorID == 0) { + s_Logger.Error("Trying to write an actor command with ActorID 0. " + + "This is likely a bug in the code that generates actor commands."); + } + + NetworkMessageBuffer messageBuffer = m_ActorsCommandsMessage; + if (!messageBuffer.Writer.WillFit(actorCommand.GetMaxBytes())) { + return false; + } + + // Increment count of entities in the message + using NetworkBufferReader reader = new NetworkBufferReader(messageBuffer.GetDataSlice(), + NetworkMessageHeader.k_HeaderSize); + reader.Read(out byte count); + if (count >= byte.MaxValue) { + return false; + } + + int savedPosition = messageBuffer.Writer.Position; + messageBuffer.Writer.Position = NetworkMessageHeader.k_HeaderSize; + messageBuffer.Writer.Write((byte) (count + 1)); + messageBuffer.Writer.Position = savedPosition; + + // Actual data writing + actorCommand.Serialize(messageBuffer.Writer); + return true; } void FlushClientPackets() { @@ -468,162 +669,265 @@ namespace RebootKit.Engine.Network { return; } - if (RR.IsServer()) { - foreach (NetworkPacket networkPacket in m_ReliablePacketQueueToServer.NetworkPackets) { - if (networkPacket.EntityCount > 0) { - OnReceivedNetworkPacket(networkPacket.Data); - } - } - - foreach (NetworkPacket networkPacket in m_UnreliablePacketQueueToServer.NetworkPackets) { - if (networkPacket.EntityCount > 0) { - OnReceivedNetworkPacket(networkPacket.Data); - } - } - } else { - foreach (NetworkPacket networkPacket in m_ReliablePacketQueueToServer.NetworkPackets) { - if (networkPacket.EntityCount == 0) { - continue; - } - + if (m_ActorsCommandsMessage.GetMessageContentSize() > sizeof(byte)) { + if (RR.IsServer()) { + using NativeArray messageData = m_ActorsCommandsMessage.GetDataCopy(Allocator.Temp); + ResetClientsMessages(); + OnReceivedNetworkMessage(Manager.LocalClientID, messageData); + } else { LastTickPacketsSentCount += 1; - ReliableReceiveNetworkPacketRpc(networkPacket.Data, RpcTarget.Server); - } - - foreach (NetworkPacket networkPacket in m_UnreliablePacketQueueToServer.NetworkPackets) { - if (networkPacket.EntityCount == 0) { - continue; - } - - LastTickPacketsSentCount += 1; - UnreliableReceiveNetworkPacketRpc(networkPacket.Data, RpcTarget.Server); + Manager.Send(0, m_ActorsCommandsMessage.GetDataSlice(), SendMode.Reliable); + ResetClientsMessages(); } } + } - m_ReliablePacketQueueToServer.Clear(); - m_UnreliablePacketQueueToServer.Clear(); + void ResetClientsMessages() { + m_ActorsCommandsMessage.Reset(); + m_ActorsCommandsMessage.WriteHeader(NetworkMessageType.ActorsCommandsList, RR.EngineConfig.protocolVersion); + m_ActorsCommandsMessage.Writer.Write((byte) 0); } void FlushServerPackets() { 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; + + foreach (NetworkClientState client in RR.Network.Clients.Values) { + bool isClientServer = client.ClientID == Manager.LocalClientID; + + client.ScheduleMessages(); + + if (isClientServer) { + while (client.ReliableMessagesQueue.Count > 0) { + NetworkMessageBuffer message = client.ReliableMessagesQueue.Dequeue(); + OnMessageReceived(LocalClientID, message.GetDataCopy(Allocator.Temp)); } - if (networkPacket.EntityCount == 0) { - continue; + while (client.UnreliableMessagesQueue.Count > 0) { + NetworkMessageBuffer message = client.UnreliableMessagesQueue.Dequeue(); + OnMessageReceived(LocalClientID, message.GetDataCopy(Allocator.Temp)); } - - if (state.IsReady) { + } else { + while (client.ReliableMessagesQueue.Count > 0) { + NetworkMessageBuffer message = client.ReliableMessagesQueue.Dequeue(); LastTickPacketsSentCount += 1; - 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; + Manager.Send(client.ClientID, message.GetDataSlice(), SendMode.Reliable); } - if (networkPacket.EntityCount == 0) { - continue; - } - - if (state.IsReady) { + while (client.UnreliableMessagesQueue.Count > 0) { + NetworkMessageBuffer message = client.UnreliableMessagesQueue.Dequeue(); LastTickPacketsSentCount += 1; - UnreliableReceiveNetworkPacketRpc(networkPacket.Data, - RpcTarget.Single(clientID, RpcTargetUse.Temp)); + Manager.Send(client.ClientID, message.GetDataSlice(), SendMode.Unreliable); } } - } - m_ReliablePacketQueue.Clear(); - m_UnreliablePacketQueue.Clear(); - - foreach (NetworkClientState clientState in Clients.Values) { - if (clientState.ClientID == NetworkManager.Singleton.LocalClientId) { - continue; + foreach (NetworkMessageBuffer messageToFree in client.ReliableMessagesQueue) { + ReturnMessageBufferToFreeList(messageToFree); } + client.ReliableMessagesQueue.Clear(); - foreach (NetworkPacket networkPacket in clientState.ReliableQueue.NetworkPackets) { - if (networkPacket.EntityCount == 0) { - continue; - } - - LastTickPacketsSentCount += 1; - ReliableReceiveNetworkPacketRpc(networkPacket.Data, - RpcTarget.Single(clientState.ClientID, RpcTargetUse.Temp)); + foreach (NetworkMessageBuffer messageToFree in client.UnreliableMessagesQueue) { + ReturnMessageBufferToFreeList(messageToFree); } - - foreach (NetworkPacket networkPacket in clientState.UnreliableQueue.NetworkPackets) { - if (networkPacket.EntityCount == 0) { - continue; - } - - LastTickPacketsSentCount += 1; - UnreliableReceiveNetworkPacketRpc(networkPacket.Data, - RpcTarget.Single(clientState.ClientID, RpcTargetUse.Temp)); - } - - clientState.ReliableQueue.Clear(); - clientState.UnreliableQueue.Clear(); + client.UnreliableMessagesQueue.Clear(); } } - [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)] - void ReliableReceiveNetworkPacketRpc(NativeArray data, RpcParams rpcParams) { - OnReceivedNetworkPacket(data); - } + void OnReceivedNetworkMessage(ulong senderID, NativeArray data) { + if (!data.IsCreated) { + s_Logger.Error("Received empty network message data."); + return; + } - [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Unreliable)] - void UnreliableReceiveNetworkPacketRpc(NativeArray data, RpcParams rpcParams) { - OnReceivedNetworkPacket(data); - } + NetworkMessageHeader messageHeader = new NetworkMessageHeader(); - 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, + using (NetworkBufferReader reader = new NetworkBufferReader(data)) { + messageHeader.Deserialize(reader); + } + Assert.IsTrue(messageHeader.MagicNumber == RConsts.k_NetworkMessageMagic, "Received packet with invalid magic number."); - if (packetHeader.EntityCount == 0) { - s_Logger.Info("Received packet with no entities.\n" + data.ToHexString()); + NativeSlice messageContent = data.Slice(NetworkMessageHeader.k_HeaderSize); + + switch (messageHeader.MessageType) { + // @MARK: Server to client messages + case NetworkMessageType.SpawnActor: { + using NetworkBufferReader contentReader = new NetworkBufferReader(messageContent); + + contentReader.Read(out FixedString64Bytes actorAssetGUID); + + ActorCoreStateSnapshot coreState = new ActorCoreStateSnapshot(); + coreState.Deserialize(contentReader); + + contentReader.Read(out byte dataSize); + contentReader.Read(out NativeSlice actorDataContent, dataSize); + + Actors.ProcessSpawnActor(actorAssetGUID, coreState, actorDataContent); + break; } - for (int i = 0; i < packetHeader.EntityCount; i++) { - NetworkDataHeader dataHeader = new NetworkDataHeader(); - dataHeader.Deserialize(reader); + case NetworkMessageType.PossessActor: { + using NetworkBufferReader contentReader = new NetworkBufferReader(messageContent); + contentReader.Read(out ushort actorID); - // s_Logger.Info($"Received entity: Type={dataHeader.Type}, ActorID={dataHeader.ActorID}, DataSize={dataHeader.DataSize}"); + RR.GameInstance.LocalPlayerController.SetPossessedActor(actorID); + break; + } - if (dataHeader.Type == NetworkDataType.None) { - s_Logger.Info("Data of packet with entry with type None:\n" + data.ToHexString()); + case NetworkMessageType.ActorsEventsList: { + using NetworkBufferReader contentReader = new NetworkBufferReader(messageContent); + contentReader.Read(out byte count); + + for (int i = 0; i < count; ++i) { + ActorEvent actorEvent = new ActorEvent(); + actorEvent.Deserialize(contentReader); + + Actors.ProcessActorEvent(actorEvent); } + break; + } - Assert.IsTrue(dataHeader.Type != NetworkDataType.None, "Received packet with invalid data type."); + case NetworkMessageType.ActorsCoreStatesUpdateList: { + using NetworkBufferReader contentReader = new NetworkBufferReader(messageContent); + contentReader.Read(out byte count); - reader.Read(out NativeArray entityData, dataHeader.DataSize, Allocator.Temp); - OnReceivedEntity(dataHeader, entityData); + for (int i = 0; i < count; ++i) { + ActorCoreStateSnapshot coreState = new ActorCoreStateSnapshot(); + coreState.Deserialize(contentReader); - entityData.Dispose(); + Actors.ProcessActorCoreState(coreState); + } + break; + } + + case NetworkMessageType.ActorsStatesUpdateList: { + using NetworkBufferReader contentReader = new NetworkBufferReader(messageContent); + contentReader.Read(out byte count); + + for (int i = 0; i < count; ++i) { + contentReader.Read(out ushort actorID); + contentReader.Read(out byte dataSize); + contentReader.Read(out NativeSlice actorDataContent, dataSize); + + Actors.ProcessActorState(actorID, actorDataContent); + } + break; + } + + case NetworkMessageType.ActorsTransformUpdateList: { + using NetworkBufferReader contentReader = new NetworkBufferReader(messageContent); + contentReader.Read(out byte count); + + for (int i = 0; i < count; ++i) { + ActorTransformSyncData transformData = new ActorTransformSyncData(); + transformData.Deserialize(contentReader); + + Actors.ProcessActorTransformState(transformData); + } + break; + } + + case NetworkMessageType.SynchronizeGameState: { + ProcessSynchronizeGameStateMessage(messageContent); + break; + } + + // @MARK: Client to server messages + case NetworkMessageType.ClientSynchronizedGameState: { + s_Logger.Info("On Player Synchronized Game State: " + senderID); + ClientSynchronized(senderID); + break; + } + + case NetworkMessageType.ActorsCommandsList: { + using NetworkBufferReader contentReader = new NetworkBufferReader(messageContent); + contentReader.Read(out byte count); + + for (int i = 0; i < count; ++i) { + ActorCommand actorCommand = new ActorCommand(); + actorCommand.Deserialize(contentReader); + + Actors.ProcessActorCommand(senderID, actorCommand); + } + break; + } + + default: + s_Logger.Error($"Received packet with unsupported message type: {messageHeader.MessageType.ToString()}"); + break; } } - void OnReceivedEntity(NetworkDataHeader header, NativeArray data) { - Actors.OnReceivedEntity(header, data); + void ProcessSynchronizeGameStateMessage(NativeSlice data) { + if (RR.IsServer()) { + s_Logger.Error("Server should not receive SynchronizeGameState message."); + return; + } + + // @TODO: Handle cancellation token properly in this method. + ProcessSynchronizeGameStateMessageAsync(data, CancellationToken.None).Forget(); + } + + async UniTask ProcessSynchronizeGameStateMessageAsync(NativeSlice data, + CancellationToken cancellationToken) { + using NativeArray dataArray = new NativeArray(data.Length, Allocator.Persistent); + data.CopyTo(dataArray); + using NetworkBufferReader reader = new NetworkBufferReader(dataArray); + + reader.Read(out m_WorldID); + + WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(m_WorldID.ToString()); + if (worldConfigAsset is null) { + s_Logger.Error($"Failed to synchronize game state: World config asset for '{m_WorldID}' not found."); + RR.Disconnect(); + return; + } + + RR.World.Unload(); + RR.CloseMainMenu(); + + await RR.World.LoadAsync(worldConfigAsset.Config, cancellationToken); + if (!Actors.ReadInSceneActorsStates(reader)) { + s_Logger.Error("Failed to read in-scene actors states during game state synchronization."); + RR.Disconnect(); + return; + } + + NotifyServerClientSynchronizedGameState(); + } + + void NotifyServerClientSynchronizedGameState() { + s_Logger.Info("Sending ClientSynchronizedGameState message to server"); + + using NetworkMessageBuffer message = new NetworkMessageBuffer(Allocator.Temp); + message.WriteHeader(NetworkMessageType.ClientSynchronizedGameState, RR.EngineConfig.protocolVersion); + message.Writer.Write(m_WorldID); + + Manager.Send(0, message.GetDataSlice(), SendMode.Reliable); + } + + // + // @MARK: Messages free list + // + internal NetworkMessageBuffer GetFreeMessageBuffer() { + if (m_MessagesFreeList.Count > 0) { + int index = m_MessagesFreeList.Count - 1; + NetworkMessageBuffer messageBuffer = m_MessagesFreeList[index]; + m_MessagesFreeList.RemoveAt(index); + return messageBuffer; + } + + return new NetworkMessageBuffer(Allocator.Persistent); + } + + internal void ReturnMessageBufferToFreeList(NetworkMessageBuffer messageBuffer) { + if (m_MessagesFreeList.Contains(messageBuffer)) { + s_Logger.Error("Trying to return a message buffer that is already in the free list."); + return; + } + + m_MessagesFreeList.Add(messageBuffer); } } } \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/NetworkWorldController.cs b/Runtime/Engine/Code/Network/NetworkWorldController.cs deleted file mode 100644 index a5182f4..0000000 --- a/Runtime/Engine/Code/Network/NetworkWorldController.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Unity.Netcode; - -namespace RebootKit.Engine.Network { - public abstract class NetworkWorldController : NetworkBehaviour { - } -} \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/NetworkWorldController.cs.meta b/Runtime/Engine/Code/Network/NetworkWorldController.cs.meta deleted file mode 100644 index 6020e5b..0000000 --- a/Runtime/Engine/Code/Network/NetworkWorldController.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: edd521a69a6f4e43b97ec258adf499a6 -timeCreated: 1751377120 \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/UnityNetworkManager.cs b/Runtime/Engine/Code/Network/UnityNetworkManager.cs new file mode 100644 index 0000000..33e0aaa --- /dev/null +++ b/Runtime/Engine/Code/Network/UnityNetworkManager.cs @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; +using R3; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Networking.Transport; +using UnityEngine; +using Logger = RebootKit.Engine.Foundation.Logger; + +namespace RebootKit.Engine.Network { + // @NOTE: This won't probably be suited for production use. + public class UnityNetworkManager : INetworkManager { + static readonly Logger s_Logger = new Logger(nameof(UnityNetworkManager)); + + const ulong k_ServerClientID = 0; + + NetworkDriver m_Driver; + + // @MARK: Server specific stuff + bool m_IsServer; + readonly Dictionary m_ServerConnections = new Dictionary(); + ulong m_ClientIDCounter = 1; + + bool m_IsClient; + NetworkConnection m_ClientConnection; + + public INetworkManagerDelegate Delegate { get; set; } + + public ulong LocalClientID { get; private set; } + + public NetworkManagerStats Stats { get; } = new NetworkManagerStats(); + + public ushort Port = 7777; + + public UnityNetworkManager() { + m_Driver = NetworkDriver.Create(); + if (!m_Driver.IsCreated) { + s_Logger.Error("Failed to create network driver"); + return; + } + + m_IsServer = false; + m_IsClient = false; + + LocalClientID = 0; // This should be set to a unique ID for the local client + } + + public void Dispose() { + if (m_Driver.IsCreated) { + m_Driver.Dispose(); + } + + m_ServerConnections.Clear(); + m_ClientConnection = default; + } + + float m_StatsTimer = 0.0f; + + public void Tick() { + m_Driver.ScheduleUpdate().Complete(); + + if (IsServer()) { + ServerTick(); + } + + if (IsClient()) { + ClientTick(); + } + + if (IsServer() || IsClient()) { + m_StatsTimer -= Time.deltaTime; + if (m_StatsTimer <= 0.0f) { + m_StatsTimer = 1.0f; + + Stats.ReliableBytesSentPerSecond = Stats.ReliableBytesSent; + Stats.UnreliableBytesSentPerSecond = Stats.UnreliableBytesSent; + Stats.BytesReceivedPerSecond = Stats.BytesReceived; + + Stats.ReliableBytesSent = 0; + Stats.UnreliableBytesSent = 0; + Stats.BytesReceived = 0; + } + } + } + + void ServerTick() { + using NativeList clientIDsToRemove = new NativeList(Allocator.Temp); + foreach ((ulong clientID, NetworkConnection connection) in m_ServerConnections) { + if (!connection.IsCreated) { + clientIDsToRemove.Add(clientID); + } + } + + foreach (ulong clientID in clientIDsToRemove) { + m_ServerConnections.Remove(clientID); + Delegate?.OnClientDisconnected(clientID); + s_Logger.Info($"Client {clientID} disconnected"); + } + + NetworkConnection incomingConnection; + while ((incomingConnection = m_Driver.Accept()) != default) { + ulong newClientID = m_ClientIDCounter++; + m_ServerConnections.Add(newClientID, incomingConnection); + + s_Logger.Info("Connection accepted: " + newClientID); + Delegate?.OnClientConnected(newClientID); + } + + // @NOTE: Handle incoming messages from clients + clientIDsToRemove.Clear(); + foreach ((ulong clientID, NetworkConnection connection) in m_ServerConnections) { + DataStreamReader stream; + NetworkEvent.Type cmd; + while ((cmd = m_Driver.PopEventForConnection(connection, out stream)) != + NetworkEvent.Type.Empty) { + if (cmd == NetworkEvent.Type.Data) { + NativeArray data = new NativeArray(stream.Length, Allocator.Temp); + stream.ReadBytes(data); + + Stats.BytesReceived += (ulong) stream.Length; + Delegate?.OnMessageReceived(clientID, data); + } else if (cmd == NetworkEvent.Type.Disconnect) { + clientIDsToRemove.Add(clientID); + } + } + } + + // @TODO: Code duplication + foreach (ulong clientID in clientIDsToRemove) { + m_ServerConnections.Remove(clientID); + Delegate?.OnClientDisconnected(clientID); + s_Logger.Info($"Client {clientID} disconnected"); + } + } + + void ClientTick() { + DataStreamReader stream; + NetworkEvent.Type cmd; + while ((cmd = m_ClientConnection.PopEvent(m_Driver, out stream)) != NetworkEvent.Type.Empty) { + if (cmd == NetworkEvent.Type.Data) { + NativeArray data = new NativeArray(stream.Length, Allocator.Temp); + stream.ReadBytes(data); + Stats.BytesReceived += (ulong) stream.Length; + Delegate?.OnMessageReceived(k_ServerClientID, data); + } else if (cmd == NetworkEvent.Type.Disconnect) { + m_IsClient = false; + m_ClientConnection = default; + Delegate?.OnClientDisconnected(LocalClientID); + s_Logger.Info("Disconnected from server"); + } else if (cmd == NetworkEvent.Type.Connect) { + s_Logger.Info("Client connected"); + Delegate?.OnClientConnected(LocalClientID); + } + } + } + + // + // @MARK: INetworkManager + // + public bool IsServer() { + return m_IsServer; + } + + public bool StartHost() { + if (IsServer()) { + s_Logger.Error("Server already started"); + return false; + } + + Stats.Reset(); + + NetworkEndpoint endpoint = NetworkEndpoint.AnyIpv4.WithPort(7777); + + if (m_Driver.Bind(endpoint) != 0) { + s_Logger.Error($"Failed to bind to port {Port}"); + return false; + } + + m_Driver.Listen(); + + m_ClientConnection = m_Driver.Connect(NetworkEndpoint.LoopbackIpv4.WithPort(Port)); + + if (!m_ClientConnection.IsCreated) { + s_Logger.Error($"Failed to create client connection on port {Port}"); + return false; + } + + LocalClientID = k_ServerClientID; + m_ServerConnections.Add(LocalClientID, m_ClientConnection); + + m_IsServer = true; + m_IsClient = true; + + Delegate?.OnServerStarted(); + Delegate?.OnClientConnected(LocalClientID); + return true; + } + + public void StopHost() { throw new NotImplementedException(); } + + public bool IsClient() { + return m_IsClient; + } + + public bool StartClient() { + if (IsClient()) { + s_Logger.Error("Client already started"); + return false; + } + + Stats.Reset(); + + NetworkEndpoint endpoint = NetworkEndpoint.LoopbackIpv4.WithPort(Port); + m_ClientConnection = m_Driver.Connect(endpoint); + + if (!m_ClientConnection.IsCreated) { + s_Logger.Error($"Failed to connect to server on port {Port}"); + return false; + } + + m_IsClient = true; + Delegate?.OnClientStarted(); + return true; + } + + public void Disconnect() { throw new NotImplementedException(); } + + public void Send(ulong clientID, NativeSlice data, SendMode mode) { + unsafe { + void* ptr = data.GetUnsafePtr(); + Send(clientID, (byte*)ptr, data.Length, mode); + } + } + + public unsafe void Send(ulong clientID, byte* data, int length, SendMode mode) { + if (IsServer()) { + if (clientID == LocalClientID) { + NativeArray messageData = new NativeArray(length, Allocator.Temp); + UnsafeUtility.MemCpy(messageData.GetUnsafePtr(), data, length); + Delegate?.OnMessageReceived(clientID, messageData); + return; + } + + if (m_ServerConnections.TryGetValue(clientID, out NetworkConnection value)) { + if (!value.IsCreated) { + s_Logger.Error($"Client {clientID} connection is not created. Cannot send message."); + return; + } + + m_Driver.BeginSend(value, out DataStreamWriter writer); + writer.WriteBytesUnsafe(data, length); + m_Driver.EndSend(writer); + + if (mode == SendMode.Reliable) { + Stats.ReliableBytesSent += (ulong) length; + } else if (mode == SendMode.Unreliable){ + Stats.UnreliableBytesSent += (ulong) length; + } + } else { + s_Logger.Error($"Client {clientID} not found. Cannot send message."); + } + } else if (IsClient()) { + if (clientID != k_ServerClientID) { + s_Logger.Error("Client ID mismatch. Cannot send message to another client from client context."); + return; + } + + if (!m_ClientConnection.IsCreated) { + s_Logger.Error("Client connection is not created. Cannot send message."); + return; + } + + if (m_Driver.BeginSend(m_ClientConnection, out DataStreamWriter writer, length) == 0) { + writer.WriteBytesUnsafe(data, length); + + if (m_Driver.EndSend(writer) < 0) { + s_Logger.Error("Failed to send message to server."); + } + + if (mode == SendMode.Reliable) { + Stats.ReliableBytesSent += (ulong) length; + } else if (mode == SendMode.Unreliable){ + Stats.UnreliableBytesSent += (ulong) length; + } + } else { + s_Logger.Error($"Failed to begin sending message to server with length {length}."); + } + } + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Network/UnityNetworkManager.cs.meta b/Runtime/Engine/Code/Network/UnityNetworkManager.cs.meta new file mode 100644 index 0000000..0144b64 --- /dev/null +++ b/Runtime/Engine/Code/Network/UnityNetworkManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f2b23e503c2946ef9b2d631cef89facb +timeCreated: 1753326254 \ No newline at end of file diff --git a/Runtime/Engine/Code/RConsts.cs b/Runtime/Engine/Code/RConsts.cs index b9482e8..0d5f77e 100755 --- a/Runtime/Engine/Code/RConsts.cs +++ b/Runtime/Engine/Code/RConsts.cs @@ -21,6 +21,7 @@ internal const string k_BuildFlagDebug = "RR_DEBUG"; internal const string k_BuildFlagSteam = "RR_STEAM"; - internal const int k_NetworkPacketMagicNumber = 0x52455245; // "RERE" in ASCII + internal const ushort k_NetworkMessageMagic = 0xBEBE; + internal const int k_NetworkMessageMaxSize = 1024; } } \ No newline at end of file diff --git a/Runtime/Engine/Code/Simulation/Actor.cs b/Runtime/Engine/Code/Simulation/Actor.cs index e9bb64e..d7bc231 100644 --- a/Runtime/Engine/Code/Simulation/Actor.cs +++ b/Runtime/Engine/Code/Simulation/Actor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using RebootKit.Engine.Foundation; @@ -6,8 +7,9 @@ using RebootKit.Engine.Main; using RebootKit.Engine.Network; using TriInspector; using Unity.Collections; -using Unity.Netcode; +using Unity.Mathematics; using UnityEngine; +using UnityEngine.Assertions; using Logger = RebootKit.Engine.Foundation.Logger; namespace RebootKit.Engine.Simulation { @@ -24,59 +26,54 @@ namespace RebootKit.Engine.Simulation { } public struct ActorCommand : ISerializableEntity { - public ulong ActorID; - public ulong ClientID; - public ushort CommandID; + public ushort ActorID; + public byte CommandID; public NativeArray Data; - + public int GetMaxBytes() { - return sizeof(ulong) + // ActorID - sizeof(ulong) + // ClientID - sizeof(ushort) + // CommandID - sizeof(ushort) + // Data length - sizeof(byte) * Data.Length; // Data + return sizeof(ushort) + // ActorID + sizeof(byte) + // CommandID + sizeof(byte) + // Data length + sizeof(byte) * Data.Length; // Data } - + public void Serialize(NetworkBufferWriter writer) { writer.Write(ActorID); - writer.Write(ClientID); writer.Write(CommandID); - writer.Write((ushort) Data.Length); + writer.Write((byte) Data.Length); if (Data.IsCreated) { writer.Write(Data); } } - + public void Deserialize(NetworkBufferReader reader) { reader.Read(out ActorID); - reader.Read(out ClientID); reader.Read(out CommandID); - reader.Read(out ushort dataLength); + reader.Read(out byte dataLength); if (dataLength > 0) { reader.Read(out Data, dataLength, Allocator.Temp); } } } - // @NOTE: ActorEvent is used to send events from the server to clients and only clients. - // Server should not receive ActorEvents. public struct ActorEvent : ISerializableEntity { - public ulong ActorID; - public ushort EventID; + public ushort ActorID; + public byte EventID; public NativeArray Data; public int GetMaxBytes() { - return sizeof(ulong) + // ActorID - sizeof(ushort) + // EventID - sizeof(ushort) + // Data length + return sizeof(ushort) + // ActorID + sizeof(byte) + // EventID + sizeof(byte) + // Data length sizeof(byte) * Data.Length; // Data } public void Serialize(NetworkBufferWriter writer) { writer.Write(ActorID); writer.Write(EventID); + Assert.IsTrue(Data.Length < byte.MaxValue, "Data of ActorEvent is too large to fit in a byte."); - writer.Write((ushort) Data.Length); + writer.Write((byte) Data.Length); if (Data.IsCreated) { writer.Write(Data); } @@ -85,7 +82,7 @@ namespace RebootKit.Engine.Simulation { public void Deserialize(NetworkBufferReader reader) { reader.Read(out ActorID); reader.Read(out EventID); - reader.Read(out ushort dataLength); + reader.Read(out byte dataLength); if (dataLength > 0) { reader.Read(out Data, dataLength, Allocator.Temp); @@ -94,13 +91,14 @@ namespace RebootKit.Engine.Simulation { } [Flags] - enum ActorPhysicsFlags : byte { + enum ActorFlags : byte { None = 0, - IsKinematic = 1 << 0, + Hidden = 1 << 0, DisableColliders = 1 << 1, } struct ActorCoreStateSnapshot : ISerializableEntity { + public ushort ActorID; public DateTime Timestamp; // @NOTE: Position, Rotation, and Scale are in local space. @@ -108,102 +106,65 @@ namespace RebootKit.Engine.Simulation { public Quaternion Rotation; public Vector3 Scale; - public bool IsHidden; - public ActorPhysicsFlags Flags; + public ActorFlags Flags; - public ulong MasterActorID; + public ushort MasterActorID; public FixedString32Bytes MasterSocketName; public void Serialize(NetworkBufferWriter writer) { + writer.Write(ActorID); 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 ActorID); 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; + Flags = (ActorFlags) flagsByte; reader.Read(out MasterActorID); reader.Read(out MasterSocketName); } public int GetMaxBytes() { - return sizeof(long) + // Timestamp + return sizeof(ushort) + // ActorID + 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(ushort) + // MasterActorID sizeof(byte) * 32; // MasterSocketName } } - /// - /// Represents the synchronization mode for actor transforms (and rigidbody). - /// @TODO: Might be a good idea to keep client-side actors rigidbody as kinematic and simulate physics only on the server. - /// IMPORTANT: - /// - Position, Rotation, and Scale are in local space. - /// - 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, - // @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. + Scale = 1 << 2 } public struct ActorTransformSyncData : ISerializableEntity { + public ushort ActorID; public ActorTransformSyncMode SyncMode; public Vector3 Position; - public Quaternion Rotation; + public Vector3 Rotation; public Vector3 Scale; - public Vector3 Velocity; - public Vector3 AngularVelocity; - - public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { - serializer.SerializeValue(ref SyncMode); - - if ((SyncMode & ActorTransformSyncMode.Position) != 0) { - serializer.SerializeValue(ref Position); - } - - if ((SyncMode & ActorTransformSyncMode.Rotation) != 0) { - serializer.SerializeValue(ref Rotation); - } - - if ((SyncMode & ActorTransformSyncMode.Scale) != 0) { - serializer.SerializeValue(ref Scale); - } - - if ((SyncMode & ActorTransformSyncMode.Velocity) != 0) { - serializer.SerializeValue(ref Velocity); - } - - if ((SyncMode & ActorTransformSyncMode.AngularVelocity) != 0) { - serializer.SerializeValue(ref AngularVelocity); - } - } public void Serialize(NetworkBufferWriter writer) { + writer.Write(ActorID); writer.Write((byte) SyncMode); if ((SyncMode & ActorTransformSyncMode.Position) != 0) { @@ -211,23 +172,26 @@ namespace RebootKit.Engine.Simulation { } if ((SyncMode & ActorTransformSyncMode.Rotation) != 0) { - writer.Write(Rotation); + Rotation.x = Mathf.Repeat(Rotation.x, 360.0f); + Rotation.y = Mathf.Repeat(Rotation.y, 360.0f); + Rotation.z = Mathf.Repeat(Rotation.z, 360.0f); + + ushort rotX = QuantizationUtility.FloatToUShort(Rotation.x, 0.0f, 360.0f); + ushort rotY = QuantizationUtility.FloatToUShort(Rotation.y, 0.0f, 360.0f); + ushort rotZ = QuantizationUtility.FloatToUShort(Rotation.z, 0.0f, 360.0f); + + writer.Write(rotX); + writer.Write(rotY); + writer.Write(rotZ); } 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 ActorID); reader.Read(out byte syncModeByte); SyncMode = (ActorTransformSyncMode) syncModeByte; @@ -236,19 +200,17 @@ namespace RebootKit.Engine.Simulation { } if ((SyncMode & ActorTransformSyncMode.Rotation) != 0) { - reader.Read(out Rotation); + reader.Read(out ushort rotX); + reader.Read(out ushort rotY); + reader.Read(out ushort rotZ); + + Rotation.x = QuantizationUtility.UShortToFloat(rotX, 0.0f, 360.0f); + Rotation.y = QuantizationUtility.UShortToFloat(rotY, 0.0f, 360.0f); + Rotation.z = QuantizationUtility.UShortToFloat(rotZ, 0.0f, 360.0f); } 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); + reader.Read(out Vector3 scale); } } @@ -260,21 +222,13 @@ namespace RebootKit.Engine.Simulation { } if ((SyncMode & ActorTransformSyncMode.Rotation) != 0) { - size += sizeof(float) * 4; // Quaternion + size += sizeof(ushort) * 3; // Vector3 - Euler angles } 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; } } @@ -283,7 +237,8 @@ namespace RebootKit.Engine.Simulation { static readonly Logger s_ActorLogger = new Logger(nameof(Actor)); [field: SerializeField, TriInspector.ReadOnly] public string SourceActorPath { get; internal set; } = ""; - [field: SerializeField, Unity.Collections.ReadOnly] public ulong ActorID { get; internal set; } + [field: SerializeField, TriInspector.ReadOnly] public ulong ActorStaticID { get; internal set; } + [field: SerializeField, TriInspector.ReadOnly] public ushort ActorID { get; internal set; } [NonSerialized] internal IActorData Data; @@ -302,11 +257,22 @@ namespace RebootKit.Engine.Simulation { [SerializeField] bool m_SetKinematicOnMount = true; [SerializeField] bool m_DisableCollidersOnMount = true; - internal ActorPhysicsFlags PhysicsFlagsBeforeMount = ActorPhysicsFlags.None; - internal ActorPhysicsFlags PhysicsFlags = ActorPhysicsFlags.None; + internal ActorFlags Flags = ActorFlags.None; // @NOTE: Sync won't happen if actor is mounted to another actor. - [SerializeField] internal ActorTransformSyncMode transformSyncMode = ActorTransformSyncMode.None; + [SerializeField] internal bool syncTransform = true; + [SerializeField] internal bool syncPosition = true; + [SerializeField] internal bool syncRotation = true; + [SerializeField] internal bool syncScale = false; + + class ActorClientState { + public ulong LastSyncTick; + public Vector3 Position; + public Quaternion Rotation; + public Vector3 Scale; + } + + readonly Dictionary m_ClientsStates = new Dictionary(); [Serializable] public struct AttachmentSocket { @@ -334,8 +300,8 @@ namespace RebootKit.Engine.Simulation { // @MARK: Unity callbacks // void OnValidate() { - if (ActorID == 0) { - ActorID = UniqueID.NewULongFromGuid(); + if (ActorStaticID == 0) { + ActorStaticID = UniqueID.NewULongFromGuid(); } } @@ -347,7 +313,7 @@ namespace RebootKit.Engine.Simulation { // @MARK: Server side public virtual void OnServerTick(float deltaTime) { } - protected virtual void OnActorCommandServer(ActorCommand actorCommand) { } + protected virtual void OnActorCommandServer(ulong senderID, ActorCommand actorCommand) { } // Override this method to implement client-side logic public virtual void OnClientTick(float deltaTime) { } @@ -364,12 +330,18 @@ namespace RebootKit.Engine.Simulation { bool shouldBeActive = !hidden; if (gameObject.activeSelf == shouldBeActive) { - s_ActorLogger - .Warning($"Actor {name} (ID: {ActorID}) is already in the desired visibility state: {shouldBeActive.ToString()}"); + s_ActorLogger.Warning($"Actor {name} (ID: {ActorID}) is already in the desired visibility state: {shouldBeActive.ToString()}"); return; } gameObject.SetActive(shouldBeActive); + + if (hidden) { + Flags |= ActorFlags.Hidden; + } else { + Flags &= ~ActorFlags.Hidden; + } + IsCoreStateDirty = true; } @@ -383,16 +355,15 @@ namespace RebootKit.Engine.Simulation { MasterActor = actor; MasterSocketName = new FixedString32Bytes(slotName); - PhysicsFlagsBeforeMount = PhysicsFlags; - if (m_SetKinematicOnMount) { - PhysicsFlags |= ActorPhysicsFlags.IsKinematic; - } - if (m_DisableCollidersOnMount) { - PhysicsFlags |= ActorPhysicsFlags.DisableColliders; + Flags |= ActorFlags.DisableColliders; + UpdateLocalCollidersState(false); + } + + if (m_SetKinematicOnMount) { + actorRigidbody.isKinematic = true; } - UpdateLocalPhysicsState(PhysicsFlags); UpdateMountedTransform(); IsCoreStateDirty = true; } @@ -413,51 +384,15 @@ namespace RebootKit.Engine.Simulation { MasterSocketName = default; UpdateMountedTransform(); - PhysicsFlags = PhysicsFlagsBeforeMount; - UpdateLocalPhysicsState(PhysicsFlags); - - IsCoreStateDirty = true; - } - - public void SetCollidersEnabled(bool enableColliders) { - if (!RR.IsServer()) { - s_ActorLogger.Error($"Only the server can enable/disable colliders. Actor: {name} (ID: {ActorID})"); - return; + if (m_DisableCollidersOnMount) { + UpdateLocalCollidersState(true); + Flags &= ~ActorFlags.DisableColliders; + } + + if (m_SetKinematicOnMount) { + actorRigidbody.isKinematic = false; } - if (actorRigidbody is null) { - s_ActorLogger.Error($"Actor {name} (ID: {ActorID}) has no Rigidbody to set colliders on."); - return; - } - - if (enableColliders) { - PhysicsFlags &= ~ActorPhysicsFlags.DisableColliders; - } else { - PhysicsFlags |= ActorPhysicsFlags.DisableColliders; - } - - UpdateLocalCollidersState(enableColliders); - IsCoreStateDirty = true; - } - - public void SetKinematic(bool isKinematic) { - if (!RR.IsServer()) { - s_ActorLogger.Error($"Only the server can set kinematic state. Actor: {name} (ID: {ActorID})"); - return; - } - - if (actorRigidbody is null) { - s_ActorLogger.Error($"Actor {name} (ID: {ActorID}) has no Rigidbody to set kinematic state on."); - return; - } - - if (isKinematic) { - PhysicsFlags |= ActorPhysicsFlags.IsKinematic; - } else { - PhysicsFlags &= ~ActorPhysicsFlags.IsKinematic; - } - - actorRigidbody.isKinematic = isKinematic; IsCoreStateDirty = true; } @@ -468,14 +403,14 @@ namespace RebootKit.Engine.Simulation { return !gameObject.activeSelf; } - protected void SendActorCommand(ushort commandID, ref TCmdData commandData) + protected void SendActorCommand(byte commandID, ref TCmdData commandData) where TCmdData : struct, ISerializableEntity { NativeArray data = DataSerializationUtils.Serialize(commandData); SendActorCommand(commandID, data); } - protected void SendActorCommand(ushort commandID, NativeArray data = default) { + protected void SendActorCommand(byte commandID, NativeArray data = default) { if (Manager is null) { s_ActorLogger.Error($"Cannot send command because Manager is null for actor {name} (ID: {ActorID})"); return; @@ -483,7 +418,6 @@ namespace RebootKit.Engine.Simulation { ActorCommand command = new ActorCommand { ActorID = ActorID, - ClientID = NetworkManager.Singleton.LocalClientId, CommandID = commandID, Data = data }; @@ -491,19 +425,19 @@ namespace RebootKit.Engine.Simulation { Manager.SendActorCommand(command); } - protected void SendActorEvent(ushort eventID, ref TEventData eventData) + protected void SendActorEvent(byte eventID, ref TEventData eventData) where TEventData : struct, ISerializableEntity { NativeArray data = DataSerializationUtils.Serialize(eventData); SendActorEvent(eventID, data); } - protected void SendActorEvent(ushort eventID, NativeArray data = default) { + protected void SendActorEvent(byte eventID, NativeArray data = default) { if (!RR.IsServer()) { s_ActorLogger.Error($"Only the server can send actor events. Actor: {name} (ID: {ActorID})"); return; } - if (Manager is null) { + if (Manager == null) { s_ActorLogger.Error($"Cannot send event because Manager is null for actor {name} (ID: {ActorID})"); return; } @@ -540,17 +474,22 @@ namespace RebootKit.Engine.Simulation { // // @MARK: Internal // + internal void InitializeOnClient() { + if (actorRigidbody != null) { + actorRigidbody.isKinematic = true; + } + } + internal ActorCoreStateSnapshot GetCoreStateSnapshot() { ActorCoreStateSnapshot snapshot = new ActorCoreStateSnapshot(); + snapshot.ActorID = ActorID; snapshot.Timestamp = DateTime.UtcNow; snapshot.Position = transform.localPosition; snapshot.Rotation = transform.localRotation; snapshot.Scale = transform.localScale; + snapshot.Flags = Flags; - snapshot.IsHidden = !gameObject.activeSelf; - snapshot.Flags = PhysicsFlags; - - snapshot.MasterActorID = MasterActor != null ? MasterActor.ActorID : 0; + snapshot.MasterActorID = MasterActor != null ? MasterActor.ActorID : (ushort)0; if (snapshot.MasterActorID != 0) { snapshot.MasterSocketName = MasterSocketName; } else { @@ -567,7 +506,7 @@ namespace RebootKit.Engine.Simulation { return; } LastCoreStateSyncTime = snapshot.Timestamp; - PhysicsFlags = snapshot.Flags; + Flags = snapshot.Flags; if (snapshot.MasterActorID != 0) { MasterActor = RR.FindSpawnedActor(snapshot.MasterActorID); @@ -583,69 +522,78 @@ namespace RebootKit.Engine.Simulation { transform.localScale = snapshot.Scale; } - if (snapshot.IsHidden) { + if ((snapshot.Flags & ActorFlags.Hidden) != 0) { gameObject.SetActive(false); } else { gameObject.SetActive(true); } - - UpdateLocalPhysicsState(PhysicsFlags); + + bool enableColliders = (Flags & ActorFlags.DisableColliders) == 0; + UpdateLocalCollidersState(enableColliders); } - internal ActorTransformSyncData GetTransformSyncData() { - ActorTransformSyncData data = new ActorTransformSyncData { - SyncMode = transformSyncMode + ActorClientState GetActorClientState(ulong clientID) { + if (m_ClientsStates.TryGetValue(clientID, out ActorClientState clientState)) { + return clientState; + } + + clientState = new ActorClientState { + LastSyncTick = 0, + Position = transform.localPosition, + Rotation = transform.localRotation, + Scale = transform.localScale }; + m_ClientsStates[clientID] = clientState; - bool useRigidbody = (data.SyncMode & ActorTransformSyncMode.UsingRigidbody) != 0; + return clientState; + } + + internal ulong GetLastSyncTick(ulong clientID) { + ActorClientState actorClientState = GetActorClientState(clientID); + return actorClientState.LastSyncTick; + } - if (useRigidbody && actorRigidbody == null) { - s_ActorLogger - .Error($"Actor {name} (ID: {ActorID.ToString()}) has no Rigidbody to sync transform. Ignoring transform sync."); - data.SyncMode = ActorTransformSyncMode.None; + internal void UpdateClientState(ulong clientID, ulong serverTick) { + ActorClientState actorClientState = GetActorClientState(clientID); + actorClientState.LastSyncTick = serverTick; + actorClientState.Position = transform.localPosition; + actorClientState.Rotation = transform.localRotation; + actorClientState.Scale = transform.localScale; + } + + internal ActorTransformSyncData GetTransformSyncDataForClient(ulong clientID) { + ActorTransformSyncData data = new ActorTransformSyncData { + ActorID = ActorID, + SyncMode = ActorTransformSyncMode.None + }; + + if (!syncTransform || MasterActor != null) { return data; } + + data.Position = transform.localPosition; + data.Rotation = transform.localRotation.eulerAngles; + data.Scale = transform.localScale; - if ((data.SyncMode & ActorTransformSyncMode.Position) != 0) { - if (useRigidbody) { - data.Position = actorRigidbody.position; - } else { - data.Position = transform.localPosition; - } + ActorClientState actorClientState = GetActorClientState(clientID); + + if (syncPosition && math.distancesq(actorClientState.Position, transform.localPosition) > 0.01f) { + data.SyncMode |= ActorTransformSyncMode.Position; + } + + if (syncRotation && Quaternion.Angle(actorClientState.Rotation, transform.localRotation) > 0.01f) { + data.SyncMode |= ActorTransformSyncMode.Rotation; } - if ((data.SyncMode & ActorTransformSyncMode.Rotation) != 0) { - if (useRigidbody) { - data.Rotation = actorRigidbody.rotation; - } else { - data.Rotation = transform.localRotation; - } - } - - if ((data.SyncMode & ActorTransformSyncMode.Scale) != 0) { - data.Scale = transform.localScale; - } - - if (useRigidbody && actorRigidbody != null) { - if ((data.SyncMode & ActorTransformSyncMode.Velocity) != 0) { - data.Velocity = actorRigidbody.linearVelocity; - } - - if ((data.SyncMode & ActorTransformSyncMode.AngularVelocity) != 0) { - data.AngularVelocity = actorRigidbody.angularVelocity; - } + if (syncScale && math.distancesq(actorClientState.Scale, transform.localScale) > 0.01f) { + data.SyncMode |= ActorTransformSyncMode.Scale; } return data; } internal void RestoreTransformState(ActorTransformSyncData data) { - bool useRigidbody = (data.SyncMode & ActorTransformSyncMode.UsingRigidbody) != 0; - if (useRigidbody && actorRigidbody == null) { - s_ActorLogger - .Error($"Actor {name} (ID: {ActorID.ToString()}) has no Rigidbody to restore transform state. Ignoring transform sync."); - return; - } + bool useRigidbody = actorRigidbody != null; if ((data.SyncMode & ActorTransformSyncMode.Position) != 0) { if (useRigidbody) { @@ -657,38 +605,15 @@ namespace RebootKit.Engine.Simulation { if ((data.SyncMode & ActorTransformSyncMode.Rotation) != 0) { if (useRigidbody) { - actorRigidbody.rotation = data.Rotation; + actorRigidbody.rotation = Quaternion.Euler(data.Rotation); } else { - transform.localRotation = data.Rotation; + transform.localRotation = Quaternion.Euler(data.Rotation); } } if ((data.SyncMode & ActorTransformSyncMode.Scale) != 0) { transform.localScale = data.Scale; } - - if (useRigidbody && (data.SyncMode & ActorTransformSyncMode.Velocity) != 0) { - actorRigidbody.linearVelocity = data.Velocity; - } - - if (useRigidbody && (data.SyncMode & ActorTransformSyncMode.AngularVelocity) != 0) { - actorRigidbody.angularVelocity = data.AngularVelocity; - } - } - - void UpdateLocalPhysicsState(ActorPhysicsFlags flags) { - if (actorRigidbody == null) { - return; - } - - if ((flags & ActorPhysicsFlags.IsKinematic) != 0) { - actorRigidbody.isKinematic = true; - } else { - actorRigidbody.isKinematic = false; - } - - bool enableColliders = (flags & ActorPhysicsFlags.DisableColliders) == 0; - UpdateLocalCollidersState(enableColliders); } void UpdateLocalCollidersState(bool enable) { @@ -720,7 +645,7 @@ namespace RebootKit.Engine.Simulation { return CreateActorData(); } - internal void HandleActorCommand(ActorCommand actorCommand) { + internal void HandleActorCommand(ulong senderID, ActorCommand actorCommand) { if (!RR.IsServer()) { s_ActorLogger.Error($"Only the server can handle actor commands. Actor: {name} (ID: {ActorID})"); return; @@ -737,7 +662,7 @@ namespace RebootKit.Engine.Simulation { return; } - OnActorCommandServer(actorCommand); + OnActorCommandServer(senderID, actorCommand); } internal void HandleActorEvent(ActorEvent actorEvent) { diff --git a/Runtime/Engine/Code/Simulation/ActorsManager.cs b/Runtime/Engine/Code/Simulation/ActorsManager.cs index 38cae06..29decc0 100644 --- a/Runtime/Engine/Code/Simulation/ActorsManager.cs +++ b/Runtime/Engine/Code/Simulation/ActorsManager.cs @@ -1,52 +1,51 @@ using System; using System.Collections.Generic; -using System.Runtime.InteropServices; -using NUnit.Framework; +using System.Linq; using RebootKit.Engine.Extensions; using RebootKit.Engine.Foundation; using RebootKit.Engine.Main; using RebootKit.Engine.Network; using Unity.Collections; -using Unity.Netcode; using UnityEngine; using UnityEngine.AddressableAssets; +using UnityEngine.Assertions; using Logger = RebootKit.Engine.Foundation.Logger; +using Object = UnityEngine.Object; namespace RebootKit.Engine.Simulation { // @TODO: // - Actors States might be packed into chunks to reduce the number of RPCs sent. // - Release addressables when they are no longer needed. - public class ActorsManager : NetworkBehaviour { + public class ActorsManager : IDisposable { static readonly Logger s_Logger = new Logger(nameof(ActorsManager)); + readonly NetworkSystem m_Network; + readonly List m_InSceneActors = new List(); readonly List m_SpawnedActors = new List(); - public int InSceneActorsCount { get { return m_InSceneActors.Count; } } - public int SpawnedActorsCount { get { return m_SpawnedActors.Count; } } - public int TotalActorsCount { get { return m_InSceneActors.Count + m_SpawnedActors.Count; } } + ushort m_ActorIDCounter = 0; - // - // @MARK: NetworkBehaviour callbacks - // - public override void OnNetworkSpawn() { - base.OnNetworkSpawn(); + public ushort InSceneActorsCount { get { return (ushort) m_InSceneActors.Count; } } + public ushort SpawnedActorsCount { get { return (ushort) m_SpawnedActors.Count; } } + public int TotalActorsCount { get { return InSceneActorsCount + SpawnedActorsCount; } } + + public ActorsManager(NetworkSystem networkSystem) { + m_Network = networkSystem; } - public override void OnNetworkDespawn() { - base.OnNetworkDespawn(); - } + public void Dispose() { } // - // @MARK: Unity callbacks + // @MARK: Update // - void Update() { + public void Tick(float deltaTime) { foreach (Actor actor in m_InSceneActors) { - actor.OnClientTick(Time.deltaTime); + actor.OnClientTick(deltaTime); } foreach (Actor actor in m_SpawnedActors) { - actor.OnClientTick(Time.deltaTime); + actor.OnClientTick(deltaTime); } } @@ -54,7 +53,7 @@ namespace RebootKit.Engine.Simulation { // @MARK: Server-side logic // public void ServerTick(float dt) { - if (!IsServer) { + if (!RR.IsServer()) { return; } @@ -70,23 +69,50 @@ namespace RebootKit.Engine.Simulation { actor.IsDataDirty = false; if (actor.Data.GetMaxBytes() > 0) { - RR.NetworkSystemInstance.WriteActorState(NetworkPacketTarget.AllClients(), actor.ActorID, actor.Data); + foreach (NetworkClientState client in m_Network.Clients.Values) { + if (client.IsServer) { + continue; + } + + m_Network.WriteActorState(client.ClientID, actor.ActorID, actor.Data); + } } } if (actor.IsCoreStateDirty) { actor.IsCoreStateDirty = false; - RR.NetworkSystemInstance.WriteActorCoreState(NetworkPacketTarget.AllClients(), - actor.ActorID, - actor.GetCoreStateSnapshot()); + foreach (NetworkClientState client in m_Network.Clients.Values) { + if (client.IsServer) { + continue; + } + + m_Network.WriteActorCoreState(client.ClientID, actor.GetCoreStateSnapshot()); + } + } + } + + foreach (NetworkClientState client in m_Network.Clients.Values) { + if (client.IsServer) { + continue; } - if (actor.transformSyncMode != ActorTransformSyncMode.None && actor.MasterActor == null) { - ActorTransformSyncData syncData = actor.GetTransformSyncData(); - RR.NetworkSystemInstance.WriteActorTransformState(NetworkPacketTarget.AllClients(), - actor.ActorID, - syncData); + foreach (Actor actor in actors.OrderBy(t => t.GetLastSyncTick(client.ClientID))) { + if (!actor.syncTransform || actor.MasterActor != null) { + continue; + } + + ActorTransformSyncData syncData = actor.GetTransformSyncDataForClient(client.ClientID); + if (syncData.SyncMode == ActorTransformSyncMode.None) { + continue; + } + + if (m_Network.WriteActorTransformState(client.ClientID, syncData)) { + actor.UpdateClientState(client.ClientID, RR.Network.TickCount); + } else { + // @NOTE: We ran out of space in the packet + break; + } } } } @@ -95,7 +121,7 @@ namespace RebootKit.Engine.Simulation { // @MARK: Server API // public Actor SpawnActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) { - if (!IsServer) { + if (!RR.IsServer()) { s_Logger.Error("Only the server can spawn actors."); return null; } @@ -105,13 +131,16 @@ namespace RebootKit.Engine.Simulation { return null; } - ulong actorID = UniqueID.NewULongFromGuid(); + if (!TryGenerateNextActorID(out ushort actorID)) { + s_Logger.Error("Cannot spawn actor: Failed to generate next actor ID."); + return null; + } GameObject actorObject = assetReference.InstantiateAsync(position, rotation).WaitForCompletion(); Actor actor = actorObject.GetComponent(); if (actor is null) { s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component."); - Destroy(actorObject); + Object.Destroy(actorObject); return null; } @@ -119,36 +148,44 @@ namespace RebootKit.Engine.Simulation { actor.SourceActorPath = assetReference.AssetGUID; actor.ActorID = actorID; actor.Data = actor.InternalCreateActorData(); - m_SpawnedActors.Add(actor); - RR.NetworkSystemInstance.WriteSpawnActor(NetworkPacketTarget.AllClients(), - assetReference.AssetGUID, - actor.ActorID, - actor.GetCoreStateSnapshot(), - actor.Data); + foreach (NetworkClientState client in m_Network.Clients.Values) { + if (client.IsServer) { + continue; + } + + m_Network.SendSpawnActor(client.ClientID, + assetReference.AssetGUID, + actor.GetCoreStateSnapshot(), + actor.Data); + } + return actor; } - public void CleanUp() { - if (IsServer) { - CleanUpRpc(); - } - - m_InSceneActors.Clear(); - - foreach (Actor actor in m_SpawnedActors) { - if (actor.OrNull() != null) { - Destroy(actor.gameObject); - } - } - - m_SpawnedActors.Clear(); + bool TryGenerateNextActorID(out ushort actorID) { + m_ActorIDCounter += 1; + actorID = m_ActorIDCounter; + return true; } - [Rpc(SendTo.NotMe)] - void CleanUpRpc() { - CleanUp(); + internal void AssignActorsIDs() { + if (!RR.IsServer()) { + s_Logger.Info("Only the server can assign actors IDs."); + return; + } + + m_ActorIDCounter = 0; + + foreach (Actor actor in m_InSceneActors) { + if (!TryGenerateNextActorID(out ushort actorID)) { + s_Logger.Error("Failed to generate actor ID. Probably reached the limit of 65535 actors."); + return; + } + + actor.ActorID = actorID; + } } // @@ -164,7 +201,17 @@ namespace RebootKit.Engine.Simulation { m_InSceneActors.Add(actor); } - public Actor FindActorByID(ulong actorID) { + Actor FindInSceneActorWithStaticID(ulong staticID) { + foreach (Actor actor in m_InSceneActors) { + if (actor.ActorStaticID == staticID) { + return actor; + } + } + + return null; + } + + public Actor FindActorByID(ushort actorID) { foreach (Actor actor in m_InSceneActors) { if (actor.ActorID == actorID) { return actor; @@ -180,186 +227,112 @@ namespace RebootKit.Engine.Simulation { return null; } - // - // @MARK: Initial synchronization - // - internal void InitializeActorsForClient(ulong clientID) { - if (RR.NetworkSystemInstance.TryGetClientState(clientID, out NetworkClientState clientState)) { - clientState.SyncState = NetworkClientSyncState.PreparingForActorsSync; - RR.NetworkSystemInstance.UpdateClientState(clientState); - PrepareClientForActorsSyncRpc(RpcTarget.Single(clientState.ClientID, RpcTargetUse.Temp)); - } else { - s_Logger.Error($"Client state for {clientID} not found. Cannot synchronize actors."); - } - } + public void CleanUp() { + m_InSceneActors.Clear(); - [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)] - void PrepareClientForActorsSyncRpc(RpcParams rpcParams) { - foreach (Actor spawnedActor in m_SpawnedActors) { - Destroy(spawnedActor.gameObject); + foreach (Actor actor in m_SpawnedActors) { + if (actor.OrNull() != null) { + Object.Destroy(actor.gameObject); + } } m_SpawnedActors.Clear(); - - ClientIsReadyForActorsSyncRpc(); - } - - [Rpc(SendTo.Server)] - void ClientIsReadyForActorsSyncRpc(RpcParams rpcParams = default) { - ulong clientID = rpcParams.Receive.SenderClientId; - if (!RR.NetworkSystemInstance.TryGetClientState(clientID, out NetworkClientState clientState)) { - s_Logger.Error($"Client state for {clientID} not found. Cannot mark client as ready for actors sync."); - return; - } - - clientState.SyncState = NetworkClientSyncState.SyncingActors; - clientState.ActorsSyncPacketsLeft = m_InSceneActors.Count; - RR.NetworkSystemInstance.UpdateClientState(clientState); - - 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) { - RR.NetworkSystemInstance.WriteActorSynchronize(NetworkPacketTarget.Single(clientID), - actor.ActorID, - actor.GetCoreStateSnapshot(), - actor.Data); - } - - foreach (Actor actor in m_SpawnedActors) { - 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.Server, Delivery = RpcDelivery.Reliable)] - void ClientSynchronizedActorRpc(RpcParams rpcParams = default) { - ulong clientID = rpcParams.Receive.SenderClientId; - - if (!RR.NetworkSystemInstance.TryGetClientState(clientID, out NetworkClientState clientState)) { - s_Logger.Error($"Client state for {clientID} not found. Cannot mark client as synchronized."); - return; - } - - clientState.ActorsSyncPacketsLeft--; - s_Logger.Info($"Synchronized actor for client {clientID}. Packets left: {clientState.ActorsSyncPacketsLeft}"); - RR.NetworkSystemInstance.UpdateClientState(clientState); - - if (clientState.ActorsSyncPacketsLeft == 0) { - RR.NetworkSystemInstance.ClientSynchronizedActors(clientID); - } } /// /// @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; - } - - using NetworkBufferReader reader = new NetworkBufferReader(data); - ActorEvent actorEvent = new ActorEvent(); - actorEvent.Deserialize(reader); - - actor.HandleActorEvent(actorEvent); - } else if (header.Type == NetworkDataType.ActorCommand) { - if (!RR.IsServer()) { - s_Logger.Error($"Received ActorCommand on client, but this should only be handled on the server."); - return; - } - - Actor actor = FindActorByID(header.ActorID); - if (actor == null) { - s_Logger.Error($"Failed to find actor with ID {header.ActorID} for command handling."); - return; - } - - using NetworkBufferReader reader = new NetworkBufferReader(data); - ActorCommand actorCommand = new ActorCommand(); - actorCommand.Deserialize(reader); - - actor.HandleActorCommand(actorCommand); - } 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); + /// + internal void ProcessActorEvent(in ActorEvent actorEvent) { + Actor actor = FindActorByID(actorEvent.ActorID); + if (actor == null) { + s_Logger.Error($"Failed to find actor with ID {actorEvent.ActorID} for event handling."); + return; } + + actor.HandleActorEvent(actorEvent); + } + + internal void ProcessActorCommand(ulong senderID, in ActorCommand actorCommand) { + if (!RR.IsServer()) { + s_Logger.Error("Received ActorCommand on client, but this should only be handled on the server."); + return; + } + + Actor actor = FindActorByID(actorCommand.ActorID); + if (actor == null) { + s_Logger.Error($"Failed to find actor with ID {actorCommand.ActorID} for command handling."); + return; + } + + actor.HandleActorCommand(senderID, actorCommand); + } + + internal void ProcessActorCoreState(in ActorCoreStateSnapshot coreState) { + if (RR.IsServer()) { + s_Logger.Error("Received ActorCoreState on server, but this should only be handled on the client."); + return; + } + + Actor actor = FindActorByID(coreState.ActorID); + if (actor == null) { + s_Logger.Error($"Failed to find actor with ID {coreState.ActorID} for core state update."); + return; + } + + actor.RestoreCoreState(coreState); + } + + internal void ProcessActorState(ushort actorID, NativeSlice stateData) { + if (RR.IsServer()) { + s_Logger.Error("Received ActorState on server, but this should only be handled on the client."); + return; + } + + Actor actor = FindActorByID(actorID); + if (actor == null) { + s_Logger.Error($"Failed to find actor with ID {actorID} for state update."); + return; + } + + DataSerializationUtils.Deserialize(stateData, ref actor.Data); + } + + internal void ProcessActorTransformState(in ActorTransformSyncData transformData) { + if (RR.IsServer()) { + s_Logger.Error("Received ActorTransformSync on server, but this should only be handled on the client."); + return; + } + + Actor actor = FindActorByID(transformData.ActorID); + if (actor == null) { + s_Logger.Error($"Failed to find actor with ID {transformData.ActorID} for transform state update."); + return; + } + + actor.RestoreTransformState(transformData); + } + + internal void ProcessSpawnActor(in FixedString64Bytes assetGUID, + in ActorCoreStateSnapshot coreState, + in NativeSlice stateData) { + if (RR.IsServer()) { + s_Logger.Error("Received SpawnActor on server, but this should only be handled on the client."); + return; + } + + if (assetGUID.IsEmpty) { + s_Logger.Error("Received SpawnActor with empty asset GUID, this should not happen."); + return; + } + + SpawnLocalActor(assetGUID.ToString(), + coreState, + stateData); } void SpawnLocalActor(string guid, - ulong actorID, ActorCoreStateSnapshot coreStateSnapshot, - NativeArray stateData) { + NativeSlice stateData) { AssetReferenceGameObject assetReference = new AssetReferenceGameObject(guid); if (!assetReference.RuntimeKeyIsValid()) { s_Logger.Error($"Invalid asset reference for actor with GUID {guid}"); @@ -377,17 +350,24 @@ namespace RebootKit.Engine.Simulation { Actor actor = actorObject.GetComponent(); if (actor is null) { s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component."); - Destroy(actorObject); + Object.Destroy(actorObject); return; } actor.Manager = this; actor.SourceActorPath = guid; - actor.ActorID = actorID; + actor.ActorID = coreStateSnapshot.ActorID; actor.Data = actor.InternalCreateActorData(); + if (!RR.IsServer()) { + actor.InitializeOnClient(); + } + actor.RestoreCoreState(coreStateSnapshot); - DataSerializationUtils.Deserialize(stateData, ref actor.Data); + if (stateData.Length > 0) { + DataSerializationUtils.Deserialize(stateData, ref actor.Data); + } + m_SpawnedActors.Add(actor); } @@ -399,12 +379,91 @@ namespace RebootKit.Engine.Simulation { s_Logger.Error("Only the server can send actor events."); return; } - - RR.NetworkSystemInstance.WriteActorEvent(NetworkPacketTarget.AllClients(), actorEvent); + + foreach (ulong clientID in m_Network.Clients.Keys) { + m_Network.WriteActorEvent(clientID, actorEvent); + } } internal void SendActorCommand(ActorCommand actorCommand) { - RR.NetworkSystemInstance.WriteActorCommand(actorCommand); + m_Network.WriteActorCommand(actorCommand); + } + + /// + /// @MARK: Synchronization Helpers + /// + + // @TODO: We might want to sending this in chunks because it probably wont fit into a single packet in real game. + internal void WriteInSceneActorsStates(NetworkBufferWriter writer) { + writer.Write((ushort) InSceneActorsCount); + + foreach (Actor actor in m_InSceneActors) { + writer.Write(actor.ActorStaticID); + writer.Write(actor.ActorID); + actor.GetCoreStateSnapshot().Serialize(writer); + + writer.Write((byte) actor.Data.GetMaxBytes()); + actor.Data.Serialize(writer); + } + } + + internal bool ReadInSceneActorsStates(NetworkBufferReader reader) { + reader.Read(out ushort count); + + for (int i = 0; i < count; ++i) { + reader.Read(out ulong actorStaticID); + reader.Read(out ushort actorID); + + s_Logger.Info($"Reading actor with StaticID {actorStaticID} and ID {actorID} during synchronization."); + + Actor actor = FindInSceneActorWithStaticID(actorStaticID); + if (actor == null) { + s_Logger.Error($"Failed to find actor with ID {actorID} during synchronization."); + return false; + } + + ActorCoreStateSnapshot coreState = new ActorCoreStateSnapshot(); + coreState.Deserialize(reader); + + reader.Read(out byte actorDataSize); + reader.Read(out NativeSlice stateData, actorDataSize); + + actor.RestoreCoreState(coreState); + if (stateData.Length > 0) { + DataSerializationUtils.Deserialize(stateData, ref actor.Data); + } + + s_Logger.Info($"Assigning StaticID {actorStaticID} and ID {actorID}"); + actor.ActorID = actorID; + s_Logger.Info("Actor id set to " + actor.ActorID); + } + + foreach (Actor inSceneActor in m_InSceneActors) { + s_Logger.Info($"InSceneActor: StaticID={inSceneActor.ActorStaticID}, ID={inSceneActor.ActorID}, Path={inSceneActor.SourceActorPath}"); + } + + return true; + } + + internal void SpawnDynamicActorsForClient(ulong clientID) { + if (!RR.IsServer()) { + s_Logger.Error("Only the server can spawn dynamic actors for clients."); + return; + } + + foreach (Actor actor in m_SpawnedActors) { + if (actor.OrNull() == null) { + continue; + } + + ActorCoreStateSnapshot coreState = actor.GetCoreStateSnapshot(); + m_Network.SendSpawnActor(clientID, + actor.SourceActorPath, + coreState, + actor.Data); + } + } } + } \ No newline at end of file diff --git a/Runtime/Engine/Code/Simulation/WorldService.cs b/Runtime/Engine/Code/Simulation/WorldService.cs index 2a5c82d..bafe4bf 100644 --- a/Runtime/Engine/Code/Simulation/WorldService.cs +++ b/Runtime/Engine/Code/Simulation/WorldService.cs @@ -3,9 +3,7 @@ using System.Threading; using Cysharp.Threading.Tasks; using RebootKit.Engine.Foundation; using RebootKit.Engine.Main; -using Unity.Netcode; using UnityEngine; -using UnityEngine.AddressableAssets; using UnityEngine.Assertions; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.ResourceManagement.ResourceProviders; @@ -59,7 +57,7 @@ namespace RebootKit.Engine.Simulation { public void Dispose() { Unload(); } - + public async UniTask LoadAsync(WorldConfig worldConfig, CancellationToken cancellationToken) { await UniTask.WaitWhile(() => m_WorldState == WorldState.Loading, cancellationToken: cancellationToken); @@ -73,9 +71,7 @@ namespace RebootKit.Engine.Simulation { await m_SceneInstance.Result.ActivateAsync(); SceneManager.SetActiveScene(m_SceneInstance.Result.Scene); - - // await UniTask.WaitWhile(() => RR.CoreNetworkGameSystemsInstance is null, cancellationToken: cancellationToken); - + foreach (GameObject root in m_SceneInstance.Result.Scene.GetRootGameObjects()) { if (root.TryGetComponent(out IWorldContext worldContext)) { Assert.IsNull(Context, @@ -84,7 +80,7 @@ namespace RebootKit.Engine.Simulation { } foreach (Actor actor in root.GetComponentsInChildren()) { - RR.NetworkSystemInstance.Actors.RegisterInSceneActor(actor); + RR.Network.Actors.RegisterInSceneActor(actor); } } @@ -96,8 +92,8 @@ namespace RebootKit.Engine.Simulation { return; } - if (RR.NetworkSystemInstance != null) { - RR.NetworkSystemInstance.Actors.CleanUp(); + if (RR.Network != null) { + RR.Network.Actors.CleanUp(); } if (m_SceneInstance.IsValid()) { diff --git a/Runtime/Engine/Code/Steam/FacepunchTransport.cs b/Runtime/Engine/Code/Steam/FacepunchTransport.cs deleted file mode 100644 index 2d9bc9f..0000000 --- a/Runtime/Engine/Code/Steam/FacepunchTransport.cs +++ /dev/null @@ -1,277 +0,0 @@ -// Source: https://github.com/Unity-Technologies/multiplayer-community-contributions/blob/main/Transports/com.community.netcode.transport.facepunch/Runtime/FacepunchTransport.cs - -using System; -using System.Collections; -using System.Collections.Generic; -using Steamworks; -using Steamworks.Data; -using Unity.Collections; -using Unity.Collections.LowLevel.Unsafe; -using Unity.Netcode; -using UnityEngine; - -namespace RebootKit.Engine.Steam { - using SocketConnection = Connection; - - public class FacepunchTransport : NetworkTransport, IConnectionManager, ISocketManager { - private ConnectionManager connectionManager; - private SocketManager socketManager; - private Dictionary connectedClients; - - [Space] - [Tooltip("The Steam App ID of your game. Technically you're not allowed to use 480, but Valve doesn't do anything about it so it's fine for testing purposes.")] - [SerializeField] private uint steamAppId = 480; - - [Tooltip("The Steam ID of the user targeted when joining as a client.")] - [SerializeField] public ulong targetSteamId; - - [Header("Info")] - [ReadOnly] - [Tooltip("When in play mode, this will display your Steam ID.")] - [SerializeField] private ulong userSteamId; - - private LogLevel LogLevel => NetworkManager.Singleton.LogLevel; - - private class Client { - public SteamId steamId; - public SocketConnection connection; - } - -// #region MonoBehaviour Messages -// -// private void Awake() { -// try { -// SteamClient.Init(steamAppId, false); -// } catch (Exception e) { -// if (LogLevel <= LogLevel.Error) -// Debug.LogError($"[{nameof(FacepunchTransport)}] - Caught an exeption during initialization of Steam client: {e}"); -// } finally { -// StartCoroutine(InitSteamworks()); -// } -// } -// -// private void Update() { -// SteamClient.RunCallbacks(); -// } -// -// private void OnDestroy() { -// SteamClient.Shutdown(); -// } -// -// #endregion - -#region NetworkTransport Overrides - - public override ulong ServerClientId => 0; - - public override void DisconnectLocalClient() { - connectionManager?.Connection.Close(); - - if (LogLevel <= LogLevel.Developer) - Debug.Log($"[{nameof(FacepunchTransport)}] - Disconnecting local client."); - } - - public override void DisconnectRemoteClient(ulong clientId) { - if (connectedClients.TryGetValue(clientId, out Client user)) { - // Flush any pending messages before closing the connection - user.connection.Flush(); - user.connection.Close(); - connectedClients.Remove(clientId); - - if (LogLevel <= LogLevel.Developer) - Debug.Log($"[{nameof(FacepunchTransport)}] - Disconnecting remote client with ID {clientId}."); - } else if (LogLevel <= LogLevel.Normal) - Debug.LogWarning($"[{nameof(FacepunchTransport)}] - Failed to disconnect remote client with ID {clientId}, client not connected."); - } - - public override unsafe ulong GetCurrentRtt(ulong clientId) { - return 0; - } - - public override void Initialize(NetworkManager networkManager = null) { - connectedClients = new Dictionary(); - } - - private SendType NetworkDeliveryToSendType(NetworkDelivery delivery) { - return delivery switch { - NetworkDelivery.Reliable => SendType.Reliable, - NetworkDelivery.ReliableFragmentedSequenced => SendType.Reliable, - NetworkDelivery.ReliableSequenced => SendType.Reliable, - NetworkDelivery.Unreliable => SendType.Unreliable, - NetworkDelivery.UnreliableSequenced => SendType.Unreliable, - _ => SendType.Reliable - }; - } - - public override void Shutdown() { - try { - if (LogLevel <= LogLevel.Developer) - Debug.Log($"[{nameof(FacepunchTransport)}] - Shutting down."); - - connectionManager?.Close(); - socketManager?.Close(); - } catch (Exception e) { - if (LogLevel <= LogLevel.Error) - Debug.LogError($"[{nameof(FacepunchTransport)}] - Caught an exception while shutting down: {e}"); - } - } - - public override void Send(ulong clientId, ArraySegment data, NetworkDelivery delivery) { - var sendType = NetworkDeliveryToSendType(delivery); - - if (clientId == ServerClientId) - connectionManager.Connection.SendMessage(data.Array, data.Offset, data.Count, sendType); - else if (connectedClients.TryGetValue(clientId, out Client user)) - user.connection.SendMessage(data.Array, data.Offset, data.Count, sendType); - else if (LogLevel <= LogLevel.Normal) - Debug.LogWarning($"[{nameof(FacepunchTransport)}] - Failed to send packet to remote client with ID {clientId}, client not connected."); - } - - public override NetworkEvent PollEvent(out ulong clientId, - out ArraySegment payload, - out float receiveTime) { - connectionManager?.Receive(); - socketManager?.Receive(); - - clientId = 0; - receiveTime = Time.realtimeSinceStartup; - payload = default; - return NetworkEvent.Nothing; - } - - public override bool StartClient() { - if (LogLevel <= LogLevel.Developer) - Debug.Log($"[{nameof(FacepunchTransport)}] - Starting as client."); - - connectionManager = SteamNetworkingSockets.ConnectRelay(targetSteamId); - connectionManager.Interface = this; - return true; - } - - public override bool StartServer() { - if (LogLevel <= LogLevel.Developer) - Debug.Log($"[{nameof(FacepunchTransport)}] - Starting as server."); - - socketManager = SteamNetworkingSockets.CreateRelaySocket(); - socketManager.Interface = this; - return true; - } - -#endregion - -#region ConnectionManager Implementation - - private byte[] payloadCache = new byte[4096]; - - private void EnsurePayloadCapacity(int size) { - if (payloadCache.Length >= size) - return; - - payloadCache = new byte[Math.Max(payloadCache.Length * 2, size)]; - } - - void IConnectionManager.OnConnecting(ConnectionInfo info) { - if (LogLevel <= LogLevel.Developer) - Debug.Log($"[{nameof(FacepunchTransport)}] - Connecting with Steam user {info.Identity.SteamId}."); - } - - void IConnectionManager.OnConnected(ConnectionInfo info) { - InvokeOnTransportEvent(NetworkEvent.Connect, ServerClientId, default, Time.realtimeSinceStartup); - - if (LogLevel <= LogLevel.Developer) - Debug.Log($"[{nameof(FacepunchTransport)}] - Connected with Steam user {info.Identity.SteamId}."); - } - - void IConnectionManager.OnDisconnected(ConnectionInfo info) { - InvokeOnTransportEvent(NetworkEvent.Disconnect, ServerClientId, default, Time.realtimeSinceStartup); - - if (LogLevel <= LogLevel.Developer) - Debug.Log($"[{nameof(FacepunchTransport)}] - Disconnected Steam user {info.Identity.SteamId}."); - } - - unsafe void IConnectionManager.OnMessage(IntPtr data, int size, long messageNum, long recvTime, int channel) { - EnsurePayloadCapacity(size); - - fixed (byte* payload = payloadCache) { - UnsafeUtility.MemCpy(payload, (byte*) data, size); - } - - InvokeOnTransportEvent(NetworkEvent.Data, ServerClientId, new ArraySegment(payloadCache, 0, size), - Time.realtimeSinceStartup); - } - -#endregion - -#region SocketManager Implementation - - void ISocketManager.OnConnecting(SocketConnection connection, ConnectionInfo info) { - if (LogLevel <= LogLevel.Developer) - Debug.Log($"[{nameof(FacepunchTransport)}] - Accepting connection from Steam user {info.Identity.SteamId}."); - - connection.Accept(); - } - - void ISocketManager.OnConnected(SocketConnection connection, ConnectionInfo info) { - if (!connectedClients.ContainsKey(connection.Id)) { - connectedClients.Add(connection.Id, new Client() { - connection = connection, - steamId = info.Identity.SteamId - }); - - InvokeOnTransportEvent(NetworkEvent.Connect, connection.Id, default, Time.realtimeSinceStartup); - - if (LogLevel <= LogLevel.Developer) - Debug.Log($"[{nameof(FacepunchTransport)}] - Connected with Steam user {info.Identity.SteamId}."); - } else if (LogLevel <= LogLevel.Normal) - Debug.LogWarning($"[{nameof(FacepunchTransport)}] - Failed to connect client with ID {connection.Id}, client already connected."); - } - - void ISocketManager.OnDisconnected(SocketConnection connection, ConnectionInfo info) { - if (connectedClients.Remove(connection.Id)) { - InvokeOnTransportEvent(NetworkEvent.Disconnect, connection.Id, default, Time.realtimeSinceStartup); - - if (LogLevel <= LogLevel.Developer) - Debug.Log($"[{nameof(FacepunchTransport)}] - Disconnected Steam user {info.Identity.SteamId}"); - } else if (LogLevel <= LogLevel.Normal) - Debug.LogWarning($"[{nameof(FacepunchTransport)}] - Failed to diconnect client with ID {connection.Id}, client not connected."); - } - - unsafe void ISocketManager.OnMessage(SocketConnection connection, - NetIdentity identity, - IntPtr data, - int size, - long messageNum, - long recvTime, - int channel) { - EnsurePayloadCapacity(size); - - fixed (byte* payload = payloadCache) { - UnsafeUtility.MemCpy(payload, (byte*) data, size); - } - - InvokeOnTransportEvent(NetworkEvent.Data, connection.Id, new ArraySegment(payloadCache, 0, size), - Time.realtimeSinceStartup); - } - -#endregion - -#region Utility Methods - - private IEnumerator InitSteamworks() { - yield return new WaitUntil(() => SteamClient.IsValid); - - SteamNetworkingUtils.InitRelayNetworkAccess(); - - if (LogLevel <= LogLevel.Developer) - Debug.Log($"[{nameof(FacepunchTransport)}] - Initialized access to Steam Relay Network."); - - userSteamId = SteamClient.SteamId; - - if (LogLevel <= LogLevel.Developer) - Debug.Log($"[{nameof(FacepunchTransport)}] - Fetched user Steam ID."); - } - -#endregion - - } -} \ No newline at end of file diff --git a/Runtime/Engine/Code/Steam/FacepunchTransport.cs.meta b/Runtime/Engine/Code/Steam/FacepunchTransport.cs.meta deleted file mode 100644 index a7eb004..0000000 --- a/Runtime/Engine/Code/Steam/FacepunchTransport.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 516179f3a4a7476ea24875d52fae3042 -timeCreated: 1751378412 \ No newline at end of file diff --git a/Runtime/Engine/Code/Steam/SteamManager.cs b/Runtime/Engine/Code/Steam/SteamManager.cs index 2b2c96a..94cbe83 100644 --- a/Runtime/Engine/Code/Steam/SteamManager.cs +++ b/Runtime/Engine/Code/Steam/SteamManager.cs @@ -51,6 +51,10 @@ namespace RebootKit.Engine.Steam { s_Logger.Warning("Join request key is empty. Cannot process join request."); return; } + + if (ulong.TryParse(key, out ulong steamID)) { + RR.ConnectWithSteamID(steamID); + } } } } \ No newline at end of file diff --git a/Runtime/Engine/Code/Steam/SteamNetworkManager.cs b/Runtime/Engine/Code/Steam/SteamNetworkManager.cs index 936916e..4bcc8d7 100644 --- a/Runtime/Engine/Code/Steam/SteamNetworkManager.cs +++ b/Runtime/Engine/Code/Steam/SteamNetworkManager.cs @@ -1,14 +1,332 @@ -#define RR_LOCALHOST_ONLY - -using System; -using R3; -using RebootKit.Engine.Foundation; -using RebootKit.Engine.Main; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using RebootKit.Engine.Network; using Steamworks; using Steamworks.Data; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; using Logger = RebootKit.Engine.Foundation.Logger; namespace RebootKit.Engine.Steam { + public class SteamNetworkManager : INetworkManager, ISocketManager, IConnectionManager { + static readonly Logger s_Logger = new Logger(nameof(SteamNetworkManager)); + + public const ulong k_ServerClientID = 0; + const int k_DefaultPort = 419; + + class Client { + public ulong ClientID; + public Connection Connection; + } + + readonly Dictionary m_Clients = new Dictionary(); + + SocketManager m_SocketManager; + ConnectionManager m_ConnectionManager; + + float m_StatsRefreshTimer; + + public INetworkManagerDelegate Delegate { get; set; } + + public ulong LocalClientID { + get { + return SteamClient.SteamId; + } + } + + public NetworkManagerStats Stats { get; } = new NetworkManagerStats(); + + // @NOTE: Set this before starting the client to specify which server to connect to. + public ulong TargetSteamID; + + public SteamNetworkManager() { + SteamNetworkingUtils.InitRelayNetworkAccess(); + SteamNetworkingUtils.OnDebugOutput += SteamDebugOutput; + SteamNetworkingUtils.DebugLevel = NetDebugOutput.None; + } + + public void Dispose() { + SteamNetworkingUtils.OnDebugOutput -= SteamDebugOutput; + + StopHost(); + Disconnect(); + } + + void SteamDebugOutput(NetDebugOutput output, string msg) { + s_Logger.Info($"Output: {output} - {msg}"); + } + + // + // @MARK: Server + // + public bool IsServer() { + return m_SocketManager != null; + } + + public bool StartHost() { + try { + TargetSteamID = SteamClient.SteamId; + + m_SocketManager = SteamNetworkingSockets.CreateRelaySocket(k_DefaultPort); + m_SocketManager.Interface = this; + + m_ConnectionManager = + SteamNetworkingSockets.ConnectRelay(TargetSteamID, k_DefaultPort); + m_ConnectionManager.Interface = this; + + SteamFriends.SetRichPresence("connect", LocalClientID.ToString()); + + Stats.Reset(); + + Delegate?.OnServerStarted(); + Delegate?.OnClientStarted(); + } catch (Exception e) { + s_Logger.Error($"Failed to create server: {e.Message}"); + m_SocketManager = null; + return false; + } + + return true; + } + + public void StopHost() { + if (!IsServer()) { + return; + } + + s_Logger.Info("Stopping server..."); + + SteamFriends.SetRichPresence("connect", null); + + foreach (Client client in m_Clients.Values) { + client.Connection.Close(); + } + + m_Clients.Clear(); + + m_SocketManager.Close(); + m_SocketManager = null; + + Delegate?.OnClientStopped(); + Delegate?.OnServerStopped(); + } + + // + // @MARK: Client + // + public bool IsClient() { + return m_ConnectionManager != null; + } + + public bool StartClient() { + if (IsClient()) { + s_Logger.Error("Cannot start client while already connected."); + return false; + } + + s_Logger.Info("Connecting to server with steam ID: " + TargetSteamID); + try { + m_ConnectionManager = + SteamNetworkingSockets.ConnectRelay(TargetSteamID, k_DefaultPort); + m_ConnectionManager.Interface = this; + + Stats.Reset(); + + Delegate?.OnClientStarted(); + } catch (Exception e) { + s_Logger.Error($"Failed to connect to server with ID {TargetSteamID}: {e.Message}"); + m_ConnectionManager = null; + return false; + } + + return true; + } + + public void Disconnect() { + if (!IsClient()) { + return; + } + + s_Logger.Info("Disconnecting from server..."); + + m_ConnectionManager.Close(); + m_ConnectionManager = null; + + Delegate?.OnClientStopped(); + } + + // + // @MARK: Data Sending/Receiving + // + public void Send(ulong clientID, NativeSlice data, SendMode mode) { + if (data.Length == 0) { + s_Logger.Error("Cannot send empty data."); + return; + } + + unsafe { + Send(clientID, (byte*) data.GetUnsafePtr(), data.Length, mode); + } + } + + public unsafe void Send(ulong clientID, byte* data, int length, SendMode mode) { + if (IsServer()) { + if (clientID == k_ServerClientID || clientID == LocalClientID) { + NativeArray dataArray = new NativeArray(length, Allocator.Temp); + UnsafeUtility.MemCpy(dataArray.GetUnsafePtr(), data, length); + MessageReceived(LocalClientID, dataArray); + return; + } + + if (!m_Clients.TryGetValue(clientID, out Client client)) { + s_Logger.Error($"Client with ID {clientID} not found."); + return; + } + + Result sendResult = client.Connection.SendMessage((IntPtr) data, length, GetSteamSendType(mode)); + if (sendResult == Result.OK) { + if (mode == SendMode.Reliable) { + Stats.ReliableBytesSent += (ulong) length; + } else if (mode == SendMode.Unreliable) { + Stats.UnreliableBytesSent += (ulong) length; + } + } else { + s_Logger.Error($"Failed to send message to clientID, {clientID}. Result: {sendResult}."); + } + } else if (IsClient()) { + if (clientID != k_ServerClientID) { + s_Logger.Error($"Client can only send messages to the server (clientID must be {k_ServerClientID.ToString()})."); + return; + } + + Result sendResult = m_ConnectionManager.Connection.SendMessage((IntPtr) data, + length, + GetSteamSendType(mode)); + if (sendResult == Result.OK) { + if (mode == SendMode.Reliable) { + Stats.ReliableBytesSent += (ulong) length; + } else if (mode == SendMode.Unreliable) { + Stats.UnreliableBytesSent += (ulong) length; + } + } else { + s_Logger.Error($"Failed to send message to server. Result: {sendResult}. Flushing and retrying..."); + } + } else { + s_Logger.Error("Cannot send data, server or client is not started."); + } + } + + void MessageReceived(ulong senderID, NativeArray data) { + Delegate?.OnMessageReceived(senderID, data); + } + + public void Tick() { + m_SocketManager?.Receive(); + m_ConnectionManager?.Receive(); + + m_StatsRefreshTimer -= UnityEngine.Time.deltaTime; + + if (m_StatsRefreshTimer <= 0f) { + m_StatsRefreshTimer = 1.0f; + + Stats.ReliableBytesSentPerSecond = Stats.ReliableBytesSent; + Stats.UnreliableBytesSentPerSecond = Stats.UnreliableBytesSent; + Stats.BytesReceivedPerSecond = Stats.BytesReceived; + + Stats.ReliableBytesSent = 0; + Stats.UnreliableBytesSent = 0; + Stats.BytesReceived = 0; + } + } + + // + // @MARK: IConnectionManager + // + void ISocketManager.OnConnecting(Connection connection, ConnectionInfo info) { + s_Logger.Info($"OnConnecting: {connection.Id} - {info.Identity}"); + connection.Accept(); + } + + void ISocketManager.OnConnected(Connection connection, ConnectionInfo info) { + if (!m_Clients.ContainsKey(info.Identity.SteamId)) { + s_Logger.Info($"OnConnected: {connection.Id} - {info.Identity}"); + + Client client = new Client { + ClientID = info.Identity.SteamId, + Connection = connection + }; + m_Clients.Add(info.Identity.SteamId, client); + + Delegate?.OnClientConnected(client.ClientID); + } else { + s_Logger.Error("Failed to add client, already exists: " + info.Identity.SteamId); + connection.Close(); + } + } + + void ISocketManager.OnDisconnected(Connection connection, ConnectionInfo info) { + s_Logger.Info($"OnDisconnected: {connection.Id} - {info.Identity}"); + + if (m_Clients.Remove(info.Identity.SteamId)) { + Delegate?.OnClientDisconnected(info.Identity.SteamId); + } else { + s_Logger.Error("Failed to remove client, not found: " + info.Identity.SteamId); + } + } + + void ISocketManager.OnMessage(Connection connection, + NetIdentity identity, + IntPtr data, + int size, + long messageNum, + long recvTime, + int channel) { + NativeArray dataArray = new NativeArray(size, Allocator.Temp); + unsafe { + UnsafeUtility.MemCpy(dataArray.GetUnsafePtr(), (void*) data, size); + } + + Stats.BytesReceived += (ulong) size; + MessageReceived(identity.SteamId, dataArray); + } + + // + // @MARK: ISocketManager + // + void IConnectionManager.OnConnecting(ConnectionInfo info) { } + + void IConnectionManager.OnConnected(ConnectionInfo info) { + Delegate?.OnClientConnected(info.Identity.SteamId); + } + + void IConnectionManager.OnDisconnected(ConnectionInfo info) { + Delegate?.OnClientDisconnected(info.Identity.SteamId); + } + + void IConnectionManager.OnMessage(IntPtr data, int size, long messageNum, long recvTime, int channel) { + NativeArray dataArray = new NativeArray(size, Allocator.Temp); + unsafe { + UnsafeUtility.MemCpy(dataArray.GetUnsafePtr(), (void*) data, size); + } + + Stats.BytesReceived += (ulong) size; + MessageReceived(k_ServerClientID, dataArray); + } + + // + // @MARK: Utilities + // + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static SendType GetSteamSendType(SendMode mode) { + return mode switch { + SendMode.Reliable => SendType.Reliable, + SendMode.Unreliable => SendType.Unreliable, + _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null) + }; + } + } + // class SteamNetworkManager : INetworkManager { // static readonly Logger s_Logger = new Logger(nameof(SteamNetworkManager)); // diff --git a/Runtime/Engine/RebootKit.Engine.asmdef b/Runtime/Engine/RebootKit.Engine.asmdef index f74778d..fadf745 100755 --- a/Runtime/Engine/RebootKit.Engine.asmdef +++ b/Runtime/Engine/RebootKit.Engine.asmdef @@ -15,10 +15,7 @@ "GUID:84651a3751eca9349aac36a66bba901b", "GUID:d8b63aba1907145bea998dd612889d6b", "GUID:f2d49d9fa7e7eb3418e39723a7d3b92f", - "GUID:324caed91501a9c47a04ebfd87b68794", - "GUID:1491147abca9d7d4bb7105af628b223e", - "GUID:b1cd7326e664a434ab35daa802773c7f", - "GUID:2c360f3794ebc41388fc11424ddbfdd0" + "GUID:324caed91501a9c47a04ebfd87b68794" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Runtime/Engine/Resources.meta b/Runtime/Engine/Resources.meta deleted file mode 100755 index aaf4871..0000000 --- a/Runtime/Engine/Resources.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 3117fc3405548ba42b44318e9dce1efd -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Runtime/Engine/Resources/RebootKit.meta b/Runtime/Engine/Resources/RebootKit.meta deleted file mode 100755 index 49f35c1..0000000 --- a/Runtime/Engine/Resources/RebootKit.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 2eeb6bda1b70984479edf1306d3bdc99 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Runtime/Engine/Resources/RebootKit/core_network_game_systems.prefab b/Runtime/Engine/Resources/RebootKit/core_network_game_systems.prefab deleted file mode 100644 index efa4b1c..0000000 --- a/Runtime/Engine/Resources/RebootKit/core_network_game_systems.prefab +++ /dev/null @@ -1,87 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!1 &1321683558189709310 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1878935056881269035} - - component: {fileID: 258784011466397156} - - component: {fileID: 8437866359923088342} - - component: {fileID: 2149791309811179493} - m_Layer: 0 - m_Name: core_network_game_systems - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!4 &1878935056881269035 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1321683558189709310} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!114 &258784011466397156 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1321683558189709310} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} - m_Name: - m_EditorClassIdentifier: - GlobalObjectIdHash: 2588764075 - InScenePlacedSourceGlobalObjectIdHash: 0 - DeferredDespawnTick: 0 - Ownership: 1 - AlwaysReplicateAsRoot: 0 - SynchronizeTransform: 0 - ActiveSceneSynchronization: 0 - SceneMigrationSynchronization: 0 - SpawnWithObservers: 1 - DontDestroyWithOwner: 0 - AutoObjectParentSync: 0 - SyncOwnerTransformWhenParented: 0 - AllowOwnerToParent: 0 ---- !u!114 &8437866359923088342 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1321683558189709310} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 1f967d37c17e4704b80849c305a53be9, type: 3} - m_Name: - m_EditorClassIdentifier: - k__BackingField: {fileID: 2149791309811179493} ---- !u!114 &2149791309811179493 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1321683558189709310} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: af0c5ff8ac4945eca2a9cab0a88268f4, type: 3} - m_Name: - m_EditorClassIdentifier: - ShowTopMostFoldoutHeaderGroup: 1 diff --git a/Runtime/Engine/Resources/RebootKit/core_network_game_systems.prefab.meta b/Runtime/Engine/Resources/RebootKit/core_network_game_systems.prefab.meta deleted file mode 100644 index e77881a..0000000 --- a/Runtime/Engine/Resources/RebootKit/core_network_game_systems.prefab.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 2cc631d24ab41194ebdeffff7faf62a5 -PrefabImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Runtime/Engine/core_assets/scenes/scn_main.unity b/Runtime/Engine/core_assets/scenes/scn_main.unity index 4e81f8a..e676f46 100644 --- a/Runtime/Engine/core_assets/scenes/scn_main.unity +++ b/Runtime/Engine/core_assets/scenes/scn_main.unity @@ -490,7 +490,7 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: m_Document: {fileID: 638618785} ---- !u!1 &735095186 +--- !u!1 &1014886342 GameObject: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -498,50 +498,48 @@ GameObject: m_PrefabAsset: {fileID: 0} serializedVersion: 6 m_Component: - - component: {fileID: 735095188} - - component: {fileID: 735095187} - m_Layer: 0 - m_Name: Runtime Network Stats Monitor + - component: {fileID: 1014886344} + - component: {fileID: 1014886343} + m_Layer: 5 + m_Name: watermark m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!114 &735095187 +--- !u!114 &1014886343 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 735095186} + m_GameObject: {fileID: 1014886342} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 17737e0516da2445b9b0077ae2bd9b4f, type: 3} + m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0} m_Name: m_EditorClassIdentifier: - m_Visible: 1 - m_MaxRefreshRate: 30 - k__BackingField: {fileID: 0} - k__BackingField: {fileID: 0} - k__BackingField: - k__BackingField: 1 - m_PositionLeftToRight: 0 - m_PositionTopToBottom: 0 - k__BackingField: {fileID: 11400000, guid: 8f1b4e3792d8446399527749cfb591a2, type: 2} ---- !u!4 &735095188 + m_PanelSettings: {fileID: 11400000, guid: 7f72ab2868182b849a37094d51821bc2, type: 2} + m_ParentUI: {fileID: 0} + sourceAsset: {fileID: 9197481963319205126, guid: 70f3e7058aab07444af9dc0114ecec32, type: 3} + m_SortingOrder: 9999 + m_WorldSpaceSizeMode: 1 + m_WorldSpaceWidth: 1920 + m_WorldSpaceHeight: 1080 +--- !u!4 &1014886344 Transform: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 735095186} + m_GameObject: {fileID: 1014886342} serializedVersion: 2 - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] - m_Father: {fileID: 1493735124} + m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1468139325 GameObject: @@ -621,7 +619,6 @@ MonoBehaviour: m_EditorClassIdentifier: m_DebugOverlayView: {fileID: 1809244332} m_GameVersionOverlay: {fileID: 638618787} - m_NetworkStatsOverlay: {fileID: 735095187} --- !u!4 &1493735124 Transform: m_ObjectHideFlags: 0 @@ -637,7 +634,6 @@ Transform: m_Children: - {fileID: 1809244330} - {fileID: 638618786} - - {fileID: 735095188} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1809244329 @@ -713,3 +709,4 @@ SceneRoots: - {fileID: 1468139327} - {fileID: 1493735124} - {fileID: 599568090} + - {fileID: 1014886344} diff --git a/Runtime/Engine/core_assets/ui/tss_rebootkit.tss b/Runtime/Engine/core_assets/ui/tss_rebootkit.tss index e7b246d..6ab1c2b 100755 --- a/Runtime/Engine/core_assets/ui/tss_rebootkit.tss +++ b/Runtime/Engine/core_assets/ui/tss_rebootkit.tss @@ -139,7 +139,7 @@ color: #ffffff; -unity-text-outline-color: black; -unity-text-outline-width: 1px; - font-size: 16px; + font-size: 14px; -unity-font-definition: url("project://database/Assets/RebootKit/Runtime/Engine/core_assets/fonts/JetBrainsMono-Bold.ttf"); text-shadow: 0 2px 16px rgba(0, 0, 0, 0.75); }