示例#1
0
        public async Task <IEnumerable <ModEntryModel> > PostAsync([FromBody] ModSearchModel model)
        {
            if (model?.Mods == null)
            {
                return(new ModEntryModel[0]);
            }

            // fetch wiki data
            WikiCompatibilityEntry[] wikiData = await this.GetWikiDataAsync();

            IDictionary <string, ModEntryModel> mods = new Dictionary <string, ModEntryModel>(StringComparer.CurrentCultureIgnoreCase);

            foreach (ModSearchEntryModel mod in model.Mods)
            {
                if (string.IsNullOrWhiteSpace(mod.ID))
                {
                    continue;
                }

                ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata);

                mods[mod.ID] = result;
            }

            // return data
            return(mods.Values);
        }
示例#2
0
        public async Task <IEnumerable <ModEntryModel> > PostAsync([FromBody] ModSearchModel model, [FromRoute] string version)
        {
            if (model?.Mods == null)
            {
                return(new ModEntryModel[0]);
            }

            // fetch wiki data
            WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.Data).ToArray();
            IDictionary <string, ModEntryModel> mods = new Dictionary <string, ModEntryModel>(StringComparer.CurrentCultureIgnoreCase);

            foreach (ModSearchEntryModel mod in model.Mods)
            {
                if (string.IsNullOrWhiteSpace(mod.ID))
                {
                    continue;
                }

                ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata, model.ApiVersion);

                if (!model.IncludeExtendedMetadata && (model.ApiVersion == null || mod.InstalledVersion == null))
                {
                    var errors = new List <string>(result.Errors);
                    errors.Add($"This API can't suggest an update because {nameof(model.ApiVersion)} or {nameof(mod.InstalledVersion)} are null, and you didn't specify {nameof(model.IncludeExtendedMetadata)} to get other info. See the SMAPI technical docs for usage.");
                    result.Errors = errors.ToArray();
                }

                mods[mod.ID] = result;
            }

            // return data
            return(mods.Values);
        }
示例#3
0
        public async Task <object> PostAsync([FromBody] ModSearchModel model)
        {
            // parse request data
            ISemanticVersion apiVersion = this.GetApiVersion();

            ModSearchEntryModel[] searchMods = this.GetSearchMods(model, apiVersion).ToArray();

            // fetch wiki data
            WikiCompatibilityEntry[] wikiData = await this.GetWikiDataAsync();

            // fetch data
            IDictionary <string, ModEntryModel> mods = new Dictionary <string, ModEntryModel>(StringComparer.CurrentCultureIgnoreCase);

            foreach (ModSearchEntryModel mod in searchMods)
            {
                if (string.IsNullOrWhiteSpace(mod.ID))
                {
                    continue;
                }

                ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata);

                result.SetBackwardsCompatibility(apiVersion);
                mods[mod.ID] = result;
            }

            // return in expected structure
            return(apiVersion.IsNewerThan("2.6-beta.18")
                ? mods.Values
                : (object)mods);
        }
        /// <summary>Gets the update status of all mods.</summary>
        /// <param name="statuses">All mod statuses.</param>
        /// <returns>Whether any non skipped statuses were added.</returns>
        public bool GetUpdateStatuses(out IList <ModStatus> statuses)
        {
            statuses = new List <ModStatus>();

            bool addedNonSkippedStatus = false;

            foreach (object modMetaData in GetInstanceField <IEnumerable <object> >(GetInstanceField <object>(this.helper.ModRegistry, "Registry"), "Mods"))
            {
                ModEntryModel result   = GetInstanceProperty <ModEntryModel>(modMetaData, "UpdateCheckData");
                IManifest     manifest = GetInstanceProperty <IManifest>(modMetaData, "Manifest");

                string fallbackURL = manifest.UpdateKeys?.Select(this.toolkit.GetUpdateUrl).FirstOrDefault(p => p != null) ?? "";

                if (result == null)
                {
                    statuses.Add(new ModStatus(UpdateStatus.Skipped, manifest, fallbackURL, null, "SMAPI didn't check for an update"));
                    continue;
                }

                if (result.SuggestedUpdate == null)
                {
                    if (result.Errors.Length != 0)
                    {
                        string error = result.Errors[0];

                        if (result.Errors.Any(err => err.Contains("???")))
                        {
                            statuses.Add(new ModStatus(UpdateStatus.Skipped, manifest, fallbackURL, null, "This mod intentionally doesn't have an update key"));
                        }
                        else
                        {
                            // Return the first error. That's not perfect, but generally users don't care why each different update failed, they just want to know there was an error.
                            statuses.Add(new ModStatus(UpdateStatus.UpToDate, manifest, fallbackURL, null, result.Errors[0]));
                        }
                    }
                    else
                    {
                        statuses.Add(new ModStatus(UpdateStatus.UpToDate, manifest, fallbackURL));
                    }
                }
                else
                {
                    statuses.Add(new ModStatus(UpdateStatus.OutOfDate, manifest, result.SuggestedUpdate.Url, result.SuggestedUpdate.Version.ToString()));
                }

                addedNonSkippedStatus = true;
            }

            return(addedNonSkippedStatus);
        }
