move mod metadata resolution into its own class (#285)
This commit is contained in:
parent
c932c53137
commit
66d2b5746a
|
@ -0,0 +1,300 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using StardewModdingAPI.Framework.Models;
|
||||||
|
using StardewModdingAPI.Framework.Serialisation;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI.Framework.ModLoading
|
||||||
|
{
|
||||||
|
/// <summary>Finds and processes mod metadata.</summary>
|
||||||
|
internal class ModResolver
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Properties
|
||||||
|
*********/
|
||||||
|
/// <summary>Encapsulates monitoring and logging.</summary>
|
||||||
|
private readonly IMonitor Monitor;
|
||||||
|
|
||||||
|
/// <summary>Manages deprecation warnings.</summary>
|
||||||
|
private readonly DeprecationManager DeprecationManager;
|
||||||
|
|
||||||
|
/// <summary>Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</summary>
|
||||||
|
private readonly ModCompatibility[] CompatibilityRecords;
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Public methods
|
||||||
|
*********/
|
||||||
|
public ModResolver(IMonitor monitor, DeprecationManager deprecationManager, IEnumerable<ModCompatibility> compatibilityRecords)
|
||||||
|
{
|
||||||
|
this.Monitor = monitor;
|
||||||
|
this.DeprecationManager = deprecationManager;
|
||||||
|
this.CompatibilityRecords = compatibilityRecords.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Find all mods in the given folder.</summary>
|
||||||
|
/// <param name="rootPath">The root mod path to search.</param>
|
||||||
|
/// <param name="jsonHelper">The JSON helper with which to read the manifest file.</param>
|
||||||
|
/// <param name="deprecationWarnings">A list to populate with any deprecation warnings.</param>
|
||||||
|
public ModMetadata[] FindMods(string rootPath, JsonHelper jsonHelper, IList<Action> deprecationWarnings)
|
||||||
|
{
|
||||||
|
this.Monitor.Log("Finding mods...");
|
||||||
|
void LogSkip(string displayName, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {displayName} because {reasonPhrase}", level);
|
||||||
|
|
||||||
|
// load mod metadata
|
||||||
|
List<ModMetadata> mods = new List<ModMetadata>();
|
||||||
|
foreach (string modRootPath in Directory.GetDirectories(rootPath))
|
||||||
|
{
|
||||||
|
if (this.Monitor.IsExiting)
|
||||||
|
return new ModMetadata[0]; // exit in progress
|
||||||
|
|
||||||
|
// init metadata
|
||||||
|
string displayName = modRootPath.Replace(rootPath, "").Trim('/', '\\');
|
||||||
|
|
||||||
|
// passthrough empty directories
|
||||||
|
DirectoryInfo directory = new DirectoryInfo(modRootPath);
|
||||||
|
while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1)
|
||||||
|
directory = directory.GetDirectories().First();
|
||||||
|
|
||||||
|
// get manifest path
|
||||||
|
string manifestPath = Path.Combine(directory.FullName, "manifest.json");
|
||||||
|
if (!File.Exists(manifestPath))
|
||||||
|
{
|
||||||
|
LogSkip(displayName, "it doesn't have a manifest.", LogLevel.Warn);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read manifest
|
||||||
|
Manifest manifest;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// read manifest file
|
||||||
|
string json = File.ReadAllText(manifestPath);
|
||||||
|
if (string.IsNullOrEmpty(json))
|
||||||
|
{
|
||||||
|
LogSkip(displayName, "its manifest is empty.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse manifest
|
||||||
|
manifest = jsonHelper.ReadJsonFile<Manifest>(Path.Combine(directory.FullName, "manifest.json"));
|
||||||
|
if (manifest == null)
|
||||||
|
{
|
||||||
|
LogSkip(displayName, "its manifest is invalid.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate manifest
|
||||||
|
if (string.IsNullOrWhiteSpace(manifest.EntryDll))
|
||||||
|
{
|
||||||
|
LogSkip(displayName, "its manifest doesn't set an entry DLL.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(manifest.UniqueID))
|
||||||
|
deprecationWarnings.Add(() => this.Monitor.Log($"{manifest.Name} doesn't have a {nameof(IManifest.UniqueID)} in its manifest. This will be required in an upcoming SMAPI release.", LogLevel.Warn));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogSkip(displayName, $"parsing its manifest failed:\n{ex.GetLogSummary()}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(manifest.Name))
|
||||||
|
displayName = manifest.Name;
|
||||||
|
|
||||||
|
// validate compatibility
|
||||||
|
ModCompatibility compatibility = this.GetCompatibilityRecord(manifest);
|
||||||
|
if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken)
|
||||||
|
{
|
||||||
|
bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl);
|
||||||
|
bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl);
|
||||||
|
|
||||||
|
string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game";
|
||||||
|
string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:";
|
||||||
|
if (hasOfficialUrl)
|
||||||
|
error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}";
|
||||||
|
if (hasUnofficialUrl)
|
||||||
|
error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}";
|
||||||
|
|
||||||
|
LogSkip(displayName, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate SMAPI version
|
||||||
|
if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion);
|
||||||
|
if (minVersion.IsNewerThan(Constants.ApiVersion))
|
||||||
|
{
|
||||||
|
LogSkip(displayName, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version"))
|
||||||
|
{
|
||||||
|
LogSkip(displayName, $"it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create per-save directory
|
||||||
|
if (manifest.PerSaveConfigs)
|
||||||
|
{
|
||||||
|
deprecationWarnings.Add(() => this.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string psDir = Path.Combine(directory.FullName, "psconfigs");
|
||||||
|
Directory.CreateDirectory(psDir);
|
||||||
|
if (!Directory.Exists(psDir))
|
||||||
|
{
|
||||||
|
LogSkip(displayName, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogSkip(displayName, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate DLL path
|
||||||
|
string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll);
|
||||||
|
if (!File.Exists(assemblyPath))
|
||||||
|
{
|
||||||
|
LogSkip(displayName, $"its DLL '{manifest.EntryDll}' doesn't exist.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add mod metadata
|
||||||
|
mods.Add(new ModMetadata(displayName, directory.FullName, manifest, compatibility));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.HandleModDependencies(mods.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Private methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Get metadata that indicates whether SMAPI should assume the mod is compatible or broken, regardless of whether it detects incompatible code.</summary>
|
||||||
|
/// <param name="manifest">The mod manifest.</param>
|
||||||
|
/// <returns>Returns the incompatibility record if applicable, else <c>null</c>.</returns>
|
||||||
|
private ModCompatibility GetCompatibilityRecord(IManifest manifest)
|
||||||
|
{
|
||||||
|
string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll;
|
||||||
|
return (
|
||||||
|
from mod in this.CompatibilityRecords
|
||||||
|
where
|
||||||
|
mod.ID == key
|
||||||
|
&& (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion))
|
||||||
|
&& !manifest.Version.IsNewerThan(mod.UpperSemanticVersion)
|
||||||
|
select mod
|
||||||
|
).FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Sort a set of mods by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies.</summary>
|
||||||
|
/// <param name="mods">The mods to process.</param>
|
||||||
|
private ModMetadata[] HandleModDependencies(ModMetadata[] mods)
|
||||||
|
{
|
||||||
|
this.Monitor.Log("Checking mod dependencies...");
|
||||||
|
var unsortedMods = mods.ToList();
|
||||||
|
var sortedMods = new Stack<ModMetadata>();
|
||||||
|
var visitedMods = new bool[unsortedMods.Count];
|
||||||
|
var currentChain = new List<ModMetadata>();
|
||||||
|
bool success = true;
|
||||||
|
|
||||||
|
for (int index = 0; index < unsortedMods.Count; index++)
|
||||||
|
{
|
||||||
|
success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods);
|
||||||
|
if (!success)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
// Failed to sort list, return no mods.
|
||||||
|
this.Monitor.Log("No mods will be loaded.", LogLevel.Error);
|
||||||
|
return new ModMetadata[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortedMods.Reverse().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Sort a mod's dependencies by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies.</summary>
|
||||||
|
/// <param name="modIndex">The index of the mod being processed in the <paramref name="unsortedMods"/>.</param>
|
||||||
|
/// <param name="visitedMods">The mods which have been processed.</param>
|
||||||
|
/// <param name="sortedMods">The list in which to save mods sorted by dependency order.</param>
|
||||||
|
/// <param name="currentChain">The current change of mod dependencies.</param>
|
||||||
|
/// <param name="unsortedMods">The mods remaining to sort.</param>
|
||||||
|
/// <returns>Returns whether the mod can be loaded.</returns>
|
||||||
|
private bool HandleModDependencies(int modIndex, bool[] visitedMods, Stack<ModMetadata> sortedMods, List<ModMetadata> currentChain, List<ModMetadata> unsortedMods)
|
||||||
|
{
|
||||||
|
// visit mod
|
||||||
|
if (visitedMods[modIndex])
|
||||||
|
return true; // already sorted
|
||||||
|
ModMetadata mod = unsortedMods[modIndex];
|
||||||
|
visitedMods[modIndex] = true;
|
||||||
|
|
||||||
|
// process dependencies
|
||||||
|
bool success = true;
|
||||||
|
if (mod.Manifest.Dependencies != null && mod.Manifest.Dependencies.Any())
|
||||||
|
{
|
||||||
|
// validate required dependencies are present
|
||||||
|
{
|
||||||
|
string missingMods = null;
|
||||||
|
foreach (IManifestDependency dependency in mod.Manifest.Dependencies)
|
||||||
|
{
|
||||||
|
if (!unsortedMods.Any(m => m.Manifest.UniqueID.Equals(dependency.UniqueID)))
|
||||||
|
missingMods += $"{dependency.UniqueID}, ";
|
||||||
|
}
|
||||||
|
if (missingMods != null)
|
||||||
|
{
|
||||||
|
this.Monitor.Log($"Skipped {mod.DisplayName} because it requires mods which aren't installed ({missingMods.Substring(0, missingMods.Length - 2)}).", LogLevel.Error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get mods which should be loaded before this one
|
||||||
|
ModMetadata[] modsToLoadFirst =
|
||||||
|
(
|
||||||
|
from unsorted in unsortedMods
|
||||||
|
where mod.Manifest.Dependencies.Any(required => required.UniqueID == unsorted.Manifest.UniqueID)
|
||||||
|
select unsorted
|
||||||
|
)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// detect circular references
|
||||||
|
ModMetadata circularReferenceMod = currentChain.FirstOrDefault(modsToLoadFirst.Contains);
|
||||||
|
if (circularReferenceMod != null)
|
||||||
|
{
|
||||||
|
this.Monitor.Log($"Skipped {mod.DisplayName} because its dependencies have a circular reference: {string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {circularReferenceMod.DisplayName}).", LogLevel.Error);
|
||||||
|
string chain = $"{mod.Manifest.UniqueID} -> {circularReferenceMod.Manifest.UniqueID}";
|
||||||
|
for (int i = currentChain.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
chain = $"{currentChain[i].Manifest.UniqueID} -> " + chain;
|
||||||
|
if (currentChain[i].Manifest.UniqueID.Equals(mod.Manifest.UniqueID)) break;
|
||||||
|
}
|
||||||
|
this.Monitor.Log(chain, LogLevel.Error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
currentChain.Add(mod);
|
||||||
|
|
||||||
|
// recursively sort dependencies
|
||||||
|
foreach (ModMetadata requiredMod in modsToLoadFirst)
|
||||||
|
{
|
||||||
|
int index = unsortedMods.IndexOf(requiredMod);
|
||||||
|
success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods);
|
||||||
|
if (!success)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark mod sorted
|
||||||
|
sortedMods.Push(mod);
|
||||||
|
currentChain.Remove(mod);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using StardewModdingAPI.Framework.Models;
|
|
||||||
|
|
||||||
namespace StardewModdingAPI.Framework
|
namespace StardewModdingAPI.Framework
|
||||||
{
|
{
|
||||||
|
@ -19,21 +18,10 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <summary>The friendly mod names treated as deprecation warning sources (assembly full name => mod name).</summary>
|
/// <summary>The friendly mod names treated as deprecation warning sources (assembly full name => mod name).</summary>
|
||||||
private readonly IDictionary<string, string> ModNamesByAssembly = new Dictionary<string, string>();
|
private readonly IDictionary<string, string> ModNamesByAssembly = new Dictionary<string, string>();
|
||||||
|
|
||||||
/// <summary>Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</summary>
|
|
||||||
private readonly ModCompatibility[] CompatibilityRecords;
|
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Construct an instance.</summary>
|
|
||||||
/// <param name="compatibilityRecords">Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</param>
|
|
||||||
public ModRegistry(IEnumerable<ModCompatibility> compatibilityRecords)
|
|
||||||
{
|
|
||||||
this.CompatibilityRecords = compatibilityRecords.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/****
|
/****
|
||||||
** IModRegistry
|
** IModRegistry
|
||||||
****/
|
****/
|
||||||
|
@ -125,21 +113,5 @@ namespace StardewModdingAPI.Framework
|
||||||
// no known assembly found
|
// no known assembly found
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Get metadata that indicates whether SMAPI should assume the mod is compatible or broken, regardless of whether it detects incompatible code.</summary>
|
|
||||||
/// <param name="manifest">The mod manifest.</param>
|
|
||||||
/// <returns>Returns the incompatibility record if applicable, else <c>null</c>.</returns>
|
|
||||||
internal ModCompatibility GetCompatibilityRecord(IManifest manifest)
|
|
||||||
{
|
|
||||||
string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll;
|
|
||||||
return (
|
|
||||||
from mod in this.CompatibilityRecords
|
|
||||||
where
|
|
||||||
mod.ID == key
|
|
||||||
&& (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion))
|
|
||||||
&& !manifest.Version.IsNewerThan(mod.UpperSemanticVersion)
|
|
||||||
select mod
|
|
||||||
).FirstOrDefault();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -269,7 +269,7 @@ namespace StardewModdingAPI
|
||||||
this.GameInstance.VerboseLogging = this.Settings.VerboseLogging;
|
this.GameInstance.VerboseLogging = this.Settings.VerboseLogging;
|
||||||
|
|
||||||
// load core components
|
// load core components
|
||||||
this.ModRegistry = new ModRegistry(this.Settings.ModCompatibility);
|
this.ModRegistry = new ModRegistry();
|
||||||
this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry);
|
this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry);
|
||||||
this.CommandManager = new CommandManager();
|
this.CommandManager = new CommandManager();
|
||||||
|
|
||||||
|
@ -316,8 +316,8 @@ namespace StardewModdingAPI
|
||||||
// load mods
|
// load mods
|
||||||
JsonHelper jsonHelper = new JsonHelper();
|
JsonHelper jsonHelper = new JsonHelper();
|
||||||
IList<Action> deprecationWarnings = new List<Action>();
|
IList<Action> deprecationWarnings = new List<Action>();
|
||||||
ModMetadata[] mods = this.FindMods(Constants.ModPath, new JsonHelper(), deprecationWarnings);
|
ModMetadata[] mods = new ModResolver(this.Monitor, this.DeprecationManager, this.Settings.ModCompatibility)
|
||||||
mods = this.HandleModDependencies(mods);
|
.FindMods(Constants.ModPath, new JsonHelper(), deprecationWarnings);
|
||||||
modsLoaded = this.LoadMods(mods, jsonHelper, (SContentManager)Game1.content, deprecationWarnings);
|
modsLoaded = this.LoadMods(mods, jsonHelper, (SContentManager)Game1.content, deprecationWarnings);
|
||||||
|
|
||||||
// log deprecation warnings together
|
// log deprecation warnings together
|
||||||
|
@ -338,109 +338,6 @@ namespace StardewModdingAPI
|
||||||
new Thread(this.RunConsoleLoop).Start();
|
new Thread(this.RunConsoleLoop).Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Sort a set of mods by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies.</summary>
|
|
||||||
/// <param name="mods">The mods to process.</param>
|
|
||||||
private ModMetadata[] HandleModDependencies(ModMetadata[] mods)
|
|
||||||
{
|
|
||||||
this.Monitor.Log("Checking mod dependencies...");
|
|
||||||
var unsortedMods = mods.ToList();
|
|
||||||
var sortedMods = new Stack<ModMetadata>();
|
|
||||||
var visitedMods = new bool[unsortedMods.Count];
|
|
||||||
var currentChain = new List<ModMetadata>();
|
|
||||||
bool success = true;
|
|
||||||
|
|
||||||
for (int index = 0; index < unsortedMods.Count; index++)
|
|
||||||
{
|
|
||||||
success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods);
|
|
||||||
if (!success)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!success)
|
|
||||||
{
|
|
||||||
// Failed to sort list, return no mods.
|
|
||||||
this.Monitor.Log("No mods will be loaded.", LogLevel.Error);
|
|
||||||
return new ModMetadata[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return sortedMods.Reverse().ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Sort a mod's dependencies by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies.</summary>
|
|
||||||
/// <param name="modIndex">The index of the mod being processed in the <paramref name="unsortedMods"/>.</param>
|
|
||||||
/// <param name="visitedMods">The mods which have been processed.</param>
|
|
||||||
/// <param name="sortedMods">The list in which to save mods sorted by dependency order.</param>
|
|
||||||
/// <param name="currentChain">The current change of mod dependencies.</param>
|
|
||||||
/// <param name="unsortedMods">The mods remaining to sort.</param>
|
|
||||||
/// <returns>Returns whether the mod can be loaded.</returns>
|
|
||||||
private bool HandleModDependencies(int modIndex, bool[] visitedMods, Stack<ModMetadata> sortedMods, List<ModMetadata> currentChain, List<ModMetadata> unsortedMods)
|
|
||||||
{
|
|
||||||
// visit mod
|
|
||||||
if (visitedMods[modIndex])
|
|
||||||
return true; // already sorted
|
|
||||||
ModMetadata mod = unsortedMods[modIndex];
|
|
||||||
visitedMods[modIndex] = true;
|
|
||||||
|
|
||||||
// process dependencies
|
|
||||||
bool success = true;
|
|
||||||
if (mod.Manifest.Dependencies != null && mod.Manifest.Dependencies.Any())
|
|
||||||
{
|
|
||||||
// validate required dependencies are present
|
|
||||||
{
|
|
||||||
string missingMods = null;
|
|
||||||
foreach (IManifestDependency dependency in mod.Manifest.Dependencies)
|
|
||||||
{
|
|
||||||
if (!unsortedMods.Any(m => m.Manifest.UniqueID.Equals(dependency.UniqueID)))
|
|
||||||
missingMods += $"{dependency.UniqueID}, ";
|
|
||||||
}
|
|
||||||
if (missingMods != null)
|
|
||||||
{
|
|
||||||
this.Monitor.Log($"Skipped {mod.DisplayName} because it requires mods which aren't installed ({missingMods.Substring(0, missingMods.Length - 2)}).", LogLevel.Error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get mods which should be loaded before this one
|
|
||||||
ModMetadata[] modsToLoadFirst =
|
|
||||||
(
|
|
||||||
from unsorted in unsortedMods
|
|
||||||
where mod.Manifest.Dependencies.Any(required => required.UniqueID == unsorted.Manifest.UniqueID)
|
|
||||||
select unsorted
|
|
||||||
)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
// detect circular references
|
|
||||||
ModMetadata circularReferenceMod = currentChain.FirstOrDefault(modsToLoadFirst.Contains);
|
|
||||||
if (circularReferenceMod != null)
|
|
||||||
{
|
|
||||||
this.Monitor.Log($"Skipped {mod.DisplayName} because its dependencies have a circular reference: {string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {circularReferenceMod.DisplayName}).", LogLevel.Error);
|
|
||||||
string chain = $"{mod.Manifest.UniqueID} -> {circularReferenceMod.Manifest.UniqueID}";
|
|
||||||
for (int i = currentChain.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
chain = $"{currentChain[i].Manifest.UniqueID} -> " + chain;
|
|
||||||
if (currentChain[i].Manifest.UniqueID.Equals(mod.Manifest.UniqueID)) break;
|
|
||||||
}
|
|
||||||
this.Monitor.Log(chain, LogLevel.Error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
currentChain.Add(mod);
|
|
||||||
|
|
||||||
// recursively sort dependencies
|
|
||||||
foreach (ModMetadata requiredMod in modsToLoadFirst)
|
|
||||||
{
|
|
||||||
int index = unsortedMods.IndexOf(requiredMod);
|
|
||||||
success = this.HandleModDependencies(index, visitedMods, sortedMods, currentChain, unsortedMods);
|
|
||||||
if (!success)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// mark mod sorted
|
|
||||||
sortedMods.Push(mod);
|
|
||||||
currentChain.Remove(mod);
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Run a loop handling console input.</summary>
|
/// <summary>Run a loop handling console input.</summary>
|
||||||
[SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")]
|
[SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")]
|
||||||
private void RunConsoleLoop()
|
private void RunConsoleLoop()
|
||||||
|
@ -560,147 +457,6 @@ namespace StardewModdingAPI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Find all mods in the given folder.</summary>
|
|
||||||
/// <param name="rootPath">The root mod path to search.</param>
|
|
||||||
/// <param name="jsonHelper">The JSON helper with which to read the manifest file.</param>
|
|
||||||
/// <param name="deprecationWarnings">A list to populate with any deprecation warnings.</param>
|
|
||||||
private ModMetadata[] FindMods(string rootPath, JsonHelper jsonHelper, IList<Action> deprecationWarnings)
|
|
||||||
{
|
|
||||||
this.Monitor.Log("Finding mods...");
|
|
||||||
void LogSkip(string displayName, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {displayName} because {reasonPhrase}", level);
|
|
||||||
|
|
||||||
// load mod metadata
|
|
||||||
List<ModMetadata> mods = new List<ModMetadata>();
|
|
||||||
foreach (string modRootPath in Directory.GetDirectories(rootPath))
|
|
||||||
{
|
|
||||||
if (this.Monitor.IsExiting)
|
|
||||||
return new ModMetadata[0]; // exit in progress
|
|
||||||
|
|
||||||
// init metadata
|
|
||||||
string displayName = modRootPath.Replace(rootPath, "").Trim('/', '\\');
|
|
||||||
|
|
||||||
// passthrough empty directories
|
|
||||||
DirectoryInfo directory = new DirectoryInfo(modRootPath);
|
|
||||||
while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1)
|
|
||||||
directory = directory.GetDirectories().First();
|
|
||||||
|
|
||||||
// get manifest path
|
|
||||||
string manifestPath = Path.Combine(directory.FullName, "manifest.json");
|
|
||||||
if (!File.Exists(manifestPath))
|
|
||||||
{
|
|
||||||
LogSkip(displayName, "it doesn't have a manifest.", LogLevel.Warn);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// read manifest
|
|
||||||
Manifest manifest;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// read manifest file
|
|
||||||
string json = File.ReadAllText(manifestPath);
|
|
||||||
if (string.IsNullOrEmpty(json))
|
|
||||||
{
|
|
||||||
LogSkip(displayName, "its manifest is empty.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse manifest
|
|
||||||
manifest = jsonHelper.ReadJsonFile<Manifest>(Path.Combine(directory.FullName, "manifest.json"));
|
|
||||||
if (manifest == null)
|
|
||||||
{
|
|
||||||
LogSkip(displayName, "its manifest is invalid.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate manifest
|
|
||||||
if (string.IsNullOrWhiteSpace(manifest.EntryDll))
|
|
||||||
{
|
|
||||||
LogSkip(displayName, "its manifest doesn't set an entry DLL.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (string.IsNullOrWhiteSpace(manifest.UniqueID))
|
|
||||||
deprecationWarnings.Add(() => this.Monitor.Log($"{manifest.Name} doesn't have a {nameof(IManifest.UniqueID)} in its manifest. This will be required in an upcoming SMAPI release.", LogLevel.Warn));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
LogSkip(displayName, $"parsing its manifest failed:\n{ex.GetLogSummary()}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrWhiteSpace(manifest.Name))
|
|
||||||
displayName = manifest.Name;
|
|
||||||
|
|
||||||
// validate compatibility
|
|
||||||
ModCompatibility compatibility = this.ModRegistry.GetCompatibilityRecord(manifest);
|
|
||||||
if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken)
|
|
||||||
{
|
|
||||||
bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl);
|
|
||||||
bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl);
|
|
||||||
|
|
||||||
string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game";
|
|
||||||
string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:";
|
|
||||||
if (hasOfficialUrl)
|
|
||||||
error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}";
|
|
||||||
if (hasUnofficialUrl)
|
|
||||||
error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}";
|
|
||||||
|
|
||||||
LogSkip(displayName, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate SMAPI version
|
|
||||||
if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion);
|
|
||||||
if (minVersion.IsNewerThan(Constants.ApiVersion))
|
|
||||||
{
|
|
||||||
LogSkip(displayName, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version"))
|
|
||||||
{
|
|
||||||
LogSkip(displayName, $"it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create per-save directory
|
|
||||||
if (manifest.PerSaveConfigs)
|
|
||||||
{
|
|
||||||
deprecationWarnings.Add(() => this.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info));
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string psDir = Path.Combine(directory.FullName, "psconfigs");
|
|
||||||
Directory.CreateDirectory(psDir);
|
|
||||||
if (!Directory.Exists(psDir))
|
|
||||||
{
|
|
||||||
LogSkip(displayName, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
LogSkip(displayName, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate DLL path
|
|
||||||
string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll);
|
|
||||||
if (!File.Exists(assemblyPath))
|
|
||||||
{
|
|
||||||
LogSkip(displayName, $"its DLL '{manifest.EntryDll}' doesn't exist.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add mod metadata
|
|
||||||
mods.Add(new ModMetadata(displayName, directory.FullName, manifest, compatibility));
|
|
||||||
}
|
|
||||||
|
|
||||||
return mods.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Load and hook up the given mods.</summary>
|
/// <summary>Load and hook up the given mods.</summary>
|
||||||
/// <param name="mods">The mods to load.</param>
|
/// <param name="mods">The mods to load.</param>
|
||||||
/// <param name="jsonHelper">The JSON helper with which to read mods' JSON files.</param>
|
/// <param name="jsonHelper">The JSON helper with which to read mods' JSON files.</param>
|
||||||
|
|
|
@ -121,6 +121,7 @@
|
||||||
<Compile Include="Events\EventArgsStringChanged.cs" />
|
<Compile Include="Events\EventArgsStringChanged.cs" />
|
||||||
<Compile Include="Events\GameEvents.cs" />
|
<Compile Include="Events\GameEvents.cs" />
|
||||||
<Compile Include="Events\GraphicsEvents.cs" />
|
<Compile Include="Events\GraphicsEvents.cs" />
|
||||||
|
<Compile Include="Framework\ModLoading\ModResolver.cs" />
|
||||||
<Compile Include="Framework\ModLoading\AssemblyDefinitionResolver.cs" />
|
<Compile Include="Framework\ModLoading\AssemblyDefinitionResolver.cs" />
|
||||||
<Compile Include="Framework\ModLoading\AssemblyParseResult.cs" />
|
<Compile Include="Framework\ModLoading\AssemblyParseResult.cs" />
|
||||||
<Compile Include="Framework\CommandManager.cs" />
|
<Compile Include="Framework\CommandManager.cs" />
|
||||||
|
|
Loading…
Reference in New Issue