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:
commit
2bcee41151
|
@ -30,9 +30,6 @@ _ReSharper*/
|
|||
# sensitive files
|
||||
appsettings.Development.json
|
||||
|
||||
# generated build files
|
||||
build/0Harmony.*
|
||||
|
||||
# Azure generated files
|
||||
src/SMAPI.Web/Properties/PublishProfiles/*.pubxml
|
||||
|
||||
|
|
Binary file not shown.
|
@ -4,7 +4,7 @@
|
|||
|
||||
<!--set properties -->
|
||||
<PropertyGroup>
|
||||
<Version>3.5.0</Version>
|
||||
<Version>3.6.1</Version>
|
||||
<Product>SMAPI</Product>
|
||||
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
|
|
@ -1,45 +1,58 @@
|
|||
← [README](README.md)
|
||||
|
||||
# Release notes
|
||||
## Upcoming release + 1
|
||||
<!--
|
||||
## Future release
|
||||
* 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).
|
||||
* 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:
|
||||
* 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.
|
||||
* 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 `BadImageFormatException` error detection.
|
||||
* Fixed `reload_i18n` command not reloading content pack translations.
|
||||
|
||||
* For the web UI:
|
||||
* 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.
|
||||
* Internal changes to improve performance and reliability.
|
||||
|
||||
* For modders:
|
||||
* 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 [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 ability to override update keys from the 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:
|
||||
* 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.
|
||||
* Added support for overriding update keys from the wiki compatibility list.
|
||||
* Improved mod rewriting for compatibility to support more cases (e.g. custom attributes and generic types).
|
||||
* Fixed `helper.Reflection` blocking access to game methods/properties intercepted by SMAPI.
|
||||
* 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.
|
||||
|
||||
* 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.
|
||||
* 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.
|
||||
* Changed SMAPI's Harmony ID from `io.smapi` to `SMAPI` for readability in Harmony summaries.
|
||||
|
||||
## 3.5
|
||||
Released 27 April 2020 for Stardew Valley 1.4.1 or later.
|
||||
|
|
|
@ -58,7 +58,7 @@ SMAPI uses a small number of conditional compilation constants, which you can se
|
|||
flag | purpose
|
||||
---- | -------
|
||||
`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
|
||||
### 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)
|
||||
on the wiki for the first-time setup.
|
||||
|
||||
1. Update the version number in `.root/build/common.targets` and `Constants::Version`. Make sure
|
||||
you use a [semantic version](https://semver.org). Recommended format:
|
||||
1. Update the version numbers in `build/common.targets`, `Constants`, and the `manifest.json` for
|
||||
bundled mods. Make sure you use a [semantic version](https://semver.org). Recommended format:
|
||||
|
||||
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`.
|
||||
4. Zip the two folders.
|
||||
|
||||
### Using a custom Harmony build
|
||||
The official SMAPI releases include [a custom build of Harmony](https://github.com/Pathoschild/Harmony),
|
||||
but compiling from source will use the official build. To use a custom build, put `0Harmony.dll` in
|
||||
the `build` folder and it'll be referenced automatically.
|
||||
|
||||
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.
|
||||
### Custom Harmony build
|
||||
SMAPI uses [a custom build of Harmony](https://github.com/Pathoschild/Harmony#readme), which is
|
||||
included in the `build` folder. To use a different build, just replace `0Harmony.dll` in that
|
||||
folder.
|
||||
|
||||
## Release notes
|
||||
See [release notes](../release-notes.md).
|
||||
|
|
|
@ -110,8 +110,9 @@ Available schemas:
|
|||
|
||||
format | schema URL
|
||||
------ | ----------
|
||||
[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: `manifest.json`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest) | https://smapi.io/schemas/manifest.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
|
||||
### Overview
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "Console Commands",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.5.0",
|
||||
"Version": "3.6.1",
|
||||
"Description": "Adds SMAPI console commands that let you manipulate the game.",
|
||||
"UniqueID": "SMAPI.ConsoleCommands",
|
||||
"EntryDll": "ConsoleCommands.dll",
|
||||
"MinimumApiVersion": "3.5.0"
|
||||
"MinimumApiVersion": "3.6.1"
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "Save Backup",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.5.0",
|
||||
"Version": "3.6.1",
|
||||
"Description": "Automatically backs up all your saves once per day into its folder.",
|
||||
"UniqueID": "SMAPI.SaveBackup",
|
||||
"EntryDll": "SaveBackup.dll",
|
||||
"MinimumApiVersion": "3.5.0"
|
||||
"MinimumApiVersion": "3.6.1"
|
||||
}
|
||||
|
|
|
@ -27,12 +27,13 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
private readonly IDictionary<string, string> SchemaFormats = new Dictionary<string, string>
|
||||
{
|
||||
["none"] = "None",
|
||||
["manifest"] = "Manifest",
|
||||
["manifest"] = "SMAPI: manifest",
|
||||
["i18n"] = "SMAPI: translations (i18n)",
|
||||
["content-patcher"] = "Content Patcher"
|
||||
};
|
||||
|
||||
/// <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>
|
||||
private readonly string TransparentToken = "$transparent";
|
||||
|
@ -57,16 +58,22 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <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="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]
|
||||
[Route("json")]
|
||||
[Route("json/{schemaName}")]
|
||||
[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);
|
||||
bool hasId = !string.IsNullOrWhiteSpace(id);
|
||||
bool isEditView = !hasId || operation?.Trim().ToLower() == "edit";
|
||||
|
||||
var result = new JsonValidatorModel(id, schemaName, this.SchemaFormats);
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
// build result model
|
||||
var result = this.GetModel(id, schemaName, isEditView);
|
||||
if (!hasId)
|
||||
return this.View("Index", result);
|
||||
|
||||
// fetch raw JSON
|
||||
|
@ -76,7 +83,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
result.SetContent(file.Content, expiry: file.Expiry, uploadWarning: file.Warning);
|
||||
|
||||
// skip parsing if we're going to the edit screen
|
||||
if (schemaName?.ToLower() == "edit")
|
||||
if (isEditView)
|
||||
return this.View("Index", result);
|
||||
|
||||
// parse JSON
|
||||
|
@ -130,7 +137,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
public async Task<ActionResult> PostAsync(JsonValidatorRequestModel request)
|
||||
{
|
||||
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
|
||||
string schemaName = this.NormalizeSchemaName(request.SchemaName);
|
||||
|
@ -138,12 +145,12 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
// get raw text
|
||||
string input = request.Content;
|
||||
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
|
||||
UploadResult result = await this.Storage.SaveAsync(input);
|
||||
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
|
||||
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>
|
||||
/// <param name="pasteID">The stored file ID.</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>
|
||||
|
|
|
@ -10,6 +10,9 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator
|
|||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>Whether to show the edit view.</summary>
|
||||
public bool IsEditView { get; set; }
|
||||
|
||||
/// <summary>The paste ID.</summary>
|
||||
public string PasteID { get; set; }
|
||||
|
||||
|
@ -51,11 +54,13 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator
|
|||
/// <param name="pasteID">The stored file ID.</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>
|
||||
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.SchemaName = schemaName;
|
||||
this.SchemaFormats = schemaFormats;
|
||||
this.IsEditView = isEditView;
|
||||
}
|
||||
|
||||
/// <summary>Set the validated content.</summary>
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
string newUploadUrl = this.Url.PlainAction("Index", "JsonValidator", new { schemaName = Model.SchemaName });
|
||||
string schemaDisplayName = null;
|
||||
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
|
||||
ViewData["Title"] = "JSON validator";
|
||||
|
@ -63,7 +62,7 @@ else if (Model.ParseError != null)
|
|||
<small v-pre>Error details: @Model.ParseError</small>
|
||||
</div>
|
||||
}
|
||||
else if (!isEditView && Model.PasteID != null)
|
||||
else if (!Model.IsEditView && Model.PasteID != null)
|
||||
{
|
||||
<div class="banner success">
|
||||
<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 *@
|
||||
@if (isEditView)
|
||||
@if (Model.IsEditView)
|
||||
{
|
||||
<h2>Upload a JSON file</h2>
|
||||
<form action="@this.Url.PlainAction("PostAsync", "JsonValidator")" method="post">
|
||||
|
@ -112,7 +111,7 @@ else if (!isEditView && Model.PasteID != null)
|
|||
}
|
||||
|
||||
@* validation results *@
|
||||
@if (!isEditView)
|
||||
@if (!Model.IsEditView)
|
||||
{
|
||||
<div id="output">
|
||||
@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>
|
||||
}
|
||||
</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>
|
||||
<pre id="raw-content" class="sunlight-highlight-javascript">@Model.Content</pre>
|
||||
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
|
||||
TimeSpan staleAge = DateTimeOffset.UtcNow - Model.LastUpdated;
|
||||
|
||||
bool hasBeta = true; // Model.BetaVersion != null;
|
||||
string betaLabel = "SMAPI 3.6 only"; //"SDV @Model.BetaVersion only";
|
||||
bool hasBeta = Model.BetaVersion != null;
|
||||
string betaLabel = "SDV @Model.BetaVersion only";
|
||||
}
|
||||
@section Head {
|
||||
<link rel="stylesheet" href="~/Content/css/mods.css?r=20200218" />
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@ namespace StardewModdingAPI
|
|||
** Public
|
||||
****/
|
||||
/// <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>
|
||||
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.5");
|
||||
|
@ -56,6 +56,14 @@ namespace StardewModdingAPI
|
|||
/****
|
||||
** 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>
|
||||
internal const string HomePageUrl = "https://smapi.io";
|
||||
|
||||
|
|
|
@ -1,16 +1,27 @@
|
|||
#if HARMONY_2
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
#if HARMONY_2
|
||||
using HarmonyLib;
|
||||
#else
|
||||
using Harmony;
|
||||
#endif
|
||||
|
||||
namespace StardewModdingAPI.Framework.Commands
|
||||
{
|
||||
/// <summary>The 'harmony_summary' SMAPI console command.</summary>
|
||||
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
|
||||
*********/
|
||||
|
@ -45,7 +56,16 @@ namespace StardewModdingAPI.Framework.Commands
|
|||
foreach (var ownerGroup in match.PatchTypesByOwner.OrderBy(p => p.Key))
|
||||
{
|
||||
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()})");
|
||||
}
|
||||
|
@ -91,15 +111,26 @@ namespace StardewModdingAPI.Framework.Commands
|
|||
/// <summary>Get all current Harmony patches.</summary>
|
||||
private IEnumerable<SearchResult> GetAllPatches()
|
||||
{
|
||||
#if HARMONY_2
|
||||
foreach (MethodBase method in Harmony.GetAllPatchedMethods())
|
||||
#else
|
||||
foreach (MethodBase method in this.HarmonyInstance.GetPatchedMethods())
|
||||
#endif
|
||||
{
|
||||
// get metadata for method
|
||||
#if HARMONY_2
|
||||
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>>
|
||||
{
|
||||
[PatchType.Prefix] = patchInfo.Prefixes,
|
||||
[PatchType.Postfix] = patchInfo.Postfixes,
|
||||
#if HARMONY_2
|
||||
[PatchType.Finalizer] = patchInfo.Finalizers,
|
||||
#endif
|
||||
[PatchType.Transpiler] = patchInfo.Transpilers
|
||||
};
|
||||
|
||||
|
@ -129,8 +160,10 @@ namespace StardewModdingAPI.Framework.Commands
|
|||
/// <summary>A postfix patch.</summary>
|
||||
Postfix,
|
||||
|
||||
#if HARMONY_2
|
||||
/// <summary>A finalizer patch.</summary>
|
||||
Finalizer,
|
||||
#endif
|
||||
|
||||
/// <summary>A transpiler patch.</summary>
|
||||
Transpiler
|
||||
|
@ -167,4 +200,3 @@ namespace StardewModdingAPI.Framework.Commands
|
|||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -47,7 +47,7 @@ namespace StardewModdingAPI.Framework.Events
|
|||
if (!(obj is ManagedEventHandler<TEventArgs> other))
|
||||
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
|
||||
? priorityCompare
|
||||
: this.RegistrationOrder.CompareTo(other.RegistrationOrder);
|
||||
|
|
|
@ -76,9 +76,10 @@ namespace StardewModdingAPI.Framework.ModLoading
|
|||
/// <param name="mod">The mod for which the assembly is being loaded.</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="rewriteInParallel">Whether to enable experimental parallel rewriting.</param>
|
||||
/// <returns>Returns the rewrite metadata for the preprocessed assembly.</returns>
|
||||
/// <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
|
||||
AssemblyParseResult[] assemblies;
|
||||
|
@ -108,7 +109,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
|||
continue;
|
||||
|
||||
// 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
|
||||
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="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="rewriteInParallel">Whether to enable experimental parallel rewriting.</param>
|
||||
/// <returns>Returns whether the assembly was modified.</returns>
|
||||
/// <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;
|
||||
string filename = $"{assembly.Name.Name}.dll";
|
||||
|
@ -330,7 +332,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
|||
return rewritten;
|
||||
}
|
||||
);
|
||||
bool anyRewritten = rewriter.RewriteModule();
|
||||
bool anyRewritten = rewriter.RewriteModule(rewriteInParallel);
|
||||
|
||||
// handle rewrite flags
|
||||
foreach (IInstructionHandler handler in handlers)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -56,15 +57,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
|
|||
}
|
||||
|
||||
/// <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>
|
||||
public bool RewriteModule()
|
||||
public bool RewriteModule(bool rewriteInParallel)
|
||||
{
|
||||
int typesChanged = 0;
|
||||
Exception exception = null;
|
||||
IEnumerable<TypeDefinition> types = this.Module.GetTypes().Where(type => type.BaseType != null); // skip special types like <Module>
|
||||
|
||||
Parallel.ForEach(
|
||||
source: this.Module.GetTypes().Where(type => type.BaseType != null), // skip special types like <Module>
|
||||
body: type =>
|
||||
// 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;
|
||||
Exception exception = null;
|
||||
|
||||
Parallel.ForEach(types, type =>
|
||||
{
|
||||
if (exception != null)
|
||||
return;
|
||||
|
@ -72,50 +78,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
|
|||
bool changed = false;
|
||||
try
|
||||
{
|
||||
changed |= this.RewriteCustomAttributes(type.CustomAttributes);
|
||||
changed |= this.RewriteGenericParameters(type.GenericParameters);
|
||||
|
||||
foreach (InterfaceImplementation @interface in type.Interfaces)
|
||||
changed |= this.RewriteTypeReference(@interface.InterfaceType, newType => @interface.InterfaceType = newType);
|
||||
|
||||
if (type.BaseType.FullName != "System.Object")
|
||||
changed |= this.RewriteTypeReference(type.BaseType, newType => type.BaseType = newType);
|
||||
|
||||
foreach (MethodDefinition method in type.Methods)
|
||||
{
|
||||
changed |= this.RewriteTypeReference(method.ReturnType, newType => method.ReturnType = newType);
|
||||
changed |= this.RewriteGenericParameters(method.GenericParameters);
|
||||
changed |= this.RewriteCustomAttributes(method.CustomAttributes);
|
||||
|
||||
foreach (ParameterDefinition parameter in method.Parameters)
|
||||
changed |= this.RewriteTypeReference(parameter.ParameterType, newType => parameter.ParameterType = newType);
|
||||
|
||||
foreach (var methodOverride in method.Overrides)
|
||||
changed |= this.RewriteMethodReference(methodOverride);
|
||||
|
||||
if (method.HasBody)
|
||||
{
|
||||
foreach (VariableDefinition variable in method.Body.Variables)
|
||||
changed |= this.RewriteTypeReference(variable.VariableType, newType => variable.VariableType = newType);
|
||||
|
||||
// check CIL instructions
|
||||
ILProcessor cil = method.Body.GetILProcessor();
|
||||
Collection<Instruction> instructions = cil.Body.Instructions;
|
||||
for (int i = 0; i < instructions.Count; i++)
|
||||
{
|
||||
var instruction = instructions[i];
|
||||
if (instruction.OpCode.Code == Code.Nop)
|
||||
continue;
|
||||
|
||||
changed |= this.RewriteInstruction(instruction, cil, newInstruction =>
|
||||
{
|
||||
changed = true;
|
||||
cil.Replace(instruction, newInstruction);
|
||||
instruction = newInstruction;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
changed = this.RewriteTypeDefinition(type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -124,18 +87,90 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
|
|||
|
||||
if (changed)
|
||||
Interlocked.Increment(ref typesChanged);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return exception == null
|
||||
? typesChanged > 0
|
||||
: throw new Exception($"Rewriting {this.Module.Name} failed.", exception);
|
||||
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.RewriteGenericParameters(type.GenericParameters);
|
||||
|
||||
foreach (InterfaceImplementation @interface in type.Interfaces)
|
||||
changed |= this.RewriteTypeReference(@interface.InterfaceType, newType => @interface.InterfaceType = newType);
|
||||
|
||||
if (type.BaseType.FullName != "System.Object")
|
||||
changed |= this.RewriteTypeReference(type.BaseType, newType => type.BaseType = newType);
|
||||
|
||||
foreach (MethodDefinition method in type.Methods)
|
||||
{
|
||||
changed |= this.RewriteTypeReference(method.ReturnType, newType => method.ReturnType = newType);
|
||||
changed |= this.RewriteGenericParameters(method.GenericParameters);
|
||||
changed |= this.RewriteCustomAttributes(method.CustomAttributes);
|
||||
|
||||
foreach (ParameterDefinition parameter in method.Parameters)
|
||||
changed |= this.RewriteTypeReference(parameter.ParameterType, newType => parameter.ParameterType = newType);
|
||||
|
||||
foreach (var methodOverride in method.Overrides)
|
||||
changed |= this.RewriteMethodReference(methodOverride);
|
||||
|
||||
if (method.HasBody)
|
||||
{
|
||||
foreach (VariableDefinition variable in method.Body.Variables)
|
||||
changed |= this.RewriteTypeReference(variable.VariableType, newType => variable.VariableType = newType);
|
||||
|
||||
// check CIL instructions
|
||||
ILProcessor cil = method.Body.GetILProcessor();
|
||||
Collection<Instruction> instructions = cil.Body.Instructions;
|
||||
for (int i = 0; i < instructions.Count; i++)
|
||||
{
|
||||
var instruction = instructions[i];
|
||||
if (instruction.OpCode.Code == Code.Nop)
|
||||
continue;
|
||||
|
||||
changed |= this.RewriteInstruction(instruction, cil, newInstruction =>
|
||||
{
|
||||
changed = true;
|
||||
cil.Replace(instruction, newInstruction);
|
||||
instruction = newInstruction;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// <summary>Rewrite a CIL instruction if needed.</summary>
|
||||
/// <param name="instruction">The current CIL instruction.</param>
|
||||
/// <param name="cil">The CIL instruction processor.</param>
|
||||
|
|
|
@ -15,12 +15,8 @@ namespace StardewModdingAPI.Framework.Models
|
|||
private static readonly IDictionary<string, object> DefaultValues = new Dictionary<string, object>
|
||||
{
|
||||
[nameof(CheckForUpdates)] = true,
|
||||
[nameof(ParanoidWarnings)] =
|
||||
#if DEBUG
|
||||
true,
|
||||
#else
|
||||
false,
|
||||
#endif
|
||||
[nameof(ParanoidWarnings)] = Constants.IsDebugBuild,
|
||||
[nameof(RewriteInParallel)] = Constants.IsDebugBuild,
|
||||
[nameof(UseBetaChannel)] = Constants.ApiVersion.IsPrerelease(),
|
||||
[nameof(GitHubProjectName)] = "MartyrPher/SMAPI-Android-Installer",
|
||||
[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>
|
||||
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>
|
||||
public bool ParanoidWarnings { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.ParanoidWarnings)];
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
|||
#if HARMONY_2
|
||||
using HarmonyLib;
|
||||
#else
|
||||
using MonoMod.RuntimeDetour;
|
||||
using Harmony;
|
||||
#endif
|
||||
|
||||
|
@ -34,6 +35,13 @@ namespace StardewModdingAPI.Framework.Patching
|
|||
#if HARMONY_2
|
||||
Harmony harmony = new Harmony("SMAPI");
|
||||
#else
|
||||
if (!HarmonyDetourBridge.Initialized && Constants.HarmonyEnabled)
|
||||
{
|
||||
try {
|
||||
HarmonyDetourBridge.Init();
|
||||
}
|
||||
catch { Constants.HarmonyEnabled = false; }
|
||||
}
|
||||
HarmonyInstance harmony = HarmonyInstance.Create("SMAPI");
|
||||
#endif
|
||||
foreach (IHarmonyPatch patch in patches)
|
||||
|
|
|
@ -347,6 +347,8 @@ namespace StardewModdingAPI.Framework
|
|||
// add headers
|
||||
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);
|
||||
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)
|
||||
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)
|
||||
|
@ -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.GameInstance.CommandManager
|
||||
.Add(new HelpCommand(this.GameInstance.CommandManager), this.Monitor)
|
||||
#if HARMONY_2
|
||||
.Add(new HarmonySummaryCommand(), this.Monitor)
|
||||
#endif
|
||||
.Add(new ReloadI18nCommand(this.ReloadTranslations), this.Monitor);
|
||||
|
||||
// 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.GameInstance.CommandManager
|
||||
.Add(new HelpCommand(this.GameInstance.CommandManager), this.Monitor)
|
||||
#if HARMONY_2
|
||||
.Add(new HarmonySummaryCommand(), this.Monitor)
|
||||
#endif
|
||||
.Add(new ReloadI18nCommand(this.ReloadTranslations), this.Monitor);
|
||||
|
||||
// start handling command line input
|
||||
|
@ -1013,7 +1011,7 @@ namespace StardewModdingAPI.Framework
|
|||
Assembly modAssembly;
|
||||
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);
|
||||
}
|
||||
catch (IncompatibleInstructionException) // details already in trace logs
|
||||
|
@ -1310,7 +1308,7 @@ namespace StardewModdingAPI.Framework
|
|||
/// <summary>Reload translations for all mods.</summary>
|
||||
private void ReloadTranslations()
|
||||
{
|
||||
this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false));
|
||||
this.ReloadTranslations(this.ModRegistry.GetAll());
|
||||
}
|
||||
|
||||
/// <summary>Reload translations for the given mods.</summary>
|
||||
|
|
|
@ -33,18 +33,30 @@ copy all the settings, or you may cause bugs due to overridden changes in future
|
|||
*/
|
||||
"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
|
||||
* 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
|
||||
* investigation. When this is commented out, it'll be true for local debug builds and false
|
||||
* otherwise.
|
||||
* investigation.
|
||||
*
|
||||
* When this is commented out, it'll be true for local debug builds and false otherwise.
|
||||
*/
|
||||
//"ParanoidWarnings": true,
|
||||
|
||||
/**
|
||||
* Whether SMAPI should show newer beta versions as an available update. When this is commented
|
||||
* out, it'll be true if the current SMAPI version is beta, and false otherwise.
|
||||
* Whether SMAPI should show newer beta versions as an available update.
|
||||
*
|
||||
* When this is commented out, it'll be true if the current SMAPI version is beta, and false
|
||||
* otherwise.
|
||||
*/
|
||||
//"UseBetaChannel": true,
|
||||
|
||||
|
|
|
@ -34,9 +34,9 @@
|
|||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<!-- <DefineConstants>TRACE;DEBUG;ANDROID_TARGET_GOOGLE</DefineConstants>-->
|
||||
<!-- <DefineConstants>TRACE;DEBUG;ANDROID_TARGET_SAMSUNG</DefineConstants>-->
|
||||
<DefineConstants>TRACE;DEBUG;ANDROID_TARGET_GOOGLE;HARMONY_1</DefineConstants>
|
||||
<!-- <DefineConstants>ANDROID_TARGET_GOOGLE</DefineConstants>-->
|
||||
<!-- <DefineConstants>ANDROID_TARGET_SAMSUNG</DefineConstants>-->
|
||||
<DefineConstants>ANDROID_TARGET_GOOGLE;HARMONY_1</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
|
@ -104,7 +104,13 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<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 Include="MonoGame.Framework">
|
||||
<HintPath>..\Loader\libs\MonoGame.Framework.dll</HintPath>
|
||||
|
|
Loading…
Reference in New Issue