From 544919ad1328c0f8b283941b1c2c5c8864a3a84d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 13 Aug 2020 20:01:22 -0400 Subject: remove experimental RewriteInParallel option --- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 10 ++-- .../ModLoading/Framework/RecursiveRewriter.cs | 57 ++++------------------ 2 files changed, 14 insertions(+), 53 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index f8c901e0..dbb5f696 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -76,10 +76,9 @@ 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, bool rewriteInParallel) + public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible) { // get referenced local assemblies AssemblyParseResult[] assemblies; @@ -109,7 +108,7 @@ namespace StardewModdingAPI.Framework.ModLoading continue; // rewrite assembly - bool changed = this.RewriteAssembly(mod, assembly.Definition, loggedMessages, logPrefix: " ", rewriteInParallel); + bool changed = this.RewriteAssembly(mod, assembly.Definition, loggedMessages, logPrefix: " "); // detect broken assembly reference foreach (AssemblyNameReference reference in assembly.Definition.MainModule.AssemblyReferences) @@ -263,10 +262,9 @@ 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, bool rewriteInParallel) + private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, HashSet loggedMessages, string logPrefix) { ModuleDefinition module = assembly.MainModule; string filename = $"{assembly.Name.Name}.dll"; @@ -315,7 +313,7 @@ namespace StardewModdingAPI.Framework.ModLoading return rewritten; } ); - bool anyRewritten = rewriter.RewriteModule(rewriteInParallel); + bool anyRewritten = rewriter.RewriteModule(); // 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 34c78c7d..fb651465 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Collections.Generic; @@ -57,59 +55,24 @@ 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(bool rewriteInParallel) + public bool RewriteModule() { IEnumerable types = this.Module.GetTypes().Where(type => type.BaseType != null); // skip special types like - // 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; - - bool changed = false; - try - { - changed = this.RewriteTypeDefinition(type); - } - catch (Exception ex) - { - exception ??= ex; - } - - if (changed) - Interlocked.Increment(ref typesChanged); - }); + bool changed = false; - return exception == null - ? typesChanged > 0 - : throw new Exception($"Rewriting {this.Module.Name} failed.", exception); + try + { + foreach (var type in types) + changed |= this.RewriteTypeDefinition(type); } - - // non-parallel rewriting + catch (Exception ex) { - 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; + throw new Exception($"Rewriting {this.Module.Name} failed.", ex); } + + return changed; } -- cgit From f012dae8728df55ab8803de1f269aee480ccc49b Mon Sep 17 00:00:00 2001 From: spacechase0 Date: Mon, 17 Aug 2020 20:28:51 -0400 Subject: Fix harmony attributes not workign cross-platform --- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index dbb5f696..c8c1ca08 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -292,6 +292,19 @@ namespace StardewModdingAPI.Framework.ModLoading IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); foreach (TypeReference type in typeReferences) this.ChangeTypeScope(type); + + // rewrite types using custom attributes + foreach (TypeDefinition type in module.GetTypes()) + { + foreach (var attr in type.CustomAttributes) + { + foreach (var conField in attr.ConstructorArguments) + { + if (conField.Value is TypeReference typeRef) + this.ChangeTypeScope(typeRef); + } + } + } } // find or rewrite code -- cgit From ad1b9a870b5383ca9ada8c52b2bd76960d5579da Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 23 Aug 2020 14:22:27 -0400 Subject: move some console/logging logic out of SCore into a new LogManager --- src/SMAPI.Tests/Core/ModResolverTests.cs | 22 +- src/SMAPI.sln.DotSettings | 2 + src/SMAPI/Framework/IModMetadata.cs | 6 +- src/SMAPI/Framework/Logging/LogManager.cs | 586 +++++++++++++++++++++++++ src/SMAPI/Framework/ModLoading/ModMetadata.cs | 83 ++-- src/SMAPI/Framework/SCore.cs | 590 +++----------------------- 6 files changed, 705 insertions(+), 584 deletions(-) create mode 100644 src/SMAPI/Framework/Logging/LogManager.cs (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 45b3673b..4f3a12cb 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -154,7 +154,7 @@ namespace SMAPI.Tests.Core new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert - mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the metadata."); + mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); } [Test(Description = "Assert that validation fails when the minimum API version is higher than the current SMAPI version.")] @@ -169,7 +169,7 @@ namespace SMAPI.Tests.Core new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert - mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the metadata."); + mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); } [Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")] @@ -183,7 +183,7 @@ namespace SMAPI.Tests.Core new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert - mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the metadata."); + mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); } [Test(Description = "Assert that validation fails when multiple mods have the same unique ID.")] @@ -200,8 +200,8 @@ namespace SMAPI.Tests.Core new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert - modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the first mod with a unique ID."); - modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the second mod with a unique ID."); + modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the first mod with a unique ID."); + modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the second mod with a unique ID."); } [Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")] @@ -367,9 +367,9 @@ namespace SMAPI.Tests.Core Assert.AreEqual(5, mods.Length, 0, "Expected to get the same number of mods input."); Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); - modC.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod C was expected to fail since it's part of a dependency loop."); - modD.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod D was expected to fail since it's part of a dependency loop."); - modE.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod E was expected to fail since it's part of a dependency loop."); + modC.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "Mod C was expected to fail since it's part of a dependency loop."); + modD.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "Mod D was expected to fail since it's part of a dependency loop."); + modE.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "Mod E was expected to fail since it's part of a dependency loop."); } [Test(Description = "Assert that dependencies are sorted correctly even if some of the mods failed during metadata loading.")] @@ -408,7 +408,7 @@ namespace SMAPI.Tests.Core // assert Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); - modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod B unexpectedly didn't fail even though it needs a newer version of Mod A."); + modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "Mod B unexpectedly didn't fail even though it needs a newer version of Mod A."); } [Test(Description = "Assert that dependencies are accepted if they meet the minimum version.")] @@ -525,8 +525,8 @@ namespace SMAPI.Tests.Core if (allowStatusChange) { mod - .Setup(p => p.SetStatus(It.IsAny(), It.IsAny())) - .Callback((status, message) => Console.WriteLine($"<{manifest.UniqueID} changed status: [{status}] {message}")) + .Setup(p => p.SetStatus(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((status, message, errorDetails) => Console.WriteLine($"<{manifest.UniqueID} changed status: [{status}] {message}\n{errorDetails}")) .Returns(mod.Object); } return mod; diff --git a/src/SMAPI.sln.DotSettings b/src/SMAPI.sln.DotSettings index 05caa938..76e863cc 100644 --- a/src/SMAPI.sln.DotSettings +++ b/src/SMAPI.sln.DotSettings @@ -3,6 +3,7 @@ DO_NOT_SHOW HINT HINT + DO_NOT_SHOW Field, Property, Event, Method Field, Property, Event, Method True @@ -53,6 +54,7 @@ True True True + True True True True diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 1231b494..6a635b76 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -37,6 +37,9 @@ namespace StardewModdingAPI.Framework /// The reason the metadata is invalid, if any. string Error { get; } + /// A detailed technical message for , if any. + public string ErrorDetails { get; } + /// Whether the mod folder should be ignored. This is true if it was found within a folder whose name starts with a dot. bool IsIgnored { get; } @@ -65,8 +68,9 @@ namespace StardewModdingAPI.Framework /// Set the mod status. /// The metadata resolution status. /// The reason the metadata is invalid, if any. + /// A detailed technical message, if any. /// Return the instance for chaining. - IModMetadata SetStatus(ModMetadataStatus status, string error = null); + IModMetadata SetStatus(ModMetadataStatus status, string error = null, string errorDetails = null); /// Set a warning flag for the mod. /// The warning to set. diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs new file mode 100644 index 00000000..3786e940 --- /dev/null +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -0,0 +1,586 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using StardewModdingAPI.Framework.Commands; +using StardewModdingAPI.Internal.ConsoleWriting; +using StardewModdingAPI.Toolkit.Framework.ModData; +using StardewModdingAPI.Toolkit.Utilities; + +namespace StardewModdingAPI.Framework.Logging +{ + /// Manages the SMAPI console window and log file. + internal class LogManager : IDisposable + { + /********* + ** Fields + *********/ + /// The log file to which to write messages. + private readonly LogFileManager LogFile; + + /// Manages console output interception. + private readonly ConsoleInterceptionManager InterceptionManager = new ConsoleInterceptionManager(); + + /// Get a named monitor instance. + private readonly Func GetMonitorImpl; + + /// Regex patterns which match console non-error messages to suppress from the console and log. + private readonly Regex[] SuppressConsolePatterns = + { + new Regex(@"^TextBox\.Selected is now '(?:True|False)'\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant), + new Regex(@"^(?:FRUIT )?TREE: IsClient:(?:True|False) randomOutput: \d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant), + new Regex(@"^loadPreferences\(\); begin", RegexOptions.Compiled | RegexOptions.CultureInvariant), + new Regex(@"^savePreferences\(\); async=", RegexOptions.Compiled | RegexOptions.CultureInvariant), + new Regex(@"^DebugOutput:\s+(?:added CLOUD|added cricket|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant) + }; + + /// Regex patterns which match console messages to show a more friendly error for. + private readonly ReplaceLogPattern[] ReplaceConsolePatterns = + { + // Steam not loaded + new ReplaceLogPattern( + search: new Regex(@"^System\.InvalidOperationException: Steamworks is not initialized\.[\s\S]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant), + replacement: +#if SMAPI_FOR_WINDOWS + "Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that (see 'Part 2: Configure Steam' in the install guide for more info: https://smapi.io/install).", +#else + "Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that.", +#endif + logLevel: LogLevel.Error + ), + + // save file not found error + new ReplaceLogPattern( + search: new Regex(@"^System\.IO\.FileNotFoundException: [^\n]+\n[^:]+: '[^\n]+[/\\]Saves[/\\]([^'\r\n]+)[/\\]([^'\r\n]+)'[\s\S]+LoadGameMenu\.FindSaveGames[\s\S]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant), + replacement: "The game can't find the '$2' file for your '$1' save. See https://stardewvalleywiki.com/Saves#Troubleshooting for help.", + logLevel: LogLevel.Error + ) + }; + + + /********* + ** Accessors + *********/ + /// The core logger and monitor for SMAPI. + public Monitor Monitor { get; } + + /// The core logger and monitor on behalf of the game. + public Monitor MonitorForGame { get; } + + + /********* + ** Public methods + *********/ + /**** + ** Initialization + ****/ + /// Construct an instance. + /// The log file path to write. + /// The colors to use for text written to the SMAPI console. + /// Whether to output log messages to the console. + /// Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed. + /// Whether to enable full console output for developers. + public LogManager(string logPath, ColorSchemeConfig colorConfig, bool writeToConsole, bool isVerbose, bool isDeveloperMode) + { + // init construction logic + this.GetMonitorImpl = name => new Monitor(name, this.InterceptionManager, this.LogFile, colorConfig, isVerbose) + { + WriteToConsole = writeToConsole, + ShowTraceInConsole = isDeveloperMode, + ShowFullStampInConsole = isDeveloperMode + }; + + // init fields + this.LogFile = new LogFileManager(logPath); + this.Monitor = this.GetMonitor("SMAPI"); + this.MonitorForGame = this.GetMonitor("game"); + + // redirect direct console output + if (this.MonitorForGame.WriteToConsole) + this.InterceptionManager.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message); + } + + /// Get a monitor instance derived from SMAPI's current settings. + /// The name of the module which will log messages with this instance. + public Monitor GetMonitor(string name) + { + return this.GetMonitorImpl(name); + } + + /// Set the title of the SMAPI console window. + /// The new window title. + public void SetConsoleTitle(string title) + { + Console.Title = title; + } + + /**** + ** Console input + ****/ + /// Run a loop handling console input. + [SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")] + public void RunConsoleInputLoop(CommandManager commandManager, Action reloadTranslations, Action handleInput, Func continueWhile) + { + // prepare console + this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); + commandManager + .Add(new HelpCommand(commandManager), this.Monitor) + .Add(new HarmonySummaryCommand(), this.Monitor) + .Add(new ReloadI18nCommand(reloadTranslations), this.Monitor); + + // start handling command line input + Thread inputThread = new Thread(() => + { + while (true) + { + // get input + string input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) + continue; + + // handle command + this.Monitor.LogUserInput(input); + handleInput(input); + } + }); + inputThread.Start(); + + // keep console thread alive while the game is running + while (continueWhile()) + Thread.Sleep(1000 / 10); + if (inputThread.ThreadState == ThreadState.Running) + inputThread.Abort(); + } + + /// Show a 'press any key to exit' message, and exit when they press a key. + public void PressAnyKeyToExit() + { + this.Monitor.Log("Game has ended. Press any key to exit.", LogLevel.Info); + this.PressAnyKeyToExit(showMessage: false); + } + + /// Show a 'press any key to exit' message, and exit when they press a key. + /// Whether to print a 'press any key to exit' message to the console. + public void PressAnyKeyToExit(bool showMessage) + { + if (showMessage) + Console.WriteLine("Game has ended. Press any key to exit."); + Thread.Sleep(100); + Console.ReadKey(); + Environment.Exit(0); + } + + /**** + ** Crash/update handling + ****/ + /// Create a crash marker and duplicate the log into the crash log. + public void WriteCrashLog() + { + try + { + File.WriteAllText(Constants.FatalCrashMarker, string.Empty); + File.Copy(this.LogFile.Path, Constants.FatalCrashLog, overwrite: true); + } + catch (Exception ex) + { + this.Monitor.Log($"SMAPI failed trying to track the crash details: {ex.GetLogSummary()}", LogLevel.Error); + } + } + + /// Write an update alert marker file. + /// The new version found. + public void WriteUpdateMarker(string version) + { + File.WriteAllText(Constants.UpdateMarker, version); + } + + /// Check whether SMAPI crashed or detected an update during the last session, and display them in the SMAPI console. + public void HandleMarkerFiles() + { + // show update alert + if (File.Exists(Constants.UpdateMarker)) + { + string rawUpdateFound = File.ReadAllText(Constants.UpdateMarker); + if (SemanticVersion.TryParse(rawUpdateFound, out ISemanticVersion updateFound)) + { + if (Constants.ApiVersion.IsPrerelease() && updateFound.IsNewerThan(Constants.ApiVersion)) + { + this.Monitor.Log("A new version of SMAPI was detected last time you played.", LogLevel.Error); + this.Monitor.Log($"You can update to {updateFound}: https://smapi.io.", LogLevel.Error); + this.Monitor.Log("Press any key to continue playing anyway. (This only appears when using a SMAPI beta.)", LogLevel.Info); + Console.ReadKey(); + } + } + File.Delete(Constants.UpdateMarker); + } + + // show details if game crashed during last session + if (File.Exists(Constants.FatalCrashMarker)) + { + this.Monitor.Log("The game crashed last time you played. If it happens repeatedly, see 'get help' on https://smapi.io.", LogLevel.Error); + this.Monitor.Log("If you ask for help, make sure to share your SMAPI log: https://smapi.io/log.", LogLevel.Error); + this.Monitor.Log("Press any key to delete the crash data and continue playing.", LogLevel.Info); + Console.ReadKey(); + File.Delete(Constants.FatalCrashLog); + File.Delete(Constants.FatalCrashMarker); + } + } + + /// Log a fatal exception which prevents SMAPI from launching. + /// The exception details. + public void LogFatalLaunchError(Exception exception) + { + switch (exception) + { + // audio crash + case InvalidOperationException ex when ex.Source == "Microsoft.Xna.Framework.Xact" && ex.StackTrace.Contains("Microsoft.Xna.Framework.Audio.AudioEngine..ctor"): + this.Monitor.Log("The game couldn't load audio. Do you have speakers or headphones plugged in?", LogLevel.Error); + this.Monitor.Log($"Technical details: {ex.GetLogSummary()}"); + break; + + // missing content folder exception + case FileNotFoundException ex when ex.Message == "Could not find file 'C:\\Program Files (x86)\\Steam\\SteamApps\\common\\Stardew Valley\\Content\\XACT\\FarmerSounds.xgs'.": // path in error is hardcoded regardless of install path + this.Monitor.Log("The game can't find its Content\\XACT\\FarmerSounds.xgs file. You can usually fix this by resetting your content files (see https://smapi.io/troubleshoot#reset-content ), or by uninstalling and reinstalling the game.", LogLevel.Error); + this.Monitor.Log($"Technical details: {ex.GetLogSummary()}"); + break; + + // generic exception + default: + this.MonitorForGame.Log($"The game failed to launch: {exception.GetLogSummary()}", LogLevel.Error); + break; + } + } + + /**** + ** General log output + ****/ + /// Log the initial header with general SMAPI and system details. + /// The path from which mods will be loaded. + /// The custom SMAPI settings. + public void LogIntro(string modsPath, IDictionary customSettings) + { + // init logging + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); + this.Monitor.Log($"Mods go here: {modsPath}", LogLevel.Info); + if (modsPath != Constants.DefaultModsPath) + this.Monitor.Log("(Using custom --mods-path argument.)"); + this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC"); + + // log custom settings + if (customSettings.Any()) + this.Monitor.Log($"Loaded with custom settings: {string.Join(", ", customSettings.OrderBy(p => p.Key).Select(p => $"{p.Key}: {p.Value}"))}"); + } + + /// Log details for settings that don't match the default. + /// Whether to enable full console output for developers. + /// Whether to check for newer versions of SMAPI and mods on startup. + public void LogSettingsHeader(bool isDeveloperMode, bool checkForUpdates) + { + if (isDeveloperMode) + 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 (!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) + this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); + this.Monitor.VerboseLog("Verbose logging enabled."); + } + + /// Log info about loaded mods. + /// The full list of loaded content packs and mods. + /// The loaded content packs. + /// The loaded mods. + /// The mods which could not be loaded. + /// Whether to log issues for mods which directly use potentially sensitive .NET APIs like file or shell access. + public void LogModInfo(IModMetadata[] loaded, IModMetadata[] loadedContentPacks, IModMetadata[] loadedMods, IModMetadata[] skippedMods, bool logParanoidWarnings) + { + // log loaded mods + this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info); + foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName)) + { + IManifest manifest = metadata.Manifest; + this.Monitor.Log( + $" {metadata.DisplayName} {manifest.Version}" + + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") + + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), + LogLevel.Info + ); + } + + this.Monitor.Newline(); + + // log loaded content packs + if (loadedContentPacks.Any()) + { + string GetModDisplayName(string id) => loadedMods.FirstOrDefault(p => p.HasID(id))?.DisplayName; + + this.Monitor.Log($"Loaded {loadedContentPacks.Length} content packs:", LogLevel.Info); + foreach (IModMetadata metadata in loadedContentPacks.OrderBy(p => p.DisplayName)) + { + IManifest manifest = metadata.Manifest; + this.Monitor.Log( + $" {metadata.DisplayName} {manifest.Version}" + + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") + + $" | for {GetModDisplayName(metadata.Manifest.ContentPackFor.UniqueID)}" + + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), + LogLevel.Info + ); + } + + this.Monitor.Newline(); + } + + // log mod warnings + this.LogModWarnings(loaded, skippedMods, logParanoidWarnings); + } + + /// + public void Dispose() + { + this.InterceptionManager.Dispose(); + this.LogFile.Dispose(); + } + + + /********* + ** Protected methods + *********/ + /// Redirect messages logged directly to the console to the given monitor. + /// The monitor with which to log messages as the game. + /// The message to log. + private void HandleConsoleMessage(IMonitor gameMonitor, string message) + { + // detect exception + LogLevel level = message.Contains("Exception") ? LogLevel.Error : LogLevel.Trace; + + // ignore suppressed message + if (level != LogLevel.Error && this.SuppressConsolePatterns.Any(p => p.IsMatch(message))) + return; + + // show friendly error if applicable + foreach (ReplaceLogPattern entry in this.ReplaceConsolePatterns) + { + string newMessage = entry.Search.Replace(message, entry.Replacement); + if (message != newMessage) + { + gameMonitor.Log(newMessage, entry.LogLevel); + gameMonitor.Log(message); + return; + } + } + + // forward to monitor + gameMonitor.Log(message, level); + } + + /// Write a summary of mod warnings to the console and log. + /// The loaded mods. + /// The mods which could not be loaded. + /// Whether to log issues for mods which directly use potentially sensitive .NET APIs like file or shell access. + private void LogModWarnings(IEnumerable mods, IModMetadata[] skippedMods, bool logParanoidWarnings) + { + // get mods with warnings + IModMetadata[] modsWithWarnings = mods.Where(p => p.Warnings != ModWarning.None).ToArray(); + if (!modsWithWarnings.Any() && !skippedMods.Any()) + return; + + // log intro + { + int count = modsWithWarnings.Length + skippedMods.Length; + this.Monitor.Log($"Found {count} mod{(count == 1 ? "" : "s")} with warnings:", LogLevel.Info); + } + + // log skipped mods + if (skippedMods.Any()) + { + // get logging logic + void LogSkippedMod(IModMetadata mod) + { + string message = $" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {mod.Error}"; + + this.Monitor.Log(message, LogLevel.Error); + if (mod.ErrorDetails != null) + this.Monitor.Log($" ({mod.ErrorDetails})"); + } + + // find skipped dependencies + IModMetadata[] skippedDependencies; + { + HashSet skippedDependencyIds = new HashSet(StringComparer.OrdinalIgnoreCase); + HashSet skippedModIds = new HashSet(from mod in skippedMods where mod.HasID() select mod.Manifest.UniqueID, StringComparer.OrdinalIgnoreCase); + foreach (IModMetadata mod in skippedMods) + { + foreach (string requiredId in skippedModIds.Intersect(mod.GetRequiredModIds())) + skippedDependencyIds.Add(requiredId); + } + skippedDependencies = skippedMods.Where(p => p.HasID() && skippedDependencyIds.Contains(p.Manifest.UniqueID)).ToArray(); + } + + // log skipped mods + this.Monitor.Log(" Skipped mods", LogLevel.Error); + this.Monitor.Log(" " + "".PadRight(50, '-'), LogLevel.Error); + this.Monitor.Log(" These mods could not be added to your game.", LogLevel.Error); + this.Monitor.Newline(); + + if (skippedDependencies.Any()) + { + foreach (IModMetadata mod in skippedDependencies.OrderBy(p => p.DisplayName)) + LogSkippedMod(mod); + this.Monitor.Newline(); + } + + foreach (IModMetadata mod in skippedMods.OrderBy(p => p.DisplayName)) + LogSkippedMod(mod); + this.Monitor.Newline(); + } + + // log warnings + if (modsWithWarnings.Any()) + { + // broken code + this.LogModWarningGroup(modsWithWarnings, ModWarning.BrokenCodeLoaded, LogLevel.Error, "Broken mods", + "These mods have broken code, but you configured SMAPI to load them anyway. This may cause bugs,", + "errors, or crashes in-game." + ); + + // changes serializer + this.LogModWarningGroup(modsWithWarnings, ModWarning.ChangesSaveSerializer, LogLevel.Warn, "Changed save serializer", + "These mods change the save serializer. They may corrupt your save files, or make them unusable if", + "you uninstall these mods." + ); + + // patched game code + this.LogModWarningGroup(modsWithWarnings, ModWarning.PatchesGame, LogLevel.Info, "Patched game code", + "These mods directly change the game code. They're more likely to cause errors or bugs in-game; if", + "your game has issues, try removing these first. Otherwise you can ignore this warning." + ); + + // unvalidated update tick + this.LogModWarningGroup(modsWithWarnings, ModWarning.UsesUnvalidatedUpdateTick, LogLevel.Info, "Bypassed safety checks", + "These mods bypass SMAPI's normal safety checks, so they're more likely to cause errors or save", + "corruption. If your game has issues, try removing these first." + ); + + // paranoid warnings + if (logParanoidWarnings) + { + this.LogModWarningGroup( + modsWithWarnings, + match: mod => mod.HasUnsuppressedWarnings(ModWarning.AccessesConsole, ModWarning.AccessesFilesystem, ModWarning.AccessesShell), + level: LogLevel.Debug, + heading: "Direct system access", + blurb: new[] + { + "You enabled paranoid warnings and these mods directly access the filesystem, shells/processes, or", + "SMAPI console. (This is usually legitimate and innocent usage; this warning is only useful for", + "further investigation.)" + }, + modLabel: mod => + { + List labels = new List(); + if (mod.HasUnsuppressedWarnings(ModWarning.AccessesConsole)) + labels.Add("console"); + if (mod.HasUnsuppressedWarnings(ModWarning.AccessesFilesystem)) + labels.Add("files"); + if (mod.HasUnsuppressedWarnings(ModWarning.AccessesShell)) + labels.Add("shells/processes"); + + return $"{mod.DisplayName} ({string.Join(", ", labels)})"; + } + ); + } + + // no update keys + this.LogModWarningGroup(modsWithWarnings, ModWarning.NoUpdateKeys, LogLevel.Debug, "No update keys", + "These mods have no update keys in their manifest. SMAPI may not notify you about updates for these", + "mods. Consider notifying the mod authors about this problem." + ); + + // not crossplatform + this.LogModWarningGroup(modsWithWarnings, ModWarning.UsesDynamic, LogLevel.Debug, "Not crossplatform", + "These mods use the 'dynamic' keyword, and won't work on Linux/Mac." + ); + } + } + + /// Write a mod warning group to the console and log. + /// The mods to search. + /// Matches mods to include in the warning group. + /// The log level for the logged messages. + /// A brief heading label for the group. + /// A detailed explanation of the warning, split into lines. + /// Formats the mod label, or null to use the . + private void LogModWarningGroup(IModMetadata[] mods, Func match, LogLevel level, string heading, string[] blurb, Func modLabel = null) + { + // get matching mods + string[] modLabels = mods + .Where(match) + .Select(mod => modLabel?.Invoke(mod) ?? mod.DisplayName) + .OrderBy(p => p) + .ToArray(); + if (!modLabels.Any()) + return; + + // log header/blurb + this.Monitor.Log(" " + heading, level); + this.Monitor.Log(" " + "".PadRight(50, '-'), level); + foreach (string line in blurb) + this.Monitor.Log(" " + line, level); + this.Monitor.Newline(); + + // log mod list + foreach (string label in modLabels) + this.Monitor.Log($" - {label}", level); + + this.Monitor.Newline(); + } + + /// Write a mod warning group to the console and log. + /// The mods to search. + /// The mod warning to match. + /// The log level for the logged messages. + /// A brief heading label for the group. + /// A detailed explanation of the warning, split into lines. + private void LogModWarningGroup(IModMetadata[] mods, ModWarning warning, LogLevel level, string heading, params string[] blurb) + { + this.LogModWarningGroup(mods, mod => mod.HasUnsuppressedWarnings(warning), level, heading, blurb); + } + + + /********* + ** Protected types + *********/ + /// A console log pattern to replace with a different message. + private class ReplaceLogPattern + { + /********* + ** Accessors + *********/ + /// The regex pattern matching the portion of the message to replace. + public Regex Search { get; } + + /// The replacement string. + public string Replacement { get; } + + /// The log level for the new message. + public LogLevel LogLevel { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The regex pattern matching the portion of the message to replace. + /// The replacement string. + /// The log level for the new message. + public ReplaceLogPattern(Regex search, string replacement, LogLevel logLevel) + { + this.Search = search; + this.Replacement = replacement; + this.LogLevel = logLevel; + } + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 3ad1bd38..e793b0cd 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -16,55 +16,58 @@ namespace StardewModdingAPI.Framework.ModLoading /********* ** Accessors *********/ - /// The mod's display name. + /// public string DisplayName { get; } - /// The root path containing mods. + /// public string RootPath { get; } - /// The mod's full directory path within the . + /// public string DirectoryPath { get; } - /// The relative to the . + /// public string RelativeDirectoryPath { get; } - /// The mod manifest. + /// public IManifest Manifest { get; } - /// Metadata about the mod from SMAPI's internal data (if any). + /// public ModDataRecordVersionedFields DataRecord { get; } - /// The metadata resolution status. + /// public ModMetadataStatus Status { get; private set; } - /// Indicates non-error issues with the mod. + /// public ModWarning Warnings { get; private set; } - /// The reason the metadata is invalid, if any. + /// public string Error { get; private set; } - /// Whether the mod folder should be ignored. This is true if it was found within a folder whose name starts with a dot. + /// + public string ErrorDetails { get; private set; } + + /// public bool IsIgnored { get; } - /// The mod instance (if loaded and is false). + /// public IMod Mod { get; private set; } - /// The content pack instance (if loaded and is true). + /// public IContentPack ContentPack { get; private set; } - /// The translations for this mod (if loaded). + /// public TranslationHelper Translations { get; private set; } - /// Writes messages to the console and log file as this mod. + /// public IMonitor Monitor { get; private set; } - /// The mod-provided API (if any). + /// public object Api { get; private set; } - /// The update-check metadata for this mod (if any). + /// public ModEntryModel UpdateCheckData { get; private set; } - /// Whether the mod is a content pack. + /// public bool IsContentPack => this.Manifest?.ContentPackFor != null; @@ -89,28 +92,23 @@ namespace StardewModdingAPI.Framework.ModLoading this.IsIgnored = isIgnored; } - /// Set the mod status. - /// The metadata resolution status. - /// The reason the metadata is invalid, if any. - /// Return the instance for chaining. - public IModMetadata SetStatus(ModMetadataStatus status, string error = null) + /// + public IModMetadata SetStatus(ModMetadataStatus status, string error = null, string errorDetails = null) { this.Status = status; this.Error = error; + this.ErrorDetails = errorDetails; return this; } - /// Set a warning flag for the mod. - /// The warning to set. + /// public IModMetadata SetWarning(ModWarning warning) { this.Warnings |= warning; return this; } - /// Set the mod instance. - /// The mod instance to set. - /// The translations for this mod (if loaded). + /// public IModMetadata SetMod(IMod mod, TranslationHelper translations) { if (this.ContentPack != null) @@ -122,10 +120,7 @@ namespace StardewModdingAPI.Framework.ModLoading return this; } - /// Set the mod instance. - /// The contentPack instance to set. - /// Writes messages to the console and log file. - /// The translations for this mod (if loaded). + /// public IModMetadata SetMod(IContentPack contentPack, IMonitor monitor, TranslationHelper translations) { if (this.Mod != null) @@ -137,29 +132,27 @@ namespace StardewModdingAPI.Framework.ModLoading return this; } - /// Set the mod-provided API instance. - /// The mod-provided API. + /// public IModMetadata SetApi(object api) { this.Api = api; return this; } - /// Set the update-check metadata for this mod. - /// The update-check metadata. + /// public IModMetadata SetUpdateData(ModEntryModel data) { this.UpdateCheckData = data; return this; } - /// Whether the mod manifest was loaded (regardless of whether the mod itself was loaded). + /// public bool HasManifest() { return this.Manifest != null; } - /// Whether the mod has an ID (regardless of whether the ID is valid or the mod itself was loaded). + /// public bool HasID() { return @@ -167,8 +160,7 @@ namespace StardewModdingAPI.Framework.ModLoading && !string.IsNullOrWhiteSpace(this.Manifest.UniqueID); } - /// Whether the mod has the given ID. - /// The mod ID to check. + /// public bool HasID(string id) { return @@ -176,8 +168,7 @@ namespace StardewModdingAPI.Framework.ModLoading && string.Equals(this.Manifest.UniqueID.Trim(), id?.Trim(), StringComparison.OrdinalIgnoreCase); } - /// Get the defined update keys. - /// Only return valid update keys. + /// public IEnumerable GetUpdateKeys(bool validOnly = false) { foreach (string rawKey in this.Manifest?.UpdateKeys ?? new string[0]) @@ -188,8 +179,7 @@ namespace StardewModdingAPI.Framework.ModLoading } } - /// Get the mod IDs that must be installed to load this mod. - /// Whether to include optional dependencies. + /// public IEnumerable GetRequiredModIds(bool includeOptional = false) { HashSet required = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -209,14 +199,13 @@ namespace StardewModdingAPI.Framework.ModLoading yield return this.Manifest.ContentPackFor.UniqueID; } - /// Whether the mod has at least one valid update key set. + /// public bool HasValidUpdateKeys() { return this.GetUpdateKeys(validOnly: true).Any(); } - /// Get whether the mod has any of the given warnings which haven't been suppressed in the . - /// The warnings to check. + /// public bool HasUnsuppressedWarnings(params ModWarning[] warnings) { return warnings.Any(warning => @@ -225,7 +214,7 @@ namespace StardewModdingAPI.Framework.ModLoading ); } - /// Get a relative path which includes the root folder name. + /// public string GetRelativePathWithRoot() { string rootFolderName = Path.GetFileName(this.RootPath) ?? ""; diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index fd8d7034..5bd02dc8 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; @@ -9,14 +8,12 @@ using System.Reflection; using System.Runtime.ExceptionServices; using System.Security; using System.Text; -using System.Text.RegularExpressions; using System.Threading; #if SMAPI_FOR_WINDOWS using System.Windows.Forms; #endif using Newtonsoft.Json; using StardewModdingAPI.Events; -using StardewModdingAPI.Framework.Commands; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Logging; @@ -36,7 +33,6 @@ using StardewModdingAPI.Toolkit.Utilities; using StardewModdingAPI.Utilities; using StardewValley; using Object = StardewValley.Object; -using ThreadState = System.Threading.ThreadState; namespace StardewModdingAPI.Framework { @@ -46,17 +42,11 @@ namespace StardewModdingAPI.Framework /********* ** Fields *********/ - /// The log file to which to write messages. - private readonly LogFileManager LogFile; - - /// Manages console output interception. - private readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager(); + /// Manages the SMAPI console window and log file. + private readonly LogManager LogManager; /// The core logger and monitor for SMAPI. - private readonly Monitor Monitor; - - /// The core logger and monitor on behalf of the game. - private readonly Monitor MonitorForGame; + private Monitor Monitor => this.LogManager.Monitor; /// Tracks whether the game should exit immediately and any pending initialization should be cancelled. private readonly CancellationTokenSource CancellationToken = new CancellationTokenSource(); @@ -89,39 +79,6 @@ namespace StardewModdingAPI.Framework /// Whether the program has been disposed. private bool IsDisposed; - /// Regex patterns which match console non-error messages to suppress from the console and log. - private readonly Regex[] SuppressConsolePatterns = - { - new Regex(@"^TextBox\.Selected is now '(?:True|False)'\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new Regex(@"^(?:FRUIT )?TREE: IsClient:(?:True|False) randomOutput: \d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new Regex(@"^loadPreferences\(\); begin", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new Regex(@"^savePreferences\(\); async=", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new Regex(@"^DebugOutput:\s+(?:added CLOUD|added cricket|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant) - }; - - /// Regex patterns which match console messages to show a more friendly error for. - private readonly ReplaceLogPattern[] ReplaceConsolePatterns = - { - // Steam not loaded - new ReplaceLogPattern( - search: new Regex(@"^System\.InvalidOperationException: Steamworks is not initialized\.[\s\S]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant), - replacement: -#if SMAPI_FOR_WINDOWS - "Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that (see 'Part 2: Configure Steam' in the install guide for more info: https://smapi.io/install).", -#else - "Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that.", -#endif - logLevel: LogLevel.Error - ), - - // save file not found error - new ReplaceLogPattern( - search: new Regex(@"^System\.IO\.FileNotFoundException: [^\n]+\n[^:]+: '[^\n]+[/\\]Saves[/\\]([^'\r\n]+)[/\\]([^'\r\n]+)'[\s\S]+LoadGameMenu\.FindSaveGames[\s\S]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant), - replacement: "The game can't find the '$2' file for your '$1' save. See https://stardewvalleywiki.com/Saves#Troubleshooting for help.", - logLevel: LogLevel.Error - ) - }; - /// The mod toolkit used for generic mod interactions. private readonly ModToolkit Toolkit = new ModToolkit(); @@ -163,14 +120,7 @@ namespace StardewModdingAPI.Framework if (File.Exists(Constants.ApiUserConfigPath)) JsonConvert.PopulateObject(File.ReadAllText(Constants.ApiUserConfigPath), this.Settings); - this.LogFile = new LogFileManager(logPath); - this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.Settings.ConsoleColors, this.Settings.VerboseLogging) - { - WriteToConsole = writeToConsole, - ShowTraceInConsole = this.Settings.DeveloperMode, - ShowFullStampInConsole = this.Settings.DeveloperMode - }; - this.MonitorForGame = this.GetSecondaryMonitor("game"); + this.LogManager = new LogManager(logPath: logPath, colorConfig: this.Settings.ConsoleColors, writeToConsole: writeToConsole, isVerbose: this.Settings.VerboseLogging, isDeveloperMode: this.Settings.DeveloperMode); SCore.PerformanceMonitor = new PerformanceMonitor(this.Monitor); this.EventManager = new EventManager(this.ModRegistry, SCore.PerformanceMonitor); @@ -180,38 +130,21 @@ namespace StardewModdingAPI.Framework SDate.Translations = this.Translator; - // redirect direct console output - if (this.MonitorForGame.WriteToConsole) - this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message); - - // init logging - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); - this.Monitor.Log($"Mods go here: {modsPath}", LogLevel.Info); - if (modsPath != Constants.DefaultModsPath) - this.Monitor.Log("(Using custom --mods-path argument.)", LogLevel.Trace); - this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC", LogLevel.Trace); - - // log custom settings - { - IDictionary customSettings = this.Settings.GetCustomSettings(); - if (customSettings.Any()) - this.Monitor.Log($"Loaded with custom settings: {string.Join(", ", customSettings.OrderBy(p => p.Key).Select(p => $"{p.Key}: {p.Value}"))}", LogLevel.Trace); - } + // log SMAPI/OS info + this.LogManager.LogIntro(modsPath, this.Settings.GetCustomSettings()); // validate platform #if SMAPI_FOR_WINDOWS if (Constants.Platform != Platform.Windows) { this.Monitor.Log("Oops! You're running Windows, but this version of SMAPI is for Linux or Mac. Please reinstall SMAPI to fix this.", LogLevel.Error); - this.PressAnyKeyToExit(); - return; + this.LogManager.PressAnyKeyToExit(); } #else if (Constants.Platform == Platform.Windows) { this.Monitor.Log($"Oops! You're running {Constants.Platform}, but this version of SMAPI is for Windows. Please reinstall SMAPI to fix this.", LogLevel.Error); this.PressAnyKeyToExit(); - return; } #endif } @@ -249,7 +182,7 @@ namespace StardewModdingAPI.Framework SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper, this.InitializeBeforeFirstAssetLoaded); this.GameInstance = new SGame( monitor: this.Monitor, - monitorForGame: this.MonitorForGame, + monitorForGame: this.LogManager.MonitorForGame, reflection: this.Reflection, translator: this.Translator, eventManager: this.EventManager, @@ -267,12 +200,12 @@ namespace StardewModdingAPI.Framework // apply game patches new GamePatcher(this.Monitor).Apply( - new EventErrorPatch(this.MonitorForGame), - new DialogueErrorPatch(this.MonitorForGame, this.Reflection), + new EventErrorPatch(this.LogManager.MonitorForGame), + new DialogueErrorPatch(this.LogManager.MonitorForGame, this.Reflection), new ObjectErrorPatch(), new LoadContextPatch(this.Reflection, this.GameInstance.OnLoadStageChanged), new LoadErrorPatch(this.Monitor, this.GameInstance.OnSaveContentRemoved), - new ScheduleErrorPatch(this.MonitorForGame) + new ScheduleErrorPatch(this.LogManager.MonitorForGame) ); // add exit handler @@ -281,71 +214,29 @@ namespace StardewModdingAPI.Framework this.CancellationToken.Token.WaitHandle.WaitOne(); if (this.IsGameRunning) { - try - { - File.WriteAllText(Constants.FatalCrashMarker, string.Empty); - File.Copy(this.LogFile.Path, Constants.FatalCrashLog, overwrite: true); - } - catch (Exception ex) - { - this.Monitor.Log($"SMAPI failed trying to track the crash details: {ex.GetLogSummary()}", LogLevel.Error); - } - + this.LogManager.WriteCrashLog(); this.GameInstance.Exit(); } }).Start(); // set window titles this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"; + this.LogManager.SetConsoleTitle($"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"); } catch (Exception ex) { this.Monitor.Log($"SMAPI failed to initialize: {ex.GetLogSummary()}", LogLevel.Error); - this.PressAnyKeyToExit(); + this.LogManager.PressAnyKeyToExit(); return; } - // check update marker - if (File.Exists(Constants.UpdateMarker)) - { - string rawUpdateFound = File.ReadAllText(Constants.UpdateMarker); - if (SemanticVersion.TryParse(rawUpdateFound, out ISemanticVersion updateFound)) - { - if (Constants.ApiVersion.IsPrerelease() && updateFound.IsNewerThan(Constants.ApiVersion)) - { - this.Monitor.Log("A new version of SMAPI was detected last time you played.", LogLevel.Error); - this.Monitor.Log($"You can update to {updateFound}: https://smapi.io.", LogLevel.Error); - this.Monitor.Log("Press any key to continue playing anyway. (This only appears when using a SMAPI beta.)", LogLevel.Info); - Console.ReadKey(); - } - } - File.Delete(Constants.UpdateMarker); - } + // log basic info + this.LogManager.HandleMarkerFiles(); + this.LogManager.LogSettingsHeader(this.Settings.DeveloperMode, this.Settings.CheckForUpdates); - // show details if game crashed during last session - if (File.Exists(Constants.FatalCrashMarker)) - { - this.Monitor.Log("The game crashed last time you played. If it happens repeatedly, see 'get help' on https://smapi.io.", LogLevel.Error); - this.Monitor.Log("If you ask for help, make sure to share your SMAPI log: https://smapi.io/log.", LogLevel.Error); - this.Monitor.Log("Press any key to delete the crash data and continue playing.", LogLevel.Info); - Console.ReadKey(); - File.Delete(Constants.FatalCrashLog); - File.Delete(Constants.FatalCrashMarker); - } - - // 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.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) - this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); - this.Monitor.VerboseLog("Verbose logging enabled."); - - // update window titles + // set window titles this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"; + this.LogManager.SetConsoleTitle($"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"); // start game this.Monitor.Log("Starting game...", LogLevel.Debug); @@ -355,22 +246,10 @@ namespace StardewModdingAPI.Framework StardewValley.Program.releaseBuild = true; // game's debug logic interferes with SMAPI opening the game window this.GameInstance.Run(); } - catch (InvalidOperationException ex) when (ex.Source == "Microsoft.Xna.Framework.Xact" && ex.StackTrace.Contains("Microsoft.Xna.Framework.Audio.AudioEngine..ctor")) - { - this.Monitor.Log("The game couldn't load audio. Do you have speakers or headphones plugged in?", LogLevel.Error); - this.Monitor.Log($"Technical details: {ex.GetLogSummary()}", LogLevel.Trace); - this.PressAnyKeyToExit(); - } - catch (FileNotFoundException ex) when (ex.Message == "Could not find file 'C:\\Program Files (x86)\\Steam\\SteamApps\\common\\Stardew Valley\\Content\\XACT\\FarmerSounds.xgs'.") // path in error is hardcoded regardless of install path - { - this.Monitor.Log("The game can't find its Content\\XACT\\FarmerSounds.xgs file. You can usually fix this by resetting your content files (see https://smapi.io/troubleshoot#reset-content ), or by uninstalling and reinstalling the game.", LogLevel.Error); - this.Monitor.Log($"Technical details: {ex.GetLogSummary()}", LogLevel.Trace); - this.PressAnyKeyToExit(); - } catch (Exception ex) { - this.MonitorForGame.Log($"The game failed to launch: {ex.GetLogSummary()}", LogLevel.Error); - this.PressAnyKeyToExit(); + this.LogManager.LogFatalLaunchError(ex); + this.LogManager.PressAnyKeyToExit(); } finally { @@ -385,7 +264,7 @@ namespace StardewModdingAPI.Framework if (this.IsDisposed) return; this.IsDisposed = true; - this.Monitor.Log("Disposing...", LogLevel.Trace); + this.Monitor.Log("Disposing..."); // dispose mod data foreach (IModMetadata mod in this.ModRegistry.GetAll()) @@ -402,11 +281,10 @@ namespace StardewModdingAPI.Framework // dispose core components this.IsGameRunning = false; - this.ConsoleManager?.Dispose(); + this.LogManager?.Dispose(); this.ContentCore?.Dispose(); this.CancellationToken?.Dispose(); this.GameInstance?.Dispose(); - this.LogFile?.Dispose(); // end game (moved from Game1.OnExiting to let us clean up first) Process.GetCurrentProcess().Kill(); @@ -426,15 +304,7 @@ namespace StardewModdingAPI.Framework } // init TMX support - try - { - xTile.Format.FormatManager.Instance.RegisterMapFormat(new TMXTile.TMXFormat(Game1.tileSize / Game1.pixelZoom, Game1.tileSize / Game1.pixelZoom, Game1.pixelZoom, Game1.pixelZoom)); - } - catch (Exception ex) - { - this.Monitor.Log("SMAPI couldn't load TMX support. Some mods may not work correctly.", LogLevel.Warn); - this.Monitor.Log($"Technical details: {ex.GetLogSummary()}", LogLevel.Trace); - } + xTile.Format.FormatManager.Instance.RegisterMapFormat(new TMXTile.TMXFormat(Game1.tileSize / Game1.pixelZoom, Game1.tileSize / Game1.pixelZoom, Game1.pixelZoom, Game1.pixelZoom)); // load mod data ModToolkit toolkit = new ModToolkit(); @@ -442,14 +312,14 @@ namespace StardewModdingAPI.Framework // load mods { - this.Monitor.Log("Loading mod metadata...", LogLevel.Trace); + this.Monitor.Log("Loading mod metadata..."); ModResolver resolver = new ModResolver(); // log loose files { string[] looseFiles = new DirectoryInfo(this.ModsPath).GetFiles().Select(p => p.Name).ToArray(); if (looseFiles.Any()) - this.Monitor.Log($" Ignored loose files: {string.Join(", ", looseFiles.OrderBy(p => p, StringComparer.OrdinalIgnoreCase))}", LogLevel.Trace); + this.Monitor.Log($" Ignored loose files: {string.Join(", ", looseFiles.OrderBy(p => p, StringComparer.OrdinalIgnoreCase))}"); } // load manifests @@ -457,7 +327,7 @@ namespace StardewModdingAPI.Framework // filter out ignored mods foreach (IModMetadata mod in mods.Where(p => p.IsIgnored)) - this.Monitor.Log($" Skipped {mod.GetRelativePathWithRoot()} (folder name starts with a dot).", LogLevel.Trace); + this.Monitor.Log($" Skipped {mod.GetRelativePathWithRoot()} (folder name starts with a dot)."); mods = mods.Where(p => !p.IsIgnored).ToArray(); // load mods @@ -472,7 +342,7 @@ namespace StardewModdingAPI.Framework // update window titles int modsLoaded = this.ModRegistry.GetAll().Count(); this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"; + this.LogManager.SetConsoleTitle($"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"); } /// Initialize SMAPI and mods after the game starts. @@ -483,7 +353,14 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); // start SMAPI console - new Thread(this.RunConsoleLoop).Start(); + new Thread( + () => this.LogManager.RunConsoleInputLoop( + commandManager: this.GameInstance.CommandManager, + reloadTranslations: this.ReloadTranslations, + handleInput: input => this.GameInstance.CommandQueue.Enqueue(input), + continueWhile: () => this.IsGameRunning && !this.CancellationToken.IsCancellationRequested + ) + ).Start(); } /// Handle the game changing locale. @@ -503,53 +380,18 @@ namespace StardewModdingAPI.Framework mod.Translations.SetLocale(locale, languageCode); } - /// Run a loop handling console input. - [SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")] - private void RunConsoleLoop() - { - // prepare console - 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) - .Add(new HarmonySummaryCommand(), this.Monitor) - .Add(new ReloadI18nCommand(this.ReloadTranslations), this.Monitor); - - // start handling command line input - Thread inputThread = new Thread(() => - { - while (true) - { - // get input - string input = Console.ReadLine(); - if (string.IsNullOrWhiteSpace(input)) - continue; - - // handle command - this.Monitor.LogUserInput(input); - this.GameInstance.CommandQueue.Enqueue(input); - } - }); - inputThread.Start(); - - // keep console thread alive while the game is running - while (this.IsGameRunning && !this.CancellationToken.IsCancellationRequested) - Thread.Sleep(1000 / 10); - if (inputThread.ThreadState == ThreadState.Running) - inputThread.Abort(); - } - /// Look for common issues with the game's XNB content, and log warnings if anything looks broken or outdated. /// Returns whether all integrity checks passed. private bool ValidateContentIntegrity() { - this.Monitor.Log("Detecting common issues...", LogLevel.Trace); + this.Monitor.Log("Detecting common issues..."); bool issuesFound = false; // object format (commonly broken by outdated files) { // detect issues bool hasObjectIssues = false; - void LogIssue(int id, string issue) => this.Monitor.Log($@"Detected issue: item #{id} in Content\Data\ObjectInformation.xnb is invalid ({issue}).", LogLevel.Trace); + void LogIssue(int id, string issue) => this.Monitor.Log($@"Detected issue: item #{id} in Content\Data\ObjectInformation.xnb is invalid ({issue})."); foreach (KeyValuePair entry in Game1.objectInformation) { // must not be empty @@ -608,7 +450,7 @@ namespace StardewModdingAPI.Framework url = url.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac #endif WebApiClient client = new WebApiClient(url, Constants.ApiVersion); - this.Monitor.Log("Checking for updates...", LogLevel.Trace); + this.Monitor.Log("Checking for updates..."); // check SMAPI version ISemanticVersion updateFound = null; @@ -619,7 +461,7 @@ namespace StardewModdingAPI.Framework if (response.SuggestedUpdate != null) this.Monitor.Log($"You can update SMAPI to {response.SuggestedUpdate.Version}: {Constants.HomePageUrl}", LogLevel.Alert); else - this.Monitor.Log(" SMAPI okay.", LogLevel.Trace); + this.Monitor.Log(" SMAPI okay."); updateFound = response.SuggestedUpdate?.Version; @@ -627,7 +469,7 @@ namespace StardewModdingAPI.Framework if (response.Errors.Any()) { this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); - this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}", LogLevel.Trace); + this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}"); } } catch (Exception ex) @@ -635,13 +477,13 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you won't be notified of new versions if this keeps happening.", LogLevel.Warn); this.Monitor.Log(ex is WebException && ex.InnerException == null ? $"Error: {ex.Message}" - : $"Error: {ex.GetLogSummary()}", LogLevel.Trace + : $"Error: {ex.GetLogSummary()}" ); } // show update message on next launch if (updateFound != null) - File.WriteAllText(Constants.UpdateMarker, updateFound.ToString()); + this.LogManager.WriteUpdateMarker(updateFound.ToString()); // check mod versions if (mods.Any()) @@ -665,7 +507,7 @@ namespace StardewModdingAPI.Framework } // fetch results - this.Monitor.Log($" Checking for updates to {searchMods.Count} mods...", LogLevel.Trace); + this.Monitor.Log($" Checking for updates to {searchMods.Count} mods..."); IDictionary results = client.GetModInfo(searchMods.ToArray(), apiVersion: Constants.ApiVersion, gameVersion: Constants.GameVersion, platform: Constants.Platform); // extract update alerts & errors @@ -694,7 +536,7 @@ namespace StardewModdingAPI.Framework // show update errors if (errors.Length != 0) - this.Monitor.Log("Got update-check errors for some mods:\n" + errors.ToString().TrimEnd(), LogLevel.Trace); + this.Monitor.Log("Got update-check errors for some mods:\n" + errors.ToString().TrimEnd()); // show update alerts if (updates.Any()) @@ -710,14 +552,14 @@ namespace StardewModdingAPI.Framework } } else - this.Monitor.Log(" All mods up to date.", LogLevel.Trace); + this.Monitor.Log(" All mods up to date."); } catch (Exception ex) { this.Monitor.Log("Couldn't check for new mod versions. This won't affect your game, but you won't be notified of mod updates if this keeps happening.", LogLevel.Warn); this.Monitor.Log(ex is WebException && ex.InnerException == null ? ex.Message - : ex.ToString(), LogLevel.Trace + : ex.ToString() ); } } @@ -747,27 +589,24 @@ namespace StardewModdingAPI.Framework /// Handles access to SMAPI's internal mod metadata list. private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase) { - this.Monitor.Log("Loading mods...", LogLevel.Trace); + this.Monitor.Log("Loading mods..."); // load mods - IDictionary> skippedMods = new Dictionary>(); + IList skippedMods = new List(); using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings)) { // init HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase); InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); - void LogSkip(IModMetadata mod, string errorPhrase, string errorDetails) - { - skippedMods[mod] = Tuple.Create(errorPhrase, errorDetails); - if (mod.Status != ModMetadataStatus.Failed) - mod.SetStatus(ModMetadataStatus.Failed, errorPhrase); - } // load mods - foreach (IModMetadata contentPack in mods) + foreach (IModMetadata mod in mods) { - if (!this.TryLoadMod(contentPack, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out string errorPhrase, out string errorDetails)) - LogSkip(contentPack, errorPhrase, errorDetails); + if (!this.TryLoadMod(mod, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out string errorPhrase, out string errorDetails)) + { + mod.SetStatus(ModMetadataStatus.Failed, errorPhrase, errorDetails); + skippedMods.Add(mod); + } } } IModMetadata[] loaded = this.ModRegistry.GetAll().ToArray(); @@ -777,42 +616,8 @@ namespace StardewModdingAPI.Framework // unlock content packs this.ModRegistry.AreAllModsLoaded = true; - // log loaded mods - this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info); - foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName)) - { - IManifest manifest = metadata.Manifest; - this.Monitor.Log( - $" {metadata.DisplayName} {manifest.Version}" - + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") - + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), - LogLevel.Info - ); - } - this.Monitor.Newline(); - - // log loaded content packs - if (loadedContentPacks.Any()) - { - string GetModDisplayName(string id) => loadedMods.FirstOrDefault(p => p.HasID(id))?.DisplayName; - - this.Monitor.Log($"Loaded {loadedContentPacks.Length} content packs:", LogLevel.Info); - foreach (IModMetadata metadata in loadedContentPacks.OrderBy(p => p.DisplayName)) - { - IManifest manifest = metadata.Manifest; - this.Monitor.Log( - $" {metadata.DisplayName} {manifest.Version}" - + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") - + $" | for {GetModDisplayName(metadata.Manifest.ContentPackFor.UniqueID)}" - + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), - LogLevel.Info - ); - } - this.Monitor.Newline(); - } - - // log mod warnings - this.LogModWarnings(loaded, skippedMods); + // log mod info + this.LogManager.LogModInfo(loaded, loadedContentPacks, loadedMods, skippedMods.ToArray(), this.Settings.ParanoidWarnings); // initialize translations this.ReloadTranslations(loaded); @@ -856,7 +661,7 @@ namespace StardewModdingAPI.Framework } if (api != null) - this.Monitor.Log($" Found mod-provided API ({api.GetType().FullName}).", LogLevel.Trace); + this.Monitor.Log($" Found mod-provided API ({api.GetType().FullName})."); metadata.SetApi(api); } catch (Exception ex) @@ -918,11 +723,11 @@ namespace StardewModdingAPI.Framework { string relativePath = mod.GetRelativePathWithRoot(); if (mod.IsContentPack) - this.Monitor.Log($" {mod.DisplayName} (from {relativePath}) [content pack]...", LogLevel.Trace); + this.Monitor.Log($" {mod.DisplayName} (from {relativePath}) [content pack]..."); else if (mod.Manifest?.EntryDll != null) - this.Monitor.Log($" {mod.DisplayName} (from {relativePath}{Path.DirectorySeparatorChar}{mod.Manifest.EntryDll})...", LogLevel.Trace); // don't use Path.Combine here, since EntryDLL might not be valid + this.Monitor.Log($" {mod.DisplayName} (from {relativePath}{Path.DirectorySeparatorChar}{mod.Manifest.EntryDll})..."); // don't use Path.Combine here, since EntryDLL might not be valid else - this.Monitor.Log($" {mod.DisplayName} (from {relativePath})...", LogLevel.Trace); + this.Monitor.Log($" {mod.DisplayName} (from {relativePath})..."); } // add warning for missing update key @@ -932,7 +737,7 @@ namespace StardewModdingAPI.Framework // validate status if (mod.Status == ModMetadataStatus.Failed) { - this.Monitor.Log($" Failed: {mod.Error}", LogLevel.Trace); + this.Monitor.Log($" Failed: {mod.Error}"); errorReasonPhrase = mod.Error; return false; } @@ -958,7 +763,7 @@ namespace StardewModdingAPI.Framework if (mod.IsContentPack) { IManifest manifest = mod.Manifest; - IMonitor monitor = this.GetSecondaryMonitor(mod.DisplayName); + IMonitor monitor = this.LogManager.GetMonitor(mod.DisplayName); IContentHelper contentHelper = new ContentHelper(this.ContentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor); TranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, contentCore.GetLocale(), contentCore.Language); IContentPack contentPack = new ContentPack(mod.DirectoryPath, manifest, contentHelper, translationHelper, jsonHelper); @@ -1023,13 +828,13 @@ namespace StardewModdingAPI.Framework } // init mod helpers - IMonitor monitor = this.GetSecondaryMonitor(mod.DisplayName); + IMonitor monitor = this.LogManager.GetMonitor(mod.DisplayName); TranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, contentCore.GetLocale(), contentCore.Language); IModHelper modHelper; { IContentPack CreateFakeContentPack(string packDirPath, IManifest packManifest) { - IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); + IMonitor packMonitor = this.LogManager.GetMonitor(packManifest.Name); IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); ITranslationHelper packTranslationHelper = new TranslationHelper(packManifest.UniqueID, contentCore.GetLocale(), contentCore.Language); return new ContentPack(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper); @@ -1065,182 +870,6 @@ namespace StardewModdingAPI.Framework } } - /// Write a summary of mod warnings to the console and log. - /// The loaded mods. - /// The mods which were skipped, along with the friendly and developer reasons. - private void LogModWarnings(IEnumerable mods, IDictionary> skippedMods) - { - // get mods with warnings - IModMetadata[] modsWithWarnings = mods.Where(p => p.Warnings != ModWarning.None).ToArray(); - if (!modsWithWarnings.Any() && !skippedMods.Any()) - return; - - // log intro - { - int count = modsWithWarnings.Union(skippedMods.Keys).Count(); - this.Monitor.Log($"Found {count} mod{(count == 1 ? "" : "s")} with warnings:", LogLevel.Info); - } - - // log skipped mods - if (skippedMods.Any()) - { - // get logging logic - HashSet logged = new HashSet(); - void LogSkippedMod(IModMetadata mod, string errorReason, string errorDetails) - { - string message = $" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {errorReason}"; - - if (logged.Add($"{message}|{errorDetails}")) - { - this.Monitor.Log(message, LogLevel.Error); - if (errorDetails != null) - this.Monitor.Log($" ({errorDetails})", LogLevel.Trace); - } - } - - // find skipped dependencies - KeyValuePair>[] skippedDependencies; - { - HashSet skippedDependencyIds = new HashSet(StringComparer.OrdinalIgnoreCase); - HashSet skippedModIds = new HashSet(from mod in skippedMods where mod.Key.HasID() select mod.Key.Manifest.UniqueID, StringComparer.OrdinalIgnoreCase); - foreach (IModMetadata mod in skippedMods.Keys) - { - foreach (string requiredId in skippedModIds.Intersect(mod.GetRequiredModIds())) - skippedDependencyIds.Add(requiredId); - } - skippedDependencies = skippedMods.Where(p => p.Key.HasID() && skippedDependencyIds.Contains(p.Key.Manifest.UniqueID)).ToArray(); - } - - // log skipped mods - this.Monitor.Log(" Skipped mods", LogLevel.Error); - this.Monitor.Log(" " + "".PadRight(50, '-'), LogLevel.Error); - this.Monitor.Log(" These mods could not be added to your game.", LogLevel.Error); - this.Monitor.Newline(); - - if (skippedDependencies.Any()) - { - foreach (var pair in skippedDependencies.OrderBy(p => p.Key.DisplayName)) - LogSkippedMod(pair.Key, pair.Value.Item1, pair.Value.Item2); - this.Monitor.Newline(); - } - - foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName)) - LogSkippedMod(pair.Key, pair.Value.Item1, pair.Value.Item2); - this.Monitor.Newline(); - } - - // log warnings - if (modsWithWarnings.Any()) - { - // broken code - this.LogModWarningGroup(modsWithWarnings, ModWarning.BrokenCodeLoaded, LogLevel.Error, "Broken mods", - "These mods have broken code, but you configured SMAPI to load them anyway. This may cause bugs,", - "errors, or crashes in-game." - ); - - // changes serializer - this.LogModWarningGroup(modsWithWarnings, ModWarning.ChangesSaveSerializer, LogLevel.Warn, "Changed save serializer", - "These mods change the save serializer. They may corrupt your save files, or make them unusable if", - "you uninstall these mods." - ); - - // patched game code - this.LogModWarningGroup(modsWithWarnings, ModWarning.PatchesGame, LogLevel.Info, "Patched game code", - "These mods directly change the game code. They're more likely to cause errors or bugs in-game; if", - "your game has issues, try removing these first. Otherwise you can ignore this warning." - ); - - // unvalidated update tick - this.LogModWarningGroup(modsWithWarnings, ModWarning.UsesUnvalidatedUpdateTick, LogLevel.Info, "Bypassed safety checks", - "These mods bypass SMAPI's normal safety checks, so they're more likely to cause errors or save", - "corruption. If your game has issues, try removing these first." - ); - - // paranoid warnings - if (this.Settings.ParanoidWarnings) - { - this.LogModWarningGroup( - modsWithWarnings, - match: mod => mod.HasUnsuppressedWarnings(ModWarning.AccessesConsole, ModWarning.AccessesFilesystem, ModWarning.AccessesShell), - level: LogLevel.Debug, - heading: "Direct system access", - blurb: new[] - { - "You enabled paranoid warnings and these mods directly access the filesystem, shells/processes, or", - "SMAPI console. (This is usually legitimate and innocent usage; this warning is only useful for", - "further investigation.)" - }, - modLabel: mod => - { - List labels = new List(); - if (mod.HasUnsuppressedWarnings(ModWarning.AccessesConsole)) - labels.Add("console"); - if (mod.HasUnsuppressedWarnings(ModWarning.AccessesFilesystem)) - labels.Add("files"); - if (mod.HasUnsuppressedWarnings(ModWarning.AccessesShell)) - labels.Add("shells/processes"); - - return $"{mod.DisplayName} ({string.Join(", ", labels)})"; - } - ); - } - - // no update keys - this.LogModWarningGroup(modsWithWarnings, ModWarning.NoUpdateKeys, LogLevel.Debug, "No update keys", - "These mods have no update keys in their manifest. SMAPI may not notify you about updates for these", - "mods. Consider notifying the mod authors about this problem." - ); - - // not crossplatform - this.LogModWarningGroup(modsWithWarnings, ModWarning.UsesDynamic, LogLevel.Debug, "Not crossplatform", - "These mods use the 'dynamic' keyword, and won't work on Linux/Mac." - ); - } - } - - /// Write a mod warning group to the console and log. - /// The mods to search. - /// Matches mods to include in the warning group. - /// The log level for the logged messages. - /// A brief heading label for the group. - /// A detailed explanation of the warning, split into lines. - /// Formats the mod label, or null to use the . - private void LogModWarningGroup(IModMetadata[] mods, Func match, LogLevel level, string heading, string[] blurb, Func modLabel = null) - { - // get matching mods - string[] modLabels = mods - .Where(match) - .Select(mod => modLabel?.Invoke(mod) ?? mod.DisplayName) - .OrderBy(p => p) - .ToArray(); - if (!modLabels.Any()) - return; - - // log header/blurb - this.Monitor.Log(" " + heading, level); - this.Monitor.Log(" " + "".PadRight(50, '-'), level); - foreach (string line in blurb) - this.Monitor.Log(" " + line, level); - this.Monitor.Newline(); - - // log mod list - foreach (string label in modLabels) - this.Monitor.Log($" - {label}", level); - - this.Monitor.Newline(); - } - - /// Write a mod warning group to the console and log. - /// The mods to search. - /// The mod warning to match. - /// The log level for the logged messages. - /// A brief heading label for the group. - /// A detailed explanation of the warning, split into lines. - void LogModWarningGroup(IModMetadata[] mods, ModWarning warning, LogLevel level, string heading, params string[] blurb) - { - this.LogModWarningGroup(mods, mod => mod.HasUnsuppressedWarnings(warning), level, heading, blurb); - } - /// Load a mod's entry class. /// The mod assembly. /// The loaded instance. @@ -1366,64 +995,6 @@ namespace StardewModdingAPI.Framework return translations; } - /// Redirect messages logged directly to the console to the given monitor. - /// The monitor with which to log messages as the game. - /// The message to log. - private void HandleConsoleMessage(IMonitor gameMonitor, string message) - { - // detect exception - LogLevel level = message.Contains("Exception") ? LogLevel.Error : LogLevel.Trace; - - // ignore suppressed message - if (level != LogLevel.Error && this.SuppressConsolePatterns.Any(p => p.IsMatch(message))) - return; - - // show friendly error if applicable - foreach (ReplaceLogPattern entry in this.ReplaceConsolePatterns) - { - string newMessage = entry.Search.Replace(message, entry.Replacement); - if (message != newMessage) - { - gameMonitor.Log(newMessage, entry.LogLevel); - gameMonitor.Log(message, LogLevel.Trace); - return; - } - } - - // forward to monitor - gameMonitor.Log(message, level); - } - - /// Show a 'press any key to exit' message, and exit when they press a key. - private void PressAnyKeyToExit() - { - this.Monitor.Log("Game has ended. Press any key to exit.", LogLevel.Info); - this.PressAnyKeyToExit(showMessage: false); - } - - /// Show a 'press any key to exit' message, and exit when they press a key. - /// Whether to print a 'press any key to exit' message to the console. - private void PressAnyKeyToExit(bool showMessage) - { - if (showMessage) - Console.WriteLine("Game has ended. Press any key to exit."); - Thread.Sleep(100); - Console.ReadKey(); - Environment.Exit(0); - } - - /// Get a monitor instance derived from SMAPI's current settings. - /// The name of the module which will log messages with this instance. - private Monitor GetSecondaryMonitor(string name) - { - return new Monitor(name, this.ConsoleManager, this.LogFile, this.Settings.ConsoleColors, this.Settings.VerboseLogging) - { - WriteToConsole = this.Monitor.WriteToConsole, - ShowTraceInConsole = this.Settings.DeveloperMode, - ShowFullStampInConsole = this.Settings.DeveloperMode - }; - } - /// Get the absolute path to the next available log file. private string GetLogPath() { @@ -1474,36 +1045,5 @@ namespace StardewModdingAPI.Framework } } } - - /// A console log pattern to replace with a different message. - private class ReplaceLogPattern - { - /********* - ** Accessors - *********/ - /// The regex pattern matching the portion of the message to replace. - public Regex Search { get; } - - /// The replacement string. - public string Replacement { get; } - - /// The log level for the new message. - public LogLevel LogLevel { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The regex pattern matching the portion of the message to replace. - /// The replacement string. - /// The log level for the new message. - public ReplaceLogPattern(Regex search, string replacement, LogLevel logLevel) - { - this.Search = search; - this.Replacement = replacement; - this.LogLevel = logLevel; - } - } } } -- cgit From 94b8262692d2452e77d57fa22046dded231cdb0a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 24 Aug 2020 20:11:56 -0400 Subject: add heuristic field-to-property rewriter --- docs/release-notes.md | 3 +- .../ModLoading/Framework/BaseInstructionHandler.cs | 2 +- .../Rewriters/FieldToPropertyRewriter.cs | 63 ++++++++++++---------- src/SMAPI/Metadata/InstructionMetadata.cs | 3 ++ 4 files changed, 42 insertions(+), 29 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/docs/release-notes.md b/docs/release-notes.md index a65db68c..3f7d5198 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,12 +9,13 @@ ## Upcoming release * For players: - * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent and unpredictable errors when enabled. + * Added heuristic compatibility rewrites for some common cases. That fixes some mods which previously broke on Android or in newer game versions. * Tweaked the rules for showing update alerts (see _for SMAPI developers_ below for details). * Fixed crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute (thanks to spacechase0!). * Fixed broken URL in update alerts for unofficial versions. * Fixed rare error when a mod adds/removes event handlers asynchronously. * Fixed rare issue where the console showed incorrect colors when mods wrote to it asynchronously. + * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent and unpredictable errors when enabled. * For modders: * You can now read/write `SDate` values to JSON (e.g. for `config.json`, network mod messages, etc). diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs index fde37d68..611b2cf2 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs @@ -50,7 +50,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework ** Protected methods *********/ /// Construct an instance. - /// A brief noun phrase indicating what the handler matches. + /// A brief noun phrase indicating what the handler matches, used if is empty. protected BaseInstructionHandler(string defaultPhrase) { this.DefaultPhrase = defaultPhrase; diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs index c3b5854e..514691cf 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs @@ -1,47 +1,33 @@ using System; +using System.Collections.Generic; +using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Rewriters { - /// Rewrites field references into property references. + /// Rewrites references to fields which no longer exist, but which have an equivalent property with the exact same name. internal class FieldToPropertyRewriter : BaseInstructionHandler { /********* ** Fields *********/ - /// The type containing the field to which references should be rewritten. - private readonly Type Type; - - /// The field name to which references should be rewritten. - private readonly string FromFieldName; - - /// The new property name. - private readonly string ToPropertyName; + /// The assembly names to which to rewrite broken references. + private readonly HashSet RewriteReferencesToAssemblies; /********* ** Public methods *********/ /// Construct an instance. - /// The type whose field to which references should be rewritten. - /// The field name to rewrite. - /// The property name (if different). - public FieldToPropertyRewriter(Type type, string fieldName, string propertyName) - : base(defaultPhrase: $"{type.FullName}.{fieldName} field") + /// The assembly names to which to rewrite broken references. + public FieldToPropertyRewriter(string[] rewriteReferencesToAssemblies) + : base(defaultPhrase: "field changed to property") // ignored since we specify phrases { - this.Type = type; - this.FromFieldName = fieldName; - this.ToPropertyName = propertyName; + this.RewriteReferencesToAssemblies = new HashSet(rewriteReferencesToAssemblies); } - /// Construct an instance. - /// The type whose field to which references should be rewritten. - /// The field name to rewrite. - public FieldToPropertyRewriter(Type type, string fieldName) - : this(type, fieldName, fieldName) { } - /// Rewrite a CIL instruction reference if needed. /// The assembly module containing the instruction. /// The CIL processor. @@ -52,14 +38,37 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters { // get field ref FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); - if (!RewriteHelper.IsFieldReferenceTo(fieldRef, this.Type.FullName, this.FromFieldName)) + if (fieldRef == null || !this.ShouldValidate(fieldRef.DeclaringType)) + return false; + + // skip if not broken + if (fieldRef.Resolve() != null) + return false; + + // get equivalent property + PropertyDefinition property = fieldRef.DeclaringType.Resolve().Properties.FirstOrDefault(p => p.Name == fieldRef.Name); + MethodDefinition method = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld + ? property?.GetMethod + : property?.SetMethod; + if (method == null) return false; - // replace with property - string methodPrefix = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld ? "get" : "set"; - MethodReference propertyRef = module.ImportReference(this.Type.GetMethod($"{methodPrefix}_{this.ToPropertyName}")); + // rewrite field to property + MethodReference propertyRef = module.ImportReference(method); replaceWith(cil.Create(OpCodes.Call, propertyRef)); + this.Phrases.Add($"{fieldRef.DeclaringType.Name}.{fieldRef.Name} (field => property)"); return this.MarkRewritten(); } + + + /********* + ** Private methods + *********/ + /// Whether references to the given type should be validated. + /// The type reference. + private bool ShouldValidate(TypeReference type) + { + return type != null && this.RewriteReferencesToAssemblies.Contains(type.Scope.Name); + } } } diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 79d7a7a8..fca809f8 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -31,6 +31,9 @@ namespace StardewModdingAPI.Metadata /**** ** rewrite CIL to fix incompatible code ****/ + // generic rewrites + yield return new FieldToPropertyRewriter(this.ValidateReferencesToAssemblies); + // rewrite for crossplatform compatibility if (platformChanged) yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchFacade)); -- cgit From 1bd67baae116b0307b351222b056a0615107eb3c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 24 Aug 2020 21:39:50 -0400 Subject: support mapping fields to a different type in FieldReplaceRewriter --- docs/release-notes.md | 4 ++++ .../ModLoading/Rewriters/FieldReplaceRewriter.cs | 22 ++++++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/docs/release-notes.md b/docs/release-notes.md index 3f7d5198..efab21d5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -25,6 +25,10 @@ * For SMAPI developers: * The web API now returns an update alert in two new cases: any newer unofficial update (previously only shown if the mod was incompatible), and a newer prerelease version if the installed non-prerelease version is broken (previously only shown if the installed version was prerelease). + * Internal refactoring to simplify game updates: + * Reorganised SMAPI core to reduce coupling to `Game1` and make it easier to navigate. + * `FieldToPropertyRewriter` now auto-rewrites broken field references into properties if possible, so we no longer need to map fields manually. + * `FieldReplaceRewriter` now supports mapping to a different target type. * Internal refactoring to simplify future game updates. ## 3.6.2 diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs index 8043b13a..9166ab86 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs @@ -26,17 +26,27 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters ** Public methods *********/ /// Construct an instance. - /// The type whose field to rewrite. + /// The type whose field to rewrite. /// The field name to rewrite. + /// The new type which will have the field. /// The new field name to reference. - public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName) - : base(defaultPhrase: $"{type.FullName}.{fromFieldName} field") + public FieldReplaceRewriter(Type fromType, string fromFieldName, Type toType, string toFieldName) + : base(defaultPhrase: $"{fromType.FullName}.{fromFieldName} field") { - this.Type = type; + this.Type = fromType; this.FromFieldName = fromFieldName; - this.ToField = type.GetField(toFieldName); + this.ToField = toType.GetField(toFieldName); if (this.ToField == null) - throw new InvalidOperationException($"The {type.FullName} class doesn't have a {toFieldName} field."); + throw new InvalidOperationException($"The {toType.FullName} class doesn't have a {toFieldName} field."); + } + + /// Construct an instance. + /// The type whose field to rewrite. + /// The field name to rewrite. + /// The new field name to reference. + public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName) + : this(type, fromFieldName, type, toFieldName) + { } /// Rewrite a CIL instruction reference if needed. -- cgit From 3a890408760d0d38a418d9830374262043e2ba13 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 24 Aug 2020 22:16:48 -0400 Subject: add rewriter for method references with missing optional parameters --- docs/release-notes.md | 5 +- .../MethodWithMissingOptionalParameterRewriter.cs | 113 +++++++++++++++++++++ src/SMAPI/Metadata/InstructionMetadata.cs | 7 +- 3 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/docs/release-notes.md b/docs/release-notes.md index efab21d5..7e928aed 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,7 +9,7 @@ ## Upcoming release * For players: - * Added heuristic compatibility rewrites for some common cases. That fixes some mods which previously broke on Android or in newer game versions. + * Added heuristic compatibility rewrites, which fix some mods previously incompatible with Android or newer game versions. * Tweaked the rules for showing update alerts (see _for SMAPI developers_ below for details). * Fixed crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute (thanks to spacechase0!). * Fixed broken URL in update alerts for unofficial versions. @@ -27,7 +27,8 @@ * The web API now returns an update alert in two new cases: any newer unofficial update (previously only shown if the mod was incompatible), and a newer prerelease version if the installed non-prerelease version is broken (previously only shown if the installed version was prerelease). * Internal refactoring to simplify game updates: * Reorganised SMAPI core to reduce coupling to `Game1` and make it easier to navigate. - * `FieldToPropertyRewriter` now auto-rewrites broken field references into properties if possible, so we no longer need to map fields manually. + * Added rewriter for any method broken due to new optional parameters. + * Added rewriter for any field which was replaced by a property. * `FieldReplaceRewriter` now supports mapping to a different target type. * Internal refactoring to simplify future game updates. diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs new file mode 100644 index 00000000..9db3c3fd --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Rewrites references to methods which only broke because the definition has new optional parameters. + internal class MethodWithMissingOptionalParameterRewriter : BaseInstructionHandler + { + /********* + ** Fields + *********/ + /// The assembly names to which to rewrite broken references. + private readonly HashSet RewriteReferencesToAssemblies; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The assembly names to which to rewrite broken references. + public MethodWithMissingOptionalParameterRewriter(string[] rewriteReferencesToAssemblies) + : base(defaultPhrase: "methods with missing parameters") // ignored since we specify phrases + { + this.RewriteReferencesToAssemblies = new HashSet(rewriteReferencesToAssemblies); + } + + /// Rewrite a CIL instruction reference if needed. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The CIL instruction to handle. + /// Replaces the CIL instruction with a new one. + /// Returns whether the instruction was changed. + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) + { + // get method ref + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + if (methodRef == null || !this.ShouldValidate(methodRef.DeclaringType)) + return false; + + // skip if not broken + if (methodRef.Resolve() != null) + return false; + + // get type + var type = methodRef.DeclaringType.Resolve(); + if (type == null) + return false; + + // get method definition + MethodDefinition method = null; + foreach (var match in type.Methods.Where(p => p.Name == methodRef.Name)) + { + // reference matches initial parameters of definition + if (methodRef.Parameters.Count >= match.Parameters.Count || !this.InitialParametersMatch(methodRef, match)) + continue; + + // all remaining parameters in definition are optional + if (!match.Parameters.Skip(methodRef.Parameters.Count).All(p => p.IsOptional)) + continue; + + method = match; + break; + } + if (method == null) + return false; + + // add extra parameters + foreach (ParameterDefinition parameter in method.Parameters.Skip(methodRef.Parameters.Count)) + { + methodRef.Parameters.Add(new ParameterDefinition( + name: parameter.Name, + attributes: parameter.Attributes, + parameterType: module.ImportReference(parameter.ParameterType) + )); + } + + this.Phrases.Add($"{methodRef.DeclaringType.Name}.{methodRef.Name} (added missing optional parameters)"); + return this.MarkRewritten(); + } + + + /********* + ** Private methods + *********/ + /// Whether references to the given type should be validated. + /// The type reference. + private bool ShouldValidate(TypeReference type) + { + return type != null && this.RewriteReferencesToAssemblies.Contains(type.Scope.Name); + } + + /// Get whether every parameter in the method reference matches the exact order and type of the parameters in the method definition. This ignores extra parameters in the definition. + /// The method reference whose parameters to check. + /// The method definition whose parameters to check against. + private bool InitialParametersMatch(MethodReference methodRef, MethodDefinition method) + { + if (methodRef.Parameters.Count > method.Parameters.Count) + return false; + + for (int i = 0; i < methodRef.Parameters.Count; i++) + { + if (!RewriteHelper.IsSameType(methodRef.Parameters[i].ParameterType, method.Parameters[i].ParameterType)) + return false; + } + + return true; + } + } +} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index fca809f8..972ed91d 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -31,9 +31,6 @@ namespace StardewModdingAPI.Metadata /**** ** rewrite CIL to fix incompatible code ****/ - // generic rewrites - yield return new FieldToPropertyRewriter(this.ValidateReferencesToAssemblies); - // rewrite for crossplatform compatibility if (platformChanged) yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchFacade)); @@ -41,6 +38,10 @@ namespace StardewModdingAPI.Metadata // rewrite for Stardew Valley 1.3 yield return new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize); + // generic rewrites + yield return new FieldToPropertyRewriter(this.ValidateReferencesToAssemblies); + yield return new MethodWithMissingOptionalParameterRewriter(this.ValidateReferencesToAssemblies); + #if HARMONY_2 // rewrite for SMAPI 3.6 (Harmony 1.x => 2.0 update) yield return new Harmony1AssemblyRewriter(); -- cgit From b9a9fe36bbaa1357b98a117400a62fecc0fc56cb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 25 Aug 2020 22:05:27 -0400 Subject: fix missing-parameter rewriter not loading default values onto stack --- .../MethodWithMissingOptionalParameterRewriter.cs | 40 +++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs index 9db3c3fd..75182890 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs @@ -68,14 +68,28 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters if (method == null) return false; - // add extra parameters - foreach (ParameterDefinition parameter in method.Parameters.Skip(methodRef.Parameters.Count)) + // get instructions to inject + var injectables = method.Parameters.Skip(methodRef.Parameters.Count) + .Select(p => new { Parameter = p, LoadValueInstruction = this.GetLoadValueInstruction(p.Constant) }) + .ToArray(); + if (injectables.Any(p => p.LoadValueInstruction == null)) + return false; // SMAPI needs to load the value onto the stack before the method call, but the optional parameter type wasn't recognized + + // inject new parameters + foreach (var entry in injectables) { - methodRef.Parameters.Add(new ParameterDefinition( + // load value onto stack + cil.InsertBefore(instruction, entry.LoadValueInstruction); + + // add parameter + ParameterDefinition parameter = entry.Parameter; + var newParameter = new ParameterDefinition( name: parameter.Name, attributes: parameter.Attributes, parameterType: module.ImportReference(parameter.ParameterType) - )); + ); + newParameter.Constant = parameter.Constant; + methodRef.Parameters.Add(newParameter); } this.Phrases.Add($"{methodRef.DeclaringType.Name}.{methodRef.Name} (added missing optional parameters)"); @@ -109,5 +123,23 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return true; } + + /// Get the CIL instruction to load a value onto the stack. + /// The constant value to inject. + /// Returns the instruction, or null if the value type isn't supported. + private Instruction GetLoadValueInstruction(object rawValue) + { + return rawValue switch + { + null => Instruction.Create(OpCodes.Ldnull), + bool value => Instruction.Create(value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0), + int value => Instruction.Create(OpCodes.Ldc_I4, value), // int32 + long value => Instruction.Create(OpCodes.Ldc_I8, value), // int64 + float value => Instruction.Create(OpCodes.Ldc_R4, value), // float32 + double value => Instruction.Create(OpCodes.Ldc_R8, value), // float64 + string value => Instruction.Create(OpCodes.Ldstr, value), + _ => null + }; + } } } -- cgit From abfe40bf691e4d384d444bf6f001de8d959b12bb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 25 Aug 2020 22:49:00 -0400 Subject: fix some method references only partially rewritten Thanks to Bepis on Discord for helping find the issue! --- .../MethodWithMissingOptionalParameterRewriter.cs | 28 +++++++--------------- 1 file changed, 8 insertions(+), 20 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs index 75182890..87ccf941 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs @@ -68,29 +68,17 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters if (method == null) return false; - // get instructions to inject - var injectables = method.Parameters.Skip(methodRef.Parameters.Count) - .Select(p => new { Parameter = p, LoadValueInstruction = this.GetLoadValueInstruction(p.Constant) }) + // get instructions to inject parameter values + var loadInstructions = method.Parameters.Skip(methodRef.Parameters.Count) + .Select(p => this.GetLoadValueInstruction(p.Constant)) .ToArray(); - if (injectables.Any(p => p.LoadValueInstruction == null)) + if (loadInstructions.Any(p => p == null)) return false; // SMAPI needs to load the value onto the stack before the method call, but the optional parameter type wasn't recognized - // inject new parameters - foreach (var entry in injectables) - { - // load value onto stack - cil.InsertBefore(instruction, entry.LoadValueInstruction); - - // add parameter - ParameterDefinition parameter = entry.Parameter; - var newParameter = new ParameterDefinition( - name: parameter.Name, - attributes: parameter.Attributes, - parameterType: module.ImportReference(parameter.ParameterType) - ); - newParameter.Constant = parameter.Constant; - methodRef.Parameters.Add(newParameter); - } + // rewrite method reference + foreach (Instruction loadInstruction in loadInstructions) + cil.InsertBefore(instruction, loadInstruction); + instruction.Operand = module.ImportReference(method); this.Phrases.Add($"{methodRef.DeclaringType.Name}.{methodRef.Name} (added missing optional parameters)"); return this.MarkRewritten(); -- cgit From ec4b81819aeaaeba5f5f3edf28e72b0e6f0430c4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 25 Aug 2020 22:59:08 -0400 Subject: use inheritdoc in rewriters --- src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs | 7 +------ src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs | 7 +------ .../Framework/ModLoading/Finders/MethodFinder.cs | 7 +------ .../Framework/ModLoading/Finders/PropertyFinder.cs | 7 +------ .../ReferenceToMemberWithUnexpectedTypeFinder.cs | 7 +------ .../Finders/ReferenceToMissingMemberFinder.cs | 7 +------ .../ModLoading/Finders/TypeAssemblyFinder.cs | 6 +----- src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs | 6 +----- .../ModLoading/Framework/BaseInstructionHandler.cs | 19 +++++-------------- .../ModLoading/Rewriters/FieldReplaceRewriter.cs | 7 +------ .../ModLoading/Rewriters/FieldToPropertyRewriter.cs | 7 +------ .../ModLoading/Rewriters/Harmony1AssemblyRewriter.cs | 13 ++----------- .../ModLoading/Rewriters/MethodParentRewriter.cs | 7 +------ .../MethodWithMissingOptionalParameterRewriter.cs | 7 +------ .../Rewriters/StaticFieldToConstantRewriter.cs | 7 +------ .../ModLoading/Rewriters/TypeReferenceRewriter.cs | 6 +----- 16 files changed, 21 insertions(+), 106 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs index e1476b73..3d23da87 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs @@ -36,12 +36,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders this.Result = result; } - /// Rewrite a CIL instruction reference if needed. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The CIL instruction to handle. - /// Replaces the CIL instruction with a new one. - /// Returns whether the instruction was changed. + /// public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { if (!this.Flags.Contains(this.Result) && this.IsMatch(instruction)) diff --git a/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs index c157ed9b..b4063078 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs @@ -36,12 +36,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders this.Result = result; } - /// Rewrite a CIL instruction reference if needed. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The CIL instruction to handle. - /// Replaces the CIL instruction with a new one. - /// Returns whether the instruction was changed. + /// public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { if (!this.Flags.Contains(this.Result) && RewriteHelper.IsFieldReferenceTo(instruction, this.FullTypeName, this.FieldName)) diff --git a/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs index 82c93a7c..187bdefc 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs @@ -36,12 +36,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders this.Result = result; } - /// Rewrite a CIL instruction reference if needed. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The CIL instruction to handle. - /// Replaces the CIL instruction with a new one. - /// Returns whether the instruction was changed. + /// public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { if (!this.Flags.Contains(this.Result) && this.IsMatch(instruction)) diff --git a/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs index c96d61a2..d14058dc 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs @@ -36,12 +36,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders this.Result = result; } - /// Rewrite a CIL instruction reference if needed. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The CIL instruction to handle. - /// Replaces the CIL instruction with a new one. - /// Returns whether the instruction was changed. + /// public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { if (!this.Flags.Contains(this.Result) && this.IsMatch(instruction)) diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs index a67cfa4f..2402bf48 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -29,12 +29,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders this.ValidateReferencesToAssemblies = new HashSet(validateReferencesToAssemblies); } - /// Rewrite a CIL instruction reference if needed. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The CIL instruction to handle. - /// Replaces the CIL instruction with a new one. - /// Returns whether the instruction was changed. + /// public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { // field reference diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs index ebb62948..f97783da 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs @@ -29,12 +29,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders this.ValidateReferencesToAssemblies = new HashSet(validateReferencesToAssemblies); } - /// Rewrite a CIL instruction reference if needed. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The CIL instruction to handle. - /// Replaces the CIL instruction with a new one. - /// Returns whether the instruction was changed. + /// public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { // field reference diff --git a/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs index a1ade536..24ab2eca 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs @@ -35,11 +35,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders this.ShouldIgnore = shouldIgnore; } - /// Rewrite a type reference if needed. - /// The assembly module containing the instruction. - /// The type definition to handle. - /// Replaces the type reference with a new one. - /// Returns whether the type was changed. + /// public override bool Handle(ModuleDefinition module, TypeReference type, Action replaceWith) { if (type.Scope.Name == this.AssemblyName && this.ShouldIgnore?.Invoke(type) != true) diff --git a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs index c285414a..bbd081e8 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs @@ -35,11 +35,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders this.ShouldIgnore = shouldIgnore; } - /// Rewrite a type reference if needed. - /// The assembly module containing the instruction. - /// The type definition to handle. - /// Replaces the type reference with a new one. - /// Returns whether the type was changed. + /// public override bool Handle(ModuleDefinition module, TypeReference type, Action replaceWith) { if (type.FullName == this.FullTypeName && this.ShouldIgnore?.Invoke(type) != true) diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs index 611b2cf2..ef619761 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs @@ -11,35 +11,26 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /********* ** Accessors *********/ - /// A brief noun phrase indicating what the handler matches, used if is empty. + /// public string DefaultPhrase { get; } - /// The rewrite flags raised for the current module. + /// public ISet Flags { get; } = new HashSet(); - /// The brief noun phrases indicating what the handler matched for the current module. + /// public ISet Phrases { get; } = new HashSet(StringComparer.OrdinalIgnoreCase); /********* ** Public methods *********/ - /// Rewrite a type reference if needed. - /// The assembly module containing the instruction. - /// The type definition to handle. - /// Replaces the type reference with a new one. - /// Returns whether the type was changed. + /// public virtual bool Handle(ModuleDefinition module, TypeReference type, Action replaceWith) { return false; } - /// Rewrite a CIL instruction reference if needed. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The CIL instruction to handle. - /// Replaces the CIL instruction with a new one. - /// Returns whether the instruction was changed. + /// public virtual bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { return false; diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs index 9166ab86..2b8fbe06 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs @@ -49,12 +49,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters { } - /// Rewrite a CIL instruction reference if needed. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The CIL instruction to handle. - /// Replaces the CIL instruction with a new one. - /// Returns whether the instruction was changed. + /// public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { // get field reference diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs index 514691cf..de123cc2 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs @@ -28,12 +28,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters this.RewriteReferencesToAssemblies = new HashSet(rewriteReferencesToAssemblies); } - /// Rewrite a CIL instruction reference if needed. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The CIL instruction to handle. - /// Replaces the CIL instruction with a new one. - /// Returns whether the instruction was changed. + /// public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { // get field ref diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs index b30d686e..4b3675bc 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs @@ -25,11 +25,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters public Harmony1AssemblyRewriter() : base(defaultPhrase: "Harmony 1.x") { } - /// Rewrite a type reference if needed. - /// The assembly module containing the instruction. - /// The type definition to handle. - /// Replaces the type reference with a new one. - /// Returns whether the type was changed. + /// public override bool Handle(ModuleDefinition module, TypeReference type, Action replaceWith) { // rewrite Harmony 1.x type to Harmony 2.0 type @@ -45,12 +41,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return false; } - /// Rewrite a CIL instruction reference if needed. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The CIL instruction to handle. - /// Replaces the CIL instruction with a new one. - /// Returns whether the instruction was changed. + /// public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { // rewrite Harmony 1.x methods to Harmony 2.0 diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs index b8e53f40..dc04478f 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -40,12 +40,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters public MethodParentRewriter(Type fromType, Type toType, string nounPhrase = null) : this(fromType.FullName, toType, nounPhrase) { } - /// Rewrite a CIL instruction reference if needed. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The CIL instruction to handle. - /// Replaces the CIL instruction with a new one. - /// Returns whether the instruction was changed. + /// public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { // get method ref diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs index 87ccf941..e6e7a847 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs @@ -28,12 +28,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters this.RewriteReferencesToAssemblies = new HashSet(rewriteReferencesToAssemblies); } - /// Rewrite a CIL instruction reference if needed. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The CIL instruction to handle. - /// Replaces the CIL instruction with a new one. - /// Returns whether the instruction was changed. + /// public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { // get method ref diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs index 6ef18b26..6ea59d1c 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs @@ -37,12 +37,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters this.Value = value; } - /// Rewrite a CIL instruction reference if needed. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The CIL instruction to handle. - /// Replaces the CIL instruction with a new one. - /// Returns whether the instruction was changed. + /// public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { // get field reference diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs index c2120444..ad5cb96f 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -35,11 +35,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters this.ShouldIgnore = shouldIgnore; } - /// Rewrite a type reference if needed. - /// The assembly module containing the instruction. - /// The type definition to handle. - /// Replaces the type reference with a new one. - /// Returns whether the type was changed. + /// public override bool Handle(ModuleDefinition module, TypeReference type, Action replaceWith) { // check type reference -- cgit From fd6835555ccdfecc09d87aab29b9c5cec3820720 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 26 Aug 2020 21:55:04 -0400 Subject: fix InvalidProgramException when replacing CIL instructions in some cases --- .../ModLoading/Rewriters/FieldReplaceRewriter.cs | 4 +-- .../Rewriters/FieldToPropertyRewriter.cs | 5 ++-- .../Rewriters/StaticFieldToConstantRewriter.cs | 30 ++++++++++------------ 3 files changed, 18 insertions(+), 21 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs index 2b8fbe06..c251a30c 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs @@ -58,8 +58,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return false; // replace with new field - FieldReference newRef = module.ImportReference(this.ToField); - replaceWith(cil.Create(instruction.OpCode, newRef)); + instruction.Operand = module.ImportReference(this.ToField); + return this.MarkRewritten(); } } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs index de123cc2..0a99bde0 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs @@ -49,8 +49,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return false; // rewrite field to property - MethodReference propertyRef = module.ImportReference(method); - replaceWith(cil.Create(OpCodes.Call, propertyRef)); + instruction.OpCode = OpCodes.Call; + instruction.Operand = module.ImportReference(method); + this.Phrases.Add($"{fieldRef.DeclaringType.Name}.{fieldRef.Name} (field => property)"); return this.MarkRewritten(); } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs index 6ea59d1c..2f1122b4 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs @@ -46,24 +46,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return false; // rewrite to constant - replaceWith(this.CreateConstantInstruction(cil, this.Value)); - return this.MarkRewritten(); - } - - - /********* - ** Private methods - *********/ - /// Create a CIL constant value instruction. - /// The CIL processor. - /// The constant value to set. - private Instruction CreateConstantInstruction(ILProcessor cil, object value) - { if (typeof(TValue) == typeof(int)) - return cil.Create(OpCodes.Ldc_I4, (int)value); - if (typeof(TValue) == typeof(string)) - return cil.Create(OpCodes.Ldstr, (string)value); - throw new NotSupportedException($"Rewriting to constant values of type {typeof(TValue)} isn't currently supported."); + { + instruction.OpCode = OpCodes.Ldc_I4; + instruction.Operand = this.Value; + } + else if (typeof(TValue) == typeof(string)) + { + instruction.OpCode = OpCodes.Ldstr; + instruction.Operand = this.Value; + } + else + throw new NotSupportedException($"Rewriting to constant values of type {typeof(TValue)} isn't currently supported."); + + return this.MarkRewritten(); } } } -- cgit From 16161a214fddea19b908d7ca3dc0d39f81c259c8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 26 Aug 2020 21:55:08 -0400 Subject: remove now-unused instruction replace callback --- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 4 ++-- src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs | 3 +-- src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs | 3 +-- src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs | 3 +-- src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs | 3 +-- .../Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs | 3 +-- .../ModLoading/Finders/ReferenceToMissingMemberFinder.cs | 3 +-- .../Framework/ModLoading/Framework/BaseInstructionHandler.cs | 2 +- src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs | 10 ++-------- src/SMAPI/Framework/ModLoading/IInstructionHandler.cs | 3 +-- .../Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs | 2 +- .../Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs | 3 +-- .../Framework/ModLoading/Rewriters/MethodParentRewriter.cs | 2 +- .../Rewriters/MethodWithMissingOptionalParameterRewriter.cs | 3 +-- .../ModLoading/Rewriters/StaticFieldToConstantRewriter.cs | 2 +- 15 files changed, 17 insertions(+), 32 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index c8c1ca08..cfe4c747 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -318,11 +318,11 @@ namespace StardewModdingAPI.Framework.ModLoading rewritten |= handler.Handle(module, type, replaceWith); return rewritten; }, - rewriteInstruction: (ref Instruction instruction, ILProcessor cil, Action replaceWith) => + rewriteInstruction: (ref Instruction instruction, ILProcessor cil) => { bool rewritten = false; foreach (IInstructionHandler handler in handlers) - rewritten |= handler.Handle(module, cil, instruction, replaceWith); + rewritten |= handler.Handle(module, cil, instruction); return rewritten; } ); diff --git a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs index 3d23da87..01ed153b 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs @@ -1,4 +1,3 @@ -using System; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -37,7 +36,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// - public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { if (!this.Flags.Contains(this.Result) && this.IsMatch(instruction)) this.MarkFlag(this.Result); diff --git a/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs index b4063078..2c062243 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs @@ -1,4 +1,3 @@ -using System; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -37,7 +36,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// - public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { if (!this.Flags.Contains(this.Result) && RewriteHelper.IsFieldReferenceTo(instruction, this.FullTypeName, this.FieldName)) this.MarkFlag(this.Result); diff --git a/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs index 187bdefc..d2340f01 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs @@ -1,4 +1,3 @@ -using System; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -37,7 +36,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// - public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { if (!this.Flags.Contains(this.Result) && this.IsMatch(instruction)) this.MarkFlag(this.Result); diff --git a/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs index d14058dc..99344848 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs @@ -1,4 +1,3 @@ -using System; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -37,7 +36,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// - public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { if (!this.Flags.Contains(this.Result) && this.IsMatch(instruction)) this.MarkFlag(this.Result); diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs index 2402bf48..b01a3240 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using Mono.Cecil; @@ -30,7 +29,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// - public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { // field reference FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs index f97783da..9afd1de0 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using Mono.Cecil; @@ -30,7 +29,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// - public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { // field reference FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs index ef619761..624113b3 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs @@ -31,7 +31,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework } /// - public virtual bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) + public virtual bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { return false; } diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index fb651465..ea29550a 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -22,9 +22,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// Rewrite a CIL instruction in the assembly code. /// The current CIL instruction. /// The CIL instruction processor. - /// Replaces the CIL instruction with the given instruction. /// Returns whether the instruction was changed. - public delegate bool RewriteInstructionDelegate(ref Instruction instruction, ILProcessor cil, Action replaceWith); + public delegate bool RewriteInstructionDelegate(ref Instruction instruction, ILProcessor cil); /********* @@ -161,12 +160,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework // instruction itself // (should be done after the above type rewrites to ensure valid types) - rewritten |= this.RewriteInstructionImpl(ref instruction, cil, newInstruction => - { - rewritten = true; - cil.Replace(instruction, newInstruction); - instruction = newInstruction; - }); + rewritten |= this.RewriteInstructionImpl(ref instruction, cil); return rewritten; } diff --git a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs index e6de6785..17c9ba68 100644 --- a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs @@ -35,8 +35,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// The assembly module containing the instruction. /// The CIL processor. /// The CIL instruction to handle. - /// Replaces the CIL instruction with a new one. /// Returns whether the instruction was changed. - bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith); + bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction); } } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs index c251a30c..0b679e9d 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs @@ -50,7 +50,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters } /// - public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { // get field reference FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs index 0a99bde0..aaf04b79 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using Mono.Cecil; @@ -29,7 +28,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters } /// - public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { // get field ref FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs index dc04478f..9933e2ca 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -41,7 +41,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters : this(fromType.FullName, toType, nounPhrase) { } /// - public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { // get method ref MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs index e6e7a847..89c8ede7 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using Mono.Cecil; @@ -29,7 +28,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters } /// - public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { // get method ref MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs index 2f1122b4..f34d4943 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters } /// - public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { // get field reference FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); -- cgit From d3c5fe0764806684cc71508abf009473b9d7bc0a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 26 Aug 2020 22:14:25 -0400 Subject: rename new heuristic rewriters for clarity --- .../Rewriters/FieldToPropertyRewriter.cs | 69 ----------- .../ModLoading/Rewriters/HeuristicFieldRewriter.cs | 69 +++++++++++ .../Rewriters/HeuristicMethodRewriter.cs | 127 +++++++++++++++++++++ .../MethodWithMissingOptionalParameterRewriter.cs | 127 --------------------- src/SMAPI/Metadata/InstructionMetadata.cs | 6 +- 5 files changed, 199 insertions(+), 199 deletions(-) delete mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs delete mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs deleted file mode 100644 index aaf04b79..00000000 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Mono.Cecil; -using Mono.Cecil.Cil; -using StardewModdingAPI.Framework.ModLoading.Framework; - -namespace StardewModdingAPI.Framework.ModLoading.Rewriters -{ - /// Rewrites references to fields which no longer exist, but which have an equivalent property with the exact same name. - internal class FieldToPropertyRewriter : BaseInstructionHandler - { - /********* - ** Fields - *********/ - /// The assembly names to which to rewrite broken references. - private readonly HashSet RewriteReferencesToAssemblies; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The assembly names to which to rewrite broken references. - public FieldToPropertyRewriter(string[] rewriteReferencesToAssemblies) - : base(defaultPhrase: "field changed to property") // ignored since we specify phrases - { - this.RewriteReferencesToAssemblies = new HashSet(rewriteReferencesToAssemblies); - } - - /// - public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) - { - // get field ref - FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); - if (fieldRef == null || !this.ShouldValidate(fieldRef.DeclaringType)) - return false; - - // skip if not broken - if (fieldRef.Resolve() != null) - return false; - - // get equivalent property - PropertyDefinition property = fieldRef.DeclaringType.Resolve().Properties.FirstOrDefault(p => p.Name == fieldRef.Name); - MethodDefinition method = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld - ? property?.GetMethod - : property?.SetMethod; - if (method == null) - return false; - - // rewrite field to property - instruction.OpCode = OpCodes.Call; - instruction.Operand = module.ImportReference(method); - - this.Phrases.Add($"{fieldRef.DeclaringType.Name}.{fieldRef.Name} (field => property)"); - return this.MarkRewritten(); - } - - - /********* - ** Private methods - *********/ - /// Whether references to the given type should be validated. - /// The type reference. - private bool ShouldValidate(TypeReference type) - { - return type != null && this.RewriteReferencesToAssemblies.Contains(type.Scope.Name); - } - } -} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs new file mode 100644 index 00000000..5a088ed8 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Automatically fix references to fields that have been replaced by a property. + internal class HeuristicFieldRewriter : BaseInstructionHandler + { + /********* + ** Fields + *********/ + /// The assembly names to which to rewrite broken references. + private readonly HashSet RewriteReferencesToAssemblies; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The assembly names to which to rewrite broken references. + public HeuristicFieldRewriter(string[] rewriteReferencesToAssemblies) + : base(defaultPhrase: "field changed to property") // ignored since we specify phrases + { + this.RewriteReferencesToAssemblies = new HashSet(rewriteReferencesToAssemblies); + } + + /// + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) + { + // get field ref + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + if (fieldRef == null || !this.ShouldValidate(fieldRef.DeclaringType)) + return false; + + // skip if not broken + if (fieldRef.Resolve() != null) + return false; + + // get equivalent property + PropertyDefinition property = fieldRef.DeclaringType.Resolve().Properties.FirstOrDefault(p => p.Name == fieldRef.Name); + MethodDefinition method = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld + ? property?.GetMethod + : property?.SetMethod; + if (method == null) + return false; + + // rewrite field to property + instruction.OpCode = OpCodes.Call; + instruction.Operand = module.ImportReference(method); + + this.Phrases.Add($"{fieldRef.DeclaringType.Name}.{fieldRef.Name} (field => property)"); + return this.MarkRewritten(); + } + + + /********* + ** Private methods + *********/ + /// Whether references to the given type should be validated. + /// The type reference. + private bool ShouldValidate(TypeReference type) + { + return type != null && this.RewriteReferencesToAssemblies.Contains(type.Scope.Name); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs new file mode 100644 index 00000000..21b42e12 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs @@ -0,0 +1,127 @@ +using System.Collections.Generic; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Automatically fix references to methods that had extra optional parameters added. + internal class HeuristicMethodRewriter : BaseInstructionHandler + { + /********* + ** Fields + *********/ + /// The assembly names to which to rewrite broken references. + private readonly HashSet RewriteReferencesToAssemblies; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The assembly names to which to rewrite broken references. + public HeuristicMethodRewriter(string[] rewriteReferencesToAssemblies) + : base(defaultPhrase: "methods with missing parameters") // ignored since we specify phrases + { + this.RewriteReferencesToAssemblies = new HashSet(rewriteReferencesToAssemblies); + } + + /// + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) + { + // get method ref + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + if (methodRef == null || !this.ShouldValidate(methodRef.DeclaringType)) + return false; + + // skip if not broken + if (methodRef.Resolve() != null) + return false; + + // get type + var type = methodRef.DeclaringType.Resolve(); + if (type == null) + return false; + + // get method definition + MethodDefinition method = null; + foreach (var match in type.Methods.Where(p => p.Name == methodRef.Name)) + { + // reference matches initial parameters of definition + if (methodRef.Parameters.Count >= match.Parameters.Count || !this.InitialParametersMatch(methodRef, match)) + continue; + + // all remaining parameters in definition are optional + if (!match.Parameters.Skip(methodRef.Parameters.Count).All(p => p.IsOptional)) + continue; + + method = match; + break; + } + if (method == null) + return false; + + // get instructions to inject parameter values + var loadInstructions = method.Parameters.Skip(methodRef.Parameters.Count) + .Select(p => this.GetLoadValueInstruction(p.Constant)) + .ToArray(); + if (loadInstructions.Any(p => p == null)) + return false; // SMAPI needs to load the value onto the stack before the method call, but the optional parameter type wasn't recognized + + // rewrite method reference + foreach (Instruction loadInstruction in loadInstructions) + cil.InsertBefore(instruction, loadInstruction); + instruction.Operand = module.ImportReference(method); + + this.Phrases.Add($"{methodRef.DeclaringType.Name}.{methodRef.Name} (added missing optional parameters)"); + return this.MarkRewritten(); + } + + + /********* + ** Private methods + *********/ + /// Whether references to the given type should be validated. + /// The type reference. + private bool ShouldValidate(TypeReference type) + { + return type != null && this.RewriteReferencesToAssemblies.Contains(type.Scope.Name); + } + + /// Get whether every parameter in the method reference matches the exact order and type of the parameters in the method definition. This ignores extra parameters in the definition. + /// The method reference whose parameters to check. + /// The method definition whose parameters to check against. + private bool InitialParametersMatch(MethodReference methodRef, MethodDefinition method) + { + if (methodRef.Parameters.Count > method.Parameters.Count) + return false; + + for (int i = 0; i < methodRef.Parameters.Count; i++) + { + if (!RewriteHelper.IsSameType(methodRef.Parameters[i].ParameterType, method.Parameters[i].ParameterType)) + return false; + } + + return true; + } + + /// Get the CIL instruction to load a value onto the stack. + /// The constant value to inject. + /// Returns the instruction, or null if the value type isn't supported. + private Instruction GetLoadValueInstruction(object rawValue) + { + return rawValue switch + { + null => Instruction.Create(OpCodes.Ldnull), + bool value => Instruction.Create(value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0), + int value => Instruction.Create(OpCodes.Ldc_I4, value), // int32 + long value => Instruction.Create(OpCodes.Ldc_I8, value), // int64 + float value => Instruction.Create(OpCodes.Ldc_R4, value), // float32 + double value => Instruction.Create(OpCodes.Ldc_R8, value), // float64 + string value => Instruction.Create(OpCodes.Ldstr, value), + _ => null + }; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs deleted file mode 100644 index 89c8ede7..00000000 --- a/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Mono.Cecil; -using Mono.Cecil.Cil; -using StardewModdingAPI.Framework.ModLoading.Framework; - -namespace StardewModdingAPI.Framework.ModLoading.Rewriters -{ - /// Rewrites references to methods which only broke because the definition has new optional parameters. - internal class MethodWithMissingOptionalParameterRewriter : BaseInstructionHandler - { - /********* - ** Fields - *********/ - /// The assembly names to which to rewrite broken references. - private readonly HashSet RewriteReferencesToAssemblies; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The assembly names to which to rewrite broken references. - public MethodWithMissingOptionalParameterRewriter(string[] rewriteReferencesToAssemblies) - : base(defaultPhrase: "methods with missing parameters") // ignored since we specify phrases - { - this.RewriteReferencesToAssemblies = new HashSet(rewriteReferencesToAssemblies); - } - - /// - public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) - { - // get method ref - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); - if (methodRef == null || !this.ShouldValidate(methodRef.DeclaringType)) - return false; - - // skip if not broken - if (methodRef.Resolve() != null) - return false; - - // get type - var type = methodRef.DeclaringType.Resolve(); - if (type == null) - return false; - - // get method definition - MethodDefinition method = null; - foreach (var match in type.Methods.Where(p => p.Name == methodRef.Name)) - { - // reference matches initial parameters of definition - if (methodRef.Parameters.Count >= match.Parameters.Count || !this.InitialParametersMatch(methodRef, match)) - continue; - - // all remaining parameters in definition are optional - if (!match.Parameters.Skip(methodRef.Parameters.Count).All(p => p.IsOptional)) - continue; - - method = match; - break; - } - if (method == null) - return false; - - // get instructions to inject parameter values - var loadInstructions = method.Parameters.Skip(methodRef.Parameters.Count) - .Select(p => this.GetLoadValueInstruction(p.Constant)) - .ToArray(); - if (loadInstructions.Any(p => p == null)) - return false; // SMAPI needs to load the value onto the stack before the method call, but the optional parameter type wasn't recognized - - // rewrite method reference - foreach (Instruction loadInstruction in loadInstructions) - cil.InsertBefore(instruction, loadInstruction); - instruction.Operand = module.ImportReference(method); - - this.Phrases.Add($"{methodRef.DeclaringType.Name}.{methodRef.Name} (added missing optional parameters)"); - return this.MarkRewritten(); - } - - - /********* - ** Private methods - *********/ - /// Whether references to the given type should be validated. - /// The type reference. - private bool ShouldValidate(TypeReference type) - { - return type != null && this.RewriteReferencesToAssemblies.Contains(type.Scope.Name); - } - - /// Get whether every parameter in the method reference matches the exact order and type of the parameters in the method definition. This ignores extra parameters in the definition. - /// The method reference whose parameters to check. - /// The method definition whose parameters to check against. - private bool InitialParametersMatch(MethodReference methodRef, MethodDefinition method) - { - if (methodRef.Parameters.Count > method.Parameters.Count) - return false; - - for (int i = 0; i < methodRef.Parameters.Count; i++) - { - if (!RewriteHelper.IsSameType(methodRef.Parameters[i].ParameterType, method.Parameters[i].ParameterType)) - return false; - } - - return true; - } - - /// Get the CIL instruction to load a value onto the stack. - /// The constant value to inject. - /// Returns the instruction, or null if the value type isn't supported. - private Instruction GetLoadValueInstruction(object rawValue) - { - return rawValue switch - { - null => Instruction.Create(OpCodes.Ldnull), - bool value => Instruction.Create(value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0), - int value => Instruction.Create(OpCodes.Ldc_I4, value), // int32 - long value => Instruction.Create(OpCodes.Ldc_I8, value), // int64 - float value => Instruction.Create(OpCodes.Ldc_R4, value), // float32 - double value => Instruction.Create(OpCodes.Ldc_R8, value), // float64 - string value => Instruction.Create(OpCodes.Ldstr, value), - _ => null - }; - } - } -} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 972ed91d..86e16e1e 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -38,9 +38,9 @@ namespace StardewModdingAPI.Metadata // rewrite for Stardew Valley 1.3 yield return new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize); - // generic rewrites - yield return new FieldToPropertyRewriter(this.ValidateReferencesToAssemblies); - yield return new MethodWithMissingOptionalParameterRewriter(this.ValidateReferencesToAssemblies); + // heuristic rewrites + yield return new HeuristicFieldRewriter(this.ValidateReferencesToAssemblies); + yield return new HeuristicMethodRewriter(this.ValidateReferencesToAssemblies); #if HARMONY_2 // rewrite for SMAPI 3.6 (Harmony 1.x => 2.0 update) -- cgit From 54e7fb7a0bcd994f6d49348c879cb96902dbe07b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 26 Aug 2020 22:32:59 -0400 Subject: fix some broken field references not detected --- docs/release-notes.md | 1 + .../Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/docs/release-notes.md b/docs/release-notes.md index 7e928aed..21f9d213 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ * Fixed broken URL in update alerts for unofficial versions. * Fixed rare error when a mod adds/removes event handlers asynchronously. * Fixed rare issue where the console showed incorrect colors when mods wrote to it asynchronously. + * Fixed SMAPI not always detecting broken field references in mod code. * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent and unpredictable errors when enabled. * For modders: diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs index 9afd1de0..75575c97 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -35,8 +34,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType)) { - FieldDefinition target = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name); - if (target == null) + FieldDefinition target = fieldRef.Resolve(); + if (target == null || target.HasConstant) { this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)"); return false; -- cgit From 0bf692addc3e309a8448de9ffb2a41cb701cfddf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 26 Aug 2020 23:11:41 -0400 Subject: add heuristic rewrite for field => const changes --- docs/release-notes.md | 12 ++-- .../ModLoading/Framework/RewriteHelper.cs | 18 ++++++ .../ModLoading/Rewriters/HeuristicFieldRewriter.cs | 65 +++++++++++++++++----- .../Rewriters/HeuristicMethodRewriter.cs | 20 +------ .../Rewriters/StaticFieldToConstantRewriter.cs | 65 ---------------------- src/SMAPI/Metadata/InstructionMetadata.cs | 3 - 6 files changed, 76 insertions(+), 107 deletions(-) delete mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/docs/release-notes.md b/docs/release-notes.md index 21f9d213..554b9518 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -26,12 +26,12 @@ * For SMAPI developers: * The web API now returns an update alert in two new cases: any newer unofficial update (previously only shown if the mod was incompatible), and a newer prerelease version if the installed non-prerelease version is broken (previously only shown if the installed version was prerelease). - * Internal refactoring to simplify game updates: - * Reorganised SMAPI core to reduce coupling to `Game1` and make it easier to navigate. - * Added rewriter for any method broken due to new optional parameters. - * Added rewriter for any field which was replaced by a property. - * `FieldReplaceRewriter` now supports mapping to a different target type. - * Internal refactoring to simplify future game updates. + * Reorganised SMAPI core to reduce coupling to `Game1`, make it easier to navigate, and simplify future game updates. + * SMAPI now automatically fixes code broken by these changes in game code, so manual rewriters are no longer needed: + * reference to a method with new optional parameters; + * reference to a field replaced by a property; + * reference to a field replaced by a `const` field. + * `FieldReplaceRewriter` now supports mapping to a different target type. ## 3.6.2 Released 02 August 2020 for Stardew Valley 1.4.1 or later. diff --git a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs index 36058b86..4b88148f 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs @@ -59,6 +59,24 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework : null; } + /// Get the CIL instruction to load a value onto the stack. + /// The constant value to inject. + /// Returns the instruction, or null if the value type isn't supported. + public static Instruction GetLoadValueInstruction(object rawValue) + { + return rawValue switch + { + null => Instruction.Create(OpCodes.Ldnull), + bool value => Instruction.Create(value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0), + int value => Instruction.Create(OpCodes.Ldc_I4, value), // int32 + long value => Instruction.Create(OpCodes.Ldc_I8, value), // int64 + float value => Instruction.Create(OpCodes.Ldc_R4, value), // float32 + double value => Instruction.Create(OpCodes.Ldc_R8, value), // float64 + string value => Instruction.Create(OpCodes.Ldstr, value), + _ => null + }; + } + /// Get whether a type matches a type reference. /// The defined type. /// The type reference. diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs index 5a088ed8..ca04205c 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs @@ -6,7 +6,7 @@ using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Rewriters { - /// Automatically fix references to fields that have been replaced by a property. + /// Automatically fix references to fields that have been replaced by a property or const field. internal class HeuristicFieldRewriter : BaseInstructionHandler { /********* @@ -36,14 +36,40 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return false; // skip if not broken - if (fieldRef.Resolve() != null) + FieldDefinition fieldDefinition = fieldRef.Resolve(); + if (fieldDefinition != null && !fieldDefinition.HasConstant) return false; + // rewrite if possible + TypeDefinition declaringType = fieldRef.DeclaringType.Resolve(); + bool isRead = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld; + return + this.TryRewriteToProperty(module, instruction, fieldRef, declaringType, isRead) + || this.TryRewriteToConstField(instruction, fieldDefinition); + } + + + /********* + ** Private methods + *********/ + /// Whether references to the given type should be validated. + /// The type reference. + private bool ShouldValidate(TypeReference type) + { + return type != null && this.RewriteReferencesToAssemblies.Contains(type.Scope.Name); + } + + /// Try rewriting the field into a matching property. + /// The assembly module containing the instruction. + /// The CIL instruction to rewrite. + /// The field reference. + /// The type on which the field was defined. + /// Whether the field is being read; else it's being written to. + private bool TryRewriteToProperty(ModuleDefinition module, Instruction instruction, FieldReference fieldRef, TypeDefinition declaringType, bool isRead) + { // get equivalent property - PropertyDefinition property = fieldRef.DeclaringType.Resolve().Properties.FirstOrDefault(p => p.Name == fieldRef.Name); - MethodDefinition method = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld - ? property?.GetMethod - : property?.SetMethod; + PropertyDefinition property = declaringType.Properties.FirstOrDefault(p => p.Name == fieldRef.Name); + MethodDefinition method = isRead ? property?.GetMethod : property?.SetMethod; if (method == null) return false; @@ -55,15 +81,26 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return this.MarkRewritten(); } - - /********* - ** Private methods - *********/ - /// Whether references to the given type should be validated. - /// The type reference. - private bool ShouldValidate(TypeReference type) + /// Try rewriting the field into a matching const field. + /// The CIL instruction to rewrite. + /// The field definition. + private bool TryRewriteToConstField(Instruction instruction, FieldDefinition field) { - return type != null && this.RewriteReferencesToAssemblies.Contains(type.Scope.Name); + // must have been a static field read, and the new field must be const + if (instruction.OpCode != OpCodes.Ldsfld || field?.HasConstant != true) + return false; + + // get opcode for value type + Instruction loadInstruction = RewriteHelper.GetLoadValueInstruction(field.Constant); + if (loadInstruction == null) + return false; + + // rewrite to constant + instruction.OpCode = loadInstruction.OpCode; + instruction.Operand = loadInstruction.Operand; + + this.Phrases.Add($"{field.DeclaringType.Name}.{field.Name} (field => const)"); + return this.MarkRewritten(); } } } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs index 21b42e12..e133b6fa 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs @@ -64,7 +64,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters // get instructions to inject parameter values var loadInstructions = method.Parameters.Skip(methodRef.Parameters.Count) - .Select(p => this.GetLoadValueInstruction(p.Constant)) + .Select(p => RewriteHelper.GetLoadValueInstruction(p.Constant)) .ToArray(); if (loadInstructions.Any(p => p == null)) return false; // SMAPI needs to load the value onto the stack before the method call, but the optional parameter type wasn't recognized @@ -105,23 +105,5 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return true; } - - /// Get the CIL instruction to load a value onto the stack. - /// The constant value to inject. - /// Returns the instruction, or null if the value type isn't supported. - private Instruction GetLoadValueInstruction(object rawValue) - { - return rawValue switch - { - null => Instruction.Create(OpCodes.Ldnull), - bool value => Instruction.Create(value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0), - int value => Instruction.Create(OpCodes.Ldc_I4, value), // int32 - long value => Instruction.Create(OpCodes.Ldc_I8, value), // int64 - float value => Instruction.Create(OpCodes.Ldc_R4, value), // float32 - double value => Instruction.Create(OpCodes.Ldc_R8, value), // float64 - string value => Instruction.Create(OpCodes.Ldstr, value), - _ => null - }; - } } } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs deleted file mode 100644 index f34d4943..00000000 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using Mono.Cecil; -using Mono.Cecil.Cil; -using StardewModdingAPI.Framework.ModLoading.Framework; - -namespace StardewModdingAPI.Framework.ModLoading.Rewriters -{ - /// Rewrites static field references into constant values. - /// The constant value type. - internal class StaticFieldToConstantRewriter : BaseInstructionHandler - { - /********* - ** Fields - *********/ - /// The type containing the field to which references should be rewritten. - private readonly Type Type; - - /// The field name to which references should be rewritten. - private readonly string FromFieldName; - - /// The constant value to replace with. - private readonly TValue Value; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The type whose field to which references should be rewritten. - /// The field name to rewrite. - /// The constant value to replace with. - public StaticFieldToConstantRewriter(Type type, string fieldName, TValue value) - : base(defaultPhrase: $"{type.FullName}.{fieldName} field") - { - this.Type = type; - this.FromFieldName = fieldName; - this.Value = value; - } - - /// - public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) - { - // get field reference - FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); - if (!RewriteHelper.IsFieldReferenceTo(fieldRef, this.Type.FullName, this.FromFieldName)) - return false; - - // rewrite to constant - if (typeof(TValue) == typeof(int)) - { - instruction.OpCode = OpCodes.Ldc_I4; - instruction.Operand = this.Value; - } - else if (typeof(TValue) == typeof(string)) - { - instruction.OpCode = OpCodes.Ldstr; - instruction.Operand = this.Value; - } - else - throw new NotSupportedException($"Rewriting to constant values of type {typeof(TValue)} isn't currently supported."); - - return this.MarkRewritten(); - } - } -} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 86e16e1e..09a199f9 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -35,9 +35,6 @@ namespace StardewModdingAPI.Metadata if (platformChanged) yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchFacade)); - // rewrite for Stardew Valley 1.3 - yield return new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize); - // heuristic rewrites yield return new HeuristicFieldRewriter(this.ValidateReferencesToAssemblies); yield return new HeuristicMethodRewriter(this.ValidateReferencesToAssemblies); -- cgit From a13c994e8b2573e7a8be5a77a8e348f1171dbc55 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 29 Aug 2020 23:06:08 -0400 Subject: format code --- src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 6 +++--- src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 26ddb067..12d672cf 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -250,7 +250,7 @@ namespace StardewModdingAPI.Framework.ContentManagers if (pixel.A == byte.MinValue || pixel.A == byte.MaxValue) continue; // no need to change fully transparent/opaque pixels - data[i] = new Color(pixel.R * pixel.A / byte.MaxValue, pixel.G * pixel.A / byte.MaxValue, pixel.B * pixel.A / byte.MaxValue, pixel.A); // slower version: Color.FromNonPremultiplied(data[i].ToVector4()) + data[i] = new Color(pixel.R * pixel.A / byte.MaxValue, pixel.G * pixel.A / byte.MaxValue, pixel.B * pixel.A / byte.MaxValue, pixel.A); // slower version: Color.FromNonPremultiplied(data[i].ToVector4()) } texture.SetData(data); @@ -275,12 +275,12 @@ namespace StardewModdingAPI.Framework.ContentManagers /// * If the location is indoors or the desert, or the image source contains 'path' or 'object', it's loaded /// as-is relative to the Content folder. /// * Else it's loaded from Content\Maps with a seasonal prefix. - /// + /// /// That logic doesn't work well in our case, mainly because we have no location metadata at this point. /// Instead we use a more heuristic approach: check relative to the map file first, then relative to /// Content\Maps, then Content. If the image source filename contains a seasonal prefix, try for a /// seasonal variation and then an exact match. - /// + /// /// While that doesn't exactly match the game logic, it's close enough that it's unlikely to make a difference. /// private void FixCustomTilesheetPaths(Map map, string relativeMapPath) diff --git a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs index 4b88148f..207b6445 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs @@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// The type reference. public static bool IsSameType(Type type, TypeReference reference) { - // + // // duplicated by IsSameType(TypeReference, TypeReference) below // @@ -157,7 +157,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// The method reference. public static bool HasMatchingSignature(MethodBase definition, MethodReference reference) { - // + // // duplicated by HasMatchingSignature(MethodDefinition, MethodReference) below // @@ -183,7 +183,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// The method reference. public static bool HasMatchingSignature(MethodDefinition definition, MethodReference reference) { - // + // // duplicated by HasMatchingSignature(MethodBase, MethodReference) above // -- cgit From 4088f4cb2bfe777cf6f86ac5fbf64f7d67565057 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 4 Sep 2020 22:02:59 -0400 Subject: simplify error shown for duplicate mods --- docs/release-notes.md | 3 +- src/SMAPI.Tests/Core/ModResolverTests.cs | 22 ++++++------ src/SMAPI/Framework/IModMetadata.cs | 10 +++++- src/SMAPI/Framework/Logging/LogManager.cs | 13 +++++++ src/SMAPI/Framework/ModLoading/ModFailReason.cs | 27 ++++++++++++++ src/SMAPI/Framework/ModLoading/ModMetadata.cs | 14 +++++++- src/SMAPI/Framework/ModLoading/ModResolver.cs | 48 ++++++++++++------------- src/SMAPI/Framework/SCore.cs | 21 ++++++++--- 8 files changed, 116 insertions(+), 42 deletions(-) create mode 100644 src/SMAPI/Framework/ModLoading/ModFailReason.cs (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/docs/release-notes.md b/docs/release-notes.md index ae636153..297d8394 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,8 +9,9 @@ ## Upcoming release * For players: - * Added heuristic compatibility rewrites. (This fixes some mods previously broken on Android, and improves compatibility with future game updates.) + * Added heuristic compatibility rewrites. (This improves mod compatibility with Android and future game updates.) * Tweaked the rules for showing update alerts (see _for SMAPI developers_ below for details). + * Simplified error shown for duplicate mods. * Fixed crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute (thanks to spacechase0!). * Fixed map tile rotation broken when you return to the title screen and reload a save. * Fixed broken URL in update alerts for unofficial versions. diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 4f3a12cb..78056ef7 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -154,7 +154,7 @@ namespace SMAPI.Tests.Core new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert - mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); + mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); } [Test(Description = "Assert that validation fails when the minimum API version is higher than the current SMAPI version.")] @@ -169,7 +169,7 @@ namespace SMAPI.Tests.Core new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert - mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); + mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); } [Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")] @@ -183,7 +183,7 @@ namespace SMAPI.Tests.Core new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert - mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); + mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); } [Test(Description = "Assert that validation fails when multiple mods have the same unique ID.")] @@ -200,8 +200,8 @@ namespace SMAPI.Tests.Core new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert - modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the first mod with a unique ID."); - modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the second mod with a unique ID."); + modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the first mod with a unique ID."); + modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the second mod with a unique ID."); } [Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")] @@ -367,9 +367,9 @@ namespace SMAPI.Tests.Core Assert.AreEqual(5, mods.Length, 0, "Expected to get the same number of mods input."); Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); - modC.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "Mod C was expected to fail since it's part of a dependency loop."); - modD.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "Mod D was expected to fail since it's part of a dependency loop."); - modE.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "Mod E was expected to fail since it's part of a dependency loop."); + modC.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "Mod C was expected to fail since it's part of a dependency loop."); + modD.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "Mod D was expected to fail since it's part of a dependency loop."); + modE.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "Mod E was expected to fail since it's part of a dependency loop."); } [Test(Description = "Assert that dependencies are sorted correctly even if some of the mods failed during metadata loading.")] @@ -408,7 +408,7 @@ namespace SMAPI.Tests.Core // assert Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); - modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "Mod B unexpectedly didn't fail even though it needs a newer version of Mod A."); + modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "Mod B unexpectedly didn't fail even though it needs a newer version of Mod A."); } [Test(Description = "Assert that dependencies are accepted if they meet the minimum version.")] @@ -525,8 +525,8 @@ namespace SMAPI.Tests.Core if (allowStatusChange) { mod - .Setup(p => p.SetStatus(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((status, message, errorDetails) => Console.WriteLine($"<{manifest.UniqueID} changed status: [{status}] {message}\n{errorDetails}")) + .Setup(p => p.SetStatus(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((status, failReason, message, errorDetails) => Console.WriteLine($"<{manifest.UniqueID} changed status: [{status}] {message}\n{failReason}\n{errorDetails}")) .Returns(mod.Object); } return mod; diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 6a635b76..70cf0036 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -31,6 +31,9 @@ namespace StardewModdingAPI.Framework /// The metadata resolution status. ModMetadataStatus Status { get; } + /// The reason the mod failed to load, if applicable. + ModFailReason? FailReason { get; } + /// Indicates non-error issues with the mod. ModWarning Warnings { get; } @@ -65,12 +68,17 @@ namespace StardewModdingAPI.Framework /********* ** Public methods *********/ + /// Set the mod status to . + /// Return the instance for chaining. + IModMetadata SetStatusFound(); + /// Set the mod status. /// The metadata resolution status. + /// The reason a mod could not be loaded. /// The reason the metadata is invalid, if any. /// A detailed technical message, if any. /// Return the instance for chaining. - IModMetadata SetStatus(ModMetadataStatus status, string error = null, string errorDetails = null); + IModMetadata SetStatus(ModMetadataStatus status, ModFailReason reason, string error, string errorDetails = null); /// Set a warning flag for the mod. /// The warning to set. diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index d0936f3f..094dd749 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading; using StardewModdingAPI.Framework.Commands; +using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Internal.ConsoleWriting; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Utilities; @@ -397,10 +398,22 @@ namespace StardewModdingAPI.Framework.Logging if (skippedMods.Any()) { // get logging logic + HashSet loggedDuplicateIds = new HashSet(); void LogSkippedMod(IModMetadata mod) { string message = $" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {mod.Error}"; + // handle duplicate mods + // (log first duplicate only, don't show redundant version) + if (mod.FailReason == ModFailReason.Duplicate && mod.HasManifest()) + { + if (!loggedDuplicateIds.Add(mod.Manifest.UniqueID)) + return; // already logged + + message = $" - {mod.DisplayName} because {mod.Error}"; + } + + // log message this.Monitor.Log(message, LogLevel.Error); if (mod.ErrorDetails != null) this.Monitor.Log($" ({mod.ErrorDetails})"); diff --git a/src/SMAPI/Framework/ModLoading/ModFailReason.cs b/src/SMAPI/Framework/ModLoading/ModFailReason.cs new file mode 100644 index 00000000..cd4623e7 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/ModFailReason.cs @@ -0,0 +1,27 @@ +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Indicates why a mod could not be loaded. + internal enum ModFailReason + { + /// The mod has been disabled by prefixing its folder with a dot. + DisabledByDotConvention, + + /// Multiple copies of the mod are installed. + Duplicate, + + /// The mod has incompatible code instructions, needs a newer SMAPI version, or is marked 'assume broken' in the SMAPI metadata list. + Incompatible, + + /// The mod's manifest is missing or invalid. + InvalidManifest, + + /// The mod was deemed compatible, but SMAPI failed when it tried to load it. + LoadFailed, + + /// The mod requires other mods which aren't installed, or its dependencies have a circular reference. + MissingDependencies, + + /// The mod is marked obsolete in the SMAPI metadata list. + Obsolete + } +} diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index e793b0cd..18d2b112 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -37,6 +37,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// public ModMetadataStatus Status { get; private set; } + /// + public ModFailReason? FailReason { get; private set; } + /// public ModWarning Warnings { get; private set; } @@ -93,9 +96,18 @@ namespace StardewModdingAPI.Framework.ModLoading } /// - public IModMetadata SetStatus(ModMetadataStatus status, string error = null, string errorDetails = null) + public IModMetadata SetStatusFound() + { + this.SetStatus(ModMetadataStatus.Found, ModFailReason.Incompatible, null); + this.FailReason = null; + return this; + } + + /// + public IModMetadata SetStatus(ModMetadataStatus status, ModFailReason reason, string error, string errorDetails = null) { this.Status = status; + this.FailReason = reason; this.Error = error; this.ErrorDetails = errorDetails; return this; diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 8bbeb2a3..08df7b76 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -43,8 +43,13 @@ namespace StardewModdingAPI.Framework.ModLoading ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, rootPath, manifest, dataRecord, isIgnored: shouldIgnore) - .SetStatus(status, shouldIgnore ? "disabled by dot convention" : folder.ManifestParseErrorText); + var metadata = new ModMetadata(folder.DisplayName, folder.Directory.FullName, rootPath, manifest, dataRecord, isIgnored: shouldIgnore); + if (shouldIgnore) + metadata.SetStatus(status, ModFailReason.DisabledByDotConvention, "disabled by dot convention"); + else + metadata.SetStatus(status, ModFailReason.InvalidManifest, folder.ManifestParseErrorText); + + yield return metadata; } } @@ -67,7 +72,7 @@ namespace StardewModdingAPI.Framework.ModLoading switch (mod.DataRecord?.Status) { case ModStatus.Obsolete: - mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}"); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Obsolete, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}"); continue; case ModStatus.AssumeBroken: @@ -97,7 +102,7 @@ namespace StardewModdingAPI.Framework.ModLoading error += $"version newer than {mod.DataRecord.StatusUpperVersion}"; error += " at " + string.Join(" or ", updateUrls); - mod.SetStatus(ModMetadataStatus.Failed, error); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Incompatible, error); } continue; } @@ -105,7 +110,7 @@ namespace StardewModdingAPI.Framework.ModLoading // validate SMAPI version if (mod.Manifest.MinimumApiVersion?.IsNewerThan(apiVersion) == true) { - mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Incompatible, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod."); continue; } @@ -117,12 +122,12 @@ namespace StardewModdingAPI.Framework.ModLoading // validate field presence if (!hasDll && !isContentPack) { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."); continue; } if (hasDll && isContentPack) { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."); continue; } @@ -132,14 +137,14 @@ namespace StardewModdingAPI.Framework.ModLoading // invalid filename format if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any()) { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); continue; } // invalid path if (!File.Exists(Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll))) { - mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); continue; } @@ -147,7 +152,7 @@ namespace StardewModdingAPI.Framework.ModLoading string actualFilename = new DirectoryInfo(mod.DirectoryPath).GetFiles(mod.Manifest.EntryDll).FirstOrDefault()?.Name; if (actualFilename != mod.Manifest.EntryDll) { - mod.SetStatus(ModMetadataStatus.Failed, $"its {nameof(IManifest.EntryDll)} value '{mod.Manifest.EntryDll}' doesn't match the actual file capitalization '{actualFilename}'. The capitalization must match for crossplatform compatibility."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its {nameof(IManifest.EntryDll)} value '{mod.Manifest.EntryDll}' doesn't match the actual file capitalization '{actualFilename}'. The capitalization must match for crossplatform compatibility."); continue; } } @@ -158,7 +163,7 @@ namespace StardewModdingAPI.Framework.ModLoading // invalid content pack ID if (string.IsNullOrWhiteSpace(mod.Manifest.ContentPackFor.UniqueID)) { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."); continue; } } @@ -177,14 +182,14 @@ namespace StardewModdingAPI.Framework.ModLoading if (missingFields.Any()) { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); continue; } } // validate ID format if (!PathUtilities.IsSlug(mod.Manifest.UniqueID)) - mod.SetStatus(ModMetadataStatus.Failed, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."); } // validate IDs are unique @@ -199,13 +204,8 @@ namespace StardewModdingAPI.Framework.ModLoading if (mod.Status == ModMetadataStatus.Failed) continue; // don't replace metadata error - string folderList = string.Join(", ", - from entry in @group - let relativePath = entry.GetRelativePathWithRoot() - orderby relativePath - select $"{relativePath} ({entry.Manifest.Version})" - ); - mod.SetStatus(ModMetadataStatus.Failed, $"you have multiple copies of this mod installed. Found in folders: {folderList}."); + string folderList = string.Join(", ", group.Select(p => p.GetRelativePathWithRoot()).OrderBy(p => p)); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Duplicate, $"you have multiple copies of this mod installed. To fix this, delete these folders and reinstall the mod: {folderList}."); } } } @@ -298,7 +298,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (failedModNames.Any()) { sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", failedModNames)})."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it requires mods which aren't installed ({string.Join(", ", failedModNames)})."); return states[mod] = ModDependencyStatus.Failed; } } @@ -315,7 +315,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (failedLabels.Any()) { sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it needs newer versions of some mods: {string.Join(", ", failedLabels)}."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it needs newer versions of some mods: {string.Join(", ", failedLabels)}."); return states[mod] = ModDependencyStatus.Failed; } } @@ -338,7 +338,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (states[requiredMod] == ModDependencyStatus.Checking) { sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"its dependencies have a circular reference: {string.Join(" => ", subchain.Select(p => p.DisplayName))} => {requiredMod.DisplayName})."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"its dependencies have a circular reference: {string.Join(" => ", subchain.Select(p => p.DisplayName))} => {requiredMod.DisplayName})."); return states[mod] = ModDependencyStatus.Failed; } @@ -354,7 +354,7 @@ namespace StardewModdingAPI.Framework.ModLoading // failed, which means this mod can't be loaded either case ModDependencyStatus.Failed: sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it needs the '{requiredMod.DisplayName}' mod, which couldn't be loaded."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it needs the '{requiredMod.DisplayName}' mod, which couldn't be loaded."); return states[mod] = ModDependencyStatus.Failed; // unexpected status diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index bfe6e277..52b4b9cf 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1339,9 +1339,10 @@ namespace StardewModdingAPI.Framework // load mods foreach (IModMetadata mod in mods) { - if (!this.TryLoadMod(mod, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out string errorPhrase, out string errorDetails)) + if (!this.TryLoadMod(mod, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out ModFailReason? failReason, out string errorPhrase, out string errorDetails)) { - mod.SetStatus(ModMetadataStatus.Failed, errorPhrase, errorDetails); + failReason ??= ModFailReason.LoadFailed; + mod.SetStatus(ModMetadataStatus.Failed, failReason.Value, errorPhrase, errorDetails); skippedMods.Add(mod); } } @@ -1437,16 +1438,17 @@ namespace StardewModdingAPI.Framework /// Load a given mod. /// The mod to load. /// The mods being loaded. - /// Preprocesses and loads mod assemblies + /// Preprocesses and loads mod assemblies. /// Generates proxy classes to access mod APIs through an arbitrary interface. /// The JSON helper with which to read mods' JSON files. /// The content manager to use for mod content. /// Handles access to SMAPI's internal mod metadata list. /// The mod IDs to ignore when validating update keys. + /// The reason the mod couldn't be loaded, if applicable. /// The user-facing reason phrase explaining why the mod couldn't be loaded (if applicable). /// More detailed details about the error intended for developers (if any). /// Returns whether the mod was successfully loaded. - private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, AssemblyLoader assemblyLoader, InterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet suppressUpdateChecks, out string errorReasonPhrase, out string errorDetails) + private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, AssemblyLoader assemblyLoader, InterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet suppressUpdateChecks, out ModFailReason? failReason, out string errorReasonPhrase, out string errorDetails) { errorDetails = null; @@ -1469,6 +1471,7 @@ namespace StardewModdingAPI.Framework if (mod.Status == ModMetadataStatus.Failed) { this.Monitor.Log($" Failed: {mod.Error}"); + failReason = mod.FailReason; errorReasonPhrase = mod.Error; return false; } @@ -1485,6 +1488,7 @@ namespace StardewModdingAPI.Framework .FirstOrDefault(otherMod => otherMod.HasID(dependency.UniqueID)) ?.DisplayName ?? dependency.UniqueID; errorReasonPhrase = $"it needs the '{dependencyName}' mod, which couldn't be loaded."; + failReason = ModFailReason.MissingDependencies; return false; } } @@ -1502,6 +1506,7 @@ namespace StardewModdingAPI.Framework this.ModRegistry.Add(mod); errorReasonPhrase = null; + failReason = null; return true; } @@ -1524,17 +1529,20 @@ namespace StardewModdingAPI.Framework { string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(manifest.UniqueID), "https://smapi.io/mods" }.Where(p => p != null).ToArray(); errorReasonPhrase = $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}"; + failReason = ModFailReason.Incompatible; return false; } catch (SAssemblyLoadFailedException ex) { errorReasonPhrase = $"its DLL couldn't be loaded: {ex.Message}"; + failReason = ModFailReason.LoadFailed; return false; } catch (Exception ex) { errorReasonPhrase = "its DLL couldn't be loaded."; errorDetails = $"Error: {ex.GetLogSummary()}"; + failReason = ModFailReason.LoadFailed; return false; } @@ -1543,7 +1551,10 @@ namespace StardewModdingAPI.Framework { // get mod instance if (!this.TryLoadModEntry(modAssembly, out Mod modEntry, out errorReasonPhrase)) + { + failReason = ModFailReason.LoadFailed; return false; + } // get content packs IContentPack[] GetContentPacks() @@ -1591,11 +1602,13 @@ namespace StardewModdingAPI.Framework // track mod mod.SetMod(modEntry, translationHelper); this.ModRegistry.Add(mod); + failReason = null; return true; } catch (Exception ex) { errorReasonPhrase = $"initialization failed:\n{ex.GetLogSummary()}"; + failReason = ModFailReason.LoadFailed; return false; } } -- cgit From 55cd31f4f7d5122149c02abfaf0f408298503c6a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 5 Sep 2020 15:41:21 -0400 Subject: minor cleanup --- src/SMAPI/Framework/ContentCoordinator.cs | 4 ++-- src/SMAPI/Framework/ContentPack.cs | 3 --- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 3 --- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 4 ++-- .../ModLoading/Finders/ReferenceToMissingMemberFinder.cs | 2 +- src/SMAPI/Framework/Reflection/Reflector.cs | 2 +- src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs | 8 ++++---- src/SMAPI/Framework/SGame.cs | 5 +---- src/SMAPI/Metadata/CoreAssetPropagator.cs | 7 +------ src/SMAPI/SButton.cs | 2 +- 10 files changed, 13 insertions(+), 27 deletions(-) (limited to 'src/SMAPI/Framework/ModLoading') diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index d1021cad..93371415 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -95,7 +95,7 @@ namespace StardewModdingAPI.Framework this.ContentManagers.Add( this.MainContentManager = new GameContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, this.OnDisposing, onLoadingFirstAsset) ); - this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.AssertAndNormalizeAssetName, reflection, monitor); + this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.AssertAndNormalizeAssetName, reflection); } /// Get a new content manager which handles reading files from the game content folder with support for interception. @@ -236,7 +236,7 @@ namespace StardewModdingAPI.Framework { foreach (var entry in contentManager.InvalidateCache(predicate, dispose)) { - if (!removedAssets.TryGetValue(entry.Key, out Type type)) + if (!removedAssets.ContainsKey(entry.Key)) removedAssets[entry.Key] = entry.Value.GetType(); } } diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index 43621141..55c1a0b2 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -1,10 +1,7 @@ using System; using System.IO; -using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; -using xTile; namespace StardewModdingAPI.Framework { diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 80f61c13..5fd8f5e9 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -5,13 +5,10 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.IO; using System.Linq; -using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Exceptions; using StardewValley; -using xTile; namespace StardewModdingAPI.Framework.ModHelpers { diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index cfe4c747..9fb5384e 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -409,10 +409,10 @@ namespace StardewModdingAPI.Framework.ModLoading if (handler.Phrases.Any()) { foreach (string message in handler.Phrases) - this.Monitor.LogOnce(template.Replace("$phrase", message)); + this.Monitor.LogOnce(loggedMessages, template.Replace("$phrase", message)); } else - this.Monitor.LogOnce(template.Replace("$phrase", handler.DefaultPhrase ?? handler.GetType().Name)); + this.Monitor.LogOnce(loggedMessages, template.Replace("$phrase", handler.DefaultPhrase ?? handler.GetType().Name)); } /// Get the correct reference to use for compatibility with the current platform. diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs index 75575c97..b64a255e 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs @@ -49,7 +49,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders MethodDefinition target = methodRef.Resolve(); if (target == null) { - string phrase = null; + string phrase; if (this.IsProperty(methodRef)) phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)"; else if (methodRef.Name == ".ctor") diff --git a/src/SMAPI/Framework/Reflection/Reflector.cs b/src/SMAPI/Framework/Reflection/Reflector.cs index d4904878..889c7ed6 100644 --- a/src/SMAPI/Framework/Reflection/Reflector.cs +++ b/src/SMAPI/Framework/Reflection/Reflector.cs @@ -263,7 +263,7 @@ namespace StardewModdingAPI.Framework.Reflection CacheEntry entry = (CacheEntry)this.Cache[key]; return entry.IsValid ? (TMemberInfo)entry.MemberInfo - : default(TMemberInfo); + : default; } // fetch & cache new value diff --git a/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs b/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs index 121e53bc..cb499c6b 100644 --- a/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs +++ b/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs @@ -93,17 +93,17 @@ namespace StardewModdingAPI.Framework.Rendering { if (tile == null) return; - xTile.Dimensions.Rectangle tileImageBounds = tile.TileSheet.GetTileImageBounds(tile.TileIndex); + Rectangle tileImageBounds = tile.TileSheet.GetTileImageBounds(tile.TileIndex); Texture2D tileSheetTexture = this.m_tileSheetTextures[tile.TileSheet]; if (tileSheetTexture.IsDisposed) return; - this.m_tilePosition.X = (float)location.X; - this.m_tilePosition.Y = (float)location.Y; + this.m_tilePosition.X = location.X; + this.m_tilePosition.Y = location.Y; this.m_sourceRectangle.X = tileImageBounds.X; this.m_sourceRectangle.Y = tileImageBounds.Y; this.m_sourceRectangle.Width = tileImageBounds.Width; this.m_sourceRectangle.Height = tileImageBounds.Height; - this.m_spriteBatchAlpha.Draw(tileSheetTexture, this.m_tilePosition, new Microsoft.Xna.Framework.Rectangle?(this.m_sourceRectangle), this.m_modulationColour, 0.0f, Vector2.Zero, (float)Layer.zoom, SpriteEffects.None, layerDepth); + this.m_spriteBatchAlpha.Draw(tileSheetTexture, this.m_tilePosition, this.m_sourceRectangle, this.m_modulationColour, 0.0f, Vector2.Zero, Layer.zoom, SpriteEffects.None, layerDepth); } /// Finish drawing to the screen. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index ae2c028d..6680a6c9 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -297,8 +297,6 @@ namespace StardewModdingAPI.Framework } if (Game1.currentMinigame != null) { - bool batchEnded = false; - if (events.Rendering.HasListeners()) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); @@ -321,12 +319,11 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); Game1.spriteBatch.Draw(target_screen, Vector2.Zero, target_screen.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); events.Rendered.RaiseEmpty(); - batchEnded = true; Game1.spriteBatch.End(); } else { - if (!batchEnded && events.Rendered.HasListeners()) + if (events.Rendered.HasListeners()) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); events.Rendered.RaiseEmpty(); diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 5c77bf66..41d10cd4 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -34,9 +34,6 @@ namespace StardewModdingAPI.Metadata /// Simplifies access to private game code. private readonly Reflector Reflection; - /// Encapsulates monitoring and logging. - private readonly IMonitor Monitor; - /// Optimized bucket categories for batch reloading assets. private enum AssetBucket { @@ -57,12 +54,10 @@ namespace StardewModdingAPI.Metadata /// Initialize the core asset data. /// Normalizes an asset key to match the cache key and assert that it's valid. /// Simplifies access to private code. - /// Encapsulates monitoring and logging. - public CoreAssetPropagator(Func assertAndNormalizeAssetName, Reflector reflection, IMonitor monitor) + public CoreAssetPropagator(Func assertAndNormalizeAssetName, Reflector reflection) { this.AssertAndNormalizeAssetName = assertAndNormalizeAssetName; this.Reflection = reflection; - this.Monitor = monitor; } /// Reload one of the game's core assets (if applicable). diff --git a/src/SMAPI/SButton.cs b/src/SMAPI/SButton.cs index bc76c91d..cc412946 100644 --- a/src/SMAPI/SButton.cs +++ b/src/SMAPI/SButton.cs @@ -681,7 +681,7 @@ namespace StardewModdingAPI } // not valid - button = default(InputButton); + button = default; return false; } -- cgit