Merge branch 'develop' of https://github.com/Pathoschild/SMAPI into harmony2

 Conflicts:
	src/SMAPI/Constants.cs
	src/SMAPI/Framework/SCore.cs
	src/SMAPI/SMAPI.csproj
This commit is contained in:
ZaneYork 2020-06-22 09:49:35 +08:00
commit 2bcee41151
23 changed files with 283 additions and 140 deletions

3
.gitignore vendored
View File

@ -30,9 +30,6 @@ _ReSharper*/
# sensitive files # sensitive files
appsettings.Development.json appsettings.Development.json
# generated build files
build/0Harmony.*
# Azure generated files # Azure generated files
src/SMAPI.Web/Properties/PublishProfiles/*.pubxml src/SMAPI.Web/Properties/PublishProfiles/*.pubxml

BIN
build/0Harmony.dll Normal file

Binary file not shown.

View File

@ -4,7 +4,7 @@
<!--set properties --> <!--set properties -->
<PropertyGroup> <PropertyGroup>
<Version>3.5.0</Version> <Version>3.6.1</Version>
<Product>SMAPI</Product> <Product>SMAPI</Product>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>

View File

@ -1,45 +1,58 @@
&larr; [README](README.md) &larr; [README](README.md)
# Release notes # Release notes
## Upcoming release + 1 <!--
## Future release
* For modders: * For modders:
* Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info).
* Added `harmony_summary` console command which lists all current Harmony patches, optionally with a search filter. -->
## 3.6.1
Released 21 June 2020 for Stardew Valley 1.4.1 or later.
* Fixed event priority sorting.
## 3.6
Released 20 June 2020 for Stardew Valley 1.4.1 or later.
## Upcoming release
* For players: * For players:
* Mod warnings are now listed alphabetically. * Added crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute.
* Added experimental option to reduce startup time when loading mod DLLs (thanks to ZaneYork!). Enable `RewriteInParallel` in the `smapi-internal/config.json` to try it.
* Reduced processing time when a mod loads many unpacked images (thanks to Entoarox!).
* Mod load warnings are now listed alphabetically.
* MacOS files starting with `._` are now ignored and can no longer cause skipped mods. * MacOS files starting with `._` are now ignored and can no longer cause skipped mods.
* Simplified paranoid warning logs and reduced their log level. * Simplified paranoid warning logs and reduced their log level.
* Reduced startup time when loading mod DLLs (thanks to ZaneYork!).
* Reduced processing time when a mod loads many unpacked images (thanks to Entoarox!).
* Fixed `BadImageFormatException` error detection.
* Fixed black maps on Android for mods which use `.tmx` files. * Fixed black maps on Android for mods which use `.tmx` files.
* Fixed `BadImageFormatException` error detection.
* Fixed `reload_i18n` command not reloading content pack translations.
* For the web UI: * For the web UI:
* Added GitHub licenses to mod compatibility list. * Added GitHub licenses to mod compatibility list.
* Improved JSON validator:
* added SMAPI `i18n` schema;
* editing an uploaded file now remembers the selected schema;
* changed default schema to plain JSON.
* Updated ModDrop URLs. * Updated ModDrop URLs.
* Internal changes to improve performance and reliability. * Internal changes to improve performance and reliability.
* For modders: * For modders:
* Added [event priorities](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Custom_priority) (thanks to spacechase0!). * Added [event priorities](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Custom_priority) (thanks to spacechase0!).
* Added [update subkeys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Update_subkeys). * Added [update subkeys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Update_subkeys).
* Added [a custom build of Harmony](https://github.com/Pathoschild/Harmony#readme) to provide more useful stack traces in error logs.
* Added `harmony_summary` console command to list or search current Harmony patches.
* Added `Multiplayer.PeerConnected` event. * Added `Multiplayer.PeerConnected` event.
* Added ability to override update keys from the compatibility list. * Added support for overriding update keys from the wiki compatibility list.
* Harmony mods which use the `[HarmonyPatch(type)]` attribute now work crossplatform. Previously SMAPI couldn't rewrite types in custom attributes for compatibility. * Improved mod rewriting for compatibility to support more cases (e.g. custom attributes and generic types).
* Improved mod rewriting for compatibility: * Fixed `helper.Reflection` blocking access to game methods/properties intercepted by SMAPI.
* Fixed rewriting types in custom attributes.
* Fixed rewriting generic types to method references.
* Fixed `helper.Reflection` blocking access to game methods/properties that were extended by SMAPI.
* Fixed asset propagation for Gil's portraits. * Fixed asset propagation for Gil's portraits.
* Fixed `.pdb` files ignored for error stack traces for mods rewritten by SMAPI. * Fixed `.pdb` files ignored for error stack traces when mods are rewritten by SMAPI.
* Fixed `ModMessageReceived` event handlers not tracked for performance monitoring. * Fixed `ModMessageReceived` event handlers not tracked for performance monitoring.
* For SMAPI developers: * For SMAPI developers:
* Added support for bundling a custom Harmony build for upcoming use.
* Eliminated MongoDB storage in the web services, which complicated the code unnecessarily. The app still uses an abstract interface for storage, so we can wrap a distributed cache in the future if needed. * Eliminated MongoDB storage in the web services, which complicated the code unnecessarily. The app still uses an abstract interface for storage, so we can wrap a distributed cache in the future if needed.
* Overhauled update checks to simplify individual clients, centralize common logic, and enable upcoming features. * Overhauled update checks to simplify mod site integrations, centralize common logic, and enable upcoming features.
* Merged the separate legacy redirects app on AWS into the main app on Azure. * Merged the separate legacy redirects app on AWS into the main app on Azure.
* Changed SMAPI's Harmony ID from `io.smapi` to `SMAPI` for readability in Harmony summaries.
## 3.5 ## 3.5
Released 27 April 2020 for Stardew Valley 1.4.1 or later. Released 27 April 2020 for Stardew Valley 1.4.1 or later.

View File

@ -58,7 +58,7 @@ SMAPI uses a small number of conditional compilation constants, which you can se
flag | purpose flag | purpose
---- | ------- ---- | -------
`SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. `SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`.
`HARMONY_2` | Whether to enable experimental Harmony 2.0 support. Existing Harmony 1._x_ mods will be rewritten automatically for compatibility. `HARMONY_2` | Whether to enable experimental Harmony 2.0 support and rewrite existing Harmony 1._x_ mods for compatibility. Note that you need to replace `build/0Harmony.dll` with a Harmony 2.0 build (or switch to a package reference) to use this flag.
## For SMAPI developers ## For SMAPI developers
### Compiling from source ### Compiling from source
@ -81,8 +81,8 @@ To prepare a crossplatform SMAPI release, you'll need to compile it on two platf
[crossplatforming info](https://stardewvalleywiki.com/Modding:Modder_Guide/Test_and_Troubleshoot#Testing_on_all_platforms) [crossplatforming info](https://stardewvalleywiki.com/Modding:Modder_Guide/Test_and_Troubleshoot#Testing_on_all_platforms)
on the wiki for the first-time setup. on the wiki for the first-time setup.
1. Update the version number in `.root/build/common.targets` and `Constants::Version`. Make sure 1. Update the version numbers in `build/common.targets`, `Constants`, and the `manifest.json` for
you use a [semantic version](https://semver.org). Recommended format: bundled mods. Make sure you use a [semantic version](https://semver.org). Recommended format:
build type | format | example build type | format | example
:--------- | :----------------------- | :------ :--------- | :----------------------- | :------
@ -102,14 +102,10 @@ on the wiki for the first-time setup.
3. Rename the folders to `SMAPI <version> installer` and `SMAPI <version> installer for developers`. 3. Rename the folders to `SMAPI <version> installer` and `SMAPI <version> installer for developers`.
4. Zip the two folders. 4. Zip the two folders.
### Using a custom Harmony build ### Custom Harmony build
The official SMAPI releases include [a custom build of Harmony](https://github.com/Pathoschild/Harmony), SMAPI uses [a custom build of Harmony](https://github.com/Pathoschild/Harmony#readme), which is
but compiling from source will use the official build. To use a custom build, put `0Harmony.dll` in included in the `build` folder. To use a different build, just replace `0Harmony.dll` in that
the `build` folder and it'll be referenced automatically. folder.
Note that Harmony merges its dependencies into `0Harmony.dll` when compiled in release mode. To use
a debug build of Harmony, you'll need to manually copy those dependencies into your game's
`smapi-internal` folder.
## Release notes ## Release notes
See [release notes](../release-notes.md). See [release notes](../release-notes.md).

View File

@ -110,8 +110,9 @@ Available schemas:
format | schema URL format | schema URL
------ | ---------- ------ | ----------
[SMAPI `manifest.json`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest) | https://smapi.io/schemas/manifest.json [SMAPI: `manifest.json`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest) | https://smapi.io/schemas/manifest.json
[Content Patcher `content.json`](https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme) | https://smapi.io/schemas/content-patcher.json [SMAPI: translations (`i18n` folder)](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Translation) | https://smapi.io/schemas/i18n.json
[Content Patcher: `content.json`](https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme) | https://smapi.io/schemas/content-patcher.json
## Web API ## Web API
### Overview ### Overview

View File

@ -1,9 +1,9 @@
{ {
"Name": "Console Commands", "Name": "Console Commands",
"Author": "SMAPI", "Author": "SMAPI",
"Version": "3.5.0", "Version": "3.6.1",
"Description": "Adds SMAPI console commands that let you manipulate the game.", "Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands", "UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll", "EntryDll": "ConsoleCommands.dll",
"MinimumApiVersion": "3.5.0" "MinimumApiVersion": "3.6.1"
} }

View File

@ -1,9 +1,9 @@
{ {
"Name": "Save Backup", "Name": "Save Backup",
"Author": "SMAPI", "Author": "SMAPI",
"Version": "3.5.0", "Version": "3.6.1",
"Description": "Automatically backs up all your saves once per day into its folder.", "Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup", "UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll", "EntryDll": "SaveBackup.dll",
"MinimumApiVersion": "3.5.0" "MinimumApiVersion": "3.6.1"
} }

View File

@ -27,12 +27,13 @@ namespace StardewModdingAPI.Web.Controllers
private readonly IDictionary<string, string> SchemaFormats = new Dictionary<string, string> private readonly IDictionary<string, string> SchemaFormats = new Dictionary<string, string>
{ {
["none"] = "None", ["none"] = "None",
["manifest"] = "Manifest", ["manifest"] = "SMAPI: manifest",
["i18n"] = "SMAPI: translations (i18n)",
["content-patcher"] = "Content Patcher" ["content-patcher"] = "Content Patcher"
}; };
/// <summary>The schema ID to use if none was specified.</summary> /// <summary>The schema ID to use if none was specified.</summary>
private string DefaultSchemaID = "manifest"; private string DefaultSchemaID = "none";
/// <summary>A token in an error message which indicates that the child errors should be displayed instead.</summary> /// <summary>A token in an error message which indicates that the child errors should be displayed instead.</summary>
private readonly string TransparentToken = "$transparent"; private readonly string TransparentToken = "$transparent";
@ -57,16 +58,22 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Render the schema validator UI.</summary> /// <summary>Render the schema validator UI.</summary>
/// <param name="schemaName">The schema name with which to validate the JSON, or 'edit' to return to the edit screen.</param> /// <param name="schemaName">The schema name with which to validate the JSON, or 'edit' to return to the edit screen.</param>
/// <param name="id">The stored file ID.</param> /// <param name="id">The stored file ID.</param>
/// <param name="operation">The operation to perform for the selected log ID. This can be 'edit', or any other value to view.</param>
[HttpGet] [HttpGet]
[Route("json")] [Route("json")]
[Route("json/{schemaName}")] [Route("json/{schemaName}")]
[Route("json/{schemaName}/{id}")] [Route("json/{schemaName}/{id}")]
public async Task<ViewResult> Index(string schemaName = null, string id = null) [Route("json/{schemaName}/{id}/{operation}")]
public async Task<ViewResult> Index(string schemaName = null, string id = null, string operation = null)
{ {
// parse arguments
schemaName = this.NormalizeSchemaName(schemaName); schemaName = this.NormalizeSchemaName(schemaName);
bool hasId = !string.IsNullOrWhiteSpace(id);
bool isEditView = !hasId || operation?.Trim().ToLower() == "edit";
var result = new JsonValidatorModel(id, schemaName, this.SchemaFormats); // build result model
if (string.IsNullOrWhiteSpace(id)) var result = this.GetModel(id, schemaName, isEditView);
if (!hasId)
return this.View("Index", result); return this.View("Index", result);
// fetch raw JSON // fetch raw JSON
@ -76,7 +83,7 @@ namespace StardewModdingAPI.Web.Controllers
result.SetContent(file.Content, expiry: file.Expiry, uploadWarning: file.Warning); result.SetContent(file.Content, expiry: file.Expiry, uploadWarning: file.Warning);
// skip parsing if we're going to the edit screen // skip parsing if we're going to the edit screen
if (schemaName?.ToLower() == "edit") if (isEditView)
return this.View("Index", result); return this.View("Index", result);
// parse JSON // parse JSON
@ -130,7 +137,7 @@ namespace StardewModdingAPI.Web.Controllers
public async Task<ActionResult> PostAsync(JsonValidatorRequestModel request) public async Task<ActionResult> PostAsync(JsonValidatorRequestModel request)
{ {
if (request == null) if (request == null)
return this.View("Index", this.GetModel(null, null).SetUploadError("The request seems to be invalid.")); return this.View("Index", this.GetModel(null, null, isEditView: true).SetUploadError("The request seems to be invalid."));
// normalize schema name // normalize schema name
string schemaName = this.NormalizeSchemaName(request.SchemaName); string schemaName = this.NormalizeSchemaName(request.SchemaName);
@ -138,12 +145,12 @@ namespace StardewModdingAPI.Web.Controllers
// get raw text // get raw text
string input = request.Content; string input = request.Content;
if (string.IsNullOrWhiteSpace(input)) if (string.IsNullOrWhiteSpace(input))
return this.View("Index", this.GetModel(null, schemaName).SetUploadError("The JSON file seems to be empty.")); return this.View("Index", this.GetModel(null, schemaName, isEditView: true).SetUploadError("The JSON file seems to be empty."));
// upload file // upload file
UploadResult result = await this.Storage.SaveAsync(input); UploadResult result = await this.Storage.SaveAsync(input);
if (!result.Succeeded) if (!result.Succeeded)
return this.View("Index", this.GetModel(result.ID, schemaName).SetUploadError(result.UploadError)); return this.View("Index", this.GetModel(result.ID, schemaName, isEditView: true).SetContent(input, null).SetUploadError(result.UploadError));
// redirect to view // redirect to view
return this.Redirect(this.Url.PlainAction("Index", "JsonValidator", new { schemaName = schemaName, id = result.ID })); return this.Redirect(this.Url.PlainAction("Index", "JsonValidator", new { schemaName = schemaName, id = result.ID }));
@ -156,9 +163,10 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Build a JSON validator model.</summary> /// <summary>Build a JSON validator model.</summary>
/// <param name="pasteID">The stored file ID.</param> /// <param name="pasteID">The stored file ID.</param>
/// <param name="schemaName">The schema name with which the JSON was validated.</param> /// <param name="schemaName">The schema name with which the JSON was validated.</param>
private JsonValidatorModel GetModel(string pasteID, string schemaName) /// <param name="isEditView">Whether to show the edit view.</param>
private JsonValidatorModel GetModel(string pasteID, string schemaName, bool isEditView)
{ {
return new JsonValidatorModel(pasteID, schemaName, this.SchemaFormats); return new JsonValidatorModel(pasteID, schemaName, this.SchemaFormats, isEditView);
} }
/// <summary>Get a normalized schema name, or the <see cref="DefaultSchemaID"/> if blank.</summary> /// <summary>Get a normalized schema name, or the <see cref="DefaultSchemaID"/> if blank.</summary>

View File

@ -10,6 +10,9 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator
/********* /*********
** Accessors ** Accessors
*********/ *********/
/// <summary>Whether to show the edit view.</summary>
public bool IsEditView { get; set; }
/// <summary>The paste ID.</summary> /// <summary>The paste ID.</summary>
public string PasteID { get; set; } public string PasteID { get; set; }
@ -51,11 +54,13 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator
/// <param name="pasteID">The stored file ID.</param> /// <param name="pasteID">The stored file ID.</param>
/// <param name="schemaName">The schema name with which the JSON was validated.</param> /// <param name="schemaName">The schema name with which the JSON was validated.</param>
/// <param name="schemaFormats">The supported JSON schemas (names indexed by ID).</param> /// <param name="schemaFormats">The supported JSON schemas (names indexed by ID).</param>
public JsonValidatorModel(string pasteID, string schemaName, IDictionary<string, string> schemaFormats) /// <param name="isEditView">Whether to show the edit view.</param>
public JsonValidatorModel(string pasteID, string schemaName, IDictionary<string, string> schemaFormats, bool isEditView)
{ {
this.PasteID = pasteID; this.PasteID = pasteID;
this.SchemaName = schemaName; this.SchemaName = schemaName;
this.SchemaFormats = schemaFormats; this.SchemaFormats = schemaFormats;
this.IsEditView = isEditView;
} }
/// <summary>Set the validated content.</summary> /// <summary>Set the validated content.</summary>

