From 2464246fd7e3c863d759423f049f1662ee52fc3e Mon Sep 17 00:00:00 2001 From: clodanSPT Date: Tue, 25 Feb 2025 16:34:51 +0000 Subject: [PATCH] Pre spt mod loader (#102) * partial commit * pre spt mod loader refactored --------- Co-authored-by: Alex Co-authored-by: clodan --- Libraries/Core/Models/Spt/Mod/ModOrder.cs | 13 + .../Core/Models/Spt/Mod/PackageJsonData.cs | 20 +- Libraries/Core/Utils/FileUtil.cs | 6 +- Libraries/SptCommon/Semver/ISemVer.cs | 13 + .../SemanticVersioningSemVer.cs | 47 ++ Libraries/SptCommon/SptCommon.csproj | 4 + Server/{ => Modding}/HarmonyBootstrapper.cs | 2 +- Server/{ => Modding}/ModDllLoader.cs | 41 +- Server/Modding/ModLoadOrder.cs | 152 +++++++ Server/Modding/ModValidator.cs | 415 ++++++++++++++++++ Server/Program.cs | 54 ++- 11 files changed, 718 insertions(+), 49 deletions(-) create mode 100644 Libraries/Core/Models/Spt/Mod/ModOrder.cs create mode 100644 Libraries/SptCommon/Semver/ISemVer.cs create mode 100644 Libraries/SptCommon/Semver/Implementations/SemanticVersioningSemVer.cs rename Server/{ => Modding}/HarmonyBootstrapper.cs (94%) rename Server/{ => Modding}/ModDllLoader.cs (71%) create mode 100644 Server/Modding/ModLoadOrder.cs create mode 100644 Server/Modding/ModValidator.cs diff --git a/Libraries/Core/Models/Spt/Mod/ModOrder.cs b/Libraries/Core/Models/Spt/Mod/ModOrder.cs new file mode 100644 index 00000000..89548cf6 --- /dev/null +++ b/Libraries/Core/Models/Spt/Mod/ModOrder.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace Core.Models.Spt.Mod; + +public class ModOrder +{ + [JsonPropertyName("order")] + public List Order + { + get; + set; + } +} diff --git a/Libraries/Core/Models/Spt/Mod/PackageJsonData.cs b/Libraries/Core/Models/Spt/Mod/PackageJsonData.cs index eec92195..73706fdb 100644 --- a/Libraries/Core/Models/Spt/Mod/PackageJsonData.cs +++ b/Libraries/Core/Models/Spt/Mod/PackageJsonData.cs @@ -1,73 +1,87 @@ -namespace Core.Models.Spt.Mod; +using System.Text.Json.Serialization; + +namespace Core.Models.Spt.Mod; public record PackageJsonData { + [JsonPropertyName("name")] public string? Name { get; set; } + [JsonPropertyName("author")] public string? Author { get; set; } + [JsonPropertyName("contributors")] public List? Contributors { get; set; } + [JsonPropertyName("version")] public string? Version { get; set; } + [JsonPropertyName("sptVersion")] public string? SptVersion { get; set; } + [JsonPropertyName("loadBefore")] public List? LoadBefore { get; set; } + [JsonPropertyName("loadAfter")] public List? LoadAfter { get; set; } - public List? IncompatibleMods + [JsonPropertyName("incompatibilities")] + public List? Incompatibilities { get; set; } - public Dictionary? Dependencies + [JsonPropertyName("modDependencies")] + public Dictionary? ModDependencies { get; set; } + [JsonPropertyName("url")] public string? Url { get; set; } + [JsonPropertyName("isBundleMod")] public bool? IsBundleMod { get; set; } + [JsonPropertyName("licence")] public string? Licence { get; diff --git a/Libraries/Core/Utils/FileUtil.cs b/Libraries/Core/Utils/FileUtil.cs index 0b51301f..faab0178 100644 --- a/Libraries/Core/Utils/FileUtil.cs +++ b/Libraries/Core/Utils/FileUtil.cs @@ -9,13 +9,13 @@ public class FileUtil( { protected const string _modBasePath = "user/mods/"; - public List GetFiles(string path, bool recursive = false) + public List GetFiles(string path, bool recursive = false, string searchPattern = "*") { - var files = new List(Directory.GetFiles(path)); + var files = new List(Directory.GetFiles(path, searchPattern)); if (recursive) { - files.AddRange(Directory.GetDirectories(path).SelectMany(d => GetFiles(d, recursive))); + files.AddRange(Directory.GetDirectories(path).SelectMany(d => GetFiles(d, recursive, searchPattern))); } return files; diff --git a/Libraries/SptCommon/Semver/ISemVer.cs b/Libraries/SptCommon/Semver/ISemVer.cs new file mode 100644 index 00000000..c765c5c7 --- /dev/null +++ b/Libraries/SptCommon/Semver/ISemVer.cs @@ -0,0 +1,13 @@ +namespace SptCommon.Semver; + +public interface ISemVer +{ + string MaxSatisfying(List versions); + string MaxSatisfying(IEnumerable versions); + string MaxSatisfying(string version, List versions); + string MaxSatisfying(string version, IEnumerable versions); + bool Satisfies(string version, string testVersion); + bool AnySatisfies(string version, List testVersions); + bool IsValid(string version); + bool IsValidRange(string version); +} diff --git a/Libraries/SptCommon/Semver/Implementations/SemanticVersioningSemVer.cs b/Libraries/SptCommon/Semver/Implementations/SemanticVersioningSemVer.cs new file mode 100644 index 00000000..908602a3 --- /dev/null +++ b/Libraries/SptCommon/Semver/Implementations/SemanticVersioningSemVer.cs @@ -0,0 +1,47 @@ +using Range = SemanticVersioning.Range; +using Version = SemanticVersioning.Version; + +namespace SptCommon.Semver.Implementations; + +public class SemanticVersioningSemVer : ISemVer +{ + public string MaxSatisfying(List versions) + { + return MaxSatisfying(versions.AsEnumerable()); + } + + public string MaxSatisfying(IEnumerable versions) + { + return MaxSatisfying("*", versions); + } + + public string MaxSatisfying(string version, List versions) + { + return MaxSatisfying(version, versions.AsEnumerable()); + } + + public string MaxSatisfying(string version, IEnumerable versions) + { + return Range.MaxSatisfying(version, versions, true); + } + + public bool Satisfies(string version, string testVersion) + { + return Range.IsSatisfied(version, testVersion, true); + } + + public bool AnySatisfies(string version, List testVersions) + { + return testVersions.Any(v => Satisfies(version, v)); + } + + public bool IsValid(string version) + { + return Version.TryParse(version, out _); + } + + public bool IsValidRange(string version) + { + return Range.TryParse(version, out _); + } +} diff --git a/Libraries/SptCommon/SptCommon.csproj b/Libraries/SptCommon/SptCommon.csproj index 03517360..36c01228 100644 --- a/Libraries/SptCommon/SptCommon.csproj +++ b/Libraries/SptCommon/SptCommon.csproj @@ -7,4 +7,8 @@ Library + + + + diff --git a/Server/HarmonyBootstrapper.cs b/Server/Modding/HarmonyBootstrapper.cs similarity index 94% rename from Server/HarmonyBootstrapper.cs rename to Server/Modding/HarmonyBootstrapper.cs index 819ded3f..22d22de8 100644 --- a/Server/HarmonyBootstrapper.cs +++ b/Server/Modding/HarmonyBootstrapper.cs @@ -1,7 +1,7 @@ using System.Reflection; using HarmonyLib; -namespace Server; +namespace Server.Modding; public class HarmonyBootstrapper { diff --git a/Server/ModDllLoader.cs b/Server/Modding/ModDllLoader.cs similarity index 71% rename from Server/ModDllLoader.cs rename to Server/Modding/ModDllLoader.cs index 1bf74241..a5eeeb85 100644 --- a/Server/ModDllLoader.cs +++ b/Server/Modding/ModDllLoader.cs @@ -2,7 +2,7 @@ using System.Reflection; using System.Text.Json; using Core.Models.Spt.Mod; -namespace Server; +namespace Server.Modding; public class ModDllLoader { @@ -38,41 +38,9 @@ public class ModDllLoader } } - ValidateModDependencies(mods); - - // Sort by mods LoadBefore/LoadAfter collections - SortMods(mods); - return mods; } - /// - /// Ensure all mods have their dependencies - /// - /// Mods to check dependencies of - private static void ValidateModDependencies(List mods) - { - foreach (var sptMod in mods) - { - if (sptMod.PackageJson?.Dependencies?.Count > 0) - { - // Has dependencies, validate they exist - foreach (var dependency in sptMod.PackageJson.Dependencies - .Where(dependency => !mods.Exists(x => string.Equals(x.PackageJson?.Name, dependency.Key, StringComparison.OrdinalIgnoreCase)))) - { - // TODO: also check version passes semver check - throw new Exception($"Mod: {sptMod.PackageJson.Name} is unable to load as it cannot find another mod it needs: {dependency.Key} version: {dependency.Value}"); - } - } - } - } - - private static void SortMods(List mods) - { - // TODO: implement - Console.WriteLine($"NOT IMPLEMENTED: SortMods"); - } - /// /// Check the provided directory path for a dll and .json file, load into memory /// @@ -125,6 +93,13 @@ public class ModDllLoader throw new Exception($"No Assemblies found in path: {Path.GetFullPath(path)}"); } + if (result.PackageJson?.Name == null || result.PackageJson?.Author == null || + result.PackageJson?.Version == null || result.PackageJson?.Licence == null || + result.PackageJson?.SptVersion == null) + { + throw new Exception($"The package.json file for {path} is missing one of these properties: name, author, licence, version or sptVersion"); + } + if (result.Assembly is not null && result.PackageJson is not null) { Console.WriteLine($"Loaded: {result.PackageJson.Name} Version: {result.PackageJson.Version} by: {result.PackageJson.Author}"); diff --git a/Server/Modding/ModLoadOrder.cs b/Server/Modding/ModLoadOrder.cs new file mode 100644 index 00000000..00c1c82d --- /dev/null +++ b/Server/Modding/ModLoadOrder.cs @@ -0,0 +1,152 @@ +using Core.Models.Spt.Mod; +using Core.Utils.Cloners; + +namespace Server.Modding; + +public class ModLoadOrder(ICloner cloner) +{ + protected Dictionary mods = new(); + protected Dictionary modsAvailable = new(); + protected HashSet loadOrder = new(); + + public void SetModList(Dictionary mods) + { + this.mods = mods; + modsAvailable = cloner.Clone(this.mods); + loadOrder = []; + + var visited = new HashSet(); + + // invert loadBefore into loadAfter on specified mods + foreach (var (modName, modConfig) in modsAvailable) + { + if ((modConfig.LoadBefore ?? []).Count > 0) + { + InvertLoadBefore(modName); + } + } + + foreach (var modName in modsAvailable.Keys) + { + GetLoadOrderRecursive(modName, visited); + } + } + + public List GetLoadOrder() + { + return [..loadOrder]; + } + + public HashSet GetModsOnLoadBefore(string mod) + { + if (!mods.ContainsKey(mod)) + { + throw new Exception($"The mod {mod} does not exist!"); + } + + var config = mods[mod]; + + var loadBefore = new HashSet(config.LoadBefore); + + foreach (var loadBeforeMod in loadBefore) + { + if (!mods.ContainsKey(loadBeforeMod)) + { + loadBefore.Remove(loadBeforeMod); + } + } + + return loadBefore; + } + + /** + * TODO: Is this not needed at all? + */ + public HashSet GetModsOnLoadAfter(string mod) + { + if (!mods.ContainsKey(mod)) + { + throw new Exception($"The mod {mod} does not exist!"); + } + + var config = mods[mod]; + + var loadAfter = new HashSet(config.LoadAfter); + + foreach (var loadAfterMod in loadAfter) + { + if (!mods.ContainsKey(loadAfterMod)) + { + loadAfter.Remove(loadAfterMod); + } + } + + return loadAfter; + } + + protected void InvertLoadBefore(string mod) + { + var loadBefore = GetModsOnLoadBefore(mod); + + foreach (var loadBeforeMod in loadBefore) + { + var loadBeforeModConfig = modsAvailable[loadBeforeMod]; + + loadBeforeModConfig.LoadAfter ??= []; + loadBeforeModConfig.LoadAfter.Add(mod); + + modsAvailable.Add(loadBeforeMod, loadBeforeModConfig); + } + } + + protected void GetLoadOrderRecursive(string mod, HashSet visited) + { + // Validate package + if (loadOrder.Contains(mod)) + { + return; + } + + if (visited.Contains(mod)) + { + // Additional info to help debug + throw new Exception($"Cyclic dependency detected for mod {mod}!"); + } + + // Check dependencies + if (!modsAvailable.ContainsKey(mod)) + { + throw new Exception("modloader-error_parsing_mod_load_order"); + } + + var config = modsAvailable[mod]; + + config.LoadAfter ??= []; + config.ModDependencies ??= []; + + var dependencies = new HashSet(config.ModDependencies.Keys); + + foreach (var modAfter in config.LoadAfter) + { + if (modsAvailable.ContainsKey(modAfter)) + { + if (modsAvailable[modAfter]?.LoadAfter?.Contains(mod) ?? false) + { + throw new Exception("modloader-load_order_conflict"); + } + + dependencies.Add(modAfter); + } + } + + visited.Add(mod); + + foreach (var nextMod in dependencies) + { + GetLoadOrderRecursive(nextMod, visited); + } + + visited.Remove(mod); + loadOrder.Add(mod); + } +} diff --git a/Server/Modding/ModValidator.cs b/Server/Modding/ModValidator.cs new file mode 100644 index 00000000..59c59f41 --- /dev/null +++ b/Server/Modding/ModValidator.cs @@ -0,0 +1,415 @@ +using Core.Models.Spt.Config; +using Core.Models.Spt.Mod; +using Core.Models.Utils; +using Core.Servers; +using Core.Services; +using Core.Utils; +using SptCommon.Semver; +using LogLevel = Core.Models.Spt.Logging.LogLevel; + +namespace Server.Modding; + +public class ModValidator( + ISptLogger logger, + LocalisationService localisationService, + ConfigServer configServer, + ISemVer semVer, + ModLoadOrder modLoadOrder, + JsonUtil jsonUtil, + FileUtil fileUtil) +{ + protected readonly string basepath = "user/mods/"; + protected readonly string modOrderPath = "user/mods/order.json"; + protected Dictionary order = []; + protected Dictionary imported = []; + protected HashSet skippedMods = []; + + protected CoreConfig sptConfig = configServer.GetConfig(); + + public List ValidateAndSort(List mods) + { + if (ProgramStatics.MODS()) + { + ValidateMods(mods); + + modLoadOrder.SetModList(imported.ToDictionary(m => m.Key, m => m.Value.PackageJson)); + var finalList = new List(); + foreach (var orderMod in SortModsLoadOrder()) + { + if (!imported.TryGetValue(orderMod, out var loadedMod)) + { + throw new Exception($"Unable to find mod {orderMod} in loaded mods"); + } + + finalList.Add(loadedMod); + } + + return finalList; + } + + return []; + } + + public string GetModPath(string mod) + { + return $"{basepath}{mod}/"; + } + + protected void ValidateMods(List mods) + { + logger.Info(localisationService.GetText("modloader-loading_mods", mods.Count)); + + // Mod order + if (!fileUtil.FileExists(modOrderPath)) + { + logger.Info(localisationService.GetText("modloader-mod_order_missing")); + + // Write file with empty order array to disk + fileUtil.WriteFile(modOrderPath, jsonUtil.Serialize(new ModOrder + { + Order = [] + })); + } + else + { + var modOrder = File.ReadAllText(modOrderPath); + try + { + var modOrderArray = jsonUtil.Deserialize(modOrder).Order; + for (var i = 0; i < modOrderArray.Count; i++) + { + order.Add(modOrderArray[i], i); + } + } + catch (Exception e) + { + logger.Error(localisationService.GetText("modloader-mod_order_error"), e); + } + } + + // Validate and remove broken mods from mod list + var validMods = GetValidMods(mods); + + var modPackageData = validMods.ToDictionary(m => m.PackageJson!.Name!, m => m.PackageJson!); + CheckForDuplicateMods(modPackageData); + + // Used to check all errors before stopping the load execution + var errorsFound = false; + + foreach (var modToValidate in modPackageData.Values) + { + if (ShouldSkipMod(modToValidate)) + { + // skip error checking and dependency install for mods already marked as skipped. + continue; + } + + // Returns if any mod dependency is not satisfied + if (!AreModDependenciesFulfilled(modToValidate, modPackageData)) + { + errorsFound = true; + } + + // Returns if at least two incompatible mods are found + if (!IsModCompatible(modToValidate, modPackageData)) + { + errorsFound = true; + } + + // Returns if mod isnt compatible with this verison of spt + if (!IsModCombatibleWithSpt(modToValidate)) + { + errorsFound = true; + } + } + + if (errorsFound) + { + logger.Error(localisationService.GetText("modloader-no_mods_loaded")); + return; + } + + // sort mod order + var missingFromOrderJSON = new Dictionary(); + validMods.Sort((prev, next) => SortMods(prev, next, missingFromOrderJSON)); + + // log the missing mods from order.json + if (logger.IsLogEnabled(LogLevel.Debug)) + { + foreach (var missingMod in missingFromOrderJSON.Keys) + { + logger.Debug(localisationService.GetText("modloader-mod_order_missing_from_json", missingMod)); + } + } + + // add mods + foreach (var mod in validMods) + { + if (ShouldSkipMod(mod.PackageJson)) + { + logger.Warning(localisationService.GetText("modloader-skipped_mod", new { mod })); + continue; + } + + AddMod(mod); + } + } + + protected int SortMods(SptMod prev, SptMod next, Dictionary missingFromOrderJson) + { + // mod is not on the list, move the mod to last + if (!order.TryGetValue(prev.PackageJson!.Name!, out var previndex)) + { + missingFromOrderJson[prev.PackageJson.Name!] = true; + return 1; + } + + if (!order.TryGetValue(next.PackageJson!.Name!, out var nextindex)) + { + missingFromOrderJson[next.PackageJson.Name!] = true; + return -1; + } + + return previndex - nextindex; + } + + /** + * Check for duplicate mods loaded, show error if any + * @param modPackageData map of mod package.json data + */ + protected void CheckForDuplicateMods(Dictionary modPackageData) + { + var grouppedMods = new Dictionary>(); + + foreach (var mod in modPackageData.Values) + { + var name = $"{mod.Author}-{mod.Name}"; + grouppedMods.Add(name, [..(grouppedMods.GetValueOrDefault(name) ?? []), mod]); + + // if there's more than one entry for a given mod it means there's at least 2 mods with the same author and name trying to load. + if (grouppedMods[name].Count > 1 && !skippedMods.Contains(name)) + { + skippedMods.Add(name); + } + } + + // at this point skippedMods only contains mods that are duplicated, so we can just go through every single entry and log it + foreach (var modName in skippedMods) + { + logger.Error(localisationService.GetText("modloader-x_duplicates_found", modName)); + } + } + + /** + * Returns an array of valid mods. + * + * @param mods mods to validate + * @returns array of mod folder names + */ + protected List GetValidMods(List mods) + { + return mods.Where(ValidMod).ToList(); + } + + + /** + * Is the passed in mod compatible with the running server version + * @param mod Mod to check compatibiltiy with SPT + * @returns True if compatible + */ + protected bool IsModCombatibleWithSpt(PackageJsonData mod) + { + var sptVersion = ProgramStatics.SPT_VERSION() ?? sptConfig.SptVersion; + var modName = $"{mod.Author}-${mod.Name}"; + + // Error and prevent loading if sptVersion property is not a valid semver string + if (!(semVer.IsValid(mod.SptVersion) || semVer.IsValidRange(mod.SptVersion))) + { + logger.Error(localisationService.GetText("modloader-invalid_sptversion_field", modName)); + return false; + } + + // Warning and allow loading if semver is not satisfied + if (!semVer.Satisfies(sptVersion, mod.SptVersion)) + { + logger.Error( + localisationService.GetText("modloader-outdated_sptversion_field", new + { + modName = modName, + modVersion = mod.Version, + desiredSptVersion = mod.SptVersion, + }) + ); + + return false; + } + + return true; + } + + /** + * Read loadorder.json (create if doesnt exist) and return sorted list of mods + * @returns string array of sorted mod names + */ + public List SortModsLoadOrder() + { + // if loadorder.json exists: load it, otherwise generate load order + var loadOrderPath = $"{basepath}loadorder.json"; + if (fileUtil.FileExists(loadOrderPath)) + { + return jsonUtil.Deserialize>(fileUtil.ReadFile(loadOrderPath)); + } + + return modLoadOrder.GetLoadOrder(); + } + + /** + * Compile mod and add into class property "imported" + * @param mod Name of mod to compile/add + */ + protected void AddMod(SptMod mod) + { + // Add mod to imported list + imported.Add(mod.PackageJson.Name, mod); + logger.Info( + localisationService.GetText("modloader-loaded_mod", new + { + name = mod.PackageJson.Name, + version = mod.PackageJson.Version, + author = mod.PackageJson.Author, + }) + ); + } + + /** + * Checks if a given mod should be loaded or skipped. + * + * @param pkg mod package.json data + * @returns + */ + protected bool ShouldSkipMod(PackageJsonData pkg) + { + return skippedMods.Contains($"{pkg.Author}-{pkg.Name}"); + } + + protected bool AreModDependenciesFulfilled(PackageJsonData pkg, Dictionary loadedMods) + { + if (pkg.ModDependencies == null) + { + return true; + } + + // used for logging, dont remove + var modName = $"{pkg.Author}-{pkg.Name}"; + + foreach (var (modDependency, requiredVersion) in pkg.ModDependencies) + { + // Raise dependency version incompatible if the dependency is not found in the mod list + if (!loadedMods.ContainsKey(modDependency)) + { + logger.Error( + localisationService.GetText("modloader-missing_dependency", new + { + mod = modName, + modDependency = modDependency + }) + ); + return false; + } + + if (!semVer.Satisfies(loadedMods[modDependency].Version, requiredVersion)) + { + logger.Error( + localisationService.GetText("modloader-outdated_dependency", new + { + mod = modName, + modDependency = modDependency, + currentVersion = loadedMods[modDependency].Version, + requiredVersion = requiredVersion + }) + ); + return false; + } + } + + return true; + } + + protected bool IsModCompatible(PackageJsonData mod, Dictionary loadedMods) + { + var incompatbileModsList = mod.Incompatibilities; + if (incompatbileModsList == null) + { + return true; + } + + foreach (var incompatibleModName in incompatbileModsList) + { + // Raise dependency version incompatible if any incompatible mod is found + if (loadedMods.ContainsKey(incompatibleModName)) + { + logger.Error( + localisationService.GetText("modloader-incompatible_mod_found", new + { + author = mod.Author, + name = mod.Name, + incompatibleModName = incompatibleModName + }) + ); + return false; + } + } + + return true; + } + + /** + * Validate a mod passes a number of checks + * @param modName name of mod in /mods/ to validate + * @returns true if valid + */ + protected bool ValidMod(SptMod mod) + { + var modName = mod.PackageJson.Name; + var modPath = GetModPath(modName); + + var modIsCalledBepinEx = modName.ToLower() == "bepinex"; + var modIsCalledUser = modName.ToLower() == "user"; + var modIsCalledSrc = modName.ToLower() == "src"; + var modIsCalledDb = modName.ToLower() == "db"; + var hasBepinExFolderStructure = fileUtil.DirectoryExists($"{modPath}/plugins"); + var containsJs = fileUtil.GetFiles(modPath, true, "*.js").Count > 0; + var containsTs = fileUtil.GetFiles(modPath, true, "*.ts").Count > 0; + + if (modIsCalledSrc || modIsCalledDb || modIsCalledUser) + { + logger.Error(localisationService.GetText("modloader-not_correct_mod_folder", modName)); + return false; + } + + if (modIsCalledBepinEx || hasBepinExFolderStructure) + { + logger.Error(localisationService.GetText("modloader-is_client_mod", modName)); + return false; + } + + if (containsJs || containsTs) + { + // TODO, needs new localisation! + logger.Error("The mod is an old server mod, JS/TS files detected"); + return false; + } + + // Validate mod + var config = mod.PackageJson; + var issue = false; + + if (!semVer.IsValid(config.Version)) + { + logger.Error(localisationService.GetText("modloader-invalid_version_property", modName)); + issue = true; + } + + return !issue; + } +} diff --git a/Server/Program.cs b/Server/Program.cs index 193de53c..01638bb0 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -2,11 +2,17 @@ using System.Runtime; using Core.Context; using Core.Helpers; using Core.Models.External; +using Core.Models.Spt.Mod; +using Core.Models.Utils; using Core.Utils; using Serilog; using Serilog.Events; using Serilog.Exceptions; using Serilog.Extensions.Logging; +using Server.Logger; +using Server.Modding; +using SptCommon.Semver; +using SptCommon.Semver.Implementations; using SptDependencyInjection; namespace Server; @@ -15,19 +21,22 @@ public static class Program { public static void Main(string[] args) { + // Search for mod dlls var mods = ModDllLoader.LoadAllMods(); - HarmonyBootstrapper.LoadAllPatches(mods.Select(asm => asm.Assembly).ToList()); - var builder = WebApplication.CreateBuilder(args); - builder.Host.UseSerilog(); - - builder.Configuration.AddJsonFile("appsettings.json", true, true); - - CreateAndRegisterLogger(builder, out var registeredLogger); - + // Create web builder and logger + var builder = CreateNewHostBuilder(out var registeredLogger, args); + // Initialize the program variables TODO: this needs to be implemented properly ProgramStatics.Initialize(); + // validate and sort mods, this will also discard any mods that are invalid + var sortedLoadedMods = ValidateMods(mods); + // for harmony we use the original list, as some mods may only be bepinex patches only + HarmonyBootstrapper.LoadAllPatches(mods.Select(asm => asm.Assembly).ToList()); + + // register SPT components DependencyInjectionRegistrator.RegisterSptComponents(typeof(Program).Assembly, typeof(App).Assembly, builder.Services); - DependencyInjectionRegistrator.RegisterModOverrideComponents(builder.Services, mods.Select(a => a.Assembly).ToList()); + // register mod components from the filtered list + DependencyInjectionRegistrator.RegisterModOverrideComponents(builder.Services, sortedLoadedMods.Select(a => a.Assembly).ToList()); var logger = new SerilogLoggerProvider(registeredLogger).CreateLogger("Server"); try { @@ -74,6 +83,33 @@ public static class Program } } + private static WebApplicationBuilder CreateNewHostBuilder(out Serilog.Core.Logger logger, string[]? args = null) + { + var builder = WebApplication.CreateBuilder(args); + builder.Host.UseSerilog(); + builder.Configuration.AddJsonFile("appsettings.json", true, true); + CreateAndRegisterLogger(builder, out logger); + return builder; + } + + private static List ValidateMods(List mods) + { + // We need the SPT dependencies for the ModValidator, but mods are loaded before the web application + // So we create a disposable web application that we will throw away after getting the mods to load + var builder = CreateNewHostBuilder(out _); + // register SPT components + DependencyInjectionRegistrator.RegisterSptComponents(typeof(Program).Assembly, typeof(App).Assembly, builder.Services); + // register the mod validator components + var provider = builder.Services + .AddScoped(typeof(ISptLogger), typeof(SptWebApplicationLogger)) + .AddScoped(typeof(ISemVer), typeof(SemanticVersioningSemVer)) + .AddSingleton() + .AddSingleton() + .BuildServiceProvider(); + var modValidator = provider.GetRequiredService(); + return modValidator.ValidateAndSort(mods); + } + public static void CreateAndRegisterLogger(WebApplicationBuilder builder, out Serilog.Core.Logger logger) { builder.Logging.ClearProviders();