Пример #1
0
        /// <summary>Get the mod info for an update key.</summary>
        /// <param name="updateKey">The namespaced update key.</param>
        private async Task <ModInfoModel> GetInfoForUpdateKeyAsync(string updateKey)
        {
            // parse update key
            if (!this.TryParseModKey(updateKey, out string vendorKey, out string modID))
            {
                return(new ModInfoModel($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."));
            }

            // get matching repository
            if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository))
            {
                return(new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."));
            }

            // fetch mod info
            return(await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry =>
            {
                ModInfoModel result = await repository.GetModInfoAsync(modID);
                if (result.Error != null)
                {
                    if (result.Version == null)
                    {
                        result.Error = $"The update key '{updateKey}' matches a mod with no version number.";
                    }
                    else if (!Regex.IsMatch(result.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))
                    {
                        result.Error = $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'.";
                    }
                }
                entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(result.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes);
                return result;
            }));
        }
Пример #2
0
        /// <summary>Get the mod info for an update key.</summary>
        /// <param name="updateKey">The namespaced update key.</param>
        private async Task <ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey)
        {
            // get mod
            if (!this.ModCache.TryGetMod(updateKey.Repository, updateKey.ID, out CachedMod mod) || this.ModCache.IsStale(mod.LastUpdated, mod.FetchStatus == RemoteModStatus.TemporaryError ? this.ErrorCacheMinutes : this.SuccessCacheMinutes))
            {
                // get site
                if (!this.Repositories.TryGetValue(updateKey.Repository, out IModRepository repository))
                {
                    return(new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{updateKey.Repository}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."));
                }

                // fetch mod
                ModInfoModel result = await repository.GetModInfoAsync(updateKey.ID);

                if (result.Error == null)
                {
                    if (result.Version == null)
                    {
                        result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number.");
                    }
                    else if (!SemanticVersion.TryParse(result.Version, out _))
                    {
                        result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'.");
                    }
                }

                // cache mod
                this.ModCache.SaveMod(repository.VendorKey, updateKey.ID, result, out mod);
            }
            return(mod.GetModel());
        }
Пример #3
0
        public async Task <IDictionary <string, ModInfoModel> > PostAsync([FromBody] ModSearchModel search)
        {
            // parse model
            bool allowInvalidVersions = search?.AllowInvalidVersions ?? false;

            string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0])
                               .Distinct(StringComparer.CurrentCultureIgnoreCase)
                               .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase)
                               .ToArray();

            // fetch mod info
            IDictionary <string, ModInfoModel> result = new Dictionary <string, ModInfoModel>(StringComparer.CurrentCultureIgnoreCase);

            foreach (string modKey in modKeys)
            {
                // parse mod key
                if (!this.TryParseModKey(modKey, out string vendorKey, out string modID))
                {
                    result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'.");
                    continue;
                }

                // get matching repository
                if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository))
                {
                    result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}].");
                    continue;
                }

                // fetch mod info
                result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry =>
                {
                    // fetch info
                    ModInfoModel info = await repository.GetModInfoAsync(modID);

                    // validate
                    if (info.Error == null)
                    {
                        if (info.Version == null)
                        {
                            info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: "Mod has no version number.");
                        }
                        if (!allowInvalidVersions && !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))
                        {
                            info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: $"Mod has invalid semantic version '{info.Version}'.");
                        }
                    }

                    // cache & return
                    entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(info.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes);
                    return(info);
                });
            }

            return(result);
        }
Пример #4
0
        public async Task <IDictionary <string, ModInfoModel> > GetAsync(string modKeys)
        {
            // sort & filter keys
            string[] modKeysArray = (modKeys?.Split(',').Select(p => p.Trim()).ToArray() ?? new string[0])
                                    .Distinct(StringComparer.CurrentCultureIgnoreCase)
                                    .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase)
                                    .ToArray();

            // fetch mod info
            IDictionary <string, ModInfoModel> result = new Dictionary <string, ModInfoModel>(StringComparer.CurrentCultureIgnoreCase);

            foreach (string modKey in modKeysArray)
            {
                // parse mod key
                if (!this.TryParseModKey(modKey, out string vendorKey, out string modID))
                {
                    result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the mod repository key and mod ID like 'Nexus:541'.");
                    continue;
                }

                // get matching repository
                if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository))
                {
                    result[modKey] = new ModInfoModel("There's no mod repository matching this namespaced mod ID.");
                    continue;
                }

                // fetch mod info
                result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry =>
                {
                    entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes);
                    ModInfoModel info        = await repository.GetModInfoAsync(modID);
                    if (info.Error == null && !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))
                    {
                        info = new ModInfoModel(info.Name, info.Version, info.Url, $"Mod has invalid semantic version '{info.Version}'.");
                    }
                    return(info);
                });
            }

            return(result);
        }