View File

@ -9,7 +9,6 @@
string newUploadUrl = this.Url.PlainAction("Index", "JsonValidator", new { schemaName = Model.SchemaName }); string newUploadUrl = this.Url.PlainAction("Index", "JsonValidator", new { schemaName = Model.SchemaName });
string schemaDisplayName = null; string schemaDisplayName = null;
bool isValidSchema = Model.SchemaName != null && Model.SchemaFormats.TryGetValue(Model.SchemaName, out schemaDisplayName) && schemaDisplayName?.ToLower() != "none"; bool isValidSchema = Model.SchemaName != null && Model.SchemaFormats.TryGetValue(Model.SchemaName, out schemaDisplayName) && schemaDisplayName?.ToLower() != "none";
bool isEditView = Model.Content == null || Model.SchemaName?.ToLower() == "edit";
// build title // build title
ViewData["Title"] = "JSON validator"; ViewData["Title"] = "JSON validator";
@ -63,7 +62,7 @@ else if (Model.ParseError != null)
<small v-pre>Error details: @Model.ParseError</small> <small v-pre>Error details: @Model.ParseError</small>
</div> </div>
} }
else if (!isEditView && Model.PasteID != null) else if (!Model.IsEditView && Model.PasteID != null)
{ {
<div class="banner success"> <div class="banner success">
<strong>Share this link to let someone else see this page:</strong> <code>@curPageUrl</code><br /> <strong>Share this link to let someone else see this page:</strong> <code>@curPageUrl</code><br />
@ -84,7 +83,7 @@ else if (!isEditView && Model.PasteID != null)
} }
@* upload new file *@ @* upload new file *@
@if (isEditView) @if (Model.IsEditView)
{ {
<h2>Upload a JSON file</h2> <h2>Upload a JSON file</h2>
<form action="@this.Url.PlainAction("PostAsync", "JsonValidator")" method="post"> <form action="@this.Url.PlainAction("PostAsync", "JsonValidator")" method="post">
@ -112,7 +111,7 @@ else if (!isEditView && Model.PasteID != null)
} }
@* validation results *@ @* validation results *@
@if (!isEditView) @if (!Model.IsEditView)
{ {
<div id="output"> <div id="output">
@if (Model.UploadError == null) @if (Model.UploadError == null)
@ -158,7 +157,7 @@ else if (!isEditView && Model.PasteID != null)
{ {
<option value="@pair.Key" selected="@(Model.SchemaName == pair.Key)">@pair.Value</option> <option value="@pair.Key" selected="@(Model.SchemaName == pair.Key)">@pair.Value</option>
} }
</select>) or <a href="@(this.Url.PlainAction("Index", "JsonValidator", new { id = this.Model.PasteID, schemaName = "edit" }))">edit this file</a>. </select>) or <a href="@(this.Url.PlainAction("Index", "JsonValidator", new { id = this.Model.PasteID, schemaName = this.Model.SchemaName, operation = "edit" }))">edit this file</a>.
</div> </div>
<pre id="raw-content" class="sunlight-highlight-javascript">@Model.Content</pre> <pre id="raw-content" class="sunlight-highlight-javascript">@Model.Content</pre>

