diff --git a/Robust.Client/ContentStart.cs b/Robust.Client/ContentStart.cs index c1c0bb93a..5e947233e 100644 --- a/Robust.Client/ContentStart.cs +++ b/Robust.Client/ContentStart.cs @@ -7,13 +7,13 @@ namespace Robust.Client #if FULL_RELEASE throw new System.InvalidOperationException("ContentStart.Start is not available on a full release."); #else - GameController.Start(args, true); + GameController.Start(args, new GameControllerOptions(), true); #endif } public static void StartLibrary(string[] args, GameControllerOptions options) { - GameController.Start(args, true, null, options); + GameController.Start(args, options, true, null); } } } diff --git a/Robust.Client/GameController/GameController.Loader.cs b/Robust.Client/GameController/GameController.Loader.cs index 69ef8e03b..403e45c34 100644 --- a/Robust.Client/GameController/GameController.Loader.cs +++ b/Robust.Client/GameController/GameController.Loader.cs @@ -11,7 +11,7 @@ namespace Robust.Client { public void Main(IMainArgs args) { - Start(args.Args, contentStart: false, args); + Start(args.Args, new GameControllerOptions(), contentStart: false, args); } } } diff --git a/Robust.Client/GameController/GameController.Standalone.cs b/Robust.Client/GameController/GameController.Standalone.cs index 6970dd489..b8d264b83 100644 --- a/Robust.Client/GameController/GameController.Standalone.cs +++ b/Robust.Client/GameController/GameController.Standalone.cs @@ -22,10 +22,10 @@ namespace Robust.Client public static void Main(string[] args) { - Start(args); + Start(args, new GameControllerOptions()); } - public static void Start(string[] args, bool contentStart = false, IMainArgs? loaderArgs=null, GameControllerOptions? options = null) + public static void Start(string[] args, GameControllerOptions options, bool contentStart = false, IMainArgs? loaderArgs=null) { if (_hasStarted) { @@ -40,7 +40,7 @@ namespace Robust.Client } } - private static void ParsedMain(CommandLineArgs args, bool contentStart, IMainArgs? loaderArgs, GameControllerOptions? options) + private static void ParsedMain(CommandLineArgs args, bool contentStart, IMainArgs? loaderArgs, GameControllerOptions options) { IoCManager.InitThread(); @@ -51,15 +51,13 @@ namespace Robust.Client var gc = IoCManager.Resolve(); gc.SetCommandLineArgs(args); gc._loaderArgs = loaderArgs; - if(options != null) - gc.Options = options; // When the game is ran with the startup executable being content, // we have to disable the separate load context. // Otherwise the content assemblies will be loaded twice which causes *many* fun bugs. gc.ContentStart = contentStart; - gc.Run(mode); + gc.Run(mode, options); } public void OverrideMainLoop(IGameLoop gameLoop) @@ -67,9 +65,9 @@ namespace Robust.Client _mainLoop = gameLoop; } - public void Run(DisplayMode mode, Func? logHandlerFactory = null) + public void Run(DisplayMode mode, GameControllerOptions options, Func? logHandlerFactory = null) { - if (!StartupSystemSplash(logHandlerFactory)) + if (!StartupSystemSplash(options, logHandlerFactory)) { Logger.Fatal("Failed to start game controller!"); return; diff --git a/Robust.Client/GameController/GameController.cs b/Robust.Client/GameController/GameController.cs index 9498f9921..64ce62c22 100644 --- a/Robust.Client/GameController/GameController.cs +++ b/Robust.Client/GameController/GameController.cs @@ -75,8 +75,6 @@ namespace Robust.Client public GameControllerOptions Options { get; private set; } = new(); public InitialLaunchState LaunchState { get; private set; } = default!; - public bool LoadConfigAndUserData { get; set; } = true; - public void SetCommandLineArgs(CommandLineArgs args) { _commandLineArgs = args; @@ -95,7 +93,7 @@ namespace Robust.Client _modLoader.SetUseLoadContext(!ContentStart); _modLoader.SetEnableSandboxing(Options.Sandboxing); - if (!_modLoader.TryLoadModulesFrom(new ResourcePath("/Assemblies/"), Options.ContentModulePrefix)) + if (!_modLoader.TryLoadModulesFrom(Options.AssemblyDirectory, Options.ContentModulePrefix)) { Logger.Fatal("Errors while loading content assemblies."); return false; @@ -196,8 +194,9 @@ namespace Robust.Client return true; } - internal bool StartupSystemSplash(Func? logHandlerFactory) + internal bool StartupSystemSplash(GameControllerOptions options, Func? logHandlerFactory) { + Options = options; ReadInitialLaunchState(); SetupLogging(_logManager, logHandlerFactory ?? (() => new ConsoleLogHandler())); @@ -234,7 +233,7 @@ namespace Robust.Client _configurationManager.LoadCVarsFromAssembly(typeof(GameController).Assembly); // Client _configurationManager.LoadCVarsFromAssembly(typeof(IConfigurationManager).Assembly); // Shared - if (LoadConfigAndUserData) + if (Options.LoadConfigAndUserData) { var configFile = Path.Combine(userDataDir, Options.ConfigFileName); if (File.Exists(configFile)) @@ -258,13 +257,13 @@ namespace Robust.Client ProfileOptSetup.Setup(_configurationManager); - _resourceCache.Initialize(LoadConfigAndUserData ? userDataDir : null); + _resourceCache.Initialize(Options.LoadConfigAndUserData ? userDataDir : null); var mountOptions = _commandLineArgs != null ? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions; - ProgramShared.DoMounts(_resourceCache, mountOptions, Options.ContentBuildDirectory, - _loaderArgs != null && !Options.ResourceMountDisabled, ContentStart); + ProgramShared.DoMounts(_resourceCache, mountOptions, Options.ContentBuildDirectory, Options.AssemblyDirectory, + Options.LoadContentResources, _loaderArgs != null && !Options.ResourceMountDisabled, ContentStart); if (_loaderArgs != null) { diff --git a/Robust.Client/GameControllerOptions.cs b/Robust.Client/GameControllerOptions.cs index ff4e23a37..1a6937751 100644 --- a/Robust.Client/GameControllerOptions.cs +++ b/Robust.Client/GameControllerOptions.cs @@ -42,6 +42,11 @@ namespace Robust.Client /// public string ContentBuildDirectory { get; init; } = "Content.Client"; + /// + /// Directory to load all assemblies from. + /// + public ResourcePath AssemblyDirectory { get; init; } = new(@"/Assemblies/"); + /// /// Directory to load all prototypes from. /// @@ -52,6 +57,16 @@ namespace Robust.Client /// public bool ResourceMountDisabled { get; init; } = false; + /// + /// Whether to mount content resources when not on FULL_RELEASE. + /// + public bool LoadContentResources { get; init; } = true; + + /// + /// Whether to load config and user data. + /// + public bool LoadConfigAndUserData { get; init; } = true; + /// /// Whether to disable command line args server auto-connecting. /// diff --git a/Robust.Client/IGameControllerInternal.cs b/Robust.Client/IGameControllerInternal.cs index 520dbfd32..7f16dac2f 100644 --- a/Robust.Client/IGameControllerInternal.cs +++ b/Robust.Client/IGameControllerInternal.cs @@ -10,8 +10,7 @@ namespace Robust.Client GameControllerOptions Options { get; } bool ContentStart { get; set; } void SetCommandLineArgs(CommandLineArgs args); - bool LoadConfigAndUserData { get; set; } - void Run(GameController.DisplayMode mode, Func? logHandlerFactory = null); + void Run(GameController.DisplayMode mode, GameControllerOptions options, Func? logHandlerFactory = null); void KeyDown(KeyEventArgs keyEvent); void KeyUp(KeyEventArgs keyEvent); void TextEntered(TextEventArgs textEvent); diff --git a/Robust.Server/BaseServer.cs b/Robust.Server/BaseServer.cs index 06902fc27..157741483 100644 --- a/Robust.Server/BaseServer.cs +++ b/Robust.Server/BaseServer.cs @@ -102,6 +102,8 @@ namespace Robust.Server private readonly ManualResetEventSlim _shutdownEvent = new(false); + public ServerOptions Options { get; private set; } = new(); + /// public int MaxPlayers => _config.GetCVar(CVars.GameMaxPlayers); @@ -114,7 +116,7 @@ namespace Robust.Server Logger.InfoS("srv", "Restarting Server..."); Cleanup(); - Start(_logHandlerFactory); + Start(Options, _logHandlerFactory); } /// @@ -141,15 +143,16 @@ namespace Robust.Server } /// - public bool Start(Func? logHandlerFactory = null) + public bool Start(ServerOptions options, Func? logHandlerFactory = null) { + Options = options; var profilePath = Path.Join(Environment.CurrentDirectory, "AAAAAAAA"); ProfileOptimization.SetProfileRoot(profilePath); ProfileOptimization.StartProfile("AAAAAAAAAA"); _config.Initialize(true); - if (LoadConfigAndUserData) + if (Options.LoadConfigAndUserData) { // Sets up the configMgr // If a config file path was passed, use it literally. @@ -269,22 +272,26 @@ namespace Robust.Server return true; } - var dataDir = LoadConfigAndUserData + var dataDir = Options.LoadConfigAndUserData ? _commandLineArgs?.DataDir ?? PathHelpers.ExecutableRelativeFile("data") : null; // Set up the VFS _resources.Initialize(dataDir); - ProgramShared.DoMounts(_resources, _commandLineArgs?.MountOptions, "Content.Server", contentStart:ContentStart); + var mountOptions = _commandLineArgs != null + ? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions; + + ProgramShared.DoMounts(_resources, mountOptions, Options.ContentBuildDirectory, Options.AssemblyDirectory, + Options.LoadContentResources, Options.ResourceMountDisabled, ContentStart); // When the game is ran with the startup executable being content, // we have to disable the separate load context. // Otherwise the content assemblies will be loaded twice which causes *many* fun bugs. _modLoader.SetUseLoadContext(!ContentStart); - _modLoader.SetEnableSandboxing(false); + _modLoader.SetEnableSandboxing(Options.Sandboxing); - if (!_modLoader.TryLoadModulesFrom(new ResourcePath("/Assemblies/"), "Content.")) + if (!_modLoader.TryLoadModulesFrom(Options.AssemblyDirectory, Options.ContentModulePrefix)) { Logger.Fatal("Errors while loading content assemblies."); return true; @@ -335,7 +342,7 @@ namespace Robust.Server // otherwise the prototypes will be cleared var prototypeManager = IoCManager.Resolve(); prototypeManager.Initialize(); - prototypeManager.LoadDirectory(new ResourcePath(@"/Prototypes")); + prototypeManager.LoadDirectory(Options.PrototypeDirectory); prototypeManager.Resync(); IoCManager.Resolve().Initialize(); @@ -492,7 +499,6 @@ namespace Robust.Server } public bool ContentStart { get; set; } - public bool LoadConfigAndUserData { private get; set; } = true; public void OverrideMainLoop(IGameLoop gameLoop) { diff --git a/Robust.Server/ContentStart.cs b/Robust.Server/ContentStart.cs index 64ec566c6..22c962c7d 100644 --- a/Robust.Server/ContentStart.cs +++ b/Robust.Server/ContentStart.cs @@ -7,13 +7,13 @@ namespace Robust.Server #if FULL_RELEASE throw new System.InvalidOperationException("ContentStart.Start is not available on a full release."); #else - Program.Start(args, true); + Program.Start(args, new ServerOptions(), true); #endif } - public static void StartLibrary(string[] args) + public static void StartLibrary(string[] args, ServerOptions options) { - Program.Start(args, true); + Program.Start(args, options, true); } } } diff --git a/Robust.Server/IBaseServer.cs b/Robust.Server/IBaseServer.cs index 08b01ae10..b9ad666f8 100644 --- a/Robust.Server/IBaseServer.cs +++ b/Robust.Server/IBaseServer.cs @@ -23,7 +23,7 @@ namespace Robust.Server /// Sets up the server, loads the game, gets ready for client connections. /// /// - bool Start(Func? logHandler = null); + bool Start(ServerOptions options, Func? logHandler = null); /// /// Hard restarts the server, shutting it down, kicking all players, and starting the server again. @@ -44,11 +44,10 @@ namespace Robust.Server internal interface IBaseServerInternal : IBaseServer { + ServerOptions Options { get; } bool ContentStart { set; } - bool LoadConfigAndUserData { set; } void OverrideMainLoop(IGameLoop gameLoop); - void SetCommandLineArgs(CommandLineArgs args); } } diff --git a/Robust.Server/Program.cs b/Robust.Server/Program.cs index 60878d022..cbd3b0e3d 100644 --- a/Robust.Server/Program.cs +++ b/Robust.Server/Program.cs @@ -21,10 +21,10 @@ namespace Robust.Server internal static void Main(string[] args) { - Start(args); + Start(args, new ServerOptions()); } - internal static void Start(string[] args, bool contentStart = false) + internal static void Start(string[] args, ServerOptions options, bool contentStart = false) { if (_hasStarted) { @@ -47,11 +47,11 @@ namespace Robust.Server TaskCreationOptions.LongRunning, TaskContinuationOptions.None, RobustTaskScheduler.Instance - ).StartNew(() => ParsedMain(parsed, contentStart)) + ).StartNew(() => ParsedMain(parsed, contentStart, options)) .GetAwaiter().GetResult(); } - private static void ParsedMain(CommandLineArgs args, bool contentStart) + private static void ParsedMain(CommandLineArgs args, bool contentStart, ServerOptions options) { Thread.CurrentThread.Name = "Main Thread"; IoCManager.InitThread(); @@ -67,7 +67,7 @@ namespace Robust.Server Logger.Info("Server -> Starting"); - if (server.Start()) + if (server.Start(options)) { Logger.Fatal("Server -> Can not start server"); //Not like you'd see this, haha. Perhaps later for logging. diff --git a/Robust.Server/ServerOptions.cs b/Robust.Server/ServerOptions.cs new file mode 100644 index 000000000..a35964bbb --- /dev/null +++ b/Robust.Server/ServerOptions.cs @@ -0,0 +1,54 @@ +using Robust.Shared; +using Robust.Shared.Utility; + +namespace Robust.Server +{ + public class ServerOptions + { + /// + /// Whether content sandboxing will be enabled & enforced. + /// + public bool Sandboxing { get; init; } = false; + + // TODO: Expose mounting methods to games using Robust as a library. + /// + /// Lists of mount options to mount. + /// + public MountOptions MountOptions { get; init; } = new(); + + /// + /// Assemblies with this prefix will be loaded. + /// + public string ContentModulePrefix { get; init; } = "Content."; + + /// + /// Name of the content build directory, for game pack mounting purposes. + /// + public string ContentBuildDirectory { get; init; } = "Content.Server"; + + /// + /// Directory to load all assemblies from. + /// + public ResourcePath AssemblyDirectory { get; init; } = new(@"/Assemblies/"); + + /// + /// Directory to load all prototypes from. + /// + public ResourcePath PrototypeDirectory { get; init; } = new(@"/Prototypes/"); + + /// + /// Whether to disable mounting the "Resources/" folder on FULL_RELEASE. + /// + public bool ResourceMountDisabled { get; init; } = false; + + /// + /// Whether to mount content resources when not on FULL_RELEASE. + /// + public bool LoadContentResources { get; init; } = true; + + /// + /// Whether to load config and user data. + /// + public bool LoadConfigAndUserData { get; init; } = true; + } +} diff --git a/Robust.Shared/ProgramShared.cs b/Robust.Shared/ProgramShared.cs index 70bb49f8b..b6fbdd6b1 100644 --- a/Robust.Shared/ProgramShared.cs +++ b/Robust.Shared/ProgramShared.cs @@ -12,6 +12,11 @@ namespace Robust.Shared { return contentStart ? "../../" : "../../../"; } + + private static string FindEngineRootDir(bool contentStart) + { + return contentStart ? "../../RobustToolbox/" : "../../"; + } #endif internal static void PrintRuntimeInfo(ISawmill sawmill) @@ -20,17 +25,21 @@ namespace Robust.Shared sawmill.Debug($"OS: {RuntimeInformation.OSDescription} {RuntimeInformation.OSArchitecture}"); } - internal static void DoMounts(IResourceManagerInternal res, MountOptions? options, string contentBuildDir, + internal static void DoMounts(IResourceManagerInternal res, MountOptions? options, string contentBuildDir, ResourcePath assembliesPath, bool loadContentResources = true, bool loader = false, bool contentStart = false) { #if FULL_RELEASE if (!loader) res.MountContentDirectory(@"Resources/"); #else - var contentRootDir = FindContentRootDir(contentStart); - res.MountContentDirectory($@"{contentRootDir}RobustToolbox/Resources/"); - res.MountContentDirectory($@"{contentRootDir}bin/{contentBuildDir}/", new ResourcePath("/Assemblies/")); - res.MountContentDirectory($@"{contentRootDir}Resources/"); + res.MountContentDirectory($@"{FindEngineRootDir(contentStart)}Resources/"); + + if (loadContentResources) + { + var contentRootDir = FindContentRootDir(contentStart); + res.MountContentDirectory($@"{contentRootDir}bin/{contentBuildDir}/", assembliesPath); + res.MountContentDirectory($@"{contentRootDir}Resources/"); + } #endif if (options == null) diff --git a/Robust.UnitTesting/GameControllerDummy.cs b/Robust.UnitTesting/GameControllerDummy.cs index 63d769f12..c88279379 100644 --- a/Robust.UnitTesting/GameControllerDummy.cs +++ b/Robust.UnitTesting/GameControllerDummy.cs @@ -33,7 +33,7 @@ namespace Robust.UnitTesting public string? ContentRootDir { get; set; } - public void Run(GameController.DisplayMode mode, Func? logHandlerFactory = null) + public void Run(GameController.DisplayMode mode, GameControllerOptions options, Func? logHandlerFactory = null) { } diff --git a/Robust.UnitTesting/RobustIntegrationTest.cs b/Robust.UnitTesting/RobustIntegrationTest.cs index 2cc4617a6..e66f91f49 100644 --- a/Robust.UnitTesting/RobustIntegrationTest.cs +++ b/Robust.UnitTesting/RobustIntegrationTest.cs @@ -17,6 +17,7 @@ using Robust.Shared; using Robust.Shared.Asynchronous; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; +using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Network; @@ -422,7 +423,15 @@ namespace Robust.UnitTesting var server = DependencyCollection.Resolve(); - server.LoadConfigAndUserData = false; + var serverOptions = _options != null ? _options.Options : new ServerOptions() + { + LoadConfigAndUserData = false, + LoadContentResources = false, + }; + + // Autoregister components if options are null or we're NOT starting from content. + if(!_options?.ContentStart ?? true) + IoCManager.Resolve().DoAutoRegistrations(); if (_options?.ContentAssemblies != null) { @@ -447,7 +456,7 @@ namespace Robust.UnitTesting var failureLevel = _options == null ? LogLevel.Error : _options.FailureLogLevel; server.ContentStart = _options?.ContentStart ?? false; - if (server.Start(() => new TestLogHandler("SERVER", failureLevel))) + if (server.Start(serverOptions, () => new TestLogHandler("SERVER", failureLevel))) { throw new Exception("Server failed to start."); } @@ -535,13 +544,21 @@ namespace Robust.UnitTesting var client = DependencyCollection.Resolve(); + var clientOptions = _options != null ? _options.Options : new GameControllerOptions() + { + LoadContentResources = false, + LoadConfigAndUserData = false, + }; + + // Autoregister components if options are null or we're NOT starting from content. + if(!_options?.ContentStart ?? true) + IoCManager.Resolve().DoAutoRegistrations(); + if (_options?.ContentAssemblies != null) { IoCManager.Resolve().Assemblies = _options.ContentAssemblies; } - client.LoadConfigAndUserData = false; - var cfg = IoCManager.Resolve(); if (_options != null) @@ -556,15 +573,25 @@ namespace Robust.UnitTesting } } - cfg.OverrideConVars(new[] {(CVars.NetPredictLagBias.Name, "0")}); + cfg.OverrideConVars(new[] + { + (CVars.NetPredictLagBias.Name, "0"), + + // Connecting to Discord is a massive waste of time. + // Basically just makes the CI logs a mess. + (CVars.DiscordEnabled.Name, "false"), + + // Avoid preloading textures. + (CVars.TexturePreloadingEnabled.Name, "false"), + }); GameLoop = new IntegrationGameLoop(DependencyCollection.Resolve(), _fromInstanceWriter, _toInstanceReader); var failureLevel = _options == null ? LogLevel.Error : _options.FailureLogLevel; client.OverrideMainLoop(GameLoop); - client.ContentStart = true; - client.StartupSystemSplash(() => new TestLogHandler("CLIENT", failureLevel)); + client.ContentStart = _options?.ContentStart ?? false; + client.StartupSystemSplash(clientOptions, () => new TestLogHandler("CLIENT", failureLevel)); client.StartupContinue(GameController.DisplayMode.Headless); GameLoop.RunInit(); @@ -674,10 +701,20 @@ namespace Robust.UnitTesting public class ServerIntegrationOptions : IntegrationOptions { + public virtual ServerOptions Options { get; set; } = new() + { + LoadConfigAndUserData = false, + LoadContentResources = false, + }; } public class ClientIntegrationOptions : IntegrationOptions { + public virtual GameControllerOptions Options { get; set; } = new() + { + LoadContentResources = false, + LoadConfigAndUserData = false, + }; } public abstract class IntegrationOptions diff --git a/Robust.UnitTesting/Shared/EngineIntegrationTest_Test.cs b/Robust.UnitTesting/Shared/EngineIntegrationTest_Test.cs new file mode 100644 index 000000000..d63c0f672 --- /dev/null +++ b/Robust.UnitTesting/Shared/EngineIntegrationTest_Test.cs @@ -0,0 +1,75 @@ +using System.Threading.Tasks; +using NUnit.Framework; +using Robust.Shared.Enums; +using Robust.Shared.IoC; +using Robust.Shared.Network; +using IPlayerManager = Robust.Server.Player.IPlayerManager; + +namespace Robust.UnitTesting.Shared +{ + [TestFixture] + public class EngineIntegrationTest_Test : RobustIntegrationTest + { + [Test] + public void ServerStartsCorrectlyTest() + { + ServerIntegrationInstance? server = null; + Assert.DoesNotThrow(() => server = StartServer()); + Assert.That(server, Is.Not.Null); + } + + [Test] + public void ClientStartsCorrectlyTest() + { + ClientIntegrationInstance? client = null; + Assert.DoesNotThrow(() => client = StartClient()); + Assert.That(client, Is.Not.Null); + } + + [Test] + public async Task ServerClientPairConnectCorrectlyTest() + { + var server = StartServer(); + var client = StartClient(); + + Assert.That(server, Is.Not.Null); + Assert.That(client, Is.Not.Null); + + await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + + // Connect client to the server... + Assert.DoesNotThrow(() => client.SetConnectTarget(server)); + client.Post(() => IoCManager.Resolve().ClientConnect(null!, 0, null!)); + + // Run 10 synced ticks... + for (int i = 0; i < 10; i++) + { + await server.WaitRunTicks(1); + await client.WaitRunTicks(1); + } + + await server.WaitAssertion(() => + { + var playerManager = IoCManager.Resolve(); + + // There must be a player connected. + Assert.That(playerManager.PlayerCount, Is.EqualTo(1)); + + // Get the only player... + var player = playerManager.GetAllPlayers()[0]; + + Assert.That(player.Status, Is.EqualTo(SessionStatus.Connected)); + Assert.That(player.ConnectedClient.IsConnected, Is.True); + }); + + await client.WaitAssertion(() => + { + var netManager = IoCManager.Resolve(); + + Assert.That(netManager.IsConnected, Is.True); + Assert.That(netManager.ServerChannel, Is.Not.Null); + Assert.That(netManager.ServerChannel!.IsConnected, Is.True); + }); + } + } +}