/********* ** Private methods *********/ /// <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="mods">The full list of mods being validated.</param> /// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param> /// <param name="mod">The mod whose dependencies to process.</param> /// <param name="states">The dependency state for each mod.</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> /// <returns>Returns the mod dependency status.</returns> private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, ModDatabase modDatabase, IModMetadata mod, IDictionary <IModMetadata, ModDependencyStatus> states, Stack <IModMetadata> sortedMods, ICollection <IModMetadata> currentChain) { // check if already visited switch (states[mod]) { // already sorted or failed case ModDependencyStatus.Sorted: case ModDependencyStatus.Failed: return(states[mod]); // dependency loop case ModDependencyStatus.Checking: // This should never happen. The higher-level mod checks if the dependency is // already being checked, so it can fail without visiting a mod twice. If this // case is hit, that logic didn't catch the dependency loop for some reason. throw new InvalidModStateException($"A dependency loop was not caught by the calling iteration ({string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {mod.DisplayName}))."); // not visited yet, start processing case ModDependencyStatus.Queued: break; // sanity check default: throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'."); } // collect dependencies ModDependency[] dependencies = this.GetDependenciesFrom(mod.Manifest, mods).ToArray(); // mark sorted if no dependencies if (!dependencies.Any()) { sortedMods.Push(mod); return(states[mod] = ModDependencyStatus.Sorted); } // mark failed if missing dependencies { string[] failedModNames = ( from entry in dependencies where entry.IsRequired && entry.Mod == null let displayName = modDatabase.Get(entry.ID)?.DisplayName ?? entry.ID let modUrl = modDatabase.GetModPageUrlFor(entry.ID) orderby displayName select modUrl != null ? $"{displayName}: {modUrl}" : displayName ).ToArray(); if (failedModNames.Any()) { sortedMods.Push(mod); mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it requires mods which aren't installed ({string.Join(", ", failedModNames)})."); return(states[mod] = ModDependencyStatus.Failed); } } // dependency min version not met, mark failed { string[] failedLabels = ( from entry in dependencies where entry.Mod != null && entry.MinVersion != null && entry.MinVersion.IsNewerThan(entry.Mod.Manifest.Version) select $"{entry.Mod.DisplayName} (needs {entry.MinVersion} or later)" ) .ToArray(); if (failedLabels.Any()) { sortedMods.Push(mod); mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it needs newer versions of some mods: {string.Join(", ", failedLabels)}."); return(states[mod] = ModDependencyStatus.Failed); } } // process dependencies { states[mod] = ModDependencyStatus.Checking; // recursively sort dependencies foreach (var dependency in dependencies) { IModMetadata requiredMod = dependency.Mod; var subchain = new List <IModMetadata>(currentChain) { mod }; // ignore missing optional dependency if (!dependency.IsRequired && requiredMod == null) { continue; } // detect dependency loop if (states[requiredMod] == ModDependencyStatus.Checking) { sortedMods.Push(mod); mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"its dependencies have a circular reference: {string.Join(" => ", subchain.Select(p => p.DisplayName))} => {requiredMod.DisplayName})."); return(states[mod] = ModDependencyStatus.Failed); } // recursively process each dependency var substatus = this.ProcessDependencies(mods, modDatabase, requiredMod, states, sortedMods, subchain); switch (substatus) { // sorted successfully case ModDependencyStatus.Sorted: case ModDependencyStatus.Failed when !dependency.IsRequired: // ignore failed optional dependency break; // failed, which means this mod can't be loaded either case ModDependencyStatus.Failed: sortedMods.Push(mod); mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it needs the '{requiredMod.DisplayName}' mod, which couldn't be loaded."); return(states[mod] = ModDependencyStatus.Failed); // unexpected status case ModDependencyStatus.Queued: case ModDependencyStatus.Checking: throw new InvalidModStateException($"Something went wrong sorting dependencies: mod '{requiredMod.DisplayName}' unexpectedly stayed in the '{substatus}' status."); // sanity check default: throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'."); } } // all requirements sorted successfully sortedMods.Push(mod); return(states[mod] = ModDependencyStatus.Sorted); } }