Merge branch 'develop' into stable

This commit is contained in:
Jesse Plamondon-Willard 2017-12-26 00:31:36 -05:00
commit 15d4b6310e
70 changed files with 1752 additions and 689 deletions

View File

@ -2,5 +2,5 @@ using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
[assembly: ComVisible(false)] [assembly: ComVisible(false)]
[assembly: AssemblyVersion("2.2.0.0")] [assembly: AssemblyVersion("2.3.0.0")]
[assembly: AssemblyFileVersion("2.2.0.0")] [assembly: AssemblyFileVersion("2.3.0.0")]

View File

@ -1,4 +1,24 @@
# Release notes # 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 ## 2.2
* For players: * For players:
* Fixed error when a mod loads custom assets on Linux/Mac. * Fixed error when a mod loads custom assets on Linux/Mac.

View File

@ -97,8 +97,8 @@ namespace StardewModdingApi.Installer
yield return GetInstallPath("StardewModdingAPI.pdb"); yield return GetInstallPath("StardewModdingAPI.pdb");
// obsolete // obsolete
yield return GetInstallPath("Mods/.cache"); // 1.3-1.4 yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4
yield return GetInstallPath("Mods/TrainerMod"); // *2.0 (renamed to ConsoleCommands) yield return GetInstallPath(Path.Combine("Mods", "TrainerMod")); // *2.0 (renamed to ConsoleCommands)
yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.31.8 yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.31.8
yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4
if (modsDir.Exists) if (modsDir.Exists)

View File

@ -41,10 +41,6 @@
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -3,7 +3,7 @@
"Author": "SMAPI", "Author": "SMAPI",
"Version": { "Version": {
"MajorVersion": 2, "MajorVersion": 2,
"MinorVersion": 0, "MinorVersion": 3,
"PatchVersion": 0, "PatchVersion": 0,
"Build": null "Build": null
}, },

View File

@ -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";
}
}
}

View File

@ -6,8 +6,8 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework;
using StardewModdingAPI.Web.Framework.Clients.Pastebin;
using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.ConfigModels;
using StardewModdingAPI.Web.Framework.LogParser;
using StardewModdingAPI.Web.ViewModels; using StardewModdingAPI.Web.ViewModels;
namespace StardewModdingAPI.Web.Controllers namespace StardewModdingAPI.Web.Controllers
@ -19,10 +19,10 @@ namespace StardewModdingAPI.Web.Controllers
** Properties ** Properties
*********/ *********/
/// <summary>The log parser config settings.</summary> /// <summary>The log parser config settings.</summary>
private readonly LogParserConfig Config; private readonly ContextConfig Config;
/// <summary>The underlying Pastebin client.</summary> /// <summary>The underlying Pastebin client.</summary>
private readonly PastebinClient PastebinClient; private readonly IPastebinClient Pastebin;
/// <summary>The first bytes in a valid zip file.</summary> /// <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> /// <remarks>See <a href="https://en.wikipedia.org/wiki/Zip_(file_format)#File_headers"/>.</remarks>
@ -36,14 +36,12 @@ namespace StardewModdingAPI.Web.Controllers
** Constructor ** Constructor
***/ ***/
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="configProvider">The log parser config settings.</param> /// <param name="contextProvider">The context config settings.</param>
public LogParserController(IOptions<LogParserConfig> configProvider) /// <param name="pastebin">The Pastebin API client.</param>
public LogParserController(IOptions<ContextConfig> contextProvider, IPastebinClient pastebin)
{ {
// init Pastebin client this.Config = contextProvider.Value;
this.Config = configProvider.Value; this.Pastebin = pastebin;
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);
} }
/*** /***
@ -52,12 +50,11 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Render the log parser UI.</summary> /// <summary>Render the log parser UI.</summary>
/// <param name="id">The paste ID.</param> /// <param name="id">The paste ID.</param>
[HttpGet] [HttpGet]
[Route("")]
[Route("log")] [Route("log")]
[Route("log/{id}")] [Route("log/{id}")]
public ViewResult Index(string id = null) 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> /// <param name="id">The Pastebin paste ID.</param>
[HttpGet, Produces("application/json")] [HttpGet, Produces("application/json")]
[Route("log/fetch/{id}")] [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); response.Content = this.DecompressString(response.Content);
return response; return response;
} }
@ -78,10 +75,10 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="content">The log content to save.</param> /// <param name="content">The log content to save.</param>
[HttpPost, Produces("application/json"), AllowLargePosts] [HttpPost, Produces("application/json"), AllowLargePosts]
[Route("log/save")] [Route("log/save")]
public async Task<SavePasteResponse> PostAsync([FromBody] string content) public async Task<SavePasteResult> PostAsync([FromBody] string content)
{ {
content = this.CompressString(content); content = this.CompressString(content);
return await this.PastebinClient.PostAsync(content); return await this.Pastebin.PostAsync(content);
} }

View File

@ -7,6 +7,9 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using StardewModdingAPI.Common.Models; 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.ConfigModels;
using StardewModdingAPI.Web.Framework.ModRepositories; using StardewModdingAPI.Web.Framework.ModRepositories;
@ -39,39 +42,22 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="cache">The cache in which to store mod metadata.</param> /// <param name="cache">The cache in which to store mod metadata.</param>
/// <param name="configProvider">The config settings for mod update checks.</param> /// <param name="configProvider">The config settings for mod update checks.</param>
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; ModUpdateCheckConfig config = configProvider.Value;
this.Cache = cache; this.Cache = cache;
this.CacheMinutes = config.CacheMinutes; this.CacheMinutes = config.CacheMinutes;
this.VersionRegex = config.SemanticVersionRegex; this.VersionRegex = config.SemanticVersionRegex;
string version = this.GetType().Assembly.GetName().Version.ToString(3);
this.Repositories = this.Repositories =
new IModRepository[] new IModRepository[]
{ {
new ChucklefishRepository( new ChucklefishRepository(config.ChucklefishKey, chucklefish),
vendorKey: config.ChucklefishKey, new GitHubRepository(config.GitHubKey, github),
userAgent: string.Format(config.ChucklefishUserAgent, version), new NexusRepository(config.NexusKey, nexus)
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
)
} }
.ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase); .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase);
} }

View File

@ -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();
}
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}

View File

@ -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; }
}
}

View File

@ -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();
}
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}

View File

@ -1,7 +1,7 @@
namespace StardewModdingAPI.Web.Framework.LogParser namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
{ {
/// <summary>The response for a get-paste request.</summary> /// <summary>The response for a get-paste request.</summary>
internal class GetPasteResponse internal class PasteInfo
{ {
/// <summary>Whether the log was successfully fetched.</summary> /// <summary>Whether the log was successfully fetched.</summary>
public bool Success { get; set; } public bool Success { get; set; }

View File

@ -8,10 +8,10 @@ using System.Threading.Tasks;
using System.Web; using System.Web;
using Pathoschild.Http.Client; using Pathoschild.Http.Client;
namespace StardewModdingAPI.Web.Framework.LogParser namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
{ {
/// <summary>An API client for Pastebin.</summary> /// <summary>An API client for Pastebin.</summary>
internal class PastebinClient : IDisposable internal class PastebinClient : IPastebinClient
{ {
/********* /*********
** Properties ** Properties
@ -43,7 +43,7 @@ namespace StardewModdingAPI.Web.Framework.LogParser
/// <summary>Fetch a saved paste.</summary> /// <summary>Fetch a saved paste.</summary>
/// <param name="id">The paste ID.</param> /// <param name="id">The paste ID.</param>
public async Task<GetPasteResponse> GetAsync(string id) public async Task<PasteInfo> GetAsync(string id)
{ {
try try
{ {
@ -54,30 +54,30 @@ namespace StardewModdingAPI.Web.Framework.LogParser
// handle Pastebin errors // handle Pastebin errors
if (string.IsNullOrWhiteSpace(content)) 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")) 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 PasteInfo { 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 { Success = true, Content = content };
} }
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) 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) catch (Exception ex)
{ {
return new GetPasteResponse { Error = ex.ToString() }; return new PasteInfo { Error = ex.ToString() };
} }
} }
/// <summary>Save a paste to Pastebin.</summary> /// <summary>Save a paste to Pastebin.</summary>
/// <param name="content">The paste content.</param> /// <param name="content">The paste content.</param>
public async Task<SavePasteResponse> PostAsync(string content) public async Task<SavePasteResult> PostAsync(string content)
{ {
try try
{ {
// validate // validate
if (string.IsNullOrWhiteSpace(content)) 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 // post to API
string response = await this.Client string response = await this.Client
@ -96,19 +96,19 @@ namespace StardewModdingAPI.Web.Framework.LogParser
// handle Pastebin errors // handle Pastebin errors
if (string.IsNullOrWhiteSpace(response)) 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")) if (response.StartsWith("Bad API request"))
return new SavePasteResponse { Error = response }; return new SavePasteResult { Error = response };
if (!response.Contains("/")) if (!response.Contains("/"))
return new SavePasteResponse { Error = $"Received an unknown response: {response}" }; return new SavePasteResult { Error = $"Received an unknown response: {response}" };
// return paste ID // return paste ID
string pastebinID = response.Split("/").Last(); string pastebinID = response.Split("/").Last();
return new SavePasteResponse { Success = true, ID = pastebinID }; return new SavePasteResult { Success = true, ID = pastebinID };
} }
catch (Exception ex) catch (Exception ex)
{ {
return new SavePasteResponse { Success = false, Error = ex.ToString() }; return new SavePasteResult { Success = false, Error = ex.ToString() };
} }
} }

View File

@ -1,7 +1,7 @@
namespace StardewModdingAPI.Web.Framework.LogParser namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
{ {
/// <summary>The response for a save-log request.</summary> /// <summary>The response for a save-log request.</summary>
internal class SavePasteResponse internal class SavePasteResult
{ {
/// <summary>Whether the log was successfully saved.</summary> /// <summary>Whether the log was successfully saved.</summary>
public bool Success { get; set; } public bool Success { get; set; }

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -6,9 +6,6 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/********* /*********
** Accessors ** Accessors
*********/ *********/
/****
** General
****/
/// <summary>The number of minutes update checks should be cached before refetching them.</summary> /// <summary>The number of minutes update checks should be cached before refetching them.</summary>
public int CacheMinutes { get; set; } public int CacheMinutes { get; set; }
@ -16,59 +13,13 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// <remarks>Derived from SMAPI's SemanticVersion implementation.</remarks> /// <remarks>Derived from SMAPI's SemanticVersion implementation.</remarks>
public string SemanticVersionRegex { get; set; } public string SemanticVersionRegex { get; set; }
/****
** Chucklefish mod site
****/
/// <summary>The repository key for the Chucklefish mod site.</summary> /// <summary>The repository key for the Chucklefish mod site.</summary>
public string ChucklefishKey { get; set; } 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> /// <summary>The repository key for Nexus Mods.</summary>
public string GitHubKey { get; set; } 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> /// <summary>The repository key for Nexus Mods.</summary>
public string NexusKey { get; set; } 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; }
} }
} }

