renamed projects and namespaces
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
using Serilog.Events;
|
||||
using Serilog.Formatting;
|
||||
|
||||
namespace SPTarkov.Server.Logger;
|
||||
|
||||
public abstract class AbstractFormatter : ITextFormatter
|
||||
{
|
||||
public void Format(LogEvent logEvent, TextWriter output)
|
||||
{
|
||||
var newLine = Environment.NewLine;
|
||||
var timestamp = logEvent.Timestamp.ToString("HH:mm:ss.fff");
|
||||
var logLevel = logEvent.Level.ToString().ToUpper().Substring(0, 4);
|
||||
var message = logEvent.RenderMessage();
|
||||
var exception = logEvent.Exception != null ? $"{newLine}{logEvent.Exception}{newLine}{logEvent.Exception.StackTrace}" : "";
|
||||
var sourceContext = logEvent.Properties["SourceContext"].ToString().Replace("\"", "");
|
||||
var logMessage = ProcessText(GetFormattedText(timestamp, logLevel, sourceContext, $"{message}{exception}"));
|
||||
output.WriteLine(logMessage);
|
||||
}
|
||||
|
||||
protected abstract string ProcessText(string text);
|
||||
|
||||
protected virtual string GetFormattedText(string timestamp, string logLevel, string sourceContext, string message)
|
||||
{
|
||||
return $"[{timestamp} {logLevel}][{sourceContext}] {message}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace SPTarkov.Server.Logger;
|
||||
|
||||
public class ConsoleFormatter : AbstractFormatter
|
||||
{
|
||||
public static ConsoleFormatter Default
|
||||
{
|
||||
get;
|
||||
} = new();
|
||||
|
||||
protected override string ProcessText(string text)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
protected override string GetFormattedText(string timestamp, string logLevel, string sourceContext, string message)
|
||||
{
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SPTarkov.Server.Logger;
|
||||
|
||||
public class FileFormatter : AbstractFormatter
|
||||
{
|
||||
public static FileFormatter Default
|
||||
{
|
||||
get;
|
||||
} = new();
|
||||
|
||||
protected override string ProcessText(string message)
|
||||
{
|
||||
foreach (Match match in Regex.Matches(message, @"\x1b\[[0-9]{1,2}(;[0-1]{1,2}){0,1}m"))
|
||||
{
|
||||
message = message.Replace(match.Value, "");
|
||||
}
|
||||
|
||||
return message.Replace("\"", "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using SPTarkov.Server.Core.Models.Logging;
|
||||
using SPTarkov.Server.Core.Models.Utils;
|
||||
using SPTarkov.Server.Core.Utils.Logger;
|
||||
using SPTarkov.Common.Annotations;
|
||||
using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel;
|
||||
|
||||
namespace SPTarkov.Server.Logger;
|
||||
|
||||
[Injectable]
|
||||
public class SptWebApplicationLogger<T> : ISptLogger<T>
|
||||
{
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private static ILogger? _fileLogger;
|
||||
|
||||
public SptWebApplicationLogger(ILoggerProvider provider)
|
||||
{
|
||||
_logger = provider.CreateLogger(typeof(T).FullName ?? "SPT Logger Default Name");
|
||||
if (_fileLogger == null)
|
||||
{
|
||||
_fileLogger = provider.CreateLogger(typeof(FileLogger).FullName ?? "SPT Logger Default Name");
|
||||
}
|
||||
}
|
||||
|
||||
public void LogWithColor(
|
||||
string data,
|
||||
LogTextColor? textColor = null,
|
||||
LogBackgroundColor? backgroundColor = null,
|
||||
Exception? ex = null
|
||||
)
|
||||
{
|
||||
if (textColor != null || backgroundColor != null)
|
||||
{
|
||||
_logger.LogInformation(ex, GetColorizedText(data, textColor, backgroundColor));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation(ex, data);
|
||||
}
|
||||
}
|
||||
|
||||
public void Success(string data, Exception? ex = null)
|
||||
{
|
||||
_logger.LogInformation(ex, GetColorizedText(data, LogTextColor.Green));
|
||||
}
|
||||
|
||||
public void Error(string data, Exception? ex = null)
|
||||
{
|
||||
_logger.LogError(ex, GetColorizedText(data, LogTextColor.Red));
|
||||
}
|
||||
|
||||
public void Warning(string data, Exception? ex = null)
|
||||
{
|
||||
_logger.LogWarning(ex, GetColorizedText(data, LogTextColor.Yellow));
|
||||
}
|
||||
|
||||
public void Info(string data, Exception? ex = null)
|
||||
{
|
||||
_logger.LogInformation(ex, data);
|
||||
}
|
||||
|
||||
public void Debug(string data, Exception? ex = null)
|
||||
{
|
||||
_logger.LogDebug(ex, data);
|
||||
}
|
||||
|
||||
public void Critical(string data, Exception? ex = null)
|
||||
{
|
||||
_logger.LogCritical(ex, GetColorizedText(data, LogTextColor.Black, LogBackgroundColor.Red));
|
||||
}
|
||||
|
||||
public void WriteToLogFile(string body, LogLevel level = LogLevel.Info)
|
||||
{
|
||||
_fileLogger?.Log(ConvertLogLevel(level), body);
|
||||
}
|
||||
|
||||
public bool IsLogEnabled(LogLevel level)
|
||||
{
|
||||
return _logger.IsEnabled(ConvertLogLevel(level));
|
||||
}
|
||||
|
||||
private string GetColorizedText(
|
||||
string data,
|
||||
LogTextColor? textColor = null,
|
||||
LogBackgroundColor? backgroundColor = null
|
||||
)
|
||||
{
|
||||
var colorString = string.Empty;
|
||||
if (textColor != null)
|
||||
{
|
||||
colorString += ((int) textColor.Value).ToString();
|
||||
}
|
||||
|
||||
if (backgroundColor != null)
|
||||
{
|
||||
colorString += string.IsNullOrEmpty(colorString)
|
||||
? ((int) backgroundColor.Value).ToString()
|
||||
: $";{((int) backgroundColor.Value).ToString()}";
|
||||
}
|
||||
|
||||
return $"\x1b[{colorString}m{data}\x1b[0m";
|
||||
}
|
||||
|
||||
protected Microsoft.Extensions.Logging.LogLevel ConvertLogLevel(LogLevel level)
|
||||
{
|
||||
return level switch
|
||||
{
|
||||
LogLevel.Trace => Microsoft.Extensions.Logging.LogLevel.Trace,
|
||||
LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug,
|
||||
LogLevel.Success
|
||||
or LogLevel.Info
|
||||
or LogLevel.Custom => Microsoft.Extensions.Logging.LogLevel.Information,
|
||||
LogLevel.Warn => Microsoft.Extensions.Logging.LogLevel.Warning,
|
||||
LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error,
|
||||
LogLevel.Fatal => Microsoft.Extensions.Logging.LogLevel.Critical,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Reflection;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace SPTarkov.Server.Modding;
|
||||
|
||||
public class HarmonyBootstrapper
|
||||
{
|
||||
public static void LoadAllPatches(List<Assembly> assemblies)
|
||||
{
|
||||
var hamony = new Harmony("SPT");
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
try
|
||||
{
|
||||
hamony.PatchAll(assembly);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using System.Text.Json;
|
||||
using SPTarkov.Server.Core.Models.Spt.Mod;
|
||||
|
||||
namespace SPTarkov.Server.Modding;
|
||||
|
||||
public class ModDllLoader
|
||||
{
|
||||
private const string ModPath = "./user/mods/";
|
||||
|
||||
public static List<SptMod> LoadAllMods()
|
||||
{
|
||||
if (!Directory.Exists(ModPath))
|
||||
{
|
||||
Directory.CreateDirectory(ModPath);
|
||||
}
|
||||
|
||||
var mods = new List<SptMod>();
|
||||
|
||||
// foreach directory in /user/mods/
|
||||
// treat this as the MOD
|
||||
// should contain a dll and Package.json
|
||||
// load both
|
||||
// if either is missing Throw Warning and skip
|
||||
|
||||
var modDirectories = Directory.GetDirectories(ModPath);
|
||||
|
||||
// Load mods found in dir
|
||||
foreach (var modDirectory in modDirectories)
|
||||
{
|
||||
try
|
||||
{
|
||||
mods.Add(LoadMod(modDirectory));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return mods;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check the provided directory path for a dll and .json file, load into memory
|
||||
/// </summary>
|
||||
/// <param name="path">Directory path that contains mod files</param>
|
||||
/// <returns>SptMod</returns>
|
||||
private static SptMod LoadMod(string path)
|
||||
{
|
||||
var result = new SptMod
|
||||
{
|
||||
Directory = path,
|
||||
Assemblies = []
|
||||
};
|
||||
var asmCount = 0;
|
||||
var packCount = 0;
|
||||
foreach (var file in new DirectoryInfo(path).GetFiles()) // only search top level
|
||||
{
|
||||
if (file.Name.ToLower() == "package.json")
|
||||
{
|
||||
packCount++;
|
||||
|
||||
// Handle package.json
|
||||
var rawJson = File.ReadAllText(file.FullName);
|
||||
result.PackageJson = JsonSerializer.Deserialize<PackageJsonData>(rawJson);
|
||||
if (packCount > 1)
|
||||
{
|
||||
throw new Exception($"More than one package.json file found in path: {path}");
|
||||
}
|
||||
}
|
||||
|
||||
if (file.Extension.ToLower() == ".dll")
|
||||
{
|
||||
asmCount++;
|
||||
result.Assemblies.Add(AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(file.FullName)));
|
||||
}
|
||||
}
|
||||
|
||||
if (asmCount == 0 && packCount == 0)
|
||||
{
|
||||
throw new Exception($"No Assembly or package.json found in: {Path.GetFullPath(path)}");
|
||||
}
|
||||
|
||||
if (packCount == 0)
|
||||
{
|
||||
throw new Exception($"No package.json found in path: {Path.GetFullPath(path)}");
|
||||
}
|
||||
|
||||
if (asmCount == 0)
|
||||
{
|
||||
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.Assemblies is not null && result.PackageJson is not null)
|
||||
{
|
||||
Console.WriteLine($"Loaded: {result.PackageJson.Name} Version: {result.PackageJson.Version} by: {result.PackageJson.Author}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using SPTarkov.Server.Core.Models.Spt.Mod;
|
||||
using SPTarkov.Server.Core.Utils.Cloners;
|
||||
|
||||
namespace SPTarkov.Server.Modding;
|
||||
|
||||
public class ModLoadOrder(ICloner cloner)
|
||||
{
|
||||
protected Dictionary<string, PackageJsonData> mods = new();
|
||||
protected Dictionary<string, PackageJsonData> modsAvailable = new();
|
||||
protected Dictionary<string, PackageJsonData> loadOrder = new();
|
||||
|
||||
public Dictionary<string, PackageJsonData> SetModList(Dictionary<string, PackageJsonData> mods)
|
||||
{
|
||||
this.mods = mods;
|
||||
modsAvailable = cloner.Clone(this.mods);
|
||||
loadOrder = new Dictionary<string, PackageJsonData>();
|
||||
|
||||
var visited = new HashSet<string>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
return loadOrder;
|
||||
}
|
||||
|
||||
public List<string> GetLoadOrder()
|
||||
{
|
||||
return [..loadOrder.Keys];
|
||||
}
|
||||
|
||||
public HashSet<string> GetModsOnLoadBefore(string mod)
|
||||
{
|
||||
if (!mods.ContainsKey(mod))
|
||||
{
|
||||
throw new Exception($"The mod {mod} does not exist!");
|
||||
}
|
||||
|
||||
var config = mods[mod];
|
||||
|
||||
var loadBefore = new HashSet<string>(config.LoadBefore);
|
||||
|
||||
foreach (var loadBeforeMod in loadBefore)
|
||||
{
|
||||
if (!mods.ContainsKey(loadBeforeMod))
|
||||
{
|
||||
loadBefore.Remove(loadBeforeMod);
|
||||
}
|
||||
}
|
||||
|
||||
return loadBefore;
|
||||
}
|
||||
|
||||
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<string> visited)
|
||||
{
|
||||
// Validate package
|
||||
if (loadOrder.ContainsKey(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<string>(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, config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
using SPTarkov.Server.Core.Models.Spt.Config;
|
||||
using SPTarkov.Server.Core.Models.Spt.Mod;
|
||||
using SPTarkov.Server.Core.Models.Utils;
|
||||
using SPTarkov.Server.Core.Servers;
|
||||
using SPTarkov.Server.Core.Services;
|
||||
using SPTarkov.Server.Core.Utils;
|
||||
using SPTarkov.Common.Semver;
|
||||
using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel;
|
||||
|
||||
namespace SPTarkov.Server.Modding;
|
||||
|
||||
public class ModValidator(
|
||||
ISptLogger<ModValidator> 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<string, int> order = [];
|
||||
protected Dictionary<string, SptMod> imported = [];
|
||||
protected HashSet<string> skippedMods = [];
|
||||
|
||||
protected CoreConfig sptConfig = configServer.GetConfig<CoreConfig>();
|
||||
|
||||
public List<SptMod> ValidateAndSort(List<SptMod> mods)
|
||||
{
|
||||
if (ProgramStatics.MODS())
|
||||
{
|
||||
ValidateMods(mods);
|
||||
|
||||
var sortedModLoadOrder = modLoadOrder.SetModList(imported.ToDictionary(m => m.Key, m => m.Value.PackageJson));
|
||||
var finalList = new List<SptMod>();
|
||||
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<SptMod> 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>(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 (!IsModCompatibleWithSpt(modToValidate))
|
||||
{
|
||||
errorsFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorsFound)
|
||||
{
|
||||
logger.Error(localisationService.GetText("modloader-no_mods_loaded"));
|
||||
return;
|
||||
}
|
||||
|
||||
// sort mod order
|
||||
var missingFromOrderJSON = new Dictionary<string, bool>();
|
||||
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<string, bool> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check for duplicate mods loaded, show error if any
|
||||
/// </summary>
|
||||
/// <param name="modPackageData">Dictionary of mod package.json data</param>
|
||||
protected void CheckForDuplicateMods(Dictionary<string, PackageJsonData> modPackageData)
|
||||
{
|
||||
var groupedMods = new Dictionary<string, List<PackageJsonData>>();
|
||||
|
||||
foreach (var mod in modPackageData.Values)
|
||||
{
|
||||
var name = $"{mod.Author}-{mod.Name}";
|
||||
groupedMods.Add(name, [..(groupedMods.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 (groupedMods[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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an array of valid mods
|
||||
/// </summary>
|
||||
/// <param name="mods">mods to validate</param>
|
||||
/// <returns>array of mod folder names</returns>
|
||||
protected List<SptMod> GetValidMods(List<SptMod> mods)
|
||||
{
|
||||
return mods.Where(ValidMod).ToList();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Is the passed in mod compatible with the running server version
|
||||
/// </summary>
|
||||
/// <param name="mod">Mod to check compatibility with SPT</param>
|
||||
/// <returns>True if compatible</returns>
|
||||
protected bool IsModCompatibleWithSpt(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read loadorder.json (create if doesnt exist) and return sorted list of mods
|
||||
/// </summary>
|
||||
/// <returns>string array of sorted mod names</returns>
|
||||
public List<string> SortModsLoadOrder()
|
||||
{
|
||||
// if loadorder.json exists: load it, otherwise generate load order
|
||||
var loadOrderPath = $"{basepath}loadorder.json";
|
||||
if (fileUtil.FileExists(loadOrderPath))
|
||||
{
|
||||
return jsonUtil.Deserialize<List<string>>(fileUtil.ReadFile(loadOrderPath));
|
||||
}
|
||||
|
||||
return modLoadOrder.GetLoadOrder();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compile mod and add into class property "imported"
|
||||
/// </summary>
|
||||
/// <param name="mod">Name of mod to compile/add</param>
|
||||
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,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given mod should be loaded or skipped
|
||||
/// </summary>
|
||||
/// <param name="pkg">mod package.json data</param>
|
||||
/// <returns></returns>
|
||||
protected bool ShouldSkipMod(PackageJsonData pkg)
|
||||
{
|
||||
return skippedMods.Contains($"{pkg.Author}-{pkg.Name}");
|
||||
}
|
||||
|
||||
protected bool AreModDependenciesFulfilled(PackageJsonData pkg, Dictionary<string, PackageJsonData> 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<string, PackageJsonData> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate a mod passes a number of checks
|
||||
/// </summary>
|
||||
/// <param name="mod">name of mod in /mods/ to validate</param>
|
||||
/// <returns>true if valid</returns>
|
||||
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($"{mod.Directory}/plugins");
|
||||
var containsJs = fileUtil.GetFiles(mod.Directory, true, "*.js").Count > 0;
|
||||
var containsTs = fileUtil.GetFiles(mod.Directory, 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
using System.Runtime;
|
||||
using SPTarkov.Server.Core.Context;
|
||||
using SPTarkov.Server.Core.Helpers;
|
||||
using SPTarkov.Server.Core.Models.External;
|
||||
using SPTarkov.Server.Core.Models.Spt.Mod;
|
||||
using SPTarkov.Server.Core.Models.Utils;
|
||||
using SPTarkov.Server.Core.Utils;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using Serilog.Exceptions;
|
||||
using Serilog.Extensions.Logging;
|
||||
using SPTarkov.Common.Semver;
|
||||
using SPTarkov.Common.Semver.Implementations;
|
||||
using SPTarkov.Server.Logger;
|
||||
using SPTarkov.Server.Modding;
|
||||
using SPTarkov.DI;
|
||||
|
||||
namespace SPTarkov.Server;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
// Search for mod dlls
|
||||
var mods = ModDllLoader.LoadAllMods();
|
||||
// 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.SelectMany(asm => asm.Assemblies).ToList());
|
||||
|
||||
// register SPT components
|
||||
DependencyInjectionRegistrator.RegisterSptComponents(typeof(Program).Assembly, typeof(App).Assembly, builder.Services);
|
||||
// register mod components from the filtered list
|
||||
DependencyInjectionRegistrator.RegisterModOverrideComponents(builder.Services, sortedLoadedMods.SelectMany(a => a.Assemblies).ToList());
|
||||
var logger = new SerilogLoggerProvider(registeredLogger).CreateLogger("Server");
|
||||
try
|
||||
{
|
||||
var serviceProvider = builder.Services.BuildServiceProvider();
|
||||
var watermark = serviceProvider.GetService<Watermark>();
|
||||
// Initialize Watermak
|
||||
watermark?.Initialize();
|
||||
|
||||
// Initialize PreSptMods
|
||||
var preSptLoadMods = serviceProvider.GetServices<IPreSptLoadMod>();
|
||||
foreach (var preSptLoadMod in preSptLoadMods)
|
||||
{
|
||||
preSptLoadMod.PreSptLoad();
|
||||
}
|
||||
|
||||
var appContext = serviceProvider.GetService<ApplicationContext>();
|
||||
// Add the Loaded Mod Assemblies for later
|
||||
appContext?.AddValue(ContextVariableType.LOADED_MOD_ASSEMBLIES, mods);
|
||||
// This is the builder that will get use by the HttpServer to start up the web application
|
||||
appContext?.AddValue(ContextVariableType.APP_BUILDER, builder);
|
||||
|
||||
// Get the Built app and run it
|
||||
var app = serviceProvider.GetService<App>();
|
||||
app?.Run().Wait();
|
||||
|
||||
// Run garbage collection now server is ready to start
|
||||
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
|
||||
|
||||
|
||||
var httpServerHelper = serviceProvider.GetService<HttpServerHelper>();
|
||||
// When the application is started by the HttpServer it will be added into the AppContext of the WebApplication
|
||||
// object, which we can use here to start the webapp.
|
||||
if (httpServerHelper != null)
|
||||
{
|
||||
appContext?.GetLatestValue(ContextVariableType.WEB_APPLICATION)?.GetValue<WebApplication>().Run();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
logger.LogCritical(ex, "Critical exception, stopping server...");
|
||||
// throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
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<SptMod> ValidateMods(List<SptMod> 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<ModValidator>), typeof(SptWebApplicationLogger<ModValidator>))
|
||||
.AddScoped(typeof(ISemVer), typeof(SemanticVersioningSemVer))
|
||||
.AddSingleton<ModValidator>()
|
||||
.AddSingleton<ModLoadOrder>()
|
||||
.BuildServiceProvider();
|
||||
var modValidator = provider.GetRequiredService<ModValidator>();
|
||||
return modValidator.ValidateAndSort(mods);
|
||||
}
|
||||
|
||||
public static void CreateAndRegisterLogger(WebApplicationBuilder builder, out Serilog.Core.Logger logger)
|
||||
{
|
||||
builder.Logging.ClearProviders();
|
||||
logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(builder.Configuration)
|
||||
# if DEBUG
|
||||
.MinimumLevel.Debug()
|
||||
# endif
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Hosting.Diagnostics", LogEventLevel.Warning)
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.WithThreadId()
|
||||
.Enrich.WithExceptionDetails()
|
||||
.CreateLogger();
|
||||
builder.Logging.AddSerilog(logger);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageId>SPTarkov.Server</PackageId>
|
||||
<Authors>Single Player Tarkov</Authors>
|
||||
<Description>Single Player Tarkov server launcher.</Description>
|
||||
<Copyright>Copyright (c) Single Player Tarkov 2025</Copyright>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageProjectUrl>https://sp-tarkov.com</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/sp-tarkov/server-csharp</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||
<EnableDefaultContentItems>false</EnableDefaultContentItems>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<Version>4.0.0</Version>
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Libraries\SPTarkov.Server.Core\SPTarkov.Server.Core.csproj" />
|
||||
<ProjectReference Include="..\Libraries\SPTarkov.Server.Assets\SPTarkov.Server.Assets.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HarmonyX" Version="2.14.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Context" Version="4.6.5" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
|
||||
<PackageReference Include="Serilog.Expressions" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\**">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Remove="appsettings.json" />
|
||||
<Content Include="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\LICENSE" Pack="true" Visible="false" PackagePath=""/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Trace",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information",
|
||||
"Microsoft.AspNetCore.Hosting.Diagnostics": "Warning",
|
||||
"Microsoft.AspNetCore.Server.Kestrel.Connections": "Information"
|
||||
}
|
||||
},
|
||||
"Serilog": {
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Async",
|
||||
"Args": {
|
||||
"configure": [
|
||||
{
|
||||
"Name": "Logger",
|
||||
"Args": {
|
||||
"configureLogger": {
|
||||
"Filter": [
|
||||
{
|
||||
"Name": "ByExcluding",
|
||||
"Args": {
|
||||
"expression": "StartsWith(SourceContext, 'Core.Servers.Http.RequestLogger')"
|
||||
}
|
||||
}
|
||||
],
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Logger",
|
||||
"Args": {
|
||||
"configureLogger": {
|
||||
"Filter": [
|
||||
{
|
||||
"Name": "ByExcluding",
|
||||
"Args": {
|
||||
"expression": "StartsWith(SourceContext, 'Core.Utils.Logger.FileLogger')"
|
||||
}
|
||||
}
|
||||
],
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"formatter": "Server.Logger.ConsoleFormatter::Default, Server"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "File",
|
||||
"Args": {
|
||||
"formatter": "Server.Logger.FileFormatter::Default, Server",
|
||||
"path": "./user/logs/log.txt",
|
||||
"fileSizeLimitBytes": "20971520",
|
||||
"rollOnFileSizeLimit": true,
|
||||
"rollingInterval": "Day"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "Logger",
|
||||
"Args": {
|
||||
"configureLogger": {
|
||||
"Filter": [
|
||||
{
|
||||
"Name": "ByIncludingOnly",
|
||||
"Args": {
|
||||
"expression": "StartsWith(SourceContext, 'Core.Servers.Http.RequestLogger')"
|
||||
}
|
||||
}
|
||||
],
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "File",
|
||||
"Args": {
|
||||
"formatter": "Server.Logger.FileFormatter::Default, Server",
|
||||
"path": "./user/logs/requests.txt",
|
||||
"fileSizeLimitBytes": "20971520",
|
||||
"rollOnFileSizeLimit": true,
|
||||
"rollingInterval": "Day"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"Enrich": [
|
||||
"FromLogContext",
|
||||
"WithExceptionDetails"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user