View File

@ -8,8 +8,8 @@
TimeSpan staleAge = DateTimeOffset.UtcNow - Model.LastUpdated; TimeSpan staleAge = DateTimeOffset.UtcNow - Model.LastUpdated;
bool hasBeta = true; // Model.BetaVersion != null; bool hasBeta = Model.BetaVersion != null;
string betaLabel = "SMAPI 3.6 only"; //"SDV @Model.BetaVersion only"; string betaLabel = "SDV @Model.BetaVersion only";
} }
@section Head { @section Head {
<link rel="stylesheet" href="~/Content/css/mods.css?r=20200218" /> <link rel="stylesheet" href="~/Content/css/mods.css?r=20200218" />

View File

@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://smapi.io/schemas/i18n.json",
"title": "SMAPI i18n file",
"description": "A translation file for a SMAPI mod or content pack.",
"@documentationUrl": "https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Translation",
"type": "object",
"properties": {
"$schema": {
"title": "Schema",
"description": "A reference to this JSON schema. Not part of the actual format, but useful for validation tools.",
"type": "string",
"const": "https://smapi.io/schemas/manifest.json"
}
},
"additionalProperties": {
"type": "string",
"@errorMessages": {
"type": "Invalid property. Translation files can only contain text property values."
}
}
}

View File

@ -24,7 +24,7 @@ namespace StardewModdingAPI
** Public ** Public
****/ ****/
/// <summary>SMAPI's current semantic version.</summary> /// <summary>SMAPI's current semantic version.</summary>
public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.6.0"); public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.6.1");
/// <summary>The minimum supported version of Stardew Valley.</summary> /// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.5"); public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.5");
@ -56,6 +56,14 @@ namespace StardewModdingAPI
/**** /****
** Internal ** Internal
****/ ****/
/// <summary>Whether SMAPI was compiled in debug mode.</summary>
internal const bool IsDebugBuild =
#if DEBUG
true;
#else
false;
#endif
/// <summary>The URL of the SMAPI home page.</summary> /// <summary>The URL of the SMAPI home page.</summary>
internal const string HomePageUrl = "https://smapi.io"; internal const string HomePageUrl = "https://smapi.io";

