From 786077340f2cea37d82455fc413535ae82a912ee Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 May 2020 21:55:11 -0400 Subject: refactor update check API This simplifies the logic for individual clients, centralises common logic, and prepares for upcoming features. --- .../Framework/UpdateData/ModRepositoryKey.cs | 24 -------- .../Framework/UpdateData/ModSiteKey.cs | 24 ++++++++ .../Framework/UpdateData/UpdateKey.cs | 67 +++++++++++++--------- 3 files changed, 63 insertions(+), 52 deletions(-) delete mode 100644 src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs create mode 100644 src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs (limited to 'src/SMAPI.Toolkit/Framework/UpdateData') diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs deleted file mode 100644 index 765ca334..00000000 --- a/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.UpdateData -{ - /// A mod repository which SMAPI can check for updates. - public enum ModRepositoryKey - { - /// An unknown or invalid mod repository. - Unknown, - - /// The Chucklefish mod repository. - Chucklefish, - - /// The CurseForge mod repository. - CurseForge, - - /// A GitHub project containing releases. - GitHub, - - /// The ModDrop mod repository. - ModDrop, - - /// The Nexus Mods mod repository. - Nexus - } -} diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs new file mode 100644 index 00000000..47cd3f7e --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Toolkit.Framework.UpdateData +{ + /// A mod site which SMAPI can check for updates. + public enum ModSiteKey + { + /// An unknown or invalid mod repository. + Unknown, + + /// The Chucklefish mod repository. + Chucklefish, + + /// The CurseForge mod repository. + CurseForge, + + /// A GitHub project containing releases. + GitHub, + + /// The ModDrop mod repository. + ModDrop, + + /// The Nexus Mods mod repository. + Nexus + } +} diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs index 3fc1759e..f6044148 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -11,8 +11,8 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// The raw update key text. public string RawText { get; } - /// The mod repository containing the mod. - public ModRepositoryKey Repository { get; } + /// The mod site containing the mod. + public ModSiteKey Site { get; } /// The mod ID within the repository. public string ID { get; } @@ -26,53 +26,56 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData *********/ /// Construct an instance. /// The raw update key text. - /// The mod repository containing the mod. - /// The mod ID within the repository. - public UpdateKey(string rawText, ModRepositoryKey repository, string id) + /// The mod site containing the mod. + /// The mod ID within the site. + public UpdateKey(string rawText, ModSiteKey site, string id) { - this.RawText = rawText; - this.Repository = repository; - this.ID = id; + this.RawText = rawText?.Trim(); + this.Site = site; + this.ID = id?.Trim(); this.LooksValid = - repository != ModRepositoryKey.Unknown + site != ModSiteKey.Unknown && !string.IsNullOrWhiteSpace(id); } /// Construct an instance. - /// The mod repository containing the mod. - /// The mod ID within the repository. - public UpdateKey(ModRepositoryKey repository, string id) - : this($"{repository}:{id}", repository, id) { } + /// The mod site containing the mod. + /// The mod ID within the site. + public UpdateKey(ModSiteKey site, string id) + : this(UpdateKey.GetString(site, id), site, id) { } /// Parse a raw update key. /// The raw update key to parse. public static UpdateKey Parse(string raw) { - // split parts - string[] parts = raw?.Split(':'); - if (parts == null || parts.Length != 2) - return new UpdateKey(raw, ModRepositoryKey.Unknown, null); - - // extract parts - string repositoryKey = parts[0].Trim(); - string id = parts[1].Trim(); + // extract site + ID + string rawSite; + string id; + { + string[] parts = raw?.Trim().Split(':'); + if (parts == null || parts.Length != 2) + return new UpdateKey(raw, ModSiteKey.Unknown, null); + + rawSite = parts[0].Trim(); + id = parts[1].Trim(); + } if (string.IsNullOrWhiteSpace(id)) id = null; // parse - if (!Enum.TryParse(repositoryKey, true, out ModRepositoryKey repository)) - return new UpdateKey(raw, ModRepositoryKey.Unknown, id); + if (!Enum.TryParse(rawSite, true, out ModSiteKey site)) + return new UpdateKey(raw, ModSiteKey.Unknown, id); if (id == null) - return new UpdateKey(raw, repository, null); + return new UpdateKey(raw, site, null); - return new UpdateKey(raw, repository, id); + return new UpdateKey(raw, site, id); } /// Get a string that represents the current object. public override string ToString() { return this.LooksValid - ? $"{this.Repository}:{this.ID}" + ? UpdateKey.GetString(this.Site, this.ID) : this.RawText; } @@ -82,7 +85,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData { return other != null - && this.Repository == other.Repository + && this.Site == other.Site && string.Equals(this.ID, other.ID, StringComparison.InvariantCultureIgnoreCase); } @@ -97,7 +100,15 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// A hash code for the current object. public override int GetHashCode() { - return $"{this.Repository}:{this.ID}".ToLower().GetHashCode(); + return $"{this.Site}:{this.ID}".ToLower().GetHashCode(); + } + + /// Get the string representation of an update key. + /// The mod site containing the mod. + /// The mod ID within the repository. + public static string GetString(ModSiteKey site, string id) + { + return $"{site}:{id}".Trim(); } } } -- cgit From d97b11060c310f1fa6064f02496e6a6162a44573 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 May 2020 00:21:51 -0400 Subject: add update subkeys --- docs/release-notes.md | 1 + .../Framework/UpdateData/UpdateKey.cs | 51 +++++++++++++++++----- src/SMAPI.Web/Controllers/ModsApiController.cs | 48 ++++++++++++-------- src/SMAPI.Web/Framework/ModSiteManager.cs | 28 +++++++++--- src/SMAPI.sln.DotSettings | 1 + 5 files changed, 92 insertions(+), 37 deletions(-) (limited to 'src/SMAPI.Toolkit/Framework/UpdateData') diff --git a/docs/release-notes.md b/docs/release-notes.md index 894fd562..57d32fd7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ * For modders: * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). + * Added [update subkeys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Update_subkeys). * Added `Multiplayer.PeerConnected` event. * Added `harmony_summary` console command which lists all current Harmony patches, optionally with a search filter. * Harmony mods which use the `[HarmonyPatch(type)]` attribute now work crossplatform. Previously SMAPI couldn't rewrite types in custom attributes for compatibility. diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs index f6044148..7e4d0220 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -17,6 +17,9 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// The mod ID within the repository. public string ID { get; } + /// If specified, a substring in download names/descriptions to match. + public string Subkey { get; } + /// Whether the update key seems to be valid. public bool LooksValid { get; } @@ -28,11 +31,13 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// The raw update key text. /// The mod site containing the mod. /// The mod ID within the site. - public UpdateKey(string rawText, ModSiteKey site, string id) + /// If specified, a substring in download names/descriptions to match. + public UpdateKey(string rawText, ModSiteKey site, string id, string subkey) { this.RawText = rawText?.Trim(); this.Site = site; this.ID = id?.Trim(); + this.Subkey = subkey?.Trim(); this.LooksValid = site != ModSiteKey.Unknown && !string.IsNullOrWhiteSpace(id); @@ -41,8 +46,9 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// Construct an instance. /// The mod site containing the mod. /// The mod ID within the site. - public UpdateKey(ModSiteKey site, string id) - : this(UpdateKey.GetString(site, id), site, id) { } + /// If specified, a substring in download names/descriptions to match. + public UpdateKey(ModSiteKey site, string id, string subkey) + : this(UpdateKey.GetString(site, id, subkey), site, id, subkey) { } /// Parse a raw update key. /// The raw update key to parse. @@ -54,7 +60,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData { string[] parts = raw?.Trim().Split(':'); if (parts == null || parts.Length != 2) - return new UpdateKey(raw, ModSiteKey.Unknown, null); + return new UpdateKey(raw, ModSiteKey.Unknown, null, null); rawSite = parts[0].Trim(); id = parts[1].Trim(); @@ -62,20 +68,32 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData if (string.IsNullOrWhiteSpace(id)) id = null; + // extract subkey + string subkey = null; + if (id != null) + { + string[] parts = id.Split('@'); + if (parts.Length == 2) + { + id = parts[0].Trim(); + subkey = $"@{parts[1]}".Trim(); + } + } + // parse if (!Enum.TryParse(rawSite, true, out ModSiteKey site)) - return new UpdateKey(raw, ModSiteKey.Unknown, id); + return new UpdateKey(raw, ModSiteKey.Unknown, id, subkey); if (id == null) - return new UpdateKey(raw, site, null); + return new UpdateKey(raw, site, null, subkey); - return new UpdateKey(raw, site, id); + return new UpdateKey(raw, site, id, subkey); } /// Get a string that represents the current object. public override string ToString() { return this.LooksValid - ? UpdateKey.GetString(this.Site, this.ID) + ? UpdateKey.GetString(this.Site, this.ID, this.Subkey) : this.RawText; } @@ -83,10 +101,18 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// An object to compare with this object. public bool Equals(UpdateKey other) { + if (!this.LooksValid) + { + return + other?.LooksValid == false + && this.RawText.Equals(other.RawText, StringComparison.OrdinalIgnoreCase); + } + return other != null && this.Site == other.Site - && string.Equals(this.ID, other.ID, StringComparison.InvariantCultureIgnoreCase); + && string.Equals(this.ID, other.ID, StringComparison.OrdinalIgnoreCase) + && string.Equals(this.Subkey, other.Subkey, StringComparison.OrdinalIgnoreCase); } /// Determines whether the specified object is equal to the current object. @@ -100,15 +126,16 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// A hash code for the current object. public override int GetHashCode() { - return $"{this.Site}:{this.ID}".ToLower().GetHashCode(); + return this.ToString().ToLower().GetHashCode(); } /// Get the string representation of an update key. /// The mod site containing the mod. /// The mod ID within the repository. - public static string GetString(ModSiteKey site, string id) + /// If specified, a substring in download names/descriptions to match. + public static string GetString(ModSiteKey site, string id, string subkey = null) { - return $"{site}:{id}".Trim(); + return $"{site}:{id}{subkey}".Trim(); } } } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 14be520d..028fc613 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -135,7 +135,7 @@ namespace StardewModdingAPI.Web.Controllers // validate update key if (!updateKey.LooksValid) { - errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541', with an optional subkey like 'Nexus:541@subkey'."); continue; } @@ -271,7 +271,7 @@ namespace StardewModdingAPI.Web.Controllers } // get version info - return this.ModSites.GetPageVersions(page, allowNonStandardVersions, mapRemoteVersions); + return this.ModSites.GetPageVersions(page, updateKey.Subkey, allowNonStandardVersions, mapRemoteVersions); } /// Get update keys based on the available mod metadata, while maintaining the precedence order. @@ -280,44 +280,56 @@ namespace StardewModdingAPI.Web.Controllers /// The mod's entry in the wiki list. private IEnumerable GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) { + // get every update key (including duplicates) IEnumerable GetRaw() { // specified update keys if (specifiedKeys != null) { foreach (string key in specifiedKeys) - yield return key?.Trim(); + { + if (!string.IsNullOrWhiteSpace(key)) + yield return key.Trim(); + } } // default update key string defaultKey = record?.GetDefaultUpdateKey(); - if (defaultKey != null) + if (!string.IsNullOrWhiteSpace(defaultKey)) yield return defaultKey; // wiki metadata if (entry != null) { if (entry.NexusID.HasValue) - yield return UpdateKey.GetString(ModSiteKey.Nexus, entry.NexusID?.ToString()); + yield return UpdateKey.GetString(ModSiteKey.Nexus, entry.NexusID.ToString()); if (entry.ModDropID.HasValue) - yield return UpdateKey.GetString(ModSiteKey.ModDrop, entry.ModDropID?.ToString()); + yield return UpdateKey.GetString(ModSiteKey.ModDrop, entry.ModDropID.ToString()); if (entry.CurseForgeID.HasValue) - yield return UpdateKey.GetString(ModSiteKey.CurseForge, entry.CurseForgeID?.ToString()); + yield return UpdateKey.GetString(ModSiteKey.CurseForge, entry.CurseForgeID.ToString()); if (entry.ChucklefishID.HasValue) - yield return UpdateKey.GetString(ModSiteKey.Chucklefish, entry.ChucklefishID?.ToString()); + yield return UpdateKey.GetString(ModSiteKey.Chucklefish, entry.ChucklefishID.ToString()); } } - HashSet seen = new HashSet(); - foreach (string rawKey in GetRaw()) - { - if (string.IsNullOrWhiteSpace(rawKey)) - continue; - - UpdateKey key = UpdateKey.Parse(rawKey); - if (seen.Add(key)) - yield return key; - } + // get unique update keys + var subkeyRoots = new HashSet(); + List updateKeys = GetRaw() + .Select(raw => + { + var key = UpdateKey.Parse(raw); + if (key.Subkey != null) + subkeyRoots.Add(new UpdateKey(key.Site, key.ID, null)); + return key; + }) + .Distinct() + .ToList(); + + // if the list has both an update key (like "Nexus:2400") and subkey (like "Nexus:2400@subkey") for the same page, the subkey takes priority + if (subkeyRoots.Any()) + updateKeys.RemoveAll(subkeyRoots.Contains); + + return updateKeys; } } } diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs index eaae7935..68b4c6ac 100644 --- a/src/SMAPI.Web/Framework/ModSiteManager.cs +++ b/src/SMAPI.Web/Framework/ModSiteManager.cs @@ -54,9 +54,10 @@ namespace StardewModdingAPI.Web.Framework /// Parse version info for the given mod page info. /// The mod page info. + /// The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.) /// Maps remote versions to a semantic version for update checks. /// Whether to allow non-standard versions. - public ModInfoModel GetPageVersions(IModPage page, bool allowNonStandardVersions, IDictionary mapRemoteVersions) + public ModInfoModel GetPageVersions(IModPage page, string subkey, bool allowNonStandardVersions, IDictionary mapRemoteVersions) { // get base model ModInfoModel model = new ModInfoModel() @@ -66,7 +67,10 @@ namespace StardewModdingAPI.Web.Framework return model; // fetch versions - if (!this.TryGetLatestVersions(page, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion mainVersion, out ISemanticVersion previewVersion)) + bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion mainVersion, out ISemanticVersion previewVersion); + if (!hasVersions && subkey != null) + hasVersions = this.TryGetLatestVersions(page, null, allowNonStandardVersions, mapRemoteVersions, out mainVersion, out previewVersion); + if (!hasVersions) return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}' has no valid versions."); // return info @@ -96,11 +100,12 @@ namespace StardewModdingAPI.Web.Framework *********/ /// Get the mod version numbers for the given mod. /// The mod to check. + /// The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.) /// Whether to allow non-standard versions. /// Maps remote versions to a semantic version for update checks. /// The main mod version. /// The latest prerelease version, if newer than . - private bool TryGetLatestVersions(IModPage mod, bool allowNonStandardVersions, IDictionary mapRemoteVersions, out ISemanticVersion main, out ISemanticVersion preview) + private bool TryGetLatestVersions(IModPage mod, string subkey, bool allowNonStandardVersions, IDictionary mapRemoteVersions, out ISemanticVersion main, out ISemanticVersion preview) { main = null; preview = null; @@ -113,14 +118,23 @@ namespace StardewModdingAPI.Web.Framework if (mod != null) { - // get versions - main = ParseVersion(mod.Version); - foreach (string rawVersion in mod.Downloads.Select(p => p.Version)) + // get mod version + if (subkey == null) + main = ParseVersion(mod.Version); + + // get file versions + foreach (IModDownload download in mod.Downloads) { - ISemanticVersion cur = ParseVersion(rawVersion); + // check for subkey if specified + if (subkey != null && download.Name?.Contains(subkey, StringComparison.OrdinalIgnoreCase) != true && download.Description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) != true) + continue; + + // parse version + ISemanticVersion cur = ParseVersion(download.Version); if (cur == null) continue; + // track highest versions if (main == null || cur.IsNewerThan(main)) main = cur; if (cur.IsPrerelease() && (preview == null || cur.IsNewerThan(preview))) diff --git a/src/SMAPI.sln.DotSettings b/src/SMAPI.sln.DotSettings index 556f1ec0..05caa938 100644 --- a/src/SMAPI.sln.DotSettings +++ b/src/SMAPI.sln.DotSettings @@ -58,6 +58,7 @@ True True True + True True True True -- cgit