diff --git a/Directory.Build.props b/Directory.Build.props
index 9433b81..97da1d9 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -21,6 +21,10 @@
$(SolutionDir)build/obj/modules/$(Configuration)
$(SolutionDir)build/bin/patcher/$(Configuration)
$(SolutionDir)build/obj/patcher/$(Configuration)
+ $(SolutionDir)build/bin/restarter/$(Configuration)
+ $(SolutionDir)build/obj/restarter/$(Configuration)
+ $(SolutionDir)build/bin/preloader/$(Configuration)
+ $(SolutionDir)build/obj/preloader/$(Configuration)
$(ModulesBinPath)/$(MSBuildProjectName)
$(ModulesObjPath)/$(MSBuildProjectName)
$(MSBuildProjectName)
diff --git a/SpaceWarp.sln b/SpaceWarp.sln
index c19f2bb..8a7e14a 100644
--- a/SpaceWarp.sln
+++ b/SpaceWarp.sln
@@ -8,6 +8,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpaceWarp.Game", "src/Space
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpaceWarp.Messaging", "src/SpaceWarp.Messaging/SpaceWarp.Messaging.csproj", "{66BA4E01-8521-42EB-9D7D-8EB653757177}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpaceWarp.Preloader", "src/SpaceWarp.Preloader/SpaceWarp.Preloader.csproj", "{0957B5C2-B882-45A4-981E-F9EEE0B551C6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpaceWarpRestarter", "src\SpaceWarpRestarter\SpaceWarpRestarter.csproj", "{4B509D31-4C02-4D6E-8508-8417F0745776}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpaceWarp.Sound", "src/SpaceWarp.Sound/SpaceWarp.Sound.csproj", "{BA439A24-7EA3-4E79-A44C-1FA303B9331C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpaceWarp.UI", "src/SpaceWarp.UI/SpaceWarp.UI.csproj", "{CB131B63-51E6-4ED7-A47C-28B1EB65B8D7}"
@@ -98,6 +102,22 @@ Global
{8DB42693-9177-40B9-AC6A-B6D7A4823FAD}.Deploy|Any CPU.Build.0 = Deploy|Any CPU
{8DB42693-9177-40B9-AC6A-B6D7A4823FAD}.DeployAndRun|Any CPU.ActiveCfg = DeployAndRun|Any CPU
{8DB42693-9177-40B9-AC6A-B6D7A4823FAD}.DeployAndRun|Any CPU.Build.0 = DeployAndRun|Any CPU
+ {4B509D31-4C02-4D6E-8508-8417F0745776}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4B509D31-4C02-4D6E-8508-8417F0745776}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4B509D31-4C02-4D6E-8508-8417F0745776}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4B509D31-4C02-4D6E-8508-8417F0745776}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4B509D31-4C02-4D6E-8508-8417F0745776}.Deploy|Any CPU.ActiveCfg = Deploy|Any CPU
+ {4B509D31-4C02-4D6E-8508-8417F0745776}.Deploy|Any CPU.Build.0 = Deploy|Any CPU
+ {4B509D31-4C02-4D6E-8508-8417F0745776}.DeployAndRun|Any CPU.ActiveCfg = DeployAndRun|Any CPU
+ {4B509D31-4C02-4D6E-8508-8417F0745776}.DeployAndRun|Any CPU.Build.0 = DeployAndRun|Any CPU
+ {0957B5C2-B882-45A4-981E-F9EEE0B551C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0957B5C2-B882-45A4-981E-F9EEE0B551C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0957B5C2-B882-45A4-981E-F9EEE0B551C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0957B5C2-B882-45A4-981E-F9EEE0B551C6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0957B5C2-B882-45A4-981E-F9EEE0B551C6}.Deploy|Any CPU.ActiveCfg = Deploy|Any CPU
+ {0957B5C2-B882-45A4-981E-F9EEE0B551C6}.Deploy|Any CPU.Build.0 = Deploy|Any CPU
+ {0957B5C2-B882-45A4-981E-F9EEE0B551C6}.DeployAndRun|Any CPU.ActiveCfg = DeployAndRun|Any CPU
+ {0957B5C2-B882-45A4-981E-F9EEE0B551C6}.DeployAndRun|Any CPU.Build.0 = DeployAndRun|Any CPU
EndGlobalSection
EndGlobal
diff --git a/plugin_template/doorstop_config.ini b/plugin_template/doorstop_config.ini
index a68f30f..2055675 100644
--- a/plugin_template/doorstop_config.ini
+++ b/plugin_template/doorstop_config.ini
@@ -2,7 +2,7 @@
# Specifies whether assembly executing is enabled
enabled=true
# Specifies the path (absolute, or relative to the game's exe) to the DLL/EXE that should be executed by Doorstop
-targetAssembly=BepInEx\core\BepInEx.Preloader.dll
+targetAssembly=BepInEx\core\SpaceWarp.Preloader.dll
# Specifies whether Unity's output log should be redirected to \output_log.txt
redirectOutputLog=false
# If enabled, DOORSTOP_DISABLE env var value is ignored
diff --git a/src/SpaceWarp.Core/API/Mods/JSON/ModInfo.cs b/src/SpaceWarp.Core/API/Mods/JSON/ModInfo.cs
index 49b94b3..6be09bd 100644
--- a/src/SpaceWarp.Core/API/Mods/JSON/ModInfo.cs
+++ b/src/SpaceWarp.Core/API/Mods/JSON/ModInfo.cs
@@ -108,4 +108,10 @@ public string Name
///
[JsonProperty("conflicts", Required = Required.DisallowNull)]
public List Conflicts { get; internal set; } = new();
+
+ ///
+ /// The filenames of patcher assemblies of the mod.
+ ///
+ [JsonProperty("patchers", Required = Required.DisallowNull)]
+ public List Patchers { get; internal set; } = new();
}
\ No newline at end of file
diff --git a/src/SpaceWarp.Core/API/Mods/JSON/SpecVersion.cs b/src/SpaceWarp.Core/API/Mods/JSON/SpecVersion.cs
index 0deb260..1c32280 100644
--- a/src/SpaceWarp.Core/API/Mods/JSON/SpecVersion.cs
+++ b/src/SpaceWarp.Core/API/Mods/JSON/SpecVersion.cs
@@ -45,11 +45,18 @@ public sealed record SpecVersion
public static SpecVersion V1_3 { get; } = new(1, 3);
///
- /// Specification version 2.0 (SpaceWarp 1.5.x and 2.0.x) - removes support for version checking from .csproj files,
- ///
+ /// Specification version 2.0 (SpaceWarp 1.5 - 1.7.x) - removes support for version checking from .csproj files,
+ /// adds support for specifying mod conflicts. Switched to semantic versioning.
///
public static SpecVersion V2_0 { get; } = new(2, 0);
+ ///
+ /// Specification version 2.1 (SpaceWarp 1.8.x) - requires that mods specify their preload patchers in the
+ /// swinfo.json file.
+ ///
+ public static SpecVersion V2_1 { get; } = new(2, 1);
+
+
// ReSharper restore InconsistentNaming
///
diff --git a/src/SpaceWarp.Preloader/Directory.Build.props b/src/SpaceWarp.Preloader/Directory.Build.props
new file mode 100644
index 0000000..7bd628c
--- /dev/null
+++ b/src/SpaceWarp.Preloader/Directory.Build.props
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+ false
+ $(PreloaderBinPath)/$(MSBuildProjectName)
+ $(PreloaderObjPath)/$(MSBuildProjectName)
+
+
diff --git a/src/SpaceWarp.Preloader/Entrypoint.cs b/src/SpaceWarp.Preloader/Entrypoint.cs
new file mode 100644
index 0000000..92882c1
--- /dev/null
+++ b/src/SpaceWarp.Preloader/Entrypoint.cs
@@ -0,0 +1,158 @@
+using System.Reflection;
+using JetBrains.Annotations;
+using Newtonsoft.Json.Linq;
+
+namespace SpaceWarp.Preloader;
+
+internal static class Entrypoint
+{
+ private static readonly List PreloadAssemblyPaths =
+ [
+ Path.Combine("KSP2_x64_Data", "Managed", "Newtonsoft.Json.dll"),
+ Path.Combine("BepInEx", "core", "BepInEx.Preloader.dll"),
+ ];
+
+ private static string _gameFolder;
+ private static Logger _logger;
+
+ [UsedImplicitly]
+ public static void Main(string[] args)
+ {
+ _gameFolder = Path.GetDirectoryName(Environment.GetCommandLineArgs()[0])!;
+ _logger = new Logger(_gameFolder);
+
+ PreloadAssemblies();
+ ProcessAllPatchers();
+ StartBepinex();
+ }
+
+ private static void PreloadAssemblies()
+ {
+ foreach (var fullPath in PreloadAssemblyPaths.Select(assemblyPath => Path.Combine(_gameFolder, assemblyPath)))
+ {
+ try
+ {
+ _logger.LogDebug($"Preloading {fullPath}...");
+ Assembly.LoadFile(fullPath);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogException(ex, $"An error occurred while preloading the assembly {fullPath}:");
+ }
+ }
+ }
+
+ private static void StartBepinex()
+ {
+ BepInEx.Preloader.Entrypoint.Main();
+ }
+
+ #region Disabling patchers of disabled plugins
+
+ private static void ProcessAllPatchers()
+ {
+ var disabledPluginGuids = GetDisabledPluginGuids();
+
+ var swinfoPaths = Directory
+ .EnumerateFiles(
+ Path.Combine(_gameFolder, "BepInEx", "plugins"),
+ "swinfo.json",
+ SearchOption.AllDirectories
+ );
+
+ var enablePatchers = new List();
+ var disablePatchers = new List();
+
+ foreach (var swinfoPath in swinfoPaths)
+ {
+ try
+ {
+ var (guid, patchers) = ReadSwinfo(swinfoPath);
+
+ if (patchers == null)
+ {
+ continue;
+ }
+
+ if (!disabledPluginGuids.Contains(guid))
+ {
+ enablePatchers.AddRange(patchers.Select(StripExtension));
+ }
+ else
+ {
+ disablePatchers.AddRange(patchers.Select(StripExtension));
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogException(ex, $"An error occurred while processing {swinfoPath}:");
+ }
+ }
+
+ RenameAllPatchers(enablePatchers, disablePatchers);
+ }
+
+ private static string[] GetDisabledPluginGuids()
+ {
+ var disabledPluginsPath = Path.Combine(_gameFolder, "BepInEx", "disabled_plugins.cfg");
+
+ return File.Exists(disabledPluginsPath)
+ ? File.ReadAllLines(disabledPluginsPath)
+ : [];
+ }
+
+ private static (string guid, List patchers) ReadSwinfo(string swinfoPath)
+ {
+ _logger.LogDebug($"Reading {swinfoPath}...");
+
+ var swinfo = JObject.Parse(File.ReadAllText(swinfoPath));
+
+ var guid = swinfo["mod_id"]?.Value();
+ if (guid == null)
+ {
+ throw new Exception($"{swinfoPath} does not contain a mod_id.");
+ }
+
+ var patchers = swinfo["patchers"]?.Values().ToList();
+ if (patchers == null)
+ {
+ _logger.LogInfo($"{guid} does not contain patchers, skipping.");
+ }
+
+ return (guid, patchers);
+ }
+
+ private static void RenameAllPatchers(ICollection enablePatchers, ICollection disablePatchers)
+ {
+ var patchers = Directory
+ .EnumerateFiles(Path.Combine(_gameFolder, "BepInEx", "patchers"), "*", SearchOption.AllDirectories)
+ .Where(file => file.EndsWith(".dll") || file.EndsWith(".dll.disabled"));
+
+ foreach (var patcher in patchers)
+ {
+ var patcherName = StripExtension(Path.GetFileName(patcher));
+
+ if (enablePatchers.Contains(patcherName) && patcher.EndsWith(".dll.disabled"))
+ {
+ _logger.LogDebug($"Enabling {patcherName}...");
+ File.Move(patcher, patcher.Replace(".dll.disabled", ".dll"));
+ }
+ else if (disablePatchers.Contains(patcherName) && patcher.EndsWith(".dll"))
+ {
+ _logger.LogDebug($"Disabling {patcherName}...");
+ File.Move(patcher, patcher.Replace(".dll", ".dll.disabled"));
+ }
+ else
+ {
+ _logger.LogDebug($"Skipping {patcherName}...");
+ }
+ }
+ }
+
+ private static string StripExtension(string filename)
+ {
+ return filename.Replace(".disabled", "").Replace(".dll", "");
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/SpaceWarp.Preloader/Logger.cs b/src/SpaceWarp.Preloader/Logger.cs
new file mode 100644
index 0000000..fcfea00
--- /dev/null
+++ b/src/SpaceWarp.Preloader/Logger.cs
@@ -0,0 +1,79 @@
+using System.ComponentModel;
+
+namespace SpaceWarp.Preloader;
+
+internal enum LogLevel
+{
+ Debug,
+ Info,
+ Warning,
+ Error
+}
+
+internal static class LogLevelExtensions
+{
+ public static string ToLogString(this LogLevel logLevel)
+ {
+ return logLevel switch
+ {
+ LogLevel.Debug => "DEBUG",
+ LogLevel.Info => "INFO ",
+ LogLevel.Warning => "WARN ",
+ LogLevel.Error => "ERR ",
+ _ => throw new InvalidEnumArgumentException(nameof(logLevel), (int)logLevel, typeof(LogLevel))
+ };
+ }
+}
+
+internal class Logger
+{
+ private readonly string _logPath;
+
+ public Logger(string gamePath)
+ {
+ _logPath = Path.Combine(gamePath, "BepInEx", "SpaceWarp.Preload.log");
+
+ if (File.Exists(_logPath))
+ {
+ File.Delete(_logPath);
+ }
+ }
+
+ private void Log(object message, LogLevel logLevel = LogLevel.Info)
+ {
+ var logMessage =
+ $"[{logLevel.ToLogString()}: {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}{Environment.NewLine}";
+ File.AppendAllText(_logPath, logMessage);
+ }
+
+ public void LogDebug(object message)
+ {
+ Log(message, LogLevel.Debug);
+ }
+
+ public void LogInfo(object message)
+ {
+ Log(message, LogLevel.Info);
+ }
+
+ public void LogWarning(object message)
+ {
+ Log(message, LogLevel.Warning);
+ }
+
+ public void LogError(object message)
+ {
+ Log(message, LogLevel.Error);
+ }
+
+ public void LogException(Exception ex, string message = null)
+ {
+ var logMessage = $"{ex.Message}{Environment.NewLine}{ex.StackTrace}";
+ if (message != null)
+ {
+ logMessage = $"{message}{Environment.NewLine}{logMessage}";
+ }
+
+ LogError(logMessage);
+ }
+}
\ No newline at end of file
diff --git a/src/SpaceWarp.Preloader/SpaceWarp.Preloader.csproj b/src/SpaceWarp.Preloader/SpaceWarp.Preloader.csproj
new file mode 100644
index 0000000..68b83e8
--- /dev/null
+++ b/src/SpaceWarp.Preloader/SpaceWarp.Preloader.csproj
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+ $(SolutionDir)/plugin_template/BepInEx/core/BepInEx.Preloader.dll
+
+
+
diff --git a/src/SpaceWarp/Directory.Build.targets b/src/SpaceWarp/Directory.Build.targets
index a971e01..35a1ded 100644
--- a/src/SpaceWarp/Directory.Build.targets
+++ b/src/SpaceWarp/Directory.Build.targets
@@ -80,6 +80,28 @@
SourceFiles="@(PatcherPDBs)"
DestinationFolder="$(SolutionDir)/dist/$(ConfigurationName)/BepInEx/patchers/$(ProjectName)"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+ DestinationFolder="$(KSP2DIR)/%(RecursiveDir)"/>
diff --git a/src/SpaceWarp/SpaceWarp.csproj b/src/SpaceWarp/SpaceWarp.csproj
index 9cd2ccc..2eb3e56 100644
--- a/src/SpaceWarp/SpaceWarp.csproj
+++ b/src/SpaceWarp/SpaceWarp.csproj
@@ -11,6 +11,7 @@
+
diff --git a/src/SpaceWarpPatcher/SpaceWarpPatcher.csproj b/src/SpaceWarpPatcher/SpaceWarpPatcher.csproj
index c304ba3..f772030 100644
--- a/src/SpaceWarpPatcher/SpaceWarpPatcher.csproj
+++ b/src/SpaceWarpPatcher/SpaceWarpPatcher.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/src/SpaceWarpRestarter/Directory.Build.props b/src/SpaceWarpRestarter/Directory.Build.props
new file mode 100644
index 0000000..017615e
--- /dev/null
+++ b/src/SpaceWarpRestarter/Directory.Build.props
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+ false
+ $(RestarterBinPath)/$(MSBuildProjectName)
+ $(RestarterObjPath)/$(MSBuildProjectName)
+
+
diff --git a/src/SpaceWarpRestarter/Restarter.cs b/src/SpaceWarpRestarter/Restarter.cs
new file mode 100644
index 0000000..1c1ac82
--- /dev/null
+++ b/src/SpaceWarpRestarter/Restarter.cs
@@ -0,0 +1,40 @@
+using System.Diagnostics;
+using System.Reflection;
+
+if (args.Length < 1)
+{
+ Console.WriteLine(
+ $"Usage: {Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().Location)} " +
+ $"[optional arguments]"
+ );
+ Environment.Exit(1);
+}
+
+var processPath = args[0];
+var processName = Path.GetFileNameWithoutExtension(processPath);
+var processArgs = args.Length > 1 ? args[1..] : Array.Empty();
+
+Console.WriteLine($"Waiting for {processName} to exit...");
+
+while (true)
+{
+ if (Process.GetProcessesByName(processName).Length == 0)
+ {
+ try
+ {
+ Console.WriteLine($"{processName}.exe is not running. Attempting to start the process...");
+ Process.Start(processPath, processArgs);
+ Console.WriteLine($"{processName}.exe started successfully.");
+ break;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"An error occurred while starting {processName}.exe: {ex.Message}");
+ Environment.Exit(1);
+ }
+ }
+ else
+ {
+ Thread.Sleep(500);
+ }
+}
diff --git a/src/SpaceWarpRestarter/SpaceWarpRestarter.csproj b/src/SpaceWarpRestarter/SpaceWarpRestarter.csproj
new file mode 100644
index 0000000..009e5c9
--- /dev/null
+++ b/src/SpaceWarpRestarter/SpaceWarpRestarter.csproj
@@ -0,0 +1,10 @@
+
+
+
+ Exe
+ net7.0
+ enable
+ enable
+ SpaceWarpRestarter
+
+