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.Network; using RebootKit.Engine.Simulation; #if RR_STEAM using RebootKit.Engine.Steam; #endif 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; internal static EngineConfigAsset EngineConfig; static DisposableBag s_DisposableBag; static DisposableBag s_ServicesBag; static AsyncOperationHandle s_MainMenuSceneHandle; 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; } public static WorldService World { get; private set; } public static Camera MainCamera { get; internal set; } public static Game GameInstance { get; internal set; } // 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(); #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) { 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) { Object.Destroy(GameInstance); GameInstance = null; } Network.Dispose(); Network = null; #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; } // // @MARK: Game // internal 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; } Network.SetCurrentWorld(worldID); } public static Actor SpawnLocalOnlyActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) { return Network.Actors.SpawnLocalOnlyActor(assetReference, position, rotation); } 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 (Network 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 Network.Actors.SpawnActor(assetReference, position, rotation); } public static void DestroyActor(Actor actor) { if (actor == null) { s_Logger.Error("Cannot destroy actor. Actor is null."); return; } Network.Actors.DestroyActor(actor); } public static Actor FindSpawnedActor(ushort actorID) { if (Network is null) { s_Logger.Error("NetworkSystemInstance is not initialized. Cannot find actor."); return null; } Actor actor = Network.Actors.FindActorByID(actorID); if (actor is null) { s_Logger.Error($"Actor with ID {actorID} not found"); } return actor; } 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); } public static IEnumerable Actors() { foreach (Actor actor in Network.Actors.InSceneActors) { yield return actor; } foreach (Actor actor in Network.Actors.SpawnedActors) { yield return actor; } } // // @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)}"); } 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; } // // @MARK: 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); } // // @MARK: Config Variables // 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; } // // @MARK: Network API // public static bool IsServer() { return Network != null && Network.Manager.IsServer(); } public static bool IsClient() { return Network != null && Network.Manager.IsClient(); } public static void StartHost() { if (IsServer() || IsClient()) { s_Logger.Error("Already hosting a server or connected as a client"); return; } // @TODO: Handle failures s_Logger.Info("Starting host"); if (!Network.Manager.StartHost()) { s_Logger.Error("Failed to start host."); return; } } public static void StopHost() { Network.Manager.StopHost(); } public static void Connect() { if (IsClient()) { s_Logger.Error("Already connected to a server"); return; } s_Logger.Info("Connecting to server."); Network.Manager.StartClient(); } public static void ConnectWithSteamID(ulong steamId) { #if RR_STEAM if (IsClient()) { s_Logger.Error("Already connected to a server"); return; } s_Logger.Info($"Connecting to server with Steam ID: {steamId}"); 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."); } #else s_Logger.Error("Steam integration is not enabled. Cannot connect with Steam ID."); #endif } public static void Disconnect() { Network.Manager.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; } throw new NotSupportedException("Cannot send chat message. Not connected to a server."); } static void Tick() { float deltaTime = Time.deltaTime; Network.Tick(deltaTime); } internal static void OnServerStarted() { s_Logger.Info("Server started"); GameInstance = Object.Instantiate(EngineConfig.gamePrefab); } internal static void OnServerStopped() { s_Logger.Info("Server stopped"); if (GameInstance is not null) { Object.Destroy(GameInstance.gameObject); GameInstance = null; } } internal static void OnClientStarted() { if (IsServer()) { return; } 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; } } // 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); } } }