From 873abef23563f5273e9b66d7b7e3cc2f5e4e0e92 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Sep 2017 19:15:07 -0400 Subject: add mod update checks based on manifest fields (#336) --- src/StardewModdingAPI/Events/GameEvents.cs | 10 +- src/StardewModdingAPI/Framework/Models/Manifest.cs | 10 +- src/StardewModdingAPI/Framework/SGame.cs | 2 +- src/StardewModdingAPI/IManifest.cs | 10 +- src/StardewModdingAPI/Program.cs | 107 +++++++++++++++++++-- .../StardewModdingAPI.config.json | 6 +- 6 files changed, 119 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Events/GameEvents.cs b/src/StardewModdingAPI/Events/GameEvents.cs index 5610e67a..deb71a86 100644 --- a/src/StardewModdingAPI/Events/GameEvents.cs +++ b/src/StardewModdingAPI/Events/GameEvents.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Framework; @@ -39,9 +39,6 @@ namespace StardewModdingAPI.Events /// Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after . internal static event EventHandler InitializeInternal; - /// Raised during launch after configuring Stardew Valley, loading it into memory, and opening the game window. The window is still blank by this point. - internal static event EventHandler GameLoadedInternal; - #if SMAPI_1_x /// Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after . [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the " + nameof(GameEvents.Initialize) + " event, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] @@ -143,19 +140,14 @@ namespace StardewModdingAPI.Events { monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.LoadContent)}", GameEvents._LoadContent?.GetInvocationList()); } -#endif /// Raise a event. /// Encapsulates monitoring and logging. internal static void InvokeGameLoaded(IMonitor monitor) { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoadedInternal)}", GameEvents.GameLoadedInternal?.GetInvocationList()); -#if SMAPI_1_x monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoaded)}", GameEvents._GameLoaded?.GetInvocationList()); -#endif } -#if SMAPI_1_x /// Raise a event. /// Encapsulates monitoring and logging. internal static void InvokeFirstUpdateTick(IMonitor monitor) diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs index 29c3517e..f97cb8ff 100644 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Newtonsoft.Json; using StardewModdingAPI.Framework.Serialisation; @@ -35,6 +35,14 @@ namespace StardewModdingAPI.Framework.Models [JsonConverter(typeof(SFieldConverter))] public IManifestDependency[] Dependencies { get; set; } +#if !SMAPI_1_x + /// The mod's unique ID in Nexus Mods (if any), used for update checks. + public string NexusID { get; set; } + + /// The mod's organisation and project name on GitHub (if any), used for update checks. + public string GitHubProject { get; set; } +#endif + /// The unique mod ID. public string UniqueID { get; set; } diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 76c106d7..387aeacc 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -297,8 +297,8 @@ namespace StardewModdingAPI.Framework GameEvents.InvokeInitialize(this.Monitor); #if SMAPI_1_x GameEvents.InvokeLoadContent(this.Monitor); -#endif GameEvents.InvokeGameLoaded(this.Monitor); +#endif } /********* diff --git a/src/StardewModdingAPI/IManifest.cs b/src/StardewModdingAPI/IManifest.cs index 407db1ce..28f6570c 100644 --- a/src/StardewModdingAPI/IManifest.cs +++ b/src/StardewModdingAPI/IManifest.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace StardewModdingAPI { @@ -32,7 +32,13 @@ namespace StardewModdingAPI /// The other mods that must be loaded before this mod. IManifestDependency[] Dependencies { get; } + /// The mod's unique ID in Nexus Mods (if any), used for update checks. + string NexusID { get; set; } + + /// The mod's organisation and project name on GitHub (if any), used for update checks. + string GitHubProject { get; set; } + /// Any manifest fields which didn't match a valid field. IDictionary ExtraFields { get; } } -} \ No newline at end of file +} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index df94b02a..cee3aefd 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -188,7 +188,6 @@ namespace StardewModdingAPI #endif this.GameInstance.Exiting += (sender, e) => this.Dispose(); GameEvents.InitializeInternal += (sender, e) => this.InitialiseAfterGameStart(); - GameEvents.GameLoadedInternal += (sender, e) => this.CheckForUpdateAsync(); ContentEvents.AfterLocaleChanged += (sender, e) => this.OnLocaleChanged(); // set window titles @@ -437,6 +436,9 @@ namespace StardewModdingAPI #else this.LoadMods(mods, new JsonHelper(), this.ContentManager); #endif + + // check for updates + this.CheckForUpdatesAsync(mods); } if (this.Monitor.IsExiting) { @@ -563,28 +565,113 @@ namespace StardewModdingAPI return !issuesFound; } - /// Asynchronously check for a new version of SMAPI, and print a message to the console if an update is available. - private void CheckForUpdateAsync() + /// Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available. + /// The mods to include in the update check (if eligible). + private void CheckForUpdatesAsync(IModMetadata[] mods) { if (!this.Settings.CheckForUpdates) return; new Thread(() => { + // update info + List updates = new List(); + bool smapiUpdate = false; + int modUpdates = 0; + + // create client + WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); + + // fetch SMAPI version try { - var client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); - string key = $"GitHub:{this.Settings.GitHubProjectName}"; - ModInfoModel info = client.GetModInfoAsync(key).Result[key]; - if (info.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{info.Error}"); - else if (new SemanticVersion(info.Version).IsNewerThan(Constants.ApiVersion)) - this.Monitor.Log($"You can update SMAPI from version {Constants.ApiVersion} to {info.Version}.", LogLevel.Alert); + 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 +#if !SMAPI_1_x + try + { + // prepare update-check data + IDictionary modsByKey = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + foreach (IModMetadata mod in mods) + { + 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 response = client.GetModInfoAsync(modsByKey.Keys.ToArray()).Result; + IDictionary updatesByMod = new Dictionary(); + 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); + } +#endif + + // output + if (updates.Any()) + { +#if !SMAPI_1_x + this.Monitor.Newline(); +#endif + + // 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(); } diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 67d8f270..c91d169c 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -15,9 +15,9 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "DeveloperMode": true, /** - * Whether SMAPI should check for a newer version when you load the game. If a new version is - * available, a small message will appear in the console. This doesn't affect the load time even - * if your connection is offline or slow, because it happens in the background. + * Whether SMAPI should check for newer versions of SMAPI and mods when you load the game. If new + * versions are available, an alert will be shown in the console. This doesn't affect the load + * time even if your connection is offline or slow, because it happens in the background. */ "CheckForUpdates": true, -- cgit