View File

@ -1,16 +1,27 @@
#if HARMONY_2
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
#if HARMONY_2
using HarmonyLib; using HarmonyLib;
#else
using Harmony;
#endif
namespace StardewModdingAPI.Framework.Commands namespace StardewModdingAPI.Framework.Commands
{ {
/// <summary>The 'harmony_summary' SMAPI console command.</summary> /// <summary>The 'harmony_summary' SMAPI console command.</summary>
internal class HarmonySummaryCommand : IInternalCommand internal class HarmonySummaryCommand : IInternalCommand
{ {
#if !HARMONY_2
/*********
** Fields
*********/
/// <summary>The Harmony instance through which to fetch patch info.</summary>
private readonly HarmonyInstance HarmonyInstance = HarmonyInstance.Create($"SMAPI.{nameof(HarmonySummaryCommand)}");
#endif
/********* /*********
** Accessors ** Accessors
*********/ *********/
@ -45,7 +56,16 @@ namespace StardewModdingAPI.Framework.Commands
foreach (var ownerGroup in match.PatchTypesByOwner.OrderBy(p => p.Key)) foreach (var ownerGroup in match.PatchTypesByOwner.OrderBy(p => p.Key))
{ {
var sortedTypes = ownerGroup.Value var sortedTypes = ownerGroup.Value
.OrderBy(p => p switch { PatchType.Prefix => 0, PatchType.Postfix => 1, PatchType.Finalizer => 2, PatchType.Transpiler => 3, _ => 4 }); .OrderBy(p => p switch
{
PatchType.Prefix => 0,
PatchType.Postfix => 1,
#if HARMONY_2
PatchType.Finalizer => 2,
#endif
PatchType.Transpiler => 3,
_ => 4
});
result.AppendLine($" - {ownerGroup.Key} ({string.Join(", ", sortedTypes).ToLower()})"); result.AppendLine($" - {ownerGroup.Key} ({string.Join(", ", sortedTypes).ToLower()})");
} }
@ -91,15 +111,26 @@ namespace StardewModdingAPI.Framework.Commands
/// <summary>Get all current Harmony patches.</summary> /// <summary>Get all current Harmony patches.</summary>
private IEnumerable<SearchResult> GetAllPatches() private IEnumerable<SearchResult> GetAllPatches()
{ {
#if HARMONY_2
foreach (MethodBase method in Harmony.GetAllPatchedMethods()) foreach (MethodBase method in Harmony.GetAllPatchedMethods())
#else
foreach (MethodBase method in this.HarmonyInstance.GetPatchedMethods())
#endif
{ {
// get metadata for method // get metadata for method
#if HARMONY_2
HarmonyLib.Patches patchInfo = Harmony.GetPatchInfo(method); HarmonyLib.Patches patchInfo = Harmony.GetPatchInfo(method);
#else
Harmony.Patches patchInfo = this.HarmonyInstance.GetPatchInfo(method);
#endif
IDictionary<PatchType, IReadOnlyCollection<Patch>> patchGroups = new Dictionary<PatchType, IReadOnlyCollection<Patch>> IDictionary<PatchType, IReadOnlyCollection<Patch>> patchGroups = new Dictionary<PatchType, IReadOnlyCollection<Patch>>
{ {
[PatchType.Prefix] = patchInfo.Prefixes, [PatchType.Prefix] = patchInfo.Prefixes,
[PatchType.Postfix] = patchInfo.Postfixes, [PatchType.Postfix] = patchInfo.Postfixes,
#if HARMONY_2
[PatchType.Finalizer] = patchInfo.Finalizers, [PatchType.Finalizer] = patchInfo.Finalizers,
#endif
[PatchType.Transpiler] = patchInfo.Transpilers [PatchType.Transpiler] = patchInfo.Transpilers
}; };
@ -129,8 +160,10 @@ namespace StardewModdingAPI.Framework.Commands
/// <summary>A postfix patch.</summary> /// <summary>A postfix patch.</summary>
Postfix, Postfix,
#if HARMONY_2
/// <summary>A finalizer patch.</summary> /// <summary>A finalizer patch.</summary>
Finalizer, Finalizer,
#endif
/// <summary>A transpiler patch.</summary> /// <summary>A transpiler patch.</summary>
Transpiler Transpiler
@ -167,4 +200,3 @@ namespace StardewModdingAPI.Framework.Commands
} }
} }
} }
#endif

View File

@ -47,7 +47,7 @@ namespace StardewModdingAPI.Framework.Events
if (!(obj is ManagedEventHandler<TEventArgs> other)) if (!(obj is ManagedEventHandler<TEventArgs> other))
throw new ArgumentException("Can't compare to an unrelated object type."); throw new ArgumentException("Can't compare to an unrelated object type.");
int priorityCompare = this.Priority.CompareTo(other.Priority); int priorityCompare = -this.Priority.CompareTo(other.Priority); // higher value = sort first
return priorityCompare != 0 return priorityCompare != 0
? priorityCompare ? priorityCompare
: this.RegistrationOrder.CompareTo(other.RegistrationOrder); : this.RegistrationOrder.CompareTo(other.RegistrationOrder);

View File

@ -76,9 +76,10 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="mod">The mod for which the assembly is being loaded.</param> /// <param name="mod">The mod for which the assembly is being loaded.</param>
/// <param name="assemblyPath">The assembly file path.</param> /// <param name="assemblyPath">The assembly file path.</param>
/// <param name="assumeCompatible">Assume the mod is compatible, even if incompatible code is detected.</param> /// <param name="assumeCompatible">Assume the mod is compatible, even if incompatible code is detected.</param>
/// <param name="rewriteInParallel">Whether to enable experimental parallel rewriting.</param>
/// <returns>Returns the rewrite metadata for the preprocessed assembly.</returns> /// <returns>Returns the rewrite metadata for the preprocessed assembly.</returns>
/// <exception cref="IncompatibleInstructionException">An incompatible CIL instruction was found while rewriting the assembly.</exception> /// <exception cref="IncompatibleInstructionException">An incompatible CIL instruction was found while rewriting the assembly.</exception>
public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible) public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible, bool rewriteInParallel)
{ {
// get referenced local assemblies // get referenced local assemblies
AssemblyParseResult[] assemblies; AssemblyParseResult[] assemblies;
@ -108,7 +109,7 @@ namespace StardewModdingAPI.Framework.ModLoading
continue; continue;
// rewrite assembly // rewrite assembly
bool changed = this.RewriteAssembly(mod, assembly.Definition, loggedMessages, logPrefix: " "); bool changed = this.RewriteAssembly(mod, assembly.Definition, loggedMessages, logPrefix: " ", rewriteInParallel);
// detect broken assembly reference // detect broken assembly reference
foreach (AssemblyNameReference reference in assembly.Definition.MainModule.AssemblyReferences) foreach (AssemblyNameReference reference in assembly.Definition.MainModule.AssemblyReferences)
@ -279,9 +280,10 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="assembly">The assembly to rewrite.</param> /// <param name="assembly">The assembly to rewrite.</param>
/// <param name="loggedMessages">The messages that have already been logged for this mod.</param> /// <param name="loggedMessages">The messages that have already been logged for this mod.</param>
/// <param name="logPrefix">A string to prefix to log messages.</param> /// <param name="logPrefix">A string to prefix to log messages.</param>
/// <param name="rewriteInParallel">Whether to enable experimental parallel rewriting.</param>
/// <returns>Returns whether the assembly was modified.</returns> /// <returns>Returns whether the assembly was modified.</returns>
/// <exception cref="IncompatibleInstructionException">An incompatible CIL instruction was found while rewriting the assembly.</exception> /// <exception cref="IncompatibleInstructionException">An incompatible CIL instruction was found while rewriting the assembly.</exception>
private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, HashSet<string> loggedMessages, string logPrefix) private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, HashSet<string> loggedMessages, string logPrefix, bool rewriteInParallel)
{ {
ModuleDefinition module = assembly.MainModule; ModuleDefinition module = assembly.MainModule;
string filename = $"{assembly.Name.Name}.dll"; string filename = $"{assembly.Name.Name}.dll";
@ -330,7 +332,7 @@ namespace StardewModdingAPI.Framework.ModLoading
return rewritten; return rewritten;
} }
); );
bool anyRewritten = rewriter.RewriteModule(); bool anyRewritten = rewriter.RewriteModule(rewriteInParallel);
// handle rewrite flags // handle rewrite flags
foreach (IInstructionHandler handler in handlers) foreach (IInstructionHandler handler in handlers)

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -56,15 +57,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
} }
/// <summary>Rewrite the loaded module code.</summary> /// <summary>Rewrite the loaded module code.</summary>
/// <param name="rewriteInParallel">Whether to enable experimental parallel rewriting.</param>
/// <returns>Returns whether the module was modified.</returns> /// <returns>Returns whether the module was modified.</returns>
public bool RewriteModule() public bool RewriteModule(bool rewriteInParallel)
{
IEnumerable<TypeDefinition> types = this.Module.GetTypes().Where(type => type.BaseType != null); // skip special types like <Module>
// experimental parallel rewriting
// This may cause intermittent startup errors and is disabled by default: https://github.com/Pathoschild/SMAPI/issues/721
if (rewriteInParallel)
{ {
int typesChanged = 0; int typesChanged = 0;
Exception exception = null; Exception exception = null;
Parallel.ForEach( Parallel.ForEach(types, type =>
source: this.Module.GetTypes().Where(type => type.BaseType != null), // skip special types like <Module>
body: type =>
{ {
if (exception != null) if (exception != null)
return; return;
@ -72,6 +78,51 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
bool changed = false; bool changed = false;
try try
{ {
changed = this.RewriteTypeDefinition(type);
}
catch (Exception ex)
{
exception ??= ex;
}
if (changed)
Interlocked.Increment(ref typesChanged);
});
return exception == null
? typesChanged > 0
: throw new Exception($"Rewriting {this.Module.Name} failed.", exception);
}
// non-parallel rewriting
{
bool changed = false;
try
{
foreach (var type in types)
changed |= this.RewriteTypeDefinition(type);
}
catch (Exception ex)
{
throw new Exception($"Rewriting {this.Module.Name} failed.", ex);
}
return changed;
}
}
/*********
** Private methods
*********/
/// <summary>Rewrite a loaded type definition.</summary>
/// <param name="type">The type definition to rewrite.</param>
/// <returns>Returns whether the type was modified.</returns>
private bool RewriteTypeDefinition(TypeDefinition type)
{
bool changed = false;
changed |= this.RewriteCustomAttributes(type.CustomAttributes); changed |= this.RewriteCustomAttributes(type.CustomAttributes);
changed |= this.RewriteGenericParameters(type.GenericParameters); changed |= this.RewriteGenericParameters(type.GenericParameters);
@ -116,26 +167,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
} }
} }
} }
}
catch (Exception ex) return changed;
{
exception ??= ex;
} }
if (changed)
Interlocked.Increment(ref typesChanged);
}
);
return exception == null
? typesChanged > 0
: throw new Exception($"Rewriting {this.Module.Name} failed.", exception);
}
/*********
** Private methods
*********/
/// <summary>Rewrite a CIL instruction if needed.</summary> /// <summary>Rewrite a CIL instruction if needed.</summary>
/// <param name="instruction">The current CIL instruction.</param> /// <param name="instruction">The current CIL instruction.</param>
/// <param name="cil">The CIL instruction processor.</param> /// <param name="cil">The CIL instruction processor.</param>

