add support for CurseForge update keys (#605)
This commit is contained in:
parent
fed71886a9
commit
8b09a2776d
|
@ -47,6 +47,7 @@ For modders:
|
||||||
* Now ignores metadata files and folders (like `__MACOSX` and `__folder_managed_by_vortex`) and content files (like `.txt` or `.png`), which avoids missing-manifest errors in some common cases.
|
* Now ignores metadata files and folders (like `__MACOSX` and `__folder_managed_by_vortex`) and content files (like `.txt` or `.png`), which avoids missing-manifest errors in some common cases.
|
||||||
* Now detects XNB mods more accurately, and consolidates multi-folder XNB mods in logged messages.
|
* Now detects XNB mods more accurately, and consolidates multi-folder XNB mods in logged messages.
|
||||||
* SMAPI now automatically removes invalid content when loading a save to prevent crashes. A warning is shown in-game when this happens. This applies for locations and NPCs.
|
* SMAPI now automatically removes invalid content when loading a save to prevent crashes. A warning is shown in-game when this happens. This applies for locations and NPCs.
|
||||||
|
* Added update checks for CurseForge mods.
|
||||||
* Added support for configuring console colors via `smapi-internal/config.json` (intended for players with unusual consoles).
|
* Added support for configuring console colors via `smapi-internal/config.json` (intended for players with unusual consoles).
|
||||||
* Added support for specifying SMAPI command-line arguments as environment variables for Linux/Mac compatibility.
|
* Added support for specifying SMAPI command-line arguments as environment variables for Linux/Mac compatibility.
|
||||||
* Improved launch script compatibility on Linux (thanks to kurumushi and toastal!).
|
* Improved launch script compatibility on Linux (thanks to kurumushi and toastal!).
|
||||||
|
|
|
@ -9,6 +9,9 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
|
||||||
/// <summary>The Chucklefish mod repository.</summary>
|
/// <summary>The Chucklefish mod repository.</summary>
|
||||||
Chucklefish,
|
Chucklefish,
|
||||||
|
|
||||||
|
/// <summary>The CurseForge mod repository.</summary>
|
||||||
|
CurseForge,
|
||||||
|
|
||||||
/// <summary>A GitHub project containing releases.</summary>
|
/// <summary>A GitHub project containing releases.</summary>
|
||||||
GitHub,
|
GitHub,
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||||
using StardewModdingAPI.Web.Framework.Caching.Mods;
|
using StardewModdingAPI.Web.Framework.Caching.Mods;
|
||||||
using StardewModdingAPI.Web.Framework.Caching.Wiki;
|
using StardewModdingAPI.Web.Framework.Caching.Wiki;
|
||||||
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
|
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
|
||||||
|
using StardewModdingAPI.Web.Framework.Clients.CurseForge;
|
||||||
using StardewModdingAPI.Web.Framework.Clients.GitHub;
|
using StardewModdingAPI.Web.Framework.Clients.GitHub;
|
||||||
using StardewModdingAPI.Web.Framework.Clients.ModDrop;
|
using StardewModdingAPI.Web.Framework.Clients.ModDrop;
|
||||||
using StardewModdingAPI.Web.Framework.Clients.Nexus;
|
using StardewModdingAPI.Web.Framework.Clients.Nexus;
|
||||||
|
@ -61,10 +62,11 @@ namespace StardewModdingAPI.Web.Controllers
|
||||||
/// <param name="modCache">The cache in which to store mod metadata.</param>
|
/// <param name="modCache">The cache in which to store mod metadata.</param>
|
||||||
/// <param name="configProvider">The config settings for mod update checks.</param>
|
/// <param name="configProvider">The config settings for mod update checks.</param>
|
||||||
/// <param name="chucklefish">The Chucklefish API client.</param>
|
/// <param name="chucklefish">The Chucklefish API client.</param>
|
||||||
|
/// <param name="curseForge">The CurseForge API client.</param>
|
||||||
/// <param name="github">The GitHub API client.</param>
|
/// <param name="github">The GitHub API client.</param>
|
||||||
/// <param name="modDrop">The ModDrop API client.</param>
|
/// <param name="modDrop">The ModDrop API client.</param>
|
||||||
/// <param name="nexus">The Nexus API client.</param>
|
/// <param name="nexus">The Nexus API client.</param>
|
||||||
public ModsApiController(IHostingEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> configProvider, IChucklefishClient chucklefish, IGitHubClient github, IModDropClient modDrop, INexusClient nexus)
|
public ModsApiController(IHostingEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> configProvider, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus)
|
||||||
{
|
{
|
||||||
this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json"));
|
this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json"));
|
||||||
ModUpdateCheckConfig config = configProvider.Value;
|
ModUpdateCheckConfig config = configProvider.Value;
|
||||||
|
@ -78,6 +80,7 @@ namespace StardewModdingAPI.Web.Controllers
|
||||||
new IModRepository[]
|
new IModRepository[]
|
||||||
{
|
{
|
||||||
new ChucklefishRepository(chucklefish),
|
new ChucklefishRepository(chucklefish),
|
||||||
|
new CurseForgeRepository(curseForge),
|
||||||
new GitHubRepository(github),
|
new GitHubRepository(github),
|
||||||
new ModDropRepository(modDrop),
|
new ModDropRepository(modDrop),
|
||||||
new NexusRepository(nexus)
|
new NexusRepository(nexus)
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Pathoschild.Http.Client;
|
||||||
|
using StardewModdingAPI.Toolkit;
|
||||||
|
using StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
|
||||||
|
{
|
||||||
|
/// <summary>An HTTP client for fetching mod metadata from the CurseForge API.</summary>
|
||||||
|
internal class CurseForgeClient : ICurseForgeClient
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Fields
|
||||||
|
*********/
|
||||||
|
/// <summary>The underlying HTTP client.</summary>
|
||||||
|
private readonly IClient Client;
|
||||||
|
|
||||||
|
/// <summary>A regex pattern which matches a version number in a CurseForge mod file name.</summary>
|
||||||
|
private readonly Regex VersionInNamePattern = new Regex(@"^(?:.+? | *)v?(\d+\.\d+(?:\.\d+)?(?:-.+?)?) *(?:\.(?:zip|rar|7z))?$", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Public methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Construct an instance.</summary>
|
||||||
|
/// <param name="userAgent">The user agent for the API client.</param>
|
||||||
|
/// <param name="apiUrl">The base URL for the CurseForge API.</param>
|
||||||
|
public CurseForgeClient(string userAgent, string apiUrl)
|
||||||
|
{
|
||||||
|
this.Client = new FluentClient(apiUrl).SetUserAgent(userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get metadata about a mod.</summary>
|
||||||
|
/// <param name="id">The CurseForge mod ID.</param>
|
||||||
|
/// <returns>Returns the mod info if found, else <c>null</c>.</returns>
|
||||||
|
public async Task<CurseForgeMod> GetModAsync(long id)
|
||||||
|
{
|
||||||
|
// get raw data
|
||||||
|
ModModel mod = await this.Client
|
||||||
|
.GetAsync($"addon/{id}")
|
||||||
|
.As<ModModel>();
|
||||||
|
if (mod == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// get latest versions
|
||||||
|
string invalidVersion = null;
|
||||||
|
ISemanticVersion latest = null;
|
||||||
|
foreach (ModFileModel file in mod.LatestFiles)
|
||||||
|
{
|
||||||
|
// extract version
|
||||||
|
ISemanticVersion version;
|
||||||
|
{
|
||||||
|
string raw = this.GetRawVersion(file);
|
||||||
|
if (raw == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!SemanticVersion.TryParse(raw, out version))
|
||||||
|
{
|
||||||
|
if (invalidVersion == null)
|
||||||
|
invalidVersion = raw;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// track latest version
|
||||||
|
if (latest == null || version.IsNewerThan(latest))
|
||||||
|
latest = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get error
|
||||||
|
string error = null;
|
||||||
|
if (latest == null && invalidVersion == null)
|
||||||
|
{
|
||||||
|
error = mod.LatestFiles.Any()
|
||||||
|
? $"CurseForge mod {id} has no downloads which specify the version in a recognised format."
|
||||||
|
: $"CurseForge mod {id} has no downloads.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate result
|
||||||
|
return new CurseForgeMod
|
||||||
|
{
|
||||||
|
Name = mod.Name,
|
||||||
|
LatestVersion = latest?.ToString() ?? invalidVersion,
|
||||||
|
Url = mod.WebsiteUrl,
|
||||||
|
Error = error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
this.Client?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Private methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Get a raw version string for a mod file, if available.</summary>
|
||||||
|
/// <param name="file">The file whose version to get.</param>
|
||||||
|
private string GetRawVersion(ModFileModel file)
|
||||||
|
{
|
||||||
|
Match match = this.VersionInNamePattern.Match(file.DisplayName);
|
||||||
|
if (!match.Success)
|
||||||
|
match = this.VersionInNamePattern.Match(file.FileName);
|
||||||
|
|
||||||
|
return match.Success
|
||||||
|
? match.Groups[1].Value
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
|
||||||
|
{
|
||||||
|
/// <summary>Mod metadata from the CurseForge API.</summary>
|
||||||
|
internal class CurseForgeMod
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Accessors
|
||||||
|
*********/
|
||||||
|
/// <summary>The mod name.</summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>The latest file version.</summary>
|
||||||
|
public string LatestVersion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>The mod's web URL.</summary>
|
||||||
|
public string Url { get; set; }
|
||||||
|
|
||||||
|
/// <summary>A user-friendly error which indicates why fetching the mod info failed (if applicable).</summary>
|
||||||
|
public string Error { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
|
||||||
|
{
|
||||||
|
/// <summary>An HTTP client for fetching mod metadata from the CurseForge API.</summary>
|
||||||
|
internal interface ICurseForgeClient : IDisposable
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Get metadata about a mod.</summary>
|
||||||
|
/// <param name="id">The CurseForge mod ID.</param>
|
||||||
|
/// <returns>Returns the mod info if found, else <c>null</c>.</returns>
|
||||||
|
Task<CurseForgeMod> GetModAsync(long id);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels
|
||||||
|
{
|
||||||
|
/// <summary>Metadata from the CurseForge API about a mod file.</summary>
|
||||||
|
public class ModFileModel
|
||||||
|
{
|
||||||
|
/// <summary>The file name as downloaded.</summary>
|
||||||
|
public string FileName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>The file display name.</summary>
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels
|
||||||
|
{
|
||||||
|
/// <summary>An mod from the CurseForge API.</summary>
|
||||||
|
public class ModModel
|
||||||
|
{
|
||||||
|
/// <summary>The mod's unique ID on CurseForge.</summary>
|
||||||
|
public int ID { get; set; }
|
||||||
|
|
||||||
|
/// <summary>The mod name.</summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>The web URL for the mod page.</summary>
|
||||||
|
public string WebsiteUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>The available file downloads.</summary>
|
||||||
|
public ModFileModel[] LatestFiles { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,13 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
|
||||||
public string ChucklefishModPageUrlFormat { get; set; }
|
public string ChucklefishModPageUrlFormat { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/****
|
||||||
|
** CurseForge
|
||||||
|
****/
|
||||||
|
/// <summary>The base URL for the CurseForge API.</summary>
|
||||||
|
public string CurseForgeBaseUrl { get; set; }
|
||||||
|
|
||||||
|
|
||||||
/****
|
/****
|
||||||
** GitHub
|
** GitHub
|
||||||
****/
|
****/
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||||
|
using StardewModdingAPI.Web.Framework.Clients.CurseForge;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||||
|
{
|
||||||
|
/// <summary>An HTTP client for fetching mod metadata from CurseForge.</summary>
|
||||||
|
internal class CurseForgeRepository : RepositoryBase
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Fields
|
||||||
|
*********/
|
||||||
|
/// <summary>The underlying CurseForge API client.</summary>
|
||||||
|
private readonly ICurseForgeClient Client;
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Public methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Construct an instance.</summary>
|
||||||
|
/// <param name="client">The underlying CurseForge API client.</param>
|
||||||
|
public CurseForgeRepository(ICurseForgeClient client)
|
||||||
|
: base(ModRepositoryKey.CurseForge)
|
||||||
|
{
|
||||||
|
this.Client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get metadata about a mod in the repository.</summary>
|
||||||
|
/// <param name="id">The mod ID in this repository.</param>
|
||||||
|
public override async Task<ModInfoModel> GetModInfoAsync(string id)
|
||||||
|
{
|
||||||
|
// validate ID format
|
||||||
|
if (!uint.TryParse(id, out uint curseID))
|
||||||
|
return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid CurseForge mod ID, must be an integer ID.");
|
||||||
|
|
||||||
|
// fetch info
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CurseForgeMod mod = await this.Client.GetModAsync(curseID);
|
||||||
|
if (mod == null)
|
||||||
|
return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no CurseForge mod with this ID.");
|
||||||
|
if (mod.Error != null)
|
||||||
|
{
|
||||||
|
RemoteModStatus remoteStatus = RemoteModStatus.InvalidData;
|
||||||
|
return new ModInfoModel().SetError(remoteStatus, mod.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ModInfoModel(name: mod.Name, version: this.NormalizeVersion(mod.LatestVersion), url: mod.Url);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
this.Client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ using StardewModdingAPI.Web.Framework.Caching;
|
||||||
using StardewModdingAPI.Web.Framework.Caching.Mods;
|
using StardewModdingAPI.Web.Framework.Caching.Mods;
|
||||||
using StardewModdingAPI.Web.Framework.Caching.Wiki;
|
using StardewModdingAPI.Web.Framework.Caching.Wiki;
|
||||||
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
|
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
|
||||||
|
using StardewModdingAPI.Web.Framework.Clients.CurseForge;
|
||||||
using StardewModdingAPI.Web.Framework.Clients.GitHub;
|
using StardewModdingAPI.Web.Framework.Clients.GitHub;
|
||||||
using StardewModdingAPI.Web.Framework.Clients.ModDrop;
|
using StardewModdingAPI.Web.Framework.Clients.ModDrop;
|
||||||
using StardewModdingAPI.Web.Framework.Clients.Nexus;
|
using StardewModdingAPI.Web.Framework.Clients.Nexus;
|
||||||
|
@ -119,6 +120,10 @@ namespace StardewModdingAPI.Web
|
||||||
baseUrl: api.ChucklefishBaseUrl,
|
baseUrl: api.ChucklefishBaseUrl,
|
||||||
modPageUrlFormat: api.ChucklefishModPageUrlFormat
|
modPageUrlFormat: api.ChucklefishModPageUrlFormat
|
||||||
));
|
));
|
||||||
|
services.AddSingleton<ICurseForgeClient>(new CurseForgeClient(
|
||||||
|
userAgent: userAgent,
|
||||||
|
apiUrl: api.CurseForgeBaseUrl
|
||||||
|
));
|
||||||
|
|
||||||
services.AddSingleton<IGitHubClient>(new GitHubClient(
|
services.AddSingleton<IGitHubClient>(new GitHubClient(
|
||||||
baseUrl: api.GitHubBaseUrl,
|
baseUrl: api.GitHubBaseUrl,
|
||||||
|
|
|
@ -30,6 +30,8 @@
|
||||||
"ChucklefishBaseUrl": "https://community.playstarbound.com",
|
"ChucklefishBaseUrl": "https://community.playstarbound.com",
|
||||||
"ChucklefishModPageUrlFormat": "resources/{0}",
|
"ChucklefishModPageUrlFormat": "resources/{0}",
|
||||||
|
|
||||||
|
"CurseForgeBaseUrl": "https://addons-ecs.forgesvc.net/api/v2/",
|
||||||
|
|
||||||
"GitHubBaseUrl": "https://api.github.com",
|
"GitHubBaseUrl": "https://api.github.com",
|
||||||
"GitHubAcceptHeader": "application/vnd.github.v3+json",
|
"GitHubAcceptHeader": "application/vnd.github.v3+json",
|
||||||
"GitHubUsername": null, // see top note
|
"GitHubUsername": null, // see top note
|
||||||
|
|
Loading…
Reference in New Issue