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;
[assembly: ComVisible(false)]
[assembly: AssemblyVersion("2.2.0.0")]
[assembly: AssemblyFileVersion("2.2.0.0")]
[assembly: AssemblyVersion("2.3.0.0")]
[assembly: AssemblyFileVersion("2.3.0.0")]

View File

@ -1,4 +1,24 @@
# Release notes
## 2.3
* For players:
* Added a user-friendly [download page](https://smapi.io).
* Improved cryptic libgdiplus errors on Mac when Mono isn't installed.
* Fixed mod UIs hidden when menu backgrounds are enabled.
* For modders:
* **Added mod-provided APIs** to allow simple integrations between mods, even without direct assembly references.
* Added `GameEvents.FirstUpdateTick` event (called once after all mods are initialised).
* Added `IsSuppressed` to input events so mods can optionally avoid handling keys another mod has already handled.
* Added trace message for mods with no update keys.
* Adjusted reflection API to match actual usage (e.g. renamed `GetPrivate*` to `Get*`), and deprecated previous methods.
* Fixed `GraphicsEvents.OnPostRenderEvent` not being raised in some specialised cases.
* Fixed reflection API error for properties missing a `get` and `set`.
* Fixed issue where a mod could change the cursor position reported to other mods.
* Updated compatibility list.
* For the [log parser][]:
* Fixed broken favicon.
## 2.2
* For players:
* Fixed error when a mod loads custom assets on Linux/Mac.

View File

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

View File

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

View File

@ -3,7 +3,7 @@
"Author": "SMAPI",
"Version": {
"MajorVersion": 2,
"MinorVersion": 0,
"MinorVersion": 3,
"PatchVersion": 0,
"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.Extensions.Options;
using StardewModdingAPI.Web.Framework;
using StardewModdingAPI.Web.Framework.Clients.Pastebin;
using StardewModdingAPI.Web.Framework.ConfigModels;
using StardewModdingAPI.Web.Framework.LogParser;
using StardewModdingAPI.Web.ViewModels;
namespace StardewModdingAPI.Web.Controllers
@ -19,10 +19,10 @@ namespace StardewModdingAPI.Web.Controllers
** Properties
*********/
/// <summary>The log parser config settings.</summary>
private readonly LogParserConfig Config;
private readonly ContextConfig Config;
/// <summary>The underlying Pastebin client.</summary>
private readonly PastebinClient PastebinClient;
private readonly IPastebinClient Pastebin;
/// <summary>The first bytes in a valid zip file.</summary>
/// <remarks>See <a href="https://en.wikipedia.org/wiki/Zip_(file_format)#File_headers"/>.</remarks>
@ -36,14 +36,12 @@ namespace StardewModdingAPI.Web.Controllers
** Constructor
***/
/// <summary>Construct an instance.</summary>
/// <param name="configProvider">The log parser config settings.</param>
public LogParserController(IOptions<LogParserConfig> configProvider)
/// <param name="contextProvider">The context config settings.</param>
/// <param name="pastebin">The Pastebin API client.</param>
public LogParserController(IOptions<ContextConfig> contextProvider, IPastebinClient pastebin)
{
// init Pastebin client
this.Config = configProvider.Value;
string version = this.GetType().Assembly.GetName().Version.ToString(3);
string userAgent = string.Format(this.Config.PastebinUserAgent, version);
this.PastebinClient = new PastebinClient(this.Config.PastebinBaseUrl, userAgent, this.Config.PastebinUserKey, this.Config.PastebinDevKey);
this.Config = contextProvider.Value;
this.Pastebin = pastebin;
}
/***
@ -52,12 +50,11 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Render the log parser UI.</summary>
/// <param name="id">The paste ID.</param>
[HttpGet]
[Route("")]
[Route("log")]
[Route("log/{id}")]
public ViewResult Index(string id = null)
{
return this.View("Index", new LogParserModel(this.Config.SectionUrl, id));
return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id));
}
/***
@ -67,9 +64,9 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="id">The Pastebin paste ID.</param>
[HttpGet, Produces("application/json")]
[Route("log/fetch/{id}")]
public async Task<GetPasteResponse> GetAsync(string id)
public async Task<PasteInfo> GetAsync(string id)
{
GetPasteResponse response = await this.PastebinClient.GetAsync(id);
PasteInfo response = await this.Pastebin.GetAsync(id);
response.Content = this.DecompressString(response.Content);
return response;
}
@ -78,10 +75,10 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="content">The log content to save.</param>
[HttpPost, Produces("application/json"), AllowLargePosts]
[Route("log/save")]
public async Task<SavePasteResponse> PostAsync([FromBody] string content)
public async Task<SavePasteResult> PostAsync([FromBody] string content)
{
content = this.CompressString(content);
return await this.PastebinClient.PostAsync(content);
return await this.Pastebin.PostAsync(content);
}

View File

@ -7,6 +7,9 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using StardewModdingAPI.Common.Models;
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
using StardewModdingAPI.Web.Framework.Clients.GitHub;
using StardewModdingAPI.Web.Framework.Clients.Nexus;
using StardewModdingAPI.Web.Framework.ConfigModels;
using StardewModdingAPI.Web.Framework.ModRepositories;
@ -39,39 +42,22 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Construct an instance.</summary>
/// <param name="cache">The cache in which to store mod metadata.</param>
/// <param name="configProvider">The config settings for mod update checks.</param>
public ModsApiController(IMemoryCache cache, IOptions<ModUpdateCheckConfig> configProvider)
/// <param name="chucklefish">The Chucklefish API client.</param>
/// <param name="github">The GitHub API client.</param>
/// <param name="nexus">The Nexus API client.</param>
public ModsApiController(IMemoryCache cache, IOptions<ModUpdateCheckConfig> configProvider, IChucklefishClient chucklefish, IGitHubClient github, INexusClient nexus)
{
ModUpdateCheckConfig config = configProvider.Value;
this.Cache = cache;
this.CacheMinutes = config.CacheMinutes;
this.VersionRegex = config.SemanticVersionRegex;
string version = this.GetType().Assembly.GetName().Version.ToString(3);
this.Repositories =
new IModRepository[]
{
new ChucklefishRepository(
vendorKey: config.ChucklefishKey,
userAgent: string.Format(config.ChucklefishUserAgent, version),
baseUrl: config.ChucklefishBaseUrl,
modPageUrlFormat: config.ChucklefishModPageUrlFormat
),
new GitHubRepository(
vendorKey: config.GitHubKey,
baseUrl: config.GitHubBaseUrl,
releaseUrlFormat: config.GitHubReleaseUrlFormat,
userAgent: string.Format(config.GitHubUserAgent, version),
acceptHeader: config.GitHubAcceptHeader,
username: config.GitHubUsername,
password: config.GitHubPassword
),
new NexusRepository(
vendorKey: config.NexusKey,
userAgent: config.NexusUserAgent,
baseUrl: config.NexusBaseUrl,
modUrlFormat: config.NexusModUrlFormat
)
new ChucklefishRepository(config.ChucklefishKey, chucklefish),
new GitHubRepository(config.GitHubKey, github),
new NexusRepository(config.NexusKey, nexus)
}
.ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase);
}

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>
internal class GetPasteResponse
internal class PasteInfo
{
/// <summary>Whether the log was successfully fetched.</summary>
public bool Success { get; set; }

View File

@ -8,10 +8,10 @@ using System.Threading.Tasks;
using System.Web;
using Pathoschild.Http.Client;
namespace StardewModdingAPI.Web.Framework.LogParser
namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
{
/// <summary>An API client for Pastebin.</summary>
internal class PastebinClient : IDisposable
internal class PastebinClient : IPastebinClient
{
/*********
** Properties
@ -43,7 +43,7 @@ namespace StardewModdingAPI.Web.Framework.LogParser
/// <summary>Fetch a saved paste.</summary>
/// <param name="id">The paste ID.</param>
public async Task<GetPasteResponse> GetAsync(string id)
public async Task<PasteInfo> GetAsync(string id)
{
try
{
@ -54,30 +54,30 @@ namespace StardewModdingAPI.Web.Framework.LogParser
// handle Pastebin errors
if (string.IsNullOrWhiteSpace(content))
return new GetPasteResponse { Error = "Received an empty response from Pastebin." };
return new PasteInfo { Error = "Received an empty response from Pastebin." };
if (content.StartsWith("<!DOCTYPE"))
return new GetPasteResponse { Error = $"Received a captcha challenge from Pastebin. Please visit https://pastebin.com/{id} in a new window to solve it." };
return new GetPasteResponse { Success = true, Content = content };
return new PasteInfo { Error = $"Received a captcha challenge from Pastebin. Please visit https://pastebin.com/{id} in a new window to solve it." };
return new PasteInfo { Success = true, Content = content };
}
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
{
return new GetPasteResponse { Error = "There's no log with that ID." };
return new PasteInfo { Error = "There's no log with that ID." };
}
catch (Exception ex)
{
return new GetPasteResponse { Error = ex.ToString() };
return new PasteInfo { Error = ex.ToString() };
}
}
/// <summary>Save a paste to Pastebin.</summary>
/// <param name="content">The paste content.</param>
public async Task<SavePasteResponse> PostAsync(string content)
public async Task<SavePasteResult> PostAsync(string content)
{
try
{
// validate
if (string.IsNullOrWhiteSpace(content))
return new SavePasteResponse { Error = "The log content can't be empty." };
return new SavePasteResult { Error = "The log content can't be empty." };
// post to API
string response = await this.Client
@ -96,19 +96,19 @@ namespace StardewModdingAPI.Web.Framework.LogParser
// handle Pastebin errors
if (string.IsNullOrWhiteSpace(response))
return new SavePasteResponse { Error = "Received an empty response from Pastebin." };
return new SavePasteResult { Error = "Received an empty response from Pastebin." };
if (response.StartsWith("Bad API request"))
return new SavePasteResponse { Error = response };
return new SavePasteResult { Error = response };
if (!response.Contains("/"))
return new SavePasteResponse { Error = $"Received an unknown response: {response}" };
return new SavePasteResult { Error = $"Received an unknown response: {response}" };
// return paste ID
string pastebinID = response.Split("/").Last();
return new SavePasteResponse { Success = true, ID = pastebinID };
return new SavePasteResult { Success = true, ID = pastebinID };
}
catch (Exception ex)
{
return new SavePasteResponse { Success = false, Error = ex.ToString() };
return new SavePasteResult { Success = false, Error = ex.ToString() };
}
}

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>
internal class SavePasteResponse
internal class SavePasteResult
{
/// <summary>Whether the log was successfully saved.</summary>
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
*********/
/****
** General
****/
/// <summary>The number of minutes update checks should be cached before refetching them.</summary>
public int CacheMinutes { get; set; }
@ -16,59 +13,13 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// <remarks>Derived from SMAPI's SemanticVersion implementation.</remarks>
public string SemanticVersionRegex { get; set; }
/****
** Chucklefish mod site
****/
/// <summary>The repository key for the Chucklefish mod site.</summary>
public string ChucklefishKey { get; set; }
/// <summary>The user agent for the Chucklefish API client, where {0} is the SMAPI version.</summary>
public string ChucklefishUserAgent { get; set; }
/// <summary>The base URL for the Chucklefish mod site.</summary>
public string ChucklefishBaseUrl { get; set; }
/// <summary>The URL for a mod page on the Chucklefish mod site excluding the <see cref="GitHubBaseUrl"/>, where {0} is the mod ID.</summary>
public string ChucklefishModPageUrlFormat { get; set; }
/****
** GitHub
****/
/// <summary>The repository key for Nexus Mods.</summary>
public string GitHubKey { get; set; }
/// <summary>The user agent for the GitHub API client, where {0} is the SMAPI version.</summary>
public string GitHubUserAgent { get; set; }
/// <summary>The base URL for the GitHub API.</summary>
public string GitHubBaseUrl { get; set; }
/// <summary>The URL for a GitHub API latest-release query excluding the <see cref="GitHubBaseUrl"/>, where {0} is the organisation and project name.</summary>
public string GitHubReleaseUrlFormat { get; set; }
/// <summary>The Accept header value expected by the GitHub API.</summary>
public string GitHubAcceptHeader { get; set; }
/// <summary>The username with which to authenticate to the GitHub API (if any).</summary>
public string GitHubUsername { get; set; }
/// <summary>The password with which to authenticate to the GitHub API (if any).</summary>
public string GitHubPassword { get; set; }
/****
** Nexus Mods
****/
/// <summary>The repository key for Nexus Mods.</summary>
public string NexusKey { get; set; }
/// <summary>The user agent for the Nexus Mods API client.</summary>
public string NexusUserAgent { get; set; }
/// <summary>The base URL for the Nexus Mods API.</summary>
public string NexusBaseUrl { get; set; }
/// <summary>The URL for a Nexus Mods API query excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary>
public string NexusModUrlFormat { get; set; }
}
}