View File

@ -1,9 +1,7 @@
using System; using System;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using HtmlAgilityPack;
using Pathoschild.Http.Client;
using StardewModdingAPI.Common.Models; using StardewModdingAPI.Common.Models;
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
namespace StardewModdingAPI.Web.Framework.ModRepositories namespace StardewModdingAPI.Web.Framework.ModRepositories
{ {
@ -13,14 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
/********* /*********
** Properties ** 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> /// <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> /// <summary>Construct an instance.</summary>
/// <param name="vendorKey">The unique key for this vendor.</param> /// <param name="vendorKey">The unique key for this vendor.</param>
/// <param name="userAgent">The user agent for the API client.</param> /// <param name="client">The underlying HTTP client.</param>
/// <param name="baseUrl">The base URL for the Chucklefish mod site.</param> public ChucklefishRepository(string vendorKey, IChucklefishClient client)
/// <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)
: base(vendorKey) : base(vendorKey)
{ {
this.BaseUrl = baseUrl; this.Client = client;
this.ModPageUrlFormat = modPageUrlFormat;
this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
} }
/// <summary>Get metadata about a mod in the repository.</summary> /// <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) public override async Task<ModInfoModel> GetModInfoAsync(string id)
{ {
// validate ID format // 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."); return new ModInfoModel($"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID.");
// fetch info // fetch info
try try
{ {
// fetch HTML var mod = await this.Client.GetModAsync(realID);
string html; if (mod == null)
try
{
html = await this.Client
.GetAsync(string.Format(this.ModPageUrlFormat, id))
.AsString();
}
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
{
return new ModInfoModel("Found no mod with this ID."); 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 // create model
return new ModInfoModel(name, this.NormaliseVersion(version), url); return new ModInfoModel(mod.Name, this.NormaliseVersion(mod.Version), mod.Url);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -1,9 +1,7 @@
using System; using System;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json;
using Pathoschild.Http.Client;
using StardewModdingAPI.Common.Models; using StardewModdingAPI.Common.Models;
using StardewModdingAPI.Web.Framework.Clients.GitHub;
namespace StardewModdingAPI.Web.Framework.ModRepositories namespace StardewModdingAPI.Web.Framework.ModRepositories
{ {
@ -13,11 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
/********* /*********
** Properties ** Properties
*********/ *********/
/// <summary>The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID.</summary> /// <summary>The underlying GitHub API client.</summary>
private readonly string ReleaseUrlFormat; private readonly IGitHubClient Client;
/// <summary>The underlying HTTP client.</summary>
private readonly IClient Client;
/********* /*********
@ -25,22 +20,11 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
*********/ *********/
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="vendorKey">The unique key for this vendor.</param> /// <param name="vendorKey">The unique key for this vendor.</param>
/// <param name="baseUrl">The base URL for the Nexus Mods API.</param> /// <param name="client">The underlying GitHub API client.</param>
/// <param name="releaseUrlFormat">The URL for a Nexus Mods API query excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param> public GitHubRepository(string vendorKey, IGitHubClient client)
/// <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)
: base(vendorKey) : base(vendorKey)
{ {
this.ReleaseUrlFormat = releaseUrlFormat; this.Client = client;
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 metadata about a mod in the repository.</summary> /// <summary>Get metadata about a mod in the repository.</summary>
@ -54,14 +38,10 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
// fetch info // fetch info
try try
{ {
GitRelease release = await this.Client GitRelease release = await this.Client.GetLatestReleaseAsync(id);
.GetAsync(string.Format(this.ReleaseUrlFormat, id)) return release != null
.As<GitRelease>(); ? new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases")
return new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases"); : new ModInfoModel("Found no mod with this ID.");
}
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
{
return new ModInfoModel("Found no mod with this ID.");
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -74,24 +54,5 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
{ {
this.Client.Dispose(); 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; }
}
} }
} }

View File

@ -1,8 +1,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json;
using Pathoschild.Http.Client;
using StardewModdingAPI.Common.Models; using StardewModdingAPI.Common.Models;
using StardewModdingAPI.Web.Framework.Clients.Nexus;
namespace StardewModdingAPI.Web.Framework.ModRepositories namespace StardewModdingAPI.Web.Framework.ModRepositories
{ {
@ -12,11 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
/********* /*********
** Properties ** Properties
*********/ *********/
/// <summary>The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID.</summary> /// <summary>The underlying Nexus Mods API client.</summary>
private readonly string ModUrlFormat; private readonly INexusClient Client;
/// <summary>The underlying HTTP client.</summary>
private readonly IClient Client;
/********* /*********
@ -24,14 +20,11 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
*********/ *********/
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="vendorKey">The unique key for this vendor.</param> /// <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="client">The underlying Nexus Mods API client.</param>
/// <param name="baseUrl">The base URL for the Nexus Mods API.</param> public NexusRepository(string vendorKey, INexusClient client)
/// <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)
: base(vendorKey) : base(vendorKey)
{ {
this.ModUrlFormat = modUrlFormat; this.Client = client;
this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
} }
/// <summary>Get metadata about a mod in the repository.</summary> /// <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) public override async Task<ModInfoModel> GetModInfoAsync(string id)
{ {
// validate ID format // 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."); return new ModInfoModel($"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID.");
// fetch info // fetch info
try try
{ {
NexusResponseModel response = await this.Client NexusMod mod = await this.Client.GetModAsync(nexusID);
.GetAsync(string.Format(this.ModUrlFormat, id)) return mod != null
.As<NexusResponseModel>(); ? new ModInfoModel(mod.Name, this.NormaliseVersion(mod.Version), mod.Url)
return response != null
? new ModInfoModel(response.Name, this.NormaliseVersion(response.Version), response.Url)
: new ModInfoModel("Found no mod with this ID."); : new ModInfoModel("Found no mod with this ID.");
} }
catch (Exception ex) catch (Exception ex)
@ -64,26 +54,5 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
{ {
this.Client.Dispose(); 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; }
}
} }
} }

View File

@ -11,7 +11,7 @@
"IIS Express": { "IIS Express": {
"commandName": "IISExpress", "commandName": "IISExpress",
"launchBrowser": true, "launchBrowser": true,
"launchUrl": "log", "launchUrl": "",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

View File

@ -11,6 +11,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.6.0" /> <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" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.0.0" />

View File

@ -7,6 +7,10 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using StardewModdingAPI.Web.Framework; 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.ConfigModels;
using StardewModdingAPI.Web.Framework.RewriteRules; using StardewModdingAPI.Web.Framework.RewriteRules;
@ -41,9 +45,10 @@ namespace StardewModdingAPI.Web
/// <param name="services">The service injection container.</param> /// <param name="services">The service injection container.</param>
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
// init configuration
services services
.Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck")) .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))) .Configure<RouteOptions>(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint)))
.AddMemoryCache() .AddMemoryCache()
.AddMvc() .AddMvc()
@ -53,6 +58,41 @@ namespace StardewModdingAPI.Web
options.SerializerSettings.Formatting = Formatting.Indented; options.SerializerSettings.Formatting = Formatting.Indented;
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; 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> /// <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 != "localhost"
&& (req.Host.Host.StartsWith("api.") || req.Host.Host.StartsWith("log.")) && (req.Host.Host.StartsWith("api.") || req.Host.Host.StartsWith("log."))
&& !req.Path.StartsWithSegments("/content") && !req.Path.StartsWithSegments("/content")
&& !req.Path.StartsWithSegments("/favicon.ico")
)) ))
// shortcut redirects // shortcut redirects
.Add(new RedirectToUrlRule("^/docs$", "https://stardewvalleywiki.com/Modding:Index")) .Add(new RedirectToUrlRule("^/docs$", "https://stardewvalleywiki.com/Modding:Index"))
.Add(new RedirectToUrlRule("^/install$", "https://stardewvalleywiki.com/Modding:Installing_SMAPI"))
) )
.UseStaticFiles() // wwwroot folder .UseStaticFiles() // wwwroot folder
.UseMvc(); .UseMvc();

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -1,3 +1,7 @@
@using Microsoft.Extensions.Options
@using StardewModdingAPI.Web.Framework.ConfigModels
@inject IOptions<ContextConfig> ContextConfig
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@ -10,9 +14,9 @@
<div id="sidebar"> <div id="sidebar">
<h4>SMAPI</h4> <h4>SMAPI</h4>
<ul> <ul>
<li><a href="https://stardewvalleywiki.com/Modding:Index">FAQs & guides</a></li> <li><a href="@ContextConfig.Value.RootUrl">About SMAPI</a></li>
<li><a href="https://github.com/pathoschild/SMAPI/releases">Download SMAPI</a></li> <li><a href="@ContextConfig.Value.LogParserUrl">Log parser</a></li>
<li><a href="https://discord.gg/stardewvalley">Get help on Discord</a></li> <li><a href="https://stardewvalleywiki.com/Modding:Index">Docs</a></li>
</ul> </ul>
</div> </div>
<div id="content-column"> <div id="content-column">