示例#5
0
        public async Task <IEnumerable <ModEntryModel> > PostAsync([FromBody] ModSearchModel model, [FromRoute] string version)
        {
            if (model?.Mods == null)
            {
                return(new ModEntryModel[0]);
            }

            bool legacyMode = SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion) && parsedVersion.IsOlderThan("3.0.0-beta.20191109");

            // fetch wiki data
            WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.GetModel()).ToArray();
            IDictionary <string, ModEntryModel> mods = new Dictionary <string, ModEntryModel>(StringComparer.CurrentCultureIgnoreCase);

            foreach (ModSearchEntryModel mod in model.Mods)
            {
                if (string.IsNullOrWhiteSpace(mod.ID))
                {
                    continue;
                }

                ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata || legacyMode, model.ApiVersion);

                if (legacyMode)
                {
                    result.Main              = result.Metadata.Main;
                    result.Optional          = result.Metadata.Optional;
                    result.Unofficial        = result.Metadata.Unofficial;
                    result.UnofficialForBeta = result.Metadata.UnofficialForBeta;
                    result.HasBetaInfo       = result.Metadata.BetaCompatibilityStatus != null;
                    result.SuggestedUpdate   = null;
                    if (!model.IncludeExtendedMetadata)
                    {
                        result.Metadata = null;
                    }
                }
                else if (!model.IncludeExtendedMetadata && (model.ApiVersion == null || mod.InstalledVersion == null))
                {
                    var errors = new List <string>(result.Errors);
                    errors.Add($"This API can't suggest an update because {nameof(model.ApiVersion)} or {nameof(mod.InstalledVersion)} are null, and you didn't specify {nameof(model.IncludeExtendedMetadata)} to get other info. See the SMAPI technical docs for usage.");
                    result.Errors = errors.ToArray();
                }

                mods[mod.ID] = result;
            }

            // return data
            return(mods.Values);
        }
示例#6
0
        public async Task <IEnumerable <ModEntryModel> > PostAsync([FromBody] ModSearchModel model, [FromRoute] string version)
        {
            if (model?.Mods == null)
            {
                return(Array.Empty <ModEntryModel>());
            }

            ModUpdateCheckConfig config = this.Config.Value;

            // fetch wiki data
            WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.Data).ToArray();
            IDictionary <string, ModEntryModel> mods = new Dictionary <string, ModEntryModel>(StringComparer.CurrentCultureIgnoreCase);

            foreach (ModSearchEntryModel mod in model.Mods)
            {
                if (string.IsNullOrWhiteSpace(mod.ID))
                {
                    continue;
                }

                // special case: if this is an update check for the official SMAPI repo, check the Nexus mod page for beta versions
                if (mod.ID == config.SmapiInfo.ID && mod.UpdateKeys?.Any(key => key == config.SmapiInfo.DefaultUpdateKey) == true && mod.InstalledVersion?.IsPrerelease() == true)
                {
                    mod.UpdateKeys = mod.UpdateKeys.Concat(config.SmapiInfo.AddBetaUpdateKeys).ToArray();
                }

                // fetch result
                ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata, model.ApiVersion);

                if (!model.IncludeExtendedMetadata && (model.ApiVersion == null || mod.InstalledVersion == null))
                {
                    var errors = new List <string>(result.Errors);
                    errors.Add($"This API can't suggest an update because {nameof(model.ApiVersion)} or {nameof(mod.InstalledVersion)} are null, and you didn't specify {nameof(model.IncludeExtendedMetadata)} to get other info. See the SMAPI technical docs for usage.");
                    result.Errors = errors.ToArray();
                }

                mods[mod.ID] = result;
            }

            // return data
            return(mods.Values);
        }
