Skip to content

Commit d97b110

Browse files
committed
add update subkeys
1 parent 7860773 commit d97b110

5 files changed

Lines changed: 92 additions & 37 deletions

File tree

docs/release-notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
* For modders:
1414
* Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info).
15+
* Added [update subkeys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Update_subkeys).
1516
* Added `Multiplayer.PeerConnected` event.
1617
* Added `harmony_summary` console command which lists all current Harmony patches, optionally with a search filter.
1718
* Harmony mods which use the `[HarmonyPatch(type)]` attribute now work crossplatform. Previously SMAPI couldn't rewrite types in custom attributes for compatibility.

src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ public class UpdateKey : IEquatable<UpdateKey>
1717
/// <summary>The mod ID within the repository.</summary>
1818
public string ID { get; }
1919

20+
/// <summary>If specified, a substring in download names/descriptions to match.</summary>
21+
public string Subkey { get; }
22+
2023
/// <summary>Whether the update key seems to be valid.</summary>
2124
public bool LooksValid { get; }
2225

@@ -28,11 +31,13 @@ public class UpdateKey : IEquatable<UpdateKey>
2831
/// <param name="rawText">The raw update key text.</param>
2932
/// <param name="site">The mod site containing the mod.</param>
3033
/// <param name="id">The mod ID within the site.</param>
31-
public UpdateKey(string rawText, ModSiteKey site, string id)
34+
/// <param name="subkey">If specified, a substring in download names/descriptions to match.</param>
35+
public UpdateKey(string rawText, ModSiteKey site, string id, string subkey)
3236
{
3337
this.RawText = rawText?.Trim();
3438
this.Site = site;
3539
this.ID = id?.Trim();
40+
this.Subkey = subkey?.Trim();
3641
this.LooksValid =
3742
site != ModSiteKey.Unknown
3843
&& !string.IsNullOrWhiteSpace(id);
@@ -41,8 +46,9 @@ public UpdateKey(string rawText, ModSiteKey site, string id)
4146
/// <summary>Construct an instance.</summary>
4247
/// <param name="site">The mod site containing the mod.</param>
4348
/// <param name="id">The mod ID within the site.</param>
44-
public UpdateKey(ModSiteKey site, string id)
45-
: this(UpdateKey.GetString(site, id), site, id) { }
49+
/// <param name="subkey">If specified, a substring in download names/descriptions to match.</param>
50+
public UpdateKey(ModSiteKey site, string id, string subkey)
51+
: this(UpdateKey.GetString(site, id, subkey), site, id, subkey) { }
4652

4753
/// <summary>Parse a raw update key.</summary>
4854
/// <param name="raw">The raw update key to parse.</param>
@@ -54,39 +60,59 @@ public static UpdateKey Parse(string raw)
5460
{
5561
string[] parts = raw?.Trim().Split(':');
5662
if (parts == null || parts.Length != 2)
57-
return new UpdateKey(raw, ModSiteKey.Unknown, null);
63+
return new UpdateKey(raw, ModSiteKey.Unknown, null, null);
5864

5965
rawSite = parts[0].Trim();
6066
id = parts[1].Trim();
6167
}
6268
if (string.IsNullOrWhiteSpace(id))
6369
id = null;
6470

71+
// extract subkey
72+
string subkey = null;
73+
if (id != null)
74+
{
75+
string[] parts = id.Split('@');
76+
if (parts.Length == 2)
77+
{
78+
id = parts[0].Trim();
79+
subkey = $"@{parts[1]}".Trim();
80+
}
81+
}
82+
6583
// parse
6684
if (!Enum.TryParse(rawSite, true, out ModSiteKey site))
67-
return new UpdateKey(raw, ModSiteKey.Unknown, id);
85+
return new UpdateKey(raw, ModSiteKey.Unknown, id, subkey);
6886
if (id == null)
69-
return new UpdateKey(raw, site, null);
87+
return new UpdateKey(raw, site, null, subkey);
7088

71-
return new UpdateKey(raw, site, id);
89+
return new UpdateKey(raw, site, id, subkey);
7290
}
7391

7492
/// <summary>Get a string that represents the current object.</summary>
7593
public override string ToString()
7694
{
7795
return this.LooksValid
78-
? UpdateKey.GetString(this.Site, this.ID)
96+
? UpdateKey.GetString(this.Site, this.ID, this.Subkey)
7997
: this.RawText;
8098
}
8199

82100
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
83101
/// <param name="other">An object to compare with this object.</param>
84102
public bool Equals(UpdateKey other)
85103
{
104+
if (!this.LooksValid)
105+
{
106+
return
107+
other?.LooksValid == false
108+
&& this.RawText.Equals(other.RawText, StringComparison.OrdinalIgnoreCase);
109+
}
110+
86111
return
87112
other != null
88113
&& this.Site == other.Site
89-
&& string.Equals(this.ID, other.ID, StringComparison.InvariantCultureIgnoreCase);
114+
&& string.Equals(this.ID, other.ID, StringComparison.OrdinalIgnoreCase)
115+
&& string.Equals(this.Subkey, other.Subkey, StringComparison.OrdinalIgnoreCase);
90116
}
91117