View File

@ -1,9 +1,7 @@
using System;
using System.Net;
using System.Threading.Tasks;
using HtmlAgilityPack;
using Pathoschild.Http.Client;
using StardewModdingAPI.Common.Models;
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
namespace StardewModdingAPI.Web.Framework.ModRepositories
{
@ -13,14 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
/*********
** Properties
*********/
/// <summary>The base URL for the Chucklefish mod site.</summary>
private readonly string BaseUrl;
/// <summary>The URL for a mod page excluding the base URL, where {0} is the mod ID.</summary>
private readonly string ModPageUrlFormat;
/// <summary>The underlying HTTP client.</summary>
private readonly IClient Client;
private readonly IChucklefishClient Client;
/*********
@ -28,15 +20,11 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
*********/
/// <summary>Construct an instance.</summary>
/// <param name="vendorKey">The unique key for this vendor.</param>
/// <param name="userAgent">The user agent for the API client.</param>
/// <param name="baseUrl">The base URL for the Chucklefish mod site.</param>
/// <param name="modPageUrlFormat">The URL for a mod page excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
public ChucklefishRepository(string vendorKey, string userAgent, string baseUrl, string modPageUrlFormat)
/// <param name="client">The underlying HTTP client.</param>
public ChucklefishRepository(string vendorKey, IChucklefishClient client)
: base(vendorKey)
{
this.BaseUrl = baseUrl;
this.ModPageUrlFormat = modPageUrlFormat;
this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
this.Client = client;
}
/// <summary>Get metadata about a mod in the repository.</summary>
@ -44,38 +32,18 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
public override async Task<ModInfoModel> GetModInfoAsync(string id)
{
// validate ID format
if (!uint.TryParse(id, out uint _))
if (!uint.TryParse(id, out uint realID))
return new ModInfoModel($"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID.");
// fetch info
try
{
// fetch HTML
string html;
try
{
html = await this.Client
.GetAsync(string.Format(this.ModPageUrlFormat, id))
.AsString();
}
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
{
var mod = await this.Client.GetModAsync(realID);
if (mod == null)
return new ModInfoModel("Found no mod with this ID.");
}
// parse HTML
var doc = new HtmlDocument();
doc.LoadHtml(html);
// extract mod info
string url = new UriBuilder(new Uri(this.BaseUrl)) { Path = string.Format(this.ModPageUrlFormat, id) }.Uri.ToString();
string name = doc.DocumentNode.SelectSingleNode("//meta[@name='twitter:title']").Attributes["content"].Value;
if (name.StartsWith("[SMAPI] "))
name = name.Substring("[SMAPI] ".Length);
string version = doc.DocumentNode.SelectSingleNode("//h1/span").InnerText;
// create model
return new ModInfoModel(name, this.NormaliseVersion(version), url);
return new ModInfoModel(mod.Name, this.NormaliseVersion(mod.Version), mod.Url);
}
catch (Exception ex)
{

View File

@ -1,9 +1,7 @@
using System;
using System.Net;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Pathoschild.Http.Client;
using StardewModdingAPI.Common.Models;
using StardewModdingAPI.Web.Framework.Clients.GitHub;
namespace StardewModdingAPI.Web.Framework.ModRepositories
{
@ -13,11 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
/*********
** Properties
*********/
/// <summary>The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID.</summary>
private readonly string ReleaseUrlFormat;
/// <summary>The underlying HTTP client.</summary>
private readonly IClient Client;
/// <summary>The underlying GitHub API client.</summary>
private readonly IGitHubClient Client;
/*********
@ -25,22 +20,11 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
*********/
/// <summary>Construct an instance.</summary>
/// <param name="vendorKey">The unique key for this vendor.</param>
/// <param name="baseUrl">The base URL for the Nexus Mods API.</param>
/// <param name="releaseUrlFormat">The URL for a Nexus Mods API query excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
/// <param name="userAgent">The user agent for the API client.</param>
/// <param name="acceptHeader">The Accept header value expected by the GitHub API.</param>
/// <param name="username">The username with which to authenticate to the GitHub API.</param>
/// <param name="password">The password with which to authenticate to the GitHub API.</param>
public GitHubRepository(string vendorKey, string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader, string username, string password)
/// <param name="client">The underlying GitHub API client.</param>
public GitHubRepository(string vendorKey, IGitHubClient client)
: base(vendorKey)
{
this.ReleaseUrlFormat = releaseUrlFormat;
this.Client = new FluentClient(baseUrl)
.SetUserAgent(userAgent)
.AddDefault(req => req.WithHeader("Accept", acceptHeader));
if (!string.IsNullOrWhiteSpace(username))
this.Client = this.Client.SetBasicAuthentication(username, password);
this.Client = client;
}
/// <summary>Get metadata about a mod in the repository.</summary>
@ -54,14 +38,10 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
// fetch info
try
{
GitRelease release = await this.Client
.GetAsync(string.Format(this.ReleaseUrlFormat, id))
.As<GitRelease>();
return new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases");
}
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
{
return new ModInfoModel("Found no mod with this ID.");
GitRelease release = await this.Client.GetLatestReleaseAsync(id);
return release != null
? new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases")
: new ModInfoModel("Found no mod with this ID.");
}
catch (Exception ex)
{
@ -74,24 +54,5 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
{
this.Client.Dispose();
}
/*********
** Private models
*********/
/// <summary>Metadata about a GitHub release tag.</summary>
private class GitRelease
{
/*********
** Accessors
*********/
/// <summary>The display name.</summary>
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>The semantic version string.</summary>
[JsonProperty("tag_name")]
public string Tag { get; set; }
}
}
}

View File

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

View File

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

View File

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

View File

@ -7,6 +7,10 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using StardewModdingAPI.Web.Framework;
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
using StardewModdingAPI.Web.Framework.Clients.GitHub;
using StardewModdingAPI.Web.Framework.Clients.Nexus;
using StardewModdingAPI.Web.Framework.Clients.Pastebin;
using StardewModdingAPI.Web.Framework.ConfigModels;
using StardewModdingAPI.Web.Framework.RewriteRules;
@ -41,9 +45,10 @@ namespace StardewModdingAPI.Web
/// <param name="services">The service injection container.</param>
public void ConfigureServices(IServiceCollection services)
{
// init configuration
services
.Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck"))
.Configure<LogParserConfig>(this.Configuration.GetSection("LogParser"))
.Configure<ContextConfig>(this.Configuration.GetSection("Context"))
.Configure<RouteOptions>(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint)))
.AddMemoryCache()
.AddMvc()
@ -53,6 +58,41 @@ namespace StardewModdingAPI.Web
options.SerializerSettings.Formatting = Formatting.Indented;
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
});
// init API clients
{
ApiClientsConfig api = this.Configuration.GetSection("ApiClients").Get<ApiClientsConfig>();
string version = this.GetType().Assembly.GetName().Version.ToString(3);
string userAgent = string.Format(api.UserAgent, version);
services.AddSingleton<IChucklefishClient>(new ChucklefishClient(
userAgent: userAgent,
baseUrl: api.ChucklefishBaseUrl,
modPageUrlFormat: api.ChucklefishModPageUrlFormat
));
services.AddSingleton<IGitHubClient>(new GitHubClient(
baseUrl: api.GitHubBaseUrl,
releaseUrlFormat: api.GitHubReleaseUrlFormat,
userAgent: userAgent,
acceptHeader: api.GitHubAcceptHeader,
username: api.GitHubUsername,
password: api.GitHubPassword
));
services.AddSingleton<INexusClient>(new NexusClient(
userAgent: api.NexusUserAgent,
baseUrl: api.NexusBaseUrl,
modUrlFormat: api.NexusModUrlFormat
));
services.AddSingleton<IPastebinClient>(new PastebinClient(
baseUrl: api.PastebinBaseUrl,
userAgent: userAgent,
userKey: api.PastebinUserKey,
devKey: api.PastebinDevKey
));
}
}
/// <summary>The method called by the runtime to configure the HTTP request pipeline.</summary>
@ -89,11 +129,11 @@ namespace StardewModdingAPI.Web
req.Host.Host != "localhost"
&& (req.Host.Host.StartsWith("api.") || req.Host.Host.StartsWith("log."))
&& !req.Path.StartsWithSegments("/content")
&& !req.Path.StartsWithSegments("/favicon.ico")
))
// shortcut redirects
.Add(new RedirectToUrlRule("^/docs$", "https://stardewvalleywiki.com/Modding:Index"))
.Add(new RedirectToUrlRule("^/install$", "https://stardewvalleywiki.com/Modding:Installing_SMAPI"))
)
.UseStaticFiles() // wwwroot folder
.UseMvc();

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>
<html>
<head>
@ -10,9 +14,9 @@
<div id="sidebar">
<h4>SMAPI</h4>
<ul>
<li><a href="https://stardewvalleywiki.com/Modding:Index">FAQs & guides</a></li>
<li><a href="https://github.com/pathoschild/SMAPI/releases">Download SMAPI</a></li>
<li><a href="https://discord.gg/stardewvalley">Get help on Discord</a></li>
<li><a href="@ContextConfig.Value.RootUrl">About SMAPI</a></li>
<li><a href="@ContextConfig.Value.LogParserUrl">Log parser</a></li>
<li><a href="https://stardewvalleywiki.com/Modding:Index">Docs</a></li>
</ul>
</div>
<div id="content-column">