View File

@ -16,12 +16,14 @@
"Microsoft": "Information" "Microsoft": "Information"
} }
}, },
"ModUpdateCheck": { "Context": {
"GitHubUsername": null, "RootUrl": "http://localhost:59482/",
"GitHubPassword": null "LogParserUrl": "http://localhost:59482/log/"
}, },
"LogParser": { "ApiClients": {
"SectionUrl": "http://localhost:59482/log/", "GitHubUsername": null,
"GitHubPassword": null,
"PastebinUserKey": null, "PastebinUserKey": null,
"PastebinDevKey": null "PastebinDevKey": null
} }

View File

@ -13,33 +13,37 @@
"Default": "Warning" "Default": "Warning"
} }
}, },
"ModUpdateCheck": { "Context": {
"CacheMinutes": 60, "RootUrl": null, // see top note
"SemanticVersionRegex": "^(?>(?<major>0|[1-9]\\d*))\\.(?>(?<minor>0|[1-9]\\d*))(?>(?:\\.(?<patch>0|[1-9]\\d*))?)(?:-(?<prerelease>(?>[a-z0-9]+[\\-\\.]?)+))?$", "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", "ChucklefishBaseUrl": "https://community.playstarbound.com",
"ChucklefishModPageUrlFormat": "resources/{0}", "ChucklefishModPageUrlFormat": "resources/{0}",
"GitHubKey": "GitHub",
"GitHubUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)",
"GitHubBaseUrl": "https://api.github.com", "GitHubBaseUrl": "https://api.github.com",
"GitHubReleaseUrlFormat": "repos/{0}/releases/latest", "GitHubReleaseUrlFormat": "repos/{0}/releases/latest",
"GitHubAcceptHeader": "application/vnd.github.v3+json", "GitHubAcceptHeader": "application/vnd.github.v3+json",
"GitHubUsername": null, // see top note "GitHubUsername": null, // see top note
"GitHubPassword": null, // see top note "GitHubPassword": null, // see top note
"NexusKey": "Nexus",
"NexusUserAgent": "Nexus Client v0.63.15", "NexusUserAgent": "Nexus Client v0.63.15",
"NexusBaseUrl": "http://www.nexusmods.com/stardewvalley", "NexusBaseUrl": "http://www.nexusmods.com/stardewvalley",
"NexusModUrlFormat": "mods/{0}" "NexusModUrlFormat": "mods/{0}",
},
"LogParser": {
"SectionUrl": null, // see top note
"PastebinBaseUrl": "https://pastebin.com/", "PastebinBaseUrl": "https://pastebin.com/",
"PastebinUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)",
"PastebinUserKey": null, // see top note "PastebinUserKey": null, // see top note
"PastebinDevKey": 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"
} }
} }

View File

@ -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;
}

View File

@ -29,7 +29,7 @@ namespace StardewModdingAPI
** Public ** Public
****/ ****/
/// <summary>SMAPI's current semantic version.</summary> /// <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> /// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30");

View File

@ -16,7 +16,7 @@ namespace StardewModdingAPI.Events
public SButton Button { get; } public SButton Button { get; }
/// <summary>The current cursor position.</summary> /// <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> /// <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 [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> /// <summary>Whether the input should use tools on the affected tile.</summary>
public bool IsUseToolButton { get; } public bool IsUseToolButton { get; }
/// <summary>Whether a mod has indicated the key was already handled.</summary>
public bool IsSuppressed { get; private set; }
/********* /*********
** Public methods ** Public methods
@ -55,6 +58,9 @@ namespace StardewModdingAPI.Events
/// <param name="button">The button to suppress.</param> /// <param name="button">The button to suppress.</param>
public void SuppressButton(SButton button) public void SuppressButton(SButton button)
{ {
if (button == this.Button)
this.IsSuppressed = true;
// keyboard // keyboard
if (button.TryGetKeyboard(out Keys key)) if (button.TryGetKeyboard(out Keys key))
Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Union(new[] { key }).ToArray()); Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Union(new[] { key }).ToArray());

View File

@ -33,6 +33,9 @@ namespace StardewModdingAPI.Events
/// <summary>Raised every 60th tick (≈once per second).</summary> /// <summary>Raised every 60th tick (≈once per second).</summary>
public static event EventHandler OneSecondTick; 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 ** Internal methods
@ -92,5 +95,12 @@ namespace StardewModdingAPI.Events
{ {
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList()); 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());
}
} }
} }

View File

@ -57,14 +57,14 @@ namespace StardewModdingAPI.Framework.Content
public ContentCache(LocalizedContentManager contentManager, Reflector reflection, char[] possiblePathSeparators, string preferredPathSeparator) public ContentCache(LocalizedContentManager contentManager, Reflector reflection, char[] possiblePathSeparators, string preferredPathSeparator)
{ {
// init // 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.PossiblePathSeparators = possiblePathSeparators;
this.PreferredPathSeparator = preferredPathSeparator; this.PreferredPathSeparator = preferredPathSeparator;
// get key normalisation logic // get key normalisation logic
if (Constants.TargetPlatform == Platform.Windows) 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); this.NormaliseAssetNameForPlatform = path => method.Invoke<string>(path);
} }
else else

View File

@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework
/// <param name="severity">How deprecated the code is.</param> /// <param name="severity">How deprecated the code is.</param>
public void Warn(string nounPhrase, string version, DeprecationLevel severity) 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> /// <summary>Log a deprecation warning.</summary>
@ -52,7 +52,7 @@ namespace StardewModdingAPI.Framework
return; return;
// build message // 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) if (source == null)
message += $"{Environment.NewLine}{Environment.StackTrace}"; 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> /// <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) 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> /// <summary>Mark a deprecation warning as already logged.</summary>

View File

@ -30,6 +30,9 @@ namespace StardewModdingAPI.Framework
/// <summary>The mod instance (if it was loaded).</summary> /// <summary>The mod instance (if it was loaded).</summary>
IMod Mod { get; } IMod Mod { get; }
/// <summary>The mod-provided API (if any).</summary>
object Api { get; }
/********* /*********
** Public methods ** Public methods
@ -43,5 +46,9 @@ namespace StardewModdingAPI.Framework
/// <summary>Set the mod instance.</summary> /// <summary>Set the mod instance.</summary>
/// <param name="mod">The mod instance to set.</param> /// <param name="mod">The mod instance to set.</param>
IModMetadata SetMod(IMod mod); IModMetadata SetMod(IMod mod);
/// <summary>Set the mod-provided API instance.</summary>
/// <param name="api">The mod-provided API.</param>
IModMetadata SetApi(object api);
} }
} }

View File

@ -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 ** Sprite batch
****/ ****/
@ -125,7 +134,7 @@ namespace StardewModdingAPI.Framework
#endif #endif
// get result // get result
return reflection.GetPrivateField<bool>(Game1.spriteBatch, fieldName).GetValue(); return reflection.GetField<bool>(Game1.spriteBatch, fieldName).GetValue();
} }
} }
} }