示例#7
0
        public bool GetUpdateStatuses(out IList <ModStatus> statuses)
        {
            statuses = new List <ModStatus>();


            object registry = this.helper.ModRegistry.GetType()
                              .GetField("Registry", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(this.helper.ModRegistry);

            bool addedNonSkippedStatus = false;

            foreach (object modMetaData in (IEnumerable <object>)registry.GetType().GetField("Mods", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(registry))
            {
                ModEntryModel result   = GetInstanceProperty <ModEntryModel>(modMetaData, "UpdateCheckData");
                IManifest     manifest = GetInstanceProperty <IManifest>(modMetaData, "Manifest");

                if (result == null)
                {
                    statuses.Add(new ModStatus(UpdateStatus.Skipped, manifest, "", null, "SMAPI didn't check for an update"));
                    continue;
                }

                if (!(bool)modMetaData.GetType().GetMethod("HasValidUpdateKeys").Invoke(modMetaData, null))
                {
                    statuses.Add(new ModStatus(UpdateStatus.Skipped, manifest, "", null, "Mod has no update keys"));
                    continue;
                }

                ModDataRecordVersionedFields dataRecord =
                    GetInstanceProperty <ModDataRecordVersionedFields>(modMetaData, "DataRecord");

                //This section largely taken from https://github.com/Pathoschild/SMAPI/blob/924c3a5d3fe6bfad483834112883156bdf202b57/src/SMAPI/Framework/SCore.cs#L618-L630
                bool             useBetaInfo       = result.HasBetaInfo && Constants.ApiVersion.IsPrerelease();
                ISemanticVersion localVersion      = dataRecord?.GetLocalVersionForUpdateChecks(manifest.Version) ?? manifest.Version;
                ISemanticVersion latestVersion     = dataRecord?.GetRemoteVersionForUpdateChecks(result.Main?.Version) ?? result.Main?.Version;
                ISemanticVersion optionalVersion   = dataRecord?.GetRemoteVersionForUpdateChecks(result.Optional?.Version) ?? result.Optional?.Version;
                ISemanticVersion unofficialVersion = useBetaInfo ? result.UnofficialForBeta?.Version : result.Unofficial?.Version;

                if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true))
                {
                    statuses.Add(new ModStatus(UpdateStatus.OutOfDate, manifest, result.Main?.Url, latestVersion.ToString()));
                }
                else if (this.IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease()))
                {
                    statuses.Add(new ModStatus(UpdateStatus.OutOfDate, manifest, result.Optional?.Url, optionalVersion.ToString()));
                }
                else if (this.IsValidUpdate(localVersion, unofficialVersion, useBetaChannel: GetEnumName(modMetaData, "Status") == "Failed"))
                {
                    statuses.Add(new ModStatus(UpdateStatus.OutOfDate, manifest, useBetaInfo ? result.UnofficialForBeta?.Url : result.Unofficial?.Url, unofficialVersion.ToString()));
                }
                else
                {
                    string       updateURL    = null;
                    UpdateStatus updateStatus = UpdateStatus.UpToDate;
                    if (localVersion.Equals(latestVersion))
                    {
                        updateURL = result.Main?.Url;
                    }
                    else if (localVersion.Equals(optionalVersion))
                    {
                        updateURL = result.Optional?.Url;
                    }
                    else if (localVersion.Equals(unofficialVersion))
                    {
                        updateURL = useBetaInfo ? result.UnofficialForBeta?.Url : result.Unofficial?.Url;
                    }
                    else if (latestVersion != null && this.IsValidUpdate(latestVersion, localVersion, useBetaChannel: true))
                    {
                        updateURL    = result.Main?.Url;
                        updateStatus = UpdateStatus.VeryNew;
                    }
                    else if (optionalVersion != null && this.IsValidUpdate(optionalVersion, localVersion, useBetaChannel: localVersion.IsPrerelease()))
                    {
                        updateURL    = result.Optional?.Url;
                        updateStatus = UpdateStatus.VeryNew;
                    }
                    else if (unofficialVersion != null && this.IsValidUpdate(unofficialVersion, localVersion, useBetaChannel: GetEnumName(modMetaData, "Status") == "Failed"))
                    {
                        updateURL    = useBetaInfo ? result.UnofficialForBeta?.Url : result.Unofficial?.Url;
                        updateStatus = UpdateStatus.VeryNew;
                    }

                    if (updateURL != null)
                    {
                        statuses.Add(new ModStatus(updateStatus, manifest, updateURL));
                    }
                    else if (result.Errors != null && result.Errors.Any())
                    {
                        statuses.Add(new ModStatus(UpdateStatus.Error, manifest, "", "", result.Errors[0]));
                    }
                    else
                    {
                        statuses.Add(new ModStatus(UpdateStatus.Error, manifest, "", "", "Unknown Error"));
                    }
                }

                addedNonSkippedStatus = true;
            }

            return(addedNonSkippedStatus);
        }