View File

@ -15,12 +15,8 @@ namespace StardewModdingAPI.Framework.Models
private static readonly IDictionary<string, object> DefaultValues = new Dictionary<string, object> private static readonly IDictionary<string, object> DefaultValues = new Dictionary<string, object>
{ {
[nameof(CheckForUpdates)] = true, [nameof(CheckForUpdates)] = true,
[nameof(ParanoidWarnings)] = [nameof(ParanoidWarnings)] = Constants.IsDebugBuild,
#if DEBUG [nameof(RewriteInParallel)] = Constants.IsDebugBuild,
true,
#else
false,
#endif
[nameof(UseBetaChannel)] = Constants.ApiVersion.IsPrerelease(), [nameof(UseBetaChannel)] = Constants.ApiVersion.IsPrerelease(),
[nameof(GitHubProjectName)] = "MartyrPher/SMAPI-Android-Installer", [nameof(GitHubProjectName)] = "MartyrPher/SMAPI-Android-Installer",
[nameof(WebApiBaseUrl)] = "https://smapi.io/api/", [nameof(WebApiBaseUrl)] = "https://smapi.io/api/",
@ -48,6 +44,9 @@ namespace StardewModdingAPI.Framework.Models
/// <summary>Whether to check for newer versions of SMAPI and mods on startup.</summary> /// <summary>Whether to check for newer versions of SMAPI and mods on startup.</summary>
public bool CheckForUpdates { get; set; } public bool CheckForUpdates { get; set; }
/// <summary>Whether to enable experimental parallel rewriting.</summary>
public bool RewriteInParallel { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.RewriteInParallel)];
/// <summary>Whether to add a section to the 'mod issues' list for mods which which directly use potentially sensitive .NET APIs like file or shell access.</summary> /// <summary>Whether to add a section to the 'mod issues' list for mods which which directly use potentially sensitive .NET APIs like file or shell access.</summary>
public bool ParanoidWarnings { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.ParanoidWarnings)]; public bool ParanoidWarnings { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.ParanoidWarnings)];