Пример #5
0
        /// <summary>Construct an instance.</summary>
        /// <param name="site">The mod site on which the mod is found.</param>
        /// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param>
        /// <param name="mod">The mod data.</param>
        public CachedMod(ModRepositoryKey site, string id, ModInfoModel mod)
        {
            // tracking
            this.LastUpdated   = DateTimeOffset.UtcNow;
            this.LastRequested = DateTimeOffset.UtcNow;

            // metadata
            this.Site        = site;
            this.ID          = id;
            this.FetchStatus = mod.Status;
            this.FetchError  = mod.Error;

            // mod info
            this.Name           = mod.Name;
            this.MainVersion    = mod.Version;
            this.PreviewVersion = mod.PreviewVersion;
            this.Url            = mod.Url;
            this.LicenseUrl     = mod.LicenseUrl;
            this.LicenseName    = mod.LicenseName;
        }
Пример #6
0
        /// <summary>Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available.</summary>
        /// <param name="mods">The mods to include in the update check (if eligible).</param>
        private void CheckForUpdatesAsync(IModMetadata[] mods)
        {
            if (!this.Settings.CheckForUpdates)
            {
                return;
            }

            new Thread(() =>
            {
                // create client
                WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion);

                // check SMAPI version
                try
                {
                    this.Monitor.Log("Checking for SMAPI update...", LogLevel.Trace);

                    ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value;
                    if (response.Error != null)
                    {
                        this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn);
                        this.Monitor.Log($"Error: {response.Error}");
                    }
                    else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion))
                    {
                        this.Monitor.Log($"You can update SMAPI to {response.Version}: {response.Url}", LogLevel.Alert);
                    }
                    else
                    {
                        this.VerboseLog("   OK.");
                    }
                }
                catch (Exception ex)
                {
                    this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn);
                    this.Monitor.Log($"Error: {ex.GetLogSummary()}");
                }

                // check mod versions
                try
                {
                    // log issues
                    if (this.Settings.VerboseLogging)
                    {
                        this.VerboseLog("Validating mod update keys...");
                        foreach (IModMetadata mod in mods)
                        {
                            if (mod.Manifest == null)
                            {
                                this.VerboseLog($"   {mod.DisplayName}: no manifest.");
                            }
                            else if (mod.Manifest.UpdateKeys == null || !mod.Manifest.UpdateKeys.Any())
                            {
                                this.VerboseLog($"   {mod.DisplayName}: no update keys.");
                            }
                        }
                    }

                    // prepare update keys
                    Dictionary <string, IModMetadata[]> modsByKey =
                        (
                            from mod in mods
                            where mod.Manifest?.UpdateKeys != null
                            from key in mod.Manifest.UpdateKeys
                            select new { key, mod }
                        )
                        .GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase)
                        .ToDictionary(
                            group => group.Key,
                            group => group.Select(p => p.mod).ToArray(),
                            StringComparer.InvariantCultureIgnoreCase
                            );

                    // fetch results
                    this.Monitor.Log($"Checking for updates to {modsByKey.Keys.Count} keys...", LogLevel.Trace);
                    var results =
                        (
                            from entry in client.GetModInfo(modsByKey.Keys.ToArray())
                            from mod in modsByKey[entry.Key]
                            orderby mod.DisplayName
                            select new { entry.Key, Mod = mod, Info = entry.Value }
                        )
                        .ToArray();

                    // extract latest versions
                    IDictionary <IModMetadata, ModInfoModel> updatesByMod = new Dictionary <IModMetadata, ModInfoModel>();
                    foreach (var result in results)
                    {
                        IModMetadata mod  = result.Mod;
                        ModInfoModel info = result.Info;

                        // handle error
                        if (info.Error != null)
                        {
                            this.Monitor.Log($"   {mod.DisplayName} ({result.Key}): update error: {info.Error}", LogLevel.Trace);
                            continue;
                        }

                        // track update
                        ISemanticVersion localVersion = mod.DataRecord != null
                            ? new SemanticVersion(mod.DataRecord.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString()))
                            : mod.Manifest.Version;
                        ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord != null
                            ? mod.DataRecord.GetRemoteVersionForUpdateChecks(new SemanticVersion(info.Version).ToString())
                            : info.Version
                                                                             );
                        bool isUpdate = latestVersion.IsNewerThan(localVersion);
                        this.VerboseLog($"   {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {info.Version}{(!latestVersion.Equals(new SemanticVersion(info.Version)) ? $" [{latestVersion}]" : "")}" : "OK")}.");
                        if (isUpdate)
                        {
                            if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version))
                            {
                                updatesByMod[mod] = info;
                            }
                        }
                    }

                    // output
                    if (updatesByMod.Any())
                    {
                        this.Monitor.Newline();
                        this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert);
                        foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName))
                        {
                            this.Monitor.Log($"   {entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}", LogLevel.Alert);
                        }
                    }
                }
                catch (Exception ex)
                {
                    this.Monitor.Log($"Couldn't check for new mod versions:\n{ex.GetLogSummary()}", LogLevel.Trace);
                }
            }).Start();
        }
        /// <summary>Save data fetched for a mod.</summary>
        /// <param name="site">The mod site on which the mod is found.</param>
        /// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param>
        /// <param name="mod">The mod data.</param>
        /// <param name="cachedMod">The stored mod record.</param>
        public void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod)
        {
            id = this.NormalizeId(id);

            cachedMod = this.SaveMod(new CachedMod(site, id, mod));
        }