92118
/// <summary>Determines whether the specified object is equal to the current object.</summary>
@@ -100,15 +126,16 @@ public override bool Equals(object obj)
100126
/// <returns>A hash code for the current object.</returns>
101127
public override int GetHashCode()
102128
{
103-
return $"{this.Site}:{this.ID}".ToLower().GetHashCode();
129+
return this.ToString().ToLower().GetHashCode();
104130
}
105131

106132
/// <summary>Get the string representation of an update key.</summary>
107133
/// <param name="site">The mod site containing the mod.</param>
108134
/// <param name="id">The mod ID within the repository.</param>
109-
public static string GetString(ModSiteKey site, string id)
135+
/// <param name="subkey">If specified, a substring in download names/descriptions to match.</param>
136+
public static string GetString(ModSiteKey site, string id, string subkey = null)
110137
{
111-
return $"{site}:{id}".Trim();
138+
return $"{site}:{id}{subkey}".Trim();
112139
}
113140
}
114141
}

src/SMAPI.Web/Controllers/ModsApiController.cs

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ private async Task<ModEntryModel> GetModData(ModSearchEntryModel search, WikiMod
135135
// validate update key
136136
if (!updateKey.LooksValid)
137137
{
138-
errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'.");
138+
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'.");
139139
continue;
140140
}
141141

@@ -271,7 +271,7 @@ private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey, b
271271
}
272272

273273
// get version info
274-
return this.ModSites.GetPageVersions(page, allowNonStandardVersions, mapRemoteVersions);
274+
return this.ModSites.GetPageVersions(page, updateKey.Subkey, allowNonStandardVersions, mapRemoteVersions);
275275
}
276276

277277
/// <summary>Get update keys based on the available mod metadata, while maintaining the precedence order.</summary>
@@ -280,44 +280,56 @@ private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey, b
280280
/// <param name="entry">The mod's entry in the wiki list.</param>
281281
private IEnumerable<UpdateKey> GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry)
282282
{
283+
// get every update key (including duplicates)
283284
IEnumerable<string> GetRaw()
284285
{
285286
// specified update keys
286287
if (specifiedKeys != null)
287288
{
288289
foreach (string key in specifiedKeys)
289-
yield return key?.Trim();
290+
{
291+
if (!string.IsNullOrWhiteSpace(key))
292+
yield return key.Trim();
293+
}
290294
}
291295

292296
// default update key
293297
string defaultKey = record?.GetDefaultUpdateKey();
294-
if (defaultKey != null)
298+
if (!string.IsNullOrWhiteSpace(defaultKey))
295299
yield return defaultKey;
296300

297301
// wiki metadata
298302
if (entry != null)
299303
{
300304
if (entry.NexusID.HasValue)
301-
yield return UpdateKey.GetString(ModSiteKey.Nexus, entry.NexusID?.ToString());
305+
yield return UpdateKey.GetString(ModSiteKey.Nexus, entry.NexusID.ToString());
302306
if (entry.ModDropID.HasValue)
303-
yield return UpdateKey.GetString(ModSiteKey.ModDrop, entry.ModDropID?.ToString());
307+
yield return UpdateKey.GetString(ModSiteKey.ModDrop, entry.ModDropID.ToString());
304308
if (entry.CurseForgeID.HasValue)
305-
yield return UpdateKey.GetString(ModSiteKey.CurseForge, entry.CurseForgeID?.ToString());
309+
yield return UpdateKey.GetString(ModSiteKey.CurseForge, entry.CurseForgeID.ToString());
306310
if (entry.ChucklefishID.HasValue)
307-
yield return UpdateKey.GetString(ModSiteKey.Chucklefish, entry.ChucklefishID?.ToString());
311+
yield return UpdateKey.GetString(ModSiteKey.Chucklefish, entry.ChucklefishID.ToString());
308312
}
309313
}
310314

311-
HashSet<UpdateKey> seen = new HashSet<UpdateKey>();
312-
foreach (string rawKey in GetRaw())
313-
{
314-
if (string.IsNullOrWhiteSpace(rawKey))
315-
continue;
316-
317-
UpdateKey key = UpdateKey.Parse(rawKey);
318-
if (seen.Add(key))
319-
yield return key;
320-
}
315+
// get unique update keys
316+
var subkeyRoots = new HashSet<UpdateKey>();
317+
List<UpdateKey> updateKeys = GetRaw()
318+
.Select(raw =>
319+
{
320+
var key = UpdateKey.Parse(raw);
321+
if (key.Subkey != null)
322+
subkeyRoots.Add(new UpdateKey(key.Site, key.ID, null));
323+
return key;
324+
})
325+
.Distinct()
326+
.ToList();
327+
328+
// 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
329+
if (subkeyRoots.Any())
330+
updateKeys.RemoveAll(subkeyRoots.Contains);
331+
332+
return updateKeys;
321333
}
322334
}
323335
}

