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
appsettings.Development.json
# generated build files
build/0Harmony.*
# Azure generated files
src/SMAPI.Web/Properties/PublishProfiles/*.pubxml

BIN
build/0Harmony.dll Normal file

Binary file not shown.

View File

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

View File

@ -1,45 +1,58 @@
&larr; [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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
****/
/// <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";

View File

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

View File

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

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="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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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