示例#8
0
 /// <inheritdoc />
 public IModMetadata SetUpdateData(ModEntryModel data)
 {
     this.UpdateCheckData = data;
     return(this);
 }
示例#9
0
        /*********
        ** Private methods
        *********/
        /// <summary>Get the metadata for a mod.</summary>
        /// <param name="search">The mod data to match.</param>
        /// <param name="wikiData">The wiki data.</param>
        /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param>
        /// <returns>Returns the mod data if found, else <c>null</c>.</returns>
        private async Task <ModEntryModel> GetModData(ModSearchEntryModel search, WikiCompatibilityEntry[] wikiData, bool includeExtendedMetadata)
        {
            // resolve update keys
            var           updateKeys = new HashSet <string>(search.UpdateKeys ?? new string[0], StringComparer.InvariantCultureIgnoreCase);
            ModDataRecord record     = this.ModDatabase.Get(search.ID);

            if (record?.Fields != null)
            {
                string defaultUpdateKey = record.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value;
                if (!string.IsNullOrWhiteSpace(defaultUpdateKey))
                {
                    updateKeys.Add(defaultUpdateKey);
                }
            }

            // get latest versions
            ModEntryModel result = new ModEntryModel {
                ID = search.ID
            };
            IList <string> errors = new List <string>();

            foreach (string updateKey in updateKeys)
            {
                // fetch data
                ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey);

                if (data.Error != null)
                {
                    errors.Add(data.Error);
                    continue;
                }

                // handle main version
                if (data.Version != null)
                {
                    if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version))
                    {
                        errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'.");
                        continue;
                    }

                    if (this.IsNewer(version, result.Main?.Version))
                    {
                        result.Main = new ModEntryVersionModel(version, data.Url);
                    }
                }

                // handle optional version
                if (data.PreviewVersion != null)
                {
                    if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version))
                    {
                        errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'.");
                        continue;
                    }

                    if (this.IsNewer(version, result.Optional?.Version))
                    {
                        result.Optional = new ModEntryVersionModel(version, data.Url);
                    }
                }
            }

            // get unofficial version
            WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase));

            if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version))
            {
                result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl);
            }

            // fallback to preview if latest is invalid
            if (result.Main == null && result.Optional != null)
            {
                result.Main     = result.Optional;
                result.Optional = null;
            }

            // special cases
            if (result.ID == "Pathoschild.SMAPI")
            {
                if (result.Main != null)
                {
                    result.Main.Url = "https://smapi.io/";
                }
                if (result.Optional != null)
                {
                    result.Optional.Url = "https://smapi.io/";
                }
            }

            // add extended metadata
            if (includeExtendedMetadata && (wikiEntry != null || record != null))
            {
                result.Metadata = new ModExtendedMetadataModel(wikiEntry, record);
            }

            // add result
            result.Errors = errors.ToArray();
            return(result);
        }
