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); }
/// <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(); }
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); }