////Taken from https://github.com/Pathoschild/SMAPI/blob/924c3a5d3fe6bfad483834112883156bdf202b57/src/SMAPI/Framework/SCore.cs#L669 /// <summary>Get whether a given version should be offered to the user as an update.</summary> /// <param name="currentVersion">The current semantic version.</param> /// <param name="newVersion">The target semantic version.</param> /// <param name="useBetaChannel">Whether the user enabled the beta channel and should be offered pre-release updates.</param> private bool IsValidUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel) { return (newVersion != null && newVersion.IsNewerThan(currentVersion) && (useBetaChannel || !newVersion.IsPrerelease())); }
/********* ** Private methods *********/ /// <summary>Get the mod version numbers for the given mod.</summary> /// <param name="mod">The mod to check.</param> /// <param name="subkey">The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.)</param> /// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</param> /// <param name="mapRemoteVersions">Maps remote versions to a semantic version for update checks.</param> /// <param name="main">The main mod version.</param> /// <param name="preview">The latest prerelease version, if newer than <paramref name="main"/>.</param> private bool TryGetLatestVersions(IModPage mod, string subkey, bool allowNonStandardVersions, IDictionary <string, string> mapRemoteVersions, out ISemanticVersion main, out ISemanticVersion preview) { main = null; preview = null; ISemanticVersion ParseVersion(string raw) { raw = this.NormalizeVersion(raw); return(this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions)); } if (mod != null) { // get mod version if (subkey == null) { main = ParseVersion(mod.Version); } // get file versions foreach (IModDownload download in mod.Downloads) { // check for subkey if specified if (subkey != null && download.Name?.Contains(subkey, StringComparison.OrdinalIgnoreCase) != true && download.Description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) != true) { continue; } // parse version ISemanticVersion cur = ParseVersion(download.Version); if (cur == null) { continue; } // track highest versions if (main == null || cur.IsNewerThan(main)) { main = cur; } if (cur.IsPrerelease() && (preview == null || cur.IsNewerThan(preview))) { preview = cur; } } if (preview != null && !preview.IsNewerThan(main)) { preview = null; } } return(main != null); }
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); }
/********* ** 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); }
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); }