add support for mapping non-semantic remote mod versions

This commit is contained in:
Jesse Plamondon-Willard 2018-03-04 18:46:05 -05:00
parent 90c8593ba9
commit 99023f9487
7 changed files with 50 additions and 28 deletions

View File

@ -6,6 +6,7 @@
* Fixed some incompatible-mod errors not showing the mod URL.
* Fixed rare crash with a specific combination of `manifest.json` fields and internal SMAPI mod data.
* Fixed update checks not working for Nexus Mods due to a change in their API.
* Fixed update checks failing for some older mods with non-standard versions.
* For the [log parser][]:
* Fixed mod list not including all mods if at least one has no author name.

View File

@ -12,6 +12,9 @@ namespace StardewModdingAPI.Common.Models
/// <summary>The namespaced mod keys to search.</summary>
public string[] ModKeys { get; set; }
/// <summary>Whether to allow non-semantic versions, instead of returning an error for those.</summary>
public bool AllowInvalidVersions { get; set; }
/*********
** Public methods
@ -24,9 +27,11 @@ namespace StardewModdingAPI.Common.Models
/// <summary>Construct an instance.</summary>
/// <param name="modKeys">The namespaced mod keys to search.</param>
public ModSearchModel(IEnumerable<string> modKeys)
/// <param name="allowInvalidVersions">Whether to allow non-semantic versions, instead of returning an error for those.</param>
public ModSearchModel(IEnumerable<string> modKeys, bool allowInvalidVersions)
{
this.ModKeys = modKeys.ToArray();
this.AllowInvalidVersions = allowInvalidVersions;
}
}
}

View File

@ -64,14 +64,15 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Fetch version metadata for the given mods.</summary>
/// <param name="modKeys">The namespaced mod keys to search as a comma-delimited array.</param>
/// <param name="allowInvalidVersions">Whether to allow non-semantic versions, instead of returning an error for those.</param>
[HttpGet]
public async Task<IDictionary<string, ModInfoModel>> GetAsync(string modKeys)
public async Task<IDictionary<string, ModInfoModel>> GetAsync(string modKeys, bool allowInvalidVersions = false)
{
string[] modKeysArray = modKeys?.Split(',').ToArray();
if (modKeysArray == null || !modKeysArray.Any())
return new Dictionary<string, ModInfoModel>();
return await this.PostAsync(new ModSearchModel(modKeysArray));
return await this.PostAsync(new ModSearchModel(modKeysArray, allowInvalidVersions));
}
/// <summary>Fetch version metadata for the given mods.</summary>
@ -79,7 +80,8 @@ namespace StardewModdingAPI.Web.Controllers
[HttpPost]
public async Task<IDictionary<string, ModInfoModel>> PostAsync([FromBody] ModSearchModel search)
{
// sort & filter keys
// parse model
bool allowInvalidVersions = search?.AllowInvalidVersions ?? false;
string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0])
.Distinct(StringComparer.CurrentCultureIgnoreCase)
.OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase)
@ -106,12 +108,20 @@ namespace StardewModdingAPI.Web.Controllers
// fetch mod info
result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry =>
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes);
// fetch info
ModInfoModel info = await repository.GetModInfoAsync(modID);
if (info.Error == null && (info.Version == null || !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)))
info = new ModInfoModel(info.Name, info.Version, info.Url, info.Version == null ? "Mod has no version number." : $"Mod has invalid semantic version '{info.Version}'.");
// validate
if (info.Error == null)
{
if (info.Version == null)
info = new ModInfoModel(info.Name, info.Version, info.Url, "Mod has no version number.");
if (!allowInvalidVersions && !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))
info = new ModInfoModel(info.Name, info.Version, info.Url, $"Mod has invalid semantic version '{info.Version}'.");
}
// cache & return
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes);
return info;
});
}

View File

@ -106,10 +106,10 @@ namespace StardewModdingAPI.Framework.ModData
/// <summary>Get a semantic local version for update checks.</summary>
/// <param name="version">The remote version to normalise.</param>
public string GetLocalVersionForUpdateChecks(string version)
public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version)
{
return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version, out string newVersion)
? newVersion
return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion)
? new SemanticVersion(newVersion)
: version;
}
@ -117,6 +117,11 @@ namespace StardewModdingAPI.Framework.ModData
/// <param name="version">The remote version to normalise.</param>
public string GetRemoteVersionForUpdateChecks(string version)
{
// normalise version if possible
if (SemanticVersion.TryParse(version, out ISemanticVersion parsed))
version = parsed.ToString();
// fetch remote version
return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion)
? newVersion
: version;

View File

@ -33,7 +33,7 @@ namespace StardewModdingAPI.Framework.ModData
*********/
/// <summary>Get a semantic local version for update checks.</summary>
/// <param name="version">The remote version to normalise.</param>
public string GetLocalVersionForUpdateChecks(string version)
public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version)
{
return this.DataRecord.GetLocalVersionForUpdateChecks(version);
}

View File

@ -40,7 +40,7 @@ namespace StardewModdingAPI.Framework
{
return this.Post<ModSearchModel, Dictionary<string, ModInfoModel>>(
$"v{this.Version}/mods",
new ModSearchModel(modKeys)
new ModSearchModel(modKeys, allowInvalidVersions: true)
);
}

View File

@ -604,29 +604,30 @@ namespace StardewModdingAPI
foreach (var result in results)
{
IModMetadata mod = result.Mod;
ModInfoModel info = result.Info;
ModInfoModel remoteInfo = result.Info;
// handle error
if (info.Error != null)
if (remoteInfo.Error != null)
{
this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {info.Error}", LogLevel.Trace);
this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {remoteInfo.Error}", LogLevel.Trace);
continue;
}
// track update
ISemanticVersion localVersion = mod.DataRecord != null
? new SemanticVersion(mod.DataRecord.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString()))
: mod.Manifest.Version;
ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord != null
? mod.DataRecord.GetRemoteVersionForUpdateChecks(new SemanticVersion(info.Version).ToString())
: info.Version
);
bool isUpdate = latestVersion.IsNewerThan(localVersion);
this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {info.Version}{(!latestVersion.Equals(new SemanticVersion(info.Version)) ? $" [{latestVersion}]" : "")}" : "okay")}.");
// 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))
{
this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: Mod has invalid version {remoteInfo.Version}", 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}{(!remoteVersion.Equals(new SemanticVersion(remoteInfo.Version)) ? $" [{remoteVersion}]" : "")}" : "okay")}.");
if (isUpdate)
{
if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version))
updatesByMod[mod] = info;
if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || remoteVersion.IsNewerThan(other.Version))
updatesByMod[mod] = remoteInfo;
}
}