示例#10
0
        /*********
        ** Private methods
        *********/
        /// <summary>Get the metadata for a mod.</summary>
        /// <param name="search">The mod data to match.</param>
        /// <param name="wikiData">The wiki data.</param>
        /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param>
        /// <param name="apiVersion">The SMAPI version installed by the player.</param>
        /// <returns>Returns the mod data if found, else <c>null</c>.</returns>
        private async Task <ModEntryModel> GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata, ISemanticVersion apiVersion)
        {
            // cross-reference data
            ModDataRecord record    = this.ModDatabase.Get(search.ID);
            WikiModEntry  wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.OrdinalIgnoreCase));

            UpdateKey[]       updateKeys  = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray();
            ModOverrideConfig overrides   = this.Config.Value.ModOverrides.FirstOrDefault(p => p.ID.Equals(search.ID?.Trim(), StringComparison.OrdinalIgnoreCase));
            bool allowNonStandardVersions = overrides?.AllowNonStandardVersions ?? false;

            // get latest versions
            ModEntryModel result = new ModEntryModel {
                ID = search.ID
            };
            IList <string>       errors            = new List <string>();
            ModEntryVersionModel main              = null;
            ModEntryVersionModel optional          = null;
            ModEntryVersionModel unofficial        = null;
            ModEntryVersionModel unofficialForBeta = null;

            foreach (UpdateKey updateKey in updateKeys)
            {
                // validate update key
                if (!updateKey.LooksValid)
                {
                    errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541', with an optional subkey like 'Nexus:541@subkey'.");
                    continue;
                }

                // fetch data
                ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions, wikiEntry?.MapRemoteVersions);

                if (data.Status != RemoteModStatus.Ok)
                {
                    errors.Add(data.Error ?? data.Status.ToString());
                    continue;
                }

                // handle versions
                if (this.IsNewer(data.Version, main?.Version))
                {
                    main = new ModEntryVersionModel(data.Version, data.Url);
                }
                if (this.IsNewer(data.PreviewVersion, optional?.Version))
                {
                    optional = new ModEntryVersionModel(data.PreviewVersion, data.Url);
                }
            }

            // get unofficial version
            if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, optional?.Version))
            {
                unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods", absoluteUrl: true)}#{wikiEntry.Anchor}");
            }

            // get unofficial version for beta
            if (wikiEntry?.HasBetaInfo == true)
            {
                if (wikiEntry.BetaCompatibility.Status == WikiCompatibilityStatus.Unofficial)
                {
                    if (wikiEntry.BetaCompatibility.UnofficialVersion != null)
                    {
                        unofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, optional?.Version))
                            ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods", absoluteUrl: true)}#{wikiEntry.Anchor}")
                            : null;
                    }
                    else
                    {
                        unofficialForBeta = unofficial;
                    }
                }
            }

            // fallback to preview if latest is invalid
            if (main == null && optional != null)
            {
                main     = optional;
                optional = null;
            }

            // special cases
            if (overrides?.SetUrl != null)
            {
                if (main != null)
                {
                    main.Url = overrides.SetUrl;
                }
                if (optional != null)
                {
                    optional.Url = overrides.SetUrl;
                }
            }

            // get recommended update (if any)
            ISemanticVersion installedVersion = this.ModSites.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions, allowNonStandard: allowNonStandardVersions);

            if (apiVersion != null && installedVersion != null)
            {
                // get newer versions
                List <ModEntryVersionModel> updates = new List <ModEntryVersionModel>();
                if (this.IsRecommendedUpdate(installedVersion, main?.Version, useBetaChannel: true))
                {
                    updates.Add(main);
                }
                if (this.IsRecommendedUpdate(installedVersion, optional?.Version, useBetaChannel: installedVersion.IsPrerelease() || search.IsBroken))
                {
                    updates.Add(optional);
                }
                if (this.IsRecommendedUpdate(installedVersion, unofficial?.Version, useBetaChannel: true))
                {
                    updates.Add(unofficial);
                }
                if (this.IsRecommendedUpdate(installedVersion, unofficialForBeta?.Version, useBetaChannel: apiVersion.IsPrerelease()))
                {
                    updates.Add(unofficialForBeta);
                }

                // get newest version
                ModEntryVersionModel newest = null;
                foreach (ModEntryVersionModel update in updates)
                {
                    if (newest == null || update.Version.IsNewerThan(newest.Version))
                    {
                        newest = update;
                    }
                }

                // set field
                result.SuggestedUpdate = newest != null
                    ? new ModEntryVersionModel(newest.Version, newest.Url)
                    : null;
            }

            // add extended metadata
            if (includeExtendedMetadata)
            {
                result.Metadata = new ModExtendedMetadataModel(wikiEntry, record, main: main, optional: optional, unofficial: unofficial, unofficialForBeta: unofficialForBeta);
            }

            // add result
            result.Errors = errors.ToArray();
            return(result);
        }
