From aeab19f4aca366be3446b9f1ee097d51d21b5fde Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 19 Jun 2020 21:28:44 -0400 Subject: [PATCH 01/12] backport harmony_summary command to Harmony 1.x (#711) --- docs/release-notes.md | 4 +- .../Commands/HarmonySummaryCommand.cs | 38 +++++++++++++++++-- src/SMAPI/Framework/SCore.cs | 2 - 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index c47ee835..9ea3e445 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,10 +1,11 @@ ← [README](README.md) # Release notes + ## Upcoming release * For players: @@ -25,6 +26,7 @@ * 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 `Multiplayer.PeerConnected` event. + * Added `harmony_summary` console command which lists all current Harmony patches, optionally with a search filter. * 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: diff --git a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs index 8c20fbdd..8fdd4282 100644 --- a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs +++ b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs @@ -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 { /// The 'harmony_summary' SMAPI console command. internal class HarmonySummaryCommand : IInternalCommand { +#if !HARMONY_2 + /********* + ** Fields + *********/ + /// The Harmony instance through which to fetch patch info. + 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 /// Get all current Harmony patches. private IEnumerable 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> patchGroups = new Dictionary> { [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 /// A postfix patch. Postfix, +#if HARMONY_2 /// A finalizer patch. Finalizer, +#endif /// A transpiler patch. Transpiler @@ -167,4 +200,3 @@ namespace StardewModdingAPI.Framework.Commands } } } -#endif diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 2794002c..e1db563c 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -511,9 +511,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("Type 'help' for help, or 'help ' 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 From a7cf886b7145e94dbcb1157f73aa0f2922fa183f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Jun 2020 00:13:23 -0400 Subject: [PATCH 02/12] switch to custom build of Harmony 1.2.0.1 --- .gitignore | 3 --- build/0Harmony.dll | Bin 0 -> 115200 bytes docs/release-notes.md | 1 + docs/technical/smapi.md | 14 +++++--------- src/SMAPI/SMAPI.csproj | 3 +-- 5 files changed, 7 insertions(+), 14 deletions(-) create mode 100644 build/0Harmony.dll diff --git a/.gitignore b/.gitignore index 02522716..b7f0d3e6 100644 --- a/.gitignore +++ b/.gitignore @@ -30,8 +30,5 @@ _ReSharper*/ # sensitive files appsettings.Development.json -# generated build files -build/0Harmony.* - # Azure generated files src/SMAPI.Web/Properties/PublishProfiles/*.pubxml diff --git a/build/0Harmony.dll b/build/0Harmony.dll new file mode 100644 index 0000000000000000000000000000000000000000..2e893d0e538bdb274d9f1877e3491cd2193ed374 GIT binary patch literal 115200 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 installer` and `SMAPI 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). diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index c17de6d0..4af4527b 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -14,14 +14,13 @@ - - + From a9ca7dcdc025d1557d8f728c442c2a4fea386af9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Jun 2020 00:19:45 -0400 Subject: [PATCH 03/12] tweak 3.6 release notes --- docs/release-notes.md | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index eadd45eb..ed86f73f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,11 +9,12 @@ ## Upcoming release * For players: + * Reduced startup time when loading mod DLLs (thanks to ZaneYork!). + * Reduced processing time when a mod loads many unpacked images (thanks to Entoarox!). * Mod 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!). + * Mods which use the `[HarmonyPatch(type)]` attribute now work crossplatform. Previously SMAPI couldn't rewrite types in custom attributes for compatibility. * Fixed `BadImageFormatException` error detection. * Fixed black maps on Android for mods which use `.tmx` files. @@ -25,23 +26,19 @@ * 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 `harmony_summary` console command which lists all current Harmony patches, optionally with a search filter. - * Added ability to override update keys from the compatibility list. - * SMAPI now uses [a custom build of Harmony](https://github.com/Pathoschild/Harmony#readme) to provide more useful stack traces in error logs. - * 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 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. ## 3.5 From 067163da02c5a5993d88d80f04d379c22bc32cba Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Jun 2020 00:50:23 -0400 Subject: [PATCH 04/12] make parallel rewriting optional --- docs/release-notes.md | 2 +- src/SMAPI/Constants.cs | 8 + .../Framework/ModLoading/AssemblyLoader.cs | 10 +- .../ModLoading/Framework/RecursiveRewriter.cs | 145 +++++++++++------- src/SMAPI/Framework/Models/SConfig.cs | 11 +- src/SMAPI/Framework/SCore.cs | 4 +- src/SMAPI/SMAPI.config.json | 20 ++- 7 files changed, 129 insertions(+), 71 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index ed86f73f..3c3f0796 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,7 +9,7 @@ ## Upcoming release * For players: - * Reduced startup time when loading mod DLLs (thanks to ZaneYork!). + * 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 warnings are now listed alphabetically. * MacOS files starting with `._` are now ignored and can no longer cause skipped mods. diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index a898fccd..9d510d2d 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -52,6 +52,14 @@ namespace StardewModdingAPI /**** ** Internal ****/ + ///

Whether SMAPI was compiled in debug mode. + internal const bool IsDebugBuild = +#if DEBUG + true; +#else + false; +#endif + /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index dbb5f696..f8c901e0 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -76,9 +76,10 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod for which the assembly is being loaded. /// The assembly file path. /// Assume the mod is compatible, even if incompatible code is detected. + /// Whether to enable experimental parallel rewriting. /// Returns the rewrite metadata for the preprocessed assembly. /// An incompatible CIL instruction was found while rewriting the assembly. - 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) @@ -262,9 +263,10 @@ namespace StardewModdingAPI.Framework.ModLoading /// The assembly to rewrite. /// The messages that have already been logged for this mod. /// A string to prefix to log messages. + /// Whether to enable experimental parallel rewriting. /// Returns whether the assembly was modified. /// An incompatible CIL instruction was found while rewriting the assembly. - private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, HashSet loggedMessages, string logPrefix) + private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, HashSet loggedMessages, string logPrefix, bool rewriteInParallel) { ModuleDefinition module = assembly.MainModule; string filename = $"{assembly.Name.Name}.dll"; @@ -313,7 +315,7 @@ namespace StardewModdingAPI.Framework.ModLoading return rewritten; } ); - bool anyRewritten = rewriter.RewriteModule(); + bool anyRewritten = rewriter.RewriteModule(rewriteInParallel); // handle rewrite flags foreach (IInstructionHandler handler in handlers) diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index 9dc3680f..34c78c7d 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -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 } /// Rewrite the loaded module code. + /// Whether to enable experimental parallel rewriting. /// Returns whether the module was modified. - public bool RewriteModule() + public bool RewriteModule(bool rewriteInParallel) { - int typesChanged = 0; - Exception exception = null; + IEnumerable types = this.Module.GetTypes().Where(type => type.BaseType != null); // skip special types like - Parallel.ForEach( - source: this.Module.GetTypes().Where(type => type.BaseType != null), // skip special types like - 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 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 *********/ + /// Rewrite a loaded type definition. + /// The type definition to rewrite. + /// Returns whether the type was modified. + 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 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; + } + /// Rewrite a CIL instruction if needed. /// The current CIL instruction. /// The CIL instruction processor. diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index b1612aa4..a98d8c54 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -15,12 +15,8 @@ namespace StardewModdingAPI.Framework.Models private static readonly IDictionary DefaultValues = new Dictionary { [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)] = "Pathoschild/SMAPI", [nameof(WebApiBaseUrl)] = "https://smapi.io/api/", @@ -45,6 +41,9 @@ namespace StardewModdingAPI.Framework.Models /// Whether to check for newer versions of SMAPI and mods on startup. public bool CheckForUpdates { get; set; } + /// Whether to enable experimental parallel rewriting. + public bool RewriteInParallel { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.RewriteInParallel)]; + /// 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. public bool ParanoidWarnings { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.ParanoidWarnings)]; diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index e1db563c..90435f54 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -337,6 +337,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) @@ -981,7 +983,7 @@ namespace StardewModdingAPI.Framework Assembly modAssembly; try { - modAssembly = assemblyLoader.Load(mod, assemblyPath, assumeCompatible: mod.DataRecord?.Status == ModStatus.AssumeCompatible); + modAssembly = assemblyLoader.Load(mod, assemblyPath, assumeCompatible: mod.DataRecord?.Status == ModStatus.AssumeCompatible, rewriteInParallel: this.Settings.RewriteInParallel); this.ModRegistry.TrackAssemblies(mod, modAssembly); } catch (IncompatibleInstructionException) // details already in trace logs diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index a426b0ef..0a6d8372 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -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, From b32cad4046916344665c67923482db09cacb366f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Jun 2020 11:13:23 -0400 Subject: [PATCH 05/12] add i18n schema to JSON validator --- docs/release-notes.md | 1 + docs/technical/web.md | 5 ++-- .../Controllers/JsonValidatorController.cs | 3 ++- src/SMAPI.Web/wwwroot/schemas/i18n.json | 24 +++++++++++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 src/SMAPI.Web/wwwroot/schemas/i18n.json diff --git a/docs/release-notes.md b/docs/release-notes.md index 3c3f0796..43e1011c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ * For the web UI: * Added GitHub licenses to mod compatibility list. + * Added SMAPI `i18n` schema to JSON validator. * Updated ModDrop URLs. * Internal changes to improve performance and reliability. diff --git a/docs/technical/web.md b/docs/technical/web.md index d21b87ac..50237bfe 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.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 diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index c43fb929..8fb9e06a 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -27,7 +27,8 @@ namespace StardewModdingAPI.Web.Controllers private readonly IDictionary SchemaFormats = new Dictionary { ["none"] = "None", - ["manifest"] = "Manifest", + ["manifest"] = "SMAPI: manifest", + ["i18n"] = "SMAPI: translations (i18n)", ["content-patcher"] = "Content Patcher" }; diff --git a/src/SMAPI.Web/wwwroot/schemas/i18n.json b/src/SMAPI.Web/wwwroot/schemas/i18n.json new file mode 100644 index 00000000..493ad213 --- /dev/null +++ b/src/SMAPI.Web/wwwroot/schemas/i18n.json @@ -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." + } + } +} From d02a40de997edf493354e85eb018ded84eb8f782 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Jun 2020 11:31:01 -0400 Subject: [PATCH 06/12] change default JSON validator schema to none --- docs/release-notes.md | 4 +++- src/SMAPI.Web/Controllers/JsonValidatorController.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 43e1011c..2780e5ad 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,7 +20,9 @@ * For the web UI: * Added GitHub licenses to mod compatibility list. - * Added SMAPI `i18n` schema to JSON validator. + * Improved JSON validator: + * added SMAPI `i18n` schema; + * changed default schema to plain JSON. * Updated ModDrop URLs. * Internal changes to improve performance and reliability. diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index 8fb9e06a..b76d41a3 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -33,7 +33,7 @@ namespace StardewModdingAPI.Web.Controllers }; /// The schema ID to use if none was specified. - private string DefaultSchemaID = "manifest"; + private string DefaultSchemaID = "none"; /// A token in an error message which indicates that the child errors should be displayed instead. private readonly string TransparentToken = "$transparent"; From ed3309e7bb8d5f3f6c3d08df3475bd811d5b16d0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Jun 2020 11:36:22 -0400 Subject: [PATCH 07/12] remember selected schema when editing a file --- docs/release-notes.md | 1 + .../Controllers/JsonValidatorController.cs | 25 ++++++++++++------- .../JsonValidator/JsonValidatorModel.cs | 7 +++++- .../Views/JsonValidator/Index.cshtml | 9 +++---- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 2780e5ad..2bb4dc84 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,6 +22,7 @@ * 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. diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index b76d41a3..5f83eafd 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -58,16 +58,22 @@ namespace StardewModdingAPI.Web.Controllers /// Render the schema validator UI. /// The schema name with which to validate the JSON, or 'edit' to return to the edit screen. /// The stored file ID. + /// The operation to perform for the selected log ID. This can be 'edit', or any other value to view. [HttpGet] [Route("json")] [Route("json/{schemaName}")] [Route("json/{schemaName}/{id}")] - public async Task Index(string schemaName = null, string id = null) + [Route("json/{schemaName}/{id}/{operation}")] + public async Task 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 @@ -77,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 @@ -131,7 +137,7 @@ namespace StardewModdingAPI.Web.Controllers public async Task 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); @@ -139,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 })); @@ -157,9 +163,10 @@ namespace StardewModdingAPI.Web.Controllers /// Build a JSON validator model. /// The stored file ID. /// The schema name with which the JSON was validated. - private JsonValidatorModel GetModel(string pasteID, string schemaName) + /// Whether to show the edit view. + private JsonValidatorModel GetModel(string pasteID, string schemaName, bool isEditView) { - return new JsonValidatorModel(pasteID, schemaName, this.SchemaFormats); + return new JsonValidatorModel(pasteID, schemaName, this.SchemaFormats, isEditView); } /// Get a normalized schema name, or the if blank. diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs index c0dd7184..0ea69911 100644 --- a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs +++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs @@ -10,6 +10,9 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator /********* ** Accessors *********/ + /// Whether to show the edit view. + public bool IsEditView { get; set; } + /// The paste ID. public string PasteID { get; set; } @@ -51,11 +54,13 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator /// The stored file ID. /// The schema name with which the JSON was validated. /// The supported JSON schemas (names indexed by ID). - public JsonValidatorModel(string pasteID, string schemaName, IDictionary schemaFormats) + /// Whether to show the edit view. + public JsonValidatorModel(string pasteID, string schemaName, IDictionary schemaFormats, bool isEditView) { this.PasteID = pasteID; this.SchemaName = schemaName; this.SchemaFormats = schemaFormats; + this.IsEditView = isEditView; } /// Set the validated content. diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml index f23bd150..7b89a23d 100644 --- a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml +++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml @@ -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) Error details: @Model.ParseError } -else if (!isEditView && Model.PasteID != null) +else if (!Model.IsEditView && Model.PasteID != null) {