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
+}