View File

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

View File

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

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
****/
/// <summary>SMAPI's current semantic version.</summary>
public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.2");
public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.3");
/// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30");

View File

@ -16,7 +16,7 @@ namespace StardewModdingAPI.Events
public SButton Button { get; }
/// <summary>The current cursor position.</summary>
public ICursorPosition Cursor { get; set; }
public ICursorPosition Cursor { get; }
/// <summary>Whether the input is considered a 'click' by the game for enabling action.</summary>
[Obsolete("Use " + nameof(EventArgsInput.IsActionButton) + " or " + nameof(EventArgsInput.IsUseToolButton) + " instead")] // deprecated in SMAPI 2.1
@ -28,6 +28,9 @@ namespace StardewModdingAPI.Events
/// <summary>Whether the input should use tools on the affected tile.</summary>
public bool IsUseToolButton { get; }
/// <summary>Whether a mod has indicated the key was already handled.</summary>
public bool IsSuppressed { get; private set; }
/*********
** Public methods
@ -55,6 +58,9 @@ namespace StardewModdingAPI.Events
/// <param name="button">The button to suppress.</param>
public void SuppressButton(SButton button)
{
if (button == this.Button)
this.IsSuppressed = true;
// keyboard
if (button.TryGetKeyboard(out Keys key))
Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Union(new[] { key }).ToArray());

View File

@ -33,6 +33,9 @@ namespace StardewModdingAPI.Events
/// <summary>Raised every 60th tick (≈once per second).</summary>
public static event EventHandler OneSecondTick;
/// <summary>Raised once after the game initialises and all <see cref="IMod.Entry"/> methods have been called.</summary>
public static event EventHandler FirstUpdateTick;
/*********
** Internal methods
@ -92,5 +95,12 @@ namespace StardewModdingAPI.Events
{
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList());
}
/// <summary>Raise a <see cref="FirstUpdateTick"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
internal static void InvokeFirstUpdateTick(IMonitor monitor)
{
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", GameEvents.FirstUpdateTick?.GetInvocationList());
}
}
}

View File

@ -57,14 +57,14 @@ namespace StardewModdingAPI.Framework.Content
public ContentCache(LocalizedContentManager contentManager, Reflector reflection, char[] possiblePathSeparators, string preferredPathSeparator)
{
// init
this.Cache = reflection.GetPrivateField<Dictionary<string, object>>(contentManager, "loadedAssets").GetValue();
this.Cache = reflection.GetField<Dictionary<string, object>>(contentManager, "loadedAssets").GetValue();
this.PossiblePathSeparators = possiblePathSeparators;
this.PreferredPathSeparator = preferredPathSeparator;
// get key normalisation logic
if (Constants.TargetPlatform == Platform.Windows)
{
IPrivateMethod method = reflection.GetPrivateMethod(typeof(TitleContainer), "GetCleanPath");
IReflectedMethod method = reflection.GetMethod(typeof(TitleContainer), "GetCleanPath");
this.NormaliseAssetNameForPlatform = path => method.Invoke<string>(path);
}
else

View File

@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework
/// <param name="severity">How deprecated the code is.</param>
public void Warn(string nounPhrase, string version, DeprecationLevel severity)
{
this.Warn(this.ModRegistry.GetModFromStack(), nounPhrase, version, severity);
this.Warn(this.ModRegistry.GetFromStack()?.DisplayName, nounPhrase, version, severity);
}
/// <summary>Log a deprecation warning.</summary>
@ -52,7 +52,7 @@ namespace StardewModdingAPI.Framework
return;
// build message
string message = $"{source ?? "An unknown mod"} uses deprecated code ({nounPhrase}).";
string message = $"{source ?? "An unknown mod"} uses deprecated code ({nounPhrase} is deprecated since SMAPI {version}).";
if (source == null)
message += $"{Environment.NewLine}{Environment.StackTrace}";
@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework
/// <returns>Returns whether the deprecation was successfully marked as warned. Returns <c>false</c> if it was already marked.</returns>
public bool MarkWarned(string nounPhrase, string version)
{
return this.MarkWarned(this.ModRegistry.GetModFromStack(), nounPhrase, version);
return this.MarkWarned(this.ModRegistry.GetFromStack()?.DisplayName, nounPhrase, version);
}
/// <summary>Mark a deprecation warning as already logged.</summary>

View File

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

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

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
{
@ -11,6 +13,15 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>The underlying mod registry.</summary>
private readonly ModRegistry Registry;
/// <summary>Encapsulates monitoring and logging for the mod.</summary>
private readonly IMonitor Monitor;
/// <summary>The mod IDs for APIs accessed by this instanced.</summary>
private readonly HashSet<string> AccessedModApis = new HashSet<string>();
/// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary>
private readonly InterfaceProxyBuilder ProxyBuilder;
/*********
** Public methods
@ -18,16 +29,20 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>Construct an instance.</summary>
/// <param name="modID">The unique ID of the relevant mod.</param>
/// <param name="registry">The underlying mod registry.</param>
public ModRegistryHelper(string modID, ModRegistry registry)
/// <param name="proxyBuilder">Generates proxy classes to access mod APIs through an arbitrary interface.</param>
/// <param name="monitor">Encapsulates monitoring and logging for the mod.</param>
public ModRegistryHelper(string modID, ModRegistry registry, InterfaceProxyBuilder proxyBuilder, IMonitor monitor)
: base(modID)
{
this.Registry = registry;
this.ProxyBuilder = proxyBuilder;
this.Monitor = monitor;
}
/// <summary>Get metadata for all loaded mods.</summary>
public IEnumerable<IManifest> GetAll()
{
return this.Registry.GetAll();
return this.Registry.GetAll().Select(p => p.Manifest);
}
/// <summary>Get metadata for a loaded mod.</summary>
@ -35,14 +50,56 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <returns>Returns the matching mod's metadata, or <c>null</c> if not found.</returns>
public IManifest Get(string uniqueID)
{
return this.Registry.Get(uniqueID);
return this.Registry.Get(uniqueID)?.Manifest;
}
/// <summary>Get whether a mod has been loaded.</summary>
/// <param name="uniqueID">The mod's unique ID.</param>
public bool IsLoaded(string uniqueID)
{
return this.Registry.IsLoaded(uniqueID);
return this.Registry.Get(uniqueID) != null;
}
/// <summary>Get the API provided by a mod, or <c>null</c> if it has none. This signature requires using the <see cref="IModHelper.Reflection"/> API to access the API's properties and methods.</summary>
public object GetApi(string uniqueID)
{
IModMetadata mod = this.Registry.Get(uniqueID);
if (mod?.Api != null && this.AccessedModApis.Add(mod.Manifest.UniqueID))
this.Monitor.Log($"Accessed mod-provided API for {mod.DisplayName}.", LogLevel.Trace);
return mod?.Api;
}
/// <summary>Get the API provided by a mod, mapped to a given interface which specifies the expected properties and methods. If the mod has no API or it's not compatible with the given interface, get <c>null</c>.</summary>
/// <typeparam name="TInterface">The interface which matches the properties and methods you intend to access.</typeparam>
/// <param name="uniqueID">The mod's unique ID.</param>
public TInterface GetApi<TInterface>(string uniqueID) where TInterface : class
{
// validate
if (!this.Registry.AreAllModsInitialised)
{
this.Monitor.Log("Tried to access a mod-provided API before all mods were initialised.", LogLevel.Error);
return null;
}
if (!typeof(TInterface).IsInterface)
{
this.Monitor.Log("Tried to map a mod-provided API to a class; must be a public interface.", LogLevel.Error);
return null;
}
if (!typeof(TInterface).IsPublic)
{
this.Monitor.Log("Tried to map a mod-provided API to a non-public interface; must be a public interface.", LogLevel.Error);
return null;
}
// get raw API
object api = this.GetApi(uniqueID);
if (api == null)
return null;
// get API of type
if (api is TInterface castApi)
return castApi;
return this.ProxyBuilder.CreateProxy<TInterface>(api, this.ModID, uniqueID);
}
}
}

View File

@ -17,6 +17,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>The mod name for error messages.</summary>
private readonly string ModName;
/// <summary>Manages deprecation warnings.</summary>
private readonly DeprecationManager DeprecationManager;
/*********
** Public methods
@ -25,15 +28,88 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="modID">The unique ID of the relevant mod.</param>
/// <param name="modName">The mod name for error messages.</param>
/// <param name="reflector">The underlying reflection helper.</param>
public ReflectionHelper(string modID, string modName, Reflector reflector)
/// <param name="deprecationManager">Manages deprecation warnings.</param>
public ReflectionHelper(string modID, string modName, Reflector reflector, DeprecationManager deprecationManager)
: base(modID)
{
this.ModName = modName;
this.Reflector = reflector;
this.DeprecationManager = deprecationManager;
}
/// <summary>Get an instance field.</summary>
/// <typeparam name="TValue">The field type.</typeparam>
/// <param name="obj">The object which has the field.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the field is not found.</param>
public IReflectedField<TValue> GetField<TValue>(object obj, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetField<TValue>(obj, name, required)
);
}
/// <summary>Get a static field.</summary>
/// <typeparam name="TValue">The field type.</typeparam>
/// <param name="type">The type which has the field.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the field is not found.</param>
public IReflectedField<TValue> GetField<TValue>(Type type, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetField<TValue>(type, name, required)
);
}
/// <summary>Get an instance property.</summary>
/// <typeparam name="TValue">The property type.</typeparam>
/// <param name="obj">The object which has the property.</param>
/// <param name="name">The property name.</param>
/// <param name="required">Whether to throw an exception if the property is not found.</param>
public IReflectedProperty<TValue> GetProperty<TValue>(object obj, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetProperty<TValue>(obj, name, required)
);
}
/// <summary>Get a static property.</summary>
/// <typeparam name="TValue">The property type.</typeparam>
/// <param name="type">The type which has the property.</param>
/// <param name="name">The property name.</param>
/// <param name="required">Whether to throw an exception if the property is not found.</param>
public IReflectedProperty<TValue> GetProperty<TValue>(Type type, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetProperty<TValue>(type, name, required)
);
}
/// <summary>Get an instance method.</summary>
/// <param name="obj">The object which has the method.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the field is not found.</param>
public IReflectedMethod GetMethod(object obj, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetMethod(obj, name, required)
);
}
/// <summary>Get a static method.</summary>
/// <param name="type">The type which has the method.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the field is not found.</param>
public IReflectedMethod GetMethod(Type type, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetMethod(type, name, required)
);
}
/****
** Fields
** Obsolete
****/
/// <summary>Get a private instance field.</summary>
/// <typeparam name="TValue">The field type.</typeparam>
@ -41,11 +117,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
/// <returns>Returns the field wrapper, or <c>null</c> if the field doesn't exist and <paramref name="required"/> is <c>false</c>.</returns>
[Obsolete]
public IPrivateField<TValue> GetPrivateField<TValue>(object obj, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetPrivateField<TValue>(obj, name, required)
);
this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
return (IPrivateField<TValue>)this.GetField<TValue>(obj, name, required);
}
/// <summary>Get a private static field.</summary>
@ -53,26 +129,23 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="type">The type which has the field.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
[Obsolete]
public IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetPrivateField<TValue>(type, name, required)
);
this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
return (IPrivateField<TValue>)this.GetField<TValue>(type, name, required);
}
/****
** Properties
****/
/// <summary>Get a private instance property.</summary>
/// <typeparam name="TValue">The property type.</typeparam>
/// <param name="obj">The object which has the property.</param>
/// <param name="name">The property name.</param>
/// <param name="required">Whether to throw an exception if the private property is not found.</param>
[Obsolete]
public IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetPrivateProperty<TValue>(obj, name, required)
);
this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
return (IPrivateProperty<TValue>)this.GetProperty<TValue>(obj, name, required);
}
/// <summary>Get a private static property.</summary>
@ -80,17 +153,13 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="type">The type which has the property.</param>
/// <param name="name">The property name.</param>
/// <param name="required">Whether to throw an exception if the private property is not found.</param>
[Obsolete]
public IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetPrivateProperty<TValue>(type, name, required)
);
this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
return (IPrivateProperty<TValue>)this.GetProperty<TValue>(type, name, required);
}
/****
** Field values
** (shorthand since this is the most common case)
****/
/// <summary>Get the value of a private instance field.</summary>
/// <typeparam name="TValue">The field type.</typeparam>
/// <param name="obj">The object which has the field.</param>
@ -101,9 +170,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// This is a shortcut for <see cref="GetPrivateField{TValue}(object,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>.
/// When <paramref name="required" /> is false, this will return the default value if reflection fails. If you need to check whether the field exists, use <see cref="GetPrivateField{TValue}(object,string,bool)" /> instead.
/// </remarks>
[Obsolete]
public TValue GetPrivateValue<TValue>(object obj, string name, bool required = true)
{
IPrivateField<TValue> field = this.GetPrivateField<TValue>(obj, name, required);
this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
IPrivateField<TValue> field = (IPrivateField<TValue>)this.GetField<TValue>(obj, name, required);
return field != null
? field.GetValue()
: default(TValue);
@ -119,64 +190,36 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// This is a shortcut for <see cref="GetPrivateField{TValue}(Type,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>.
/// When <paramref name="required" /> is false, this will return the default value if reflection fails. If you need to check whether the field exists, use <see cref="GetPrivateField{TValue}(Type,string,bool)" /> instead.
/// </remarks>
[Obsolete]
public TValue GetPrivateValue<TValue>(Type type, string name, bool required = true)
{
IPrivateField<TValue> field = this.GetPrivateField<TValue>(type, name, required);
this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
IPrivateField<TValue> field = (IPrivateField<TValue>)this.GetField<TValue>(type, name, required);
return field != null
? field.GetValue()
: default(TValue);
}
/****
** Methods
****/
/// <summary>Get a private instance method.</summary>
/// <param name="obj">The object which has the method.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
[Obsolete]
public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetPrivateMethod(obj, name, required)
);
this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
return (IPrivateMethod)this.GetMethod(obj, name, required);
}
/// <summary>Get a private static method.</summary>
/// <param name="type">The type which has the method.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
[Obsolete]
public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetPrivateMethod(type, name, required)
);
}
/****
** Methods by signature
****/
/// <summary>Get a private instance method.</summary>
/// <param name="obj">The object which has the method.</param>
/// <param name="name">The field name.</param>
/// <param name="argumentTypes">The argument types of the method signature to find.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required)
);
}
/// <summary>Get a private static method.</summary>
/// <param name="type">The type which has the method.</param>
/// <param name="name">The field name.</param>
/// <param name="argumentTypes">The argument types of the method signature to find.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetPrivateMethod(type, name, argumentTypes, required)
);
this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice);
return (IPrivateMethod)this.GetMethod(type, name, required);
}
@ -187,7 +230,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <typeparam name="T">The field value type.</typeparam>
/// <param name="field">The field being accessed.</param>
/// <returns>Returns the same field instance for convenience.</returns>
private IPrivateField<T> AssertAccessAllowed<T>(IPrivateField<T> field)
private IReflectedField<T> AssertAccessAllowed<T>(IReflectedField<T> field)
{
this.AssertAccessAllowed(field?.FieldInfo);
return field;
@ -197,7 +240,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <typeparam name="T">The property value type.</typeparam>
/// <param name="property">The property being accessed.</param>
/// <returns>Returns the same property instance for convenience.</returns>
private IPrivateProperty<T> AssertAccessAllowed<T>(IPrivateProperty<T> property)
private IReflectedProperty<T> AssertAccessAllowed<T>(IReflectedProperty<T> property)
{
this.AssertAccessAllowed(property?.PropertyInfo);
return property;
@ -206,7 +249,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>Assert that mods can use the reflection helper to access the given member.</summary>
/// <param name="method">The method being accessed.</param>
/// <returns>Returns the same method instance for convenience.</returns>
private IPrivateMethod AssertAccessAllowed(IPrivateMethod method)
private IReflectedMethod AssertAccessAllowed(IReflectedMethod method)
{
this.AssertAccessAllowed(method?.MethodInfo);
return method;

View File

@ -29,6 +29,9 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>The mod instance (if it was loaded).</summary>
public IMod Mod { get; private set; }
/// <summary>The mod-provided API (if any).</summary>
public object Api { get; private set; }
/*********
** Public methods
@ -64,5 +67,13 @@ namespace StardewModdingAPI.Framework.ModLoading
this.Mod = mod;
return this;
}
/// <summary>Set the mod-provided API instance.</summary>
/// <param name="api">The mod-provided API.</param>
public IModMetadata SetApi(object api)
{
this.Api = api;
return this;
}
}
}

View File

@ -15,26 +15,34 @@ namespace StardewModdingAPI.Framework
/// <summary>The registered mod data.</summary>
private readonly List<IModMetadata> Mods = new List<IModMetadata>();
/// <summary>The friendly mod names treated as deprecation warning sources (assembly full name => mod name).</summary>
private readonly IDictionary<string, string> ModNamesByAssembly = new Dictionary<string, string>();
/// <summary>An assembly full name => mod lookup.</summary>
private readonly IDictionary<string, IModMetadata> ModNamesByAssembly = new Dictionary<string, IModMetadata>();
/// <summary>Whether all mods have been initialised and their <see cref="IMod.Entry"/> method called.</summary>
public bool AreAllModsInitialised { get; set; }
/*********
** Public methods
*********/
/****
** Basic metadata
****/
/// <summary>Get metadata for all loaded mods.</summary>
public IEnumerable<IManifest> GetAll()
/// <summary>Register a mod as a possible source of deprecation warnings.</summary>
/// <param name="metadata">The mod metadata.</param>
public void Add(IModMetadata metadata)
{
return this.Mods.Select(p => p.Manifest);
this.Mods.Add(metadata);
this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata;
}
/// <summary>Get metadata for all loaded mods.</summary>
public IEnumerable<IModMetadata> GetAll()
{
return this.Mods.Select(p => p);
}
/// <summary>Get metadata for a loaded mod.</summary>
/// <param name="uniqueID">The mod's unique ID.</param>
/// <returns>Returns the matching mod's metadata, or <c>null</c> if not found.</returns>
public IManifest Get(string uniqueID)
public IModMetadata Get(string uniqueID)
{
// normalise search ID
if (string.IsNullOrWhiteSpace(uniqueID))
@ -42,37 +50,13 @@ namespace StardewModdingAPI.Framework
uniqueID = uniqueID.Trim();
// find match
return this.GetAll().FirstOrDefault(p => p.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase));
return this.GetAll().FirstOrDefault(p => p.Manifest.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase));
}
/// <summary>Get whether a mod has been loaded.</summary>
/// <param name="uniqueID">The mod's unique ID.</param>
public bool IsLoaded(string uniqueID)
{
return this.Get(uniqueID) != null;
}
/****
** Mod data
****/
/// <summary>Register a mod as a possible source of deprecation warnings.</summary>
/// <param name="metadata">The mod metadata.</param>
public void Add(IModMetadata metadata)
{
this.Mods.Add(metadata);
this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata.DisplayName;
}
/// <summary>Get all enabled mods.</summary>
public IEnumerable<IModMetadata> GetMods()
{
return (from mod in this.Mods select mod);
}
/// <summary>Get the friendly mod name which defines a type.</summary>
/// <summary>Get the mod metadata from one of its assemblies.</summary>
/// <param name="type">The type to check.</param>
/// <returns>Returns the mod name, or <c>null</c> if the type isn't part of a known mod.</returns>
public string GetModFrom(Type type)
public IModMetadata GetFrom(Type type)
{
// null
if (type == null)
@ -89,7 +73,7 @@ namespace StardewModdingAPI.Framework
/// <summary>Get the friendly name for the closest assembly registered as a source of deprecation warnings.</summary>
/// <returns>Returns the source name, or <c>null</c> if no registered assemblies were found.</returns>
public string GetModFromStack()
public IModMetadata GetFromStack()
{
// get stack frames
StackTrace stack = new StackTrace();
@ -101,9 +85,9 @@ namespace StardewModdingAPI.Framework
foreach (StackFrame frame in frames)
{
MethodBase method = frame.GetMethod();
string name = this.GetModFrom(method.ReflectedType);
if (name != null)
return name;
IModMetadata mod = this.GetFrom(method.ReflectedType);
if (mod != null)
return mod;
}
// no known assembly found

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;
namespace StardewModdingAPI.Framework.Reflection
{
/// <summary>A private field obtained through reflection.</summary>
/// <summary>A field obtained through reflection.</summary>
/// <typeparam name="TValue">The field value type.</typeparam>
internal class PrivateField<TValue> : IPrivateField<TValue>
internal class ReflectedField<TValue> : IPrivateField<TValue>, IReflectedField<TValue>
{
/*********
** Properties
@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework.Reflection
/// <param name="isStatic">Whether the field is static.</param>
/// <exception cref="ArgumentNullException">The <paramref name="parentType"/> or <paramref name="field"/> is null.</exception>
/// <exception cref="ArgumentException">The <paramref name="obj"/> is null for a non-static field, or not null for a static field.</exception>
public PrivateField(Type parentType, object obj, FieldInfo field, bool isStatic)
public ReflectedField(Type parentType, object obj, FieldInfo field, bool isStatic)
{
// validate
if (parentType == null)
@ -64,11 +64,11 @@ namespace StardewModdingAPI.Framework.Reflection
}
catch (InvalidCastException)
{
throw new InvalidCastException($"Can't convert the private {this.DisplayName} field from {this.FieldInfo.FieldType.FullName} to {typeof(TValue).FullName}.");
throw new InvalidCastException($"Can't convert the {this.DisplayName} field from {this.FieldInfo.FieldType.FullName} to {typeof(TValue).FullName}.");
}
catch (Exception ex)
{
throw new Exception($"Couldn't get the value of the private {this.DisplayName} field", ex);
throw new Exception($"Couldn't get the value of the {this.DisplayName} field", ex);
}
}
@ -82,11 +82,11 @@ namespace StardewModdingAPI.Framework.Reflection
}
catch (InvalidCastException)
{
throw new InvalidCastException($"Can't assign the private {this.DisplayName} field a {typeof(TValue).FullName} value, must be compatible with {this.FieldInfo.FieldType.FullName}.");
throw new InvalidCastException($"Can't assign the {this.DisplayName} field a {typeof(TValue).FullName} value, must be compatible with {this.FieldInfo.FieldType.FullName}.");
}
catch (Exception ex)
{
throw new Exception($"Couldn't set the value of the private {this.DisplayName} field", ex);
throw new Exception($"Couldn't set the value of the {this.DisplayName} field", ex);
}
}
}

View File

@ -3,8 +3,8 @@ using System.Reflection;
namespace StardewModdingAPI.Framework.Reflection
{
/// <summary>A private method obtained through reflection.</summary>
internal class PrivateMethod : IPrivateMethod
/// <summary>A method obtained through reflection.</summary>
internal class ReflectedMethod : IPrivateMethod, IReflectedMethod
{
/*********
** Properties
@ -33,10 +33,10 @@ namespace StardewModdingAPI.Framework.Reflection
/// <param name="parentType">The type that has the method.</param>
/// <param name="obj">The object that has the instance method(if applicable).</param>
/// <param name="method">The reflection metadata.</param>
/// <param name="isStatic">Whether the field is static.</param>
/// <param name="isStatic">Whether the method is static.</param>
/// <exception cref="ArgumentNullException">The <paramref name="parentType"/> or <paramref name="method"/> is null.</exception>
/// <exception cref="ArgumentException">The <paramref name="obj"/> is null for a non-static method, or not null for a static method.</exception>
public PrivateMethod(Type parentType, object obj, MethodInfo method, bool isStatic)
public ReflectedMethod(Type parentType, object obj, MethodInfo method, bool isStatic)
{
// validate
if (parentType == null)
@ -67,7 +67,7 @@ namespace StardewModdingAPI.Framework.Reflection
}
catch (Exception ex)
{
throw new Exception($"Couldn't invoke the private {this.DisplayName} field", ex);
throw new Exception($"Couldn't invoke the {this.DisplayName} method", ex);
}
// cast return value
@ -77,7 +77,7 @@ namespace StardewModdingAPI.Framework.Reflection
}
catch (InvalidCastException)
{
throw new InvalidCastException($"Can't convert the return value of the private {this.DisplayName} method from {this.MethodInfo.ReturnType.FullName} to {typeof(TValue).FullName}.");
throw new InvalidCastException($"Can't convert the return value of the {this.DisplayName} method from {this.MethodInfo.ReturnType.FullName} to {typeof(TValue).FullName}.");
}
}
@ -92,7 +92,7 @@ namespace StardewModdingAPI.Framework.Reflection
}
catch (Exception ex)
{
throw new Exception($"Couldn't invoke the private {this.DisplayName} field", ex);
throw new Exception($"Couldn't invoke the {this.DisplayName} method", ex);
}
}
}

View File

@ -3,9 +3,9 @@ using System.Reflection;
namespace StardewModdingAPI.Framework.Reflection
{
/// <summary>A private property obtained through reflection.</summary>
/// <summary>A property obtained through reflection.</summary>
/// <typeparam name="TValue">The property value type.</typeparam>
internal class PrivateProperty<TValue> : IPrivateProperty<TValue>
internal class ReflectedProperty<TValue> : IPrivateProperty<TValue>, IReflectedProperty<TValue>
{
/*********
** Properties
@ -14,10 +14,10 @@ namespace StardewModdingAPI.Framework.Reflection
private readonly string DisplayName;
/// <summary>The underlying property getter.</summary>
private readonly Func<TValue> GetterDelegate;
private readonly Func<TValue> GetMethod;
/// <summary>The underlying property setter.</summary>
private readonly Action<TValue> SetterDelegate;
private readonly Action<TValue> SetMethod;
/*********
@ -31,13 +31,13 @@ namespace StardewModdingAPI.Framework.Reflection
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="parentType">The type that has the field.</param>
/// <param name="obj">The object that has the instance field (if applicable).</param>
/// <param name="parentType">The type that has the property.</param>
/// <param name="obj">The object that has the instance property (if applicable).</param>
/// <param name="property">The reflection metadata.</param>
/// <param name="isStatic">Whether the field is static.</param>
/// <param name="isStatic">Whether the property is static.</param>
/// <exception cref="ArgumentNullException">The <paramref name="parentType"/> or <paramref name="property"/> is null.</exception>
/// <exception cref="ArgumentException">The <paramref name="obj"/> is null for a non-static field, or not null for a static field.</exception>
public PrivateProperty(Type parentType, object obj, PropertyInfo property, bool isStatic)
/// <exception cref="ArgumentException">The <paramref name="obj"/> is null for a non-static property, or not null for a static property.</exception>
public ReflectedProperty(Type parentType, object obj, PropertyInfo property, bool isStatic)
{
// validate input
if (parentType == null)
@ -55,24 +55,29 @@ namespace StardewModdingAPI.Framework.Reflection
this.DisplayName = $"{parentType.FullName}::{property.Name}";
this.PropertyInfo = property;
this.GetterDelegate = (Func<TValue>)Delegate.CreateDelegate(typeof(Func<TValue>), obj, this.PropertyInfo.GetMethod);
this.SetterDelegate = (Action<TValue>)Delegate.CreateDelegate(typeof(Action<TValue>), obj, this.PropertyInfo.SetMethod);
if (this.PropertyInfo.GetMethod != null)
this.GetMethod = (Func<TValue>)Delegate.CreateDelegate(typeof(Func<TValue>), obj, this.PropertyInfo.GetMethod);
if (this.PropertyInfo.SetMethod != null)
this.SetMethod = (Action<TValue>)Delegate.CreateDelegate(typeof(Action<TValue>), obj, this.PropertyInfo.SetMethod);
}
/// <summary>Get the property value.</summary>
public TValue GetValue()
{
if (this.GetMethod == null)
throw new InvalidOperationException($"The {this.DisplayName} property has no get method.");
try
{
return this.GetterDelegate();
return this.GetMethod();
}
catch (InvalidCastException)
{
throw new InvalidCastException($"Can't convert the private {this.DisplayName} property from {this.PropertyInfo.PropertyType.FullName} to {typeof(TValue).FullName}.");
throw new InvalidCastException($"Can't convert the {this.DisplayName} property from {this.PropertyInfo.PropertyType.FullName} to {typeof(TValue).FullName}.");
}
catch (Exception ex)
{
throw new Exception($"Couldn't get the value of the private {this.DisplayName} property", ex);
throw new Exception($"Couldn't get the value of the {this.DisplayName} property", ex);
}
}
@ -80,17 +85,20 @@ namespace StardewModdingAPI.Framework.Reflection
//// <param name="value">The value to set.</param>
public void SetValue(TValue value)
{
if (this.SetMethod == null)
throw new InvalidOperationException($"The {this.DisplayName} property has no set method.");
try
{
this.SetterDelegate(value);
this.SetMethod(value);
}
catch (InvalidCastException)
{
throw new InvalidCastException($"Can't assign the private {this.DisplayName} property a {typeof(TValue).FullName} value, must be compatible with {this.PropertyInfo.PropertyType.FullName}.");
throw new InvalidCastException($"Can't assign the {this.DisplayName} property a {typeof(TValue).FullName} value, must be compatible with {this.PropertyInfo.PropertyType.FullName}.");
}
catch (Exception ex)
{
throw new Exception($"Couldn't set the value of the private {this.DisplayName} property", ex);
throw new Exception($"Couldn't set the value of the {this.DisplayName} property", ex);
}
}
}

View File

@ -5,7 +5,7 @@ using System.Runtime.Caching;
namespace StardewModdingAPI.Framework.Reflection
{
/// <summary>Provides helper methods for accessing private game code.</summary>
/// <summary>Provides helper methods for accessing inaccessible code.</summary>
/// <remarks>This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage).</remarks>
internal class Reflector
{
@ -25,139 +25,139 @@ namespace StardewModdingAPI.Framework.Reflection
/****
** Fields
****/
/// <summary>Get a private instance field.</summary>
/// <summary>Get a instance field.</summary>
/// <typeparam name="TValue">The field type.</typeparam>
/// <param name="obj">The object which has the field.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
/// <param name="required">Whether to throw an exception if the field is not found.</param>
/// <returns>Returns the field wrapper, or <c>null</c> if the field doesn't exist and <paramref name="required"/> is <c>false</c>.</returns>
public IPrivateField<TValue> GetPrivateField<TValue>(object obj, string name, bool required = true)
public IReflectedField<TValue> GetField<TValue>(object obj, string name, bool required = true)
{
// validate
if (obj == null)
throw new ArgumentNullException(nameof(obj), "Can't get a private instance field from a null object.");
throw new ArgumentNullException(nameof(obj), "Can't get a instance field from a null object.");
// get field from hierarchy
IPrivateField<TValue> field = this.GetFieldFromHierarchy<TValue>(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
IReflectedField<TValue> field = this.GetFieldFromHierarchy<TValue>(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (required && field == null)
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance field.");
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance field.");
return field;
}
/// <summary>Get a private static field.</summary>
/// <summary>Get a static field.</summary>
/// <typeparam name="TValue">The field type.</typeparam>
/// <param name="type">The type which has the field.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
public IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true)
/// <param name="required">Whether to throw an exception if the field is not found.</param>
public IReflectedField<TValue> GetField<TValue>(Type type, string name, bool required = true)
{
// get field from hierarchy
IPrivateField<TValue> field = this.GetFieldFromHierarchy<TValue>(type, null, name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);
IReflectedField<TValue> field = this.GetFieldFromHierarchy<TValue>(type, null, name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);
if (required && field == null)
throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static field.");
throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static field.");
return field;
}
/****
** Properties
****/
/// <summary>Get a private instance property.</summary>
/// <summary>Get a instance property.</summary>
/// <typeparam name="TValue">The property type.</typeparam>
/// <param name="obj">The object which has the property.</param>
/// <param name="name">The property name.</param>
/// <param name="required">Whether to throw an exception if the private property is not found.</param>
public IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true)
/// <param name="required">Whether to throw an exception if the property is not found.</param>
public IReflectedProperty<TValue> GetProperty<TValue>(object obj, string name, bool required = true)
{
// validate
if (obj == null)
throw new ArgumentNullException(nameof(obj), "Can't get a private instance property from a null object.");
throw new ArgumentNullException(nameof(obj), "Can't get a instance property from a null object.");
// get property from hierarchy
IPrivateProperty<TValue> property = this.GetPropertyFromHierarchy<TValue>(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
IReflectedProperty<TValue> property = this.GetPropertyFromHierarchy<TValue>(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (required && property == null)
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance property.");
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance property.");
return property;
}
/// <summary>Get a private static property.</summary>
/// <summary>Get a static property.</summary>
/// <typeparam name="TValue">The property type.</typeparam>
/// <param name="type">The type which has the property.</param>
/// <param name="name">The property name.</param>
/// <param name="required">Whether to throw an exception if the private property is not found.</param>
public IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true)
/// <param name="required">Whether to throw an exception if the property is not found.</param>
public IReflectedProperty<TValue> GetProperty<TValue>(Type type, string name, bool required = true)
{
// get field from hierarchy
IPrivateProperty<TValue> property = this.GetPropertyFromHierarchy<TValue>(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
IReflectedProperty<TValue> property = this.GetPropertyFromHierarchy<TValue>(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
if (required && property == null)
throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static property.");
throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static property.");
return property;
}
/****
** Methods
****/
/// <summary>Get a private instance method.</summary>
/// <summary>Get a instance method.</summary>
/// <param name="obj">The object which has the method.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true)
/// <param name="required">Whether to throw an exception if the field is not found.</param>
public IReflectedMethod GetMethod(object obj, string name, bool required = true)
{
// validate
if (obj == null)
throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object.");
throw new ArgumentNullException(nameof(obj), "Can't get a instance method from a null object.");
// get method from hierarchy
IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
IReflectedMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (required && method == null)
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method.");
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance method.");
return method;
}
/// <summary>Get a private static method.</summary>
/// <summary>Get a static method.</summary>
/// <param name="type">The type which has the method.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true)
/// <param name="required">Whether to throw an exception if the field is not found.</param>
public IReflectedMethod GetMethod(Type type, string name, bool required = true)
{
// get method from hierarchy
IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
IReflectedMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
if (required && method == null)
throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method.");
throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static method.");
return method;
}
/****
** Methods by signature
****/
/// <summary>Get a private instance method.</summary>
/// <summary>Get a instance method.</summary>
/// <param name="obj">The object which has the method.</param>
/// <param name="name">The field name.</param>
/// <param name="argumentTypes">The argument types of the method signature to find.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true)
/// <param name="required">Whether to throw an exception if the field is not found.</param>
public IReflectedMethod GetMethod(object obj, string name, Type[] argumentTypes, bool required = true)
{
// validate parent
if (obj == null)
throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object.");
throw new ArgumentNullException(nameof(obj), "Can't get a instance method from a null object.");
// get method from hierarchy
PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, argumentTypes);
ReflectedMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, argumentTypes);
if (required && method == null)
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method with that signature.");
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance method with that signature.");
return method;
}
/// <summary>Get a private static method.</summary>
/// <summary>Get a static method.</summary>
/// <param name="type">The type which has the method.</param>
/// <param name="name">The field name.</param>
/// <param name="argumentTypes">The argument types of the method signature to find.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true)
/// <param name="required">Whether to throw an exception if the field is not found.</param>
public IReflectedMethod GetMethod(Type type, string name, Type[] argumentTypes, bool required = true)
{
// get field from hierarchy
PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, argumentTypes);
ReflectedMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, argumentTypes);
if (required && method == null)
throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method with that signature.");
throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static method with that signature.");
return method;
}
@ -171,7 +171,7 @@ namespace StardewModdingAPI.Framework.Reflection
/// <param name="obj">The object which has the field.</param>
/// <param name="name">The field name.</param>
/// <param name="bindingFlags">The reflection binding which flags which indicates what type of field to find.</param>
private IPrivateField<TValue> GetFieldFromHierarchy<TValue>(Type type, object obj, string name, BindingFlags bindingFlags)
private IReflectedField<TValue> GetFieldFromHierarchy<TValue>(Type type, object obj, string name, BindingFlags bindingFlags)
{
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
FieldInfo field = this.GetCached<FieldInfo>($"field::{isStatic}::{type.FullName}::{name}", () =>
@ -183,7 +183,7 @@ namespace StardewModdingAPI.Framework.Reflection
});
return field != null
? new PrivateField<TValue>(type, obj, field, isStatic)
? new ReflectedField<TValue>(type, obj, field, isStatic)
: null;
}
@ -193,7 +193,7 @@ namespace StardewModdingAPI.Framework.Reflection
/// <param name="obj">The object which has the property.</param>
/// <param name="name">The property name.</param>
/// <param name="bindingFlags">The reflection binding which flags which indicates what type of property to find.</param>
private IPrivateProperty<TValue> GetPropertyFromHierarchy<TValue>(Type type, object obj, string name, BindingFlags bindingFlags)
private IReflectedProperty<TValue> GetPropertyFromHierarchy<TValue>(Type type, object obj, string name, BindingFlags bindingFlags)
{
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
PropertyInfo property = this.GetCached<PropertyInfo>($"property::{isStatic}::{type.FullName}::{name}", () =>
@ -205,7 +205,7 @@ namespace StardewModdingAPI.Framework.Reflection
});
return property != null
? new PrivateProperty<TValue>(type, obj, property, isStatic)
? new ReflectedProperty<TValue>(type, obj, property, isStatic)
: null;
}
@ -214,7 +214,7 @@ namespace StardewModdingAPI.Framework.Reflection
/// <param name="obj">The object which has the method.</param>
/// <param name="name">The method name.</param>
/// <param name="bindingFlags">The reflection binding which flags which indicates what type of method to find.</param>
private IPrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags)
private IReflectedMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags)
{
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () =>
@ -226,7 +226,7 @@ namespace StardewModdingAPI.Framework.Reflection
});
return method != null
? new PrivateMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static))
? new ReflectedMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static))
: null;
}
@ -236,7 +236,7 @@ namespace StardewModdingAPI.Framework.Reflection
/// <param name="name">The method name.</param>
/// <param name="bindingFlags">The reflection binding which flags which indicates what type of method to find.</param>
/// <param name="argumentTypes">The argument types of the method signature to find.</param>
private PrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes)
private ReflectedMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes)
{
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}({string.Join(",", argumentTypes.Select(p => p.FullName))})", () =>
@ -247,7 +247,7 @@ namespace StardewModdingAPI.Framework.Reflection
return methodInfo;
});
return method != null
? new PrivateMethod(type, obj, method, isStatic)
? new ReflectedMethod(type, obj, method, isStatic)
: null;
}

