Exemple #1
0
        /*********
        ** 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);
            }
        }