View File

@ -2,6 +2,7 @@ using System;
#if HARMONY_2 #if HARMONY_2
using HarmonyLib; using HarmonyLib;
#else #else
using MonoMod.RuntimeDetour;
using Harmony; using Harmony;
#endif #endif
@ -34,6 +35,13 @@ namespace StardewModdingAPI.Framework.Patching
#if HARMONY_2 #if HARMONY_2
Harmony harmony = new Harmony("SMAPI"); Harmony harmony = new Harmony("SMAPI");
#else #else
if (!HarmonyDetourBridge.Initialized && Constants.HarmonyEnabled)
{
try {
HarmonyDetourBridge.Init();
}
catch { Constants.HarmonyEnabled = false; }
}
HarmonyInstance harmony = HarmonyInstance.Create("SMAPI"); HarmonyInstance harmony = HarmonyInstance.Create("SMAPI");
#endif #endif
foreach (IHarmonyPatch patch in patches) foreach (IHarmonyPatch patch in patches)

View File

@ -347,6 +347,8 @@ namespace StardewModdingAPI.Framework
// add headers // add headers
if (this.Settings.DeveloperMode) if (this.Settings.DeveloperMode)
this.Monitor.Log($"You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); this.Monitor.Log($"You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info);
if (this.Settings.RewriteInParallel)
this.Monitor.Log($"You enabled experimental parallel rewriting. This may result in faster startup times, but intermittent startup errors. You can disable it by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Info);
if (!this.Settings.CheckForUpdates) if (!this.Settings.CheckForUpdates)
this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn);
//if (!this.Monitor.WriteToConsole) //if (!this.Monitor.WriteToConsole)
@ -489,9 +491,7 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log("Type 'help' for help, or 'help <cmd>' for a command's usage", LogLevel.Info); this.Monitor.Log("Type 'help' for help, or 'help <cmd>' for a command's usage", LogLevel.Info);
this.GameInstance.CommandManager this.GameInstance.CommandManager
.Add(new HelpCommand(this.GameInstance.CommandManager), this.Monitor) .Add(new HelpCommand(this.GameInstance.CommandManager), this.Monitor)
#if HARMONY_2
.Add(new HarmonySummaryCommand(), this.Monitor) .Add(new HarmonySummaryCommand(), this.Monitor)
#endif
.Add(new ReloadI18nCommand(this.ReloadTranslations), this.Monitor); .Add(new ReloadI18nCommand(this.ReloadTranslations), this.Monitor);
// update window titles // update window titles
@ -540,9 +540,7 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log("Type 'help' for help, or 'help <cmd>' for a command's usage", LogLevel.Info); this.Monitor.Log("Type 'help' for help, or 'help <cmd>' for a command's usage", LogLevel.Info);
this.GameInstance.CommandManager this.GameInstance.CommandManager
.Add(new HelpCommand(this.GameInstance.CommandManager), this.Monitor) .Add(new HelpCommand(this.GameInstance.CommandManager), this.Monitor)
#if HARMONY_2
.Add(new HarmonySummaryCommand(), this.Monitor) .Add(new HarmonySummaryCommand(), this.Monitor)
#endif
.Add(new ReloadI18nCommand(this.ReloadTranslations), this.Monitor); .Add(new ReloadI18nCommand(this.ReloadTranslations), this.Monitor);
// start handling command line input // start handling command line input
@ -1013,7 +1011,7 @@ namespace StardewModdingAPI.Framework
Assembly modAssembly; Assembly modAssembly;
try try
{ {
modAssembly = assemblyLoader.Load(mod, assemblyPath, assumeCompatible: true);//mod.DataRecord?.Status == ModStatus.AssumeCompatible); modAssembly = assemblyLoader.Load(mod, assemblyPath, assumeCompatible: true/*mod.DataRecord?.Status == ModStatus.AssumeCompatible*/, rewriteInParallel: this.Settings.RewriteInParallel);
this.ModRegistry.TrackAssemblies(mod, modAssembly); this.ModRegistry.TrackAssemblies(mod, modAssembly);
} }
catch (IncompatibleInstructionException) // details already in trace logs catch (IncompatibleInstructionException) // details already in trace logs
@ -1310,7 +1308,7 @@ namespace StardewModdingAPI.Framework
/// <summary>Reload translations for all mods.</summary> /// <summary>Reload translations for all mods.</summary>
private void ReloadTranslations() private void ReloadTranslations()
{ {
this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); this.ReloadTranslations(this.ModRegistry.GetAll());
} }
/// <summary>Reload translations for the given mods.</summary> /// <summary>Reload translations for the given mods.</summary>