View File

@ -45,7 +45,7 @@ namespace StardewModdingAPI.Framework
private readonly ContentCache Cache;
/// <summary>The private <see cref="LocalizedContentManager"/> method which generates the locale portion of an asset name.</summary>
private readonly IPrivateMethod GetKeyLocale;
private readonly IReflectedMethod GetKeyLocale;
/// <summary>The language codes used in asset keys.</summary>
private readonly IDictionary<string, LanguageCode> KeyLocales;
@ -101,7 +101,7 @@ namespace StardewModdingAPI.Framework
// init
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
this.Cache = new ContentCache(this, reflection, SContentManager.PossiblePathSeparators, SContentManager.PreferredPathSeparator);
this.GetKeyLocale = reflection.GetPrivateMethod(this, "languageCode");
this.GetKeyLocale = reflection.GetMethod(this, "languageCode");
this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath);
// get asset data
@ -205,7 +205,7 @@ namespace StardewModdingAPI.Framework
return this.LoadImpl<T>(assetName, instance);
// load mod content
SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading content asset '{assetName}': {reasonPhrase}.");
SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading content asset '{assetName}': {reasonPhrase}");
try
{
return this.WithWriteLock(() =>
@ -252,6 +252,8 @@ namespace StardewModdingAPI.Framework
}
catch (Exception ex) when (!(ex is SContentLoadException))
{
if (ex.GetInnermostException() is DllNotFoundException dllEx && dllEx.Message == "libgdiplus.dylib")
throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher.");
throw new SContentLoadException($"The content manager failed loading content asset '{assetName}'.", ex);
}
}
@ -413,7 +415,7 @@ namespace StardewModdingAPI.Framework
private IDictionary<string, LanguageCode> GetKeyLocales(Reflector reflection)
{
// get the private code field directly to avoid changed-code logic
IPrivateField<LanguageCode> codeField = reflection.GetPrivateField<LanguageCode>(typeof(LocalizedContentManager), "_currentLangCode");
IReflectedField<LanguageCode> codeField = reflection.GetField<LanguageCode>(typeof(LocalizedContentManager), "_currentLangCode");
// remember previous settings
LanguageCode previousCode = codeField.GetValue();

View File

@ -133,20 +133,20 @@ namespace StardewModdingAPI.Framework
// ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming
/// <summary>Used to access private fields and methods.</summary>
private static List<float> _fpsList => SGame.Reflection.GetPrivateField<List<float>>(typeof(Game1), nameof(_fpsList)).GetValue();
private static Stopwatch _fpsStopwatch => SGame.Reflection.GetPrivateField<Stopwatch>(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue();
private static List<float> _fpsList => SGame.Reflection.GetField<List<float>>(typeof(Game1), nameof(_fpsList)).GetValue();
private static Stopwatch _fpsStopwatch => SGame.Reflection.GetField<Stopwatch>(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue();
private static float _fps
{
set => SGame.Reflection.GetPrivateField<float>(typeof(Game1), nameof(_fps)).SetValue(value);
set => SGame.Reflection.GetField<float>(typeof(Game1), nameof(_fps)).SetValue(value);
}
private static Task _newDayTask => SGame.Reflection.GetPrivateField<Task>(typeof(Game1), nameof(_newDayTask)).GetValue();
private Color bgColor => SGame.Reflection.GetPrivateField<Color>(this, nameof(bgColor)).GetValue();
public RenderTarget2D screenWrapper => SGame.Reflection.GetPrivateProperty<RenderTarget2D>(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop
public BlendState lightingBlend => SGame.Reflection.GetPrivateField<BlendState>(this, nameof(lightingBlend)).GetValue();
private readonly Action drawFarmBuildings = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke();
private readonly Action drawHUD = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawHUD)).Invoke();
private readonly Action drawDialogueBox = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke();
private readonly Action renderScreenBuffer = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke();
private static Task _newDayTask => SGame.Reflection.GetField<Task>(typeof(Game1), nameof(_newDayTask)).GetValue();
private Color bgColor => SGame.Reflection.GetField<Color>(this, nameof(bgColor)).GetValue();
public RenderTarget2D screenWrapper => SGame.Reflection.GetProperty<RenderTarget2D>(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop
public BlendState lightingBlend => SGame.Reflection.GetField<BlendState>(this, nameof(lightingBlend)).GetValue();
private readonly Action drawFarmBuildings = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke();
private readonly Action drawHUD = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawHUD)).Invoke();
private readonly Action drawDialogueBox = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke();
private readonly Action renderScreenBuffer = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke();
// ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming
@ -182,7 +182,7 @@ namespace StardewModdingAPI.Framework
this.SContentManager = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor, reflection);
this.Content = new ContentManagerShim(this.SContentManager, "SGame.Content");
Game1.content = new ContentManagerShim(this.SContentManager, "Game1.content");
reflection.GetPrivateField<LocalizedContentManager>(typeof(Game1), "_temporaryContent").SetValue(new ContentManagerShim(this.SContentManager, "Game1._temporaryContent")); // regenerate value with new content manager
reflection.GetField<LocalizedContentManager>(typeof(Game1), "_temporaryContent").SetValue(new ContentManagerShim(this.SContentManager, "Game1._temporaryContent")); // regenerate value with new content manager
}
/****
@ -557,9 +557,12 @@ namespace StardewModdingAPI.Framework
/*********
** Update events
*********/
GameEvents.InvokeUpdateTick(this.Monitor);
if (this.FirstUpdate)
{
this.FirstUpdate = false;
GameEvents.InvokeFirstUpdateTick(this.Monitor);
}
GameEvents.InvokeUpdateTick(this.Monitor);
if (this.CurrentUpdateTick % 2 == 0)
GameEvents.InvokeSecondUpdateTick(this.Monitor);
if (this.CurrentUpdateTick % 4 == 0)
@ -689,6 +692,7 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log($"The {activeClickableMenu.GetType().FullName} menu crashed while drawing itself during save. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error);
activeClickableMenu.exitThisMenu();
}
this.RaisePostRender();
Game1.spriteBatch.End();
}
//base.Draw(gameTime);
@ -712,6 +716,7 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error);
Game1.activeClickableMenu.exitThisMenu();
}
this.RaisePostRender();
Game1.spriteBatch.End();
if ((double)Game1.options.zoomLevel != 1.0)
{
@ -721,11 +726,12 @@ namespace StardewModdingAPI.Framework
Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
Game1.spriteBatch.End();
}
if (Game1.overlayMenu == null)
return;
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End();
if (Game1.overlayMenu != null)
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End();
}
}
else if ((int)Game1.gameMode == 11)
{
@ -733,6 +739,7 @@ namespace StardewModdingAPI.Framework
Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink);
Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, (int)byte.MaxValue, 0));
Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White);
this.RaisePostRender();
Game1.spriteBatch.End();
}
else if (Game1.currentMinigame != null)
@ -744,6 +751,7 @@ namespace StardewModdingAPI.Framework
Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha));
Game1.spriteBatch.End();
}
this.RaisePostRender(needsNewBatch: true);
if ((double)Game1.options.zoomLevel != 1.0)
{
this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null);
@ -752,11 +760,12 @@ namespace StardewModdingAPI.Framework
Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
Game1.spriteBatch.End();
}
if (Game1.overlayMenu == null)
return;
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End();
if (Game1.overlayMenu != null)
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End();
}
}
else if (Game1.showingEndOfNightStuff)
{
@ -775,6 +784,7 @@ namespace StardewModdingAPI.Framework
Game1.activeClickableMenu.exitThisMenu();
}
}
this.RaisePostRender();
Game1.spriteBatch.End();
if ((double)Game1.options.zoomLevel != 1.0)
{
@ -784,11 +794,12 @@ namespace StardewModdingAPI.Framework
Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
Game1.spriteBatch.End();
}
if (Game1.overlayMenu == null)
return;
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End();
if (Game1.overlayMenu != null)
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End();
}
}
else if ((int)Game1.gameMode == 6)
{
@ -806,6 +817,7 @@ namespace StardewModdingAPI.Framework
int x = 64;
int y = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - height;
SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str5, -1);
this.RaisePostRender();
Game1.spriteBatch.End();
if ((double)Game1.options.zoomLevel != 1.0)
{
@ -815,11 +827,12 @@ namespace StardewModdingAPI.Framework
Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f);
Game1.spriteBatch.End();
}
if (Game1.overlayMenu == null)
return;
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End();
if (Game1.overlayMenu != null)
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End();
}
}
else
{
@ -1265,6 +1278,8 @@ namespace StardewModdingAPI.Framework
}
else if (Game1.farmEvent != null)
Game1.farmEvent.drawAboveEverything(Game1.spriteBatch);
this.RaisePostRender();
Game1.spriteBatch.End();
if (Game1.overlayMenu != null)
{
@ -1272,14 +1287,6 @@ namespace StardewModdingAPI.Framework
Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End();
}
if (GraphicsEvents.HasPostRenderListeners())
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor);
Game1.spriteBatch.End();
}
this.renderScreenBuffer();
}
}
@ -1401,5 +1408,19 @@ namespace StardewModdingAPI.Framework
hash ^= v.GetHashCode();
return hash;
}
/// <summary>Raise the <see cref="GraphicsEvents.OnPostRenderEvent"/> if there are any listeners.</summary>
/// <param name="needsNewBatch">Whether to create a new sprite batch.</param>
private void RaisePostRender(bool needsNewBatch = false)
{
if (GraphicsEvents.HasPostRenderListeners())
{
if (needsNewBatch)
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor);
if (needsNewBatch)
Game1.spriteBatch.End();
}
}
}
}