View File

@ -1,4 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using StardewModdingAPI.Framework.Reflection;
namespace StardewModdingAPI.Framework.ModHelpers namespace StardewModdingAPI.Framework.ModHelpers
{ {
@ -11,6 +13,15 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>The underlying mod registry.</summary> /// <summary>The underlying mod registry.</summary>
private readonly ModRegistry Registry; 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 ** Public methods
@ -18,16 +29,20 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="modID">The unique ID of the relevant mod.</param> /// <param name="modID">The unique ID of the relevant mod.</param>
/// <param name="registry">The underlying mod registry.</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) : base(modID)
{ {
this.Registry = registry; this.Registry = registry;
this.ProxyBuilder = proxyBuilder;
this.Monitor = monitor;
} }
/// <summary>Get metadata for all loaded mods.</summary> /// <summary>Get metadata for all loaded mods.</summary>
public IEnumerable<IManifest> GetAll() public IEnumerable<IManifest> GetAll()
{ {
return this.Registry.GetAll(); return this.Registry.GetAll().Select(p => p.Manifest);
} }
/// <summary>Get metadata for a loaded mod.</summary> /// <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> /// <returns>Returns the matching mod's metadata, or <c>null</c> if not found.</returns>
public IManifest Get(string uniqueID) 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> /// <summary>Get whether a mod has been loaded.</summary>
/// <param name="uniqueID">The mod's unique ID.</param> /// <param name="uniqueID">The mod's unique ID.</param>
public bool IsLoaded(string uniqueID) 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);
} }
} }
} }

View File

@ -17,6 +17,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>The mod name for error messages.</summary> /// <summary>The mod name for error messages.</summary>
private readonly string ModName; private readonly string ModName;
/// <summary>Manages deprecation warnings.</summary>
private readonly DeprecationManager DeprecationManager;
/********* /*********
** Public methods ** Public methods
@ -25,15 +28,88 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="modID">The unique ID of the relevant mod.</param> /// <param name="modID">The unique ID of the relevant mod.</param>
/// <param name="modName">The mod name for error messages.</param> /// <param name="modName">The mod name for error messages.</param>
/// <param name="reflector">The underlying reflection helper.</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) : base(modID)
{ {
this.ModName = modName; this.ModName = modName;
this.Reflector = reflector; 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> /// <summary>Get a private instance field.</summary>
/// <typeparam name="TValue">The field type.</typeparam> /// <typeparam name="TValue">The field type.</typeparam>
@ -41,11 +117,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="name">The field name.</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 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> /// <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) public IPrivateField<TValue> GetPrivateField<TValue>(object obj, string name, bool required = true)
{ {
return this.AssertAccessAllowed( this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
this.Reflector.GetPrivateField<TValue>(obj, name, required) return (IPrivateField<TValue>)this.GetField<TValue>(obj, name, required);
);
} }
/// <summary>Get a private static field.</summary> /// <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="type">The type which has the field.</param>
/// <param name="name">The field name.</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 private field is not found.</param>
[Obsolete]
public IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true) public IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true)
{ {
return this.AssertAccessAllowed( this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
this.Reflector.GetPrivateField<TValue>(type, name, required) return (IPrivateField<TValue>)this.GetField<TValue>(type, name, required);
);
} }
/****
** Properties
****/
/// <summary>Get a private instance property.</summary> /// <summary>Get a private instance property.</summary>
/// <typeparam name="TValue">The property type.</typeparam> /// <typeparam name="TValue">The property type.</typeparam>
/// <param name="obj">The object which has the property.</param> /// <param name="obj">The object which has the property.</param>
/// <param name="name">The property name.</param> /// <param name="name">The property name.</param>
/// <param name="required">Whether to throw an exception if the private property is not found.</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) public IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true)
{ {
return this.AssertAccessAllowed( this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
this.Reflector.GetPrivateProperty<TValue>(obj, name, required) return (IPrivateProperty<TValue>)this.GetProperty<TValue>(obj, name, required);
);
} }
/// <summary>Get a private static property.</summary> /// <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="type">The type which has the property.</param>
/// <param name="name">The property name.</param> /// <param name="name">The property name.</param>
/// <param name="required">Whether to throw an exception if the private property is not found.</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) public IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true)
{ {
return this.AssertAccessAllowed( this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
this.Reflector.GetPrivateProperty<TValue>(type, name, required) 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> /// <summary>Get the value of a private instance field.</summary>
/// <typeparam name="TValue">The field type.</typeparam> /// <typeparam name="TValue">The field type.</typeparam>
/// <param name="obj">The object which has the field.</param> /// <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"/>. /// 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. /// 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> /// </remarks>
[Obsolete]
public TValue GetPrivateValue<TValue>(object obj, string name, bool required = true) 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 return field != null
? field.GetValue() ? field.GetValue()
: default(TValue); : 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"/>. /// 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. /// 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> /// </remarks>
[Obsolete]
public TValue GetPrivateValue<TValue>(Type type, string name, bool required = true) 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 return field != null
? field.GetValue() ? field.GetValue()
: default(TValue); : default(TValue);
} }
/****
** Methods
****/
/// <summary>Get a private instance method.</summary> /// <summary>Get a private instance method.</summary>
/// <param name="obj">The object which has the method.</param> /// <param name="obj">The object which has the method.</param>
/// <param name="name">The field name.</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 private field is not found.</param>
[Obsolete]
public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true)
{ {
return this.AssertAccessAllowed( this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
this.Reflector.GetPrivateMethod(obj, name, required) return (IPrivateMethod)this.GetMethod(obj, name, required);
);
} }
/// <summary>Get a private static method.</summary> /// <summary>Get a private static method.</summary>
/// <param name="type">The type which has the method.</param> /// <param name="type">The type which has the method.</param>
/// <param name="name">The field name.</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 private field is not found.</param>
[Obsolete]
public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true)
{ {
return this.AssertAccessAllowed( this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
this.Reflector.GetPrivateMethod(type, name, required) return (IPrivateMethod)this.GetMethod(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)
);
} }
@ -187,7 +230,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <typeparam name="T">The field value type.</typeparam> /// <typeparam name="T">The field value type.</typeparam>
/// <param name="field">The field being accessed.</param> /// <param name="field">The field being accessed.</param>
/// <returns>Returns the same field instance for convenience.</returns> /// <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); this.AssertAccessAllowed(field?.FieldInfo);
return field; return field;
@ -197,7 +240,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <typeparam name="T">The property value type.</typeparam> /// <typeparam name="T">The property value type.</typeparam>
/// <param name="property">The property being accessed.</param> /// <param name="property">The property being accessed.</param>
/// <returns>Returns the same property instance for convenience.</returns> /// <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); this.AssertAccessAllowed(property?.PropertyInfo);
return property; 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> /// <summary>Assert that mods can use the reflection helper to access the given member.</summary>
/// <param name="method">The method being accessed.</param> /// <param name="method">The method being accessed.</param>
/// <returns>Returns the same method instance for convenience.</returns> /// <returns>Returns the same method instance for convenience.</returns>
private IPrivateMethod AssertAccessAllowed(IPrivateMethod method) private IReflectedMethod AssertAccessAllowed(IReflectedMethod method)
{ {
this.AssertAccessAllowed(method?.MethodInfo); this.AssertAccessAllowed(method?.MethodInfo);
return method; return method;

View File

@ -29,6 +29,9 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>The mod instance (if it was loaded).</summary> /// <summary>The mod instance (if it was loaded).</summary>
public IMod Mod { get; private set; } public IMod Mod { get; private set; }
/// <summary>The mod-provided API (if any).</summary>
public object Api { get; private set; }
/********* /*********
** Public methods ** Public methods
@ -64,5 +67,13 @@ namespace StardewModdingAPI.Framework.ModLoading
this.Mod = mod; this.Mod = mod;
return this; 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;
}
} }
} }

View File

@ -15,26 +15,34 @@ namespace StardewModdingAPI.Framework
/// <summary>The registered mod data.</summary> /// <summary>The registered mod data.</summary>
private readonly List<IModMetadata> Mods = new List<IModMetadata>(); private readonly List<IModMetadata> Mods = new List<IModMetadata>();
/// <summary>The friendly mod names treated as deprecation warning sources (assembly full name => mod name).</summary> /// <summary>An assembly full name => mod lookup.</summary>
private readonly IDictionary<string, string> ModNamesByAssembly = new Dictionary<string, string>(); 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 ** Public methods
*********/ *********/
/**** /// <summary>Register a mod as a possible source of deprecation warnings.</summary>
** Basic metadata /// <param name="metadata">The mod metadata.</param>
****/ public void Add(IModMetadata metadata)
/// <summary>Get metadata for all loaded mods.</summary>
public IEnumerable<IManifest> GetAll()
{ {
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> /// <summary>Get metadata for a loaded mod.</summary>
/// <param name="uniqueID">The mod's unique ID.</param> /// <param name="uniqueID">The mod's unique ID.</param>
/// <returns>Returns the matching mod's metadata, or <c>null</c> if not found.</returns> /// <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 // normalise search ID
if (string.IsNullOrWhiteSpace(uniqueID)) if (string.IsNullOrWhiteSpace(uniqueID))
@ -42,37 +50,13 @@ namespace StardewModdingAPI.Framework
uniqueID = uniqueID.Trim(); uniqueID = uniqueID.Trim();
// find match // 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> /// <summary>Get the mod metadata from one of its assemblies.</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>
/// <param name="type">The type to check.</param> /// <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> /// <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 // null
if (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> /// <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> /// <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 // get stack frames
StackTrace stack = new StackTrace(); StackTrace stack = new StackTrace();
@ -101,9 +85,9 @@ namespace StardewModdingAPI.Framework
foreach (StackFrame frame in frames) foreach (StackFrame frame in frames)
{ {
MethodBase method = frame.GetMethod(); MethodBase method = frame.GetMethod();
string name = this.GetModFrom(method.ReflectedType); IModMetadata mod = this.GetFrom(method.ReflectedType);
if (name != null) if (mod != null)
return name; return mod;
} }
// no known assembly found // no known assembly found

View File

@ -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);
}
}
}
}