Пример #8
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);
        }
Пример #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>
        /// <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);
        }
Пример #10
0
        /// <summary>Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available.</summary>
        /// <param name="mods">The mods to include in the update check (if eligible).</param>
        private void CheckForUpdatesAsync(IModMetadata[] mods)
        {
            if (!this.Settings.CheckForUpdates)
            {
                return;
            }

            new Thread(() =>
            {
                // update info
                List <string> updates = new List <string>();
                bool smapiUpdate      = false;
                int modUpdates        = 0;

                // create client
                WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion);

                // fetch SMAPI version
                try
                {
                    ModInfoModel response = client.GetModInfoAsync($"GitHub:{this.Settings.GitHubProjectName}").Result.Single().Value;
                    if (response.Error != null)
                    {
                        this.Monitor.Log($"Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.\n{response.Error}", LogLevel.Warn);
                    }
                    else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion))
                    {
                        smapiUpdate = true;
                        updates.Add($"SMAPI {response.Version}: {response.Url}");
                    }
                }
                catch (Exception ex)
                {
                    this.Monitor.Log($"Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.\n{ex.GetLogSummary()}");
                }

                // fetch mod versions
                try
                {
                    // prepare update-check data
                    IDictionary <string, IModMetadata> modsByKey = new Dictionary <string, IModMetadata>(StringComparer.InvariantCultureIgnoreCase);
                    foreach (IModMetadata mod in mods)
                    {
                        if (!string.IsNullOrWhiteSpace(mod.Manifest.ChucklefishID))
                        {
                            modsByKey[$"Chucklefish:{mod.Manifest.ChucklefishID}"] = mod;
                        }
                        if (!string.IsNullOrWhiteSpace(mod.Manifest.NexusID))
                        {
                            modsByKey[$"Nexus:{mod.Manifest.NexusID}"] = mod;
                        }
                        if (!string.IsNullOrWhiteSpace(mod.Manifest.GitHubProject))
                        {
                            modsByKey[$"GitHub:{mod.Manifest.GitHubProject}"] = mod;
                        }
                    }

                    // fetch results
                    IDictionary <string, ModInfoModel> response           = client.GetModInfoAsync(modsByKey.Keys.ToArray()).Result;
                    IDictionary <IModMetadata, ModInfoModel> updatesByMod = new Dictionary <IModMetadata, ModInfoModel>();
                    foreach (var entry in response)
                    {
                        // handle error
                        if (entry.Value.Error != null)
                        {
                            this.Monitor.Log($"Couldn't fetch version of {modsByKey[entry.Key].DisplayName} with key {entry.Key}:\n{entry.Value.Error}", LogLevel.Trace);
                            continue;
                        }

                        // collect latest mod version
                        IModMetadata mod         = modsByKey[entry.Key];
                        ISemanticVersion version = new SemanticVersion(entry.Value.Version);
                        if (version.IsNewerThan(mod.Manifest.Version))
                        {
                            if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || version.IsNewerThan(other.Version))
                            {
                                updatesByMod[mod] = entry.Value;
                                modUpdates++;
                            }
                        }
                    }

                    // add to output queue
                    if (updatesByMod.Any())
                    {
                        foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName))
                        {
                            updates.Add($"{entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}");
                        }
                    }
                }
                catch (Exception ex)
                {
                    this.Monitor.Log($"Couldn't check for new mod versions:\n{ex.GetLogSummary()}", LogLevel.Trace);
                }

                // output
                if (updates.Any())
                {
                    this.Monitor.Newline();

                    // print intro
                    string intro = "";
                    if (smapiUpdate)
                    {
                        intro = "You can update SMAPI";
                    }
                    if (modUpdates > 0)
                    {
                        intro += $"{(smapiUpdate ? " and" : "You can update")} {modUpdates} mod{(modUpdates != 1 ? "s" : "")}";
                    }
                    intro += ":";
                    this.Monitor.Log(intro, LogLevel.Alert);

                    // print update list
                    foreach (string line in updates)
                    {
                        this.Monitor.Log($"   {line}", LogLevel.Alert);
                    }
                }
            }).Start();
        }
Пример #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);
        }
Пример #12
0
        /// <summary>Save data fetched for a mod.</summary>
        /// <param name="site">The mod site on which the mod is found.</param>
        /// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param>
        /// <param name="mod">The mod data.</param>
        /// <param name="cachedMod">The stored mod record.</param>
        public void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod)
        {
            string key = this.GetKey(site, id);

            cachedMod = this.SaveMod(new CachedMod(site, id, mod));
        }