View File

@ -33,18 +33,30 @@ copy all the settings, or you may cause bugs due to overridden changes in future
*/ */
"DeveloperMode": true, "DeveloperMode": true,
/**
* Whether to enable experimental parallel rewriting when SMAPI is loading mods. This can
* reduce startup time when you have many mods installed, but is experimental and may cause
* intermittent startup errors.
*
* When this is commented out, it'll be true for local debug builds and false otherwise.
*/
//"RewriteInParallel": false,
/** /**
* Whether to add a section to the 'mod issues' list for mods which directly use potentially * Whether to add a section to the 'mod issues' list for mods which directly use potentially
* sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as * sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as
* part of their normal functionality, so these warnings are meaningless without further * part of their normal functionality, so these warnings are meaningless without further
* investigation. When this is commented out, it'll be true for local debug builds and false * investigation.
* otherwise. *
* When this is commented out, it'll be true for local debug builds and false otherwise.
*/ */
//"ParanoidWarnings": true, //"ParanoidWarnings": true,
/** /**
* Whether SMAPI should show newer beta versions as an available update. When this is commented * Whether SMAPI should show newer beta versions as an available update.
* out, it'll be true if the current SMAPI version is beta, and false otherwise. *
* When this is commented out, it'll be true if the current SMAPI version is beta, and false
* otherwise.
*/ */
//"UseBetaChannel": true, //"UseBetaChannel": true,

