private static QMod CreateFromJsonManifestFile(string subDirectory) { string jsonFile = Path.Combine(subDirectory, "mod.json"); if (!File.Exists(jsonFile)) { return(null); } try { var settings = new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Ignore }; string jsonText = File.ReadAllText(jsonFile); QMod mod = JsonConvert.DeserializeObject <QMod>(jsonText); mod.SubDirectory = subDirectory; return(mod); } catch (Exception e) { Logger.Error($"\"mod.json\" deserialization failed for file \"{jsonFile}\"!"); Logger.Exception(e); return(null); } }
private static QMod CreateFromJsonManifestFile(string subDirectory) { string jsonFile = Path.Combine(subDirectory, "mod.json"); if (!File.Exists(jsonFile)) { return(null); } try { var deserializer = new JsonSerializer { NullValueHandling = NullValueHandling.Ignore, MissingMemberHandling = MissingMemberHandling.Ignore }; string jsonText = File.ReadAllText(jsonFile); using StreamReader sr = new StreamReader(jsonFile); using JsonReader reader = new JsonTextReader(sr); QMod mod = deserializer.Deserialize <QMod>(reader); mod.SubDirectory = subDirectory; return(mod); } catch (Exception e) { Logger.Error($"\"mod.json\" deserialization failed for file \"{jsonFile}\"!"); Logger.Exception(e); return(null); } }
public void LoadAssembly(QMod mod) { string modAssemblyPath = Path.Combine(mod.SubDirectory, mod.AssemblyName); if (string.IsNullOrEmpty(modAssemblyPath) || !File.Exists(modAssemblyPath)) { Logger.Debug($"Did not find a DLL at {modAssemblyPath}"); mod.Status = ModStatus.MissingAssemblyFile; return; } else { try { mod.LoadedAssembly = Assembly.LoadFrom(modAssemblyPath); } catch (Exception aEx) { Logger.Error($"Failed loading the dll found at \"{modAssemblyPath}\" for mod \"{mod.DisplayName}\""); Logger.Exception(aEx); mod.Status = ModStatus.FailedLoadingAssemblyFile; return; } } }
/// <summary> /// Searches through all folders in the provided directory and returns an ordered list of mods to load.<para/> /// Mods that cannot be loaded will have an unsuccessful <see cref="QMod.Status"/> value. /// </summary> /// <param name="qmodsDirectory">The QMods directory</param> /// <returns>A new, sorted <see cref="List{QMod}"/> ready to be initialized or skipped.</returns> public List <QMod> BuildModLoadingList(string qmodsDirectory) { if (!Directory.Exists(qmodsDirectory)) { Logger.Info("QMods directory was not found! Creating..."); Directory.CreateDirectory(qmodsDirectory); return(new List <QMod>(0)); } string[] subDirectories = Directory.GetDirectories(qmodsDirectory); var modSorter = new SortedCollection <string, QMod>(); var earlyErrors = new List <QMod>(subDirectories.Length); foreach (string subDir in subDirectories) { string[] dllFiles = Directory.GetFiles(subDir, "*.dll", SearchOption.TopDirectoryOnly); if (dllFiles.Length < 1) { continue; } string jsonFile = Path.Combine(subDir, "mod.json"); string folderName = new DirectoryInfo(subDir).Name; if (!File.Exists(jsonFile)) { Logger.Error($"Unable to set up mod in folder \"{folderName}\""); earlyErrors.Add(new QModPlaceholder(folderName, ModStatus.InvalidCoreInfo)); continue; } QMod mod = CreateFromJsonManifestFile(subDir); ModStatus status = Validator.ValidateManifest(mod, subDir); if (status != ModStatus.Success) { Logger.Debug($"Mod '{mod.Id}' will not be loaded"); earlyErrors.Add(mod); continue; } Logger.Debug($"Sorting mod {mod.Id}"); bool added = modSorter.AddSorted(mod); if (!added) { Logger.Debug($"DuplicateId on mod {mod.Id}"); mod.Status = ModStatus.DuplicateIdDetected; earlyErrors.Add(mod); } } List <QMod> modsToLoad = modSorter.GetSortedList(); return(CreateModStatusList(earlyErrors, modsToLoad)); }
internal ModStatus FindPatchMethods(QMod qMod) { if (!string.IsNullOrEmpty(qMod.EntryMethod)) { // Legacy string[] entryMethodSig = qMod.EntryMethod.Split('.'); string entryType = string.Join(".", entryMethodSig.Take(entryMethodSig.Length - 1).ToArray()); string entryMethod = entryMethodSig[entryMethodSig.Length - 1]; MethodInfo jsonPatchMethod = qMod.LoadedAssembly.GetType(entryType).GetMethod(entryMethod, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance); if (jsonPatchMethod != null && jsonPatchMethod.GetParameters().Length == 0) { qMod.PatchMethods[PatchingOrder.NormalInitialize] = new QModPatchMethod(jsonPatchMethod, qMod, PatchingOrder.NormalInitialize); } } // QMM 3.0 foreach (Type type in qMod.LoadedAssembly.GetTypes()) { foreach (QModCoreAttribute core in type.GetCustomAttributes(typeof(QModCoreAttribute), false)) { foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance)) { foreach (QModPatchAttributeBase patch in method.GetCustomAttributes(typeof(QModPatchAttributeBase), false)) { switch (patch.PatchOrder) { case PatchingOrder.MetaPreInitialize: case PatchingOrder.MetaPostInitialize: patch.ValidateSecretPassword(method, qMod); break; } if (qMod.PatchMethods.TryGetValue(patch.PatchOrder, out QModPatchMethod extra)) { if (extra.Method.Name != method.Name) { return(ModStatus.TooManyPatchMethods); } } else { qMod.PatchMethods[patch.PatchOrder] = new QModPatchMethod(method, qMod, patch.PatchOrder); } } } } } if (qMod.PatchMethods.Count == 0) { return(ModStatus.MissingPatchMethod); } return(ModStatus.Success); }
internal static List <QMod> CreateModStatusList(List <QMod> earlyErrors, List <QMod> modsToLoad) { var modList = new List <QMod>(modsToLoad.Count + earlyErrors.Count); foreach (QMod mod in modsToLoad) { Logger.Debug($"{mod.Id} ready to load"); modList.Add(mod); } foreach (QMod erroredMod in earlyErrors) { Logger.Debug($"{erroredMod.Id} had an early error"); modList.Add(erroredMod); } foreach (QMod mod in modList) { if (mod.Status != ModStatus.Success) { continue; } if (mod.RequiredMods == null) { continue; } foreach (RequiredQMod requiredMod in mod.RequiredMods) { QMod dependency = modsToLoad.Find(d => d.Id == requiredMod.Id); if (dependency == null || dependency.Status != ModStatus.Success) { mod.Status = ModStatus.MissingDependency; break; } if (dependency.ParsedVersion < requiredMod.MinimumVersion) { mod.Status = ModStatus.OutOfDateDependency; break; } } } return(modList); }
private void ValidateDependencies(List <QMod> modsToLoad, QMod mod) { // Check the mod dependencies foreach (RequiredQMod requiredMod in mod.RequiredMods) { QMod dependencyQMod = modsToLoad.Find(d => d.Id == requiredMod.Id); if (dependencyQMod == null) // QMod for dependency was not found { if (PluginCollection.IsKnownPlugin(mod.Id)) { PluginCollection.MarkAsRequired(mod.Id); continue;// Dependency is a BenInEx plugin, not a QMod, and can be ignored here } else { // Dependency not found Logger.Error($"{mod.Id} cannot be loaded because it is missing a dependency. Missing mod: '{requiredMod.Id}'"); mod.Status = ModStatus.MissingDependency; break; } } if (dependencyQMod.HasDependencies) { // If the dependency has any dependencies itself, make sure they are also okay ValidateDependencies(modsToLoad, dependencyQMod); } if (dependencyQMod.Status != ModStatus.Success) { // Dependency failed - treat as missing Logger.Error($"{mod.Id} cannot be loaded because one or more of its dependencies failed to load. Failed dependency: '{requiredMod.Id}'"); mod.Status = ModStatus.MissingDependency; break; } if (dependencyQMod.ParsedVersion < requiredMod.MinimumVersion) { // Dependency version is older than the version required by this mod Logger.Error($"{mod.Id} cannot be loaded because its dependency is out of date. Outdated mod: {requiredMod.Id}"); mod.Status = ModStatus.OutOfDateDependency; break; } } }
internal void LoadModsFromDirectories(string[] subDirectories, SortedCollection <string, QMod> modSorter, List <QMod> earlyErrors) { foreach (string subDir in subDirectories.Where(subDir => (new DirectoryInfo(subDir).Attributes & FileAttributes.Hidden) != FileAttributes.Hidden)) // exclude hidden directories { string[] dllFiles = Directory.GetFiles(subDir, "*.dll", SearchOption.TopDirectoryOnly); if (dllFiles.Length < 1) { continue; } string jsonFile = Path.Combine(subDir, "mod.json"); string folderName = new DirectoryInfo(subDir).Name; if (!File.Exists(jsonFile)) { Logger.Error($"Unable to set up mod in folder \"{folderName}\""); earlyErrors.Add(new QModPlaceholder(folderName, ModStatus.MissingCoreInfo)); continue; } QMod mod = CreateFromJsonManifestFile(subDir); if (mod == null) { Logger.Error($"Unable to set up mod in folder \"{folderName}\""); earlyErrors.Add(new QModPlaceholder(folderName, ModStatus.MissingCoreInfo)); continue; } this.Validator.CheckRequiredMods(mod); Logger.Debug($"Sorting mod {mod.Id}"); bool added = modSorter.AddSorted(mod); if (!added) { Logger.Debug($"DuplicateId on mod {mod.Id}"); mod.Status = ModStatus.DuplicateIdDetected; earlyErrors.Add(mod); } } }
public void CheckRequiredMods(QMod mod) { foreach (string item in mod.Dependencies) { mod.RequiredDependencies.Add(item); } foreach (string item in mod.LoadBefore) { mod.LoadBeforePreferences.Add(item); } foreach (string item in mod.LoadAfter) { mod.LoadAfterPreferences.Add(item); } if (mod.VersionDependencies.Count > 0) { var versionedDependencies = new List <RequiredQMod>(mod.VersionDependencies.Count); foreach (KeyValuePair <string, string> item in mod.VersionDependencies) { string cleanVersion = VersionRegex.Matches(item.Value)?[0]?.Value; if (string.IsNullOrEmpty(cleanVersion)) { versionedDependencies.Add(new RequiredQMod(item.Key)); } else if (Version.TryParse(cleanVersion, out Version version)) { versionedDependencies.Add(new RequiredQMod(item.Key, version)); } else { versionedDependencies.Add(new RequiredQMod(item.Key)); } mod.RequiredDependencies.Add(item.Key); } mod.RequiredMods = versionedDependencies; } }
private void ValidateDependencies(List <QMod> modsToLoad, QMod mod) { // Check the mod dependencies foreach (RequiredQMod requiredMod in mod.RequiredMods) { QMod dependency = modsToLoad.Find(d => d.Id == requiredMod.Id); if (dependency == null || dependency.Status != ModStatus.Success) { // Dependency not found or failed Logger.Error($"{mod.Id} cannot be loaded because it is missing a dependency. Missing mod: {requiredMod.Id}"); mod.Status = ModStatus.MissingDependency; break; } if (dependency.LoadedAssembly == null) { // Dependency hasn't been validated yet this.Validator.ValidateManifest(dependency); } if (dependency.Status != ModStatus.Success) { // Dependency failed to load successfully // Treat it as missing Logger.Error($"{mod.Id} cannot be loaded because its dependency failed to load. Failed mod: {requiredMod.Id}"); mod.Status = ModStatus.MissingDependency; break; } if (dependency.ParsedVersion < requiredMod.MinimumVersion) { // Dependency version is older than the version required by this mod Logger.Error($"{mod.Id} cannot be loaded because its dependency is out of date. Outdated mod: {requiredMod.Id}"); mod.Status = ModStatus.OutOfDateDependency; break; } } }
public void ValidateBasicManifest(QMod mod) { if (mod.Status != ModStatus.Success) { return; } if (mod.PatchMethods.Count > 0) { return; } Logger.Debug($"Validating mod '{mod.Id}'"); if (string.IsNullOrEmpty(mod.Id) || string.IsNullOrEmpty(mod.DisplayName) || string.IsNullOrEmpty(mod.Author)) { mod.Status = ModStatus.MissingCoreInfo; return; } if (!mod.Enable) { mod.Status = ModStatus.CanceledByUser; return; } if (ProhibitedModIDs.TryGetValue(mod.Id, out ModStatus reason)) { mod.Status = reason; return; } switch (mod.Game) { case "BelowZero": mod.SupportedGame = QModGame.BelowZero; break; case "Both": mod.SupportedGame = QModGame.Both; break; case "Subnautica": mod.SupportedGame = QModGame.Subnautica; break; default: { mod.Status = ModStatus.FailedIdentifyingGame; return; } } try { mod.ParsedVersion = VersionParserService.GetVersion(mod.Version); } catch (Exception vEx) { Logger.Error($"There was an error parsing version \"{mod.Version}\" for mod \"{mod.DisplayName}\""); Logger.Exception(vEx); mod.Status = ModStatus.InvalidCoreInfo; return; } }
public void FindPatchMethods(QMod qMod) { try { if (!string.IsNullOrEmpty(qMod.EntryMethod)) { // Legacy string[] entryMethodSig = qMod.EntryMethod.Split('.'); string entryType = string.Join(".", entryMethodSig.Take(entryMethodSig.Length - 1).ToArray()); string entryMethod = entryMethodSig[entryMethodSig.Length - 1]; MethodInfo jsonPatchMethod = qMod.LoadedAssembly.GetType(entryType)?.GetMethod(entryMethod, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance); if (jsonPatchMethod != null && jsonPatchMethod.GetParameters().Length == 0) { qMod.PatchMethods[PatchingOrder.NormalInitialize] = new QModPatchMethod(jsonPatchMethod, qMod, PatchingOrder.NormalInitialize); qMod.Status = ModStatus.Success; return; } } // QMM 3.0 foreach (Type type in qMod.LoadedAssembly.GetTypes()) { if (type.IsNotPublic || type.IsEnum || type.ContainsGenericParameters) { continue; } foreach (QModCoreAttribute core in type.GetCustomAttributes(typeof(QModCoreAttribute), false)) { foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance)) { foreach (QModPatchAttributeBase patch in method.GetCustomAttributes(typeof(QModPatchAttributeBase), false)) { switch (patch.PatchOrder) { case PatchingOrder.MetaPreInitialize: case PatchingOrder.MetaPostInitialize: if (!patch.ValidateSecretPassword(method, qMod)) { Logger.Error($"The mod {qMod.Id} has an invalid priority patching password."); qMod.PatchMethods.Clear(); qMod.Status = ModStatus.InvalidCoreInfo; return; } break; } if (qMod.PatchMethods.TryGetValue(patch.PatchOrder, out QModPatchMethod extra)) { if (extra.Method.Name != method.Name) { qMod.Status = ModStatus.TooManyPatchMethods; return; } } else { qMod.PatchMethods[patch.PatchOrder] = new QModPatchMethod(method, qMod, patch.PatchOrder); continue; } } } } } } catch (TypeLoadException tlEx) { Logger.Debug($"Unable to load types for '{qMod.Id}': " + tlEx.Message); qMod.Status = ModStatus.MissingDependency; } catch (MissingMethodException mmEx) { Logger.Debug($"Unable to find patch method for '{qMod.Id}': " + mmEx.Message); qMod.Status = ModStatus.MissingDependency; } catch (ReflectionTypeLoadException rtle) { Logger.Debug($"Unable to load types for '{qMod.Id}': \nInnerException: \n" + rtle.InnerException + "\n LoaderExceptions:\n" + string.Join("/n", rtle.LoaderExceptions.ToList())); qMod.Status = ModStatus.MissingDependency; } qMod.Status = qMod.PatchMethods.Count == 0 ? ModStatus.MissingPatchMethod : ModStatus.Success; }
public void CheckRequiredMods(QMod mod) { var requiredMods = new Dictionary <string, RequiredQMod>(mod.VersionDependencies.Count + mod.Dependencies.Length); foreach (string id in mod.Dependencies) { mod.RequiredDependencies.Add(id); requiredMods.Add(id, new RequiredQMod(id)); } foreach (string id in mod.LoadBefore) { mod.LoadBeforePreferences.Add(id); } foreach (string id in mod.LoadAfter) { mod.LoadAfterPreferences.Add(id); } if (mod.VersionDependencies.Count > 0) { foreach (KeyValuePair <string, string> item in mod.VersionDependencies) { string id = item.Key; string versionString = item.Value; Version version = VersionParserService.GetVersion(versionString); requiredMods[id] = new RequiredQMod(id, version); mod.RequiredDependencies.Add(id); } } mod.RequiredMods = requiredMods.Values; if (Logger.DebugLogsEnabled) { string GetModList(IEnumerable <string> modIds) { string modList = string.Empty; foreach (var id in modIds) { modList += $"{id} "; } return(modList); } if (requiredMods.Count > 0) { Logger.Debug($"{mod.Id} has required mods: {GetModList(mod.RequiredMods.Select(mod => mod.Id))}"); } if (mod.LoadBeforePreferences.Count > 0) { Logger.Debug($"{mod.Id} should load before: {GetModList(mod.LoadBeforePreferences)}"); } if (mod.LoadAfterPreferences.Count > 0) { Logger.Debug($"{mod.Id} should load after: {GetModList(mod.LoadAfterPreferences)}"); } } }
public void ValidateManifest(QMod mod) { if (mod.Status != ModStatus.Success) { return; } if (mod.PatchMethods.Count > 0) { return; } Logger.Debug($"Validating mod in '{mod.SubDirectory}'"); if (string.IsNullOrEmpty(mod.Id) || string.IsNullOrEmpty(mod.DisplayName) || string.IsNullOrEmpty(mod.Author)) { mod.Status = ModStatus.MissingCoreInfo; return; } if (!mod.Enable) { mod.Status = ModStatus.CanceledByUser; return; } if (ProhibitedModIDs.TryGetValue(mod.Id, out ModStatus reason)) { mod.Status = reason; return; } switch (mod.Game) { case "BelowZero": mod.SupportedGame = QModGame.BelowZero; break; case "Both": mod.SupportedGame = QModGame.Both; break; case "Subnautica": mod.SupportedGame = QModGame.Subnautica; break; default: { mod.Status = ModStatus.FailedIdentifyingGame; return; } } try { if (Version.TryParse(mod.Version, out Version version)) { mod.ParsedVersion = version; } } catch (Exception vEx) { Logger.Error($"There was an error parsing version \"{mod.Version}\" for mod \"{mod.DisplayName}\""); Logger.Exception(vEx); mod.Status = ModStatus.InvalidCoreInfo; return; } string modAssemblyPath = Path.Combine(mod.SubDirectory, mod.AssemblyName); if (string.IsNullOrEmpty(modAssemblyPath) || !File.Exists(modAssemblyPath)) { Logger.Debug($"Did not find a DLL at {modAssemblyPath}"); mod.Status = ModStatus.MissingAssemblyFile; return; } else { try { mod.LoadedAssembly = Assembly.LoadFrom(modAssemblyPath); } catch (Exception aEx) { Logger.Error($"Failed loading the dll found at \"{modAssemblyPath}\" for mod \"{mod.DisplayName}\""); Logger.Exception(aEx); mod.Status = ModStatus.FailedLoadingAssemblyFile; return; } } ModStatus patchMethodResults = FindPatchMethods(mod); if (patchMethodResults != ModStatus.Success) { mod.Status = patchMethodResults; return; } }
public ModStatus ValidateManifest(QMod mod, string subDirectory) { Logger.Debug($"Validating mod in '{subDirectory}'"); if (string.IsNullOrEmpty(mod.Id) || string.IsNullOrEmpty(mod.DisplayName) || string.IsNullOrEmpty(mod.Author)) { return(mod.Status = ModStatus.MissingCoreInfo); } switch (mod.Game) { case "BelowZero": mod.SupportedGame = QModGame.BelowZero; break; case "Both": mod.SupportedGame = QModGame.Both; break; case "Subnautica": mod.SupportedGame = QModGame.Subnautica; break; default: return(mod.Status = ModStatus.FailedIdentifyingGame); } try { if (System.Version.TryParse(mod.Version, out Version version)) { mod.ParsedVersion = version; } } catch (Exception vEx) { Logger.Error($"There was an error parsing version \"{mod.Version}\" for mod \"{mod.DisplayName}\""); Logger.Exception(vEx); return(mod.Status = ModStatus.InvalidCoreInfo); } string modAssemblyPath = Path.Combine(subDirectory, mod.AssemblyName); if (string.IsNullOrEmpty(modAssemblyPath) || !File.Exists(modAssemblyPath)) { Logger.Debug($"Did not find a DLL at {modAssemblyPath}"); return(mod.Status = ModStatus.MissingAssemblyFile); } else { try { mod.LoadedAssembly = Assembly.LoadFrom(modAssemblyPath); } catch (Exception aEx) { Logger.Error($"Failed loading the dll found at \"{modAssemblyPath}\" for mod \"{mod.DisplayName}\""); Logger.Exception(aEx); return(mod.Status = ModStatus.FailedLoadingAssemblyFile); } } try { ModStatus patchMethodResults = FindPatchMethods(mod); if (patchMethodResults != ModStatus.Success) { return(mod.Status = patchMethodResults); } } catch (Exception ex) { Logger.Exception(ex); return(mod.Status = ModStatus.MissingPatchMethod); } foreach (string item in mod.Dependencies) { mod.RequiredDependencies.Add(item); } foreach (string item in mod.LoadBefore) { mod.LoadBeforePreferences.Add(item); } foreach (string item in mod.LoadAfter) { mod.LoadAfterPreferences.Add(item); } if (mod.VersionDependencies.Count > 0) { var versionedDependencies = new List <RequiredQMod>(mod.VersionDependencies.Count); foreach (KeyValuePair <string, string> item in mod.VersionDependencies) { string cleanVersion = VersionRegex.Matches(item.Value)?[0]?.Value; if (string.IsNullOrEmpty(cleanVersion)) { versionedDependencies.Add(new RequiredQMod(item.Key)); } else if (System.Version.TryParse(cleanVersion, out Version version)) { versionedDependencies.Add(new RequiredQMod(item.Key, version)); } else { versionedDependencies.Add(new RequiredQMod(item.Key)); } } } if (!mod.Enable) { return(mod.Status = ModStatus.CanceledByUser); } return(mod.Status = ModStatus.Success); }
internal QModPatchMethod(MethodInfo method, QMod qmod, PatchingOrder order) { this.Method = method; this.Origin = qmod; this.Order = order; }