View File

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

View File

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
namespace StardewModdingAPI
{
@ -16,5 +16,14 @@ namespace StardewModdingAPI
/// <summary>Get whether a mod has been loaded.</summary>
/// <param name="uniqueID">The mod's unique ID.</param>
bool IsLoaded(string uniqueID);
/// <summary>Get the API provided by a mod, or <c>null</c> if it has none. This signature requires using the <see cref="IModHelper.Reflection"/> API to access the API's properties and methods.</summary>
/// <param name="uniqueID">The mod's unique ID.</param>
object GetApi(string uniqueID);
/// <summary>Get the API provided by a mod, mapped to a given interface which specifies the expected properties and methods. If the mod has no API or it's not compatible with the given interface, get <c>null</c>.</summary>
/// <typeparam name="TInterface">The interface which matches the properties and methods you intend to access.</typeparam>
/// <param name="uniqueID">The mod's unique ID.</param>
TInterface GetApi<TInterface>(string uniqueID) where TInterface : class;
}
}

View File

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

View File

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

View File

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

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
{
/// <summary>Provides an API for accessing private game code.</summary>
/// <summary>Provides an API for accessing inaccessible code.</summary>
public interface IReflectionHelper : IModLinked
{
/*********
** Public methods
*********/
/// <summary>Get an instance field.</summary>
/// <typeparam name="TValue">The field type.</typeparam>
/// <param name="obj">The object which has the field.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the field is not found.</param>
IReflectedField<TValue> GetField<TValue>(object obj, string name, bool required = true);
/// <summary>Get a static field.</summary>
/// <typeparam name="TValue">The field type.</typeparam>
/// <param name="type">The type which has the field.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the field is not found.</param>
IReflectedField<TValue> GetField<TValue>(Type type, string name, bool required = true);
/// <summary>Get an instance property.</summary>
/// <typeparam name="TValue">The property type.</typeparam>
/// <param name="obj">The object which has the property.</param>
/// <param name="name">The property name.</param>
/// <param name="required">Whether to throw an exception if the property is not found.</param>
IReflectedProperty<TValue> GetProperty<TValue>(object obj, string name, bool required = true);
/// <summary>Get a static property.</summary>
/// <typeparam name="TValue">The property type.</typeparam>
/// <param name="type">The type which has the property.</param>
/// <param name="name">The property name.</param>
/// <param name="required">Whether to throw an exception if the property is not found.</param>
IReflectedProperty<TValue> GetProperty<TValue>(Type type, string name, bool required = true);
/// <summary>Get an instance method.</summary>
/// <param name="obj">The object which has the method.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the field is not found.</param>
IReflectedMethod GetMethod(object obj, string name, bool required = true);
/// <summary>Get a static method.</summary>
/// <param name="type">The type which has the method.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the field is not found.</param>
IReflectedMethod GetMethod(Type type, string name, bool required = true);
/*****
** Obsolete
*****/
/// <summary>Get a private instance field.</summary>
/// <typeparam name="TValue">The field type.</typeparam>
/// <param name="obj">The object which has the field.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " instead")]
IPrivateField<TValue> GetPrivateField<TValue>(object obj, string name, bool required = true);
/// <summary>Get a private static field.</summary>
@ -20,6 +64,7 @@ namespace StardewModdingAPI
/// <param name="type">The type which has the field.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " instead")]
IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true);
/// <summary>Get a private instance property.</summary>
@ -27,6 +72,7 @@ namespace StardewModdingAPI
/// <param name="obj">The object which has the property.</param>
/// <param name="name">The property name.</param>
/// <param name="required">Whether to throw an exception if the private property is not found.</param>
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")]
IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true);
/// <summary>Get a private static property.</summary>
@ -34,6 +80,7 @@ namespace StardewModdingAPI
/// <param name="type">The type which has the property.</param>
/// <param name="name">The property name.</param>
/// <param name="required">Whether to throw an exception if the private property is not found.</param>
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")]
IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true);
/// <summary>Get the value of a private instance field.</summary>
@ -42,6 +89,7 @@ namespace StardewModdingAPI
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
/// <remarks>This is a shortcut for <see cref="GetPrivateField{TValue}(object,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>.</remarks>
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " or " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")]
TValue GetPrivateValue<TValue>(object obj, string name, bool required = true);
/// <summary>Get the value of a private static field.</summary>
@ -50,18 +98,21 @@ namespace StardewModdingAPI
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
/// <remarks>This is a shortcut for <see cref="GetPrivateField{TValue}(Type,string,bool)"/> followed by <see cref="IPrivateField{TValue}.GetValue"/>.</remarks>
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetField) + " or " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetProperty) + " instead")]
TValue GetPrivateValue<TValue>(Type type, string name, bool required = true);
/// <summary>Get a private instance method.</summary>
/// <param name="obj">The object which has the method.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetMethod) + " instead")]
IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true);
/// <summary>Get a private static method.</summary>
/// <param name="type">The type which has the method.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
[Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetMethod) + " instead")]
IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true);
}
}