View File

@ -34,9 +34,9 @@
<DebugType>pdbonly</DebugType> <DebugType>pdbonly</DebugType>
<Optimize>true</Optimize> <Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath> <OutputPath>bin\Release\</OutputPath>
<!-- <DefineConstants>TRACE;DEBUG;ANDROID_TARGET_GOOGLE</DefineConstants>--> <!-- <DefineConstants>ANDROID_TARGET_GOOGLE</DefineConstants>-->
<!-- <DefineConstants>TRACE;DEBUG;ANDROID_TARGET_SAMSUNG</DefineConstants>--> <!-- <DefineConstants>ANDROID_TARGET_SAMSUNG</DefineConstants>-->
<DefineConstants>TRACE;DEBUG;ANDROID_TARGET_GOOGLE;HARMONY_1</DefineConstants> <DefineConstants>ANDROID_TARGET_GOOGLE;HARMONY_1</DefineConstants>
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
<LangVersion>8.0</LangVersion> <LangVersion>8.0</LangVersion>
@ -104,7 +104,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="0Harmony"> <Reference Include="0Harmony">
<HintPath>..\Loader\libs\0Harmony.dll</HintPath> <HintPath>..\..\build\0Harmony.dll</HintPath>
</Reference>
<Reference Include="MonoMod.RuntimeDetour">
<HintPath>..\Loader\libs\MonoMod.RuntimeDetour.dll</HintPath>
</Reference>
<Reference Include="MonoMod.Utils">
<HintPath>..\Loader\libs\MonoMod.Utils.dll</HintPath>
</Reference> </Reference>
<Reference Include="MonoGame.Framework"> <Reference Include="MonoGame.Framework">
<HintPath>..\Loader\libs\MonoGame.Framework.dll</HintPath> <HintPath>..\Loader\libs\MonoGame.Framework.dll</HintPath>