View File

@ -1,11 +1,11 @@
using System; using System;
using System.Reflection; using System.Reflection;
namespace StardewModdingAPI.Framework.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> /// <typeparam name="TValue">The field value type.</typeparam>
internal class PrivateField<TValue> : IPrivateField<TValue> internal class ReflectedField<TValue> : IPrivateField<TValue>, IReflectedField<TValue>
{ {
/********* /*********
** Properties ** Properties
@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework.Reflection
/// <param name="isStatic">Whether the field is static.</param> /// <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="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> /// <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 // validate
if (parentType == null) if (parentType == null)
@ -64,11 +64,11 @@ namespace StardewModdingAPI.Framework.Reflection
} }
catch (InvalidCastException) 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) 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) 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) 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);
} }
} }
} }

View File

@ -3,8 +3,8 @@ using System.Reflection;
namespace StardewModdingAPI.Framework.Reflection namespace StardewModdingAPI.Framework.Reflection
{ {
/// <summary>A private method obtained through reflection.</summary> /// <summary>A method obtained through reflection.</summary>
internal class PrivateMethod : IPrivateMethod internal class ReflectedMethod : IPrivateMethod, IReflectedMethod
{ {
/********* /*********
** Properties ** Properties
@ -33,10 +33,10 @@ namespace StardewModdingAPI.Framework.Reflection
/// <param name="parentType">The type that has the method.</param> /// <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="obj">The object that has the instance method(if applicable).</param>
/// <param name="method">The reflection metadata.</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="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> /// <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 // validate
if (parentType == null) if (parentType == null)
@ -67,7 +67,7 @@ namespace StardewModdingAPI.Framework.Reflection
} }
catch (Exception ex) 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 // cast return value
@ -77,7 +77,7 @@ namespace StardewModdingAPI.Framework.Reflection
} }
catch (InvalidCastException) 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) 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);
} }
} }
} }

View File

@ -3,9 +3,9 @@ using System.Reflection;
namespace StardewModdingAPI.Framework.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> /// <typeparam name="TValue">The property value type.</typeparam>
internal class PrivateProperty<TValue> : IPrivateProperty<TValue> internal class ReflectedProperty<TValue> : IPrivateProperty<TValue>, IReflectedProperty<TValue>
{ {
/********* /*********
** Properties ** Properties
@ -14,10 +14,10 @@ namespace StardewModdingAPI.Framework.Reflection
private readonly string DisplayName; private readonly string DisplayName;
/// <summary>The underlying property getter.</summary> /// <summary>The underlying property getter.</summary>
private readonly Func<TValue> GetterDelegate; private readonly Func<TValue> GetMethod;
/// <summary>The underlying property setter.</summary> /// <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 ** Public methods
*********/ *********/
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="parentType">The type that has the field.</param> /// <param name="parentType">The type that has the property.</param>
/// <param name="obj">The object that has the instance field (if applicable).</param> /// <param name="obj">The object that has the instance property (if applicable).</param>
/// <param name="property">The reflection metadata.</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="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> /// <exception cref="ArgumentException">The <paramref name="obj"/> is null for a non-static property, or not null for a static property.</exception>
public PrivateProperty(Type parentType, object obj, PropertyInfo property, bool isStatic) public ReflectedProperty(Type parentType, object obj, PropertyInfo property, bool isStatic)
{ {
// validate input // validate input
if (parentType == null) if (parentType == null)
@ -55,24 +55,29 @@ namespace StardewModdingAPI.Framework.Reflection
this.DisplayName = $"{parentType.FullName}::{property.Name}"; this.DisplayName = $"{parentType.FullName}::{property.Name}";
this.PropertyInfo = property; this.PropertyInfo = property;
this.GetterDelegate = (Func<TValue>)Delegate.CreateDelegate(typeof(Func<TValue>), obj, this.PropertyInfo.GetMethod); if (this.PropertyInfo.GetMethod != null)
this.SetterDelegate = (Action<TValue>)Delegate.CreateDelegate(typeof(Action<TValue>), obj, this.PropertyInfo.SetMethod); 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> /// <summary>Get the property value.</summary>
public TValue GetValue() public TValue GetValue()
{ {
if (this.GetMethod == null)
throw new InvalidOperationException($"The {this.DisplayName} property has no get method.");
try try
{ {
return this.GetterDelegate(); return this.GetMethod();
} }
catch (InvalidCastException) 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) 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> //// <param name="value">The value to set.</param>
public void SetValue(TValue value) public void SetValue(TValue value)
{ {
if (this.SetMethod == null)
throw new InvalidOperationException($"The {this.DisplayName} property has no set method.");
try try
{ {
this.SetterDelegate(value); this.SetMethod(value);
} }
catch (InvalidCastException) 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) 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);
} }
} }
} }

View File