View File

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

View File

@ -25,6 +25,9 @@ namespace StardewModdingAPI
/// <param name="helper">Provides simplified APIs for writing mods.</param>
public abstract void Entry(IModHelper helper);
/// <summary>Get an API that other mods can access. This is always called after <see cref="Entry"/>.</summary>
public virtual object GetApi() => null;
/// <summary>Release or reset unmanaged resources.</summary>
public void Dispose()
{

View File

@ -247,7 +247,7 @@ namespace StardewModdingAPI
this.IsDisposed = true;
// dispose mod data
foreach (IModMetadata mod in this.ModRegistry.GetMods())
foreach (IModMetadata mod in this.ModRegistry.GetAll())
{
try
{
@ -374,7 +374,7 @@ namespace StardewModdingAPI
}
// update window titles
int modsLoaded = this.ModRegistry.GetMods().Count();
int modsLoaded = this.ModRegistry.GetAll().Count();
this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods";
Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods";
@ -390,7 +390,7 @@ namespace StardewModdingAPI
LocalizedContentManager.LanguageCode languageCode = this.ContentManager.GetCurrentLanguage();
// update mod translation helpers
foreach (IModMetadata mod in this.ModRegistry.GetMods())
foreach (IModMetadata mod in this.ModRegistry.GetAll())
(mod.Mod.Helper.Translation as TranslationHelper)?.SetLocale(locale, languageCode);
}
@ -500,12 +500,11 @@ namespace StardewModdingAPI
{
// create client
WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion);
this.Monitor.Log("Checking for updates...", LogLevel.Trace);
// check SMAPI version
try
{
this.Monitor.Log("Checking for SMAPI update...", LogLevel.Trace);
ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value;
if (response.Error != null)
{
@ -515,7 +514,7 @@ namespace StardewModdingAPI
else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion))
this.Monitor.Log($"You can update SMAPI to {response.Version}: {response.Url}", LogLevel.Alert);
else
this.VerboseLog(" OK.");
this.Monitor.Log(" SMAPI okay.", LogLevel.Trace);
}
catch (Exception ex)
{
@ -527,94 +526,101 @@ namespace StardewModdingAPI
}
// check mod versions
try
if (mods.Any())
{
// log issues
if (this.Settings.VerboseLogging)
try
{
this.VerboseLog("Validating mod update keys...");
foreach (IModMetadata mod in mods)
// prepare update keys
Dictionary<string, IModMetadata[]> modsByKey =
(
from mod in mods
where mod.Manifest?.UpdateKeys != null
from key in mod.Manifest.UpdateKeys
select new { key, mod }
)
.GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase)
.ToDictionary(
group => group.Key,
group => group.Select(p => p.mod).ToArray(),
StringComparer.InvariantCultureIgnoreCase
);
// report update keys
{
if (mod.Manifest == null)
this.VerboseLog($" {mod.DisplayName}: no manifest.");
else if (mod.Manifest.UpdateKeys == null || !mod.Manifest.UpdateKeys.Any())
this.VerboseLog($" {mod.DisplayName}: no update keys.");
IModMetadata[] modsWithoutKeys = (
from mod in mods
where
mod.Manifest != null
&& (mod.Manifest.UpdateKeys == null || !mod.Manifest.UpdateKeys.Any())
&& (mod.Manifest?.UniqueID != "SMAPI.ConsoleCommands" && mod.Manifest?.UniqueID != "SMAPI.TrainerMod")
orderby mod.DisplayName
select mod
).ToArray();
string message = $"Checking {modsByKey.Count} mod update keys.";
if (modsWithoutKeys.Any())
message += $" {modsWithoutKeys.Length} mods have no update keys: {string.Join(", ", modsWithoutKeys.Select(p => p.DisplayName))}.";
this.Monitor.Log($" {message}", LogLevel.Trace);
}
// fetch results
var results =
(
from entry in client.GetModInfo(modsByKey.Keys.ToArray())
from mod in modsByKey[entry.Key]
orderby mod.DisplayName
select new { entry.Key, Mod = mod, Info = entry.Value }
)
.ToArray();
// extract latest versions
IDictionary<IModMetadata, ModInfoModel> updatesByMod = new Dictionary<IModMetadata, ModInfoModel>();
foreach (var result in results)
{
IModMetadata mod = result.Mod;
ModInfoModel info = result.Info;
// handle error
if (info.Error != null)
{
this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {info.Error}", LogLevel.Trace);
continue;
}
// track update
ISemanticVersion localVersion = mod.DataRecord != null
? new SemanticVersion(mod.DataRecord.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString()))
: mod.Manifest.Version;
ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord != null
? mod.DataRecord.GetRemoteVersionForUpdateChecks(new SemanticVersion(info.Version).ToString())
: info.Version
);
bool isUpdate = latestVersion.IsNewerThan(localVersion);
this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {info.Version}{(!latestVersion.Equals(new SemanticVersion(info.Version)) ? $" [{latestVersion}]" : "")}" : "okay")}.");
if (isUpdate)
{
if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version))
updatesByMod[mod] = info;
}
}
// output
if (updatesByMod.Any())
{
this.Monitor.Newline();
this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert);
foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName))
this.Monitor.Log($" {entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}", LogLevel.Alert);
}
}
// prepare update keys
Dictionary<string, IModMetadata[]> modsByKey =
(
from mod in mods
where mod.Manifest?.UpdateKeys != null
from key in mod.Manifest.UpdateKeys
select new { key, mod }
)
.GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase)
.ToDictionary(
group => group.Key,
group => group.Select(p => p.mod).ToArray(),
StringComparer.InvariantCultureIgnoreCase
catch (Exception ex)
{
this.Monitor.Log("Couldn't check for new mod versions. This won't affect your game, but you won't be notified of mod updates if this keeps happening.", LogLevel.Warn);
this.Monitor.Log(ex is WebException && ex.InnerException == null
? ex.Message
: ex.ToString()
);
// fetch results
this.Monitor.Log($"Checking for updates to {modsByKey.Keys.Count} keys...", LogLevel.Trace);
var results =
(
from entry in client.GetModInfo(modsByKey.Keys.ToArray())
from mod in modsByKey[entry.Key]
orderby mod.DisplayName
select new { entry.Key, Mod = mod, Info = entry.Value }
)
.ToArray();
// extract latest versions
IDictionary<IModMetadata, ModInfoModel> updatesByMod = new Dictionary<IModMetadata, ModInfoModel>();
foreach (var result in results)
{
IModMetadata mod = result.Mod;
ModInfoModel info = result.Info;
// handle error
if (info.Error != null)
{
this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {info.Error}", LogLevel.Trace);
continue;
}
// track update
ISemanticVersion localVersion = mod.DataRecord != null
? new SemanticVersion(mod.DataRecord.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString()))
: mod.Manifest.Version;
ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord != null
? mod.DataRecord.GetRemoteVersionForUpdateChecks(new SemanticVersion(info.Version).ToString())
: info.Version
);
bool isUpdate = latestVersion.IsNewerThan(localVersion);
this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {info.Version}{(!latestVersion.Equals(new SemanticVersion(info.Version)) ? $" [{latestVersion}]" : "")}" : "OK")}.");
if (isUpdate)
{
if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version))
updatesByMod[mod] = info;
}
}
// output
if (updatesByMod.Any())
{
this.Monitor.Newline();
this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert);
foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName))
this.Monitor.Log($" {entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}", LogLevel.Alert);
}
}
catch (Exception ex)
{
this.Monitor.Log("Couldn't check for new mod versions. This won't affect your game, but you won't be notified of mod updates if this keeps happening.", LogLevel.Warn);
this.Monitor.Log(ex is WebException && ex.InnerException == null
? ex.Message
: ex.ToString()
);
}
}).Start();
}
@ -649,6 +655,7 @@ namespace StardewModdingAPI
AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode);
AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name);
InterfaceProxyBuilder proxyBuilder = new InterfaceProxyBuilder();
foreach (IModMetadata metadata in mods)
{
// get basic info
@ -690,53 +697,30 @@ namespace StardewModdingAPI
continue;
}
// validate assembly
try
{
int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract);
if (modEntries == 0)
{
TrackSkip(metadata, $"its DLL has no '{nameof(Mod)}' subclass.");
continue;
}
if (modEntries > 1)
{
TrackSkip(metadata, $"its DLL contains multiple '{nameof(Mod)}' subclasses.");
continue;
}
}
catch (Exception ex)
{
TrackSkip(metadata, $"its DLL couldn't be loaded:\n{ex.GetLogSummary()}");
continue;
}
// initialise mod
try
{
// get implementation
TypeInfo modEntryType = modAssembly.DefinedTypes.First(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract);
Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString());
if (mod == null)
// init mod helpers
IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName);
IModHelper modHelper;
{
TrackSkip(metadata, "its entry class couldn't be instantiated.");
continue;
}
// inject data
{
IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName);
ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager);
IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor);
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection);
IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry);
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager);
IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyBuilder, monitor);
ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage());
mod.ModManifest = manifest;
mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper);
mod.Monitor = monitor;
modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper);
}
// get mod instance
if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod))
continue;
// init mod
mod.ModManifest = manifest;
mod.Helper = modHelper;
mod.Monitor = monitor;
// track mod
metadata.SetMod(mod);
this.ModRegistry.Add(metadata);
@ -747,7 +731,7 @@ namespace StardewModdingAPI
}
}
}
IModMetadata[] loadedMods = this.ModRegistry.GetMods().ToArray();
IModMetadata[] loadedMods = this.ModRegistry.GetAll().ToArray();
// log skipped mods
this.Monitor.Newline();
@ -811,6 +795,19 @@ namespace StardewModdingAPI
{
this.Monitor.Log($"{metadata.DisplayName} failed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error);
}
// get mod API
try
{
object api = metadata.Mod.GetApi();
if (api != null)
this.Monitor.Log($" Found mod-provided API ({api.GetType().FullName}).", LogLevel.Trace);
metadata.SetApi(api);
}
catch (Exception ex)
{
this.Monitor.Log($"Failed loading mod-provided API for {metadata.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error);
}
}
// invalidate cache entries when needed
@ -846,13 +843,48 @@ namespace StardewModdingAPI
this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace);
this.ContentManager.InvalidateCacheFor(editors, loaders);
}
// unlock mod integrations
this.ModRegistry.AreAllModsInitialised = true;
}
/// <summary>Load a mod's entry class.</summary>
/// <param name="modAssembly">The mod assembly.</param>
/// <param name="onError">A callback invoked when loading fails.</param>
/// <param name="mod">The loaded instance.</param>
private bool TryLoadModEntry(Assembly modAssembly, Action<string> onError, out Mod mod)
{
mod = null;
// find type
TypeInfo[] modEntries = modAssembly.DefinedTypes.Where(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract).Take(2).ToArray();
if (modEntries.Length == 0)
{
onError($"its DLL has no '{nameof(Mod)}' subclass.");
return false;
}
if (modEntries.Length > 1)
{
onError($"its DLL contains multiple '{nameof(Mod)}' subclasses.");
return false;
}
// get implementation
mod = (Mod)modAssembly.CreateInstance(modEntries[0].ToString());
if (mod == null)
{
onError("its entry class couldn't be instantiated.");
return false;
}
return true;
}
/// <summary>Reload translations for all mods.</summary>
private void ReloadTranslations()
{
JsonHelper jsonHelper = new JsonHelper();
foreach (IModMetadata metadata in this.ModRegistry.GetMods())
foreach (IModMetadata metadata in this.ModRegistry.GetAll())
{
// read translation files
IDictionary<string, IDictionary<string, string>> translations = new Dictionary<string, IDictionary<string, string>>();

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
"UpdateKeys": [ "Nexus:1401" ]
},
{
// TrainerMod
"ID": "SMAPI.TrainerMod",
"Compatibility": {
"~": {
"Status": "Obsolete",
"ReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer."
}
}
},
{
// Tree Transplant
"ID": "TreeTransplant",

View File

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