示例#11
0
        /*********
        ** Private methods
        *********/
        /// <summary>Get the metadata for a mod.</summary>
        /// <param name="search">The mod data to match.</param>
        /// <param name="wikiData">The wiki data.</param>
        /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param>
        /// <returns>Returns the mod data if found, else <c>null</c>.</returns>
        private async Task <ModEntryModel> GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata)
        {
            // cross-reference data
            ModDataRecord record    = this.ModDatabase.Get(search.ID);
            WikiModEntry  wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.InvariantCultureIgnoreCase));

            UpdateKey[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray();

            // get latest versions
            ModEntryModel result = new ModEntryModel {
                ID = search.ID
            };
            IList <string> errors = new List <string>();

            foreach (UpdateKey updateKey in updateKeys)
            {
                // validate update key
                if (!updateKey.LooksValid)
                {
                    errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'.");
                    continue;
                }

                // fetch data
                ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey);

                if (data.Error != null)
                {
                    errors.Add(data.Error);
                    continue;
                }

                // handle main version
                if (data.Version != null)
                {
                    if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version))
                    {
                        errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'.");
                        continue;
                    }

                    if (this.IsNewer(version, result.Main?.Version))
                    {
                        result.Main = new ModEntryVersionModel(version, data.Url);
                    }
                }

                // handle optional version
                if (data.PreviewVersion != null)
                {
                    if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version))
                    {
                        errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'.");
                        continue;
                    }

                    if (this.IsNewer(version, result.Optional?.Version))
                    {
                        result.Optional = new ModEntryVersionModel(version, data.Url);
                    }
                }
            }

            // get unofficial version
            if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Optional?.Version))
            {
                result.Unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}");
            }

            // get unofficial version for beta
            if (wikiEntry?.HasBetaInfo == true)
            {
                result.HasBetaInfo = true;
                if (wikiEntry.BetaCompatibility.Status == WikiCompatibilityStatus.Unofficial)
                {
                    if (wikiEntry.BetaCompatibility.UnofficialVersion != null)
                    {
                        result.UnofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Optional?.Version))
                            ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}")
                            : null;
                    }
                    else
                    {
                        result.UnofficialForBeta = result.Unofficial;
                    }
                }
            }

            // fallback to preview if latest is invalid
            if (result.Main == null && result.Optional != null)
            {
                result.Main     = result.Optional;
                result.Optional = null;
            }

            // special cases
            if (result.ID == "Pathoschild.SMAPI")
            {
                if (result.Main != null)
                {
                    result.Main.Url = "https://smapi.io/";
                }
                if (result.Optional != null)
                {
                    result.Optional.Url = "https://smapi.io/";
                }
            }

            // add extended metadata
            if (includeExtendedMetadata && (wikiEntry != null || record != null))
            {
                result.Metadata = new ModExtendedMetadataModel(wikiEntry, record);
            }

            // add result
            result.Errors = errors.ToArray();
            return(result);
        }
        public bool GetUpdateStatuses(out IList <ModStatus> statuses)
        {
            statuses = new List <ModStatus>();


            object registry = this.helper.ModRegistry.GetType()
                              .GetField("Registry", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(this.helper.ModRegistry);

            foreach (object modMetaData in (IEnumerable <object>)registry.GetType()
                     .GetMethod("GetAll", BindingFlags.Public | BindingFlags.Instance)
                     .Invoke(registry, new object[] { true, true }))
            {
                ModEntryModel updateCheckModel = GetInstanceProperty <ModEntryModel>(modMetaData, "UpdateCheckData");

                if (updateCheckModel == null)
                {
                    return(false);
                }

                IManifest modManifest = GetInstanceProperty <IManifest>(modMetaData, "Manifest");

                ModEntryVersionModel latestModEntryVersionModel     = updateCheckModel.Main;
                ModEntryVersionModel optionalModEntryVersionModel   = updateCheckModel.Optional;
                ModEntryVersionModel unofficialModEntryVersionModel = updateCheckModel.Unofficial;

                // get versions
                ISemanticVersion localVersion      = modManifest.Version;
                ISemanticVersion latestVersion     = latestModEntryVersionModel?.Version;
                ISemanticVersion optionalVersion   = optionalModEntryVersionModel?.Version;
                ISemanticVersion unofficialVersion = unofficialModEntryVersionModel?.Version;

                UpdateStatus         status     = UpdateStatus.OutOfDate;
                ModEntryVersionModel whichModel = null;
                ISemanticVersion     updateVersion;
                string error = null;

                // get update alerts
                if (IsValidUpdate(localVersion, latestVersion, useBetaChannel: true))
                {
                    whichModel    = latestModEntryVersionModel;
                    updateVersion = latestVersion;
                }
                else if (IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease()))
                {
                    whichModel    = optionalModEntryVersionModel;
                    updateVersion = optionalVersion;
                }
                else if (IsValidUpdate(localVersion, unofficialVersion, useBetaChannel: true))
                //Different from SMAPI: useBetaChannel is always true
                {
                    whichModel = unofficialModEntryVersionModel;
                    unofficialModEntryVersionModel.Url = $"https://stardewvalleywiki.com/Modding:SMAPI_compatibility#{GenerateAnchor(modManifest.Name)}";
                    updateVersion = unofficialVersion;
                }
                else
                {
                    if (updateCheckModel.Errors.Length > 0)
                    {
                        status        = UpdateStatus.Error;
                        error         = updateCheckModel.Errors[0];
                        updateVersion = modManifest.Version;
                    }
                    else
                    {
                        updateVersion = modManifest.Version;
                        status        = UpdateStatus.UpToDate;

                        if (latestVersion != null && (latestVersion.Equals(localVersion) || IsValidUpdate(latestVersion, localVersion, true)))
                        {
                            whichModel = latestModEntryVersionModel;
                        }
                        else if (optionalVersion != null && (optionalVersion.Equals(localVersion) || IsValidUpdate(optionalVersion, localVersion, true)))
                        {
                            whichModel = optionalModEntryVersionModel;
                        }
                        else if (unofficialVersion != null && (unofficialVersion.Equals(localVersion) || IsValidUpdate(unofficialVersion, localVersion, true)))
                        {
                            whichModel = unofficialModEntryVersionModel;
                        }
                        else
                        {
                            status = UpdateStatus.Skipped;
                        }
                    }
                }

                statuses.Add(new ModStatus(status, modManifest.UniqueID, modManifest.Name, modManifest.Author,
                                           whichModel?.Url ?? "",
                                           modManifest.Version.ToString(), updateVersion?.ToString() ?? "", error));
            }

            return(true);
        }