@ -5,7 +5,7 @@ using System.Runtime.Caching;
namespace StardewModdingAPI.Framework.Reflection 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> /// <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 internal class Reflector
{ {
@ -25,139 +25,139 @@ namespace StardewModdingAPI.Framework.Reflection
/**** /****
** Fields ** Fields
****/ ****/
/// <summary>Get a private instance field.</summary> /// <summary>Get a instance field.</summary>
/// <typeparam name="TValue">The field type.</typeparam> /// <typeparam name="TValue">The field type.</typeparam>
/// <param name="obj">The object which has the field.</param> /// <param name="obj">The object which has the field.</param>
/// <param name="name">The field name.</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> /// <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 // validate
if (obj == null) 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 // 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) 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; return field;
} }
/// <summary>Get a private static field.</summary> /// <summary>Get a static field.</summary>
/// <typeparam name="TValue">The field type.</typeparam> /// <typeparam name="TValue">The field type.</typeparam>
/// <param name="type">The type which has the field.</param> /// <param name="type">The type which has the field.</param>
/// <param name="name">The field name.</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>
public IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true) public IReflectedField<TValue> GetField<TValue>(Type type, string name, bool required = true)
{ {
// get field from hierarchy // 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) 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; return field;
} }
/**** /****
** Properties ** Properties
****/ ****/
/// <summary>Get a private instance property.</summary> /// <summary>Get a instance property.</summary>
/// <typeparam name="TValue">The property type.</typeparam> /// <typeparam name="TValue">The property type.</typeparam>
/// <param name="obj">The object which has the property.</param> /// <param name="obj">The object which has the property.</param>
/// <param name="name">The property name.</param> /// <param name="name">The property name.</param>
/// <param name="required">Whether to throw an exception if the private property is not found.</param> /// <param name="required">Whether to throw an exception if the property is not found.</param>
public IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true) public IReflectedProperty<TValue> GetProperty<TValue>(object obj, string name, bool required = true)
{ {
// validate // validate
if (obj == null) 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 // 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) 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; return property;
} }
/// <summary>Get a private static property.</summary> /// <summary>Get a static property.</summary>
/// <typeparam name="TValue">The property type.</typeparam> /// <typeparam name="TValue">The property type.</typeparam>
/// <param name="type">The type which has the property.</param> /// <param name="type">The type which has the property.</param>
/// <param name="name">The property name.</param> /// <param name="name">The property name.</param>
/// <param name="required">Whether to throw an exception if the private property is not found.</param> /// <param name="required">Whether to throw an exception if the property is not found.</param>
public IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true) public IReflectedProperty<TValue> GetProperty<TValue>(Type type, string name, bool required = true)
{ {
// get field from hierarchy // 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) 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; return property;
} }
/**** /****
** Methods ** 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="obj">The object which has the method.</param>
/// <param name="name">The field name.</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>
public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) public IReflectedMethod GetMethod(object obj, string name, bool required = true)
{ {
// validate // validate
if (obj == null) 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 // 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) 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; 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="type">The type which has the method.</param>
/// <param name="name">The field name.</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>
public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) public IReflectedMethod GetMethod(Type type, string name, bool required = true)
{ {
// get method from hierarchy // 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) 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; return method;
} }
/**** /****
** Methods by signature ** 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="obj">The object which has the method.</param>
/// <param name="name">The field name.</param> /// <param name="name">The field name.</param>
/// <param name="argumentTypes">The argument types of the method signature to find.</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> /// <param name="required">Whether to throw an exception if the field is not found.</param>
public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) public IReflectedMethod GetMethod(object obj, string name, Type[] argumentTypes, bool required = true)
{ {
// validate parent // validate parent
if (obj == null) 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 // 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) 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; 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="type">The type which has the method.</param>
/// <param name="name">The field name.</param> /// <param name="name">The field name.</param>
/// <param name="argumentTypes">The argument types of the method signature to find.</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> /// <param name="required">Whether to throw an exception if the field is not found.</param>
public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) public IReflectedMethod GetMethod(Type type, string name, Type[] argumentTypes, bool required = true)
{ {
// get field from hierarchy // 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) 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; return method;
} }
@ -171,7 +171,7 @@ namespace StardewModdingAPI.Framework.Reflection
/// <param name="obj">The object which has the field.</param> /// <param name="obj">The object which has the field.</param>
/// <param name="name">The field name.</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> /// <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); bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
FieldInfo field = this.GetCached<FieldInfo>($"field::{isStatic}::{type.FullName}::{name}", () => FieldInfo field = this.GetCached<FieldInfo>($"field::{isStatic}::{type.FullName}::{name}", () =>
@ -183,7 +183,7 @@ namespace StardewModdingAPI.Framework.Reflection
}); });
return field != null return field != null
? new PrivateField<TValue>(type, obj, field, isStatic) ? new ReflectedField<TValue>(type, obj, field, isStatic)
: null; : null;
} }
@ -193,7 +193,7 @@ namespace StardewModdingAPI.Framework.Reflection
/// <param name="obj">The object which has the property.</param> /// <param name="obj">The object which has the property.</param>
/// <param name="name">The property name.</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> /// <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); bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
PropertyInfo property = this.GetCached<PropertyInfo>($"property::{isStatic}::{type.FullName}::{name}", () => PropertyInfo property = this.GetCached<PropertyInfo>($"property::{isStatic}::{type.FullName}::{name}", () =>
@ -205,7 +205,7 @@ namespace StardewModdingAPI.Framework.Reflection
}); });
return property != null return property != null
? new PrivateProperty<TValue>(type, obj, property, isStatic) ? new ReflectedProperty<TValue>(type, obj, property, isStatic)
: null; : null;
} }
@ -214,7 +214,7 @@ namespace StardewModdingAPI.Framework.Reflection
/// <param name="obj">The object which has the method.</param> /// <param name="obj">The object which has the method.</param>
/// <param name="name">The method name.</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> /// <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); bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () => MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () =>
@ -226,7 +226,7 @@ namespace StardewModdingAPI.Framework.Reflection
}); });
return method != null return method != null
? new PrivateMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) ? new ReflectedMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static))
: null; : null;
} }
@ -236,7 +236,7 @@ namespace StardewModdingAPI.Framework.Reflection
/// <param name="name">The method name.</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> /// <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> /// <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); bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}({string.Join(",", argumentTypes.Select(p => p.FullName))})", () => 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 methodInfo;
}); });
return method != null return method != null
? new PrivateMethod(type, obj, method, isStatic) ? new ReflectedMethod(type, obj, method, isStatic)
: null; : null;
} }

View File

@ -45,7 +45,7 @@ namespace StardewModdingAPI.Framework
private readonly ContentCache Cache; private readonly ContentCache Cache;
/// <summary>The private <see cref="LocalizedContentManager"/> method which generates the locale portion of an asset name.</summary> /// <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> /// <summary>The language codes used in asset keys.</summary>
private readonly IDictionary<string, LanguageCode> KeyLocales; private readonly IDictionary<string, LanguageCode> KeyLocales;
@ -101,7 +101,7 @@ namespace StardewModdingAPI.Framework
// init // init
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
this.Cache = new ContentCache(this, reflection, SContentManager.PossiblePathSeparators, SContentManager.PreferredPathSeparator); 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); this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath);
// get asset data // get asset data
@ -205,7 +205,7 @@ namespace StardewModdingAPI.Framework
return this.LoadImpl<T>(assetName, instance); return this.LoadImpl<T>(assetName, instance);
// load mod content // 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 try
{ {
return this.WithWriteLock(() => return this.WithWriteLock(() =>
@ -252,6 +252,8 @@ namespace StardewModdingAPI.Framework
} }
catch (Exception ex) when (!(ex is SContentLoadException)) 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); 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) private IDictionary<string, LanguageCode> GetKeyLocales(Reflector reflection)
{ {
// get the private code field directly to avoid changed-code logic // 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 // remember previous settings
LanguageCode previousCode = codeField.GetValue(); LanguageCode previousCode = codeField.GetValue();

View File

@ -133,20 +133,20 @@ namespace StardewModdingAPI.Framework
// ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming // ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming
/// <summary>Used to access private fields and methods.</summary> /// <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 List<float> _fpsList => SGame.Reflection.GetField<List<float>>(typeof(Game1), nameof(_fpsList)).GetValue();
private static Stopwatch _fpsStopwatch => SGame.Reflection.GetPrivateField<Stopwatch>(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue(); private static Stopwatch _fpsStopwatch => SGame.Reflection.GetField<Stopwatch>(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue();
private static float _fps 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 static Task _newDayTask => SGame.Reflection.GetField<Task>(typeof(Game1), nameof(_newDayTask)).GetValue();
private Color bgColor => SGame.Reflection.GetPrivateField<Color>(this, nameof(bgColor)).GetValue(); private Color bgColor => SGame.Reflection.GetField<Color>(this, nameof(bgColor)).GetValue();
public RenderTarget2D screenWrapper => SGame.Reflection.GetPrivateProperty<RenderTarget2D>(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop public RenderTarget2D screenWrapper => SGame.Reflection.GetProperty<RenderTarget2D>(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop
public BlendState lightingBlend => SGame.Reflection.GetPrivateField<BlendState>(this, nameof(lightingBlend)).GetValue(); public BlendState lightingBlend => SGame.Reflection.GetField<BlendState>(this, nameof(lightingBlend)).GetValue();
private readonly Action drawFarmBuildings = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke(); private readonly Action drawFarmBuildings = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke();
private readonly Action drawHUD = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawHUD)).Invoke(); private readonly Action drawHUD = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawHUD)).Invoke();
private readonly Action drawDialogueBox = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke(); private readonly Action drawDialogueBox = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke();
private readonly Action renderScreenBuffer = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(); private readonly Action renderScreenBuffer = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke();
// ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming // 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.SContentManager = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor, reflection);
this.Content = new ContentManagerShim(this.SContentManager, "SGame.Content"); this.Content = new ContentManagerShim(this.SContentManager, "SGame.Content");
Game1.content = new ContentManagerShim(this.SContentManager, "Game1.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 ** Update events
*********/ *********/
GameEvents.InvokeUpdateTick(this.Monitor);
if (this.FirstUpdate) if (this.FirstUpdate)
{
this.FirstUpdate = false; this.FirstUpdate = false;
GameEvents.InvokeFirstUpdateTick(this.Monitor);
}
GameEvents.InvokeUpdateTick(this.Monitor);
if (this.CurrentUpdateTick % 2 == 0) if (this.CurrentUpdateTick % 2 == 0)
GameEvents.InvokeSecondUpdateTick(this.Monitor); GameEvents.InvokeSecondUpdateTick(this.Monitor);
if (this.CurrentUpdateTick % 4 == 0) 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); 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(); activeClickableMenu.exitThisMenu();
} }
this.RaisePostRender();
Game1.spriteBatch.End(); Game1.spriteBatch.End();
} }
//base.Draw(gameTime); //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); 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(); Game1.activeClickableMenu.exitThisMenu();
} }
this.RaisePostRender();
Game1.spriteBatch.End(); Game1.spriteBatch.End();
if ((double)Game1.options.zoomLevel != 1.0) if ((double)Game1.options.zoomLevel != 1.0)
{ {
@ -721,18 +726,20 @@ 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.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(); Game1.spriteBatch.End();
} }
if (Game1.overlayMenu == null) if (Game1.overlayMenu != null)
return; {
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.overlayMenu.draw(Game1.spriteBatch); Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End(); Game1.spriteBatch.End();
} }
}
else if ((int)Game1.gameMode == 11) else if ((int)Game1.gameMode == 11)
{ {
Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
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.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.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); 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(); Game1.spriteBatch.End();
} }
else if (Game1.currentMinigame != null) 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.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha));
Game1.spriteBatch.End(); Game1.spriteBatch.End();
} }
this.RaisePostRender(needsNewBatch: true);
if ((double)Game1.options.zoomLevel != 1.0) if ((double)Game1.options.zoomLevel != 1.0)
{ {
this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null);
@ -752,12 +760,13 @@ 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.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(); Game1.spriteBatch.End();
} }
if (Game1.overlayMenu == null) if (Game1.overlayMenu != null)
return; {
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.overlayMenu.draw(Game1.spriteBatch); Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End(); Game1.spriteBatch.End();
} }
}
else if (Game1.showingEndOfNightStuff) else if (Game1.showingEndOfNightStuff)
{ {
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
@ -775,6 +784,7 @@ namespace StardewModdingAPI.Framework
Game1.activeClickableMenu.exitThisMenu(); Game1.activeClickableMenu.exitThisMenu();
} }
} }
this.RaisePostRender();
Game1.spriteBatch.End(); Game1.spriteBatch.End();
if ((double)Game1.options.zoomLevel != 1.0) if ((double)Game1.options.zoomLevel != 1.0)
{ {
@ -784,12 +794,13 @@ 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.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(); Game1.spriteBatch.End();
} }
if (Game1.overlayMenu == null) if (Game1.overlayMenu != null)
return; {
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.overlayMenu.draw(Game1.spriteBatch); Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End(); Game1.spriteBatch.End();
} }
}
else if ((int)Game1.gameMode == 6) else if ((int)Game1.gameMode == 6)
{ {
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
@ -806,6 +817,7 @@ namespace StardewModdingAPI.Framework
int x = 64; int x = 64;
int y = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - height; 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); SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str5, -1);
this.RaisePostRender();
Game1.spriteBatch.End(); Game1.spriteBatch.End();
if ((double)Game1.options.zoomLevel != 1.0) if ((double)Game1.options.zoomLevel != 1.0)
{ {
@ -815,12 +827,13 @@ 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.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(); Game1.spriteBatch.End();
} }
if (Game1.overlayMenu == null) if (Game1.overlayMenu != null)
return; {
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.overlayMenu.draw(Game1.spriteBatch); Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End(); Game1.spriteBatch.End();
} }
}
else else
{ {
Microsoft.Xna.Framework.Rectangle rectangle; Microsoft.Xna.Framework.Rectangle rectangle;
@ -1265,6 +1278,8 @@ namespace StardewModdingAPI.Framework
} }
else if (Game1.farmEvent != null) else if (Game1.farmEvent != null)
Game1.farmEvent.drawAboveEverything(Game1.spriteBatch); Game1.farmEvent.drawAboveEverything(Game1.spriteBatch);
this.RaisePostRender();
Game1.spriteBatch.End(); Game1.spriteBatch.End();
if (Game1.overlayMenu != null) if (Game1.overlayMenu != null)
{ {
@ -1272,14 +1287,6 @@ namespace StardewModdingAPI.Framework
Game1.overlayMenu.draw(Game1.spriteBatch); Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End(); 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(); this.renderScreenBuffer();
} }
} }
@ -1401,5 +1408,19 @@ namespace StardewModdingAPI.Framework
hash ^= v.GetHashCode(); hash ^= v.GetHashCode();
return hash; 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();
}
}
} }
} }