src/SMAPI.Web/Framework/ModSiteManager.cs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,10 @@ public async Task<IModPage> GetModPageAsync(UpdateKey updateKey)
5454

5555
/// <summary>Parse version info for the given mod page info.</summary>
5656
/// <param name="page">The mod page info.</param>
57+
/// <param name="subkey">The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.)</param>
5758
/// <param name="mapRemoteVersions">Maps remote versions to a semantic version for update checks.</param>
5859
/// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</param>
59-
public ModInfoModel GetPageVersions(IModPage page, bool allowNonStandardVersions, IDictionary<string, string> mapRemoteVersions)
60+
public ModInfoModel GetPageVersions(IModPage page, string subkey, bool allowNonStandardVersions, IDictionary<string, string> mapRemoteVersions)
6061
{
6162
// get base model
6263
ModInfoModel model = new ModInfoModel()
@@ -66,7 +67,10 @@ public ModInfoModel GetPageVersions(IModPage page, bool allowNonStandardVersions
6667
return model;
6768

6869
// fetch versions
69-
if (!this.TryGetLatestVersions(page, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion mainVersion, out ISemanticVersion previewVersion))
70+
bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion mainVersion, out ISemanticVersion previewVersion);
71+
if (!hasVersions && subkey != null)
72+
hasVersions = this.TryGetLatestVersions(page, null, allowNonStandardVersions, mapRemoteVersions, out mainVersion, out previewVersion);
73+
if (!hasVersions)
7074
return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}' has no valid versions.");
7175

7276
// return info
@@ -96,11 +100,12 @@ public ISemanticVersion GetMappedVersion(string version, IDictionary<string, str
96100
*********/
97101
/// <summary>Get the mod version numbers for the given mod.</summary>
98102
/// <param name="mod">The mod to check.</param>
103+
/// <param name="subkey">The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.)</param>
99104
/// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</param>
100105
/// <param name="mapRemoteVersions">Maps remote versions to a semantic version for update checks.</param>
101106
/// <param name="main">The main mod version.</param>
102107
/// <param name="preview">The latest prerelease version, if newer than <paramref name="main"/>.</param>
103-
private bool TryGetLatestVersions(IModPage mod, bool allowNonStandardVersions, IDictionary<string, string> mapRemoteVersions, out ISemanticVersion main, out ISemanticVersion preview)
108+
private bool TryGetLatestVersions(IModPage mod, string subkey, bool allowNonStandardVersions, IDictionary<string, string> mapRemoteVersions, out ISemanticVersion main, out ISemanticVersion preview)
104109
{
105110
main = null;
106111
preview = null;
@@ -113,14 +118,23 @@ ISemanticVersion ParseVersion(string raw)
113118

114119
if (mod != null)
115120
{
116-
// get versions
117-
main = ParseVersion(mod.Version);
118-
foreach (string rawVersion in mod.Downloads.Select(p => p.Version))
121+
// get mod version
122+
if (subkey == null)
123+
main = ParseVersion(mod.Version);
124+
125+
// get file versions
126+
foreach (IModDownload download in mod.Downloads)
119127
{
120-
ISemanticVersion cur = ParseVersion(rawVersion);
128+
// check for subkey if specified
129+
if (subkey != null && download.Name?.Contains(subkey, StringComparison.OrdinalIgnoreCase) != true && download.Description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) != true)
130+
continue;
131+
132+
// parse version
133+
ISemanticVersion cur = ParseVersion(download.Version);
121134
if (cur == null)
122135
continue;
123136

137+
// track highest versions
124138
if (main == null || cur.IsNewerThan(main))
125139
main = cur;
126140
if (cur.IsPrerelease() && (preview == null || cur.IsNewerThan(preview)))

src/SMAPI.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
<s:Boolean x:Key="/Default/UserDictionary/Words/=stackable/@EntryIndexedValue">True</s:Boolean>
5959
<s:Boolean x:Key="/Default/UserDictionary/Words/=Stardew/@EntryIndexedValue">True</s:Boolean>
6060
<s:Boolean x:Key="/Default/UserDictionary/Words/=subdomain/@EntryIndexedValue">True</s:Boolean>
61+
<s:Boolean x:Key="/Default/UserDictionary/Words/=subkey/@EntryIndexedValue">True</s:Boolean>
6162
<s:Boolean x:Key="/Default/UserDictionary/Words/=synchronised/@EntryIndexedValue">True</s:Boolean>
6263
<s:Boolean x:Key="/Default/UserDictionary/Words/=textbox/@EntryIndexedValue">True</s:Boolean>
6364
<s:Boolean x:Key="/Default/UserDictionary/Words/=thumbstick/@EntryIndexedValue">True</s:Boolean>

0 commit comments

Comments
 (0)