diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index c0d6408d..b71c8056 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -1,5 +1,6 @@ using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.ModUpdateChecking; namespace StardewModdingAPI.Framework { @@ -45,6 +46,11 @@ namespace StardewModdingAPI.Framework /// Whether the mod is a content pack. bool IsContentPack { get; } + /// The update status of this mod (if any). + ModUpdateStatus UpdateStatus { get; } + + /// The preview update status of this mod (if any). + ModUpdateStatus PreviewUpdateStatus { get; } /********* ** Public methods @@ -72,6 +78,14 @@ namespace StardewModdingAPI.Framework /// The mod-provided API. IModMetadata SetApi(object api); + /// Set the update status. + /// The mod update status. + IModMetadata SetUpdateStatus(ModUpdateStatus updateStatus); + + /// Set the preview update status. + /// The mod preview update status. + IModMetadata SetPreviewUpdateStatus(ModUpdateStatus previewUpdateStatus); + /// Whether the mod manifest was loaded (regardless of whether the mod itself was loaded). bool HasManifest(); diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index e4ec7e3b..88d2770c 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using StardewModdingAPI.Framework.ModData; +using StardewModdingAPI.Framework.ModUpdateChecking; namespace StardewModdingAPI.Framework.ModLoading { @@ -43,6 +44,12 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod-provided API (if any). public object Api { get; private set; } + /// The update status of this mod (if any). + public ModUpdateStatus UpdateStatus { get; private set; } + + /// The preview update status of this mod (if any). + public ModUpdateStatus PreviewUpdateStatus { get; private set; } + /// Whether the mod is a content pack. public bool IsContentPack => this.Manifest?.ContentPackFor != null; @@ -115,6 +122,22 @@ namespace StardewModdingAPI.Framework.ModLoading return this; } + /// Set the update status. + /// The mod update status. + public IModMetadata SetUpdateStatus(ModUpdateStatus updateStatus) + { + this.UpdateStatus = updateStatus; + return this; + } + + /// Set the preview update status. + /// The mod preview update status. + public IModMetadata SetPreviewUpdateStatus(ModUpdateStatus previewUpdateStatus) + { + this.PreviewUpdateStatus = previewUpdateStatus; + return this; + } + /// Whether the mod manifest was loaded (regardless of whether the mod itself was loaded). public bool HasManifest() { diff --git a/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs b/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs new file mode 100644 index 00000000..efb32aef --- /dev/null +++ b/src/SMAPI/Framework/ModUpdateChecking/ModUpdateStatus.cs @@ -0,0 +1,37 @@ +namespace StardewModdingAPI.Framework.ModUpdateChecking +{ + /// Update status for a mod. + internal class ModUpdateStatus + { + /********* + ** Accessors + *********/ + /// The version that this mod can be updated to (if any). + public ISemanticVersion Version { get; } + + /// The error checking for updates of this mod (if any). + public string Error { get; } + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The version that this mod can be update to. + public ModUpdateStatus(ISemanticVersion version) + { + this.Version = version; + } + + /// Construct an instance. + /// The error checking for updates of this mod. + public ModUpdateStatus(string error) + { + this.Error = error; + } + + /// Construct an instance. + public ModUpdateStatus() + { + } + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 8c6cea1e..2ee18a29 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -24,6 +24,7 @@ using StardewModdingAPI.Framework.ModData; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.ModUpdateChecking; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; @@ -662,7 +663,7 @@ namespace StardewModdingAPI .ToArray(); // extract latest versions - IDictionary updatesByMod = new Dictionary(); + IDictionary> updatesByMod = new Dictionary>(); foreach (var result in results) { IModMetadata mod = result.Mod; @@ -671,35 +672,65 @@ namespace StardewModdingAPI // handle error if (remoteInfo.Error != null) { + if (mod.UpdateStatus?.Version == null) + mod.SetUpdateStatus(new ModUpdateStatus(remoteInfo.Error)); + if (mod.PreviewUpdateStatus?.Version == null) + mod.SetUpdateStatus(new ModUpdateStatus(remoteInfo.Error)); + this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {remoteInfo.Error}", LogLevel.Trace); continue; } // normalise versions ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; - if (!SemanticVersion.TryParse(mod.DataRecord?.GetRemoteVersionForUpdateChecks(remoteInfo.Version) ?? remoteInfo.Version, out ISemanticVersion remoteVersion)) + bool validVersion = SemanticVersion.TryParse(mod.DataRecord?.GetRemoteVersionForUpdateChecks(remoteInfo.Version) ?? remoteInfo.Version, out ISemanticVersion remoteVersion); + bool validPreviewVersion = SemanticVersion.TryParse(remoteInfo.PreviewVersion, out ISemanticVersion remotePreviewVersion); + + if (!validVersion && mod.UpdateStatus?.Version == null) + mod.SetUpdateStatus(new ModUpdateStatus($"Version is invalid: {remoteInfo.Version}")); + if (!validPreviewVersion && mod.PreviewUpdateStatus?.Version == null) + mod.SetPreviewUpdateStatus(new ModUpdateStatus($"Version is invalid: {remoteInfo.PreviewVersion}")); + + if (!validVersion && !validPreviewVersion) { - this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: Mod has invalid version {remoteInfo.Version}", LogLevel.Trace); + this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: Mod has invalid versions. version: {remoteInfo.Version}, preview version: {remoteInfo.PreviewVersion}", LogLevel.Trace); continue; } // compare versions - bool isUpdate = remoteVersion.IsNewerThan(localVersion); - this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {remoteInfo.Version}" : "okay")}."); + bool isPreviewUpdate = validPreviewVersion && localVersion.IsNewerThan(remoteVersion) && remotePreviewVersion.IsNewerThan(localVersion); + bool isUpdate = (validVersion && remoteVersion.IsNewerThan(localVersion)) || isPreviewUpdate; + + this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {(isPreviewUpdate ? remoteInfo.PreviewVersion : remoteInfo.Version)}" : "okay")}."); if (isUpdate) { - if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || remoteVersion.IsNewerThan(other.Version)) - updatesByMod[mod] = remoteInfo; + if (!updatesByMod.TryGetValue(mod, out Tuple other) || (isPreviewUpdate ? remotePreviewVersion : remoteVersion).IsNewerThan(other.Item2 ? other.Item1.PreviewVersion : other.Item1.Version)) + { + updatesByMod[mod] = new Tuple(remoteInfo, isPreviewUpdate); + + if (isPreviewUpdate) + mod.SetPreviewUpdateStatus(new ModUpdateStatus(remotePreviewVersion)); + else + mod.SetUpdateStatus(new ModUpdateStatus(remoteVersion)); + } } } + // set mods to have no updates + foreach (IModMetadata mod in results.Select(item => item.Mod) + .Where(item => !updatesByMod.ContainsKey(item))) + { + mod.SetUpdateStatus(new ModUpdateStatus()); + mod.SetPreviewUpdateStatus(new ModUpdateStatus()); + } + // 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); + this.Monitor.Log($" {entry.Key.DisplayName} {(entry.Value.Item2 ? entry.Value.Item1.PreviewVersion : entry.Value.Item1.Version)}: {entry.Value.Item1.Url}", LogLevel.Alert); } else this.Monitor.Log(" All mods up to date.", LogLevel.Trace); diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index fcd54c34..916dd053 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -110,6 +110,7 @@ +