View File

@ -1,4 +1,4 @@
namespace StardewModdingAPI namespace StardewModdingAPI
{ {
/// <summary>The implementation for a Stardew Valley mod.</summary> /// <summary>The implementation for a Stardew Valley mod.</summary>
public interface IMod public interface IMod
@ -22,5 +22,8 @@
/// <summary>The mod entry point, called after the mod is first loaded.</summary> /// <summary>The mod entry point, called after the mod is first loaded.</summary>
/// <param name="helper">Provides simplified APIs for writing mods.</param> /// <param name="helper">Provides simplified APIs for writing mods.</param>
void Entry(IModHelper helper); void Entry(IModHelper helper);
/// <summary>Get an API that other mods can access. This is always called after <see cref="Entry"/>.</summary>
object GetApi();
} }
} }

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace StardewModdingAPI namespace StardewModdingAPI
{ {
@ -16,5 +16,14 @@ namespace StardewModdingAPI
/// <summary>Get whether a mod has been loaded.</summary> /// <summary>Get whether a mod has been loaded.</summary>
/// <param name="uniqueID">The mod's unique ID.</param> /// <param name="uniqueID">The mod's unique ID.</param>
bool IsLoaded(string uniqueID); 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;
} }
} }

View File

@ -1,9 +1,11 @@
using System.Reflection; using System;
using System.Reflection;
namespace StardewModdingAPI namespace StardewModdingAPI
{ {
/// <summary>A private field obtained through reflection.</summary> /// <summary>A private field obtained through reflection.</summary>
/// <typeparam name="TValue">The field value type.</typeparam> /// <typeparam name="TValue">The field value type.</typeparam>
[Obsolete("Use " + nameof(IReflectedField<TValue>) + " instead")]
public interface IPrivateField<TValue> public interface IPrivateField<TValue>
{ {
/********* /*********

View File

@ -1,8 +1,10 @@
using System.Reflection; using System;
using System.Reflection;
namespace StardewModdingAPI namespace StardewModdingAPI
{ {
/// <summary>A private method obtained through reflection.</summary> /// <summary>A private method obtained through reflection.</summary>
[Obsolete("Use " + nameof(IReflectedMethod) + " instead")]
public interface IPrivateMethod public interface IPrivateMethod
{ {
/********* /*********

View File

@ -1,9 +1,11 @@
using System.Reflection; using System;
using System.Reflection;
namespace StardewModdingAPI namespace StardewModdingAPI
{ {
/// <summary>A private property obtained through reflection.</summary> /// <summary>A private property obtained through reflection.</summary>
/// <typeparam name="TValue">The property value type.</typeparam> /// <typeparam name="TValue">The property value type.</typeparam>
[Obsolete("Use " + nameof(IPrivateProperty<TValue>) + " instead")]
public interface IPrivateProperty<TValue> public interface IPrivateProperty<TValue>
{ {
/********* /*********

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -1,18 +1,62 @@
using System; using System;
namespace StardewModdingAPI 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 interface IReflectionHelper : IModLinked
{ {
/********* /*********
** Public methods ** 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> /// <summary>Get a private instance field.</summary>
/// <typeparam name="TValue">The field type.</typeparam> /// <typeparam name="TValue">The field type.</typeparam>
/// <param name="obj">The object which has the field.</param> /// <param name="obj">The object which has the field.</param>
/// <param name="name">The field name.</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 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); IPrivateField<TValue> GetPrivateField<TValue>(object obj, string name, bool required = true);
/// <summary>Get a private static field.</summary> /// <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="type">The type which has the field.</param>
/// <param name="name">The field name.</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 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); IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true);
/// <summary>Get a private instance property.</summary> /// <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="obj">The object which has the property.</param>
/// <param name="name">The property name.</param> /// <param name="name">The property name.</param>
/// <param name="required">Whether to throw an exception if the private property is not found.</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); IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true);
/// <summary>Get a private static property.</summary> /// <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="type">The type which has the property.</param>
/// <param name="name">The property name.</param> /// <param name="name">The property name.</param>
/// <param name="required">Whether to throw an exception if the private property is not found.</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); IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true);
/// <summary>Get the value of a private instance field.</summary> /// <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="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 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> /// <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); TValue GetPrivateValue<TValue>(object obj, string name, bool required = true);
/// <summary>Get the value of a private static field.</summary> /// <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="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 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> /// <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); TValue GetPrivateValue<TValue>(Type type, string name, bool required = true);
/// <summary>Get a private instance method.</summary> /// <summary>Get a private instance method.</summary>
/// <param name="obj">The object which has the method.</param> /// <param name="obj">The object which has the method.</param>
/// <param name="name">The field name.</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 private field is not found.</param>
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetMethod) + " instead")]
IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true); IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true);
/// <summary>Get a private static method.</summary> /// <summary>Get a private static method.</summary>
/// <param name="type">The type which has the method.</param> /// <param name="type">The type which has the method.</param>
/// <param name="name">The field name.</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 private field is not found.</param>
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetMethod) + " instead")]
IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true); IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true);
} }
} }

View File

@ -50,7 +50,6 @@ namespace StardewModdingAPI.Metadata
new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize", InstructionHandleResult.NotCompatible), new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize", InstructionHandleResult.NotCompatible),
new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent", InstructionHandleResult.NotCompatible), new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent", InstructionHandleResult.NotCompatible),
new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded", 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", "LoadedGame", InstructionHandleResult.NotCompatible),
new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged", InstructionHandleResult.NotCompatible), new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged", InstructionHandleResult.NotCompatible),
new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged", InstructionHandleResult.NotCompatible), new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged", InstructionHandleResult.NotCompatible),

View File

@ -25,6 +25,9 @@ namespace StardewModdingAPI
/// <param name="helper">Provides simplified APIs for writing mods.</param> /// <param name="helper">Provides simplified APIs for writing mods.</param>
public abstract void Entry(IModHelper helper); 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> /// <summary>Release or reset unmanaged resources.</summary>
public void Dispose() public void Dispose()
{ {

View File

@ -247,7 +247,7 @@ namespace StardewModdingAPI
this.IsDisposed = true; this.IsDisposed = true;
// dispose mod data // dispose mod data
foreach (IModMetadata mod in this.ModRegistry.GetMods()) foreach (IModMetadata mod in this.ModRegistry.GetAll())
{ {
try try
{ {
@ -374,7 +374,7 @@ namespace StardewModdingAPI
} }
// update window titles // 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"; 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"; 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(); LocalizedContentManager.LanguageCode languageCode = this.ContentManager.GetCurrentLanguage();
// update mod translation helpers // 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); (mod.Mod.Helper.Translation as TranslationHelper)?.SetLocale(locale, languageCode);
} }
@ -500,12 +500,11 @@ namespace StardewModdingAPI
{ {
// create client // create client
WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion);
this.Monitor.Log("Checking for updates...", LogLevel.Trace);
// check SMAPI version // check SMAPI version
try try
{ {
this.Monitor.Log("Checking for SMAPI update...", LogLevel.Trace);
ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value; ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value;
if (response.Error != null) if (response.Error != null)
{ {
@ -515,7 +514,7 @@ namespace StardewModdingAPI
else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion)) else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion))
this.Monitor.Log($"You can update SMAPI to {response.Version}: {response.Url}", LogLevel.Alert); this.Monitor.Log($"You can update SMAPI to {response.Version}: {response.Url}", LogLevel.Alert);
else else
this.VerboseLog(" OK."); this.Monitor.Log(" SMAPI okay.", LogLevel.Trace);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -527,21 +526,10 @@ namespace StardewModdingAPI
} }
// check mod versions // check mod versions
if (mods.Any())
{
try try
{ {
// log issues
if (this.Settings.VerboseLogging)
{
this.VerboseLog("Validating mod update keys...");
foreach (IModMetadata mod in mods)
{
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.");
}
}
// prepare update keys // prepare update keys
Dictionary<string, IModMetadata[]> modsByKey = Dictionary<string, IModMetadata[]> modsByKey =
( (
@ -557,8 +545,25 @@ namespace StardewModdingAPI
StringComparer.InvariantCultureIgnoreCase StringComparer.InvariantCultureIgnoreCase
); );
// report 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 // fetch results
this.Monitor.Log($"Checking for updates to {modsByKey.Keys.Count} keys...", LogLevel.Trace);
var results = var results =
( (
from entry in client.GetModInfo(modsByKey.Keys.ToArray()) from entry in client.GetModInfo(modsByKey.Keys.ToArray())
@ -591,7 +596,7 @@ namespace StardewModdingAPI
: info.Version : info.Version
); );
bool isUpdate = latestVersion.IsNewerThan(localVersion); 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")}."); 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 (isUpdate)
{ {
if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version)) if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version))
@ -616,6 +621,7 @@ namespace StardewModdingAPI
: ex.ToString() : ex.ToString()
); );
} }
}
}).Start(); }).Start();
} }
@ -649,6 +655,7 @@ namespace StardewModdingAPI
AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode); AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode);
AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name);
InterfaceProxyBuilder proxyBuilder = new InterfaceProxyBuilder();
foreach (IModMetadata metadata in mods) foreach (IModMetadata metadata in mods)
{ {
// get basic info // get basic info
@ -690,53 +697,30 @@ namespace StardewModdingAPI
continue; 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 // initialise mod
try try
{ {
// get implementation // init mod helpers
TypeInfo modEntryType = modAssembly.DefinedTypes.First(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract);
Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString());
if (mod == null)
{
TrackSkip(metadata, "its entry class couldn't be instantiated.");
continue;
}
// inject data
{
IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName);
IModHelper modHelper;
{
ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager);
IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor);
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager);
IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyBuilder, monitor);
ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage());
modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper);
mod.ModManifest = manifest;
mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper);
mod.Monitor = monitor;
} }
// 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 // track mod
metadata.SetMod(mod); metadata.SetMod(mod);
this.ModRegistry.Add(metadata); 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 // log skipped mods
this.Monitor.Newline(); 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); 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 // invalidate cache entries when needed
@ -846,13 +843,48 @@ namespace StardewModdingAPI
this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace);
this.ContentManager.InvalidateCacheFor(editors, loaders); 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> /// <summary>Reload translations for all mods.</summary>
private void ReloadTranslations() private void ReloadTranslations()
{ {
JsonHelper jsonHelper = new JsonHelper(); JsonHelper jsonHelper = new JsonHelper();
foreach (IModMetadata metadata in this.ModRegistry.GetMods()) foreach (IModMetadata metadata in this.ModRegistry.GetAll())
{ {
// read translation files // read translation files
IDictionary<string, IDictionary<string, string>> translations = new Dictionary<string, IDictionary<string, string>>(); IDictionary<string, IDictionary<string, string>> translations = new Dictionary<string, IDictionary<string, string>>();

View File

@ -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 "ID": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod | Pathoschild.TractorMod", // changed in 3.2, 4.0 beta, and 4.0
"UpdateKeys": [ "Nexus:1401" ] "UpdateKeys": [ "Nexus:1401" ]
}, },
{
// TrainerMod
"ID": "SMAPI.TrainerMod",
"Compatibility": {
"~": {
"Status": "Obsolete",
"ReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer."
}
}
},
{ {
// Tree Transplant // Tree Transplant
"ID": "TreeTransplant", "ID": "TreeTransplant",

View File

@ -79,10 +79,6 @@
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="System.Windows.Forms" Condition="$(OS) == 'Windows_NT'" /> <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" /> <Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -110,7 +106,11 @@
<Compile Include="Framework\ContentManagerShim.cs" /> <Compile Include="Framework\ContentManagerShim.cs" />
<Compile Include="Framework\Exceptions\SAssemblyLoadFailedException.cs" /> <Compile Include="Framework\Exceptions\SAssemblyLoadFailedException.cs" />
<Compile Include="Framework\ModLoading\AssemblyLoadStatus.cs" /> <Compile Include="Framework\ModLoading\AssemblyLoadStatus.cs" />
<Compile Include="Framework\Reflection\InterfaceProxyBuilder.cs" />
<Compile Include="Framework\Utilities\ContextHash.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="Metadata\CoreAssets.cs" />
<Compile Include="ContentSource.cs" /> <Compile Include="ContentSource.cs" />
<Compile Include="Events\ContentEvents.cs" /> <Compile Include="Events\ContentEvents.cs" />
@ -169,7 +169,7 @@
<Compile Include="Framework\Models\ModStatus.cs" /> <Compile Include="Framework\Models\ModStatus.cs" />
<Compile Include="Framework\Models\SConfig.cs" /> <Compile Include="Framework\Models\SConfig.cs" />
<Compile Include="Framework\ModLoading\ModMetadata.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\RequestExitDelegate.cs" />
<Compile Include="Framework\SContentManager.cs" /> <Compile Include="Framework\SContentManager.cs" />
<Compile Include="Framework\Exceptions\SParseException.cs" /> <Compile Include="Framework\Exceptions\SParseException.cs" />
@ -198,8 +198,8 @@
<Compile Include="Framework\Models\ModDataRecord.cs" /> <Compile Include="Framework\Models\ModDataRecord.cs" />
<Compile Include="Framework\ModLoading\AssemblyLoader.cs" /> <Compile Include="Framework\ModLoading\AssemblyLoader.cs" />
<Compile Include="Framework\Reflection\CacheEntry.cs" /> <Compile Include="Framework\Reflection\CacheEntry.cs" />
<Compile Include="Framework\Reflection\PrivateField.cs" /> <Compile Include="Framework\Reflection\ReflectedField.cs" />
<Compile Include="Framework\Reflection\PrivateMethod.cs" /> <Compile Include="Framework\Reflection\ReflectedMethod.cs" />
<Compile Include="Framework\Reflection\Reflector.cs" /> <Compile Include="Framework\Reflection\Reflector.cs" />
<Compile Include="IManifest.cs" /> <Compile Include="IManifest.cs" />
<Compile Include="IMod.cs" /> <Compile Include="IMod.cs" />