/// <summary>Assert that the versions of all SMAPI components are correct.</summary> /// <remarks>Players sometimes have mismatched versions (particularly when installed through Vortex), which can cause some very confusing bugs without this check.</remarks> private static void AssertSmapiVersions() { // get SMAPI version without prerelease suffix (since we can't get that from the assembly versions) ISemanticVersion smapiVersion = new SemanticVersion(Constants.ApiVersion.MajorVersion, Constants.ApiVersion.MinorVersion, Constants.ApiVersion.PatchVersion); // compare with assembly versions foreach (var type in new[] { typeof(IManifest), typeof(Manifest) }) { AssemblyName assemblyName = type.Assembly.GetName(); ISemanticVersion assemblyVersion = new SemanticVersion(assemblyName.Version); if (!assemblyVersion.Equals(smapiVersion)) { Program.PrintErrorAndExit($"Oops! The 'smapi-internal/{assemblyName.Name}.dll' file is version {assemblyVersion} instead of the required {Constants.ApiVersion}. SMAPI doesn't seem to be installed correctly."); } } }
/// <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(); }