From e04a1f7dc291f6ce8a43caae62f7caa58a8b52f1 Mon Sep 17 00:00:00 2001 From: George local server Date: Tue, 19 May 2026 09:07:18 -0700 Subject: [PATCH] Support official dedicated server build --- Nuclei/Features/Commands/CommandService.cs | 4 +- .../DefaultCommands/NewMissionCommand.cs | 6 +- Nuclei/Features/MissionService.cs | 4 +- Nuclei/Features/Server.cs | 308 +----------------- Nuclei/Features/SteamLobbyService.cs | 73 +---- Nuclei/GlobalUsings.cs | 3 + Nuclei/Helpers/Globals.cs | 6 +- Nuclei/Patches/CSteamAPIContextPatches.cs | 13 +- Nuclei/Patches/ChatManagerPatches.cs | 6 +- 9 files changed, 44 insertions(+), 379 deletions(-) create mode 100644 Nuclei/GlobalUsings.cs diff --git a/Nuclei/Features/Commands/CommandService.cs b/Nuclei/Features/Commands/CommandService.cs index 357da8a..814fd08 100644 --- a/Nuclei/Features/Commands/CommandService.cs +++ b/Nuclei/Features/Commands/CommandService.cs @@ -38,7 +38,7 @@ public static void RegisterCommand(ICommand command) public static PermissionLevel GetPlayerPermissionLevel(Player player) { if (NucleiConfig.Owner!.Value == player.SteamID.ToString()) - return PermissionLevel.Admin; + return PermissionLevel.Owner; if (NucleiConfig.AdminsList.Contains(player.SteamID.ToString())) return PermissionLevel.Admin; @@ -118,4 +118,4 @@ public static bool TryExecuteCommand(Player player, string message) return TryExecuteCommand(player, commandName, args); } -} \ No newline at end of file +} diff --git a/Nuclei/Features/Commands/DefaultCommands/NewMissionCommand.cs b/Nuclei/Features/Commands/DefaultCommands/NewMissionCommand.cs index faa31d4..0eec33d 100644 --- a/Nuclei/Features/Commands/DefaultCommands/NewMissionCommand.cs +++ b/Nuclei/Features/Commands/DefaultCommands/NewMissionCommand.cs @@ -20,9 +20,9 @@ public override bool Validate(Player player, string[] args) public override bool Execute(Player player, string[] args) { - _ = Server.StartOrRestartLobby(); - return true; + ChatService.SendPrivateChatMessage("The newmission command is disabled on the official dedicated server build.", player); + return false; } public override PermissionLevel DefaultPermissionLevel { get; } = PermissionLevel.Admin; -} \ No newline at end of file +} diff --git a/Nuclei/Features/MissionService.cs b/Nuclei/Features/MissionService.cs index 49ea733..9c6515e 100644 --- a/Nuclei/Features/MissionService.cs +++ b/Nuclei/Features/MissionService.cs @@ -47,7 +47,7 @@ public static class MissionService /// /// The current mission time. /// - public static float CurrentMissionTime => Globals.MissionManagerInstance.missionTime; + public static float CurrentMissionTime => Globals.MissionManagerInstance.MissionTime; /// /// Gets all Mission Keys as an IEnumerable. @@ -205,4 +205,4 @@ public static bool TryGetConsumePreselectedMission(out Mission? mission) PreselectedMissionKey = null; return true; } -} \ No newline at end of file +} diff --git a/Nuclei/Features/Server.cs b/Nuclei/Features/Server.cs index c9fbb69..ffbe804 100644 --- a/Nuclei/Features/Server.cs +++ b/Nuclei/Features/Server.cs @@ -1,332 +1,46 @@ -using System.Linq; -using System.Threading.Tasks; -using Cysharp.Threading.Tasks; -using NuclearOption.Networking; -using NuclearOption.SavedMission; -using Nuclei.Events; using Nuclei.Helpers; using UnityEngine; namespace Nuclei.Features; /// -/// Server functionality for Nuclei. +/// Server helpers for the official Nuclear Option dedicated server. /// public static class Server { /// - /// Indicates whether the server is currently running. + /// Indicates whether the dedicated server network layer is active. /// public static bool IsServerRunning => Globals.NetworkManagerNuclearOptionInstance.Server?.Active ?? false; /// - /// The current mission time, calculated from the server's perspective. + /// The current mission time, calculated from the dedicated server's mission manager. /// - public static double MissionTime => Globals.LocalPlayer.NetworkTime.Time; - - private static int _lastMotDSent; - - private static void CheckSendMotD() - { - if (NucleiConfig.MotDFrequency!.Value <= 0) - return; - - if (Time.time - _lastMotDSent < NucleiConfig.MotDFrequency!.Value && _lastMotDSent > 0) - return; - - ChatService.SendMotD(); - _lastMotDSent = (int) Time.time; - } - - private static void CheckMissionOverTime() - { - if (NucleiConfig.MissionDuration!.Value <= 0) - return; - - if (MissionTime < NucleiConfig.MissionDuration!.Value) - return; - - Nuclei.Logger?.LogInfo("Mission over-time reached, notifying players..."); - ChatService.SendChatMessage("Mission over-time reached, ending mission prematurely..."); - - _ = SlowlyEndMission(); - } - - private static void OnMissionEnded(Mission mission) - { - Nuclei.Logger?.LogInfo($"Mission ended: {mission.Name}. Faction scores: {mission.factions.Aggregate("", (current, faction) => current + $"{faction.factionName}: {faction.FactionHQ.factionScore} ")}"); - - _ = SlowlyEndMission(); - } - - private static async UniTask SlowlyEndMission() - { - ChatService.SendChatMessage("Ending mission and starting a new one in 30 seconds..."); - - await Task.Delay(20000); - - ChatService.SendChatMessage("Ending mission and starting a new one in 10 seconds..."); - - await Task.Delay(10000); - - Nuclei.Logger?.LogInfo("Ending mission and starting a new one..."); - - await StartOrRestartLobby(); - } - - private static void TimeStatus() - { - Nuclei.Logger?.LogInfo($"Mission time: {MissionTime} seconds ({MissionTime / 60} minutes)"); - Nuclei.Logger?.LogInfo($"Remaining time: {NucleiConfig.MissionDuration!.Value - MissionTime} seconds ({(NucleiConfig.MissionDuration!.Value - MissionTime) / 60} minutes)"); - Nuclei.Logger?.LogInfo($"Server FPS: {GetServerFPS()}"); - } + public static double MissionTime => Globals.MissionManagerInstance.MissionTime; /// /// Gets the current server FPS. /// - /// The current server FPS. public static double GetServerFPS() { return 1 / Time.unscaledDeltaTime; } /// - /// Stops the dedicated server. + /// Stops the dedicated server process. /// public static void StopServer() { Nuclei.Logger?.LogInfo("Stopping server..."); - - EndMission(); - - Task.Delay(3000).ContinueWith(_ => - { - Nuclei.Logger?.LogInfo("Server stopped."); - Application.Quit(); - }); - } - - /// - /// Ends the current mission and returns the server to the main menu state. - /// - public static void EndMission() - { - Nuclei.Logger?.LogInfo("Ending mission..."); - - if (!IsServerRunning) - { - Nuclei.Logger?.LogWarning("Server is not running."); - return; - } - - MessageService.SendHostEndedMessage(); - - Globals.NetworkManagerNuclearOptionInstance.Stop(false); - - SteamLobbyService.StopSteamLobby(); - - Nuclei.Logger?.LogInfo("Mission ended."); + ChatService.SendChatMessage("Server is stopping..."); + Application.Quit(); } /// - /// Select the next mission on the server, based on the server config's mission list and MissionSelectMode. + /// Mission restarts are handled by the official dedicated server runner. /// - public static void SelectNextMission() + public static void StartOrRestartLobby() { - Nuclei.Logger?.LogInfo("Selecting next mission..."); - - var mission = MissionService.GetNextMission(NucleiConfig.MissionSelectMode!.Value, NucleiConfig.UseAllMissions!.Value); - - if (mission == null) - { - Nuclei.Logger?.LogError("Failed to get a new mission."); - return; - } - - SelectMission(mission); - } - - /// - /// Select the given mission on the server. - /// - /// The mission to start. - public static void SelectMission(Mission mission) - { - Nuclei.Logger?.LogInfo($"Selected mission: {mission.Name}"); - - if (IsServerRunning) - { - Nuclei.Logger?.LogWarning("Server is already running."); - return; - } - - MissionService.SetMission(mission); - } - - /// - /// Creates a object based on the server config and the currently selected mission. - /// - /// The created object. - public static HostOptions CreateHostOptionsForCurrentMission() - { - return CreateHostOptions(MissionService.CurrentMission!); - } - - /// - /// Creates a object based on the server config and a given mission. - /// - /// The mission to create the object for. - /// The created object. - public static HostOptions CreateHostOptions(Mission mission) - { - return new HostOptions - { - SocketType = NucleiConfig.UseSteamSocket!.Value ? SocketType.Steam : SocketType.UDP, - MaxConnections = NucleiConfig.MaxPlayers!.Value, - Map = mission.MapKey, - UdpPort = NucleiConfig.UseSteamSocket!.Value ? null : NucleiConfig.UdpPort!.Value - }; - } - - /// - /// Starts a mission on the server, using the currently selected mission. - /// - public static async UniTask StartMission() - { - Nuclei.Logger?.LogInfo("Starting mission..."); - - if (IsServerRunning) - { - Nuclei.Logger?.LogWarning("Server is already running."); - return; - } - - await Globals.NetworkManagerNuclearOptionInstance.StartHostAsync(CreateHostOptionsForCurrentMission()); - - MissionEvents.OnNewMissionStarted(MissionService.CurrentMission!); - - Nuclei.Logger?.LogInfo("Mission started."); - } - - /// - /// Starts or restarts the lobby, selecting a random mission first. - /// - public static async UniTask StartOrRestartLobby() - { - if (IsServerRunning) - { - Nuclei.Logger?.LogInfo("Already running, ending mission first..."); - EndMission(); - await UniTask.WaitUntil(() => !Globals.NetworkManagerNuclearOptionInstance.stopping); - } - - GameManager.SetGameState(GameManager.GameState.Multiplayer); - - if (MissionService.TryGetConsumePreselectedMission(out var mission)) - SelectMission(mission!); - else - SelectNextMission(); - - await SteamLobbyService.StartSteamLobby(); - await StartMission(); - await SteamLobbyService.SetPingData(); - SteamLobbyService.SetLobbyData(); - - ApplyFramerateSettings(); - ApplyExperimentalSettings(); - - if (NucleiConfig.MuteAfterStart!.Value) - DisableAudio(); - - Resources.UnloadUnusedAssets(); - } - - private static void DisableAudio() - { - Nuclei.Logger?.LogDebug("Disabling audio..."); - - Globals.AudioMixerVolumeInstance.ChangeMixerVolume(AudioMixerVolume.Master, 0); - - Nuclei.Logger?.LogDebug("Audio disabled."); - } - - private static void ApplyFramerateSettings() - { - QualitySettings.SetQualityLevel(0, true); - QualitySettings.maxQueuedFrames = 0; - QualitySettings.vSyncCount = 0; - Application.targetFrameRate = NucleiConfig.TargetFrameRate!.Value; - } - - private static void ApplyExperimentalSettings() - { - if (NucleiConfig.UseUpdateForPhysicsUpdate!.Value) - Physics.simulationMode = SimulationMode.Update; - - if (NucleiConfig.PhysicsUpdatesPerSecond!.Value != 60) - Time.fixedDeltaTime = 1f / NucleiConfig.PhysicsUpdatesPerSecond!.Value; - } - - private static bool IsSteamManagerInitializedOrTimeout(float startTime) - { - return SteamManager.Initialized || Time.time - startTime > 10; - } - - /// - /// Starts the dedicated server. - /// - public static async UniTask StartServer() - { - if (IsServerRunning) - { - Nuclei.Logger?.LogWarning("Server is already running."); - return; - } - - Nuclei.Logger?.LogInfo("Starting server..."); - - Nuclei.Logger?.LogDebug("Checking Steam API availability..."); - - if (!SteamLobbyService.IsSteamAPIAvailable()) - { - Nuclei.Logger?.LogError("Steam API is not available! Aborting server launch."); - return; - } - - Nuclei.Logger?.LogDebug("Waiting for SteamManager to initialize..."); - - var startTime = Time.time; - await UniTask.WaitUntil(() => IsSteamManagerInitializedOrTimeout(startTime)); - - if (!SteamManager.Initialized) - { - Nuclei.Logger?.LogError("SteamManager failed to initialize within 10 seconds! Aborting server launch."); - return; - } - - Nuclei.Logger?.LogDebug("Validating mission config..."); - - if (!MissionService.ValidateMissionConfig()) - { - Nuclei.Logger?.LogError("Failed to validate mission config! Aborting server launch."); - return; - } - - await StartOrRestartLobby(); - - TimeService.Initialize(); - - TimeEvents.EveryMinute += CheckSendMotD; - TimeEvents.EveryMinute += CheckMissionOverTime; - TimeEvents.Every10Minutes += TimeStatus; - MissionEvents.MissionEnded += OnMissionEnded; - - if (NucleiConfig.RefreshServerNamePeriodically!.Value) - { - Nuclei.Logger?.LogDebug("Hooked up periodic server name refresh event."); - TimeEvents.Every10Minutes += SteamLobbyService.SetLobbyData; - } - - Nuclei.Logger?.LogInfo($"Server started. (Took {Time.time} seconds)"); + Nuclei.Logger?.LogWarning("Mission restart is not implemented for the official dedicated server build."); } -} \ No newline at end of file +} diff --git a/Nuclei/Features/SteamLobbyService.cs b/Nuclei/Features/SteamLobbyService.cs index 3329ef1..5e17678 100644 --- a/Nuclei/Features/SteamLobbyService.cs +++ b/Nuclei/Features/SteamLobbyService.cs @@ -1,84 +1,41 @@ using Cysharp.Threading.Tasks; -using Nuclei.Helpers; -using Steamworks; -using UnityEngine; namespace Nuclei.Features; /// -/// Steam lobby wrapper service for Nuclei. +/// Compatibility shim. Steam server advertisement is handled by the official dedicated server. /// public static class SteamLobbyService { - private const string KeyHostAddress = "HostAddress"; - private const string KeyName = "name"; - private const string KeyVersion = "version"; - private const string KeyHostPing = "HostPing"; - - /// - /// Starts a Steam lobby. - /// - public static async UniTask StartSteamLobby() + public static UniTask StartSteamLobby() { - Nuclei.Logger?.LogInfo("Starting Steam lobby..."); - - var result = await Globals.SteamLobbyInstance.Steam_CreateLobbyAsync(NucleiConfig.LobbyType!.Value, - NucleiConfig.MaxPlayers!.Value); - - if (result.m_eResult != EResult.k_EResultOK) - { - Nuclei.Logger?.LogError("Failed to create Steam lobby."); - return; - } - - Nuclei.Logger?.LogInfo("Steam lobby started."); + Nuclei.Logger?.LogDebug("Skipping Nuclei Steam lobby creation; official dedicated server owns advertisement."); + return UniTask.CompletedTask; } + internal static void SetLobbyData() { - SteamMatchmaking.SetLobbyData(Globals.LobbySteamID, KeyHostAddress, SteamUser.GetSteamID().ToString()); - SteamMatchmaking.SetLobbyData(Globals.LobbySteamID, KeyVersion, Application.version); - UpdateLobbyName(); + Nuclei.Logger?.LogDebug("Skipping Nuclei Steam lobby data update; official dedicated server owns advertisement."); } - /// - /// Updates the lobby name. - /// - /// It is useful to re-run this periodically in case of dynamic placeholders. public static void UpdateLobbyName() { - SteamMatchmaking.SetLobbyData(Globals.LobbySteamID, KeyName, DynamicPlaceholderUtils.ReplaceDynamicPlaceholders(NucleiConfig.ServerName!.Value)); + Nuclei.Logger?.LogDebug("Skipping Nuclei Steam lobby name update; official dedicated server owns advertisement."); } - internal static async UniTask SetPingData() + internal static UniTask SetPingData() { - if (!await Globals.SteamLobbyInstance.WaitForLocalLocation()) - { - Nuclei.Logger?.LogError("Failed to await Steam Lobby local location fetch for ping data. Ping will be empty."); - return; - } - if (SteamLobby.GetLocalLocation(out var location)) - SteamMatchmaking.SetLobbyData(Globals.LobbySteamID, KeyHostPing, location); - else - Nuclei.Logger?.LogError("Failed to get local location for ping data. Ping will be empty."); + Nuclei.Logger?.LogDebug("Skipping Nuclei Steam lobby ping data update; official dedicated server owns advertisement."); + return UniTask.CompletedTask; } - - /// - /// Stops the Steam lobby. - /// + public static void StopSteamLobby() { - Nuclei.Logger?.LogDebug("Stopping Steam lobby..."); - SteamMatchmaking.LeaveLobby(Globals.LobbySteamID); - Nuclei.Logger?.LogDebug("Steam lobby stopped."); + Nuclei.Logger?.LogDebug("Skipping Nuclei Steam lobby shutdown; official dedicated server owns advertisement."); } - - /// - /// Checks if the Steam API is available. - /// - /// True if the Steam API is available, false otherwise. + public static bool IsSteamAPIAvailable() { - return SteamAPI.IsSteamRunning(); + return true; } - -} \ No newline at end of file +} diff --git a/Nuclei/GlobalUsings.cs b/Nuclei/GlobalUsings.cs new file mode 100644 index 0000000..53d74f7 --- /dev/null +++ b/Nuclei/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using NuclearOption.Chat; +global using NuclearOption.Networking; +global using NuclearOption.Networking.Lobbies; diff --git a/Nuclei/Helpers/Globals.cs b/Nuclei/Helpers/Globals.cs index 7121e5b..0f68fe2 100644 --- a/Nuclei/Helpers/Globals.cs +++ b/Nuclei/Helpers/Globals.cs @@ -34,7 +34,7 @@ public static class Globals /// /// Gets the local player. (The host / server) /// - public static Player LocalPlayer => GameManager.LocalPlayer ?? throw new NullReferenceException("Local player is null."); + public static Player LocalPlayer => GameManager.GetLocalPlayer(out var localPlayer) ? localPlayer : throw new NullReferenceException("Local player is null."); /// /// Gets the instance of the class. @@ -49,10 +49,10 @@ public static class Globals /// /// Gets the Steam ID of the lobby. /// - public static CSteamID LobbySteamID => SteamLobbyInstance._currentLobbyID; + public static CSteamID LobbySteamID => throw new NotSupportedException("Nuclei no longer manages Steam lobbies on the official dedicated server."); /// /// Get a read-only list of all authenticated players. /// public static IReadOnlyList AuthenticatedPlayers => NetworkManagerNuclearOptionInstance.Server.AuthenticatedPlayers; -} \ No newline at end of file +} diff --git a/Nuclei/Patches/CSteamAPIContextPatches.cs b/Nuclei/Patches/CSteamAPIContextPatches.cs index b5f78de..7314d71 100644 --- a/Nuclei/Patches/CSteamAPIContextPatches.cs +++ b/Nuclei/Patches/CSteamAPIContextPatches.cs @@ -1,6 +1,4 @@ -using System; using HarmonyLib; -using Nuclei.Features; using Steamworks; namespace Nuclei.Patches; @@ -14,13 +12,6 @@ internal static class CSteamAPIContextPatches [HarmonyPatch(nameof(CSteamAPIContext.Init))] private static void InitPostfix() { - try - { - _ = Server.StartServer(); - } - catch (Exception e) - { - Nuclei.Logger?.LogError($"Aborting server launch: Failed to start the server. For more information, see this error trace:\n{e}"); - } + Nuclei.Logger?.LogInfo("Steam API context initialized; attaching Nuclei to the official dedicated server."); } -} \ No newline at end of file +} diff --git a/Nuclei/Patches/ChatManagerPatches.cs b/Nuclei/Patches/ChatManagerPatches.cs index 856756b..2680787 100644 --- a/Nuclei/Patches/ChatManagerPatches.cs +++ b/Nuclei/Patches/ChatManagerPatches.cs @@ -12,8 +12,8 @@ namespace Nuclei.Patches; internal static class ChatManagerPatches { [HarmonyPrefix] - [HarmonyPatch(nameof(ChatManager.UserCode_CmdSendChatMessage_1323305531))] - private static bool UserCode_CmdSendChatMessage_1323305531Prefix(string message, bool allChat, INetworkPlayer sender) + [HarmonyPatch("UserCode_CmdSendChatMessage_-456754112")] + private static bool UserCode_CmdSendChatMessagePrefix(string message, bool allChat, INetworkPlayer sender) { if (!sender.TryGetPlayer(out var player)) Nuclei.Logger?.LogWarning("Player component is null"); @@ -28,4 +28,4 @@ private static bool UserCode_CmdSendChatMessage_1323305531Prefix(string message, return true; } -} \ No newline at end of file +}