diff --git a/Robust.Client/ContentStart.cs b/Robust.Client/ContentStart.cs index b54a49639..c1c0bb93a 100644 --- a/Robust.Client/ContentStart.cs +++ b/Robust.Client/ContentStart.cs @@ -5,10 +5,15 @@ namespace Robust.Client public static void Start(string[] args) { #if FULL_RELEASE - throw new System.InvalidOperationException("ContentStart is not available on a full release."); + throw new System.InvalidOperationException("ContentStart.Start is not available on a full release."); #else GameController.Start(args, true); #endif } + + public static void StartLibrary(string[] args, GameControllerOptions options) + { + GameController.Start(args, true, null, options); + } } } diff --git a/Robust.Client/GameController.cs b/Robust.Client/GameController.cs index a71712def..e1b5bccc2 100644 --- a/Robust.Client/GameController.cs +++ b/Robust.Client/GameController.cs @@ -61,18 +61,18 @@ namespace Robust.Client [Dependency] private readonly IFontManagerInternal _fontManager = default!; [Dependency] private readonly IModLoaderInternal _modLoader = default!; [Dependency] private readonly IScriptClient _scriptClient = default!; - [Dependency] private readonly IComponentManager _componentManager = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IRobustMappedStringSerializer _stringSerializer = default!; [Dependency] private readonly IAuthManager _authManager = default!; [Dependency] private readonly IMidiManager _midiManager = default!; [Dependency] private readonly IEyeManager _eyeManager = default!; private CommandLineArgs? _commandLineArgs; - private bool _disableAssemblyLoadContext; + // Arguments for loader-load. Not used otherwise. private IMainArgs? _loaderArgs; + public bool ContentStart { get; set; } = false; + public GameControllerOptions Options { get; private set; } = new(); public InitialLaunchState LaunchState { get; private set; } = default!; public bool LoadConfigAndUserData { get; set; } = true; @@ -89,10 +89,10 @@ namespace Robust.Client // Disable load context usage on content start. // This prevents Content.Client being loaded twice and things like csi blowing up because of it. - _modLoader.SetUseLoadContext(!_disableAssemblyLoadContext); - _modLoader.SetEnableSandboxing(true); + _modLoader.SetUseLoadContext(!ContentStart); + _modLoader.SetEnableSandboxing(Options.Sandboxing); - if (!_modLoader.TryLoadModulesFrom(new ResourcePath("/Assemblies/"), "Content.")) + if (!_modLoader.TryLoadModulesFrom(new ResourcePath("/Assemblies/"), Options.ContentModulePrefix)) { Logger.Fatal("Errors while loading content assemblies."); return false; @@ -118,7 +118,7 @@ namespace Robust.Client _inputManager.Initialize(); _console.Initialize(); _prototypeManager.Initialize(); - _prototypeManager.LoadDirectory(new ResourcePath(@"/Prototypes/")); + _prototypeManager.LoadDirectory(Options.PrototypeDirectory); _prototypeManager.Resync(); _mapManager.Initialize(); _entityManager.Initialize(); @@ -143,7 +143,8 @@ namespace Robust.Client _clyde.Ready(); - if ((_commandLineArgs?.Connect == true || _commandLineArgs?.Launcher == true) + if (!Options.DisableCommandLineConnect && + (_commandLineArgs?.Connect == true || _commandLineArgs?.Launcher == true) && LaunchState.ConnectEndpoint != null) { _client.ConnectToServer(LaunchState.ConnectEndpoint); @@ -172,7 +173,7 @@ namespace Robust.Client if (LoadConfigAndUserData) { - var configFile = Path.Combine(userDataDir, "client_config.toml"); + var configFile = Path.Combine(userDataDir, Options.ConfigFileName); if (File.Exists(configFile)) { // Load config from user data if available. @@ -196,7 +197,12 @@ namespace Robust.Client _resourceCache.Initialize(LoadConfigAndUserData ? userDataDir : null); - ProgramShared.DoMounts(_resourceCache, _commandLineArgs?.MountOptions, "Content.Client", _loaderArgs != null); + var mountOptions = _commandLineArgs != null + ? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions; + + ProgramShared.DoMounts(_resourceCache, mountOptions, Options.ContentBuildDirectory, + _loaderArgs != null && !Options.ResourceMountDisabled, ContentStart); + if (_loaderArgs != null) { _stringSerializer.EnableCaching = false; @@ -217,7 +223,7 @@ namespace Robust.Client return false; } - _clyde.SetWindowTitle("Space Station 14"); + _clyde.SetWindowTitle(Options.DefaultWindowTitle); _fontManager.SetFontDpi((uint) _configurationManager.GetCVar(CVars.DisplayFontDpi)); return true; diff --git a/Robust.Client/GameController/GameController.Standalone.cs b/Robust.Client/GameController/GameController.Standalone.cs index 5c0c7433e..0e4fd6476 100644 --- a/Robust.Client/GameController/GameController.Standalone.cs +++ b/Robust.Client/GameController/GameController.Standalone.cs @@ -19,7 +19,7 @@ namespace Robust.Client Start(args); } - public static void Start(string[] args, bool contentStart = false, IMainArgs? loaderArgs=null) + public static void Start(string[] args, bool contentStart = false, IMainArgs? loaderArgs=null, GameControllerOptions? options = null) { if (_hasStarted) { @@ -30,11 +30,11 @@ namespace Robust.Client if (CommandLineArgs.TryParse(args, out var parsed)) { - ParsedMain(parsed, contentStart, loaderArgs); + ParsedMain(parsed, contentStart, loaderArgs, options); } } - private static void ParsedMain(CommandLineArgs args, bool contentStart, IMainArgs? loaderArgs) + private static void ParsedMain(CommandLineArgs args, bool contentStart, IMainArgs? loaderArgs, GameControllerOptions? options) { IoCManager.InitThread(); @@ -45,11 +45,13 @@ namespace Robust.Client var gc = (GameController) 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._disableAssemblyLoadContext = contentStart; + gc.ContentStart = contentStart; if (!gc.Startup()) { Logger.Fatal("Failed to start game controller!"); diff --git a/Robust.Client/GameControllerOptions.cs b/Robust.Client/GameControllerOptions.cs new file mode 100644 index 000000000..ff4e23a37 --- /dev/null +++ b/Robust.Client/GameControllerOptions.cs @@ -0,0 +1,60 @@ +using Robust.Shared; +using Robust.Shared.Utility; + +namespace Robust.Client +{ + public class GameControllerOptions + { + /// + /// Whether content sandboxing will be enabled & enforced. + /// + public bool Sandboxing { get; init; } = true; + + // TODO: Expose mounting methods to games using Robust as a library. + /// + /// Lists of mount options to mount. + /// + public MountOptions MountOptions { get; init; } = new(); + + /// + /// Name the userdata directory will have. + /// + public string UserDataDirectoryName { get; init; } = "Space Station 14"; + + /// + /// Name of the configuration file in the user data directory. + /// + public string ConfigFileName { get; init; } = "client_config.toml"; + + // TODO: Define engine branding from json file in resources. + /// + /// Default window title. + /// + public string DefaultWindowTitle { get; init; } = "Space Station 14"; + + /// + /// 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.Client"; + + /// + /// 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 disable command line args server auto-connecting. + /// + public bool DisableCommandLineConnect { get; init; } = false; + } +} diff --git a/Robust.Client/IGameControllerInternal.cs b/Robust.Client/IGameControllerInternal.cs index a0cf03c41..c5a1677bc 100644 --- a/Robust.Client/IGameControllerInternal.cs +++ b/Robust.Client/IGameControllerInternal.cs @@ -7,6 +7,8 @@ namespace Robust.Client { internal interface IGameControllerInternal : IGameController { + GameControllerOptions Options { get; } + bool ContentStart { get; set; } void SetCommandLineArgs(CommandLineArgs args); bool LoadConfigAndUserData { get; set; } bool Startup(Func? logHandlerFactory = null); @@ -18,4 +20,4 @@ namespace Robust.Client void MouseWheel(MouseWheelEventArgs mouseWheelEventArgs); void OverrideMainLoop(IGameLoop gameLoop); } -} \ No newline at end of file +} diff --git a/Robust.Client/Utility/UserDataDir.cs b/Robust.Client/Utility/UserDataDir.cs index 18b342f66..614512f10 100644 --- a/Robust.Client/Utility/UserDataDir.cs +++ b/Robust.Client/Utility/UserDataDir.cs @@ -1,6 +1,7 @@ using System; using System.IO; using JetBrains.Annotations; +using Robust.Shared.IoC; namespace Robust.Client.Utility { @@ -29,7 +30,7 @@ namespace Robust.Client.Utility appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); #endif - return Path.Combine(appDataDir, "Space Station 14", "data"); + return Path.Combine(appDataDir, IoCManager.Resolve().Options.UserDataDirectoryName, "data"); } } diff --git a/Robust.Server/BaseServer.cs b/Robust.Server/BaseServer.cs index 9f13a49bc..f06d16929 100644 --- a/Robust.Server/BaseServer.cs +++ b/Robust.Server/BaseServer.cs @@ -254,9 +254,12 @@ namespace Robust.Server // Set up the VFS _resources.Initialize(dataDir); - ProgramShared.DoMounts(_resources, _commandLineArgs?.MountOptions, "Content.Server"); + ProgramShared.DoMounts(_resources, _commandLineArgs?.MountOptions, "Content.Server", contentStart:ContentStart); - _modLoader.SetUseLoadContext(!DisableLoadContext); + // 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); if (!_modLoader.TryLoadModulesFrom(new ResourcePath("/Assemblies/"), "Content.")) @@ -431,7 +434,7 @@ namespace Robust.Server _shutdownEvent.Set(); } - public bool DisableLoadContext { private get; set; } + public bool ContentStart { get; set; } public bool LoadConfigAndUserData { private get; set; } = true; public void OverrideMainLoop(IGameLoop gameLoop) diff --git a/Robust.Server/IBaseServer.cs b/Robust.Server/IBaseServer.cs index 998f0ffe1..08b01ae10 100644 --- a/Robust.Server/IBaseServer.cs +++ b/Robust.Server/IBaseServer.cs @@ -44,7 +44,7 @@ namespace Robust.Server internal interface IBaseServerInternal : IBaseServer { - bool DisableLoadContext { set; } + bool ContentStart { set; } bool LoadConfigAndUserData { set; } void OverrideMainLoop(IGameLoop gameLoop); diff --git a/Robust.Server/Program.cs b/Robust.Server/Program.cs index 44e650df3..148df3e0c 100644 --- a/Robust.Server/Program.cs +++ b/Robust.Server/Program.cs @@ -62,10 +62,7 @@ namespace Robust.Server var server = IoCManager.Resolve(); - // 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. - server.DisableLoadContext = contentStart; + server.ContentStart = contentStart; server.SetCommandLineArgs(args); Logger.Info("Server -> Starting"); diff --git a/Robust.Shared/MountOptions.cs b/Robust.Shared/MountOptions.cs index e1de8d627..64bab83e6 100644 --- a/Robust.Shared/MountOptions.cs +++ b/Robust.Shared/MountOptions.cs @@ -2,9 +2,32 @@ namespace Robust.Shared { - internal sealed class MountOptions + public sealed class MountOptions { public List ZipMounts = new(); public List DirMounts = new(); + + public MountOptions() + { } + + public MountOptions(List zipMounts, List dirMounts) + { + ZipMounts = zipMounts; + DirMounts = dirMounts; + } + + public static MountOptions Merge(MountOptions a, MountOptions b) + { + var zipMounts = new List(); + var dirMounts = new List(); + + zipMounts.AddRange(a.ZipMounts); + zipMounts.AddRange(b.ZipMounts); + + dirMounts.AddRange(a.DirMounts); + dirMounts.AddRange(b.DirMounts); + + return new MountOptions(zipMounts, dirMounts); + } } } diff --git a/Robust.Shared/ProgramShared.cs b/Robust.Shared/ProgramShared.cs index c062754c0..3c16526d2 100644 --- a/Robust.Shared/ProgramShared.cs +++ b/Robust.Shared/ProgramShared.cs @@ -7,28 +7,19 @@ namespace Robust.Shared internal static class ProgramShared { #if !FULL_RELEASE - private static string FindContentRootDir() + private static string FindContentRootDir(bool contentStart) { - var contentPath = PathHelpers.ExecutableRelativeFile("Content.Shared.dll"); - - // If Content.Shared.dll is next to us, - // that means we've been executed from one of content's bin directories. - if (File.Exists(contentPath)) - { - return "../../"; - } - - return "../../../"; + return contentStart ? "../../" : "../../../"; } #endif - internal static void DoMounts(IResourceManagerInternal res, MountOptions? options, string contentBuildDir, bool loader=false) + internal static void DoMounts(IResourceManagerInternal res, MountOptions? options, string contentBuildDir, bool loader=false, bool contentStart=false) { #if FULL_RELEASE if (!loader) res.MountContentDirectory(@"Resources/"); #else - var contentRootDir = FindContentRootDir(); + var contentRootDir = FindContentRootDir(contentStart); res.MountContentDirectory($@"{contentRootDir}RobustToolbox/Resources/"); res.MountContentDirectory($@"{contentRootDir}bin/{contentBuildDir}/", new ResourcePath("/Assemblies/")); res.MountContentDirectory($@"{contentRootDir}Resources/"); diff --git a/Robust.UnitTesting/GameControllerDummy.cs b/Robust.UnitTesting/GameControllerDummy.cs index aee1aec75..e1e7451c0 100644 --- a/Robust.UnitTesting/GameControllerDummy.cs +++ b/Robust.UnitTesting/GameControllerDummy.cs @@ -9,6 +9,8 @@ namespace Robust.UnitTesting internal sealed class GameControllerDummy : IGameControllerInternal { public InitialLaunchState LaunchState { get; } = new(false, null, null, null); + public GameControllerOptions Options { get; } = new(); + public bool ContentStart { get; set; } public void Shutdown(string? reason = null) { diff --git a/Robust.UnitTesting/RobustIntegrationTest.cs b/Robust.UnitTesting/RobustIntegrationTest.cs index 91e948123..580104d32 100644 --- a/Robust.UnitTesting/RobustIntegrationTest.cs +++ b/Robust.UnitTesting/RobustIntegrationTest.cs @@ -356,6 +356,7 @@ namespace Robust.UnitTesting cfg.OverrideConVars(new []{("log.runtimelog", "false"), (CVars.SysWinTickPeriod.Name, "-1")}); var failureLevel = _options == null ? LogLevel.Error : _options.FailureLogLevel; + server.ContentStart = _options?.ContentStart ?? false; if (server.Start(() => new TestLogHandler("SERVER", failureLevel))) { throw new Exception("Server failed to start."); @@ -449,6 +450,7 @@ namespace Robust.UnitTesting cfg.OverrideConVars(new []{(CVars.NetPredictLagBias.Name, "0")}); var failureLevel = _options == null ? LogLevel.Error : _options.FailureLogLevel; + client.ContentStart = _options?.ContentStart ?? false; client.Startup(() => new TestLogHandler("CLIENT", failureLevel)); var gameLoop = new IntegrationGameLoop(DependencyCollection.Resolve(), @@ -566,6 +568,7 @@ namespace Robust.UnitTesting public Assembly[]? ContentAssemblies { get; set; } public string? ExtraPrototypes { get; set; } public LogLevel? FailureLogLevel { get; set; } = LogLevel.Error; + public bool ContentStart { get; set; } = false; public Dictionary CVarOverrides { get; } = new(); }