Merge branch 'develop' into stable
This commit is contained in:
commit
15d4b6310e
|
@ -2,5 +2,5 @@ using System.Reflection;
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: AssemblyVersion("2.2.0.0")]
|
||||
[assembly: AssemblyFileVersion("2.2.0.0")]
|
||||
[assembly: AssemblyVersion("2.3.0.0")]
|
||||
[assembly: AssemblyFileVersion("2.3.0.0")]
|
||||
|
|
|
@ -1,4 +1,24 @@
|
|||
# Release notes
|
||||
## 2.3
|
||||
* For players:
|
||||
* Added a user-friendly [download page](https://smapi.io).
|
||||
* Improved cryptic libgdiplus errors on Mac when Mono isn't installed.
|
||||
* Fixed mod UIs hidden when menu backgrounds are enabled.
|
||||
|
||||
* For modders:
|
||||
* **Added mod-provided APIs** to allow simple integrations between mods, even without direct assembly references.
|
||||
* Added `GameEvents.FirstUpdateTick` event (called once after all mods are initialised).
|
||||
* Added `IsSuppressed` to input events so mods can optionally avoid handling keys another mod has already handled.
|
||||
* Added trace message for mods with no update keys.
|
||||
* Adjusted reflection API to match actual usage (e.g. renamed `GetPrivate*` to `Get*`), and deprecated previous methods.
|
||||
* Fixed `GraphicsEvents.OnPostRenderEvent` not being raised in some specialised cases.
|
||||
* Fixed reflection API error for properties missing a `get` and `set`.
|
||||
* Fixed issue where a mod could change the cursor position reported to other mods.
|
||||
* Updated compatibility list.
|
||||
|
||||
* For the [log parser][]:
|
||||
* Fixed broken favicon.
|
||||
|
||||
## 2.2
|
||||
* For players:
|
||||
* Fixed error when a mod loads custom assets on Linux/Mac.
|
||||
|
|
|
@ -97,8 +97,8 @@ namespace StardewModdingApi.Installer
|
|||
yield return GetInstallPath("StardewModdingAPI.pdb");
|
||||
|
||||
// obsolete
|
||||
yield return GetInstallPath("Mods/.cache"); // 1.3-1.4
|
||||
yield return GetInstallPath("Mods/TrainerMod"); // *–2.0 (renamed to ConsoleCommands)
|
||||
yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4
|
||||
yield return GetInstallPath(Path.Combine("Mods", "TrainerMod")); // *–2.0 (renamed to ConsoleCommands)
|
||||
yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8
|
||||
yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4
|
||||
if (modsDir.Exists)
|
||||
|
|
|
@ -41,10 +41,6 @@
|
|||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"Author": "SMAPI",
|
||||
"Version": {
|
||||
"MajorVersion": 2,
|
||||
"MinorVersion": 0,
|
||||
"MinorVersion": 3,
|
||||
"PatchVersion": 0,
|
||||
"Build": null
|
||||
},
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using StardewModdingAPI.Web.Framework.Clients.GitHub;
|
||||
using StardewModdingAPI.Web.ViewModels;
|
||||
|
||||
namespace StardewModdingAPI.Web.Controllers
|
||||
{
|
||||
/// <summary>Provides an info/download page about SMAPI.</summary>
|
||||
[Route("")]
|
||||
[Route("install")]
|
||||
internal class IndexController : Controller
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The cache in which to store release data.</summary>
|
||||
private readonly IMemoryCache Cache;
|
||||
|
||||
/// <summary>The GitHub API client.</summary>
|
||||
private readonly IGitHubClient GitHub;
|
||||
|
||||
/// <summary>The cache time for release info.</summary>
|
||||
private readonly TimeSpan CacheTime = TimeSpan.FromMinutes(5);
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="cache">The cache in which to store release data.</param>
|
||||
/// <param name="github">The GitHub API client.</param>
|
||||
public IndexController(IMemoryCache cache, IGitHubClient github)
|
||||
{
|
||||
this.Cache = cache;
|
||||
this.GitHub = github;
|
||||
}
|
||||
|
||||
/// <summary>Display the index page.</summary>
|
||||
[HttpGet]
|
||||
public async Task<ViewResult> Index()
|
||||
{
|
||||
// fetch latest SMAPI release
|
||||
GitRelease release = await this.Cache.GetOrCreateAsync("latest-smapi-release", async entry =>
|
||||
{
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime);
|
||||
return await this.GitHub.GetLatestReleaseAsync("Pathoschild/SMAPI");
|
||||
});
|
||||
string downloadUrl = this.GetMainDownloadUrl(release);
|
||||
string devDownloadUrl = this.GetDevDownloadUrl(release);
|
||||
|
||||
// render view
|
||||
var model = new IndexModel(release.Name, release.Body, downloadUrl, devDownloadUrl);
|
||||
return this.View(model);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Get the main download URL for a SMAPI release.</summary>
|
||||
/// <param name="release">The SMAPI release.</param>
|
||||
private string GetMainDownloadUrl(GitRelease release)
|
||||
{
|
||||
// get main download URL
|
||||
foreach (GitAsset asset in release.Assets ?? new GitAsset[0])
|
||||
{
|
||||
if (Regex.IsMatch(asset.FileName, @"SMAPI-[\d\.]+-installer.zip"))
|
||||
return asset.DownloadUrl;
|
||||
}
|
||||
|
||||
// fallback just in case
|
||||
return "https://github.com/pathoschild/SMAPI/releases";
|
||||
}
|
||||
|
||||
/// <summary>Get the for-developers download URL for a SMAPI release.</summary>
|
||||
/// <param name="release">The SMAPI release.</param>
|
||||
private string GetDevDownloadUrl(GitRelease release)
|
||||
{
|
||||
// get dev download URL
|
||||
foreach (GitAsset asset in release.Assets ?? new GitAsset[0])
|
||||
{
|
||||
if (Regex.IsMatch(asset.FileName, @"SMAPI-[\d\.]+-installer-for-developers.zip"))
|
||||
return asset.DownloadUrl;
|
||||
}
|
||||
|
||||
// fallback just in case
|
||||
return "https://github.com/pathoschild/SMAPI/releases";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,8 +6,8 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StardewModdingAPI.Web.Framework;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Pastebin;
|
||||
using StardewModdingAPI.Web.Framework.ConfigModels;
|
||||
using StardewModdingAPI.Web.Framework.LogParser;
|
||||
using StardewModdingAPI.Web.ViewModels;
|
||||
|
||||
namespace StardewModdingAPI.Web.Controllers
|
||||
|
@ -19,10 +19,10 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
** Properties
|
||||
*********/
|
||||
/// <summary>The log parser config settings.</summary>
|
||||
private readonly LogParserConfig Config;
|
||||
private readonly ContextConfig Config;
|
||||
|
||||
/// <summary>The underlying Pastebin client.</summary>
|
||||
private readonly PastebinClient PastebinClient;
|
||||
private readonly IPastebinClient Pastebin;
|
||||
|
||||
/// <summary>The first bytes in a valid zip file.</summary>
|
||||
/// <remarks>See <a href="https://en.wikipedia.org/wiki/Zip_(file_format)#File_headers"/>.</remarks>
|
||||
|
@ -36,14 +36,12 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
** Constructor
|
||||
***/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="configProvider">The log parser config settings.</param>
|
||||
public LogParserController(IOptions<LogParserConfig> configProvider)
|
||||
/// <param name="contextProvider">The context config settings.</param>
|
||||
/// <param name="pastebin">The Pastebin API client.</param>
|
||||
public LogParserController(IOptions<ContextConfig> contextProvider, IPastebinClient pastebin)
|
||||
{
|
||||
// init Pastebin client
|
||||
this.Config = configProvider.Value;
|
||||
string version = this.GetType().Assembly.GetName().Version.ToString(3);
|
||||
string userAgent = string.Format(this.Config.PastebinUserAgent, version);
|
||||
this.PastebinClient = new PastebinClient(this.Config.PastebinBaseUrl, userAgent, this.Config.PastebinUserKey, this.Config.PastebinDevKey);
|
||||
this.Config = contextProvider.Value;
|
||||
this.Pastebin = pastebin;
|
||||
}
|
||||
|
||||
/***
|
||||
|
@ -52,12 +50,11 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <summary>Render the log parser UI.</summary>
|
||||
/// <param name="id">The paste ID.</param>
|
||||
[HttpGet]
|
||||
[Route("")]
|
||||
[Route("log")]
|
||||
[Route("log/{id}")]
|
||||
public ViewResult Index(string id = null)
|
||||
{
|
||||
return this.View("Index", new LogParserModel(this.Config.SectionUrl, id));
|
||||
return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id));
|
||||
}
|
||||
|
||||
/***
|
||||
|
@ -67,9 +64,9 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <param name="id">The Pastebin paste ID.</param>
|
||||
[HttpGet, Produces("application/json")]
|
||||
[Route("log/fetch/{id}")]
|
||||
public async Task<GetPasteResponse> GetAsync(string id)
|
||||
public async Task<PasteInfo> GetAsync(string id)
|
||||
{
|
||||
GetPasteResponse response = await this.PastebinClient.GetAsync(id);
|
||||
PasteInfo response = await this.Pastebin.GetAsync(id);
|
||||
response.Content = this.DecompressString(response.Content);
|
||||
return response;
|
||||
}
|
||||
|
@ -78,10 +75,10 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <param name="content">The log content to save.</param>
|
||||
[HttpPost, Produces("application/json"), AllowLargePosts]
|
||||
[Route("log/save")]
|
||||
public async Task<SavePasteResponse> PostAsync([FromBody] string content)
|
||||
public async Task<SavePasteResult> PostAsync([FromBody] string content)
|
||||
{
|
||||
content = this.CompressString(content);
|
||||
return await this.PastebinClient.PostAsync(content);
|
||||
return await this.Pastebin.PostAsync(content);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -7,6 +7,9 @@ using Microsoft.AspNetCore.Mvc;
|
|||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StardewModdingAPI.Common.Models;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
|
||||
using StardewModdingAPI.Web.Framework.Clients.GitHub;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Nexus;
|
||||
using StardewModdingAPI.Web.Framework.ConfigModels;
|
||||
using StardewModdingAPI.Web.Framework.ModRepositories;
|
||||
|
||||
|
@ -39,39 +42,22 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="cache">The cache in which to store mod metadata.</param>
|
||||
/// <param name="configProvider">The config settings for mod update checks.</param>
|
||||
public ModsApiController(IMemoryCache cache, IOptions<ModUpdateCheckConfig> configProvider)
|
||||
/// <param name="chucklefish">The Chucklefish API client.</param>
|
||||
/// <param name="github">The GitHub API client.</param>
|
||||
/// <param name="nexus">The Nexus API client.</param>
|
||||
public ModsApiController(IMemoryCache cache, IOptions<ModUpdateCheckConfig> configProvider, IChucklefishClient chucklefish, IGitHubClient github, INexusClient nexus)
|
||||
{
|
||||
ModUpdateCheckConfig config = configProvider.Value;
|
||||
|
||||
this.Cache = cache;
|
||||
this.CacheMinutes = config.CacheMinutes;
|
||||
this.VersionRegex = config.SemanticVersionRegex;
|
||||
|
||||
string version = this.GetType().Assembly.GetName().Version.ToString(3);
|
||||
this.Repositories =
|
||||
new IModRepository[]
|
||||
{
|
||||
new ChucklefishRepository(
|
||||
vendorKey: config.ChucklefishKey,
|
||||
userAgent: string.Format(config.ChucklefishUserAgent, version),
|
||||
baseUrl: config.ChucklefishBaseUrl,
|
||||
modPageUrlFormat: config.ChucklefishModPageUrlFormat
|
||||
),
|
||||
new GitHubRepository(
|
||||
vendorKey: config.GitHubKey,
|
||||
baseUrl: config.GitHubBaseUrl,
|
||||
releaseUrlFormat: config.GitHubReleaseUrlFormat,
|
||||
userAgent: string.Format(config.GitHubUserAgent, version),
|
||||
acceptHeader: config.GitHubAcceptHeader,
|
||||
username: config.GitHubUsername,
|
||||
password: config.GitHubPassword
|
||||
),
|
||||
new NexusRepository(
|
||||
vendorKey: config.NexusKey,
|
||||
userAgent: config.NexusUserAgent,
|
||||
baseUrl: config.NexusBaseUrl,
|
||||
modUrlFormat: config.NexusModUrlFormat
|
||||
)
|
||||
new ChucklefishRepository(config.ChucklefishKey, chucklefish),
|
||||
new GitHubRepository(config.GitHubKey, github),
|
||||
new NexusRepository(config.NexusKey, nexus)
|
||||
}
|
||||
.ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using HtmlAgilityPack;
|
||||
using Pathoschild.Http.Client;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
|
||||
{
|
||||
/// <summary>An HTTP client for fetching mod metadata from the Chucklefish mod site.</summary>
|
||||
internal class ChucklefishClient : IChucklefishClient
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The base URL for the Chucklefish mod site.</summary>
|
||||
private readonly string BaseUrl;
|
||||
|
||||
/// <summary>The URL for a mod page excluding the base URL, where {0} is the mod ID.</summary>
|
||||
private readonly string ModPageUrlFormat;
|
||||
|
||||
/// <summary>The underlying HTTP client.</summary>
|
||||
private readonly IClient Client;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="userAgent">The user agent for the API client.</param>
|
||||
/// <param name="baseUrl">The base URL for the Chucklefish mod site.</param>
|
||||
/// <param name="modPageUrlFormat">The URL for a mod page excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
|
||||
public ChucklefishClient(string userAgent, string baseUrl, string modPageUrlFormat)
|
||||
{
|
||||
this.BaseUrl = baseUrl;
|
||||
this.ModPageUrlFormat = modPageUrlFormat;
|
||||
this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
|
||||
}
|
||||
|
||||
/// <summary>Get metadata about a mod.</summary>
|
||||
/// <param name="id">The Chucklefish mod ID.</param>
|
||||
/// <returns>Returns the mod info if found, else <c>null</c>.</returns>
|
||||
public async Task<ChucklefishMod> GetModAsync(uint id)
|
||||
{
|
||||
// fetch HTML
|
||||
string html;
|
||||
try
|
||||
{
|
||||
html = await this.Client
|
||||
.GetAsync(string.Format(this.ModPageUrlFormat, id))
|
||||
.AsString();
|
||||
}
|
||||
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// parse HTML
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(html);
|
||||
|
||||
// extract mod info
|
||||
string url = new UriBuilder(new Uri(this.BaseUrl)) { Path = string.Format(this.ModPageUrlFormat, id) }.Uri.ToString();
|
||||
string name = doc.DocumentNode.SelectSingleNode("//meta[@name='twitter:title']").Attributes["content"].Value;
|
||||
if (name.StartsWith("[SMAPI] "))
|
||||
name = name.Substring("[SMAPI] ".Length);
|
||||
string version = doc.DocumentNode.SelectSingleNode("//h1/span").InnerText;
|
||||
|
||||
// create model
|
||||
return new ChucklefishMod
|
||||
{
|
||||
Name = name,
|
||||
Version = version,
|
||||
Url = url
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this.Client?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
|
||||
{
|
||||
/// <summary>Mod metadata from the Chucklefish mod site.</summary>
|
||||
internal class ChucklefishMod
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The mod name.</summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>The mod's semantic version number.</summary>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>The mod's web URL.</summary>
|
||||
public string Url { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
|
||||
{
|
||||
/// <summary>An HTTP client for fetching mod metadata from the Chucklefish mod site.</summary>
|
||||
internal interface IChucklefishClient : IDisposable
|
||||
{
|
||||
/*********
|
||||
** Methods
|
||||
*********/
|
||||
/// <summary>Get metadata about a mod.</summary>
|
||||
/// <param name="id">The Chucklefish mod ID.</param>
|
||||
/// <returns>Returns the mod info if found, else <c>null</c>.</returns>
|
||||
Task<ChucklefishMod> GetModAsync(uint id);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.GitHub
|
||||
{
|
||||
/// <summary>A GitHub download attached to a release.</summary>
|
||||
internal class GitAsset
|
||||
{
|
||||
/// <summary>The file name.</summary>
|
||||
[JsonProperty("name")]
|
||||
public string FileName { get; set; }
|
||||
|
||||
/// <summary>The file content type.</summary>
|
||||
[JsonProperty("content_type")]
|
||||
public string ContentType { get; set; }
|
||||
|
||||
/// <summary>The download URL.</summary>
|
||||
[JsonProperty("browser_download_url")]
|
||||
public string DownloadUrl { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Pathoschild.Http.Client;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.GitHub
|
||||
{
|
||||
/// <summary>An HTTP client for fetching metadata from GitHub.</summary>
|
||||
internal class GitHubClient : IGitHubClient
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The URL for a GitHub releases API query excluding the base URL, where {0} is the repository owner and name.</summary>
|
||||
private readonly string ReleaseUrlFormat;
|
||||
|
||||
/// <summary>The underlying HTTP client.</summary>
|
||||
private readonly IClient Client;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="baseUrl">The base URL for the GitHub API.</param>
|
||||
/// <param name="releaseUrlFormat">The URL for a GitHub releases API query excluding the <paramref name="baseUrl"/>, where {0} is the repository owner and name.</param>
|
||||
/// <param name="userAgent">The user agent for the API client.</param>
|
||||
/// <param name="acceptHeader">The Accept header value expected by the GitHub API.</param>
|
||||
/// <param name="username">The username with which to authenticate to the GitHub API.</param>
|
||||
/// <param name="password">The password with which to authenticate to the GitHub API.</param>
|
||||
public GitHubClient(string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader, string username, string password)
|
||||
{
|
||||
this.ReleaseUrlFormat = releaseUrlFormat;
|
||||
|
||||
this.Client = new FluentClient(baseUrl)
|
||||
.SetUserAgent(userAgent)
|
||||
.AddDefault(req => req.WithHeader("Accept", acceptHeader));
|
||||
if (!string.IsNullOrWhiteSpace(username))
|
||||
this.Client = this.Client.SetBasicAuthentication(username, password);
|
||||
}
|
||||
|
||||
/// <summary>Get the latest release for a GitHub repository.</summary>
|
||||
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
|
||||
/// <returns>Returns the latest release if found, else <c>null</c>.</returns>
|
||||
public async Task<GitRelease> GetLatestReleaseAsync(string repo)
|
||||
{
|
||||
// validate key format
|
||||
if (!repo.Contains("/") || repo.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != repo.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase))
|
||||
throw new ArgumentException($"The value '{repo}' isn't a valid GitHub repository key, must be a username and project name like 'Pathoschild/SMAPI'.", nameof(repo));
|
||||
|
||||
// fetch info
|
||||
try
|
||||
{
|
||||
return await this.Client
|
||||
.GetAsync(string.Format(this.ReleaseUrlFormat, repo))
|
||||
.As<GitRelease>();
|
||||
}
|
||||
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this.Client?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.GitHub
|
||||
{
|
||||
/// <summary>A GitHub project release.</summary>
|
||||
internal class GitRelease
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The display name.</summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>The semantic version string.</summary>
|
||||
[JsonProperty("tag_name")]
|
||||
public string Tag { get; set; }
|
||||
|
||||
/// <summary>The Markdown description for the release.</summary>
|
||||
public string Body { get; set; }
|
||||
|
||||
/// <summary>The attached files.</summary>
|
||||
public GitAsset[] Assets { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.GitHub
|
||||
{
|
||||
/// <summary>An HTTP client for fetching metadata from GitHub.</summary>
|
||||
internal interface IGitHubClient : IDisposable
|
||||
{
|
||||
/*********
|
||||
** Methods
|
||||
*********/
|
||||
/// <summary>Get the latest release for a GitHub repository.</summary>
|
||||
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
|
||||
/// <returns>Returns the latest release if found, else <c>null</c>.</returns>
|
||||
Task<GitRelease> GetLatestReleaseAsync(string repo);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
||||
{
|
||||
/// <summary>An HTTP client for fetching mod metadata from Nexus Mods.</summary>
|
||||
internal interface INexusClient : IDisposable
|
||||
{
|
||||
/*********
|
||||
** Methods
|
||||
*********/
|
||||
/// <summary>Get metadata about a mod.</summary>
|
||||
/// <param name="id">The Nexus mod ID.</param>
|
||||
/// <returns>Returns the mod info if found, else <c>null</c>.</returns>
|
||||
Task<NexusMod> GetModAsync(uint id);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
using System.Threading.Tasks;
|
||||
using Pathoschild.Http.Client;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
||||
{
|
||||
/// <summary>An HTTP client for fetching mod metadata from Nexus Mods.</summary>
|
||||
internal class NexusClient : INexusClient
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID.</summary>
|
||||
private readonly string ModUrlFormat;
|
||||
|
||||
/// <summary>The underlying HTTP client.</summary>
|
||||
private readonly IClient Client;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="userAgent">The user agent for the Nexus Mods API client.</param>
|
||||
/// <param name="baseUrl">The base URL for the Nexus Mods API.</param>
|
||||
/// <param name="modUrlFormat">The URL for a Nexus Mods API query excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
|
||||
public NexusClient(string userAgent, string baseUrl, string modUrlFormat)
|
||||
{
|
||||
this.ModUrlFormat = modUrlFormat;
|
||||
this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
|
||||
}
|
||||
|
||||
/// <summary>Get metadata about a mod.</summary>
|
||||
/// <param name="id">The Nexus mod ID.</param>
|
||||
/// <returns>Returns the mod info if found, else <c>null</c>.</returns>
|
||||
public async Task<NexusMod> GetModAsync(uint id)
|
||||
{
|
||||
return await this.Client
|
||||
.GetAsync(string.Format(this.ModUrlFormat, id))
|
||||
.As<NexusMod>();
|
||||
}
|
||||
|
||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this.Client?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.Nexus
|
||||
{
|
||||
/// <summary>Mod metadata from Nexus Mods.</summary>
|
||||
internal class NexusMod
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The mod name.</summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>The mod's semantic version number.</summary>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>The mod's web URL.</summary>
|
||||
[JsonProperty("mod_page_uri")]
|
||||
public string Url { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
|
||||
{
|
||||
/// <summary>An API client for Pastebin.</summary>
|
||||
internal interface IPastebinClient : IDisposable
|
||||
{
|
||||
/// <summary>Fetch a saved paste.</summary>
|
||||
/// <param name="id">The paste ID.</param>
|
||||
Task<PasteInfo> GetAsync(string id);
|
||||
|
||||
/// <summary>Save a paste to Pastebin.</summary>
|
||||
/// <param name="content">The paste content.</param>
|
||||
Task<SavePasteResult> PostAsync(string content);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
namespace StardewModdingAPI.Web.Framework.LogParser
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
|
||||
{
|
||||
/// <summary>The response for a get-paste request.</summary>
|
||||
internal class GetPasteResponse
|
||||
internal class PasteInfo
|
||||
{
|
||||
/// <summary>Whether the log was successfully fetched.</summary>
|
||||
public bool Success { get; set; }
|
|
@ -8,10 +8,10 @@ using System.Threading.Tasks;
|
|||
using System.Web;
|
||||
using Pathoschild.Http.Client;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.LogParser
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
|
||||
{
|
||||
/// <summary>An API client for Pastebin.</summary>
|
||||
internal class PastebinClient : IDisposable
|
||||
internal class PastebinClient : IPastebinClient
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
|
@ -43,7 +43,7 @@ namespace StardewModdingAPI.Web.Framework.LogParser
|
|||
|
||||
/// <summary>Fetch a saved paste.</summary>
|
||||
/// <param name="id">The paste ID.</param>
|
||||
public async Task<GetPasteResponse> GetAsync(string id)
|
||||
public async Task<PasteInfo> GetAsync(string id)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -54,30 +54,30 @@ namespace StardewModdingAPI.Web.Framework.LogParser
|
|||
|
||||
// handle Pastebin errors
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
return new GetPasteResponse { Error = "Received an empty response from Pastebin." };
|
||||
return new PasteInfo { Error = "Received an empty response from Pastebin." };
|
||||
if (content.StartsWith("<!DOCTYPE"))
|
||||
return new GetPasteResponse { Error = $"Received a captcha challenge from Pastebin. Please visit https://pastebin.com/{id} in a new window to solve it." };
|
||||
return new GetPasteResponse { Success = true, Content = content };
|
||||
return new PasteInfo { Error = $"Received a captcha challenge from Pastebin. Please visit https://pastebin.com/{id} in a new window to solve it." };
|
||||
return new PasteInfo { Success = true, Content = content };
|
||||
}
|
||||
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
|
||||
{
|
||||
return new GetPasteResponse { Error = "There's no log with that ID." };
|
||||
return new PasteInfo { Error = "There's no log with that ID." };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new GetPasteResponse { Error = ex.ToString() };
|
||||
return new PasteInfo { Error = ex.ToString() };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Save a paste to Pastebin.</summary>
|
||||
/// <param name="content">The paste content.</param>
|
||||
public async Task<SavePasteResponse> PostAsync(string content)
|
||||
public async Task<SavePasteResult> PostAsync(string content)
|
||||
{
|
||||
try
|
||||
{
|
||||
// validate
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
return new SavePasteResponse { Error = "The log content can't be empty." };
|
||||
return new SavePasteResult { Error = "The log content can't be empty." };
|
||||
|
||||
// post to API
|
||||
string response = await this.Client
|
||||
|
@ -96,19 +96,19 @@ namespace StardewModdingAPI.Web.Framework.LogParser
|
|||
|
||||
// handle Pastebin errors
|
||||
if (string.IsNullOrWhiteSpace(response))
|
||||
return new SavePasteResponse { Error = "Received an empty response from Pastebin." };
|
||||
return new SavePasteResult { Error = "Received an empty response from Pastebin." };
|
||||
if (response.StartsWith("Bad API request"))
|
||||
return new SavePasteResponse { Error = response };
|
||||
return new SavePasteResult { Error = response };
|
||||
if (!response.Contains("/"))
|
||||
return new SavePasteResponse { Error = $"Received an unknown response: {response}" };
|
||||
return new SavePasteResult { Error = $"Received an unknown response: {response}" };
|
||||
|
||||
// return paste ID
|
||||
string pastebinID = response.Split("/").Last();
|
||||
return new SavePasteResponse { Success = true, ID = pastebinID };
|
||||
return new SavePasteResult { Success = true, ID = pastebinID };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new SavePasteResponse { Success = false, Error = ex.ToString() };
|
||||
return new SavePasteResult { Success = false, Error = ex.ToString() };
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
namespace StardewModdingAPI.Web.Framework.LogParser
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
|
||||
{
|
||||
/// <summary>The response for a save-log request.</summary>
|
||||
internal class SavePasteResponse
|
||||
internal class SavePasteResult
|
||||
{
|
||||
/// <summary>Whether the log was successfully saved.</summary>
|
||||
public bool Success { get; set; }
|
|
@ -0,0 +1,72 @@
|
|||
namespace StardewModdingAPI.Web.Framework.ConfigModels
|
||||
{
|
||||
/// <summary>The config settings for the API clients.</summary>
|
||||
internal class ApiClientsConfig
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/****
|
||||
** Generic
|
||||
****/
|
||||
/// <summary>The user agent for API clients, where {0} is the SMAPI version.</summary>
|
||||
public string UserAgent { get; set; }
|
||||
|
||||
|
||||
/****
|
||||
** Chucklefish
|
||||
****/
|
||||
/// <summary>The base URL for the Chucklefish mod site.</summary>
|
||||
public string ChucklefishBaseUrl { get; set; }
|
||||
|
||||
/// <summary>The URL for a mod page on the Chucklefish mod site excluding the <see cref="GitHubBaseUrl"/>, where {0} is the mod ID.</summary>
|
||||
public string ChucklefishModPageUrlFormat { get; set; }
|
||||
|
||||
|
||||
/****
|
||||
** GitHub
|
||||
****/
|
||||
/// <summary>The base URL for the GitHub API.</summary>
|
||||
public string GitHubBaseUrl { get; set; }
|
||||
|
||||
/// <summary>The URL for a GitHub API latest-release query excluding the <see cref="GitHubBaseUrl"/>, where {0} is the organisation and project name.</summary>
|
||||
public string GitHubReleaseUrlFormat { get; set; }
|
||||
|
||||
/// <summary>The Accept header value expected by the GitHub API.</summary>
|
||||
public string GitHubAcceptHeader { get; set; }
|
||||
|
||||
/// <summary>The username with which to authenticate to the GitHub API (if any).</summary>
|
||||
public string GitHubUsername { get; set; }
|
||||
|
||||
/// <summary>The password with which to authenticate to the GitHub API (if any).</summary>
|
||||
public string GitHubPassword { get; set; }
|
||||
|
||||
/****
|
||||
** Nexus Mods
|
||||
****/
|
||||
/// <summary>The user agent for the Nexus Mods API client.</summary>
|
||||
public string NexusUserAgent { get; set; }
|
||||
|
||||
/// <summary>The base URL for the Nexus Mods API.</summary>
|
||||
public string NexusBaseUrl { get; set; }
|
||||
|
||||
/// <summary>The URL for a Nexus Mods API query excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary>
|
||||
public string NexusModUrlFormat { get; set; }
|
||||
|
||||
/****
|
||||
** Pastebin
|
||||
****/
|
||||
/// <summary>The base URL for the Pastebin API.</summary>
|
||||
public string PastebinBaseUrl { get; set; }
|
||||
|
||||
/// <summary>The user agent for the Pastebin API client, where {0} is the SMAPI version.</summary>
|
||||
public string PastebinUserAgent { get; set; }
|
||||
|
||||
/// <summary>The user key used to authenticate with the Pastebin API.</summary>
|
||||
public string PastebinUserKey { get; set; }
|
||||
|
||||
/// <summary>The developer key used to authenticate with the Pastebin API.</summary>
|
||||
public string PastebinDevKey { get; set; }
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
namespace StardewModdingAPI.Web.Framework.ConfigModels
|
||||
{
|
||||
/// <summary>The config settings for the app context.</summary>
|
||||
public class ContextConfig // must be public to pass into views
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The root URL for the app.</summary>
|
||||
public string RootUrl { get; set; }
|
||||
|
||||
/// <summary>The root URL for the log parser.</summary>
|
||||
public string LogParserUrl { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
namespace StardewModdingAPI.Web.Framework.ConfigModels
|
||||
{
|
||||
/// <summary>The config settings for the log parser.</summary>
|
||||
internal class LogParserConfig
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The root URL for the log parser controller.</summary>
|
||||
public string SectionUrl { get; set; }
|
||||
|
||||
/// <summary>The base URL for the Pastebin API.</summary>
|
||||
public string PastebinBaseUrl { get; set; }
|
||||
|
||||
/// <summary>The user agent for the Pastebin API client, where {0} is the SMAPI version.</summary>
|
||||
public string PastebinUserAgent { get; set; }
|
||||
|
||||
/// <summary>The user key used to authenticate with the Pastebin API.</summary>
|
||||
public string PastebinUserKey { get; set; }
|
||||
|
||||
/// <summary>The developer key used to authenticate with the Pastebin API.</summary>
|
||||
public string PastebinDevKey { get; set; }
|
||||
}
|
||||
}
|
|
@ -6,9 +6,6 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
|
|||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/****
|
||||
** General
|
||||
****/
|
||||
/// <summary>The number of minutes update checks should be cached before refetching them.</summary>
|
||||
public int CacheMinutes { get; set; }
|
||||
|
||||
|
@ -16,59 +13,13 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
|
|||
/// <remarks>Derived from SMAPI's SemanticVersion implementation.</remarks>
|
||||
public string SemanticVersionRegex { get; set; }
|
||||
|
||||
/****
|
||||
** Chucklefish mod site
|
||||
****/
|
||||
/// <summary>The repository key for the Chucklefish mod site.</summary>
|
||||
public string ChucklefishKey { get; set; }
|
||||
|
||||
/// <summary>The user agent for the Chucklefish API client, where {0} is the SMAPI version.</summary>
|
||||
public string ChucklefishUserAgent { get; set; }
|
||||
|
||||
/// <summary>The base URL for the Chucklefish mod site.</summary>
|
||||
public string ChucklefishBaseUrl { get; set; }
|
||||
|
||||
/// <summary>The URL for a mod page on the Chucklefish mod site excluding the <see cref="GitHubBaseUrl"/>, where {0} is the mod ID.</summary>
|
||||
public string ChucklefishModPageUrlFormat { get; set; }
|
||||
|
||||
|
||||
/****
|
||||
** GitHub
|
||||
****/
|
||||
/// <summary>The repository key for Nexus Mods.</summary>
|
||||
public string GitHubKey { get; set; }
|
||||
|
||||
/// <summary>The user agent for the GitHub API client, where {0} is the SMAPI version.</summary>
|
||||
public string GitHubUserAgent { get; set; }
|
||||
|
||||
/// <summary>The base URL for the GitHub API.</summary>
|
||||
public string GitHubBaseUrl { get; set; }
|
||||
|
||||
/// <summary>The URL for a GitHub API latest-release query excluding the <see cref="GitHubBaseUrl"/>, where {0} is the organisation and project name.</summary>
|
||||
public string GitHubReleaseUrlFormat { get; set; }
|
||||
|
||||
/// <summary>The Accept header value expected by the GitHub API.</summary>
|
||||
public string GitHubAcceptHeader { get; set; }
|
||||
|
||||
/// <summary>The username with which to authenticate to the GitHub API (if any).</summary>
|
||||
public string GitHubUsername { get; set; }
|
||||
|
||||
/// <summary>The password with which to authenticate to the GitHub API (if any).</summary>
|
||||
public string GitHubPassword { get; set; }
|
||||
|
||||
/****
|
||||
** Nexus Mods
|
||||
****/
|
||||
/// <summary>The repository key for Nexus Mods.</summary>
|
||||
public string NexusKey { get; set; }
|
||||
|
||||
/// <summary>The user agent for the Nexus Mods API client.</summary>
|
||||
public string NexusUserAgent { get; set; }
|
||||
|
||||
/// <summary>The base URL for the Nexus Mods API.</summary>
|
||||
public string NexusBaseUrl { get; set; }
|
||||
|
||||
/// <summary>The URL for a Nexus Mods API query excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary>
|
||||
public string NexusModUrlFormat { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using HtmlAgilityPack;
|
||||
using Pathoschild.Http.Client;
|
||||
using StardewModdingAPI.Common.Models;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
{
|
||||
|
@ -13,14 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The base URL for the Chucklefish mod site.</summary>
|
||||
private readonly string BaseUrl;
|
||||
|
||||
/// <summary>The URL for a mod page excluding the base URL, where {0} is the mod ID.</summary>
|
||||
private readonly string ModPageUrlFormat;
|
||||
|
||||
/// <summary>The underlying HTTP client.</summary>
|
||||
private readonly IClient Client;
|
||||
private readonly IChucklefishClient Client;
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -28,15 +20,11 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="vendorKey">The unique key for this vendor.</param>
|
||||
/// <param name="userAgent">The user agent for the API client.</param>
|
||||
/// <param name="baseUrl">The base URL for the Chucklefish mod site.</param>
|
||||
/// <param name="modPageUrlFormat">The URL for a mod page excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
|
||||
public ChucklefishRepository(string vendorKey, string userAgent, string baseUrl, string modPageUrlFormat)
|
||||
/// <param name="client">The underlying HTTP client.</param>
|
||||
public ChucklefishRepository(string vendorKey, IChucklefishClient client)
|
||||
: base(vendorKey)
|
||||
{
|
||||
this.BaseUrl = baseUrl;
|
||||
this.ModPageUrlFormat = modPageUrlFormat;
|
||||
this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
|
||||
this.Client = client;
|
||||
}
|
||||
|
||||
/// <summary>Get metadata about a mod in the repository.</summary>
|
||||
|
@ -44,38 +32,18 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
public override async Task<ModInfoModel> GetModInfoAsync(string id)
|
||||
{
|
||||
// validate ID format
|
||||
if (!uint.TryParse(id, out uint _))
|
||||
if (!uint.TryParse(id, out uint realID))
|
||||
return new ModInfoModel($"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID.");
|
||||
|
||||
// fetch info
|
||||
try
|
||||
{
|
||||
// fetch HTML
|
||||
string html;
|
||||
try
|
||||
{
|
||||
html = await this.Client
|
||||
.GetAsync(string.Format(this.ModPageUrlFormat, id))
|
||||
.AsString();
|
||||
}
|
||||
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
|
||||
{
|
||||
var mod = await this.Client.GetModAsync(realID);
|
||||
if (mod == null)
|
||||
return new ModInfoModel("Found no mod with this ID.");
|
||||
}
|
||||
|
||||
// parse HTML
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(html);
|
||||
|
||||
// extract mod info
|
||||
string url = new UriBuilder(new Uri(this.BaseUrl)) { Path = string.Format(this.ModPageUrlFormat, id) }.Uri.ToString();
|
||||
string name = doc.DocumentNode.SelectSingleNode("//meta[@name='twitter:title']").Attributes["content"].Value;
|
||||
if (name.StartsWith("[SMAPI] "))
|
||||
name = name.Substring("[SMAPI] ".Length);
|
||||
string version = doc.DocumentNode.SelectSingleNode("//h1/span").InnerText;
|
||||
|
||||
// create model
|
||||
return new ModInfoModel(name, this.NormaliseVersion(version), url);
|
||||
return new ModInfoModel(mod.Name, this.NormaliseVersion(mod.Version), mod.Url);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Pathoschild.Http.Client;
|
||||
using StardewModdingAPI.Common.Models;
|
||||
using StardewModdingAPI.Web.Framework.Clients.GitHub;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
{
|
||||
|
@ -13,11 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID.</summary>
|
||||
private readonly string ReleaseUrlFormat;
|
||||
|
||||
/// <summary>The underlying HTTP client.</summary>
|
||||
private readonly IClient Client;
|
||||
/// <summary>The underlying GitHub API client.</summary>
|
||||
private readonly IGitHubClient Client;
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -25,22 +20,11 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="vendorKey">The unique key for this vendor.</param>
|
||||
/// <param name="baseUrl">The base URL for the Nexus Mods API.</param>
|
||||
/// <param name="releaseUrlFormat">The URL for a Nexus Mods API query excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
|
||||
/// <param name="userAgent">The user agent for the API client.</param>
|
||||
/// <param name="acceptHeader">The Accept header value expected by the GitHub API.</param>
|
||||
/// <param name="username">The username with which to authenticate to the GitHub API.</param>
|
||||
/// <param name="password">The password with which to authenticate to the GitHub API.</param>
|
||||
public GitHubRepository(string vendorKey, string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader, string username, string password)
|
||||
/// <param name="client">The underlying GitHub API client.</param>
|
||||
public GitHubRepository(string vendorKey, IGitHubClient client)
|
||||
: base(vendorKey)
|
||||
{
|
||||
this.ReleaseUrlFormat = releaseUrlFormat;
|
||||
|
||||
this.Client = new FluentClient(baseUrl)
|
||||
.SetUserAgent(userAgent)
|
||||
.AddDefault(req => req.WithHeader("Accept", acceptHeader));
|
||||
if (!string.IsNullOrWhiteSpace(username))
|
||||
this.Client = this.Client.SetBasicAuthentication(username, password);
|
||||
this.Client = client;
|
||||
}
|
||||
|
||||
/// <summary>Get metadata about a mod in the repository.</summary>
|
||||
|
@ -54,14 +38,10 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
// fetch info
|
||||
try
|
||||
{
|
||||
GitRelease release = await this.Client
|
||||
.GetAsync(string.Format(this.ReleaseUrlFormat, id))
|
||||
.As<GitRelease>();
|
||||
return new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases");
|
||||
}
|
||||
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
|
||||
{
|
||||
return new ModInfoModel("Found no mod with this ID.");
|
||||
GitRelease release = await this.Client.GetLatestReleaseAsync(id);
|
||||
return release != null
|
||||
? new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases")
|
||||
: new ModInfoModel("Found no mod with this ID.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -74,24 +54,5 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
{
|
||||
this.Client.Dispose();
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private models
|
||||
*********/
|
||||
/// <summary>Metadata about a GitHub release tag.</summary>
|
||||
private class GitRelease
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The display name.</summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>The semantic version string.</summary>
|
||||
[JsonProperty("tag_name")]
|
||||
public string Tag { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Pathoschild.Http.Client;
|
||||
using StardewModdingAPI.Common.Models;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Nexus;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.ModRepositories
|
||||
{
|
||||
|
@ -12,11 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID.</summary>
|
||||
private readonly string ModUrlFormat;
|
||||
|
||||
/// <summary>The underlying HTTP client.</summary>
|
||||
private readonly IClient Client;
|
||||
/// <summary>The underlying Nexus Mods API client.</summary>
|
||||
private readonly INexusClient Client;
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -24,14 +20,11 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="vendorKey">The unique key for this vendor.</param>
|
||||
/// <param name="userAgent">The user agent for the Nexus Mods API client.</param>
|
||||
/// <param name="baseUrl">The base URL for the Nexus Mods API.</param>
|
||||
/// <param name="modUrlFormat">The URL for a Nexus Mods API query excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
|
||||
public NexusRepository(string vendorKey, string userAgent, string baseUrl, string modUrlFormat)
|
||||
/// <param name="client">The underlying Nexus Mods API client.</param>
|
||||
public NexusRepository(string vendorKey, INexusClient client)
|
||||
: base(vendorKey)
|
||||
{
|
||||
this.ModUrlFormat = modUrlFormat;
|
||||
this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
|
||||
this.Client = client;
|
||||
}
|
||||
|
||||
/// <summary>Get metadata about a mod in the repository.</summary>
|
||||
|
@ -39,18 +32,15 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
public override async Task<ModInfoModel> GetModInfoAsync(string id)
|
||||
{
|
||||
// validate ID format
|
||||
if (!uint.TryParse(id, out uint _))
|
||||
if (!uint.TryParse(id, out uint nexusID))
|
||||
return new ModInfoModel($"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID.");
|
||||
|
||||
// fetch info
|
||||
try
|
||||
{
|
||||
NexusResponseModel response = await this.Client
|
||||
.GetAsync(string.Format(this.ModUrlFormat, id))
|
||||
.As<NexusResponseModel>();
|
||||
|
||||
return response != null
|
||||
? new ModInfoModel(response.Name, this.NormaliseVersion(response.Version), response.Url)
|
||||
NexusMod mod = await this.Client.GetModAsync(nexusID);
|
||||
return mod != null
|
||||
? new ModInfoModel(mod.Name, this.NormaliseVersion(mod.Version), mod.Url)
|
||||
: new ModInfoModel("Found no mod with this ID.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -64,26 +54,5 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
{
|
||||
this.Client.Dispose();
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private models
|
||||
*********/
|
||||
/// <summary>A mod metadata response from Nexus Mods.</summary>
|
||||
private class NexusResponseModel
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The mod name.</summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>The mod's semantic version number.</summary>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>The mod's web URL.</summary>
|
||||
[JsonProperty("mod_page_uri")]
|
||||
public string Url { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "log",
|
||||
"launchUrl": "",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.6.0" />
|
||||
<PackageReference Include="Markdig" Version="0.14.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.0.0" />
|
||||
|
|
|
@ -7,6 +7,10 @@ using Microsoft.Extensions.DependencyInjection;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using StardewModdingAPI.Web.Framework;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
|
||||
using StardewModdingAPI.Web.Framework.Clients.GitHub;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Nexus;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Pastebin;
|
||||
using StardewModdingAPI.Web.Framework.ConfigModels;
|
||||
using StardewModdingAPI.Web.Framework.RewriteRules;
|
||||
|
||||
|
@ -41,9 +45,10 @@ namespace StardewModdingAPI.Web
|
|||
/// <param name="services">The service injection container.</param>
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
// init configuration
|
||||
services
|
||||
.Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck"))
|
||||
.Configure<LogParserConfig>(this.Configuration.GetSection("LogParser"))
|
||||
.Configure<ContextConfig>(this.Configuration.GetSection("Context"))
|
||||
.Configure<RouteOptions>(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint)))
|
||||
.AddMemoryCache()
|
||||
.AddMvc()
|
||||
|
@ -53,6 +58,41 @@ namespace StardewModdingAPI.Web
|
|||
options.SerializerSettings.Formatting = Formatting.Indented;
|
||||
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
||||
});
|
||||
|
||||
// init API clients
|
||||
{
|
||||
ApiClientsConfig api = this.Configuration.GetSection("ApiClients").Get<ApiClientsConfig>();
|
||||
string version = this.GetType().Assembly.GetName().Version.ToString(3);
|
||||
string userAgent = string.Format(api.UserAgent, version);
|
||||
|
||||
services.AddSingleton<IChucklefishClient>(new ChucklefishClient(
|
||||
userAgent: userAgent,
|
||||
baseUrl: api.ChucklefishBaseUrl,
|
||||
modPageUrlFormat: api.ChucklefishModPageUrlFormat
|
||||
));
|
||||
|
||||
services.AddSingleton<IGitHubClient>(new GitHubClient(
|
||||
baseUrl: api.GitHubBaseUrl,
|
||||
releaseUrlFormat: api.GitHubReleaseUrlFormat,
|
||||
userAgent: userAgent,
|
||||
acceptHeader: api.GitHubAcceptHeader,
|
||||
username: api.GitHubUsername,
|
||||
password: api.GitHubPassword
|
||||
));
|
||||
|
||||
services.AddSingleton<INexusClient>(new NexusClient(
|
||||
userAgent: api.NexusUserAgent,
|
||||
baseUrl: api.NexusBaseUrl,
|
||||
modUrlFormat: api.NexusModUrlFormat
|
||||
));
|
||||
|
||||
services.AddSingleton<IPastebinClient>(new PastebinClient(
|
||||
baseUrl: api.PastebinBaseUrl,
|
||||
userAgent: userAgent,
|
||||
userKey: api.PastebinUserKey,
|
||||
devKey: api.PastebinDevKey
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The method called by the runtime to configure the HTTP request pipeline.</summary>
|
||||
|
@ -89,11 +129,11 @@ namespace StardewModdingAPI.Web
|
|||
req.Host.Host != "localhost"
|
||||
&& (req.Host.Host.StartsWith("api.") || req.Host.Host.StartsWith("log."))
|
||||
&& !req.Path.StartsWithSegments("/content")
|
||||
&& !req.Path.StartsWithSegments("/favicon.ico")
|
||||
))
|
||||
|
||||
// shortcut redirects
|
||||
.Add(new RedirectToUrlRule("^/docs$", "https://stardewvalleywiki.com/Modding:Index"))
|
||||
.Add(new RedirectToUrlRule("^/install$", "https://stardewvalleywiki.com/Modding:Installing_SMAPI"))
|
||||
)
|
||||
.UseStaticFiles() // wwwroot folder
|
||||
.UseMvc();
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
namespace StardewModdingAPI.Web.ViewModels
|
||||
{
|
||||
/// <summary>The view model for the index page.</summary>
|
||||
public class IndexModel
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The latest SMAPI version.</summary>
|
||||
public string LatestVersion { get; set; }
|
||||
|
||||
/// <summary>The Markdown description for the release.</summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>The main download URL.</summary>
|
||||
public string DownloadUrl { get; set; }
|
||||
|
||||
/// <summary>The for-developers download URL.</summary>
|
||||
public string DevDownloadUrl { get; set; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
public IndexModel() { }
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="latestVersion">The latest SMAPI version.</param>
|
||||
/// <param name="description">The Markdown description for the release.</param>
|
||||
/// <param name="downloadUrl">The main download URL.</param>
|
||||
/// <param name="devDownloadUrl">The for-developers download URL.</param>
|
||||
internal IndexModel(string latestVersion, string description, string downloadUrl, string devDownloadUrl)
|
||||
{
|
||||
this.LatestVersion = latestVersion;
|
||||
this.Description = description;
|
||||
this.DownloadUrl = downloadUrl;
|
||||
this.DevDownloadUrl = devDownloadUrl;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
@{
|
||||
ViewData["Title"] = "SMAPI";
|
||||
}
|
||||
@model StardewModdingAPI.Web.ViewModels.IndexModel
|
||||
@section Head {
|
||||
<link rel="stylesheet" href="~/Content/css/index.css" />
|
||||
}
|
||||
|
||||
<p id="blurb">
|
||||
The mod loader for Stardew Valley. It works fine with GOG and Steam achievements, it's
|
||||
compatible with Linux/Mac/Windows, you can uninstall it anytime, and there's a friendly
|
||||
community if you need help. It's a cool pufferchick.
|
||||
</p>
|
||||
|
||||
<div id="call-to-action">
|
||||
<a href="@Model.DownloadUrl" class="main-cta">Download SMAPI @Model.LatestVersion</a><br />
|
||||
<a href="https://stardewvalleywiki.com/Modding:Installing_SMAPI" class="secondary-cta">Install guide</a><br />
|
||||
<a href="https://stardewvalleywiki.com/Modding:Player_FAQs" class="secondary-cta">FAQs</a><br />
|
||||
<img src="favicon.ico" />
|
||||
</div>
|
||||
|
||||
<h2>Get help</h2>
|
||||
<ul>
|
||||
<li><a href="https://stardewvalleywiki.com/Modding:SMAPI_compatibility">Mod compatibility list</a></li>
|
||||
<li>Get help <a href="https://stardewvalleywiki.com/Modding:Community#Discord">on Discord</a> or <a href="https://community.playstarbound.com/threads/smapi-stardew-modding-api.108375/">in the forums</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>What's new in SMAPI @Model.LatestVersion?</h2>
|
||||
<div class="github-description">
|
||||
@Html.Raw(Markdig.Markdown.ToHtml(Model.Description))
|
||||
</div>
|
||||
|
||||
<p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="https://stardewvalleywiki.com/Modding:SMAPI_compatibility">mod compatibility list</a> for more info.</p>
|
||||
|
||||
<h2>Donate to support SMAPI ♥</h2>
|
||||
<p>
|
||||
SMAPI is an open-source project by Pathoschild. It will always be free, but donations
|
||||
are much appreciated to help pay for development, server hosting, domain fees, coffee, etc.
|
||||
</p>
|
||||
<ul id="support-links">
|
||||
<li><a href="https://www.paypal.me/pathoschild">Donate once</a></li>
|
||||
<li>
|
||||
<a href="https://www.patreon.com/pathoschild">Donate $1 per month (or more)</a><br />
|
||||
<small>
|
||||
You can cancel anytime. You'll have access to all private posts with behind-the-scenes
|
||||
info, upcoming features, and early previews of SMAPI updates. You can optionally
|
||||
provide early feedback on SMAPI features to influence development. Donate $5/month and
|
||||
you'll be publicly credited (with optional link) below!
|
||||
</small>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Special thanks to
|
||||
acerbicon,
|
||||
<a href="https://www.nexusmods.com/stardewvalley/users/31393530">ChefRude</a>,
|
||||
jwdred,
|
||||
OfficialPiAddict,
|
||||
Robby LaFarge,
|
||||
and a few anonymous users for their ongoing support; you're awesome! 🏅
|
||||
</p>
|
||||
|
||||
<h2>For mod creators</h2>
|
||||
<ul>
|
||||
<li><a href="@Model.DevDownloadUrl">SMAPI 2.2 for developers</a> (includes <a href="https://docs.microsoft.com/en-us/visualstudio/ide/using-intellisense">intellisense</a> and full console output)</li>
|
||||
<li><a href="https://stardewvalleywiki.com/Modding:Index">Modding documentation</a></li>
|
||||
<li>Need help? Come <a href="https://stardewvalleywiki.com/Modding:Community#Discord">chat on Discord</a>.</li>
|
||||
</ul>
|
|
@ -1,3 +1,7 @@
|
|||
@using Microsoft.Extensions.Options
|
||||
@using StardewModdingAPI.Web.Framework.ConfigModels
|
||||
@inject IOptions<ContextConfig> ContextConfig
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
@ -10,9 +14,9 @@
|
|||
<div id="sidebar">
|
||||
<h4>SMAPI</h4>
|
||||
<ul>
|
||||
<li><a href="https://stardewvalleywiki.com/Modding:Index">FAQs & guides</a></li>
|
||||
<li><a href="https://github.com/pathoschild/SMAPI/releases">Download SMAPI</a></li>
|
||||
<li><a href="https://discord.gg/stardewvalley">Get help on Discord</a></li>
|
||||
<li><a href="@ContextConfig.Value.RootUrl">About SMAPI</a></li>
|
||||
<li><a href="@ContextConfig.Value.LogParserUrl">Log parser</a></li>
|
||||
<li><a href="https://stardewvalleywiki.com/Modding:Index">Docs</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="content-column">
|
||||
|
|
|
@ -16,12 +16,14 @@
|
|||
"Microsoft": "Information"
|
||||
}
|
||||
},
|
||||
"ModUpdateCheck": {
|
||||
"GitHubUsername": null,
|
||||
"GitHubPassword": null
|
||||
"Context": {
|
||||
"RootUrl": "http://localhost:59482/",
|
||||
"LogParserUrl": "http://localhost:59482/log/"
|
||||
},
|
||||
"LogParser": {
|
||||
"SectionUrl": "http://localhost:59482/log/",
|
||||
"ApiClients": {
|
||||
"GitHubUsername": null,
|
||||
"GitHubPassword": null,
|
||||
|
||||
"PastebinUserKey": null,
|
||||
"PastebinDevKey": null
|
||||
}
|
||||
|
|
|
@ -13,33 +13,37 @@
|
|||
"Default": "Warning"
|
||||
}
|
||||
},
|
||||
"ModUpdateCheck": {
|
||||
"CacheMinutes": 60,
|
||||
"SemanticVersionRegex": "^(?>(?<major>0|[1-9]\\d*))\\.(?>(?<minor>0|[1-9]\\d*))(?>(?:\\.(?<patch>0|[1-9]\\d*))?)(?:-(?<prerelease>(?>[a-z0-9]+[\\-\\.]?)+))?$",
|
||||
"Context": {
|
||||
"RootUrl": null, // see top note
|
||||
"LogParserUrl": null // see top note
|
||||
},
|
||||
"ApiClients": {
|
||||
"UserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)",
|
||||
|
||||
"ChucklefishKey": "Chucklefish",
|
||||
"ChucklefishUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)",
|
||||
"ChucklefishBaseUrl": "https://community.playstarbound.com",
|
||||
"ChucklefishModPageUrlFormat": "resources/{0}",
|
||||
|
||||
"GitHubKey": "GitHub",
|
||||
"GitHubUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)",
|
||||
"GitHubBaseUrl": "https://api.github.com",
|
||||
"GitHubReleaseUrlFormat": "repos/{0}/releases/latest",
|
||||
"GitHubAcceptHeader": "application/vnd.github.v3+json",
|
||||
"GitHubUsername": null, // see top note
|
||||
"GitHubPassword": null, // see top note
|
||||
|
||||
"NexusKey": "Nexus",
|
||||
"NexusUserAgent": "Nexus Client v0.63.15",
|
||||
"NexusBaseUrl": "http://www.nexusmods.com/stardewvalley",
|
||||
"NexusModUrlFormat": "mods/{0}"
|
||||
},
|
||||
"LogParser": {
|
||||
"SectionUrl": null, // see top note
|
||||
"NexusModUrlFormat": "mods/{0}",
|
||||
|
||||
"PastebinBaseUrl": "https://pastebin.com/",
|
||||
"PastebinUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)",
|
||||
"PastebinUserKey": null, // see top note
|
||||
"PastebinDevKey": null // see top note
|
||||
},
|
||||
|
||||
"ModUpdateCheck": {
|
||||
"CacheMinutes": 60,
|
||||
"SemanticVersionRegex": "^(?>(?<major>0|[1-9]\\d*))\\.(?>(?<minor>0|[1-9]\\d*))(?>(?:\\.(?<patch>0|[1-9]\\d*))?)(?:-(?<prerelease>(?>[a-z0-9]+[\\-\\.]?)+))?$",
|
||||
|
||||
"ChucklefishKey": "Chucklefish",
|
||||
"GitHubKey": "GitHub",
|
||||
"NexusKey": "Nexus"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*********
|
||||
** Intro
|
||||
*********/
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 6em;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#blurb {
|
||||
margin: auto;
|
||||
width: 30em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#call-to-action {
|
||||
margin: 3em 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#call-to-action a {
|
||||
box-shadow: #caefab 0 1px 0 0 inset;
|
||||
background: linear-gradient(#77d42a 5%, #5cb811 100%) #77d42a;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #268a16;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
color: #306108;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1em;
|
||||
padding: 6px 24px;
|
||||
text-decoration: none;
|
||||
text-shadow: #aade7c 0 1px 0;
|
||||
}
|
||||
|
||||
#call-to-action a.secondary-cta {
|
||||
background: #768d87;
|
||||
border: 1px solid #566963;
|
||||
color: #ffffff;
|
||||
text-shadow: #2b665e 0 1px 0;
|
||||
}
|
||||
|
||||
/*********
|
||||
** Subsections
|
||||
*********/
|
||||
.github-description {
|
||||
border-left: 0.25em solid #dfe2e5;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.github-description .noinclude {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#support-links li small {
|
||||
display: block;
|
||||
width: 50em;
|
||||
}
|
|
@ -29,7 +29,7 @@ namespace StardewModdingAPI
|
|||
** Public
|
||||
****/
|
||||
/// <summary>SMAPI's current semantic version.</summary>
|
||||
public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.2");
|
||||
public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.3");
|
||||
|
||||
/// <summary>The minimum supported version of Stardew Valley.</summary>
|
||||
public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30");
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace StardewModdingAPI.Events
|
|||
public SButton Button { get; }
|
||||
|
||||
/// <summary>The current cursor position.</summary>
|
||||
public ICursorPosition Cursor { get; set; }
|
||||
public ICursorPosition Cursor { get; }
|
||||
|
||||
/// <summary>Whether the input is considered a 'click' by the game for enabling action.</summary>
|
||||
[Obsolete("Use " + nameof(EventArgsInput.IsActionButton) + " or " + nameof(EventArgsInput.IsUseToolButton) + " instead")] // deprecated in SMAPI 2.1
|
||||
|
@ -28,6 +28,9 @@ namespace StardewModdingAPI.Events
|
|||
/// <summary>Whether the input should use tools on the affected tile.</summary>
|
||||
public bool IsUseToolButton { get; }
|
||||
|
||||
/// <summary>Whether a mod has indicated the key was already handled.</summary>
|
||||
public bool IsSuppressed { get; private set; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
|
@ -55,6 +58,9 @@ namespace StardewModdingAPI.Events
|
|||
/// <param name="button">The button to suppress.</param>
|
||||
public void SuppressButton(SButton button)
|
||||
{
|
||||
if (button == this.Button)
|
||||
this.IsSuppressed = true;
|
||||
|
||||
// keyboard
|
||||
if (button.TryGetKeyboard(out Keys key))
|
||||
Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Union(new[] { key }).ToArray());
|
||||
|
|
|
@ -33,6 +33,9 @@ namespace StardewModdingAPI.Events
|
|||
/// <summary>Raised every 60th tick (≈once per second).</summary>
|
||||
public static event EventHandler OneSecondTick;
|
||||
|
||||
/// <summary>Raised once after the game initialises and all <see cref="IMod.Entry"/> methods have been called.</summary>
|
||||
public static event EventHandler FirstUpdateTick;
|
||||
|
||||
|
||||
/*********
|
||||
** Internal methods
|
||||
|
@ -92,5 +95,12 @@ namespace StardewModdingAPI.Events
|
|||
{
|
||||
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList());
|
||||
}
|
||||
|
||||
/// <summary>Raise a <see cref="FirstUpdateTick"/> event.</summary>
|
||||
/// <param name="monitor">Encapsulates monitoring and logging.</param>
|
||||
internal static void InvokeFirstUpdateTick(IMonitor monitor)
|
||||
{
|
||||
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", GameEvents.FirstUpdateTick?.GetInvocationList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,14 +57,14 @@ namespace StardewModdingAPI.Framework.Content
|
|||
public ContentCache(LocalizedContentManager contentManager, Reflector reflection, char[] possiblePathSeparators, string preferredPathSeparator)
|
||||
{
|
||||
// init
|
||||
this.Cache = reflection.GetPrivateField<Dictionary<string, object>>(contentManager, "loadedAssets").GetValue();
|
||||
this.Cache = reflection.GetField<Dictionary<string, object>>(contentManager, "loadedAssets").GetValue();
|
||||
this.PossiblePathSeparators = possiblePathSeparators;
|
||||
this.PreferredPathSeparator = preferredPathSeparator;
|
||||
|
||||
// get key normalisation logic
|
||||
if (Constants.TargetPlatform == Platform.Windows)
|
||||
{
|
||||
IPrivateMethod method = reflection.GetPrivateMethod(typeof(TitleContainer), "GetCleanPath");
|
||||
IReflectedMethod method = reflection.GetMethod(typeof(TitleContainer), "GetCleanPath");
|
||||
this.NormaliseAssetNameForPlatform = path => method.Invoke<string>(path);
|
||||
}
|
||||
else
|
||||
|
|
|
@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework
|
|||
/// <param name="severity">How deprecated the code is.</param>
|
||||
public void Warn(string nounPhrase, string version, DeprecationLevel severity)
|
||||
{
|
||||
this.Warn(this.ModRegistry.GetModFromStack(), nounPhrase, version, severity);
|
||||
this.Warn(this.ModRegistry.GetFromStack()?.DisplayName, nounPhrase, version, severity);
|
||||
}
|
||||
|
||||
/// <summary>Log a deprecation warning.</summary>
|
||||
|
@ -52,7 +52,7 @@ namespace StardewModdingAPI.Framework
|
|||
return;
|
||||
|
||||
// build message
|
||||
string message = $"{source ?? "An unknown mod"} uses deprecated code ({nounPhrase}).";
|
||||
string message = $"{source ?? "An unknown mod"} uses deprecated code ({nounPhrase} is deprecated since SMAPI {version}).";
|
||||
if (source == null)
|
||||
message += $"{Environment.NewLine}{Environment.StackTrace}";
|
||||
|
||||
|
@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework
|
|||
/// <returns>Returns whether the deprecation was successfully marked as warned. Returns <c>false</c> if it was already marked.</returns>
|
||||
public bool MarkWarned(string nounPhrase, string version)
|
||||
{
|
||||
return this.MarkWarned(this.ModRegistry.GetModFromStack(), nounPhrase, version);
|
||||
return this.MarkWarned(this.ModRegistry.GetFromStack()?.DisplayName, nounPhrase, version);
|
||||
}
|
||||
|
||||
/// <summary>Mark a deprecation warning as already logged.</summary>
|
||||
|
|
|
@ -30,6 +30,9 @@ namespace StardewModdingAPI.Framework
|
|||
/// <summary>The mod instance (if it was loaded).</summary>
|
||||
IMod Mod { get; }
|
||||
|
||||
/// <summary>The mod-provided API (if any).</summary>
|
||||
object Api { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
|
@ -43,5 +46,9 @@ namespace StardewModdingAPI.Framework
|
|||
/// <summary>Set the mod instance.</summary>
|
||||
/// <param name="mod">The mod instance to set.</param>
|
||||
IModMetadata SetMod(IMod mod);
|
||||
|
||||
/// <summary>Set the mod-provided API instance.</summary>
|
||||
/// <param name="api">The mod-provided API.</param>
|
||||
IModMetadata SetApi(object api);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -108,6 +108,15 @@ namespace StardewModdingAPI.Framework
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Get the lowest exception in an exception stack.</summary>
|
||||
/// <param name="exception">The exception from which to search.</param>
|
||||
public static Exception GetInnermostException(this Exception exception)
|
||||
{
|
||||
while (exception.InnerException != null)
|
||||
exception = exception.InnerException;
|
||||
return exception;
|
||||
}
|
||||
|
||||
/****
|
||||
** Sprite batch
|
||||
****/
|
||||
|
@ -125,7 +134,7 @@ namespace StardewModdingAPI.Framework
|
|||
#endif
|
||||
|
||||
// get result
|
||||
return reflection.GetPrivateField<bool>(Game1.spriteBatch, fieldName).GetValue();
|
||||
return reflection.GetField<bool>(Game1.spriteBatch, fieldName).GetValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using StardewModdingAPI.Framework.Reflection;
|
||||
|
||||
namespace StardewModdingAPI.Framework.ModHelpers
|
||||
{
|
||||
|
@ -11,6 +13,15 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
/// <summary>The underlying mod registry.</summary>
|
||||
private readonly ModRegistry Registry;
|
||||
|
||||
/// <summary>Encapsulates monitoring and logging for the mod.</summary>
|
||||
private readonly IMonitor Monitor;
|
||||
|
||||
/// <summary>The mod IDs for APIs accessed by this instanced.</summary>
|
||||
private readonly HashSet<string> AccessedModApis = new HashSet<string>();
|
||||
|
||||
/// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary>
|
||||
private readonly InterfaceProxyBuilder ProxyBuilder;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
|
@ -18,16 +29,20 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="modID">The unique ID of the relevant mod.</param>
|
||||
/// <param name="registry">The underlying mod registry.</param>
|
||||
public ModRegistryHelper(string modID, ModRegistry registry)
|
||||
/// <param name="proxyBuilder">Generates proxy classes to access mod APIs through an arbitrary interface.</param>
|
||||
/// <param name="monitor">Encapsulates monitoring and logging for the mod.</param>
|
||||
public ModRegistryHelper(string modID, ModRegistry registry, InterfaceProxyBuilder proxyBuilder, IMonitor monitor)
|
||||
: base(modID)
|
||||
{
|
||||
this.Registry = registry;
|
||||
this.ProxyBuilder = proxyBuilder;
|
||||
this.Monitor = monitor;
|
||||
}
|
||||
|
||||
/// <summary>Get metadata for all loaded mods.</summary>
|
||||
public IEnumerable<IManifest> GetAll()
|
||||
{
|
||||
return this.Registry.GetAll();
|
||||
return this.Registry.GetAll().Select(p => p.Manifest);
|
||||
}
|
||||
|
||||
/// <summary>Get metadata for a loaded mod.</summary>
|
||||
|
@ -35,14 +50,56 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
/// <returns>Returns the matching mod's metadata, or <c>null</c> if not found.</returns>
|
||||
public IManifest Get(string uniqueID)
|
||||
{
|
||||
return this.Registry.Get(uniqueID);
|
||||
return this.Registry.Get(uniqueID)?.Manifest;
|
||||
}
|
||||
|
||||
/// <summary>Get whether a mod has been loaded.</summary>
|
||||
/// <param name="uniqueID">The mod's unique ID.</param>
|
||||
public bool IsLoaded(string uniqueID)
|
||||
{
|
||||
return this.Registry.IsLoaded(uniqueID);
|
||||
return this.Registry.Get(uniqueID) != null;
|
||||
}
|
||||
|
||||
/// <summary>Get the API provided by a mod, or <c>null</c> if it has none. This signature requires using the <see cref="IModHelper.Reflection"/> API to access the API's properties and methods.</summary>
|
||||
public object GetApi(string uniqueID)
|
||||
{
|
||||
IModMetadata mod = this.Registry.Get(uniqueID);
|
||||
if (mod?.Api != null && this.AccessedModApis.Add(mod.Manifest.UniqueID))
|
||||
this.Monitor.Log($"Accessed mod-provided API for {mod.DisplayName}.", LogLevel.Trace);
|
||||
return mod?.Api;
|
||||
}
|
||||
|
||||
/// <summary>Get the API provided by a mod, mapped to a given interface which specifies the expected properties and methods. If the mod has no API or it's not compatible with the given interface, get <c>null</c>.</summary>
|
||||
/// <typeparam name="TInterface">The interface which matches the properties and methods you intend to access.</typeparam>
|
||||
/// <param name="uniqueID">The mod's unique ID.</param>
|
||||
public TInterface GetApi<TInterface>(string uniqueID) where TInterface : class
|
||||
{
|
||||
// validate
|
||||
if (!this.Registry.AreAllModsInitialised)
|
||||
{
|
||||
this.Monitor.Log("Tried to access a mod-provided API before all mods were initialised.", LogLevel.Error);
|
||||
return null;
|
||||
}
|
||||
if (!typeof(TInterface).IsInterface)
|
||||
{
|
||||
this.Monitor.Log("Tried to map a mod-provided API to a class; must be a public interface.", LogLevel.Error);
|
||||
return null;
|
||||
}
|
||||
if (!typeof(TInterface).IsPublic)
|
||||
{
|
||||
this.Monitor.Log("Tried to map a mod-provided API to a non-public interface; must be a public interface.", LogLevel.Error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// get raw API
|
||||
object api = this.GetApi(uniqueID);
|
||||
if (api == null)
|
||||
return null;
|
||||
|
||||
// get API of type
|
||||
if (api is TInterface castApi)
|
||||
return castApi;
|
||||
return this.ProxyBuilder.CreateProxy<TInterface>(api, this.ModID, uniqueID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
/// <summary>The mod name for error messages.</summary>
|
||||
private readonly string ModName;
|
||||
|
||||
/// <summary>Manages deprecation warnings.</summary>
|
||||
private readonly DeprecationManager DeprecationManager;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
|
@ -25,15 +28,88 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
/// <param name="modID">The unique ID of the relevant mod.</param>
|
||||
/// <param name="modName">The mod name for error messages.</param>
|
||||
/// <param name="reflector">The underlying reflection helper.</param>
|
||||
public ReflectionHelper(string modID, string modName, Reflector reflector)
|
||||
/// <param name="deprecationManager">Manages deprecation warnings.</param>
|
||||
public ReflectionHelper(string modID, string modName, Reflector reflector, DeprecationManager deprecationManager)
|
||||
: base(modID)
|
||||
{
|
||||
this.ModName = modName;
|
||||
this.Reflector = reflector;
|
||||
this.DeprecationManager = deprecationManager;
|
||||
}
|
||||
|
||||
/// <summary>Get an instance field.</summary>
|
||||
/// <typeparam name="TValue">The field type.</typeparam>
|
||||
/// <param name="obj">The object which has the field.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
public IReflectedField<TValue> GetField<TValue>(object obj, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
this.Reflector.GetField<TValue>(obj, name, required)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Get a static field.</summary>
|
||||
/// <typeparam name="TValue">The field type.</typeparam>
|
||||
/// <param name="type">The type which has the field.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
public IReflectedField<TValue> GetField<TValue>(Type type, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
this.Reflector.GetField<TValue>(type, name, required)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Get an instance property.</summary>
|
||||
/// <typeparam name="TValue">The property type.</typeparam>
|
||||
/// <param name="obj">The object which has the property.</param>
|
||||
/// <param name="name">The property name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the property is not found.</param>
|
||||
public IReflectedProperty<TValue> GetProperty<TValue>(object obj, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
this.Reflector.GetProperty<TValue>(obj, name, required)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Get a static property.</summary>
|
||||
/// <typeparam name="TValue">The property type.</typeparam>
|
||||
/// <param name="type">The type which has the property.</param>
|
||||
/// <param name="name">The property name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the property is not found.</param>
|
||||
public IReflectedProperty<TValue> GetProperty<TValue>(Type type, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
this.Reflector.GetProperty<TValue>(type, name, required)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Get an instance method.</summary>
|
||||
/// <param name="obj">The object which has the method.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
public IReflectedMethod GetMethod(object obj, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
this.Reflector.GetMethod(obj, name, required)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Get a static method.</summary>
|
||||
/// <param name="type">The type which has the method.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
public IReflectedMethod GetMethod(Type type, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
this.Reflector.GetMethod(type, name, required)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/****
|
||||
** Fields
|
||||
** Obsolete
|
||||
****/
|
||||
/// <summary>Get a private instance field.</summary>
|
||||
/// <typeparam name="TValue">The field type.</typeparam>
|
||||
|
@ -41,11 +117,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
/// <returns>Returns the field wrapper, or <c>null</c> if the field doesn't exist and <paramref name="required"/> is <c>false</c>.</returns>
|
||||
[Obsolete]
|
||||
public IPrivateField<TValue> GetPrivateField<TValue>(object obj, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
this.Reflector.GetPrivateField<TValue>(obj, name, required)
|
||||
);
|
||||
this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
|
||||
return (IPrivateField<TValue>)this.GetField<TValue>(obj, name, required);
|
||||
}
|
||||
|
||||
/// <summary>Get a private static field.</summary>
|
||||
|
@ -53,26 +129,23 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
/// <param name="type">The type which has the field.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
[Obsolete]
|
||||
public IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
this.Reflector.GetPrivateField<TValue>(type, name, required)
|
||||
);
|
||||
this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
|
||||
return (IPrivateField<TValue>)this.GetField<TValue>(type, name, required);
|
||||
}
|
||||
|
||||
/****
|
||||
** Properties
|
||||
****/
|
||||
/// <summary>Get a private instance property.</summary>
|
||||
/// <typeparam name="TValue">The property type.</typeparam>
|
||||
/// <param name="obj">The object which has the property.</param>
|
||||
/// <param name="name">The property name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private property is not found.</param>
|
||||
[Obsolete]
|
||||
public IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
this.Reflector.GetPrivateProperty<TValue>(obj, name, required)
|
||||
);
|
||||
this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
|
||||
return (IPrivateProperty<TValue>)this.GetProperty<TValue>(obj, name, required);
|
||||
}
|
||||
|
||||
/// <summary>Get a private static property.</summary>
|
||||
|
@ -80,17 +153,13 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
/// <param name="type">The type which has the property.</param>
|
||||
/// <param name="name">The property name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private property is not found.</param>
|
||||
[Obsolete]
|
||||
public IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
this.Reflector.GetPrivateProperty<TValue>(type, name, required)
|
||||
);
|
||||
this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
|
||||
return (IPrivateProperty<TValue>)this.GetProperty<TValue>(type, name, required);
|
||||
}
|
||||
|
||||
/****
|
||||
** Field values
|
||||
** (shorthand since this is the most common case)
|
||||
****/
|
||||
/// <summary>Get the value of a private instance field.</summary>
|
||||
/// <typeparam name="TValue">The field type.</typeparam>
|
||||
/// <param name="obj">The object which has the field.</param>
|
||||
|
@ -101,9 +170,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
/// This is a shortcut for <see cref="GetPrivateField{TValue}(object,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>.
|
||||
/// When <paramref name="required" /> is false, this will return the default value if reflection fails. If you need to check whether the field exists, use <see cref="GetPrivateField{TValue}(object,string,bool)" /> instead.
|
||||
/// </remarks>
|
||||
[Obsolete]
|
||||
public TValue GetPrivateValue<TValue>(object obj, string name, bool required = true)
|
||||
{
|
||||
IPrivateField<TValue> field = this.GetPrivateField<TValue>(obj, name, required);
|
||||
this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
|
||||
IPrivateField<TValue> field = (IPrivateField<TValue>)this.GetField<TValue>(obj, name, required);
|
||||
return field != null
|
||||
? field.GetValue()
|
||||
: default(TValue);
|
||||
|
@ -119,64 +190,36 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
/// This is a shortcut for <see cref="GetPrivateField{TValue}(Type,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>.
|
||||
/// When <paramref name="required" /> is false, this will return the default value if reflection fails. If you need to check whether the field exists, use <see cref="GetPrivateField{TValue}(Type,string,bool)" /> instead.
|
||||
/// </remarks>
|
||||
[Obsolete]
|
||||
public TValue GetPrivateValue<TValue>(Type type, string name, bool required = true)
|
||||
{
|
||||
IPrivateField<TValue> field = this.GetPrivateField<TValue>(type, name, required);
|
||||
this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
|
||||
IPrivateField<TValue> field = (IPrivateField<TValue>)this.GetField<TValue>(type, name, required);
|
||||
return field != null
|
||||
? field.GetValue()
|
||||
: default(TValue);
|
||||
}
|
||||
|
||||
/****
|
||||
** Methods
|
||||
****/
|
||||
/// <summary>Get a private instance method.</summary>
|
||||
/// <param name="obj">The object which has the method.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
[Obsolete]
|
||||
public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
this.Reflector.GetPrivateMethod(obj, name, required)
|
||||
);
|
||||
this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
|
||||
return (IPrivateMethod)this.GetMethod(obj, name, required);
|
||||
}
|
||||
|
||||
/// <summary>Get a private static method.</summary>
|
||||
/// <param name="type">The type which has the method.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
[Obsolete]
|
||||
public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
this.Reflector.GetPrivateMethod(type, name, required)
|
||||
);
|
||||
}
|
||||
|
||||
/****
|
||||
** Methods by signature
|
||||
****/
|
||||
/// <summary>Get a private instance method.</summary>
|
||||
/// <param name="obj">The object which has the method.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="argumentTypes">The argument types of the method signature to find.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Get a private static method.</summary>
|
||||
/// <param name="type">The type which has the method.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="argumentTypes">The argument types of the method signature to find.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
this.Reflector.GetPrivateMethod(type, name, argumentTypes, required)
|
||||
);
|
||||
this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
|
||||
return (IPrivateMethod)this.GetMethod(type, name, required);
|
||||
}
|
||||
|
||||
|
||||
|
@ -187,7 +230,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
/// <typeparam name="T">The field value type.</typeparam>
|
||||
/// <param name="field">The field being accessed.</param>
|
||||
/// <returns>Returns the same field instance for convenience.</returns>
|
||||
private IPrivateField<T> AssertAccessAllowed<T>(IPrivateField<T> field)
|
||||
private IReflectedField<T> AssertAccessAllowed<T>(IReflectedField<T> field)
|
||||
{
|
||||
this.AssertAccessAllowed(field?.FieldInfo);
|
||||
return field;
|
||||
|
@ -197,7 +240,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
/// <typeparam name="T">The property value type.</typeparam>
|
||||
/// <param name="property">The property being accessed.</param>
|
||||
/// <returns>Returns the same property instance for convenience.</returns>
|
||||
private IPrivateProperty<T> AssertAccessAllowed<T>(IPrivateProperty<T> property)
|
||||
private IReflectedProperty<T> AssertAccessAllowed<T>(IReflectedProperty<T> property)
|
||||
{
|
||||
this.AssertAccessAllowed(property?.PropertyInfo);
|
||||
return property;
|
||||
|
@ -206,7 +249,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
/// <summary>Assert that mods can use the reflection helper to access the given member.</summary>
|
||||
/// <param name="method">The method being accessed.</param>
|
||||
/// <returns>Returns the same method instance for convenience.</returns>
|
||||
private IPrivateMethod AssertAccessAllowed(IPrivateMethod method)
|
||||
private IReflectedMethod AssertAccessAllowed(IReflectedMethod method)
|
||||
{
|
||||
this.AssertAccessAllowed(method?.MethodInfo);
|
||||
return method;
|
||||
|
|
|
@ -29,6 +29,9 @@ namespace StardewModdingAPI.Framework.ModLoading
|
|||
/// <summary>The mod instance (if it was loaded).</summary>
|
||||
public IMod Mod { get; private set; }
|
||||
|
||||
/// <summary>The mod-provided API (if any).</summary>
|
||||
public object Api { get; private set; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
|
@ -64,5 +67,13 @@ namespace StardewModdingAPI.Framework.ModLoading
|
|||
this.Mod = mod;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Set the mod-provided API instance.</summary>
|
||||
/// <param name="api">The mod-provided API.</param>
|
||||
public IModMetadata SetApi(object api)
|
||||
{
|
||||
this.Api = api;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,26 +15,34 @@ namespace StardewModdingAPI.Framework
|
|||
/// <summary>The registered mod data.</summary>
|
||||
private readonly List<IModMetadata> Mods = new List<IModMetadata>();
|
||||
|
||||
/// <summary>The friendly mod names treated as deprecation warning sources (assembly full name => mod name).</summary>
|
||||
private readonly IDictionary<string, string> ModNamesByAssembly = new Dictionary<string, string>();
|
||||
/// <summary>An assembly full name => mod lookup.</summary>
|
||||
private readonly IDictionary<string, IModMetadata> ModNamesByAssembly = new Dictionary<string, IModMetadata>();
|
||||
|
||||
/// <summary>Whether all mods have been initialised and their <see cref="IMod.Entry"/> method called.</summary>
|
||||
public bool AreAllModsInitialised { get; set; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/****
|
||||
** Basic metadata
|
||||
****/
|
||||
/// <summary>Get metadata for all loaded mods.</summary>
|
||||
public IEnumerable<IManifest> GetAll()
|
||||
/// <summary>Register a mod as a possible source of deprecation warnings.</summary>
|
||||
/// <param name="metadata">The mod metadata.</param>
|
||||
public void Add(IModMetadata metadata)
|
||||
{
|
||||
return this.Mods.Select(p => p.Manifest);
|
||||
this.Mods.Add(metadata);
|
||||
this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata;
|
||||
}
|
||||
|
||||
/// <summary>Get metadata for all loaded mods.</summary>
|
||||
public IEnumerable<IModMetadata> GetAll()
|
||||
{
|
||||
return this.Mods.Select(p => p);
|
||||
}
|
||||
|
||||
/// <summary>Get metadata for a loaded mod.</summary>
|
||||
/// <param name="uniqueID">The mod's unique ID.</param>
|
||||
/// <returns>Returns the matching mod's metadata, or <c>null</c> if not found.</returns>
|
||||
public IManifest Get(string uniqueID)
|
||||
public IModMetadata Get(string uniqueID)
|
||||
{
|
||||
// normalise search ID
|
||||
if (string.IsNullOrWhiteSpace(uniqueID))
|
||||
|
@ -42,37 +50,13 @@ namespace StardewModdingAPI.Framework
|
|||
uniqueID = uniqueID.Trim();
|
||||
|
||||
// find match
|
||||
return this.GetAll().FirstOrDefault(p => p.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase));
|
||||
return this.GetAll().FirstOrDefault(p => p.Manifest.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>Get whether a mod has been loaded.</summary>
|
||||
/// <param name="uniqueID">The mod's unique ID.</param>
|
||||
public bool IsLoaded(string uniqueID)
|
||||
{
|
||||
return this.Get(uniqueID) != null;
|
||||
}
|
||||
|
||||
/****
|
||||
** Mod data
|
||||
****/
|
||||
/// <summary>Register a mod as a possible source of deprecation warnings.</summary>
|
||||
/// <param name="metadata">The mod metadata.</param>
|
||||
public void Add(IModMetadata metadata)
|
||||
{
|
||||
this.Mods.Add(metadata);
|
||||
this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata.DisplayName;
|
||||
}
|
||||
|
||||
/// <summary>Get all enabled mods.</summary>
|
||||
public IEnumerable<IModMetadata> GetMods()
|
||||
{
|
||||
return (from mod in this.Mods select mod);
|
||||
}
|
||||
|
||||
/// <summary>Get the friendly mod name which defines a type.</summary>
|
||||
/// <summary>Get the mod metadata from one of its assemblies.</summary>
|
||||
/// <param name="type">The type to check.</param>
|
||||
/// <returns>Returns the mod name, or <c>null</c> if the type isn't part of a known mod.</returns>
|
||||
public string GetModFrom(Type type)
|
||||
public IModMetadata GetFrom(Type type)
|
||||
{
|
||||
// null
|
||||
if (type == null)
|
||||
|
@ -89,7 +73,7 @@ namespace StardewModdingAPI.Framework
|
|||
|
||||
/// <summary>Get the friendly name for the closest assembly registered as a source of deprecation warnings.</summary>
|
||||
/// <returns>Returns the source name, or <c>null</c> if no registered assemblies were found.</returns>
|
||||
public string GetModFromStack()
|
||||
public IModMetadata GetFromStack()
|
||||
{
|
||||
// get stack frames
|
||||
StackTrace stack = new StackTrace();
|
||||
|
@ -101,9 +85,9 @@ namespace StardewModdingAPI.Framework
|
|||
foreach (StackFrame frame in frames)
|
||||
{
|
||||
MethodBase method = frame.GetMethod();
|
||||
string name = this.GetModFrom(method.ReflectedType);
|
||||
if (name != null)
|
||||
return name;
|
||||
IModMetadata mod = this.GetFrom(method.ReflectedType);
|
||||
if (mod != null)
|
||||
return mod;
|
||||
}
|
||||
|
||||
// no known assembly found
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
|
||||
namespace StardewModdingAPI.Framework.Reflection
|
||||
{
|
||||
/// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary>
|
||||
internal class InterfaceProxyBuilder
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The CLR module in which to create proxy classes.</summary>
|
||||
private readonly ModuleBuilder ModuleBuilder;
|
||||
|
||||
/// <summary>The generated proxy types.</summary>
|
||||
private readonly IDictionary<string, Type> GeneratedTypes = new Dictionary<string, Type>();
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
public InterfaceProxyBuilder()
|
||||
{
|
||||
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run);
|
||||
this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies");
|
||||
}
|
||||
|
||||
/// <summary>Create an API proxy.</summary>
|
||||
/// <typeparam name="TInterface">The interface through which to access the API.</typeparam>
|
||||
/// <param name="instance">The API instance to access.</param>
|
||||
/// <param name="sourceModID">The unique ID of the mod consuming the API.</param>
|
||||
/// <param name="targetModID">The unique ID of the mod providing the API.</param>
|
||||
public TInterface CreateProxy<TInterface>(object instance, string sourceModID, string targetModID)
|
||||
where TInterface : class
|
||||
{
|
||||
// validate
|
||||
if (instance == null)
|
||||
throw new InvalidOperationException("Can't proxy access to a null API.");
|
||||
if (!typeof(TInterface).IsInterface)
|
||||
throw new InvalidOperationException("The proxy type must be an interface, not a class.");
|
||||
|
||||
// get proxy type
|
||||
Type targetType = instance.GetType();
|
||||
string proxyTypeName = $"StardewModdingAPI.Proxies.From<{sourceModID}_{typeof(TInterface).FullName}>_To<{targetModID}_{targetType.FullName}>";
|
||||
if (!this.GeneratedTypes.TryGetValue(proxyTypeName, out Type type))
|
||||
{
|
||||
type = this.CreateProxyType(proxyTypeName, typeof(TInterface), targetType);
|
||||
this.GeneratedTypes[proxyTypeName] = type;
|
||||
}
|
||||
|
||||
// create instance
|
||||
ConstructorInfo constructor = type.GetConstructor(new[] { targetType });
|
||||
if (constructor == null)
|
||||
throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{proxyTypeName}'."); // should never happen
|
||||
return (TInterface)constructor.Invoke(new[] { instance });
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Define a class which proxies access to a target type through an interface.</summary>
|
||||
/// <param name="proxyTypeName">The name of the proxy type to generate.</param>
|
||||
/// <param name="interfaceType">The interface type through which to access the target.</param>
|
||||
/// <param name="targetType">The target type to access.</param>
|
||||
private Type CreateProxyType(string proxyTypeName, Type interfaceType, Type targetType)
|
||||
{
|
||||
// define proxy type
|
||||
TypeBuilder proxyBuilder = this.ModuleBuilder.DefineType(proxyTypeName, TypeAttributes.Public | TypeAttributes.Class);
|
||||
proxyBuilder.AddInterfaceImplementation(interfaceType);
|
||||
|
||||
// create field to store target instance
|
||||
FieldBuilder field = proxyBuilder.DefineField("__Target", targetType, FieldAttributes.Private);
|
||||
|
||||
// create constructor which accepts target instance
|
||||
{
|
||||
ConstructorBuilder constructor = proxyBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { targetType });
|
||||
ILGenerator il = constructor.GetILGenerator();
|
||||
|
||||
il.Emit(OpCodes.Ldarg_0); // this
|
||||
// ReSharper disable once AssignNullToNotNullAttribute -- never null
|
||||
il.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[0])); // call base constructor
|
||||
il.Emit(OpCodes.Ldarg_0); // this
|
||||
il.Emit(OpCodes.Ldarg_1); // load argument
|
||||
il.Emit(OpCodes.Stfld, field); // set field to loaded argument
|
||||
il.Emit(OpCodes.Ret);
|
||||
}
|
||||
|
||||
// proxy methods
|
||||
foreach (MethodInfo proxyMethod in interfaceType.GetMethods())
|
||||
{
|
||||
var targetMethod = targetType.GetMethod(proxyMethod.Name, proxyMethod.GetParameters().Select(a => a.ParameterType).ToArray());
|
||||
if (targetMethod == null)
|
||||
throw new InvalidOperationException($"The {interfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API.");
|
||||
|
||||
this.ProxyMethod(proxyBuilder, targetMethod, field);
|
||||
}
|
||||
|
||||
// create type
|
||||
return proxyBuilder.CreateType();
|
||||
}
|
||||
|
||||
/// <summary>Define a method which proxies access to a method on the target.</summary>
|
||||
/// <param name="proxyBuilder">The proxy type being generated.</param>
|
||||
/// <param name="target">The target method.</param>
|
||||
/// <param name="instanceField">The proxy field containing the API instance.</param>
|
||||
private void ProxyMethod(TypeBuilder proxyBuilder, MethodInfo target, FieldBuilder instanceField)
|
||||
{
|
||||
Type[] argTypes = target.GetParameters().Select(a => a.ParameterType).ToArray();
|
||||
|
||||
// create method
|
||||
MethodBuilder methodBuilder = proxyBuilder.DefineMethod(target.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual);
|
||||
methodBuilder.SetParameters(argTypes);
|
||||
methodBuilder.SetReturnType(target.ReturnType);
|
||||
|
||||
// create method body
|
||||
{
|
||||
ILGenerator il = methodBuilder.GetILGenerator();
|
||||
|
||||
// load target instance
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Ldfld, instanceField);
|
||||
|
||||
// invoke target method on instance
|
||||
for (int i = 0; i < argTypes.Length; i++)
|
||||
il.Emit(OpCodes.Ldarg, i + 1);
|
||||
il.Emit(OpCodes.Call, target);
|
||||
|
||||
// return result
|
||||
il.Emit(OpCodes.Ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace StardewModdingAPI.Framework.Reflection
|
||||
{
|
||||
/// <summary>A private field obtained through reflection.</summary>
|
||||
/// <summary>A field obtained through reflection.</summary>
|
||||
/// <typeparam name="TValue">The field value type.</typeparam>
|
||||
internal class PrivateField<TValue> : IPrivateField<TValue>
|
||||
internal class ReflectedField<TValue> : IPrivateField<TValue>, IReflectedField<TValue>
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
|
@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
/// <param name="isStatic">Whether the field is static.</param>
|
||||
/// <exception cref="ArgumentNullException">The <paramref name="parentType"/> or <paramref name="field"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">The <paramref name="obj"/> is null for a non-static field, or not null for a static field.</exception>
|
||||
public PrivateField(Type parentType, object obj, FieldInfo field, bool isStatic)
|
||||
public ReflectedField(Type parentType, object obj, FieldInfo field, bool isStatic)
|
||||
{
|
||||
// validate
|
||||
if (parentType == null)
|
||||
|
@ -64,11 +64,11 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new InvalidCastException($"Can't convert the private {this.DisplayName} field from {this.FieldInfo.FieldType.FullName} to {typeof(TValue).FullName}.");
|
||||
throw new InvalidCastException($"Can't convert the {this.DisplayName} field from {this.FieldInfo.FieldType.FullName} to {typeof(TValue).FullName}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Couldn't get the value of the private {this.DisplayName} field", ex);
|
||||
throw new Exception($"Couldn't get the value of the {this.DisplayName} field", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,11 +82,11 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new InvalidCastException($"Can't assign the private {this.DisplayName} field a {typeof(TValue).FullName} value, must be compatible with {this.FieldInfo.FieldType.FullName}.");
|
||||
throw new InvalidCastException($"Can't assign the {this.DisplayName} field a {typeof(TValue).FullName} value, must be compatible with {this.FieldInfo.FieldType.FullName}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Couldn't set the value of the private {this.DisplayName} field", ex);
|
||||
throw new Exception($"Couldn't set the value of the {this.DisplayName} field", ex);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,8 +3,8 @@ using System.Reflection;
|
|||
|
||||
namespace StardewModdingAPI.Framework.Reflection
|
||||
{
|
||||
/// <summary>A private method obtained through reflection.</summary>
|
||||
internal class PrivateMethod : IPrivateMethod
|
||||
/// <summary>A method obtained through reflection.</summary>
|
||||
internal class ReflectedMethod : IPrivateMethod, IReflectedMethod
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
|
@ -33,10 +33,10 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
/// <param name="parentType">The type that has the method.</param>
|
||||
/// <param name="obj">The object that has the instance method(if applicable).</param>
|
||||
/// <param name="method">The reflection metadata.</param>
|
||||
/// <param name="isStatic">Whether the field is static.</param>
|
||||
/// <param name="isStatic">Whether the method is static.</param>
|
||||
/// <exception cref="ArgumentNullException">The <paramref name="parentType"/> or <paramref name="method"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">The <paramref name="obj"/> is null for a non-static method, or not null for a static method.</exception>
|
||||
public PrivateMethod(Type parentType, object obj, MethodInfo method, bool isStatic)
|
||||
public ReflectedMethod(Type parentType, object obj, MethodInfo method, bool isStatic)
|
||||
{
|
||||
// validate
|
||||
if (parentType == null)
|
||||
|
@ -67,7 +67,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Couldn't invoke the private {this.DisplayName} field", ex);
|
||||
throw new Exception($"Couldn't invoke the {this.DisplayName} method", ex);
|
||||
}
|
||||
|
||||
// cast return value
|
||||
|
@ -77,7 +77,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new InvalidCastException($"Can't convert the return value of the private {this.DisplayName} method from {this.MethodInfo.ReturnType.FullName} to {typeof(TValue).FullName}.");
|
||||
throw new InvalidCastException($"Can't convert the return value of the {this.DisplayName} method from {this.MethodInfo.ReturnType.FullName} to {typeof(TValue).FullName}.");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Couldn't invoke the private {this.DisplayName} field", ex);
|
||||
throw new Exception($"Couldn't invoke the {this.DisplayName} method", ex);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,9 +3,9 @@ using System.Reflection;
|
|||
|
||||
namespace StardewModdingAPI.Framework.Reflection
|
||||
{
|
||||
/// <summary>A private property obtained through reflection.</summary>
|
||||
/// <summary>A property obtained through reflection.</summary>
|
||||
/// <typeparam name="TValue">The property value type.</typeparam>
|
||||
internal class PrivateProperty<TValue> : IPrivateProperty<TValue>
|
||||
internal class ReflectedProperty<TValue> : IPrivateProperty<TValue>, IReflectedProperty<TValue>
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
|
@ -14,10 +14,10 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
private readonly string DisplayName;
|
||||
|
||||
/// <summary>The underlying property getter.</summary>
|
||||
private readonly Func<TValue> GetterDelegate;
|
||||
private readonly Func<TValue> GetMethod;
|
||||
|
||||
/// <summary>The underlying property setter.</summary>
|
||||
private readonly Action<TValue> SetterDelegate;
|
||||
private readonly Action<TValue> SetMethod;
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -31,13 +31,13 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="parentType">The type that has the field.</param>
|
||||
/// <param name="obj">The object that has the instance field (if applicable).</param>
|
||||
/// <param name="parentType">The type that has the property.</param>
|
||||
/// <param name="obj">The object that has the instance property (if applicable).</param>
|
||||
/// <param name="property">The reflection metadata.</param>
|
||||
/// <param name="isStatic">Whether the field is static.</param>
|
||||
/// <param name="isStatic">Whether the property is static.</param>
|
||||
/// <exception cref="ArgumentNullException">The <paramref name="parentType"/> or <paramref name="property"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">The <paramref name="obj"/> is null for a non-static field, or not null for a static field.</exception>
|
||||
public PrivateProperty(Type parentType, object obj, PropertyInfo property, bool isStatic)
|
||||
/// <exception cref="ArgumentException">The <paramref name="obj"/> is null for a non-static property, or not null for a static property.</exception>
|
||||
public ReflectedProperty(Type parentType, object obj, PropertyInfo property, bool isStatic)
|
||||
{
|
||||
// validate input
|
||||
if (parentType == null)
|
||||
|
@ -55,24 +55,29 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
this.DisplayName = $"{parentType.FullName}::{property.Name}";
|
||||
this.PropertyInfo = property;
|
||||
|
||||
this.GetterDelegate = (Func<TValue>)Delegate.CreateDelegate(typeof(Func<TValue>), obj, this.PropertyInfo.GetMethod);
|
||||
this.SetterDelegate = (Action<TValue>)Delegate.CreateDelegate(typeof(Action<TValue>), obj, this.PropertyInfo.SetMethod);
|
||||
if (this.PropertyInfo.GetMethod != null)
|
||||
this.GetMethod = (Func<TValue>)Delegate.CreateDelegate(typeof(Func<TValue>), obj, this.PropertyInfo.GetMethod);
|
||||
if (this.PropertyInfo.SetMethod != null)
|
||||
this.SetMethod = (Action<TValue>)Delegate.CreateDelegate(typeof(Action<TValue>), obj, this.PropertyInfo.SetMethod);
|
||||
}
|
||||
|
||||
/// <summary>Get the property value.</summary>
|
||||
public TValue GetValue()
|
||||
{
|
||||
if (this.GetMethod == null)
|
||||
throw new InvalidOperationException($"The {this.DisplayName} property has no get method.");
|
||||
|
||||
try
|
||||
{
|
||||
return this.GetterDelegate();
|
||||
return this.GetMethod();
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new InvalidCastException($"Can't convert the private {this.DisplayName} property from {this.PropertyInfo.PropertyType.FullName} to {typeof(TValue).FullName}.");
|
||||
throw new InvalidCastException($"Can't convert the {this.DisplayName} property from {this.PropertyInfo.PropertyType.FullName} to {typeof(TValue).FullName}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Couldn't get the value of the private {this.DisplayName} property", ex);
|
||||
throw new Exception($"Couldn't get the value of the {this.DisplayName} property", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,17 +85,20 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
//// <param name="value">The value to set.</param>
|
||||
public void SetValue(TValue value)
|
||||
{
|
||||
if (this.SetMethod == null)
|
||||
throw new InvalidOperationException($"The {this.DisplayName} property has no set method.");
|
||||
|
||||
try
|
||||
{
|
||||
this.SetterDelegate(value);
|
||||
this.SetMethod(value);
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new InvalidCastException($"Can't assign the private {this.DisplayName} property a {typeof(TValue).FullName} value, must be compatible with {this.PropertyInfo.PropertyType.FullName}.");
|
||||
throw new InvalidCastException($"Can't assign the {this.DisplayName} property a {typeof(TValue).FullName} value, must be compatible with {this.PropertyInfo.PropertyType.FullName}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Couldn't set the value of the private {this.DisplayName} property", ex);
|
||||
throw new Exception($"Couldn't set the value of the {this.DisplayName} property", ex);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ using System.Runtime.Caching;
|
|||
|
||||
namespace StardewModdingAPI.Framework.Reflection
|
||||
{
|
||||
/// <summary>Provides helper methods for accessing private game code.</summary>
|
||||
/// <summary>Provides helper methods for accessing inaccessible code.</summary>
|
||||
/// <remarks>This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage).</remarks>
|
||||
internal class Reflector
|
||||
{
|
||||
|
@ -25,139 +25,139 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
/****
|
||||
** Fields
|
||||
****/
|
||||
/// <summary>Get a private instance field.</summary>
|
||||
/// <summary>Get a instance field.</summary>
|
||||
/// <typeparam name="TValue">The field type.</typeparam>
|
||||
/// <param name="obj">The object which has the field.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
/// <returns>Returns the field wrapper, or <c>null</c> if the field doesn't exist and <paramref name="required"/> is <c>false</c>.</returns>
|
||||
public IPrivateField<TValue> GetPrivateField<TValue>(object obj, string name, bool required = true)
|
||||
public IReflectedField<TValue> GetField<TValue>(object obj, string name, bool required = true)
|
||||
{
|
||||
// validate
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj), "Can't get a private instance field from a null object.");
|
||||
throw new ArgumentNullException(nameof(obj), "Can't get a instance field from a null object.");
|
||||
|
||||
// get field from hierarchy
|
||||
IPrivateField<TValue> field = this.GetFieldFromHierarchy<TValue>(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
IReflectedField<TValue> field = this.GetFieldFromHierarchy<TValue>(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
if (required && field == null)
|
||||
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance field.");
|
||||
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance field.");
|
||||
return field;
|
||||
}
|
||||
|
||||
/// <summary>Get a private static field.</summary>
|
||||
/// <summary>Get a static field.</summary>
|
||||
/// <typeparam name="TValue">The field type.</typeparam>
|
||||
/// <param name="type">The type which has the field.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
public IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true)
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
public IReflectedField<TValue> GetField<TValue>(Type type, string name, bool required = true)
|
||||
{
|
||||
// get field from hierarchy
|
||||
IPrivateField<TValue> field = this.GetFieldFromHierarchy<TValue>(type, null, name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);
|
||||
IReflectedField<TValue> field = this.GetFieldFromHierarchy<TValue>(type, null, name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);
|
||||
if (required && field == null)
|
||||
throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static field.");
|
||||
throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static field.");
|
||||
return field;
|
||||
}
|
||||
|
||||
/****
|
||||
** Properties
|
||||
****/
|
||||
/// <summary>Get a private instance property.</summary>
|
||||
/// <summary>Get a instance property.</summary>
|
||||
/// <typeparam name="TValue">The property type.</typeparam>
|
||||
/// <param name="obj">The object which has the property.</param>
|
||||
/// <param name="name">The property name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private property is not found.</param>
|
||||
public IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true)
|
||||
/// <param name="required">Whether to throw an exception if the property is not found.</param>
|
||||
public IReflectedProperty<TValue> GetProperty<TValue>(object obj, string name, bool required = true)
|
||||
{
|
||||
// validate
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj), "Can't get a private instance property from a null object.");
|
||||
throw new ArgumentNullException(nameof(obj), "Can't get a instance property from a null object.");
|
||||
|
||||
// get property from hierarchy
|
||||
IPrivateProperty<TValue> property = this.GetPropertyFromHierarchy<TValue>(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
IReflectedProperty<TValue> property = this.GetPropertyFromHierarchy<TValue>(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
if (required && property == null)
|
||||
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance property.");
|
||||
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance property.");
|
||||
return property;
|
||||
}
|
||||
|
||||
/// <summary>Get a private static property.</summary>
|
||||
/// <summary>Get a static property.</summary>
|
||||
/// <typeparam name="TValue">The property type.</typeparam>
|
||||
/// <param name="type">The type which has the property.</param>
|
||||
/// <param name="name">The property name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private property is not found.</param>
|
||||
public IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true)
|
||||
/// <param name="required">Whether to throw an exception if the property is not found.</param>
|
||||
public IReflectedProperty<TValue> GetProperty<TValue>(Type type, string name, bool required = true)
|
||||
{
|
||||
// get field from hierarchy
|
||||
IPrivateProperty<TValue> property = this.GetPropertyFromHierarchy<TValue>(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
|
||||
IReflectedProperty<TValue> property = this.GetPropertyFromHierarchy<TValue>(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
|
||||
if (required && property == null)
|
||||
throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static property.");
|
||||
throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static property.");
|
||||
return property;
|
||||
}
|
||||
|
||||
/****
|
||||
** Methods
|
||||
****/
|
||||
/// <summary>Get a private instance method.</summary>
|
||||
/// <summary>Get a instance method.</summary>
|
||||
/// <param name="obj">The object which has the method.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true)
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
public IReflectedMethod GetMethod(object obj, string name, bool required = true)
|
||||
{
|
||||
// validate
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object.");
|
||||
throw new ArgumentNullException(nameof(obj), "Can't get a instance method from a null object.");
|
||||
|
||||
// get method from hierarchy
|
||||
IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
IReflectedMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
if (required && method == null)
|
||||
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method.");
|
||||
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance method.");
|
||||
return method;
|
||||
}
|
||||
|
||||
/// <summary>Get a private static method.</summary>
|
||||
/// <summary>Get a static method.</summary>
|
||||
/// <param name="type">The type which has the method.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true)
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
public IReflectedMethod GetMethod(Type type, string name, bool required = true)
|
||||
{
|
||||
// get method from hierarchy
|
||||
IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
|
||||
IReflectedMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
|
||||
if (required && method == null)
|
||||
throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method.");
|
||||
throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static method.");
|
||||
return method;
|
||||
}
|
||||
|
||||
/****
|
||||
** Methods by signature
|
||||
****/
|
||||
/// <summary>Get a private instance method.</summary>
|
||||
/// <summary>Get a instance method.</summary>
|
||||
/// <param name="obj">The object which has the method.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="argumentTypes">The argument types of the method signature to find.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true)
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
public IReflectedMethod GetMethod(object obj, string name, Type[] argumentTypes, bool required = true)
|
||||
{
|
||||
// validate parent
|
||||
if (obj == null)
|
||||
throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object.");
|
||||
throw new ArgumentNullException(nameof(obj), "Can't get a instance method from a null object.");
|
||||
|
||||
// get method from hierarchy
|
||||
PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, argumentTypes);
|
||||
ReflectedMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, argumentTypes);
|
||||
if (required && method == null)
|
||||
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method with that signature.");
|
||||
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance method with that signature.");
|
||||
return method;
|
||||
}
|
||||
|
||||
/// <summary>Get a private static method.</summary>
|
||||
/// <summary>Get a static method.</summary>
|
||||
/// <param name="type">The type which has the method.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="argumentTypes">The argument types of the method signature to find.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true)
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
public IReflectedMethod GetMethod(Type type, string name, Type[] argumentTypes, bool required = true)
|
||||
{
|
||||
// get field from hierarchy
|
||||
PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, argumentTypes);
|
||||
ReflectedMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, argumentTypes);
|
||||
if (required && method == null)
|
||||
throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method with that signature.");
|
||||
throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static method with that signature.");
|
||||
return method;
|
||||
}
|
||||
|
||||
|
@ -171,7 +171,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
/// <param name="obj">The object which has the field.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="bindingFlags">The reflection binding which flags which indicates what type of field to find.</param>
|
||||
private IPrivateField<TValue> GetFieldFromHierarchy<TValue>(Type type, object obj, string name, BindingFlags bindingFlags)
|
||||
private IReflectedField<TValue> GetFieldFromHierarchy<TValue>(Type type, object obj, string name, BindingFlags bindingFlags)
|
||||
{
|
||||
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
|
||||
FieldInfo field = this.GetCached<FieldInfo>($"field::{isStatic}::{type.FullName}::{name}", () =>
|
||||
|
@ -183,7 +183,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
});
|
||||
|
||||
return field != null
|
||||
? new PrivateField<TValue>(type, obj, field, isStatic)
|
||||
? new ReflectedField<TValue>(type, obj, field, isStatic)
|
||||
: null;
|
||||
}
|
||||
|
||||
|
@ -193,7 +193,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
/// <param name="obj">The object which has the property.</param>
|
||||
/// <param name="name">The property name.</param>
|
||||
/// <param name="bindingFlags">The reflection binding which flags which indicates what type of property to find.</param>
|
||||
private IPrivateProperty<TValue> GetPropertyFromHierarchy<TValue>(Type type, object obj, string name, BindingFlags bindingFlags)
|
||||
private IReflectedProperty<TValue> GetPropertyFromHierarchy<TValue>(Type type, object obj, string name, BindingFlags bindingFlags)
|
||||
{
|
||||
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
|
||||
PropertyInfo property = this.GetCached<PropertyInfo>($"property::{isStatic}::{type.FullName}::{name}", () =>
|
||||
|
@ -205,7 +205,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
});
|
||||
|
||||
return property != null
|
||||
? new PrivateProperty<TValue>(type, obj, property, isStatic)
|
||||
? new ReflectedProperty<TValue>(type, obj, property, isStatic)
|
||||
: null;
|
||||
}
|
||||
|
||||
|
@ -214,7 +214,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
/// <param name="obj">The object which has the method.</param>
|
||||
/// <param name="name">The method name.</param>
|
||||
/// <param name="bindingFlags">The reflection binding which flags which indicates what type of method to find.</param>
|
||||
private IPrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags)
|
||||
private IReflectedMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags)
|
||||
{
|
||||
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
|
||||
MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () =>
|
||||
|
@ -226,7 +226,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
});
|
||||
|
||||
return method != null
|
||||
? new PrivateMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static))
|
||||
? new ReflectedMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static))
|
||||
: null;
|
||||
}
|
||||
|
||||
|
@ -236,7 +236,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
/// <param name="name">The method name.</param>
|
||||
/// <param name="bindingFlags">The reflection binding which flags which indicates what type of method to find.</param>
|
||||
/// <param name="argumentTypes">The argument types of the method signature to find.</param>
|
||||
private PrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes)
|
||||
private ReflectedMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes)
|
||||
{
|
||||
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
|
||||
MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}({string.Join(",", argumentTypes.Select(p => p.FullName))})", () =>
|
||||
|
@ -247,7 +247,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
|||
return methodInfo;
|
||||
});
|
||||
return method != null
|
||||
? new PrivateMethod(type, obj, method, isStatic)
|
||||
? new ReflectedMethod(type, obj, method, isStatic)
|
||||
: null;
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ namespace StardewModdingAPI.Framework
|
|||
private readonly ContentCache Cache;
|
||||
|
||||
/// <summary>The private <see cref="LocalizedContentManager"/> method which generates the locale portion of an asset name.</summary>
|
||||
private readonly IPrivateMethod GetKeyLocale;
|
||||
private readonly IReflectedMethod GetKeyLocale;
|
||||
|
||||
/// <summary>The language codes used in asset keys.</summary>
|
||||
private readonly IDictionary<string, LanguageCode> KeyLocales;
|
||||
|
@ -101,7 +101,7 @@ namespace StardewModdingAPI.Framework
|
|||
// init
|
||||
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
|
||||
this.Cache = new ContentCache(this, reflection, SContentManager.PossiblePathSeparators, SContentManager.PreferredPathSeparator);
|
||||
this.GetKeyLocale = reflection.GetPrivateMethod(this, "languageCode");
|
||||
this.GetKeyLocale = reflection.GetMethod(this, "languageCode");
|
||||
this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath);
|
||||
|
||||
// get asset data
|
||||
|
@ -205,7 +205,7 @@ namespace StardewModdingAPI.Framework
|
|||
return this.LoadImpl<T>(assetName, instance);
|
||||
|
||||
// load mod content
|
||||
SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading content asset '{assetName}': {reasonPhrase}.");
|
||||
SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading content asset '{assetName}': {reasonPhrase}");
|
||||
try
|
||||
{
|
||||
return this.WithWriteLock(() =>
|
||||
|
@ -252,6 +252,8 @@ namespace StardewModdingAPI.Framework
|
|||
}
|
||||
catch (Exception ex) when (!(ex is SContentLoadException))
|
||||
{
|
||||
if (ex.GetInnermostException() is DllNotFoundException dllEx && dllEx.Message == "libgdiplus.dylib")
|
||||
throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher.");
|
||||
throw new SContentLoadException($"The content manager failed loading content asset '{assetName}'.", ex);
|
||||
}
|
||||
}
|
||||
|
@ -413,7 +415,7 @@ namespace StardewModdingAPI.Framework
|
|||
private IDictionary<string, LanguageCode> GetKeyLocales(Reflector reflection)
|
||||
{
|
||||
// get the private code field directly to avoid changed-code logic
|
||||
IPrivateField<LanguageCode> codeField = reflection.GetPrivateField<LanguageCode>(typeof(LocalizedContentManager), "_currentLangCode");
|
||||
IReflectedField<LanguageCode> codeField = reflection.GetField<LanguageCode>(typeof(LocalizedContentManager), "_currentLangCode");
|
||||
|
||||
// remember previous settings
|
||||
LanguageCode previousCode = codeField.GetValue();
|
||||
|
|
|
@ -133,20 +133,20 @@ namespace StardewModdingAPI.Framework
|
|||
|
||||
// ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming
|
||||
/// <summary>Used to access private fields and methods.</summary>
|
||||
private static List<float> _fpsList => SGame.Reflection.GetPrivateField<List<float>>(typeof(Game1), nameof(_fpsList)).GetValue();
|
||||
private static Stopwatch _fpsStopwatch => SGame.Reflection.GetPrivateField<Stopwatch>(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue();
|
||||
private static List<float> _fpsList => SGame.Reflection.GetField<List<float>>(typeof(Game1), nameof(_fpsList)).GetValue();
|
||||
private static Stopwatch _fpsStopwatch => SGame.Reflection.GetField<Stopwatch>(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue();
|
||||
private static float _fps
|
||||
{
|
||||
set => SGame.Reflection.GetPrivateField<float>(typeof(Game1), nameof(_fps)).SetValue(value);
|
||||
set => SGame.Reflection.GetField<float>(typeof(Game1), nameof(_fps)).SetValue(value);
|
||||
}
|
||||
private static Task _newDayTask => SGame.Reflection.GetPrivateField<Task>(typeof(Game1), nameof(_newDayTask)).GetValue();
|
||||
private Color bgColor => SGame.Reflection.GetPrivateField<Color>(this, nameof(bgColor)).GetValue();
|
||||
public RenderTarget2D screenWrapper => SGame.Reflection.GetPrivateProperty<RenderTarget2D>(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop
|
||||
public BlendState lightingBlend => SGame.Reflection.GetPrivateField<BlendState>(this, nameof(lightingBlend)).GetValue();
|
||||
private readonly Action drawFarmBuildings = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke();
|
||||
private readonly Action drawHUD = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawHUD)).Invoke();
|
||||
private readonly Action drawDialogueBox = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke();
|
||||
private readonly Action renderScreenBuffer = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke();
|
||||
private static Task _newDayTask => SGame.Reflection.GetField<Task>(typeof(Game1), nameof(_newDayTask)).GetValue();
|
||||
private Color bgColor => SGame.Reflection.GetField<Color>(this, nameof(bgColor)).GetValue();
|
||||
public RenderTarget2D screenWrapper => SGame.Reflection.GetProperty<RenderTarget2D>(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop
|
||||
public BlendState lightingBlend => SGame.Reflection.GetField<BlendState>(this, nameof(lightingBlend)).GetValue();
|
||||
private readonly Action drawFarmBuildings = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke();
|
||||
private readonly Action drawHUD = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawHUD)).Invoke();
|
||||
private readonly Action drawDialogueBox = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke();
|
||||
private readonly Action renderScreenBuffer = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke();
|
||||
// ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming
|
||||
|
||||
|
||||
|
@ -182,7 +182,7 @@ namespace StardewModdingAPI.Framework
|
|||
this.SContentManager = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor, reflection);
|
||||
this.Content = new ContentManagerShim(this.SContentManager, "SGame.Content");
|
||||
Game1.content = new ContentManagerShim(this.SContentManager, "Game1.content");
|
||||
reflection.GetPrivateField<LocalizedContentManager>(typeof(Game1), "_temporaryContent").SetValue(new ContentManagerShim(this.SContentManager, "Game1._temporaryContent")); // regenerate value with new content manager
|
||||
reflection.GetField<LocalizedContentManager>(typeof(Game1), "_temporaryContent").SetValue(new ContentManagerShim(this.SContentManager, "Game1._temporaryContent")); // regenerate value with new content manager
|
||||
}
|
||||
|
||||
/****
|
||||
|
@ -557,9 +557,12 @@ namespace StardewModdingAPI.Framework
|
|||
/*********
|
||||
** Update events
|
||||
*********/
|
||||
GameEvents.InvokeUpdateTick(this.Monitor);
|
||||
if (this.FirstUpdate)
|
||||
{
|
||||
this.FirstUpdate = false;
|
||||
GameEvents.InvokeFirstUpdateTick(this.Monitor);
|
||||
}
|
||||
GameEvents.InvokeUpdateTick(this.Monitor);
|
||||
if (this.CurrentUpdateTick % 2 == 0)
|
||||
GameEvents.InvokeSecondUpdateTick(this.Monitor);
|
||||
if (this.CurrentUpdateTick % 4 == 0)
|
||||
|
@ -689,6 +692,7 @@ namespace StardewModdingAPI.Framework
|
|||
this.Monitor.Log($"The {activeClickableMenu.GetType().FullName} menu crashed while drawing itself during save. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error);
|
||||
activeClickableMenu.exitThisMenu();
|
||||
}
|
||||
this.RaisePostRender();
|
||||
Game1.spriteBatch.End();
|
||||
}
|
||||
//base.Draw(gameTime);
|
||||
|
@ -712,6 +716,7 @@ namespace StardewModdingAPI.Framework
|
|||
this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error);
|
||||
Game1.activeClickableMenu.exitThisMenu();
|
||||
}
|
||||
this.RaisePostRender();
|
||||
Game1.spriteBatch.End();
|
||||
if ((double)Game1.options.zoomLevel != 1.0)
|
||||
{
|
||||
|
@ -721,11 +726,12 @@ namespace StardewModdingAPI.Framework
|
|||
Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
|
||||
Game1.spriteBatch.End();
|
||||
}
|
||||
if (Game1.overlayMenu == null)
|
||||
return;
|
||||
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
|
||||
Game1.overlayMenu.draw(Game1.spriteBatch);
|
||||
Game1.spriteBatch.End();
|
||||
if (Game1.overlayMenu != null)
|
||||
{
|
||||
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
|
||||
Game1.overlayMenu.draw(Game1.spriteBatch);
|
||||
Game1.spriteBatch.End();
|
||||
}
|
||||
}
|
||||
else if ((int)Game1.gameMode == 11)
|
||||
{
|
||||
|
@ -733,6 +739,7 @@ namespace StardewModdingAPI.Framework
|
|||
Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink);
|
||||
Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, (int)byte.MaxValue, 0));
|
||||
Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White);
|
||||
this.RaisePostRender();
|
||||
Game1.spriteBatch.End();
|
||||
}
|
||||
else if (Game1.currentMinigame != null)
|
||||
|
@ -744,6 +751,7 @@ namespace StardewModdingAPI.Framework
|
|||
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha));
|
||||
Game1.spriteBatch.End();
|
||||
}
|
||||
this.RaisePostRender(needsNewBatch: true);
|
||||
if ((double)Game1.options.zoomLevel != 1.0)
|
||||
{
|
||||
this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null);
|
||||
|
@ -752,11 +760,12 @@ namespace StardewModdingAPI.Framework
|
|||
Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
|
||||
Game1.spriteBatch.End();
|
||||
}
|
||||
if (Game1.overlayMenu == null)
|
||||
return;
|
||||
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
|
||||
Game1.overlayMenu.draw(Game1.spriteBatch);
|
||||
Game1.spriteBatch.End();
|
||||
if (Game1.overlayMenu != null)
|
||||
{
|
||||
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
|
||||
Game1.overlayMenu.draw(Game1.spriteBatch);
|
||||
Game1.spriteBatch.End();
|
||||
}
|
||||
}
|
||||
else if (Game1.showingEndOfNightStuff)
|
||||
{
|
||||
|
@ -775,6 +784,7 @@ namespace StardewModdingAPI.Framework
|
|||
Game1.activeClickableMenu.exitThisMenu();
|
||||
}
|
||||
}
|
||||
this.RaisePostRender();
|
||||
Game1.spriteBatch.End();
|
||||
if ((double)Game1.options.zoomLevel != 1.0)
|
||||
{
|
||||
|
@ -784,11 +794,12 @@ namespace StardewModdingAPI.Framework
|
|||
Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
|
||||
Game1.spriteBatch.End();
|
||||
}
|
||||
if (Game1.overlayMenu == null)
|
||||
return;
|
||||
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
|
||||
Game1.overlayMenu.draw(Game1.spriteBatch);
|
||||
Game1.spriteBatch.End();
|
||||
if (Game1.overlayMenu != null)
|
||||
{
|
||||
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
|
||||
Game1.overlayMenu.draw(Game1.spriteBatch);
|
||||
Game1.spriteBatch.End();
|
||||
}
|
||||
}
|
||||
else if ((int)Game1.gameMode == 6)
|
||||
{
|
||||
|
@ -806,6 +817,7 @@ namespace StardewModdingAPI.Framework
|
|||
int x = 64;
|
||||
int y = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - height;
|
||||
SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str5, -1);
|
||||
this.RaisePostRender();
|
||||
Game1.spriteBatch.End();
|
||||
if ((double)Game1.options.zoomLevel != 1.0)
|
||||
{
|
||||
|
@ -815,11 +827,12 @@ namespace StardewModdingAPI.Framework
|
|||
Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
|
||||
Game1.spriteBatch.End();
|
||||
}
|
||||
if (Game1.overlayMenu == null)
|
||||
return;
|
||||
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
|
||||
Game1.overlayMenu.draw(Game1.spriteBatch);
|
||||
Game1.spriteBatch.End();
|
||||
if (Game1.overlayMenu != null)
|
||||
{
|
||||
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
|
||||
Game1.overlayMenu.draw(Game1.spriteBatch);
|
||||
Game1.spriteBatch.End();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1265,6 +1278,8 @@ namespace StardewModdingAPI.Framework
|
|||
}
|
||||
else if (Game1.farmEvent != null)
|
||||
Game1.farmEvent.drawAboveEverything(Game1.spriteBatch);
|
||||
|
||||
this.RaisePostRender();
|
||||
Game1.spriteBatch.End();
|
||||
if (Game1.overlayMenu != null)
|
||||
{
|
||||
|
@ -1272,14 +1287,6 @@ namespace StardewModdingAPI.Framework
|
|||
Game1.overlayMenu.draw(Game1.spriteBatch);
|
||||
Game1.spriteBatch.End();
|
||||
}
|
||||
|
||||
if (GraphicsEvents.HasPostRenderListeners())
|
||||
{
|
||||
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
|
||||
GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor);
|
||||
Game1.spriteBatch.End();
|
||||
}
|
||||
|
||||
this.renderScreenBuffer();
|
||||
}
|
||||
}
|
||||
|
@ -1401,5 +1408,19 @@ namespace StardewModdingAPI.Framework
|
|||
hash ^= v.GetHashCode();
|
||||
return hash;
|
||||
}
|
||||
|
||||
/// <summary>Raise the <see cref="GraphicsEvents.OnPostRenderEvent"/> if there are any listeners.</summary>
|
||||
/// <param name="needsNewBatch">Whether to create a new sprite batch.</param>
|
||||
private void RaisePostRender(bool needsNewBatch = false)
|
||||
{
|
||||
if (GraphicsEvents.HasPostRenderListeners())
|
||||
{
|
||||
if (needsNewBatch)
|
||||
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
|
||||
GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor);
|
||||
if (needsNewBatch)
|
||||
Game1.spriteBatch.End();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
namespace StardewModdingAPI
|
||||
namespace StardewModdingAPI
|
||||
{
|
||||
/// <summary>The implementation for a Stardew Valley mod.</summary>
|
||||
public interface IMod
|
||||
|
@ -22,5 +22,8 @@
|
|||
/// <summary>The mod entry point, called after the mod is first loaded.</summary>
|
||||
/// <param name="helper">Provides simplified APIs for writing mods.</param>
|
||||
void Entry(IModHelper helper);
|
||||
|
||||
/// <summary>Get an API that other mods can access. This is always called after <see cref="Entry"/>.</summary>
|
||||
object GetApi();
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StardewModdingAPI
|
||||
{
|
||||
|
@ -16,5 +16,14 @@ namespace StardewModdingAPI
|
|||
/// <summary>Get whether a mod has been loaded.</summary>
|
||||
/// <param name="uniqueID">The mod's unique ID.</param>
|
||||
bool IsLoaded(string uniqueID);
|
||||
|
||||
/// <summary>Get the API provided by a mod, or <c>null</c> if it has none. This signature requires using the <see cref="IModHelper.Reflection"/> API to access the API's properties and methods.</summary>
|
||||
/// <param name="uniqueID">The mod's unique ID.</param>
|
||||
object GetApi(string uniqueID);
|
||||
|
||||
/// <summary>Get the API provided by a mod, mapped to a given interface which specifies the expected properties and methods. If the mod has no API or it's not compatible with the given interface, get <c>null</c>.</summary>
|
||||
/// <typeparam name="TInterface">The interface which matches the properties and methods you intend to access.</typeparam>
|
||||
/// <param name="uniqueID">The mod's unique ID.</param>
|
||||
TInterface GetApi<TInterface>(string uniqueID) where TInterface : class;
|
||||
}
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
using System.Reflection;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace StardewModdingAPI
|
||||
{
|
||||
/// <summary>A private field obtained through reflection.</summary>
|
||||
/// <typeparam name="TValue">The field value type.</typeparam>
|
||||
[Obsolete("Use " + nameof(IReflectedField<TValue>) + " instead")]
|
||||
public interface IPrivateField<TValue>
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
using System.Reflection;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace StardewModdingAPI
|
||||
{
|
||||
/// <summary>A private method obtained through reflection.</summary>
|
||||
[Obsolete("Use " + nameof(IReflectedMethod) + " instead")]
|
||||
public interface IPrivateMethod
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
using System.Reflection;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace StardewModdingAPI
|
||||
{
|
||||
/// <summary>A private property obtained through reflection.</summary>
|
||||
/// <typeparam name="TValue">The property value type.</typeparam>
|
||||
[Obsolete("Use " + nameof(IPrivateProperty<TValue>) + " instead")]
|
||||
public interface IPrivateProperty<TValue>
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
using System.Reflection;
|
||||
|
||||
namespace StardewModdingAPI
|
||||
{
|
||||
/// <summary>A field obtained through reflection.</summary>
|
||||
/// <typeparam name="TValue">The field value type.</typeparam>
|
||||
public interface IReflectedField<TValue>
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The reflection metadata.</summary>
|
||||
FieldInfo FieldInfo { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Get the field value.</summary>
|
||||
TValue GetValue();
|
||||
|
||||
/// <summary>Set the field value.</summary>
|
||||
//// <param name="value">The value to set.</param>
|
||||
void SetValue(TValue value);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
using System.Reflection;
|
||||
|
||||
namespace StardewModdingAPI
|
||||
{
|
||||
/// <summary>A method obtained through reflection.</summary>
|
||||
public interface IReflectedMethod
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The reflection metadata.</summary>
|
||||
MethodInfo MethodInfo { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Invoke the method.</summary>
|
||||
/// <typeparam name="TValue">The return type.</typeparam>
|
||||
/// <param name="arguments">The method arguments to pass in.</param>
|
||||
TValue Invoke<TValue>(params object[] arguments);
|
||||
|
||||
/// <summary>Invoke the method.</summary>
|
||||
/// <param name="arguments">The method arguments to pass in.</param>
|
||||
void Invoke(params object[] arguments);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
using System.Reflection;
|
||||
|
||||
namespace StardewModdingAPI
|
||||
{
|
||||
/// <summary>A property obtained through reflection.</summary>
|
||||
/// <typeparam name="TValue">The property value type.</typeparam>
|
||||
public interface IReflectedProperty<TValue>
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The reflection metadata.</summary>
|
||||
PropertyInfo PropertyInfo { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Get the property value.</summary>
|
||||
TValue GetValue();
|
||||
|
||||
/// <summary>Set the property value.</summary>
|
||||
//// <param name="value">The value to set.</param>
|
||||
void SetValue(TValue value);
|
||||
}
|
||||
}
|
|
@ -1,18 +1,62 @@
|
|||
using System;
|
||||
using System;
|
||||
|
||||
namespace StardewModdingAPI
|
||||
{
|
||||
/// <summary>Provides an API for accessing private game code.</summary>
|
||||
/// <summary>Provides an API for accessing inaccessible code.</summary>
|
||||
public interface IReflectionHelper : IModLinked
|
||||
{
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Get an instance field.</summary>
|
||||
/// <typeparam name="TValue">The field type.</typeparam>
|
||||
/// <param name="obj">The object which has the field.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
IReflectedField<TValue> GetField<TValue>(object obj, string name, bool required = true);
|
||||
|
||||
/// <summary>Get a static field.</summary>
|
||||
/// <typeparam name="TValue">The field type.</typeparam>
|
||||
/// <param name="type">The type which has the field.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
IReflectedField<TValue> GetField<TValue>(Type type, string name, bool required = true);
|
||||
|
||||
/// <summary>Get an instance property.</summary>
|
||||
/// <typeparam name="TValue">The property type.</typeparam>
|
||||
/// <param name="obj">The object which has the property.</param>
|
||||
/// <param name="name">The property name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the property is not found.</param>
|
||||
IReflectedProperty<TValue> GetProperty<TValue>(object obj, string name, bool required = true);
|
||||
|
||||
/// <summary>Get a static property.</summary>
|
||||
/// <typeparam name="TValue">The property type.</typeparam>
|
||||
/// <param name="type">The type which has the property.</param>
|
||||
/// <param name="name">The property name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the property is not found.</param>
|
||||
IReflectedProperty<TValue> GetProperty<TValue>(Type type, string name, bool required = true);
|
||||
|
||||
/// <summary>Get an instance method.</summary>
|
||||
/// <param name="obj">The object which has the method.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
IReflectedMethod GetMethod(object obj, string name, bool required = true);
|
||||
|
||||
/// <summary>Get a static method.</summary>
|
||||
/// <param name="type">The type which has the method.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
IReflectedMethod GetMethod(Type type, string name, bool required = true);
|
||||
|
||||
/*****
|
||||
** Obsolete
|
||||
*****/
|
||||
/// <summary>Get a private instance field.</summary>
|
||||
/// <typeparam name="TValue">The field type.</typeparam>
|
||||
/// <param name="obj">The object which has the field.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " instead")]
|
||||
IPrivateField<TValue> GetPrivateField<TValue>(object obj, string name, bool required = true);
|
||||
|
||||
/// <summary>Get a private static field.</summary>
|
||||
|
@ -20,6 +64,7 @@ namespace StardewModdingAPI
|
|||
/// <param name="type">The type which has the field.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " instead")]
|
||||
IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true);
|
||||
|
||||
/// <summary>Get a private instance property.</summary>
|
||||
|
@ -27,6 +72,7 @@ namespace StardewModdingAPI
|
|||
/// <param name="obj">The object which has the property.</param>
|
||||
/// <param name="name">The property name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private property is not found.</param>
|
||||
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")]
|
||||
IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true);
|
||||
|
||||
/// <summary>Get a private static property.</summary>
|
||||
|
@ -34,6 +80,7 @@ namespace StardewModdingAPI
|
|||
/// <param name="type">The type which has the property.</param>
|
||||
/// <param name="name">The property name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private property is not found.</param>
|
||||
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")]
|
||||
IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true);
|
||||
|
||||
/// <summary>Get the value of a private instance field.</summary>
|
||||
|
@ -42,6 +89,7 @@ namespace StardewModdingAPI
|
|||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
/// <remarks>This is a shortcut for <see cref="GetPrivateField{TValue}(object,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>.</remarks>
|
||||
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " or " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")]
|
||||
TValue GetPrivateValue<TValue>(object obj, string name, bool required = true);
|
||||
|
||||
/// <summary>Get the value of a private static field.</summary>
|
||||
|
@ -50,18 +98,21 @@ namespace StardewModdingAPI
|
|||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
/// <remarks>This is a shortcut for <see cref="GetPrivateField{TValue}(Type,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>.</remarks>
|
||||
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " or " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")]
|
||||
TValue GetPrivateValue<TValue>(Type type, string name, bool required = true);
|
||||
|
||||
/// <summary>Get a private instance method.</summary>
|
||||
/// <param name="obj">The object which has the method.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetMethod) + " instead")]
|
||||
IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true);
|
||||
|
||||
/// <summary>Get a private static method.</summary>
|
||||
/// <param name="type">The type which has the method.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
|
||||
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetMethod) + " instead")]
|
||||
IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,7 +50,6 @@ namespace StardewModdingAPI.Metadata
|
|||
new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize", InstructionHandleResult.NotCompatible),
|
||||
new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent", InstructionHandleResult.NotCompatible),
|
||||
new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded", InstructionHandleResult.NotCompatible),
|
||||
new EventFinder("StardewModdingAPI.Events.GameEvents", "FirstUpdateTick", InstructionHandleResult.NotCompatible),
|
||||
new EventFinder("StardewModdingAPI.Events.PlayerEvents", "LoadedGame", InstructionHandleResult.NotCompatible),
|
||||
new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged", InstructionHandleResult.NotCompatible),
|
||||
new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged", InstructionHandleResult.NotCompatible),
|
||||
|
|
|
@ -25,6 +25,9 @@ namespace StardewModdingAPI
|
|||
/// <param name="helper">Provides simplified APIs for writing mods.</param>
|
||||
public abstract void Entry(IModHelper helper);
|
||||
|
||||
/// <summary>Get an API that other mods can access. This is always called after <see cref="Entry"/>.</summary>
|
||||
public virtual object GetApi() => null;
|
||||
|
||||
/// <summary>Release or reset unmanaged resources.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
|
@ -247,7 +247,7 @@ namespace StardewModdingAPI
|
|||
this.IsDisposed = true;
|
||||
|
||||
// dispose mod data
|
||||
foreach (IModMetadata mod in this.ModRegistry.GetMods())
|
||||
foreach (IModMetadata mod in this.ModRegistry.GetAll())
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -374,7 +374,7 @@ namespace StardewModdingAPI
|
|||
}
|
||||
|
||||
// update window titles
|
||||
int modsLoaded = this.ModRegistry.GetMods().Count();
|
||||
int modsLoaded = this.ModRegistry.GetAll().Count();
|
||||
this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods";
|
||||
Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods";
|
||||
|
||||
|
@ -390,7 +390,7 @@ namespace StardewModdingAPI
|
|||
LocalizedContentManager.LanguageCode languageCode = this.ContentManager.GetCurrentLanguage();
|
||||
|
||||
// update mod translation helpers
|
||||
foreach (IModMetadata mod in this.ModRegistry.GetMods())
|
||||
foreach (IModMetadata mod in this.ModRegistry.GetAll())
|
||||
(mod.Mod.Helper.Translation as TranslationHelper)?.SetLocale(locale, languageCode);
|
||||
}
|
||||
|
||||
|
@ -500,12 +500,11 @@ namespace StardewModdingAPI
|
|||
{
|
||||
// create client
|
||||
WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion);
|
||||
this.Monitor.Log("Checking for updates...", LogLevel.Trace);
|
||||
|
||||
// check SMAPI version
|
||||
try
|
||||
{
|
||||
this.Monitor.Log("Checking for SMAPI update...", LogLevel.Trace);
|
||||
|
||||
ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value;
|
||||
if (response.Error != null)
|
||||
{
|
||||
|
@ -515,7 +514,7 @@ namespace StardewModdingAPI
|
|||
else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion))
|
||||
this.Monitor.Log($"You can update SMAPI to {response.Version}: {response.Url}", LogLevel.Alert);
|
||||
else
|
||||
this.VerboseLog(" OK.");
|
||||
this.Monitor.Log(" SMAPI okay.", LogLevel.Trace);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -527,94 +526,101 @@ namespace StardewModdingAPI
|
|||
}
|
||||
|
||||
// check mod versions
|
||||
try
|
||||
if (mods.Any())
|
||||
{
|
||||
// log issues
|
||||
if (this.Settings.VerboseLogging)
|
||||
try
|
||||
{
|
||||
this.VerboseLog("Validating mod update keys...");
|
||||
foreach (IModMetadata mod in mods)
|
||||
// prepare update keys
|
||||
Dictionary<string, IModMetadata[]> modsByKey =
|
||||
(
|
||||
from mod in mods
|
||||
where mod.Manifest?.UpdateKeys != null
|
||||
from key in mod.Manifest.UpdateKeys
|
||||
select new { key, mod }
|
||||
)
|
||||
.GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group.Select(p => p.mod).ToArray(),
|
||||
StringComparer.InvariantCultureIgnoreCase
|
||||
);
|
||||
|
||||
// report update keys
|
||||
{
|
||||
if (mod.Manifest == null)
|
||||
this.VerboseLog($" {mod.DisplayName}: no manifest.");
|
||||
else if (mod.Manifest.UpdateKeys == null || !mod.Manifest.UpdateKeys.Any())
|
||||
this.VerboseLog($" {mod.DisplayName}: no update keys.");
|
||||
IModMetadata[] modsWithoutKeys = (
|
||||
from mod in mods
|
||||
where
|
||||
mod.Manifest != null
|
||||
&& (mod.Manifest.UpdateKeys == null || !mod.Manifest.UpdateKeys.Any())
|
||||
&& (mod.Manifest?.UniqueID != "SMAPI.ConsoleCommands" && mod.Manifest?.UniqueID != "SMAPI.TrainerMod")
|
||||
orderby mod.DisplayName
|
||||
select mod
|
||||
).ToArray();
|
||||
|
||||
string message = $"Checking {modsByKey.Count} mod update keys.";
|
||||
if (modsWithoutKeys.Any())
|
||||
message += $" {modsWithoutKeys.Length} mods have no update keys: {string.Join(", ", modsWithoutKeys.Select(p => p.DisplayName))}.";
|
||||
this.Monitor.Log($" {message}", LogLevel.Trace);
|
||||
}
|
||||
|
||||
// fetch results
|
||||
var results =
|
||||
(
|
||||
from entry in client.GetModInfo(modsByKey.Keys.ToArray())
|
||||
from mod in modsByKey[entry.Key]
|
||||
orderby mod.DisplayName
|
||||
select new { entry.Key, Mod = mod, Info = entry.Value }
|
||||
)
|
||||
.ToArray();
|
||||
|
||||
// extract latest versions
|
||||
IDictionary<IModMetadata, ModInfoModel> updatesByMod = new Dictionary<IModMetadata, ModInfoModel>();
|
||||
foreach (var result in results)
|
||||
{
|
||||
IModMetadata mod = result.Mod;
|
||||
ModInfoModel info = result.Info;
|
||||
|
||||
// handle error
|
||||
if (info.Error != null)
|
||||
{
|
||||
this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {info.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")}.");
|
||||
if (isUpdate)
|
||||
{
|
||||
if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version))
|
||||
updatesByMod[mod] = info;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// prepare update keys
|
||||
Dictionary<string, IModMetadata[]> modsByKey =
|
||||
(
|
||||
from mod in mods
|
||||
where mod.Manifest?.UpdateKeys != null
|
||||
from key in mod.Manifest.UpdateKeys
|
||||
select new { key, mod }
|
||||
)
|
||||
.GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group.Select(p => p.mod).ToArray(),
|
||||
StringComparer.InvariantCultureIgnoreCase
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Monitor.Log("Couldn't check for new mod versions. This won't affect your game, but you won't be notified of mod updates if this keeps happening.", LogLevel.Warn);
|
||||
this.Monitor.Log(ex is WebException && ex.InnerException == null
|
||||
? ex.Message
|
||||
: ex.ToString()
|
||||
);
|
||||
|
||||
// fetch results
|
||||
this.Monitor.Log($"Checking for updates to {modsByKey.Keys.Count} keys...", LogLevel.Trace);
|
||||
var results =
|
||||
(
|
||||
from entry in client.GetModInfo(modsByKey.Keys.ToArray())
|
||||
from mod in modsByKey[entry.Key]
|
||||
orderby mod.DisplayName
|
||||
select new { entry.Key, Mod = mod, Info = entry.Value }
|
||||
)
|
||||
.ToArray();
|
||||
|
||||
// extract latest versions
|
||||
IDictionary<IModMetadata, ModInfoModel> updatesByMod = new Dictionary<IModMetadata, ModInfoModel>();
|
||||
foreach (var result in results)
|
||||
{
|
||||
IModMetadata mod = result.Mod;
|
||||
ModInfoModel info = result.Info;
|
||||
|
||||
// handle error
|
||||
if (info.Error != null)
|
||||
{
|
||||
this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {info.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}]" : "")}" : "OK")}.");
|
||||
if (isUpdate)
|
||||
{
|
||||
if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version))
|
||||
updatesByMod[mod] = info;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Monitor.Log("Couldn't check for new mod versions. This won't affect your game, but you won't be notified of mod updates if this keeps happening.", LogLevel.Warn);
|
||||
this.Monitor.Log(ex is WebException && ex.InnerException == null
|
||||
? ex.Message
|
||||
: ex.ToString()
|
||||
);
|
||||
}
|
||||
}).Start();
|
||||
}
|
||||
|
@ -649,6 +655,7 @@ namespace StardewModdingAPI
|
|||
|
||||
AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode);
|
||||
AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name);
|
||||
InterfaceProxyBuilder proxyBuilder = new InterfaceProxyBuilder();
|
||||
foreach (IModMetadata metadata in mods)
|
||||
{
|
||||
// get basic info
|
||||
|
@ -690,53 +697,30 @@ namespace StardewModdingAPI
|
|||
continue;
|
||||
}
|
||||
|
||||
// validate assembly
|
||||
try
|
||||
{
|
||||
int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract);
|
||||
if (modEntries == 0)
|
||||
{
|
||||
TrackSkip(metadata, $"its DLL has no '{nameof(Mod)}' subclass.");
|
||||
continue;
|
||||
}
|
||||
if (modEntries > 1)
|
||||
{
|
||||
TrackSkip(metadata, $"its DLL contains multiple '{nameof(Mod)}' subclasses.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TrackSkip(metadata, $"its DLL couldn't be loaded:\n{ex.GetLogSummary()}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// initialise mod
|
||||
try
|
||||
{
|
||||
// get implementation
|
||||
TypeInfo modEntryType = modAssembly.DefinedTypes.First(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract);
|
||||
Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString());
|
||||
if (mod == null)
|
||||
// init mod helpers
|
||||
IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName);
|
||||
IModHelper modHelper;
|
||||
{
|
||||
TrackSkip(metadata, "its entry class couldn't be instantiated.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// inject data
|
||||
{
|
||||
IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName);
|
||||
ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager);
|
||||
IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor);
|
||||
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection);
|
||||
IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry);
|
||||
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager);
|
||||
IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyBuilder, monitor);
|
||||
ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage());
|
||||
|
||||
mod.ModManifest = manifest;
|
||||
mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper);
|
||||
mod.Monitor = monitor;
|
||||
modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper);
|
||||
}
|
||||
|
||||
// get mod instance
|
||||
if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod))
|
||||
continue;
|
||||
|
||||
// init mod
|
||||
mod.ModManifest = manifest;
|
||||
mod.Helper = modHelper;
|
||||
mod.Monitor = monitor;
|
||||
|
||||
// track mod
|
||||
metadata.SetMod(mod);
|
||||
this.ModRegistry.Add(metadata);
|
||||
|
@ -747,7 +731,7 @@ namespace StardewModdingAPI
|
|||
}
|
||||
}
|
||||
}
|
||||
IModMetadata[] loadedMods = this.ModRegistry.GetMods().ToArray();
|
||||
IModMetadata[] loadedMods = this.ModRegistry.GetAll().ToArray();
|
||||
|
||||
// log skipped mods
|
||||
this.Monitor.Newline();
|
||||
|
@ -811,6 +795,19 @@ namespace StardewModdingAPI
|
|||
{
|
||||
this.Monitor.Log($"{metadata.DisplayName} failed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error);
|
||||
}
|
||||
|
||||
// get mod API
|
||||
try
|
||||
{
|
||||
object api = metadata.Mod.GetApi();
|
||||
if (api != null)
|
||||
this.Monitor.Log($" Found mod-provided API ({api.GetType().FullName}).", LogLevel.Trace);
|
||||
metadata.SetApi(api);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Monitor.Log($"Failed loading mod-provided API for {metadata.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// invalidate cache entries when needed
|
||||
|
@ -846,13 +843,48 @@ namespace StardewModdingAPI
|
|||
this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace);
|
||||
this.ContentManager.InvalidateCacheFor(editors, loaders);
|
||||
}
|
||||
|
||||
// unlock mod integrations
|
||||
this.ModRegistry.AreAllModsInitialised = true;
|
||||
}
|
||||
|
||||
/// <summary>Load a mod's entry class.</summary>
|
||||
/// <param name="modAssembly">The mod assembly.</param>
|
||||
/// <param name="onError">A callback invoked when loading fails.</param>
|
||||
/// <param name="mod">The loaded instance.</param>
|
||||
private bool TryLoadModEntry(Assembly modAssembly, Action<string> onError, out Mod mod)
|
||||
{
|
||||
mod = null;
|
||||
|
||||
// find type
|
||||
TypeInfo[] modEntries = modAssembly.DefinedTypes.Where(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract).Take(2).ToArray();
|
||||
if (modEntries.Length == 0)
|
||||
{
|
||||
onError($"its DLL has no '{nameof(Mod)}' subclass.");
|
||||
return false;
|
||||
}
|
||||
if (modEntries.Length > 1)
|
||||
{
|
||||
onError($"its DLL contains multiple '{nameof(Mod)}' subclasses.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// get implementation
|
||||
mod = (Mod)modAssembly.CreateInstance(modEntries[0].ToString());
|
||||
if (mod == null)
|
||||
{
|
||||
onError("its entry class couldn't be instantiated.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Reload translations for all mods.</summary>
|
||||
private void ReloadTranslations()
|
||||
{
|
||||
JsonHelper jsonHelper = new JsonHelper();
|
||||
foreach (IModMetadata metadata in this.ModRegistry.GetMods())
|
||||
foreach (IModMetadata metadata in this.ModRegistry.GetAll())
|
||||
{
|
||||
// read translation files
|
||||
IDictionary<string, IDictionary<string, string>> translations = new Dictionary<string, IDictionary<string, string>>();
|
||||
|
|
|
@ -1890,6 +1890,16 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha
|
|||
"ID": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod | Pathoschild.TractorMod", // changed in 3.2, 4.0 beta, and 4.0
|
||||
"UpdateKeys": [ "Nexus:1401" ]
|
||||
},
|
||||
{
|
||||
// TrainerMod
|
||||
"ID": "SMAPI.TrainerMod",
|
||||
"Compatibility": {
|
||||
"~": {
|
||||
"Status": "Obsolete",
|
||||
"ReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
// Tree Transplant
|
||||
"ID": "TreeTransplant",
|
||||
|
|
|
@ -79,10 +79,6 @@
|
|||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.Windows.Forms" Condition="$(OS) == 'Windows_NT'" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -110,7 +106,11 @@
|
|||
<Compile Include="Framework\ContentManagerShim.cs" />
|
||||
<Compile Include="Framework\Exceptions\SAssemblyLoadFailedException.cs" />
|
||||
<Compile Include="Framework\ModLoading\AssemblyLoadStatus.cs" />
|
||||
<Compile Include="Framework\Reflection\InterfaceProxyBuilder.cs" />
|
||||
<Compile Include="Framework\Utilities\ContextHash.cs" />
|
||||
<Compile Include="IReflectedField.cs" />
|
||||
<Compile Include="IReflectedMethod.cs" />
|
||||
<Compile Include="IReflectedProperty.cs" />
|
||||
<Compile Include="Metadata\CoreAssets.cs" />
|
||||
<Compile Include="ContentSource.cs" />
|
||||
<Compile Include="Events\ContentEvents.cs" />
|
||||
|
@ -169,7 +169,7 @@
|
|||
<Compile Include="Framework\Models\ModStatus.cs" />
|
||||
<Compile Include="Framework\Models\SConfig.cs" />
|
||||
<Compile Include="Framework\ModLoading\ModMetadata.cs" />
|
||||
<Compile Include="Framework\Reflection\PrivateProperty.cs" />
|
||||
<Compile Include="Framework\Reflection\ReflectedProperty.cs" />
|
||||
<Compile Include="Framework\RequestExitDelegate.cs" />
|
||||
<Compile Include="Framework\SContentManager.cs" />
|
||||
<Compile Include="Framework\Exceptions\SParseException.cs" />
|
||||
|
@ -198,8 +198,8 @@
|
|||
<Compile Include="Framework\Models\ModDataRecord.cs" />
|
||||
<Compile Include="Framework\ModLoading\AssemblyLoader.cs" />
|
||||
<Compile Include="Framework\Reflection\CacheEntry.cs" />
|
||||
<Compile Include="Framework\Reflection\PrivateField.cs" />
|
||||
<Compile Include="Framework\Reflection\PrivateMethod.cs" />
|
||||
<Compile Include="Framework\Reflection\ReflectedField.cs" />
|
||||
<Compile Include="Framework\Reflection\ReflectedMethod.cs" />
|
||||
<Compile Include="Framework\Reflection\Reflector.cs" />
|
||||
<Compile Include="IManifest.cs" />
|
||||
<Compile Include="IMod.cs" />
|
||||
|
|
Loading…
Reference in New Issue