using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using Cysharp.Threading.Tasks; using R3; using RebootKit.Engine.Console; using RebootKit.Engine.Foundation; using RebootKit.Engine.Input; using RebootKit.Engine.Simulation; using RebootKit.Engine.Steam; using Unity.Netcode; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.ResourceManagement.ResourceProviders; using UnityEngine.SceneManagement; using Assert = UnityEngine.Assertions.Assert; using Logger = RebootKit.Engine.Foundation.Logger; using Object = UnityEngine.Object; [assembly: InternalsVisibleTo("RebootKit.Editor")] namespace RebootKit.Engine.Main { public static class RR { static readonly Logger s_Logger = new Logger("RR"); [ConfigVar("con.write_log", 1, "Enables writing game log to console output")] static ConfigVar s_writeLogToConsole; [ConfigVar("sv.tick_rate", 24, "Server tick rate in Hz")] public static ConfigVar TickRate; internal static EngineConfigAsset EngineConfig; static DisposableBag s_disposableBag; static DisposableBag s_servicesBag; static AsyncOperationHandle s_mainMenuSceneHandle; static NetworkSystem s_networkSystemPrefab; internal static NetworkSystem NetworkSystemInstance; internal static ConsoleService Console { get; private set; } public static InputService Input { get; private set; } public static WorldService World { get; private set; } public static Camera MainCamera { get; internal set; } public static Game GameInstance { get; internal set; } public static ulong TickCount { get; private set; } public static event Action ServerTick = delegate { }; public static event Action ClientTick = delegate { }; // Lifecycle API // @NOTE: This method is called at the very start of the game, when boot scene loaded. internal static async UniTask InitAsync(EngineConfigAsset configAsset, CancellationToken cancellationToken) { Assert.IsNotNull(configAsset, "Config asset is required"); EngineConfig = configAsset; s_Logger.Info("Initializing"); s_servicesBag = new DisposableBag(); s_disposableBag = new DisposableBag(); s_Logger.Info("Registering core services"); Console = CreateService(); Input = new InputService(EngineConfig.inputConfig); s_servicesBag.Add(Input); World = CreateService(); await InitializeAssetsAsync(cancellationToken); #if RR_STEAM await SteamManager.InitializeAsync(cancellationToken); #endif } // @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); await OpenMainMenuAsync(cancellationToken); #if UNITY_EDITOR string scriptContent = UnityEditor.EditorPrefs.GetString("RebootKitEditor.OnGameRunScriptContent", ""); s_Logger.Info($"Executing script: {scriptContent}"); if (!string.IsNullOrEmpty(scriptContent)) { foreach (string cmd in scriptContent.Split('\n')) { s_Logger.Info($"Executing command: {cmd}"); Console.Execute(cmd); } } #endif } internal static void Shutdown() { 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; } #if RR_STEAM SteamManager.Shutdown(); #endif s_servicesBag.Dispose(); s_disposableBag.Dispose(); } // Assets API static readonly List s_WorldConfigsAssets = new List(); public static IReadOnlyList WorldConfigsAssets => s_WorldConfigsAssets; static async UniTask InitializeAssetsAsync(CancellationToken cancellationToken) { s_WorldConfigsAssets.Clear(); s_Logger.Info("Loading game assets"); await Addressables.LoadAssetsAsync("world", asset => { s_WorldConfigsAssets.Add(asset); }) .ToUniTask(cancellationToken: cancellationToken); } public static WorldConfigAsset GetWorldConfigAsset(string name) { if (string.IsNullOrEmpty(name)) { throw new ArgumentException("World config name cannot be null or empty", nameof(name)); } WorldConfigAsset worldConfig = s_WorldConfigsAssets.Find(asset => asset.Config.name.Equals(name, StringComparison.Ordinal)); if (worldConfig is null) { throw new KeyNotFoundException($"World config '{name}' not found"); } return worldConfig; } // Game API public static async UniTask OpenMainMenuAsync(CancellationToken cancellationToken) { s_Logger.Info("Opening main menu"); World.Unload(); if (!EngineConfig.mainMenuScene.RuntimeKeyIsValid()) { s_Logger.Error("Main menu scene is not set in EngineConfig"); return; } s_mainMenuSceneHandle = Addressables.LoadSceneAsync(EngineConfig.mainMenuScene, LoadSceneMode.Additive); await s_mainMenuSceneHandle; } internal static void CloseMainMenu() { if (!s_mainMenuSceneHandle.IsValid()) { return; } Addressables.UnloadSceneAsync(s_mainMenuSceneHandle); } public static void SetServerWorld(string worldID) { if (!IsServer()) { s_Logger.Error("Cannot set server world. Not a server instance."); return; } if (GameInstance is null) { s_Logger.Error("Game is not initialized. Cannot set server world."); return; } s_Logger.Info($"Setting server world: {worldID}"); WorldConfigAsset worldConfigAsset = GetWorldConfigAsset(worldID); if (worldConfigAsset is null) { s_Logger.Error($"World '{worldID}' not found"); return; } NetworkSystemInstance.SetCurrentWorld(worldID); } public static Actor SpawnActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) { if (!IsServer()) { s_Logger.Error("Cannot spawn actor. Not a server instance."); return null; } if (NetworkSystemInstance is null) { s_Logger.Error("NetworkSystemInstance is not initialized. Cannot spawn actor."); return null; } if (!assetReference.RuntimeKeyIsValid()) { s_Logger.Error("Asset reference is not valid. Cannot spawn actor."); return null; } s_Logger.Info($"Spawning actor from asset reference: {assetReference.RuntimeKey}"); return NetworkSystemInstance.Actors.SpawnActor(assetReference, position, rotation); } public static Actor FindSpawnedActor(ulong actorID) { if (NetworkSystemInstance is null) { s_Logger.Error("NetworkSystemInstance is not initialized. Cannot find actor."); return null; } Actor actor = NetworkSystemInstance.Actors.FindActorByID(actorID); if (actor is null) { s_Logger.Error($"Actor with ID {actorID} not found"); } return actor; } // Service API public static TService CreateService(ServiceAsset asset) where TService : class, IService { if (asset is null) { throw new ArgumentNullException($"Null asset of type {typeof(TService)}"); } TService service = asset.Create(); s_servicesBag.Add(service); return service; } public static TService CreateService() where TService : class, IService { TService service = Activator.CreateInstance(); s_servicesBag.Add(service); return service; } // Logging API public static void Log(string message) { Debug.Log(message); Console?.WriteToOutput(message); } public static void LogWarning(string message) { Debug.LogWarning(message); Console?.WriteToOutput(message); } public static void LogError(string message) { Debug.LogError(message); Console?.WriteToOutput(message); } public static void WriteToConsole(string message) { Console?.WriteToOutput(message); } // CVar API public static ConfigVar CVarIndex(string name, int defaultValue = -1) { ConfigVar cvar = ConfigVarsContainer.Get(name); if (cvar != null) { return cvar; } cvar = new ConfigVar(name, defaultValue); ConfigVarsContainer.Register(cvar); return cvar; } public static ConfigVar CVarNumber(string name, double defaultValue = 0) { ConfigVar cvar = ConfigVarsContainer.Get(name); if (cvar != null) { return cvar; } cvar = new ConfigVar(name, defaultValue); ConfigVarsContainer.Register(cvar); return cvar; } public static ConfigVar CVarString(string name, string defaultValue = "") { ConfigVar cvar = ConfigVarsContainer.Get(name); if (cvar != null) { return cvar; } cvar = new ConfigVar(name, defaultValue); ConfigVarsContainer.Register(cvar); return cvar; } // Network API public static bool IsServer() { return NetworkManager.Singleton.IsServer; } public static bool IsClient() { return NetworkManager.Singleton.IsClient; } public static void StartHost() { if (NetworkManager.Singleton.IsHost) { s_Logger.Error("Already hosting a server"); return; } s_Logger.Info("Starting host"); NetworkManager.Singleton.StartHost(); } public static void StopServer() { } public static void Connect() { if (NetworkManager.Singleton.IsClient) { s_Logger.Error("Already connected to a server"); return; } s_Logger.Info($"Connecting to server."); NetworkManager.Singleton.StartClient(); } public static void ConnectWithSteamID(ulong steamId) { #if RR_STEAM if (NetworkManager.Singleton.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(); } else { s_Logger.Error("Network transport is not FacepunchTransport. Cannot connect with Steam ID."); } #else s_Logger.Error("Steam integration is not enabled. Cannot connect with Steam ID."); #endif } public static void Disconnect() { } public static void SendChatMessage(string message) { if (!IsClient()) { s_Logger.Error("Cannot send chat message. Not connected to a server."); return; } if (string.IsNullOrEmpty(message)) { return; } GameInstance.SendChatMessageRpc(message); } static float s_tickTimer; static void Tick() { float deltaTime = Time.deltaTime; float minTickTime = 1.0f / TickRate.IndexValue; s_tickTimer += deltaTime; while (s_tickTimer >= minTickTime) { s_tickTimer -= minTickTime; if (IsServer()) { ServerTick?.Invoke(TickCount); } if (IsClient()) { ClientTick?.Invoke(); } TickCount++; } } static void OnConnectionEvent(NetworkManager network, ConnectionEventData data) { s_Logger.Info("Connection event: " + data.EventType); } 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) { s_Logger.Info("Server stopped"); if (GameInstance is not null) { GameInstance.NetworkObject.Despawn(); GameInstance = null; } if (NetworkSystemInstance is not null) { if (NetworkSystemInstance.NetworkObject is not null && NetworkSystemInstance.NetworkObject.IsSpawned) { NetworkSystemInstance.NetworkObject.Despawn(); } NetworkSystemInstance = null; } } // Console Commands [RCCMD("say", "Sends chat message")] static void Say(string[] args) { if (args.Length < 2) { Console.WriteToOutput("Usage: say "); return; } string message = string.Join(" ", args, 1, args.Length - 1); SendChatMessage(message); } } }