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) --- release-notes.md | 5 +- 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 +- 7 files changed, 123 insertions(+), 27 deletions(-) diff --git a/release-notes.md b/release-notes.md index c31d2bae..a07de71f 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,12 +6,15 @@ For players: * The console is now simpler and easier to read. * The console now adjusts its colors when you have a light terminal background. * SMAPI now detects mods which may impact game stability and shows a warning in the console. -* Updated compatibility list. +* SMAPI now alerts you in the console when one of your mods has a new version. * Renamed installer folder from `SMAPI 2.0` to `SMAPI 2.0 installer` to avoid confusion. +* Updated compatibility list. +* Fixed update check errors on Linux/Mac. For mod developers: * Added new APIs to edit, inject, and reload XNB assets loaded by the game at any time. _This let mods do anything previously only possible with XNB mods, plus enables new mod scenarios (e.g. seasonal textures, NPC clothing that depend on the weather or location, etc)._ +* Added new manifest fields to enable automatic update checks. * Added new input events. _The new `InputEvents` combine keyboard + mouse + controller input into one event for easy handling, add metadata like the cursor position and grab tile to support click handling, and add an option to suppress input from the game to enable new scenarios like action highjacking and UI overlays._ * Added support for optional dependencies. 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