From 066f1857a145c8b9e80a095d2dee1be6419f957b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Aug 2020 11:56:40 -0400 Subject: fix error when mods add/remove events asynchronously --- src/SMAPI/Framework/Events/ManagedEvent.cs | 43 ++++++++++++++++++------------ 1 file changed, 26 insertions(+), 17 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index 8b25a9b5..f2dfb2ab 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -70,27 +70,33 @@ namespace StardewModdingAPI.Framework.Events /// The mod which added the event handler. public void Add(EventHandler handler, IModMetadata mod) { - EventPriority priority = handler.Method.GetCustomAttribute()?.Priority ?? EventPriority.Normal; - var managedHandler = new ManagedEventHandler(handler, this.RegistrationIndex++, priority, mod); + lock (this.Handlers) + { + EventPriority priority = handler.Method.GetCustomAttribute()?.Priority ?? EventPriority.Normal; + var managedHandler = new ManagedEventHandler(handler, this.RegistrationIndex++, priority, mod); - this.Handlers.Add(managedHandler); - this.CachedHandlers = null; - this.HasNewHandlers = true; + this.Handlers.Add(managedHandler); + this.CachedHandlers = null; + this.HasNewHandlers = true; + } } /// Remove an event handler. /// The event handler. public void Remove(EventHandler handler) { - // match C# events: if a handler is listed multiple times, remove the last one added - for (int i = this.Handlers.Count - 1; i >= 0; i--) + lock (this.Handlers) { - if (this.Handlers[i].Handler != handler) - continue; + // match C# events: if a handler is listed multiple times, remove the last one added + for (int i = this.Handlers.Count - 1; i >= 0; i--) + { + if (this.Handlers[i].Handler != handler) + continue; - this.Handlers.RemoveAt(i); - this.CachedHandlers = null; - break; + this.Handlers.RemoveAt(i); + this.CachedHandlers = null; + break; + } } } @@ -106,14 +112,17 @@ namespace StardewModdingAPI.Framework.Events // update cached data // (This is debounced here to avoid repeatedly sorting when handlers are added/removed, // and keeping a separate cached list allows changes during enumeration.) - var handlers = this.CachedHandlers; // iterate local copy in case a mod adds/removes a handler while handling the event + var handlers = this.CachedHandlers; // iterate local copy in case a mod adds/removes a handler while handling the event, which will set this field to null if (handlers == null) { - if (this.HasNewHandlers && this.Handlers.Any(p => p.Priority != EventPriority.Normal)) - this.Handlers.Sort(); + lock (this.Handlers) + { + if (this.HasNewHandlers && this.Handlers.Any(p => p.Priority != EventPriority.Normal)) + this.Handlers.Sort(); - this.CachedHandlers = handlers = this.Handlers.ToArray(); - this.HasNewHandlers = false; + this.CachedHandlers = handlers = this.Handlers.ToArray(); + this.HasNewHandlers = false; + } } // raise event -- cgit 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 --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 10 ++-- .../ModLoading/Framework/RecursiveRewriter.cs | 57 ++++------------------ src/SMAPI/Framework/Models/SConfig.cs | 4 -- src/SMAPI/Framework/SCore.cs | 4 +- src/SMAPI/SMAPI.config.json | 9 ---- 6 files changed, 16 insertions(+), 69 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2e1e050e..2ff4ec0a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ ## Upcoming release * For players: * Fixed rare error when a mod adds/removes event handlers asynchronously. + * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent 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/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; } diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 1c682f96..3a3f6960 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -16,7 +16,6 @@ namespace StardewModdingAPI.Framework.Models { [nameof(CheckForUpdates)] = true, [nameof(ParanoidWarnings)] = Constants.IsDebugBuild, - [nameof(RewriteInParallel)] = Constants.IsDebugBuild, [nameof(UseBetaChannel)] = Constants.ApiVersion.IsPrerelease(), [nameof(GitHubProjectName)] = "Pathoschild/SMAPI", [nameof(WebApiBaseUrl)] = "https://smapi.io/api/", @@ -41,9 +40,6 @@ namespace StardewModdingAPI.Framework.Models /// Whether to check for newer versions of SMAPI and mods on startup. public bool CheckForUpdates { get; set; } - /// Whether to enable experimental parallel rewriting. - public bool RewriteInParallel { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.RewriteInParallel)]; - /// Whether to add a section to the 'mod issues' list for mods which which directly use potentially sensitive .NET APIs like file or shell access. public bool ParanoidWarnings { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.ParanoidWarnings)]; diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 72ef9095..fd8d7034 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -337,8 +337,6 @@ namespace StardewModdingAPI.Framework // add headers if (this.Settings.DeveloperMode) this.Monitor.Log($"You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); - if (this.Settings.RewriteInParallel) - this.Monitor.Log($"You enabled experimental parallel rewriting. This may result in faster startup times, but intermittent startup errors. You can disable it by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Info); if (!this.Settings.CheckForUpdates) this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); if (!this.Monitor.WriteToConsole) @@ -983,7 +981,7 @@ namespace StardewModdingAPI.Framework Assembly modAssembly; try { - modAssembly = assemblyLoader.Load(mod, assemblyPath, assumeCompatible: mod.DataRecord?.Status == ModStatus.AssumeCompatible, rewriteInParallel: this.Settings.RewriteInParallel); + modAssembly = assemblyLoader.Load(mod, assemblyPath, assumeCompatible: mod.DataRecord?.Status == ModStatus.AssumeCompatible); this.ModRegistry.TrackAssemblies(mod, modAssembly); } catch (IncompatibleInstructionException) // details already in trace logs diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 0a6d8372..6ba64fe7 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -33,15 +33,6 @@ copy all the settings, or you may cause bugs due to overridden changes in future */ "DeveloperMode": true, - /** - * Whether to enable experimental parallel rewriting when SMAPI is loading mods. This can - * reduce startup time when you have many mods installed, but is experimental and may cause - * intermittent startup errors. - * - * When this is commented out, it'll be true for local debug builds and false otherwise. - */ - //"RewriteInParallel": false, - /** * Whether to add a section to the 'mod issues' list for mods which directly use potentially * sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as -- 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') 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') 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 fd3bc77d8183070ca338d6a2702edeea7245b5bf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 23 Aug 2020 17:24:08 -0400 Subject: centralise core SMAPI logic, decouple from Game1 where possible --- src/SMAPI/Events/OneSecondUpdateTickedEventArgs.cs | 2 +- .../Events/OneSecondUpdateTickingEventArgs.cs | 2 +- .../Events/UnvalidatedUpdateTickedEventArgs.cs | 2 +- .../Events/UnvalidatedUpdateTickingEventArgs.cs | 2 +- src/SMAPI/Events/UpdateTickedEventArgs.cs | 2 +- src/SMAPI/Events/UpdateTickingEventArgs.cs | 2 +- src/SMAPI/Framework/SCore.cs | 801 ++++++++++++++++++-- src/SMAPI/Framework/SGame.cs | 810 +-------------------- src/SMAPI/Framework/SGameConstructorHack.cs | 43 -- 9 files changed, 797 insertions(+), 869 deletions(-) delete mode 100644 src/SMAPI/Framework/SGameConstructorHack.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Events/OneSecondUpdateTickedEventArgs.cs b/src/SMAPI/Events/OneSecondUpdateTickedEventArgs.cs index 48e08e5e..608bdc69 100644 --- a/src/SMAPI/Events/OneSecondUpdateTickedEventArgs.cs +++ b/src/SMAPI/Events/OneSecondUpdateTickedEventArgs.cs @@ -10,7 +10,7 @@ namespace StardewModdingAPI.Events ** Accessors *********/ /// The number of ticks elapsed since the game started, including the current tick. - public uint Ticks => SGame.TicksElapsed; + public uint Ticks => SCore.TicksElapsed; /********* diff --git a/src/SMAPI/Events/OneSecondUpdateTickingEventArgs.cs b/src/SMAPI/Events/OneSecondUpdateTickingEventArgs.cs index 58cf802a..f30a2245 100644 --- a/src/SMAPI/Events/OneSecondUpdateTickingEventArgs.cs +++ b/src/SMAPI/Events/OneSecondUpdateTickingEventArgs.cs @@ -10,7 +10,7 @@ namespace StardewModdingAPI.Events ** Accessors *********/ /// The number of ticks elapsed since the game started, excluding the upcoming tick. - public uint Ticks => SGame.TicksElapsed; + public uint Ticks => SCore.TicksElapsed; /********* diff --git a/src/SMAPI/Events/UnvalidatedUpdateTickedEventArgs.cs b/src/SMAPI/Events/UnvalidatedUpdateTickedEventArgs.cs index 258e2f99..ff635787 100644 --- a/src/SMAPI/Events/UnvalidatedUpdateTickedEventArgs.cs +++ b/src/SMAPI/Events/UnvalidatedUpdateTickedEventArgs.cs @@ -10,7 +10,7 @@ namespace StardewModdingAPI.Events ** Accessors *********/ /// The number of ticks elapsed since the game started, including the current tick. - public uint Ticks => SGame.TicksElapsed; + public uint Ticks => SCore.TicksElapsed; /// Whether is a multiple of 60, which happens approximately once per second. public bool IsOneSecond => this.Ticks % 60 == 0; diff --git a/src/SMAPI/Events/UnvalidatedUpdateTickingEventArgs.cs b/src/SMAPI/Events/UnvalidatedUpdateTickingEventArgs.cs index e3c8b3ee..4a01c98b 100644 --- a/src/SMAPI/Events/UnvalidatedUpdateTickingEventArgs.cs +++ b/src/SMAPI/Events/UnvalidatedUpdateTickingEventArgs.cs @@ -10,7 +10,7 @@ namespace StardewModdingAPI.Events ** Accessors *********/ /// The number of ticks elapsed since the game started, excluding the upcoming tick. - public uint Ticks => SGame.TicksElapsed; + public uint Ticks => SCore.TicksElapsed; /// Whether is a multiple of 60, which happens approximately once per second. public bool IsOneSecond => this.Ticks % 60 == 0; diff --git a/src/SMAPI/Events/UpdateTickedEventArgs.cs b/src/SMAPI/Events/UpdateTickedEventArgs.cs index 4f3329ac..ce01ba1e 100644 --- a/src/SMAPI/Events/UpdateTickedEventArgs.cs +++ b/src/SMAPI/Events/UpdateTickedEventArgs.cs @@ -10,7 +10,7 @@ namespace StardewModdingAPI.Events ** Accessors *********/ /// The number of ticks elapsed since the game started, including the current tick. - public uint Ticks => SGame.TicksElapsed; + public uint Ticks => SCore.TicksElapsed; /// Whether is a multiple of 60, which happens approximately once per second. public bool IsOneSecond => this.Ticks % 60 == 0; diff --git a/src/SMAPI/Events/UpdateTickingEventArgs.cs b/src/SMAPI/Events/UpdateTickingEventArgs.cs index 0d3187cd..483142ec 100644 --- a/src/SMAPI/Events/UpdateTickingEventArgs.cs +++ b/src/SMAPI/Events/UpdateTickingEventArgs.cs @@ -10,7 +10,7 @@ namespace StardewModdingAPI.Events ** Accessors *********/ /// The number of ticks elapsed since the game started, excluding the upcoming tick. - public uint Ticks => SGame.TicksElapsed; + public uint Ticks => SCore.TicksElapsed; /// Whether is a multiple of 60, which happens approximately once per second. public bool IsOneSecond => this.Ticks % 60 == 0; diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 5bd02dc8..47a23c87 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -9,21 +10,31 @@ using System.Runtime.ExceptionServices; using System.Security; using System.Text; using System.Threading; +using System.Threading.Tasks; +using Microsoft.Xna.Framework; #if SMAPI_FOR_WINDOWS using System.Windows.Forms; #endif using Newtonsoft.Json; +using StardewModdingAPI.Enums; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.Networking; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.PerformanceMonitoring; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Rendering; using StardewModdingAPI.Framework.Serialization; +using StardewModdingAPI.Framework.StateTracking.Comparers; +using StardewModdingAPI.Framework.StateTracking.Snapshots; +using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Patches; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; @@ -42,15 +53,18 @@ namespace StardewModdingAPI.Framework /********* ** Fields *********/ + /**** + ** Low-level components + ****/ + /// Tracks whether the game should exit immediately and any pending initialization should be cancelled. + private readonly CancellationTokenSource CancellationToken = new CancellationTokenSource(); + /// Manages the SMAPI console window and log file. private readonly LogManager LogManager; /// The core logger and monitor for SMAPI. 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(); - /// Simplifies access to private game code. private readonly Reflector Reflection = new Reflector(); @@ -60,11 +74,26 @@ namespace StardewModdingAPI.Framework /// The SMAPI configuration settings. private readonly SConfig Settings; + /// The mod toolkit used for generic mod interactions. + private readonly ModToolkit Toolkit = new ModToolkit(); + + /**** + ** Higher-level components + ****/ + /// Manages console commands. + private readonly CommandManager CommandManager = new CommandManager(); + /// The underlying game instance. private SGame GameInstance; - /// The underlying content manager. - private ContentCoordinator ContentCore => this.GameInstance.ContentCore; + /// Manages input visible to the game. + private SInputState Input => SGame.Input; + + /// The game's core multiplayer utility. + private SMultiplayer Multiplayer => SGame.Multiplayer; + + /// SMAPI's content manager. + private ContentCoordinator ContentCore; /// Tracks the installed mods. /// This is initialized after the game starts. @@ -73,17 +102,52 @@ namespace StardewModdingAPI.Framework /// Manages SMAPI events for mods. private readonly EventManager EventManager; + /// Monitors the entire game state for changes. + private WatcherCore Watchers; + + /// A snapshot of the current state. + private readonly WatcherSnapshot WatcherSnapshot = new WatcherSnapshot(); + + /**** + ** State + ****/ + /// The path to search for mods. + private string ModsPath => Constants.ModsPath; + /// Whether the game is currently running. private bool IsGameRunning; /// Whether the program has been disposed. private bool IsDisposed; - /// The mod toolkit used for generic mod interactions. - private readonly ModToolkit Toolkit = new ModToolkit(); + /// Whether the next content manager requested by the game will be for . + private bool NextContentManagerIsMain; - /// The path to search for mods. - private string ModsPath => Constants.ModsPath; + /// Whether post-game-startup initialization has been performed. + private bool IsInitialized; + + /// The maximum number of consecutive attempts SMAPI should make to recover from an update error. + private readonly Countdown UpdateCrashTimer = new Countdown(60); // 60 ticks = roughly one second + + /// The number of ticks until SMAPI should notify mods that the game has loaded. + /// Skipping a few frames ensures the game finishes initializing the world before mods try to change it. + private readonly Countdown AfterLoadTimer = new Countdown(5); + + /// Whether custom content was removed from the save data to avoid a crash. + private bool IsSaveContentRemoved; + + /// Whether the game is saving and SMAPI has already raised . + private bool IsBetweenSaveEvents; + + /// Whether the game is creating the save file and SMAPI has already raised . + private bool IsBetweenCreateEvents; + + /// Asset interceptors added or removed since the last tick. + private readonly List ReloadAssetInterceptorsQueue = new List(); + + /// A list of queued commands to execute. + /// This property must be thread-safe, since it's accessed from a separate console input thread. + public ConcurrentQueue CommandQueue { get; } = new ConcurrentQueue(); /********* @@ -97,6 +161,9 @@ namespace StardewModdingAPI.Framework /// This is initialized after the game starts. This is non-private for use by Console Commands. internal static PerformanceMonitor PerformanceMonitor { get; private set; } + /// The number of update ticks which have already executed. This is similar to , but incremented more consistently for every tick. + internal static uint TicksElapsed { get; private set; } + /********* ** Public methods @@ -179,23 +246,24 @@ namespace StardewModdingAPI.Framework LocalizedContentManager.OnLanguageChange += locale => this.OnLocaleChanged(); // override game - SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper, this.InitializeBeforeFirstAssetLoaded); + var multiplayer = new SMultiplayer(this.Monitor, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.Reflection, this.OnModMessageReceived, this.Settings.LogNetworkTraffic); + var modHooks = new SModHooks(this.OnNewDayAfterFade); + SGame.CreateContentManagerImpl = this.CreateContentManager; // must be static since the game accesses it before the SGame constructor is called this.GameInstance = new SGame( monitor: this.Monitor, - monitorForGame: this.LogManager.MonitorForGame, reflection: this.Reflection, - translator: this.Translator, eventManager: this.EventManager, - jsonHelper: this.Toolkit.JsonHelper, - modRegistry: this.ModRegistry, - deprecationManager: SCore.DeprecationManager, - performanceMonitor: SCore.PerformanceMonitor, - onGameInitialized: this.InitializeAfterGameStart, - onGameExiting: this.Dispose, - cancellationToken: this.CancellationToken, - logNetworkTraffic: this.Settings.LogNetworkTraffic + modHooks: modHooks, + multiplayer: multiplayer, + exitGameImmediately: this.ExitGameImmediately ); - this.Translator.SetLocale(this.GameInstance.ContentCore.GetLocale(), this.GameInstance.ContentCore.Language); + + // hook game events + this.GameInstance.OnGameContentLoaded += this.OnLoadContent; + this.GameInstance.OnGameUpdating += this.OnGameUpdating; + this.GameInstance.OnGameExiting += this.OnGameExiting; + + this.Translator.SetLocale(this.ContentCore.GetLocale(), this.ContentCore.Language); StardewValley.Program.gamePtr = this.GameInstance; // apply game patches @@ -203,8 +271,8 @@ namespace StardewModdingAPI.Framework 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 LoadContextPatch(this.Reflection, this.OnLoadStageChanged), + new LoadErrorPatch(this.Monitor, this.OnSaveContentRemoved), new ScheduleErrorPatch(this.LogManager.MonitorForGame) ); @@ -345,9 +413,15 @@ namespace StardewModdingAPI.Framework this.LogManager.SetConsoleTitle($"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"); } - /// Initialize SMAPI and mods after the game starts. - private void InitializeAfterGameStart() + /// Raised after the game finishes initializing. + private void OnGameInitialized() { + // set initial state + this.Input.TrueUpdate(); + + // init watchers + this.Watchers = new WatcherCore(this.Input); + // validate XNB integrity if (!this.ValidateContentIntegrity()) 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); @@ -355,14 +429,557 @@ namespace StardewModdingAPI.Framework // start SMAPI console new Thread( () => this.LogManager.RunConsoleInputLoop( - commandManager: this.GameInstance.CommandManager, + commandManager: this.CommandManager, reloadTranslations: this.ReloadTranslations, - handleInput: input => this.GameInstance.CommandQueue.Enqueue(input), + handleInput: input => this.CommandQueue.Enqueue(input), continueWhile: () => this.IsGameRunning && !this.CancellationToken.IsCancellationRequested ) ).Start(); } + /// Raised after the game finishes loading its initial content. + private void OnLoadContent() + { + // override map display device + Game1.mapDisplayDevice = new SDisplayDevice(Game1.content, Game1.game1.GraphicsDevice); + + // log GPU info +#if SMAPI_FOR_WINDOWS + this.Monitor.Log($"Running on GPU: {Game1.game1.GraphicsDevice?.Adapter?.Description ?? ""}"); +#endif + } + + /// Raised when the game is updating its state (roughly 60 times per second). + /// A snapshot of the game timing state. + /// Invoke the game's update logic. + private void OnGameUpdating(GameTime gameTime, Action runGameUpdate) + { + var events = this.EventManager; + + try + { + SCore.DeprecationManager.PrintQueued(); + SCore.PerformanceMonitor.PrintQueuedAlerts(); + + /********* + ** First-tick initialization + *********/ + if (!this.IsInitialized) + { + this.IsInitialized = true; + this.OnGameInitialized(); + } + + /********* + ** Update input + *********/ + // This should *always* run, even when suppressing mod events, since the game uses + // this too. For example, doing this after mod event suppression would prevent the + // user from doing anything on the overnight shipping screen. + SInputState inputState = this.Input; + if (this.GameInstance.IsActive) + inputState.TrueUpdate(); + + /********* + ** Special cases + *********/ + // Abort if SMAPI is exiting. + if (this.CancellationToken.IsCancellationRequested) + { + this.Monitor.Log("SMAPI shutting down: aborting update."); + return; + } + + // Run async tasks synchronously to avoid issues due to mod events triggering + // concurrently with game code. + bool saveParsed = false; + if (Game1.currentLoader != null) + { + this.Monitor.Log("Game loader synchronizing..."); + while (Game1.currentLoader?.MoveNext() == true) + { + // raise load stage changed + switch (Game1.currentLoader.Current) + { + case 20 when (!saveParsed && SaveGame.loaded != null): + saveParsed = true; + this.OnLoadStageChanged(LoadStage.SaveParsed); + break; + + case 36: + this.OnLoadStageChanged(LoadStage.SaveLoadedBasicInfo); + break; + + case 50: + this.OnLoadStageChanged(LoadStage.SaveLoadedLocations); + break; + + default: + if (Game1.gameMode == Game1.playingGameMode) + this.OnLoadStageChanged(LoadStage.Preloaded); + break; + } + } + + Game1.currentLoader = null; + this.Monitor.Log("Game loader done."); + } + if (SGame.NewDayTask?.Status == TaskStatus.Created) + { + this.Monitor.Log("New day task synchronizing..."); + SGame.NewDayTask.RunSynchronously(); + this.Monitor.Log("New day task done."); + } + + // While a background task is in progress, the game may make changes to the game + // state while mods are running their code. This is risky, because data changes can + // conflict (e.g. collection changed during enumeration errors) and data may change + // unexpectedly from one mod instruction to the next. + // + // Therefore we can just run Game1.Update here without raising any SMAPI events. There's + // a small chance that the task will finish after we defer but before the game checks, + // which means technically events should be raised, but the effects of missing one + // update tick are negligible and not worth the complications of bypassing Game1.Update. + if (SGame.NewDayTask != null || Game1.gameMode == Game1.loadingMode) + { + events.UnvalidatedUpdateTicking.RaiseEmpty(); + SCore.TicksElapsed++; + runGameUpdate(); + events.UnvalidatedUpdateTicked.RaiseEmpty(); + return; + } + + // Raise minimal events while saving. + // While the game is writing to the save file in the background, mods can unexpectedly + // fail since they don't have exclusive access to resources (e.g. collection changed + // during enumeration errors). To avoid problems, events are not invoked while a save + // is in progress. It's safe to raise SaveEvents.BeforeSave as soon as the menu is + // opened (since the save hasn't started yet), but all other events should be suppressed. + if (Context.IsSaving) + { + // raise before-create + if (!Context.IsWorldReady && !this.IsBetweenCreateEvents) + { + this.IsBetweenCreateEvents = true; + this.Monitor.Log("Context: before save creation."); + events.SaveCreating.RaiseEmpty(); + } + + // raise before-save + if (Context.IsWorldReady && !this.IsBetweenSaveEvents) + { + this.IsBetweenSaveEvents = true; + this.Monitor.Log("Context: before save."); + events.Saving.RaiseEmpty(); + } + + // suppress non-save events + events.UnvalidatedUpdateTicking.RaiseEmpty(); + SCore.TicksElapsed++; + runGameUpdate(); + events.UnvalidatedUpdateTicked.RaiseEmpty(); + return; + } + + /********* + ** Reload assets when interceptors are added/removed + *********/ + if (this.ReloadAssetInterceptorsQueue.Any()) + { + // get unique interceptors + AssetInterceptorChange[] interceptors = this.ReloadAssetInterceptorsQueue + .GroupBy(p => p.Instance, new ObjectReferenceComparer()) + .Select(p => p.First()) + .ToArray(); + this.ReloadAssetInterceptorsQueue.Clear(); + + // log summary + this.Monitor.Log("Invalidating cached assets for new editors & loaders..."); + this.Monitor.Log( + " changed: " + + string.Join(", ", + interceptors + .GroupBy(p => p.Mod) + .OrderBy(p => p.Key.DisplayName) + .Select(modGroup => + $"{modGroup.Key.DisplayName} (" + + string.Join(", ", modGroup.GroupBy(p => p.WasAdded).ToDictionary(p => p.Key, p => p.Count()).Select(p => $"{(p.Key ? "added" : "removed")} {p.Value}")) + + ")" + ) + ) + ); + + // reload affected assets + this.ContentCore.InvalidateCache(asset => interceptors.Any(p => p.CanIntercept(asset))); + } + + /********* + ** Execute commands + *********/ + while (this.CommandQueue.TryDequeue(out string rawInput)) + { + // parse command + string name; + string[] args; + Command command; + try + { + if (!this.CommandManager.TryParse(rawInput, out name, out args, out command)) + { + this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); + continue; + } + } + catch (Exception ex) + { + this.Monitor.Log($"Failed parsing that command:\n{ex.GetLogSummary()}", LogLevel.Error); + continue; + } + + // execute command + try + { + command.Callback.Invoke(name, args); + } + catch (Exception ex) + { + if (command.Mod != null) + command.Mod.LogAsMod($"Mod failed handling that command:\n{ex.GetLogSummary()}", LogLevel.Error); + else + this.Monitor.Log($"Failed handling that command:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + + /********* + ** Update context + *********/ + bool wasWorldReady = Context.IsWorldReady; + if ((Context.IsWorldReady && !Context.IsSaveLoaded) || Game1.exitToTitle) + { + Context.IsWorldReady = false; + this.AfterLoadTimer.Reset(); + } + else if (Context.IsSaveLoaded && this.AfterLoadTimer.Current > 0 && Game1.currentLocation != null) + { + if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialized yet) + this.AfterLoadTimer.Decrement(); + Context.IsWorldReady = this.AfterLoadTimer.Current == 0; + } + + /********* + ** Update watchers + ** (Watchers need to be updated, checked, and reset in one go so we can detect any changes mods make in event handlers.) + *********/ + this.Watchers.Update(); + this.WatcherSnapshot.Update(this.Watchers); + this.Watchers.Reset(); + WatcherSnapshot state = this.WatcherSnapshot; + + /********* + ** Display in-game warnings + *********/ + // save content removed + if (this.IsSaveContentRemoved && Context.IsWorldReady) + { + this.IsSaveContentRemoved = false; + Game1.addHUDMessage(new HUDMessage(this.Translator.Get("warn.invalid-content-removed"), HUDMessage.error_type)); + } + + /********* + ** Pre-update events + *********/ + { + /********* + ** Save created/loaded events + *********/ + if (this.IsBetweenCreateEvents) + { + // raise after-create + this.IsBetweenCreateEvents = false; + this.Monitor.Log($"Context: after save creation, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}."); + this.OnLoadStageChanged(LoadStage.CreatedSaveFile); + events.SaveCreated.RaiseEmpty(); + } + if (this.IsBetweenSaveEvents) + { + // raise after-save + this.IsBetweenSaveEvents = false; + this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}."); + events.Saved.RaiseEmpty(); + events.DayStarted.RaiseEmpty(); + } + + /********* + ** Locale changed events + *********/ + if (state.Locale.IsChanged) + this.Monitor.Log($"Context: locale set to {state.Locale.New}."); + + /********* + ** Load / return-to-title events + *********/ + if (wasWorldReady && !Context.IsWorldReady) + this.OnLoadStageChanged(LoadStage.None); + else if (Context.IsWorldReady && Context.LoadStage != LoadStage.Ready) + { + // print context + string context = $"Context: loaded save '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}, locale set to {this.ContentCore.Language}."; + if (Context.IsMultiplayer) + { + int onlineCount = Game1.getOnlineFarmers().Count(); + context += $" {(Context.IsMainPlayer ? "Main player" : "Farmhand")} with {onlineCount} {(onlineCount == 1 ? "player" : "players")} online."; + } + else + context += " Single-player."; + this.Monitor.Log(context); + + // raise events + this.OnLoadStageChanged(LoadStage.Ready); + events.SaveLoaded.RaiseEmpty(); + events.DayStarted.RaiseEmpty(); + } + + /********* + ** Window events + *********/ + // Here we depend on the game's viewport instead of listening to the Window.Resize + // event because we need to notify mods after the game handles the resize, so the + // game's metadata (like Game1.viewport) are updated. That's a bit complicated + // since the game adds & removes its own handler on the fly. + if (state.WindowSize.IsChanged) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: window size changed to {state.WindowSize.New}."); + + events.WindowResized.Raise(new WindowResizedEventArgs(state.WindowSize.Old, state.WindowSize.New)); + } + + /********* + ** Input events (if window has focus) + *********/ + if (this.GameInstance.IsActive) + { + // raise events + bool isChatInput = Game1.IsChatting || (Context.IsMultiplayer && Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.currentMinigame == null && inputState.IsAnyDown(Game1.options.chatButton)); + if (!isChatInput) + { + ICursorPosition cursor = this.Input.CursorPosition; + + // raise cursor moved event + if (state.Cursor.IsChanged) + events.CursorMoved.Raise(new CursorMovedEventArgs(state.Cursor.Old, state.Cursor.New)); + + // raise mouse wheel scrolled + if (state.MouseWheelScroll.IsChanged) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: mouse wheel scrolled to {state.MouseWheelScroll.New}."); + events.MouseWheelScrolled.Raise(new MouseWheelScrolledEventArgs(cursor, state.MouseWheelScroll.Old, state.MouseWheelScroll.New)); + } + + // raise input button events + foreach (var pair in inputState.LastButtonStates) + { + SButton button = pair.Key; + SButtonState status = pair.Value; + + if (status == SButtonState.Pressed) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: button {button} pressed."); + + events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState)); + } + else if (status == SButtonState.Released) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: button {button} released."); + + events.ButtonReleased.Raise(new ButtonReleasedEventArgs(button, cursor, inputState)); + } + } + } + } + + /********* + ** Menu events + *********/ + if (state.ActiveMenu.IsChanged) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Context: menu changed from {state.ActiveMenu.Old?.GetType().FullName ?? "none"} to {state.ActiveMenu.New?.GetType().FullName ?? "none"}."); + + // raise menu events + events.MenuChanged.Raise(new MenuChangedEventArgs(state.ActiveMenu.Old, state.ActiveMenu.New)); + } + + /********* + ** World & player events + *********/ + if (Context.IsWorldReady) + { + bool raiseWorldEvents = !state.SaveID.IsChanged; // don't report changes from unloaded => loaded + + // location list changes + if (state.Locations.LocationList.IsChanged && (events.LocationListChanged.HasListeners() || this.Monitor.IsVerbose)) + { + var added = state.Locations.LocationList.Added.ToArray(); + var removed = state.Locations.LocationList.Removed.ToArray(); + + if (this.Monitor.IsVerbose) + { + string addedText = added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none"; + string removedText = removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none"; + this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText})."); + } + + events.LocationListChanged.Raise(new LocationListChangedEventArgs(added, removed)); + } + + // raise location contents changed + if (raiseWorldEvents) + { + foreach (LocationSnapshot locState in state.Locations.Locations) + { + var location = locState.Location; + + // buildings changed + if (locState.Buildings.IsChanged) + events.BuildingListChanged.Raise(new BuildingListChangedEventArgs(location, locState.Buildings.Added, locState.Buildings.Removed)); + + // debris changed + if (locState.Debris.IsChanged) + events.DebrisListChanged.Raise(new DebrisListChangedEventArgs(location, locState.Debris.Added, locState.Debris.Removed)); + + // large terrain features changed + if (locState.LargeTerrainFeatures.IsChanged) + events.LargeTerrainFeatureListChanged.Raise(new LargeTerrainFeatureListChangedEventArgs(location, locState.LargeTerrainFeatures.Added, locState.LargeTerrainFeatures.Removed)); + + // NPCs changed + if (locState.Npcs.IsChanged) + events.NpcListChanged.Raise(new NpcListChangedEventArgs(location, locState.Npcs.Added, locState.Npcs.Removed)); + + // objects changed + if (locState.Objects.IsChanged) + events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, locState.Objects.Added, locState.Objects.Removed)); + + // chest items changed + if (events.ChestInventoryChanged.HasListeners()) + { + foreach (var pair in locState.ChestItems) + { + SnapshotItemListDiff diff = pair.Value; + events.ChestInventoryChanged.Raise(new ChestInventoryChangedEventArgs(pair.Key, location, added: diff.Added, removed: diff.Removed, quantityChanged: diff.QuantityChanged)); + } + } + + // terrain features changed + if (locState.TerrainFeatures.IsChanged) + events.TerrainFeatureListChanged.Raise(new TerrainFeatureListChangedEventArgs(location, locState.TerrainFeatures.Added, locState.TerrainFeatures.Removed)); + } + } + + // raise time changed + if (raiseWorldEvents && state.Time.IsChanged) + events.TimeChanged.Raise(new TimeChangedEventArgs(state.Time.Old, state.Time.New)); + + // raise player events + if (raiseWorldEvents) + { + PlayerSnapshot playerState = state.CurrentPlayer; + Farmer player = playerState.Player; + + // raise current location changed + if (playerState.Location.IsChanged) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Context: set location to {playerState.Location.New}."); + + events.Warped.Raise(new WarpedEventArgs(player, playerState.Location.Old, playerState.Location.New)); + } + + // raise player leveled up a skill + foreach (var pair in playerState.Skills) + { + if (!pair.Value.IsChanged) + continue; + + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.Old} to {pair.Value.New}."); + + events.LevelChanged.Raise(new LevelChangedEventArgs(player, pair.Key, pair.Value.Old, pair.Value.New)); + } + + // raise player inventory changed + if (playerState.Inventory.IsChanged) + { + var inventory = playerState.Inventory; + + if (this.Monitor.IsVerbose) + this.Monitor.Log("Events: player inventory changed."); + events.InventoryChanged.Raise(new InventoryChangedEventArgs(player, added: inventory.Added, removed: inventory.Removed, quantityChanged: inventory.QuantityChanged)); + } + } + } + + /********* + ** Game update + *********/ + // game launched + bool isFirstTick = SCore.TicksElapsed == 0; + if (isFirstTick) + { + Context.IsGameLaunched = true; + events.GameLaunched.Raise(new GameLaunchedEventArgs()); + } + + // preloaded + if (Context.IsSaveLoaded && Context.LoadStage != LoadStage.Loaded && Context.LoadStage != LoadStage.Ready && Game1.dayOfMonth != 0) + this.OnLoadStageChanged(LoadStage.Loaded); + } + + /********* + ** Game update tick + *********/ + { + bool isOneSecond = SCore.TicksElapsed % 60 == 0; + events.UnvalidatedUpdateTicking.RaiseEmpty(); + events.UpdateTicking.RaiseEmpty(); + if (isOneSecond) + events.OneSecondUpdateTicking.RaiseEmpty(); + try + { + this.Input.ApplyOverrides(); // if mods added any new overrides since the update, process them now + SCore.TicksElapsed++; + runGameUpdate(); + } + catch (Exception ex) + { + this.LogManager.MonitorForGame.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); + } + + events.UnvalidatedUpdateTicked.RaiseEmpty(); + events.UpdateTicked.RaiseEmpty(); + if (isOneSecond) + events.OneSecondUpdateTicked.RaiseEmpty(); + } + + /********* + ** Update events + *********/ + this.UpdateCrashTimer.Reset(); + } + catch (Exception ex) + { + // log error + this.Monitor.Log($"An error occured in the overridden update loop: {ex.GetLogSummary()}", LogLevel.Error); + + // exit if irrecoverable + if (!this.UpdateCrashTimer.Decrement()) + this.ExitGameImmediately("The game crashed when updating, and SMAPI was unable to recover the game."); + } + } + /// Handle the game changing locale. private void OnLocaleChanged() { @@ -380,6 +997,96 @@ namespace StardewModdingAPI.Framework mod.Translations.SetLocale(locale, languageCode); } + /// Raised when the low-level stage while loading a save changes. + /// The new load stage. + internal void OnLoadStageChanged(LoadStage newStage) + { + // nothing to do + if (newStage == Context.LoadStage) + return; + + // update data + LoadStage oldStage = Context.LoadStage; + Context.LoadStage = newStage; + this.Monitor.VerboseLog($"Context: load stage changed to {newStage}"); + if (newStage == LoadStage.None) + { + this.Monitor.Log("Context: returned to title"); + this.OnReturnedToTitle(); + } + + // raise events + this.EventManager.LoadStageChanged.Raise(new LoadStageChangedEventArgs(oldStage, newStage)); + if (newStage == LoadStage.None) + this.EventManager.ReturnedToTitle.RaiseEmpty(); + } + + /// Raised after custom content is removed from the save data to avoid a crash. + internal void OnSaveContentRemoved() + { + this.IsSaveContentRemoved = true; + } + + /// A callback invoked before runs. + protected void OnNewDayAfterFade() + { + this.EventManager.DayEnding.RaiseEmpty(); + } + + /// Raised after the player returns to the title screen. + private void OnReturnedToTitle() + { + // perform cleanup + this.Multiplayer.CleanupOnMultiplayerExit(); + if (!(Game1.mapDisplayDevice is SDisplayDevice)) + Game1.mapDisplayDevice = new SDisplayDevice(Game1.content, Game1.game1.GraphicsDevice); + } + + /// Raised before the game exits. + private void OnGameExiting() + { + this.Multiplayer.Disconnect(StardewValley.Multiplayer.DisconnectType.ClosedGame); + this.Dispose(); + } + + /// Raised when a mod network message is received. + /// The message to deliver to applicable mods. + private void OnModMessageReceived(ModMessageModel message) + { + // get mod IDs to notify + HashSet modIDs = new HashSet(message.ToModIDs ?? this.ModRegistry.GetAll().Select(p => p.Manifest.UniqueID), StringComparer.OrdinalIgnoreCase); + if (message.FromPlayerID == Game1.player?.UniqueMultiplayerID) + modIDs.Remove(message.FromModID); // don't send a broadcast back to the sender + + // raise events + this.EventManager.ModMessageReceived.Raise(new ModMessageReceivedEventArgs(message), mod => mod != null && modIDs.Contains(mod.Manifest.UniqueID)); + } + + /// Constructor a content manager to read game content files. + /// The service provider to use to locate services. + /// The root directory to search for content. + private LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) + { + // Game1._temporaryContent initializing from SGame constructor + // NOTE: this method is called before the SGame constructor runs. Don't depend on anything being initialized at this point. + if (this.ContentCore == null) + { + this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, this.Reflection, this.Toolkit.JsonHelper, this.InitializeBeforeFirstAssetLoaded); + this.NextContentManagerIsMain = true; + return this.ContentCore.CreateGameContentManager("Game1._temporaryContent"); + } + + // Game1.content initializing from LoadContent + if (this.NextContentManagerIsMain) + { + this.NextContentManagerIsMain = false; + return this.ContentCore.MainContentManager; + } + + // any other content manager + return this.ContentCore.CreateGameContentManager("(generated)"); + } + /// 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() @@ -635,8 +1342,8 @@ namespace StardewModdingAPI.Framework this.ContentCore.Loaders.Add(new ModLinked(metadata, loader)); // ReSharper restore SuspiciousTypeConversion.Global - helper.ObservableAssetEditors.CollectionChanged += (sender, e) => this.OnInterceptorsChanged(metadata, e.NewItems?.Cast(), e.OldItems?.Cast(), this.ContentCore.Editors); - helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => this.OnInterceptorsChanged(metadata, e.NewItems?.Cast(), e.OldItems?.Cast(), this.ContentCore.Loaders); + helper.ObservableAssetEditors.CollectionChanged += (sender, e) => this.OnAssetInterceptorsChanged(metadata, e.NewItems?.Cast(), e.OldItems?.Cast(), this.ContentCore.Editors); + helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => this.OnAssetInterceptorsChanged(metadata, e.NewItems?.Cast(), e.OldItems?.Cast(), this.ContentCore.Loaders); } // call entry method @@ -670,34 +1377,27 @@ namespace StardewModdingAPI.Framework } } - // invalidate cache entries when needed - // (These listeners are registered after Entry to avoid repeatedly reloading assets as mods initialize.) - foreach (IModMetadata metadata in loadedMods) - { - if (metadata.Mod.Helper.Content is ContentHelper helper) - { - helper.ObservableAssetEditors.CollectionChanged += (sender, e) => this.GameInstance.OnAssetInterceptorsChanged(metadata, e.NewItems, e.OldItems); - helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => this.GameInstance.OnAssetInterceptorsChanged(metadata, e.NewItems, e.OldItems); - } - } - // unlock mod integrations this.ModRegistry.AreAllModsInitialized = true; } - /// Handle a mod adding or removing asset interceptors. + /// Raised after a mod adds or removes asset interceptors. /// The asset interceptor type (one of or ). /// The mod metadata. /// The interceptors that were added. /// The interceptors that were removed. - /// The list to update. - private void OnInterceptorsChanged(IModMetadata mod, IEnumerable added, IEnumerable removed, IList> list) + /// A list of interceptors to update for the change. + private void OnAssetInterceptorsChanged(IModMetadata mod, IEnumerable added, IEnumerable removed, IList> list) { foreach (T interceptor in added ?? new T[0]) + { + this.ReloadAssetInterceptorsQueue.Add(new AssetInterceptorChange(mod, interceptor, wasAdded: true)); list.Add(new ModLinked(mod, interceptor)); + } foreach (T interceptor in removed ?? new T[0]) { + this.ReloadAssetInterceptorsQueue.Add(new AssetInterceptorChange(mod, interceptor, wasAdded: false)); foreach (ModLinked entry in list.Where(p => p.Mod == mod && object.ReferenceEquals(p.Data, interceptor)).ToArray()) list.Remove(entry); } @@ -841,15 +1541,15 @@ namespace StardewModdingAPI.Framework } IModEvents events = new ModEvents(mod, this.EventManager); - ICommandHelper commandHelper = new CommandHelper(mod, this.GameInstance.CommandManager); + ICommandHelper commandHelper = new CommandHelper(mod, this.CommandManager); IContentHelper contentHelper = new ContentHelper(contentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor); IContentPackHelper contentPackHelper = new ContentPackHelper(manifest.UniqueID, new Lazy(GetContentPacks), CreateFakeContentPack); IDataHelper dataHelper = new DataHelper(manifest.UniqueID, mod.DirectoryPath, jsonHelper); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, mod.DisplayName, this.Reflection); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); - IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer); + IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.Multiplayer); - modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, this.GameInstance.Input, events, contentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper); + modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, this.Input, events, contentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper); } // init mod @@ -969,7 +1669,6 @@ namespace StardewModdingAPI.Framework catch (Exception ex) { errors.Add($"{file.Name} file couldn't be parsed: {ex.GetLogSummary()}"); - continue; } } } @@ -1045,5 +1744,13 @@ namespace StardewModdingAPI.Framework } } } + + /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. + /// The fatal log message. + private void ExitGameImmediately(string message) + { + this.Monitor.LogFatal(message); + this.CancellationToken.Cancel(); + } } } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index abb766f2..8efc8996 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -1,29 +1,15 @@ using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Text; -using System.Threading; using System.Threading.Tasks; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Netcode; -using StardewModdingAPI.Enums; -using StardewModdingAPI.Events; -using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; -using StardewModdingAPI.Framework.Networking; -using StardewModdingAPI.Framework.PerformanceMonitoring; using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.Rendering; -using StardewModdingAPI.Framework.StateTracking.Comparers; -using StardewModdingAPI.Framework.StateTracking.Snapshots; using StardewModdingAPI.Framework.Utilities; -using StardewModdingAPI.Toolkit.Serialization; using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Events; @@ -42,107 +28,46 @@ namespace StardewModdingAPI.Framework /********* ** Fields *********/ - /**** - ** SMAPI state - ****/ /// Encapsulates monitoring and logging for SMAPI. private readonly Monitor Monitor; - /// Encapsulates monitoring and logging on the game's behalf. - private readonly IMonitor MonitorForGame; - /// Manages SMAPI events for mods. private readonly EventManager Events; - /// Tracks the installed mods. - private readonly ModRegistry ModRegistry; - - /// Manages deprecation warnings. - private readonly DeprecationManager DeprecationManager; - - /// Tracks performance metrics. - private readonly PerformanceMonitor PerformanceMonitor; - /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second - /// The maximum number of consecutive attempts SMAPI should make to recover from an update error. - private readonly Countdown UpdateCrashTimer = new Countdown(60); // 60 ticks = roughly one second - - /// The number of ticks until SMAPI should notify mods that the game has loaded. - /// Skipping a few frames ensures the game finishes initializing the world before mods try to change it. - private readonly Countdown AfterLoadTimer = new Countdown(5); - - /// Whether custom content was removed from the save data to avoid a crash. - private bool IsSaveContentRemoved; - - /// Whether the game is saving and SMAPI has already raised . - private bool IsBetweenSaveEvents; - - /// Whether the game is creating the save file and SMAPI has already raised . - private bool IsBetweenCreateEvents; - - /// A callback to invoke the first time *any* game content manager loads an asset. - private readonly Action OnLoadingFirstAsset; - - /// A callback to invoke after the game finishes initializing. - private readonly Action OnGameInitialized; - - /// A callback to invoke when the game exits. - private readonly Action OnGameExiting; - /// Simplifies access to private game code. private readonly Reflector Reflection; - /// Encapsulates access to SMAPI core translations. - private readonly Translator Translator; - - /// Propagates notification that SMAPI should exit. - private readonly CancellationTokenSource CancellationToken; - - /**** - ** Game state - ****/ - /// Monitors the entire game state for changes. - private WatcherCore Watchers; - - /// A snapshot of the current state. - private readonly WatcherSnapshot WatcherSnapshot = new WatcherSnapshot(); - - /// Whether post-game-startup initialization has been performed. - private bool IsInitialized; - - /// Whether the next content manager requested by the game will be for . - private bool NextContentManagerIsMain; + /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. + private readonly Action ExitGameImmediately; /********* ** Accessors *********/ - /// Static state to use while is initializing, which happens before the constructor runs. - internal static SGameConstructorHack ConstructorHack { get; set; } - - /// The number of update ticks which have already executed. This is similar to , but incremented more consistently for every tick. - internal static uint TicksElapsed { get; private set; } + /// Manages input visible to the game. + public static SInputState Input => (SInputState)Game1.input; - /// SMAPI's content manager. - public ContentCoordinator ContentCore { get; private set; } + /// The game's core multiplayer utility. + public static SMultiplayer Multiplayer => (SMultiplayer)Game1.multiplayer; - /// Manages console commands. - public CommandManager CommandManager { get; } = new CommandManager(); + /// The game background task which initializes a new day. + public static Task NewDayTask => Game1._newDayTask; - /// Manages input visible to the game. - public SInputState Input => (SInputState)Game1.input; + /// Construct a content manager to read game content files. + /// This must be static because the game accesses it before the constructor is called. + public static Func CreateContentManagerImpl; - /// The game's core multiplayer utility. - public SMultiplayer Multiplayer => (SMultiplayer)Game1.multiplayer; + /// Raised after the game finishes loading its initial content. + public event Action OnGameContentLoaded; - /// A list of queued commands to execute. - /// This property must be threadsafe, since it's accessed from a separate console input thread. - public ConcurrentQueue CommandQueue { get; } = new ConcurrentQueue(); + /// Raised before the game exits. + public event Action OnGameExiting; - /// Asset interceptors added or removed since the last tick. - private readonly List ReloadAssetInterceptorsQueue = new List(); + /// Raised when the game is updating its state (roughly 60 times per second). + public event Action OnGameUpdating; /********* @@ -150,74 +75,35 @@ namespace StardewModdingAPI.Framework *********/ /// Construct an instance. /// Encapsulates monitoring and logging for SMAPI. - /// Encapsulates monitoring and logging on the game's behalf. /// Simplifies access to private game code. - /// Encapsulates access to arbitrary translations. /// Manages SMAPI events for mods. - /// Encapsulates SMAPI's JSON file parsing. - /// Tracks the installed mods. - /// Manages deprecation warnings. - /// Tracks performance metrics. - /// A callback to invoke after the game finishes initializing. - /// A callback to invoke when the game exits. - /// Propagates notification that SMAPI should exit. - /// Whether to log network traffic. - internal SGame(Monitor monitor, IMonitor monitorForGame, Reflector reflection, Translator translator, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, PerformanceMonitor performanceMonitor, Action onGameInitialized, Action onGameExiting, CancellationTokenSource cancellationToken, bool logNetworkTraffic) + /// Handles mod hooks provided by the game. + /// The core multiplayer logic. + /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. + internal SGame(Monitor monitor, Reflector reflection, EventManager eventManager, SModHooks modHooks, SMultiplayer multiplayer, Action exitGameImmediately) { - this.OnLoadingFirstAsset = SGame.ConstructorHack.OnLoadingFirstAsset; - SGame.ConstructorHack = null; - - // check expectations - if (this.ContentCore == null) - throw new InvalidOperationException($"The game didn't initialize its first content manager before SMAPI's {nameof(SGame)} constructor. This indicates an incompatible lifecycle change."); - // init XNA Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; + // hook into game + Game1.input = new SInputState(); + Game1.multiplayer = multiplayer; + Game1.hooks = modHooks; + Game1.locations = new ObservableCollection(); + // init SMAPI this.Monitor = monitor; - this.MonitorForGame = monitorForGame; this.Events = eventManager; - this.ModRegistry = modRegistry; this.Reflection = reflection; - this.Translator = translator; - this.DeprecationManager = deprecationManager; - this.PerformanceMonitor = performanceMonitor; - this.OnGameInitialized = onGameInitialized; - this.OnGameExiting = onGameExiting; - Game1.input = new SInputState(); - Game1.multiplayer = new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.OnModMessageReceived, logNetworkTraffic); - Game1.hooks = new SModHooks(this.OnNewDayAfterFade); - this.CancellationToken = cancellationToken; - - // init observables - Game1.locations = new ObservableCollection(); + this.ExitGameImmediately = exitGameImmediately; } /// Load content when the game is launched. protected override void LoadContent() { - // load content base.LoadContent(); - Game1.mapDisplayDevice = new SDisplayDevice(Game1.content, this.GraphicsDevice); - // log GPU info -#if SMAPI_FOR_WINDOWS - this.Monitor.Log($"Running on GPU: {this.GraphicsDevice?.Adapter?.Description ?? ""}"); -#endif - } - - /// Initialize just before the game's first update tick. - private void InitializeAfterGameStarted() - { - // set initial state - this.Input.TrueUpdate(); - - // init watchers - this.Watchers = new WatcherCore(this.Input); - - // raise callback - this.OnGameInitialized(); + this.OnGameContentLoaded?.Invoke(); } /// Perform cleanup logic when the game exits. @@ -226,639 +112,25 @@ namespace StardewModdingAPI.Framework /// This overrides the logic in to let SMAPI clean up before exit. protected override void OnExiting(object sender, EventArgs args) { - Game1.multiplayer.Disconnect(StardewValley.Multiplayer.DisconnectType.ClosedGame); this.OnGameExiting?.Invoke(); } - /// A callback invoked before runs. - protected void OnNewDayAfterFade() - { - this.Events.DayEnding.RaiseEmpty(); - } - - /// A callback invoked when a mod message is received. - /// The message to deliver to applicable mods. - private void OnModMessageReceived(ModMessageModel message) - { - // get mod IDs to notify - HashSet modIDs = new HashSet(message.ToModIDs ?? this.ModRegistry.GetAll().Select(p => p.Manifest.UniqueID), StringComparer.OrdinalIgnoreCase); - if (message.FromPlayerID == Game1.player?.UniqueMultiplayerID) - modIDs.Remove(message.FromModID); // don't send a broadcast back to the sender - - // raise events - this.Events.ModMessageReceived.Raise(new ModMessageReceivedEventArgs(message), mod => mod != null && modIDs.Contains(mod.Manifest.UniqueID)); - } - - /// A callback invoked when custom content is removed from the save data to avoid a crash. - internal void OnSaveContentRemoved() - { - this.IsSaveContentRemoved = true; - } - - /// A callback invoked when the game's low-level load stage changes. - /// The new load stage. - internal void OnLoadStageChanged(LoadStage newStage) - { - // nothing to do - if (newStage == Context.LoadStage) - return; - - // update data - LoadStage oldStage = Context.LoadStage; - Context.LoadStage = newStage; - this.Monitor.VerboseLog($"Context: load stage changed to {newStage}"); - if (newStage == LoadStage.None) - { - this.Monitor.Log("Context: returned to title", LogLevel.Trace); - this.OnReturnedToTitle(); - } - - // raise events - this.Events.LoadStageChanged.Raise(new LoadStageChangedEventArgs(oldStage, newStage)); - if (newStage == LoadStage.None) - this.Events.ReturnedToTitle.RaiseEmpty(); - } - - /// A callback invoked when a mod adds or removes an asset interceptor. - /// The mod which added or removed interceptors. - /// The added interceptors. - /// The removed interceptors. - internal void OnAssetInterceptorsChanged(IModMetadata mod, IEnumerable added, IEnumerable removed) - { - if (added != null) - { - foreach (object instance in added) - this.ReloadAssetInterceptorsQueue.Add(new AssetInterceptorChange(mod, instance, wasAdded: true)); - } - if (removed != null) - { - foreach (object instance in removed) - this.ReloadAssetInterceptorsQueue.Add(new AssetInterceptorChange(mod, instance, wasAdded: false)); - } - } - - /// Perform cleanup when the game returns to the title screen. - private void OnReturnedToTitle() - { - this.Multiplayer.CleanupOnMultiplayerExit(); - - if (!(Game1.mapDisplayDevice is SDisplayDevice)) - Game1.mapDisplayDevice = new SDisplayDevice(Game1.content, this.GraphicsDevice); - } - - /// Constructor a content manager to read XNB files. + /// Construct a content manager to read game content files. /// The service provider to use to locate services. /// The root directory to search for content. protected override LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) { - // Game1._temporaryContent initializing from SGame constructor - // NOTE: this method is called before the SGame constructor runs. Don't depend on anything being initialized at this point. - if (this.ContentCore == null) - { - this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.ConstructorHack.Monitor, SGame.ConstructorHack.Reflection, SGame.ConstructorHack.JsonHelper, this.OnLoadingFirstAsset ?? SGame.ConstructorHack?.OnLoadingFirstAsset); - this.NextContentManagerIsMain = true; - return this.ContentCore.CreateGameContentManager("Game1._temporaryContent"); - } + if (SGame.CreateContentManagerImpl == null) + throw new InvalidOperationException($"The {nameof(SGame)}.{nameof(SGame.CreateContentManagerImpl)} must be set."); - // Game1.content initializing from LoadContent - if (this.NextContentManagerIsMain) - { - this.NextContentManagerIsMain = false; - return this.ContentCore.MainContentManager; - } - - // any other content manager - return this.ContentCore.CreateGameContentManager("(generated)"); + return SGame.CreateContentManagerImpl(serviceProvider, rootDirectory); } - /// The method called when the game is updating its state. This happens roughly 60 times per second. + /// The method called when the game is updating its state (roughly 60 times per second). /// A snapshot of the game timing state. protected override void Update(GameTime gameTime) { - var events = this.Events; - - try - { - this.DeprecationManager.PrintQueued(); - this.PerformanceMonitor.PrintQueuedAlerts(); - - /********* - ** First-tick initialization - *********/ - if (!this.IsInitialized) - { - this.IsInitialized = true; - this.InitializeAfterGameStarted(); - } - - /********* - ** Update input - *********/ - // This should *always* run, even when suppressing mod events, since the game uses - // this too. For example, doing this after mod event suppression would prevent the - // user from doing anything on the overnight shipping screen. - SInputState inputState = this.Input; - if (this.IsActive) - inputState.TrueUpdate(); - - /********* - ** Special cases - *********/ - // Abort if SMAPI is exiting. - if (this.CancellationToken.IsCancellationRequested) - { - this.Monitor.Log("SMAPI shutting down: aborting update.", LogLevel.Trace); - return; - } - - // Run async tasks synchronously to avoid issues due to mod events triggering - // concurrently with game code. - bool saveParsed = false; - if (Game1.currentLoader != null) - { - this.Monitor.Log("Game loader synchronizing...", LogLevel.Trace); - while (Game1.currentLoader?.MoveNext() == true) - { - // raise load stage changed - switch (Game1.currentLoader.Current) - { - case 20 when (!saveParsed && SaveGame.loaded != null): - saveParsed = true; - this.OnLoadStageChanged(LoadStage.SaveParsed); - break; - - case 36: - this.OnLoadStageChanged(LoadStage.SaveLoadedBasicInfo); - break; - - case 50: - this.OnLoadStageChanged(LoadStage.SaveLoadedLocations); - break; - - default: - if (Game1.gameMode == Game1.playingGameMode) - this.OnLoadStageChanged(LoadStage.Preloaded); - break; - } - } - - Game1.currentLoader = null; - this.Monitor.Log("Game loader done.", LogLevel.Trace); - } - if (Game1._newDayTask?.Status == TaskStatus.Created) - { - this.Monitor.Log("New day task synchronizing...", LogLevel.Trace); - Game1._newDayTask.RunSynchronously(); - this.Monitor.Log("New day task done.", LogLevel.Trace); - } - - // While a background task is in progress, the game may make changes to the game - // state while mods are running their code. This is risky, because data changes can - // conflict (e.g. collection changed during enumeration errors) and data may change - // unexpectedly from one mod instruction to the next. - // - // Therefore we can just run Game1.Update here without raising any SMAPI events. There's - // a small chance that the task will finish after we defer but before the game checks, - // which means technically events should be raised, but the effects of missing one - // update tick are negligible and not worth the complications of bypassing Game1.Update. - if (Game1._newDayTask != null || Game1.gameMode == Game1.loadingMode) - { - events.UnvalidatedUpdateTicking.RaiseEmpty(); - SGame.TicksElapsed++; - base.Update(gameTime); - events.UnvalidatedUpdateTicked.RaiseEmpty(); - return; - } - - // Raise minimal events while saving. - // While the game is writing to the save file in the background, mods can unexpectedly - // fail since they don't have exclusive access to resources (e.g. collection changed - // during enumeration errors). To avoid problems, events are not invoked while a save - // is in progress. It's safe to raise SaveEvents.BeforeSave as soon as the menu is - // opened (since the save hasn't started yet), but all other events should be suppressed. - if (Context.IsSaving) - { - // raise before-create - if (!Context.IsWorldReady && !this.IsBetweenCreateEvents) - { - this.IsBetweenCreateEvents = true; - this.Monitor.Log("Context: before save creation.", LogLevel.Trace); - events.SaveCreating.RaiseEmpty(); - } - - // raise before-save - if (Context.IsWorldReady && !this.IsBetweenSaveEvents) - { - this.IsBetweenSaveEvents = true; - this.Monitor.Log("Context: before save.", LogLevel.Trace); - events.Saving.RaiseEmpty(); - } - - // suppress non-save events - events.UnvalidatedUpdateTicking.RaiseEmpty(); - SGame.TicksElapsed++; - base.Update(gameTime); - events.UnvalidatedUpdateTicked.RaiseEmpty(); - return; - } - - /********* - ** Reload assets when interceptors are added/removed - *********/ - if (this.ReloadAssetInterceptorsQueue.Any()) - { - // get unique interceptors - AssetInterceptorChange[] interceptors = this.ReloadAssetInterceptorsQueue - .GroupBy(p => p.Instance, new ObjectReferenceComparer()) - .Select(p => p.First()) - .ToArray(); - this.ReloadAssetInterceptorsQueue.Clear(); - - // log summary - this.Monitor.Log("Invalidating cached assets for new editors & loaders..."); - this.Monitor.Log( - " changed: " - + string.Join(", ", - interceptors - .GroupBy(p => p.Mod) - .OrderBy(p => p.Key.DisplayName) - .Select(modGroup => - $"{modGroup.Key.DisplayName} (" - + string.Join(", ", modGroup.GroupBy(p => p.WasAdded).ToDictionary(p => p.Key, p => p.Count()).Select(p => $"{(p.Key ? "added" : "removed")} {p.Value}")) - + ")" - ) - ) - ); - - // reload affected assets - this.ContentCore.InvalidateCache(asset => interceptors.Any(p => p.CanIntercept(asset))); - } - - /********* - ** Execute commands - *********/ - while (this.CommandQueue.TryDequeue(out string rawInput)) - { - // parse command - string name; - string[] args; - Command command; - try - { - if (!this.CommandManager.TryParse(rawInput, out name, out args, out command)) - { - this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); - continue; - } - } - catch (Exception ex) - { - this.Monitor.Log($"Failed parsing that command:\n{ex.GetLogSummary()}", LogLevel.Error); - continue; - } - - // execute command - try - { - command.Callback.Invoke(name, args); - } - catch (Exception ex) - { - if (command.Mod != null) - command.Mod.LogAsMod($"Mod failed handling that command:\n{ex.GetLogSummary()}", LogLevel.Error); - else - this.Monitor.Log($"Failed handling that command:\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - - /********* - ** Update context - *********/ - bool wasWorldReady = Context.IsWorldReady; - if ((Context.IsWorldReady && !Context.IsSaveLoaded) || Game1.exitToTitle) - { - Context.IsWorldReady = false; - this.AfterLoadTimer.Reset(); - } - else if (Context.IsSaveLoaded && this.AfterLoadTimer.Current > 0 && Game1.currentLocation != null) - { - if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialized yet) - this.AfterLoadTimer.Decrement(); - Context.IsWorldReady = this.AfterLoadTimer.Current == 0; - } - - /********* - ** Update watchers - ** (Watchers need to be updated, checked, and reset in one go so we can detect any changes mods make in event handlers.) - *********/ - this.Watchers.Update(); - this.WatcherSnapshot.Update(this.Watchers); - this.Watchers.Reset(); - WatcherSnapshot state = this.WatcherSnapshot; - - /********* - ** Display in-game warnings - *********/ - // save content removed - if (this.IsSaveContentRemoved && Context.IsWorldReady) - { - this.IsSaveContentRemoved = false; - Game1.addHUDMessage(new HUDMessage(this.Translator.Get("warn.invalid-content-removed"), HUDMessage.error_type)); - } - - /********* - ** Pre-update events - *********/ - { - /********* - ** Save created/loaded events - *********/ - if (this.IsBetweenCreateEvents) - { - // raise after-create - this.IsBetweenCreateEvents = false; - this.Monitor.Log($"Context: after save creation, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - this.OnLoadStageChanged(LoadStage.CreatedSaveFile); - events.SaveCreated.RaiseEmpty(); - } - if (this.IsBetweenSaveEvents) - { - // raise after-save - this.IsBetweenSaveEvents = false; - this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - events.Saved.RaiseEmpty(); - events.DayStarted.RaiseEmpty(); - } - - /********* - ** Locale changed events - *********/ - if (state.Locale.IsChanged) - this.Monitor.Log($"Context: locale set to {state.Locale.New}.", LogLevel.Trace); - - /********* - ** Load / return-to-title events - *********/ - if (wasWorldReady && !Context.IsWorldReady) - this.OnLoadStageChanged(LoadStage.None); - else if (Context.IsWorldReady && Context.LoadStage != LoadStage.Ready) - { - // print context - string context = $"Context: loaded save '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}, locale set to {this.ContentCore.Language}."; - if (Context.IsMultiplayer) - { - int onlineCount = Game1.getOnlineFarmers().Count(); - context += $" {(Context.IsMainPlayer ? "Main player" : "Farmhand")} with {onlineCount} {(onlineCount == 1 ? "player" : "players")} online."; - } - else - context += " Single-player."; - this.Monitor.Log(context, LogLevel.Trace); - - // raise events - this.OnLoadStageChanged(LoadStage.Ready); - events.SaveLoaded.RaiseEmpty(); - events.DayStarted.RaiseEmpty(); - } - - /********* - ** Window events - *********/ - // Here we depend on the game's viewport instead of listening to the Window.Resize - // event because we need to notify mods after the game handles the resize, so the - // game's metadata (like Game1.viewport) are updated. That's a bit complicated - // since the game adds & removes its own handler on the fly. - if (state.WindowSize.IsChanged) - { - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Events: window size changed to {state.WindowSize.New}.", LogLevel.Trace); - - events.WindowResized.Raise(new WindowResizedEventArgs(state.WindowSize.Old, state.WindowSize.New)); - } - - /********* - ** Input events (if window has focus) - *********/ - if (this.IsActive) - { - // raise events - bool isChatInput = Game1.IsChatting || (Context.IsMultiplayer && Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.currentMinigame == null && inputState.IsAnyDown(Game1.options.chatButton)); - if (!isChatInput) - { - ICursorPosition cursor = this.Input.CursorPosition; - - // raise cursor moved event - if (state.Cursor.IsChanged) - events.CursorMoved.Raise(new CursorMovedEventArgs(state.Cursor.Old, state.Cursor.New)); - - // raise mouse wheel scrolled - if (state.MouseWheelScroll.IsChanged) - { - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Events: mouse wheel scrolled to {state.MouseWheelScroll.New}.", LogLevel.Trace); - events.MouseWheelScrolled.Raise(new MouseWheelScrolledEventArgs(cursor, state.MouseWheelScroll.Old, state.MouseWheelScroll.New)); - } - - // raise input button events - foreach (var pair in inputState.LastButtonStates) - { - SButton button = pair.Key; - SButtonState status = pair.Value; - - if (status == SButtonState.Pressed) - { - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace); - - events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState)); - } - else if (status == SButtonState.Released) - { - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace); - - events.ButtonReleased.Raise(new ButtonReleasedEventArgs(button, cursor, inputState)); - } - } - } - } - - /********* - ** Menu events - *********/ - if (state.ActiveMenu.IsChanged) - { - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Context: menu changed from {state.ActiveMenu.Old?.GetType().FullName ?? "none"} to {state.ActiveMenu.New?.GetType().FullName ?? "none"}.", LogLevel.Trace); - - // raise menu events - events.MenuChanged.Raise(new MenuChangedEventArgs(state.ActiveMenu.Old, state.ActiveMenu.New)); - } - - /********* - ** World & player events - *********/ - if (Context.IsWorldReady) - { - bool raiseWorldEvents = !state.SaveID.IsChanged; // don't report changes from unloaded => loaded - - // location list changes - if (state.Locations.LocationList.IsChanged && (events.LocationListChanged.HasListeners() || this.Monitor.IsVerbose)) - { - var added = state.Locations.LocationList.Added.ToArray(); - var removed = state.Locations.LocationList.Removed.ToArray(); - - if (this.Monitor.IsVerbose) - { - string addedText = added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none"; - string removedText = removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none"; - this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace); - } - - events.LocationListChanged.Raise(new LocationListChangedEventArgs(added, removed)); - } - - // raise location contents changed - if (raiseWorldEvents) - { - foreach (LocationSnapshot locState in state.Locations.Locations) - { - var location = locState.Location; - - // buildings changed - if (locState.Buildings.IsChanged) - events.BuildingListChanged.Raise(new BuildingListChangedEventArgs(location, locState.Buildings.Added, locState.Buildings.Removed)); - - // debris changed - if (locState.Debris.IsChanged) - events.DebrisListChanged.Raise(new DebrisListChangedEventArgs(location, locState.Debris.Added, locState.Debris.Removed)); - - // large terrain features changed - if (locState.LargeTerrainFeatures.IsChanged) - events.LargeTerrainFeatureListChanged.Raise(new LargeTerrainFeatureListChangedEventArgs(location, locState.LargeTerrainFeatures.Added, locState.LargeTerrainFeatures.Removed)); - - // NPCs changed - if (locState.Npcs.IsChanged) - events.NpcListChanged.Raise(new NpcListChangedEventArgs(location, locState.Npcs.Added, locState.Npcs.Removed)); - - // objects changed - if (locState.Objects.IsChanged) - events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, locState.Objects.Added, locState.Objects.Removed)); - - // chest items changed - if (events.ChestInventoryChanged.HasListeners()) - { - foreach (var pair in locState.ChestItems) - { - SnapshotItemListDiff diff = pair.Value; - events.ChestInventoryChanged.Raise(new ChestInventoryChangedEventArgs(pair.Key, location, added: diff.Added, removed: diff.Removed, quantityChanged: diff.QuantityChanged)); - } - } - - // terrain features changed - if (locState.TerrainFeatures.IsChanged) - events.TerrainFeatureListChanged.Raise(new TerrainFeatureListChangedEventArgs(location, locState.TerrainFeatures.Added, locState.TerrainFeatures.Removed)); - } - } - - // raise time changed - if (raiseWorldEvents && state.Time.IsChanged) - events.TimeChanged.Raise(new TimeChangedEventArgs(state.Time.Old, state.Time.New)); - - // raise player events - if (raiseWorldEvents) - { - PlayerSnapshot playerState = state.CurrentPlayer; - Farmer player = playerState.Player; - - // raise current location changed - if (playerState.Location.IsChanged) - { - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Context: set location to {playerState.Location.New}.", LogLevel.Trace); - - events.Warped.Raise(new WarpedEventArgs(player, playerState.Location.Old, playerState.Location.New)); - } - - // raise player leveled up a skill - foreach (var pair in playerState.Skills) - { - if (!pair.Value.IsChanged) - continue; - - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.Old} to {pair.Value.New}.", LogLevel.Trace); - - events.LevelChanged.Raise(new LevelChangedEventArgs(player, pair.Key, pair.Value.Old, pair.Value.New)); - } - - // raise player inventory changed - if (playerState.Inventory.IsChanged) - { - var inventory = playerState.Inventory; - - if (this.Monitor.IsVerbose) - this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace); - events.InventoryChanged.Raise(new InventoryChangedEventArgs(player, added: inventory.Added, removed: inventory.Removed, quantityChanged: inventory.QuantityChanged)); - } - } - } - - /********* - ** Game update - *********/ - // game launched - bool isFirstTick = SGame.TicksElapsed == 0; - if (isFirstTick) - { - Context.IsGameLaunched = true; - events.GameLaunched.Raise(new GameLaunchedEventArgs()); - } - - // preloaded - if (Context.IsSaveLoaded && Context.LoadStage != LoadStage.Loaded && Context.LoadStage != LoadStage.Ready && Game1.dayOfMonth != 0) - this.OnLoadStageChanged(LoadStage.Loaded); - } - - /********* - ** Game update tick - *********/ - { - bool isOneSecond = SGame.TicksElapsed % 60 == 0; - events.UnvalidatedUpdateTicking.RaiseEmpty(); - events.UpdateTicking.RaiseEmpty(); - if (isOneSecond) - events.OneSecondUpdateTicking.RaiseEmpty(); - try - { - this.Input.ApplyOverrides(); // if mods added any new overrides since the update, process them now - SGame.TicksElapsed++; - base.Update(gameTime); - } - catch (Exception ex) - { - this.MonitorForGame.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); - } - - events.UnvalidatedUpdateTicked.RaiseEmpty(); - events.UpdateTicked.RaiseEmpty(); - if (isOneSecond) - events.OneSecondUpdateTicked.RaiseEmpty(); - } - - /********* - ** Update events - *********/ - this.UpdateCrashTimer.Reset(); - } - catch (Exception ex) - { - // log error - this.Monitor.Log($"An error occured in the overridden update loop: {ex.GetLogSummary()}", LogLevel.Error); - - // exit if irrecoverable - if (!this.UpdateCrashTimer.Decrement()) - this.ExitGameImmediately("The game crashed when updating, and SMAPI was unable to recover the game."); - } + this.OnGameUpdating?.Invoke(gameTime, () => base.Update(gameTime)); } /// The method called to draw everything to the screen. @@ -890,7 +162,7 @@ namespace StardewModdingAPI.Framework { if (Game1.spriteBatch.IsOpen(this.Reflection)) { - this.Monitor.Log("Recovering sprite batch from error...", LogLevel.Trace); + this.Monitor.Log("Recovering sprite batch from error..."); Game1.spriteBatch.End(); } } @@ -1316,7 +588,7 @@ namespace StardewModdingAPI.Framework } Game1.drawPlayerHeldObject(Game1.player); } - label_139: + label_139: if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) Game1.drawTool(Game1.player); if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) @@ -1604,13 +876,5 @@ namespace StardewModdingAPI.Framework } } } - - /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. - /// The fatal log message. - private void ExitGameImmediately(string message) - { - this.Monitor.LogFatal(message); - this.CancellationToken.Cancel(); - } } } diff --git a/src/SMAPI/Framework/SGameConstructorHack.cs b/src/SMAPI/Framework/SGameConstructorHack.cs deleted file mode 100644 index f70dec03..00000000 --- a/src/SMAPI/Framework/SGameConstructorHack.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Toolkit.Serialization; -using StardewValley; - -namespace StardewModdingAPI.Framework -{ - /// The static state to use while is initializing, which happens before the constructor runs. - internal class SGameConstructorHack - { - /********* - ** Accessors - *********/ - /// Encapsulates monitoring and logging. - public IMonitor Monitor { get; } - - /// Simplifies access to private game code. - public Reflector Reflection { get; } - - /// Encapsulates SMAPI's JSON file parsing. - public JsonHelper JsonHelper { get; } - - /// A callback to invoke the first time *any* game content manager loads an asset. - public Action OnLoadingFirstAsset { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// Encapsulates monitoring and logging. - /// Simplifies access to private game code. - /// Encapsulates SMAPI's JSON file parsing. - /// A callback to invoke the first time *any* game content manager loads an asset. - public SGameConstructorHack(IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset) - { - this.Monitor = monitor; - this.Reflection = reflection; - this.JsonHelper = jsonHelper; - this.OnLoadingFirstAsset = onLoadingFirstAsset; - } - } -} -- cgit From c37280222dac3409f78413d90c8e50f6d410b3d1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 23 Aug 2020 20:53:09 -0400 Subject: minor cleanup --- src/SMAPI.Installer/Enums/ScriptAction.cs | 2 +- src/SMAPI.Installer/InteractiveInstaller.cs | 3 +-- src/SMAPI.Installer/Program.cs | 2 -- src/SMAPI/Events/ChangeType.cs | 2 +- src/SMAPI/Events/WarpedEventArgs.cs | 1 - src/SMAPI/Framework/CommandManager.cs | 1 + src/SMAPI/Framework/ContentCoordinator.cs | 1 - src/SMAPI/Framework/ContentManagers/BaseContentManager.cs | 2 +- src/SMAPI/Framework/DeprecationLevel.cs | 2 +- src/SMAPI/Framework/DeprecationManager.cs | 1 + src/SMAPI/Framework/Events/ModGameLoopEvents.cs | 1 - src/SMAPI/Framework/InternalExtensions.cs | 4 ++-- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 1 - src/SMAPI/Framework/RequestExitDelegate.cs | 2 +- src/SMAPI/Framework/SCore.cs | 14 +++++++++----- .../Framework/StateTracking/Snapshots/PlayerSnapshot.cs | 1 - src/SMAPI/Framework/Utilities/Countdown.cs | 2 +- src/SMAPI/IReflectedField.cs | 2 +- src/SMAPI/IReflectedMethod.cs | 2 +- src/SMAPI/Program.cs | 1 + src/SMAPI/Utilities/SDate.cs | 1 - 21 files changed, 23 insertions(+), 25 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI.Installer/Enums/ScriptAction.cs b/src/SMAPI.Installer/Enums/ScriptAction.cs index e62b2a7c..27f649a6 100644 --- a/src/SMAPI.Installer/Enums/ScriptAction.cs +++ b/src/SMAPI.Installer/Enums/ScriptAction.cs @@ -9,4 +9,4 @@ /// Remove SMAPI from the game directory. Uninstall } -} \ No newline at end of file +} diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index dc96e2e8..d0ef0b8d 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -35,8 +35,6 @@ namespace StardewModdingApi.Installer "SMAPI.ConsoleCommands" }; - - /// Get the absolute file or folder paths to remove when uninstalling SMAPI. /// The folder for Stardew Valley and SMAPI. /// The folder for SMAPI mods. @@ -84,6 +82,7 @@ namespace StardewModdingApi.Installer foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories()) yield return Path.Combine(modDir.FullName, ".cache"); // 1.4–1.7 } + yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "ErrorLogs"); // remove old log files } diff --git a/src/SMAPI.Installer/Program.cs b/src/SMAPI.Installer/Program.cs index dc6c97f4..6c479621 100644 --- a/src/SMAPI.Installer/Program.cs +++ b/src/SMAPI.Installer/Program.cs @@ -3,8 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; using System.Reflection; -using StardewModdingAPI.Internal; -using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingApi.Installer { diff --git a/src/SMAPI/Events/ChangeType.cs b/src/SMAPI/Events/ChangeType.cs index 4b207f08..0fc717df 100644 --- a/src/SMAPI/Events/ChangeType.cs +++ b/src/SMAPI/Events/ChangeType.cs @@ -12,4 +12,4 @@ namespace StardewModdingAPI.Events /// The stack size changed. StackChange } -} \ No newline at end of file +} diff --git a/src/SMAPI/Events/WarpedEventArgs.cs b/src/SMAPI/Events/WarpedEventArgs.cs index 95c53ad9..9afe4a4e 100644 --- a/src/SMAPI/Events/WarpedEventArgs.cs +++ b/src/SMAPI/Events/WarpedEventArgs.cs @@ -22,7 +22,6 @@ namespace StardewModdingAPI.Events public bool IsLocalPlayer => this.Player.IsLocalPlayer; - /********* ** Public methods *********/ diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs index 2b91d394..4a99fd4d 100644 --- a/src/SMAPI/Framework/CommandManager.cs +++ b/src/SMAPI/Framework/CommandManager.cs @@ -119,6 +119,7 @@ namespace StardewModdingAPI.Framework command.Callback.Invoke(name, arguments); return true; } + return false; } diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 479ffa7f..d1021cad 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -9,7 +9,6 @@ using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.StateTracking.Comparers; using StardewModdingAPI.Metadata; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index a8de013a..6bc3a505 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -76,7 +76,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// A callback to invoke when the content manager is being disposed. /// Whether this content manager handles managed asset keys (e.g. to load assets from a mod folder). protected BaseContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing, bool isNamespaced) - : base(serviceProvider, rootDirectory, currentCulture) + : base(serviceProvider, rootDirectory, currentCulture) { // init this.Name = name; diff --git a/src/SMAPI/Framework/DeprecationLevel.cs b/src/SMAPI/Framework/DeprecationLevel.cs index c0044053..12b50952 100644 --- a/src/SMAPI/Framework/DeprecationLevel.cs +++ b/src/SMAPI/Framework/DeprecationLevel.cs @@ -12,4 +12,4 @@ namespace StardewModdingAPI.Framework /// The code will be removed soon. Deprecation messages should be warnings in the console. PendingRemoval } -} \ No newline at end of file +} diff --git a/src/SMAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs index 11fae0b2..94a2da85 100644 --- a/src/SMAPI/Framework/DeprecationManager.cs +++ b/src/SMAPI/Framework/DeprecationManager.cs @@ -107,6 +107,7 @@ namespace StardewModdingAPI.Framework } } } + this.QueuedWarnings.Clear(); } diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs index a0119bf8..1150d641 100644 --- a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -96,7 +96,6 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the in-game clock time changes. public event EventHandler TimeChanged { - add => this.EventManager.TimeChanged.Add(value, this.Mod); remove => this.EventManager.TimeChanged.Remove(value); } diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index 8b45e196..b6704f26 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -164,9 +164,9 @@ namespace StardewModdingAPI.Framework // get field name const string fieldName = #if SMAPI_FOR_WINDOWS - "inBeginEndPair"; + "inBeginEndPair"; #else - "_beginCalled"; + "_beginCalled"; #endif // get result diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 25401e23..9fbb6072 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -2,7 +2,6 @@ using System; using System.IO; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Input; -using StardewModdingAPI.Toolkit.Serialization; namespace StardewModdingAPI.Framework.ModHelpers { diff --git a/src/SMAPI/Framework/RequestExitDelegate.cs b/src/SMAPI/Framework/RequestExitDelegate.cs index 12d0ea0c..810c399b 100644 --- a/src/SMAPI/Framework/RequestExitDelegate.cs +++ b/src/SMAPI/Framework/RequestExitDelegate.cs @@ -4,4 +4,4 @@ namespace StardewModdingAPI.Framework /// The module which requested an immediate exit. /// The reason provided for the shutdown. internal delegate void RequestExitDelegate(string module, string reason); -} \ No newline at end of file +} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 47a23c87..2346550e 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -43,7 +43,7 @@ using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; using StardewModdingAPI.Utilities; using StardewValley; -using Object = StardewValley.Object; +using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework { @@ -524,6 +524,7 @@ namespace StardewModdingAPI.Framework Game1.currentLoader = null; this.Monitor.Log("Game loader done."); } + if (SGame.NewDayTask?.Status == TaskStatus.Created) { this.Monitor.Log("New day task synchronizing..."); @@ -605,7 +606,7 @@ namespace StardewModdingAPI.Framework $"{modGroup.Key.DisplayName} (" + string.Join(", ", modGroup.GroupBy(p => p.WasAdded).ToDictionary(p => p.Key, p => p.Count()).Select(p => $"{(p.Key ? "added" : "removed")} {p.Value}")) + ")" - ) + ) ) ); @@ -700,6 +701,7 @@ namespace StardewModdingAPI.Framework this.OnLoadStageChanged(LoadStage.CreatedSaveFile); events.SaveCreated.RaiseEmpty(); } + if (this.IsBetweenSaveEvents) { // raise after-save @@ -731,6 +733,7 @@ namespace StardewModdingAPI.Framework } else context += " Single-player."; + this.Monitor.Log(context); // raise events @@ -1111,7 +1114,7 @@ namespace StardewModdingAPI.Framework // require core fields string[] fields = entry.Value.Split('/'); - if (fields.Length < Object.objectInfoDescriptionIndex + 1) + if (fields.Length < SObject.objectInfoDescriptionIndex + 1) { LogIssue(entry.Key, "too few fields for an object"); hasObjectIssues = true; @@ -1119,10 +1122,10 @@ namespace StardewModdingAPI.Framework } // check min length for specific types - switch (fields[Object.objectInfoTypeIndex].Split(new[] { ' ' }, 2)[0]) + switch (fields[SObject.objectInfoTypeIndex].Split(new[] { ' ' }, 2)[0]) { case "Cooking": - if (fields.Length < Object.objectInfoBuffDurationIndex + 1) + if (fields.Length < SObject.objectInfoBuffDurationIndex + 1) { LogIssue(entry.Key, "too few fields for a cooking item"); hasObjectIssues = true; @@ -1316,6 +1319,7 @@ namespace StardewModdingAPI.Framework } } } + IModMetadata[] loaded = this.ModRegistry.GetAll().ToArray(); IModMetadata[] loadedContentPacks = loaded.Where(p => p.IsContentPack).ToArray(); IModMetadata[] loadedMods = loaded.Where(p => !p.IsContentPack).ToArray(); diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs index f0fb9485..0908b02a 100644 --- a/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs +++ b/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs @@ -58,7 +58,6 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots this.Inventory = watcher.TryGetInventoryChanges(out SnapshotItemListDiff itemChanges) ? itemChanges : this.EmptyItemListDiff; - } } } diff --git a/src/SMAPI/Framework/Utilities/Countdown.cs b/src/SMAPI/Framework/Utilities/Countdown.cs index 921a35ce..342b4258 100644 --- a/src/SMAPI/Framework/Utilities/Countdown.cs +++ b/src/SMAPI/Framework/Utilities/Countdown.cs @@ -34,7 +34,7 @@ this.Current--; return true; } - + /// Restart the countdown. public void Reset() { diff --git a/src/SMAPI/IReflectedField.cs b/src/SMAPI/IReflectedField.cs index 43ddad42..7ff61f29 100644 --- a/src/SMAPI/IReflectedField.cs +++ b/src/SMAPI/IReflectedField.cs @@ -23,4 +23,4 @@ namespace StardewModdingAPI //// The value to set. void SetValue(TValue value); } -} \ No newline at end of file +} diff --git a/src/SMAPI/IReflectedMethod.cs b/src/SMAPI/IReflectedMethod.cs index de83b98c..646e7301 100644 --- a/src/SMAPI/IReflectedMethod.cs +++ b/src/SMAPI/IReflectedMethod.cs @@ -24,4 +24,4 @@ namespace StardewModdingAPI /// The method arguments to pass in. void Invoke(params object[] arguments); } -} \ No newline at end of file +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 05251070..23ee8453 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -59,6 +59,7 @@ namespace StardewModdingAPI if (name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.OrdinalIgnoreCase)) return Assembly.LoadFrom(dll.FullName); } + return null; } catch (Exception ex) diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs index 165667a4..cd075dcc 100644 --- a/src/SMAPI/Utilities/SDate.cs +++ b/src/SMAPI/Utilities/SDate.cs @@ -269,7 +269,6 @@ namespace StardewModdingAPI.Utilities this.Year = year; this.DayOfWeek = this.GetDayOfWeek(day); this.DaysSinceStart = this.GetDaysSinceStart(day, season, year); - } /// Get whether a date represents 0 spring Y1, which is the date during the in-game intro. -- cgit From 7f35e74ca17f50cb461abff51ede4f0cf0a8e3dd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 23 Aug 2020 22:11:11 -0400 Subject: fix object-disposed error --- src/SMAPI/Framework/SCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 2346550e..c4fb3b5d 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -349,10 +349,10 @@ namespace StardewModdingAPI.Framework // dispose core components this.IsGameRunning = false; - this.LogManager?.Dispose(); this.ContentCore?.Dispose(); this.CancellationToken?.Dispose(); this.GameInstance?.Dispose(); + this.LogManager?.Dispose(); // dispose last to allow for any last-second log messages // end game (moved from Game1.OnExiting to let us clean up first) Process.GetCurrentProcess().Kill(); -- cgit From 46d63e11cc76c70ff77ee90edb6cc055c63e7224 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 24 Aug 2020 18:35:51 -0400 Subject: switch to ILSpy for decompiled code --- src/SMAPI/Framework/SGame.cs | 1191 ++++++++++++++++++++---------------------- 1 file changed, 569 insertions(+), 622 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 8efc8996..966fbcdd 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -5,14 +5,12 @@ using System.Text; using System.Threading.Tasks; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using Netcode; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; -using StardewValley.Events; using StardewValley.Locations; using StardewValley.Menus; using StardewValley.Tools; @@ -195,686 +193,635 @@ namespace StardewModdingAPI.Framework Game1.showingHealthBar = false; if (Game1._newDayTask != null) { - this.GraphicsDevice.Clear(Game1.bgColor); + base.GraphicsDevice.Clear(Game1.bgColor); + return; } - else + if (target_screen != null) + { + base.GraphicsDevice.SetRenderTarget(target_screen); + } + if (this.IsSaving) + { + base.GraphicsDevice.Clear(Game1.bgColor); + IClickableMenu menu = Game1.activeClickableMenu; + if (menu != null) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + events.Rendering.RaiseEmpty(); + try + { + events.RenderingActiveMenu.RaiseEmpty(); + menu.draw(Game1.spriteBatch); + events.RenderedActiveMenu.RaiseEmpty(); + } + catch (Exception ex) + { + this.Monitor.Log($"The {activeClickableMenu.GetType().FullName} menu crashed while drawing itself during save. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + activeClickableMenu.exitThisMenu(); + } + events.Rendered.RaiseEmpty(); + Game1.spriteBatch.End(); + } + if (Game1.overlayMenu != null) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); + } + this.renderScreenBuffer(target_screen); + return; + } + base.GraphicsDevice.Clear(Game1.bgColor); + if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet() && !this.takingMapScreenshot) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + + events.Rendering.RaiseEmpty(); + try + { + Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); + events.RenderingActiveMenu.RaiseEmpty(); + Game1.activeClickableMenu.draw(Game1.spriteBatch); + events.RenderedActiveMenu.RaiseEmpty(); + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } + events.Rendered.RaiseEmpty(); + Game1.spriteBatch.End(); + this.drawOverlays(Game1.spriteBatch); + if (target_screen != null) + { + base.GraphicsDevice.SetRenderTarget(null); + base.GraphicsDevice.Clear(Game1.bgColor); + 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); + Game1.spriteBatch.End(); + } + if (Game1.overlayMenu != null) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); + } + return; + } + if (Game1.gameMode == 11) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + events.Rendering.RaiseEmpty(); + Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink); + Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, 255, 0)); + Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White); + events.Rendered.RaiseEmpty(); + Game1.spriteBatch.End(); + return; + } + if (Game1.currentMinigame != null) { + bool batchEnded = false; + + if (events.Rendering.HasListeners()) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + events.Rendering.RaiseEmpty(); + Game1.spriteBatch.End(); + } + + Game1.currentMinigame.draw(Game1.spriteBatch); + if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((Game1.gameMode == 0) ? (1f - Game1.fadeToBlackAlpha) : Game1.fadeToBlackAlpha)); + Game1.spriteBatch.End(); + } + this.drawOverlays(Game1.spriteBatch); if (target_screen != null) - this.GraphicsDevice.SetRenderTarget(target_screen); - if (this.IsSaving) { - this.GraphicsDevice.Clear(Game1.bgColor); - IClickableMenu activeClickableMenu = Game1.activeClickableMenu; - if (activeClickableMenu != null) + base.GraphicsDevice.SetRenderTarget(null); + base.GraphicsDevice.Clear(Game1.bgColor); + 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()) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - events.Rendering.RaiseEmpty(); - try - { - events.RenderingActiveMenu.RaiseEmpty(); - activeClickableMenu.draw(Game1.spriteBatch); - events.RenderedActiveMenu.RaiseEmpty(); - } - catch (Exception ex) - { - this.Monitor.Log($"The {activeClickableMenu.GetType().FullName} menu crashed while drawing itself during save. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - activeClickableMenu.exitThisMenu(); - } + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); events.Rendered.RaiseEmpty(); Game1.spriteBatch.End(); } - if (Game1.overlayMenu != null) + } + return; + } + if (Game1.showingEndOfNightStuff) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + events.Rendering.RaiseEmpty(); + if (Game1.activeClickableMenu != null) + { + try + { + events.RenderingActiveMenu.RaiseEmpty(); + Game1.activeClickableMenu.draw(Game1.spriteBatch); + events.RenderedActiveMenu.RaiseEmpty(); + } + catch (Exception ex) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself during end-of-night-stuff. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); } - this.renderScreenBuffer(target_screen); } - else + events.Rendered.RaiseEmpty(); + Game1.spriteBatch.End(); + this.drawOverlays(Game1.spriteBatch); + if (target_screen != null) { - this.GraphicsDevice.Clear(Game1.bgColor); - if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && (Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet() && !this.takingMapScreenshot)) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - + base.GraphicsDevice.SetRenderTarget(null); + base.GraphicsDevice.Clear(Game1.bgColor); + 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); + Game1.spriteBatch.End(); + } + return; + } + if (Game1.gameMode == 6 || (Game1.gameMode == 3 && Game1.currentLocation == null)) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + events.Rendering.RaiseEmpty(); + string addOn = ""; + for (int i = 0; (double)i < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; i++) + { + addOn += "."; + } + string str = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688"); + string msg = str + addOn; + string largestMessage = str + "... "; + int msgw = SpriteText.getWidthOfString(largestMessage); + int msgh = 64; + int msgx = 64; + int msgy = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - msgh; + SpriteText.drawString(Game1.spriteBatch, msg, msgx, msgy, 999999, msgw, msgh, 1f, 0.88f, junimoText: false, 0, largestMessage); + events.Rendered.RaiseEmpty(); + Game1.spriteBatch.End(); + this.drawOverlays(Game1.spriteBatch); + if (target_screen != null) + { + base.GraphicsDevice.SetRenderTarget(null); + base.GraphicsDevice.Clear(Game1.bgColor); + 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); + Game1.spriteBatch.End(); + } + if (Game1.overlayMenu != null) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); + } + //base.Draw(gameTime); + return; + } + byte batchOpens = 0; // used for rendering event + if (Game1.gameMode == 0) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + if (++batchOpens == 1) + events.Rendering.RaiseEmpty(); + } + else + { + if (Game1.drawLighting) + { + base.GraphicsDevice.SetRenderTarget(Game1.lightmap); + base.GraphicsDevice.Clear(Color.White * 0f); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, null, null); + if (++batchOpens == 1) events.Rendering.RaiseEmpty(); - try + Color lighting = (Game1.currentLocation.Name.StartsWith("UndergroundMine") && Game1.currentLocation is MineShaft) ? (Game1.currentLocation as MineShaft).getLightingColor(gameTime) : ((Game1.ambientLight.Equals(Color.White) || (Game1.isRaining && (bool)Game1.currentLocation.isOutdoors)) ? Game1.outdoorLight : Game1.ambientLight); + Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, lighting); + foreach (LightSource lightSource in Game1.currentLightSources) + { + if ((Game1.isRaining || Game1.isDarkOut()) && lightSource.lightContext.Value == LightSource.LightContext.WindowLight) { - Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); - events.RenderingActiveMenu.RaiseEmpty(); - Game1.activeClickableMenu.draw(Game1.spriteBatch); - events.RenderedActiveMenu.RaiseEmpty(); + continue; } - catch (Exception ex) + if (lightSource.PlayerID != 0L && lightSource.PlayerID != Game1.player.UniqueMultiplayerID) { - this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); + Farmer farmer = Game1.getFarmerMaybeOffline(lightSource.PlayerID); + if (farmer == null || (farmer.currentLocation != null && farmer.currentLocation.Name != Game1.currentLocation.Name) || (bool)farmer.hidden) + { + continue; + } } - events.Rendered.RaiseEmpty(); - Game1.spriteBatch.End(); - this.drawOverlays(Game1.spriteBatch); - if (target_screen != null) + if (Utility.isOnScreen(lightSource.position, (int)((float)lightSource.radius * 64f * 4f))) { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(Game1.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)target_screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(target_screen.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); + Game1.spriteBatch.Draw(lightSource.lightTexture, Game1.GlobalToLocal(Game1.viewport, lightSource.position) / (Game1.options.lightingQuality / 2), lightSource.lightTexture.Bounds, lightSource.color, 0f, new Vector2(lightSource.lightTexture.Bounds.Center.X, lightSource.lightTexture.Bounds.Center.Y), (float)lightSource.radius / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); } - if (Game1.overlayMenu == null) - return; - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - } - else if (Game1.gameMode == (byte)11) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - events.Rendering.RaiseEmpty(); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Microsoft.Xna.Framework.Color.HotPink); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Microsoft.Xna.Framework.Color(0, (int)byte.MaxValue, 0)); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Microsoft.Xna.Framework.Color.White); - events.Rendered.RaiseEmpty(); - Game1.spriteBatch.End(); } - else if (Game1.currentMinigame != null) + Game1.spriteBatch.End(); + base.GraphicsDevice.SetRenderTarget(target_screen); + } + if (Game1.bloomDay && Game1.bloom != null) + { + Game1.bloom.BeginDraw(); + } + base.GraphicsDevice.Clear(Game1.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + if (++batchOpens == 1) + events.Rendering.RaiseEmpty(); + events.RenderingWorld.RaiseEmpty(); + if (Game1.background != null) + { + Game1.background.draw(Game1.spriteBatch); + } + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4); + Game1.currentLocation.drawWater(Game1.spriteBatch); + this._farmerShadows.Clear(); + if (Game1.currentLocation.currentEvent != null && !Game1.currentLocation.currentEvent.isFestival && Game1.currentLocation.currentEvent.farmerActors.Count > 0) + { + foreach (Farmer f in Game1.currentLocation.currentEvent.farmerActors) { - int batchEnds = 0; - - if (events.Rendering.HasListeners()) + if ((f.IsLocalPlayer && Game1.displayFarmer) || !f.hidden) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - events.Rendering.RaiseEmpty(); - Game1.spriteBatch.End(); + this._farmerShadows.Add(f); } - Game1.currentMinigame.draw(Game1.spriteBatch); - if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) + } + } + else + { + foreach (Farmer f2 in Game1.currentLocation.farmers) + { + if ((f2.IsLocalPlayer && Game1.displayFarmer) || !f2.hidden) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.Black * (Game1.gameMode == (byte)0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha)); - Game1.spriteBatch.End(); + this._farmerShadows.Add(f2); } - this.drawOverlays(Game1.spriteBatch); - if (target_screen == null) + } + } + if (!Game1.currentLocation.shouldHideCharacters()) + { + if (Game1.CurrentEvent == null) + { + foreach (NPC k in Game1.currentLocation.characters) { - if (++batchEnds == 1 && events.Rendered.HasListeners()) + if (!k.swimming && !k.HideShadow && !k.IsInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(k.getTileLocation())) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - events.Rendered.RaiseEmpty(); - Game1.spriteBatch.End(); + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, k.Position + new Vector2((float)(k.Sprite.SpriteWidth * 4) / 2f, k.GetBoundingBox().Height + ((!k.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), (4f + (float)k.yJumpOffset / 40f) * (float)k.scale, SpriteEffects.None, Math.Max(0f, (float)k.getStandingY() / 10000f) - 1E-06f); } - return; } - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(Game1.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)target_screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(target_screen.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - if (++batchEnds == 1) - events.Rendered.RaiseEmpty(); - Game1.spriteBatch.End(); } - else if (Game1.showingEndOfNightStuff) + else { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - events.Rendering.RaiseEmpty(); - if (Game1.activeClickableMenu != null) + foreach (NPC l in Game1.CurrentEvent.actors) { - try - { - events.RenderingActiveMenu.RaiseEmpty(); - Game1.activeClickableMenu.draw(Game1.spriteBatch); - events.RenderedActiveMenu.RaiseEmpty(); - } - catch (Exception ex) + if (!l.swimming && !l.HideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(l.getTileLocation())) { - this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself during end-of-night-stuff. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, l.Position + new Vector2((float)(l.Sprite.SpriteWidth * 4) / 2f, l.GetBoundingBox().Height + ((!l.IsMonster) ? ((l.Sprite.SpriteHeight <= 16) ? (-4) : 12) : 0))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), (4f + (float)l.yJumpOffset / 40f) * (float)l.scale, SpriteEffects.None, Math.Max(0f, (float)l.getStandingY() / 10000f) - 1E-06f); } } - events.Rendered.RaiseEmpty(); - Game1.spriteBatch.End(); - this.drawOverlays(Game1.spriteBatch); - if (target_screen == null) - return; - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(Game1.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)target_screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(target_screen.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); } - else if (Game1.gameMode == (byte)6 || Game1.gameMode == (byte)3 && Game1.currentLocation == null) + foreach (Farmer f3 in this._farmerShadows) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - events.Rendering.RaiseEmpty(); - string str1 = ""; - for (int index = 0; (double)index < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; ++index) - str1 += "."; - string str2 = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688"); - string s = str2 + str1; - string str3 = str2 + "... "; - int widthOfString = SpriteText.getWidthOfString(str3, 999999); - int height = 64; - int x = 64; - int y = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - height; - SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str3, -1, SpriteText.ScrollTextAlignment.Left); - events.Rendered.RaiseEmpty(); - Game1.spriteBatch.End(); - this.drawOverlays(Game1.spriteBatch); - if (target_screen != null) - { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(Game1.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)target_screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(target_screen.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); - } - if (Game1.overlayMenu != null) + if (!Game1.multiplayer.isDisconnecting(f3.UniqueMultiplayerID) && !f3.swimming && !f3.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(f3.getTileLocation()))) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(f3.Position + new Vector2(32f, 24f)), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), 4f - (((f3.running || f3.UsingTool) && f3.FarmerSprite.currentAnimationIndex > 1) ? ((float)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[f3.FarmerSprite.CurrentFrame]) * 0.5f) : 0f), SpriteEffects.None, 0f); } - //base.Draw(gameTime); } - else + } + Layer building_layer = Game1.currentLocation.Map.GetLayer("Buildings"); + building_layer.Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4); + Game1.mapDisplayDevice.EndScene(); + Game1.spriteBatch.End(); + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + if (!Game1.currentLocation.shouldHideCharacters()) + { + if (Game1.CurrentEvent == null) { - byte batchOpens = 0; // used for rendering event - - Microsoft.Xna.Framework.Rectangle rectangle; - Viewport viewport; - if (Game1.gameMode == (byte)0) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (++batchOpens == 1) - events.Rendering.RaiseEmpty(); - } - else + foreach (NPC n in Game1.currentLocation.characters) { - if (Game1.drawLighting) - { - this.GraphicsDevice.SetRenderTarget(Game1.lightmap); - this.GraphicsDevice.Clear(Microsoft.Xna.Framework.Color.White * 0.0f); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (++batchOpens == 1) - events.Rendering.RaiseEmpty(); - Microsoft.Xna.Framework.Color color = !Game1.currentLocation.Name.StartsWith("UndergroundMine") || !(Game1.currentLocation is MineShaft) ? (Game1.ambientLight.Equals(Microsoft.Xna.Framework.Color.White) || Game1.isRaining && (bool)(NetFieldBase)Game1.currentLocation.isOutdoors ? Game1.outdoorLight : Game1.ambientLight) : (Game1.currentLocation as MineShaft).getLightingColor(gameTime); - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, color); - foreach (LightSource currentLightSource in Game1.currentLightSources) - { - if (!Game1.isRaining && !Game1.isDarkOut() || currentLightSource.lightContext.Value != LightSource.LightContext.WindowLight) - { - if (currentLightSource.PlayerID != 0L && currentLightSource.PlayerID != Game1.player.UniqueMultiplayerID) - { - Farmer farmerMaybeOffline = Game1.getFarmerMaybeOffline(currentLightSource.PlayerID); - if (farmerMaybeOffline == null || farmerMaybeOffline.currentLocation != null && farmerMaybeOffline.currentLocation.Name != Game1.currentLocation.Name || (bool)(NetFieldBase)farmerMaybeOffline.hidden) - continue; - } - if (Utility.isOnScreen((Vector2)(NetFieldBase)currentLightSource.position, (int)((double)(float)(NetFieldBase)currentLightSource.radius * 64.0 * 4.0))) - Game1.spriteBatch.Draw(currentLightSource.lightTexture, Game1.GlobalToLocal(Game1.viewport, (Vector2)(NetFieldBase)currentLightSource.position) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(currentLightSource.lightTexture.Bounds), (Microsoft.Xna.Framework.Color)(NetFieldBase)currentLightSource.color, 0.0f, new Vector2((float)currentLightSource.lightTexture.Bounds.Center.X, (float)currentLightSource.lightTexture.Bounds.Center.Y), (float)(NetFieldBase)currentLightSource.radius / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); - } - } - Game1.spriteBatch.End(); - this.GraphicsDevice.SetRenderTarget(target_screen); - } - if (Game1.bloomDay && Game1.bloom != null) - Game1.bloom.BeginDraw(); - this.GraphicsDevice.Clear(Game1.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (++batchOpens == 1) - events.Rendering.RaiseEmpty(); - events.RenderingWorld.RaiseEmpty(); - if (Game1.background != null) - Game1.background.draw(Game1.spriteBatch); - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); - Game1.currentLocation.drawWater(Game1.spriteBatch); - this._farmerShadows.Clear(); - if (Game1.currentLocation.currentEvent != null && !Game1.currentLocation.currentEvent.isFestival && Game1.currentLocation.currentEvent.farmerActors.Count > 0) - { - foreach (Farmer farmerActor in Game1.currentLocation.currentEvent.farmerActors) - { - if (farmerActor.IsLocalPlayer && Game1.displayFarmer || !(bool)(NetFieldBase)farmerActor.hidden) - this._farmerShadows.Add(farmerActor); - } - } - else - { - foreach (Farmer farmer in Game1.currentLocation.farmers) - { - if (farmer.IsLocalPlayer && Game1.displayFarmer || !(bool)(NetFieldBase)farmer.hidden) - this._farmerShadows.Add(farmer); - } - } - if (!Game1.currentLocation.shouldHideCharacters()) - { - if (Game1.CurrentEvent == null) - { - foreach (NPC character in Game1.currentLocation.characters) - { - if (!(bool)(NetFieldBase)character.swimming && !character.HideShadow && (!character.IsInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))) - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)(NetFieldBase)character.scale, SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f); - } - } - else - { - foreach (NPC actor in Game1.CurrentEvent.actors) - { - if (!(bool)(NetFieldBase)actor.swimming && !actor.HideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.Sprite.SpriteHeight <= 16 ? -4 : 12))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)(NetFieldBase)actor.scale, SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); - } - } - foreach (Farmer farmerShadow in this._farmerShadows) - { - if (!Game1.multiplayer.isDisconnecting(farmerShadow.UniqueMultiplayerID) && !(bool)(NetFieldBase)farmerShadow.swimming && !farmerShadow.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation()))) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f)); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Microsoft.Xna.Framework.Color white = Microsoft.Xna.Framework.Color.White; - Microsoft.Xna.Framework.Rectangle bounds = Game1.shadowTexture.Bounds; - double x = (double)bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num = 4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5); - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, 0.0f, origin, (float)num, SpriteEffects.None, 0.0f); - } - } - } - Layer layer1 = Game1.currentLocation.Map.GetLayer("Buildings"); - layer1.Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); - Game1.mapDisplayDevice.EndScene(); - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (!Game1.currentLocation.shouldHideCharacters()) - { - if (Game1.CurrentEvent == null) - { - foreach (NPC character in Game1.currentLocation.characters) - { - if (!(bool)(NetFieldBase)character.swimming && !character.HideShadow && (!(bool)(NetFieldBase)character.isInvisible && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))) - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)(NetFieldBase)character.scale, SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f); - } - } - else - { - foreach (NPC actor in Game1.CurrentEvent.actors) - { - if (!(bool)(NetFieldBase)actor.swimming && !actor.HideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)(NetFieldBase)actor.scale, SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); - } - } - foreach (Farmer farmerShadow in this._farmerShadows) - { - float num1 = Math.Max(0.0001f, farmerShadow.getDrawLayer() + 0.00011f) - 0.0001f; - if (!(bool)(NetFieldBase)farmerShadow.swimming && !farmerShadow.isRidingHorse() && (Game1.currentLocation != null && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation()))) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f)); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Microsoft.Xna.Framework.Color white = Microsoft.Xna.Framework.Color.White; - Microsoft.Xna.Framework.Rectangle bounds = Game1.shadowTexture.Bounds; - double x = (double)bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = 4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5); - double num3 = (double)num1; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, 0.0f, origin, (float)num2, SpriteEffects.None, (float)num3); - } - } - } - if ((Game1.eventUp || Game1.killScreen) && (!Game1.killScreen && Game1.currentLocation.currentEvent != null)) - Game1.currentLocation.currentEvent.draw(Game1.spriteBatch); - if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm")) - Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), new Microsoft.Xna.Framework.Rectangle?(Game1.player.currentUpgrade.getSourceRectangle()), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, (float)(((double)Game1.player.currentUpgrade.positionOfCarpenter.Y + 48.0) / 10000.0)); - Game1.currentLocation.draw(Game1.spriteBatch); - foreach (Vector2 key in Game1.crabPotOverlayTiles.Keys) - { - Tile tile = layer1.Tiles[(int)key.X, (int)key.Y]; - if (tile != null) - { - Vector2 local = Game1.GlobalToLocal(Game1.viewport, key * 64f); - Location location = new Location((int)local.X, (int)local.Y); - Game1.mapDisplayDevice.DrawTile(tile, location, (float)(((double)key.Y * 64.0 - 1.0) / 10000.0)); - } - } - if (Game1.eventUp && Game1.currentLocation.currentEvent != null) - { - string messageToScreen = Game1.currentLocation.currentEvent.messageToScreen; - } - if (Game1.player.ActiveObject == null && (Game1.player.UsingTool || Game1.pickingTool) && (Game1.player.CurrentTool != null && (!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool))) - Game1.drawTool(Game1.player); - if (Game1.currentLocation.Name.Equals("Farm")) - this.drawFarmBuildings(); - if (Game1.tvStation >= 0) - Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2(400f, 160f)), new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15)), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); - if (Game1.panMode) - { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / 64.0) * 64 - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / 64.0) * 64 - Game1.viewport.Y, 64, 64), Microsoft.Xna.Framework.Color.Lime * 0.75f); - foreach (Warp warp in (NetList>)Game1.currentLocation.warps) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(warp.X * 64 - Game1.viewport.X, warp.Y * 64 - Game1.viewport.Y, 64, 64), Microsoft.Xna.Framework.Color.Red * 0.75f); - } - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); - Game1.mapDisplayDevice.EndScene(); - Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch); - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.displayFarmer && Game1.player.ActiveObject != null && ((bool)(NetFieldBase)Game1.player.ActiveObject.bigCraftable && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null) - Game1.drawPlayerHeldObject(Game1.player); - else if (Game1.displayFarmer && Game1.player.ActiveObject != null) - { - if (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) == null || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")) - { - Layer layer2 = Game1.currentLocation.Map.GetLayer("Front"); - rectangle = Game1.player.GetBoundingBox(); - Location mapDisplayLocation1 = new Location(rectangle.Right, (int)Game1.player.Position.Y - 38); - xTile.Dimensions.Size size1 = Game1.viewport.Size; - if (layer2.PickTile(mapDisplayLocation1, size1) != null) - { - Layer layer3 = Game1.currentLocation.Map.GetLayer("Front"); - rectangle = Game1.player.GetBoundingBox(); - Location mapDisplayLocation2 = new Location(rectangle.Right, (int)Game1.player.Position.Y - 38); - xTile.Dimensions.Size size2 = Game1.viewport.Size; - if (layer3.PickTile(mapDisplayLocation2, size2).TileIndexProperties.ContainsKey("FrontAlways")) - goto label_139; - } - else - goto label_139; - } - Game1.drawPlayerHeldObject(Game1.player); - } - label_139: - if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) - Game1.drawTool(Game1.player); - if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) - { - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("AlwaysFront").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); - Game1.mapDisplayDevice.EndScene(); - } - if ((double)Game1.toolHold > 400.0 && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool) - { - Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.White; - switch ((int)((double)Game1.toolHold / 600.0) + 2) - { - case 1: - color = Tool.copperColor; - break; - case 2: - color = Tool.steelColor; - break; - case 3: - color = Tool.goldColor; - break; - case 4: - color = Tool.iridiumColor; - break; - } - Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : 64) - 2, (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607) + 4, 12), Microsoft.Xna.Framework.Color.Black); - Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : 64), (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607), 8), color); - } - this.drawWeather(gameTime, target_screen); - if (Game1.farmEvent != null) - Game1.farmEvent.draw(Game1.spriteBatch); - if ((double)Game1.currentLocation.LightLevel > 0.0 && Game1.timeOfDay < 2000) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.Black * Game1.currentLocation.LightLevel); - if (Game1.screenGlow) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha); - Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); - if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (double)(Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0.0 || ((Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure))) - Game1.player.CurrentTool.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.eventUp && Game1.currentLocation.currentEvent != null) - { - foreach (NPC actor in Game1.currentLocation.currentEvent.actors) - { - if (actor.isEmoting) - { - Vector2 localPosition = actor.getLocalPosition(Game1.viewport); - localPosition.Y -= 140f; - if (actor.Age == 2) - localPosition.Y += 32f; - else if (actor.Gender == 1) - localPosition.Y += 10f; - Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, localPosition, new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(actor.CurrentEmoteIndex * 16 % Game1.emoteSpriteSheet.Width, actor.CurrentEmoteIndex * 16 / Game1.emoteSpriteSheet.Width * 16, 16, 16)), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, (float)actor.getStandingY() / 10000f); - } - } - } - Game1.spriteBatch.End(); - if (Game1.drawLighting) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw((Texture2D)Game1.lightmap, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(Game1.lightmap.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 1f); - if (Game1.isRaining && (bool)(NetFieldBase)Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)) - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.OrangeRed * 0.45f); - Game1.spriteBatch.End(); - } - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - events.RenderedWorld.RaiseEmpty(); - if (Game1.drawGrid) + if (!n.swimming && !n.HideShadow && !n.isInvisible && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(n.getTileLocation())) { - int num1 = -Game1.viewport.X % 64; - float num2 = (float)(-Game1.viewport.Y % 64); - int num3 = num1; - while (true) - { - int num4 = num3; - viewport = Game1.graphics.GraphicsDevice.Viewport; - int width = viewport.Width; - if (num4 < width) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D staminaRect = Game1.staminaRect; - int x = num3; - int y = (int)num2; - viewport = Game1.graphics.GraphicsDevice.Viewport; - int height = viewport.Height; - Microsoft.Xna.Framework.Rectangle destinationRectangle = new Microsoft.Xna.Framework.Rectangle(x, y, 1, height); - Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.Red * 0.5f; - spriteBatch.Draw(staminaRect, destinationRectangle, color); - num3 += 64; - } - else - break; - } - float num5 = num2; - while (true) - { - double num4 = (double)num5; - viewport = Game1.graphics.GraphicsDevice.Viewport; - double height = (double)viewport.Height; - if (num4 < height) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D staminaRect = Game1.staminaRect; - int x = num1; - int y = (int)num5; - viewport = Game1.graphics.GraphicsDevice.Viewport; - int width = viewport.Width; - Microsoft.Xna.Framework.Rectangle destinationRectangle = new Microsoft.Xna.Framework.Rectangle(x, y, width, 1); - Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.Red * 0.5f; - spriteBatch.Draw(staminaRect, destinationRectangle, color); - num5 += 64f; - } - else - break; - } + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, n.Position + new Vector2((float)(n.Sprite.SpriteWidth * 4) / 2f, n.GetBoundingBox().Height + ((!n.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), (4f + (float)n.yJumpOffset / 40f) * (float)n.scale, SpriteEffects.None, Math.Max(0f, (float)n.getStandingY() / 10000f) - 1E-06f); } - if (Game1.currentBillboard != 0 && !this.takingMapScreenshot) - this.drawBillboard(); - if (!Game1.eventUp && Game1.farmEvent == null && (Game1.currentBillboard == 0 && Game1.gameMode == (byte)3) && (!this.takingMapScreenshot && Game1.isOutdoorMapSmallerThanViewport())) - { - SpriteBatch spriteBatch1 = Game1.spriteBatch; - Texture2D fadeToBlackRect1 = Game1.fadeToBlackRect; - int width1 = -Math.Min(Game1.viewport.X, 4096); - viewport = Game1.graphics.GraphicsDevice.Viewport; - int height1 = viewport.Height; - Microsoft.Xna.Framework.Rectangle destinationRectangle1 = new Microsoft.Xna.Framework.Rectangle(0, 0, width1, height1); - Microsoft.Xna.Framework.Color black1 = Microsoft.Xna.Framework.Color.Black; - spriteBatch1.Draw(fadeToBlackRect1, destinationRectangle1, black1); - SpriteBatch spriteBatch2 = Game1.spriteBatch; - Texture2D fadeToBlackRect2 = Game1.fadeToBlackRect; - int x = -Game1.viewport.X + Game1.currentLocation.map.Layers[0].LayerWidth * 64; - viewport = Game1.graphics.GraphicsDevice.Viewport; - int width2 = Math.Min(4096, viewport.Width - (-Game1.viewport.X + Game1.currentLocation.map.Layers[0].LayerWidth * 64)); - viewport = Game1.graphics.GraphicsDevice.Viewport; - int height2 = viewport.Height; - Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x, 0, width2, height2); - Microsoft.Xna.Framework.Color black2 = Microsoft.Xna.Framework.Color.Black; - spriteBatch2.Draw(fadeToBlackRect2, destinationRectangle2, black2); - } - if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && Game1.gameMode == (byte)3) && (!Game1.freezeControls && !Game1.panMode && (!Game1.HostPaused && !this.takingMapScreenshot))) - { - events.RenderingHud.RaiseEmpty(); - this.drawHUD(); - events.RenderedHud.RaiseEmpty(); - } - else if (Game1.activeClickableMenu == null) - { - FarmEvent farmEvent = Game1.farmEvent; - } - if (Game1.hudMessages.Count > 0 && !this.takingMapScreenshot) - { - for (int i = Game1.hudMessages.Count - 1; i >= 0; --i) - Game1.hudMessages[i].draw(Game1.spriteBatch, i); - } - } - if (Game1.farmEvent != null) - Game1.farmEvent.draw(Game1.spriteBatch); - if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && ((Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox)) && !this.takingMapScreenshot)) - this.drawDialogueBox(); - if (Game1.progressBar && !this.takingMapScreenshot) - { - SpriteBatch spriteBatch1 = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - int x1 = (Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2; - rectangle = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea(); - int y1 = rectangle.Bottom - 128; - int dialogueWidth = Game1.dialogueWidth; - Microsoft.Xna.Framework.Rectangle destinationRectangle1 = new Microsoft.Xna.Framework.Rectangle(x1, y1, dialogueWidth, 32); - Microsoft.Xna.Framework.Color lightGray = Microsoft.Xna.Framework.Color.LightGray; - spriteBatch1.Draw(fadeToBlackRect, destinationRectangle1, lightGray); - SpriteBatch spriteBatch2 = Game1.spriteBatch; - Texture2D staminaRect = Game1.staminaRect; - int x2 = (Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2; - rectangle = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea(); - int y2 = rectangle.Bottom - 128; - int width = (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth); - Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x2, y2, width, 32); - Microsoft.Xna.Framework.Color dimGray = Microsoft.Xna.Framework.Color.DimGray; - spriteBatch2.Draw(staminaRect, destinationRectangle2, dimGray); - } - if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null) - Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); - if (Game1.isRaining && Game1.currentLocation != null && ((bool)(NetFieldBase)Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert))) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D staminaRect = Game1.staminaRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.Blue * 0.2f; - spriteBatch.Draw(staminaRect, bounds, color); - } - if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && ((!Game1.nameSelectUp || Game1.messagePause) && !this.takingMapScreenshot)) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.Black * (Game1.gameMode == (byte)0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha); - spriteBatch.Draw(fadeToBlackRect, bounds, color); } - else if ((double)Game1.flashAlpha > 0.0 && !this.takingMapScreenshot) + } + else + { + foreach (NPC n2 in Game1.CurrentEvent.actors) { - if (Game1.options.screenFlash) + if (!n2.swimming && !n2.HideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(n2.getTileLocation())) { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.White * Math.Min(1f, Game1.flashAlpha); - spriteBatch.Draw(fadeToBlackRect, bounds, color); + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, n2.Position + new Vector2((float)(n2.Sprite.SpriteWidth * 4) / 2f, n2.GetBoundingBox().Height + ((!n2.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), (4f + (float)n2.yJumpOffset / 40f) * (float)n2.scale, SpriteEffects.None, Math.Max(0f, (float)n2.getStandingY() / 10000f) - 1E-06f); } - Game1.flashAlpha -= 0.1f; } - if ((Game1.messagePause || Game1.globalFade) && (Game1.dialogueUp && !this.takingMapScreenshot)) - this.drawDialogueBox(); - if (!this.takingMapScreenshot) - { - foreach (TemporaryAnimatedSprite overlayTempSprite in Game1.screenOverlayTempSprites) - overlayTempSprite.draw(Game1.spriteBatch, true, 0, 0, 1f); - } - if (Game1.debugMode) + } + foreach (Farmer f4 in this._farmerShadows) + { + float draw_layer = Math.Max(0.0001f, f4.getDrawLayer() + 0.00011f) - 0.0001f; + if (!f4.swimming && !f4.isRidingHorse() && Game1.currentLocation != null && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(f4.getTileLocation())) { - StringBuilder debugStringBuilder = Game1._debugStringBuilder; - debugStringBuilder.Clear(); - if (Game1.panMode) - { - debugStringBuilder.Append((Game1.getOldMouseX() + Game1.viewport.X) / 64); - debugStringBuilder.Append(","); - debugStringBuilder.Append((Game1.getOldMouseY() + Game1.viewport.Y) / 64); - } - else - { - debugStringBuilder.Append("player: "); - debugStringBuilder.Append(Game1.player.getStandingX() / 64); - debugStringBuilder.Append(", "); - debugStringBuilder.Append(Game1.player.getStandingY() / 64); - } - debugStringBuilder.Append(" mouseTransparency: "); - debugStringBuilder.Append(Game1.mouseCursorTransparency); - debugStringBuilder.Append(" mousePosition: "); - debugStringBuilder.Append(Game1.getMouseX()); - debugStringBuilder.Append(","); - debugStringBuilder.Append(Game1.getMouseY()); - debugStringBuilder.Append(Environment.NewLine); - debugStringBuilder.Append(" mouseWorldPosition: "); - debugStringBuilder.Append(Game1.getMouseX() + Game1.viewport.X); - debugStringBuilder.Append(","); - debugStringBuilder.Append(Game1.getMouseY() + Game1.viewport.Y); - debugStringBuilder.Append(" debugOutput: "); - debugStringBuilder.Append(Game1.debugOutput); - Game1.spriteBatch.DrawString(Game1.smallFont, debugStringBuilder, new Vector2((float)this.GraphicsDevice.Viewport.GetTitleSafeArea().X, (float)(this.GraphicsDevice.Viewport.GetTitleSafeArea().Y + Game1.smallFont.LineSpacing * 8)), Microsoft.Xna.Framework.Color.Red, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(f4.Position + new Vector2(32f, 24f)), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), 4f - (((f4.running || f4.UsingTool) && f4.FarmerSprite.currentAnimationIndex > 1) ? ((float)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[f4.FarmerSprite.CurrentFrame]) * 0.5f) : 0f), SpriteEffects.None, draw_layer); } - if (Game1.showKeyHelp && !this.takingMapScreenshot) - Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2(64f, (float)(Game1.viewport.Height - 64 - (Game1.dialogueUp ? 192 + (Game1.isQuestion ? Game1.questionChoices.Count * 64 : 0) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Microsoft.Xna.Framework.Color.LightGray, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); - if (Game1.activeClickableMenu != null && !this.takingMapScreenshot) + } + } + if ((Game1.eventUp || Game1.killScreen) && !Game1.killScreen && Game1.currentLocation.currentEvent != null) + { + Game1.currentLocation.currentEvent.draw(Game1.spriteBatch); + } + if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm")) + { + Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), Game1.player.currentUpgrade.getSourceRectangle(), Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, (Game1.player.currentUpgrade.positionOfCarpenter.Y + 48f) / 10000f); + } + Game1.currentLocation.draw(Game1.spriteBatch); + foreach (Vector2 tile_position in Game1.crabPotOverlayTiles.Keys) + { + Tile tile = building_layer.Tiles[(int)tile_position.X, (int)tile_position.Y]; + if (tile != null) + { + Vector2 vector_draw_position = Game1.GlobalToLocal(Game1.viewport, tile_position * 64f); + Location draw_location = new Location((int)vector_draw_position.X, (int)vector_draw_position.Y); + Game1.mapDisplayDevice.DrawTile(tile, draw_location, (tile_position.Y * 64f - 1f) / 10000f); + } + } + if (Game1.eventUp && Game1.currentLocation.currentEvent != null) + { + _ = Game1.currentLocation.currentEvent.messageToScreen; + } + if (Game1.player.ActiveObject == null && (Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && (!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool)) + { + Game1.drawTool(Game1.player); + } + if (Game1.currentLocation.Name.Equals("Farm")) + { + this.drawFarmBuildings(); + } + if (Game1.tvStation >= 0) + { + Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2(400f, 160f)), new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15), Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); + } + if (Game1.panMode) + { + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / 64.0) * 64 - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / 64.0) * 64 - Game1.viewport.Y, 64, 64), Color.Lime * 0.75f); + foreach (Warp w in Game1.currentLocation.warps) + { + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(w.X * 64 - Game1.viewport.X, w.Y * 64 - Game1.viewport.Y, 64, 64), Color.Red * 0.75f); + } + } + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4); + Game1.mapDisplayDevice.EndScene(); + Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch); + Game1.spriteBatch.End(); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + if (Game1.displayFarmer && Game1.player.ActiveObject != null && (bool)Game1.player.ActiveObject.bigCraftable && this.checkBigCraftableBoundariesForFrontLayer() && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null) + { + Game1.drawPlayerHeldObject(Game1.player); + } + else if (Game1.displayFarmer && Game1.player.ActiveObject != null && ((Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && !Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")) || (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.GetBoundingBox().Right, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && !Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.GetBoundingBox().Right, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")))) + { + Game1.drawPlayerHeldObject(Game1.player); + } + if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && (!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null) + { + Game1.drawTool(Game1.player); + } + if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) + { + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + Game1.currentLocation.Map.GetLayer("AlwaysFront").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4); + Game1.mapDisplayDevice.EndScene(); + } + if (Game1.toolHold > 400f && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool) + { + Color barColor = Color.White; + switch ((int)(Game1.toolHold / 600f) + 2) + { + case 1: + barColor = Tool.copperColor; + break; + case 2: + barColor = Tool.steelColor; + break; + case 3: + barColor = Tool.goldColor; + break; + case 4: + barColor = Tool.iridiumColor; + break; + } + Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - ((!Game1.player.CurrentTool.Name.Equals("Watering Can")) ? 64 : 0) - 2, (int)(Game1.toolHold % 600f * 0.08f) + 4, 12), Color.Black); + Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - ((!Game1.player.CurrentTool.Name.Equals("Watering Can")) ? 64 : 0), (int)(Game1.toolHold % 600f * 0.08f), 8), barColor); + } + this.drawWeather(gameTime, target_screen); + if (Game1.farmEvent != null) + { + Game1.farmEvent.draw(Game1.spriteBatch); + } + if (Game1.currentLocation.LightLevel > 0f && Game1.timeOfDay < 2000) + { + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * Game1.currentLocation.LightLevel); + } + if (Game1.screenGlow) + { + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha); + } + Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); + if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0f || (Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure)) + { + Game1.player.CurrentTool.draw(Game1.spriteBatch); + } + Game1.spriteBatch.End(); + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + if (Game1.eventUp && Game1.currentLocation.currentEvent != null) + { + foreach (NPC m in Game1.currentLocation.currentEvent.actors) + { + if (m.isEmoting) { - try + Vector2 emotePosition = m.getLocalPosition(Game1.viewport); + emotePosition.Y -= 140f; + if (m.Age == 2) { - events.RenderingActiveMenu.RaiseEmpty(); - Game1.activeClickableMenu.draw(Game1.spriteBatch); - events.RenderedActiveMenu.RaiseEmpty(); + emotePosition.Y += 32f; } - catch (Exception ex) + else if (m.Gender == 1) { - this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); + emotePosition.Y += 10f; } + Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, emotePosition, new Microsoft.Xna.Framework.Rectangle(m.CurrentEmoteIndex * 16 % Game1.emoteSpriteSheet.Width, m.CurrentEmoteIndex * 16 / Game1.emoteSpriteSheet.Width * 16, 16, 16), Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, (float)m.getStandingY() / 10000f); } - else if (Game1.farmEvent != null) - Game1.farmEvent.drawAboveEverything(Game1.spriteBatch); - if (Game1.emoteMenu != null && !this.takingMapScreenshot) - Game1.emoteMenu.draw(Game1.spriteBatch); - if (Game1.HostPaused && !this.takingMapScreenshot) - { - string s = Game1.content.LoadString("Strings\\StringsFromCSFiles:DayTimeMoneyBox.cs.10378"); - SpriteText.drawStringWithScrollBackground(Game1.spriteBatch, s, 96, 32, "", 1f, -1, SpriteText.ScrollTextAlignment.Left); - } - - events.Rendered.RaiseEmpty(); - Game1.spriteBatch.End(); - this.drawOverlays(Game1.spriteBatch); - this.renderScreenBuffer(target_screen); } } + Game1.spriteBatch.End(); + if (Game1.drawLighting) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, null, null); + Game1.spriteBatch.Draw(Game1.lightmap, Vector2.Zero, Game1.lightmap.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.lightingQuality / 2, SpriteEffects.None, 1f); + if (Game1.isRaining && (bool)Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)) + { + Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.OrangeRed * 0.45f); + } + Game1.spriteBatch.End(); + } + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + events.RenderedWorld.RaiseEmpty(); + if (Game1.drawGrid) + { + int startingX = -Game1.viewport.X % 64; + float startingY = -Game1.viewport.Y % 64; + for (int x = startingX; x < Game1.graphics.GraphicsDevice.Viewport.Width; x += 64) + { + Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x, (int)startingY, 1, Game1.graphics.GraphicsDevice.Viewport.Height), Color.Red * 0.5f); + } + for (float y = startingY; y < (float)Game1.graphics.GraphicsDevice.Viewport.Height; y += 64f) + { + Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(startingX, (int)y, Game1.graphics.GraphicsDevice.Viewport.Width, 1), Color.Red * 0.5f); + } + } + if (Game1.currentBillboard != 0 && !this.takingMapScreenshot) + { + this.drawBillboard(); + } + if (!Game1.eventUp && Game1.farmEvent == null && Game1.currentBillboard == 0 && Game1.gameMode == 3 && !this.takingMapScreenshot && Game1.isOutdoorMapSmallerThanViewport()) + { + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(0, 0, -Math.Min(Game1.viewport.X, 4096), Game1.graphics.GraphicsDevice.Viewport.Height), Color.Black); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(-Game1.viewport.X + Game1.currentLocation.map.Layers[0].LayerWidth * 64, 0, Math.Min(4096, Game1.graphics.GraphicsDevice.Viewport.Width - (-Game1.viewport.X + Game1.currentLocation.map.Layers[0].LayerWidth * 64)), Game1.graphics.GraphicsDevice.Viewport.Height), Color.Black); + } + if ((Game1.displayHUD || Game1.eventUp) && Game1.currentBillboard == 0 && Game1.gameMode == 3 && !Game1.freezeControls && !Game1.panMode && !Game1.HostPaused && !this.takingMapScreenshot) + { + events.RenderingHud.RaiseEmpty(); + this.drawHUD(); + events.RenderedHud.RaiseEmpty(); + } + else if (Game1.activeClickableMenu == null) + { + _ = Game1.farmEvent; + } + if (Game1.hudMessages.Count > 0 && !this.takingMapScreenshot) + { + for (int j = Game1.hudMessages.Count - 1; j >= 0; j--) + { + Game1.hudMessages[j].draw(Game1.spriteBatch, j); + } + } + } + if (Game1.farmEvent != null) + { + Game1.farmEvent.draw(Game1.spriteBatch); + } + if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && (Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox)) && !this.takingMapScreenshot) + { + this.drawDialogueBox(); + } + if (Game1.progressBar && !this.takingMapScreenshot) + { + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, Game1.dialogueWidth, 32), Color.LightGray); + Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, (int)(Game1.pauseAccumulator / Game1.pauseTime * (float)Game1.dialogueWidth), 32), Color.DimGray); + } + if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null) + { + Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); + } + if (Game1.isRaining && Game1.currentLocation != null && (bool)Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)) + { + Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Blue * 0.2f); + } + if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause) && !this.takingMapScreenshot) + { + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((Game1.gameMode == 0) ? (1f - Game1.fadeToBlackAlpha) : Game1.fadeToBlackAlpha)); + } + else if (Game1.flashAlpha > 0f && !this.takingMapScreenshot) + { + if (Game1.options.screenFlash) + { + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.White * Math.Min(1f, Game1.flashAlpha)); + } + Game1.flashAlpha -= 0.1f; + } + if ((Game1.messagePause || Game1.globalFade) && Game1.dialogueUp && !this.takingMapScreenshot) + { + this.drawDialogueBox(); + } + if (!this.takingMapScreenshot) + { + foreach (TemporaryAnimatedSprite screenOverlayTempSprite in Game1.screenOverlayTempSprites) + { + screenOverlayTempSprite.draw(Game1.spriteBatch, localPosition: true); + } + } + if (Game1.debugMode) + { + StringBuilder sb = Game1._debugStringBuilder; + sb.Clear(); + if (Game1.panMode) + { + sb.Append((Game1.getOldMouseX() + Game1.viewport.X) / 64); + sb.Append(","); + sb.Append((Game1.getOldMouseY() + Game1.viewport.Y) / 64); + } + else + { + sb.Append("player: "); + sb.Append(Game1.player.getStandingX() / 64); + sb.Append(", "); + sb.Append(Game1.player.getStandingY() / 64); + } + sb.Append(" mouseTransparency: "); + sb.Append(Game1.mouseCursorTransparency); + sb.Append(" mousePosition: "); + sb.Append(Game1.getMouseX()); + sb.Append(","); + sb.Append(Game1.getMouseY()); + sb.Append(Environment.NewLine); + sb.Append(" mouseWorldPosition: "); + sb.Append(Game1.getMouseX() + Game1.viewport.X); + sb.Append(","); + sb.Append(Game1.getMouseY() + Game1.viewport.Y); + sb.Append(" debugOutput: "); + sb.Append(Game1.debugOutput); + Game1.spriteBatch.DrawString(Game1.smallFont, sb, new Vector2(base.GraphicsDevice.Viewport.GetTitleSafeArea().X, base.GraphicsDevice.Viewport.GetTitleSafeArea().Y + Game1.smallFont.LineSpacing * 8), Color.Red, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); + } + if (Game1.showKeyHelp && !this.takingMapScreenshot) + { + Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2(64f, (float)(Game1.viewport.Height - 64 - (Game1.dialogueUp ? (192 + (Game1.isQuestion ? (Game1.questionChoices.Count * 64) : 0)) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); + } + if (Game1.activeClickableMenu != null && !this.takingMapScreenshot) + { + try + { + events.RenderingActiveMenu.RaiseEmpty(); + Game1.activeClickableMenu.draw(Game1.spriteBatch); + events.RenderedActiveMenu.RaiseEmpty(); + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } + } + else if (Game1.farmEvent != null) + { + Game1.farmEvent.drawAboveEverything(Game1.spriteBatch); + } + if (Game1.emoteMenu != null && !this.takingMapScreenshot) + { + Game1.emoteMenu.draw(Game1.spriteBatch); + } + if (Game1.HostPaused && !this.takingMapScreenshot) + { + string msg2 = Game1.content.LoadString("Strings\\StringsFromCSFiles:DayTimeMoneyBox.cs.10378"); + SpriteText.drawStringWithScrollBackground(Game1.spriteBatch, msg2, 96, 32); } + events.Rendered.RaiseEmpty(); + Game1.spriteBatch.End(); + this.drawOverlays(Game1.spriteBatch); + this.renderScreenBuffer(target_screen); } } } -- cgit From 046deb2d56b6d4665280cc5478d9e683ec1d777d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 24 Aug 2020 19:25:57 -0400 Subject: simplify console interception flow The console interceptor now uses a marker in the string (instead of a state field) to track whether the message should intercepted. This makes each write more atomic, so it's less affected by multithreading in some cases. --- docs/release-notes.md | 4 +- .../Logging/ConsoleInterceptionManager.cs | 59 ---------------------- .../Framework/Logging/InterceptingTextWriter.cs | 55 +++++++++++++------- src/SMAPI/Framework/Logging/LogManager.cs | 15 +++--- src/SMAPI/Framework/Monitor.cs | 19 +++---- 5 files changed, 56 insertions(+), 96 deletions(-) delete mode 100644 src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2282bc3d..a65db68c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,11 +9,12 @@ ## Upcoming release * For players: - * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent unpredictable errors when enabled. + * 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. * 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. * For modders: * You can now read/write `SDate` values to JSON (e.g. for `config.json`, network mod messages, etc). @@ -23,6 +24,7 @@ * 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 future game updates. ## 3.6.2 Released 02 August 2020 for Stardew Valley 1.4.1 or later. diff --git a/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs b/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs deleted file mode 100644 index ef42e536..00000000 --- a/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.Logging -{ - /// Manages console output interception. - internal class ConsoleInterceptionManager : IDisposable - { - /********* - ** Fields - *********/ - /// The intercepting console writer. - private readonly InterceptingTextWriter Output; - - - /********* - ** Accessors - *********/ - /// The event raised when a message is written to the console directly. - public event Action OnMessageIntercepted; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public ConsoleInterceptionManager() - { - // redirect output through interceptor - this.Output = new InterceptingTextWriter(Console.Out); - this.Output.OnMessageIntercepted += line => this.OnMessageIntercepted?.Invoke(line); - Console.SetOut(this.Output); - } - - /// Get an exclusive lock and write to the console output without interception. - /// The action to perform within the exclusive write block. - public void ExclusiveWriteWithoutInterception(Action action) - { - lock (Console.Out) - { - try - { - this.Output.ShouldIntercept = false; - action(); - } - finally - { - this.Output.ShouldIntercept = true; - } - } - } - - /// Release all resources. - public void Dispose() - { - Console.SetOut(this.Output.Out); - this.Output.Dispose(); - } - } -} diff --git a/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs index 9ca61b59..d99f1dd2 100644 --- a/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs +++ b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs @@ -7,18 +7,22 @@ namespace StardewModdingAPI.Framework.Logging /// A text writer which allows intercepting output. internal class InterceptingTextWriter : TextWriter { + /********* + ** Fields + *********/ + /// Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.) + private readonly char IgnoreChar; + + /********* ** Accessors *********/ /// The underlying console output. public TextWriter Out { get; } - /// The character encoding in which the output is written. + /// public override Encoding Encoding => this.Out.Encoding; - /// Whether to intercept console output. - public bool ShouldIntercept { get; set; } - /// The event raised when a message is written to the console directly. public event Action OnMessageIntercepted; @@ -28,36 +32,53 @@ namespace StardewModdingAPI.Framework.Logging *********/ /// Construct an instance. /// The underlying output writer. - public InterceptingTextWriter(TextWriter output) + /// Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.) + public InterceptingTextWriter(TextWriter output, char ignoreChar) { this.Out = output; + this.IgnoreChar = ignoreChar; } - /// Writes a subarray of characters to the text string or stream. - /// The character array to write data from. - /// The character position in the buffer at which to start retrieving data. - /// The number of characters to write. + /// public override void Write(char[] buffer, int index, int count) { - if (this.ShouldIntercept) - this.OnMessageIntercepted?.Invoke(new string(buffer, index, count).TrimEnd('\r', '\n')); - else + if (buffer.Length == 0) this.Out.Write(buffer, index, count); + else if (buffer[0] == this.IgnoreChar) + this.Out.Write(buffer, index + 1, count - 1); + else if (this.IsEmptyOrNewline(buffer)) + this.Out.Write(buffer, index, count); + else + this.OnMessageIntercepted?.Invoke(new string(buffer, index, count).TrimEnd('\r', '\n')); } - /// Writes a character to the text string or stream. - /// The character to write to the text stream. - /// Console log messages from the game should be caught by . This method passes through anything that bypasses that method for some reason, since it's better to show it to users than hide it from everyone. + /// public override void Write(char ch) { this.Out.Write(ch); } - /// Releases the unmanaged resources used by the and optionally releases the managed resources. - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + /// protected override void Dispose(bool disposing) { this.OnMessageIntercepted = null; } + + + /********* + ** Private methods + *********/ + /// Get whether a buffer represents a line break. + /// The buffer to check. + private bool IsEmptyOrNewline(char[] buffer) + { + foreach (char ch in buffer) + { + if (ch != '\n' && ch != '\r') + return false; + } + + return true; + } } } diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index 3786e940..d0936f3f 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -21,8 +21,8 @@ namespace StardewModdingAPI.Framework.Logging /// The log file to which to write messages. private readonly LogFileManager LogFile; - /// Manages console output interception. - private readonly ConsoleInterceptionManager InterceptionManager = new ConsoleInterceptionManager(); + /// Prefixing a low-level message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.) + private readonly char IgnoreChar = '\u200B'; /// Get a named monitor instance. private readonly Func GetMonitorImpl; @@ -86,7 +86,7 @@ namespace StardewModdingAPI.Framework.Logging 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) + this.GetMonitorImpl = name => new Monitor(name, this.IgnoreChar, this.LogFile, colorConfig, isVerbose) { WriteToConsole = writeToConsole, ShowTraceInConsole = isDeveloperMode, @@ -99,8 +99,10 @@ namespace StardewModdingAPI.Framework.Logging this.MonitorForGame = this.GetMonitor("game"); // redirect direct console output - if (this.MonitorForGame.WriteToConsole) - this.InterceptionManager.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message); + var output = new InterceptingTextWriter(Console.Out, this.IgnoreChar); + if (writeToConsole) + output.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message); + Console.SetOut(output); } /// Get a monitor instance derived from SMAPI's current settings. @@ -167,7 +169,7 @@ namespace StardewModdingAPI.Framework.Logging public void PressAnyKeyToExit(bool showMessage) { if (showMessage) - Console.WriteLine("Game has ended. Press any key to exit."); + this.Monitor.Log("Game has ended. Press any key to exit."); Thread.Sleep(100); Console.ReadKey(); Environment.Exit(0); @@ -339,7 +341,6 @@ namespace StardewModdingAPI.Framework.Logging /// public void Dispose() { - this.InterceptionManager.Dispose(); this.LogFile.Dispose(); } diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 44eeabe6..527cba64 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -18,8 +18,8 @@ namespace StardewModdingAPI.Framework /// Handles writing text to the console. private readonly IConsoleWriter ConsoleWriter; - /// Manages access to the console output. - private readonly ConsoleInterceptionManager ConsoleInterceptor; + /// Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.) + private readonly char IgnoreChar; /// The log file to which to write messages. private readonly LogFileManager LogFile; @@ -52,11 +52,11 @@ namespace StardewModdingAPI.Framework *********/ /// Construct an instance. /// The name of the module which logs messages using this instance. - /// Intercepts access to the console output. + /// A character which indicates the message should not be intercepted if it appears as the first character of a string written to the console. The character itself is not logged in that case. /// The log file to which to write messages. /// The colors to use for text written to the SMAPI console. /// Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed. - public Monitor(string source, ConsoleInterceptionManager consoleInterceptor, LogFileManager logFile, ColorSchemeConfig colorConfig, bool isVerbose) + public Monitor(string source, char ignoreChar, LogFileManager logFile, ColorSchemeConfig colorConfig, bool isVerbose) { // validate if (string.IsNullOrWhiteSpace(source)) @@ -66,7 +66,7 @@ namespace StardewModdingAPI.Framework this.Source = source; this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null."); this.ConsoleWriter = new ColorfulConsoleWriter(Constants.Platform, colorConfig); - this.ConsoleInterceptor = consoleInterceptor; + this.IgnoreChar = ignoreChar; this.IsVerbose = isVerbose; } @@ -99,7 +99,7 @@ namespace StardewModdingAPI.Framework internal void Newline() { if (this.WriteToConsole) - this.ConsoleInterceptor.ExclusiveWriteWithoutInterception(Console.WriteLine); + Console.WriteLine(); this.LogFile.WriteLine(""); } @@ -136,12 +136,7 @@ namespace StardewModdingAPI.Framework // write to console if (this.WriteToConsole && (this.ShowTraceInConsole || level != ConsoleLogLevel.Trace)) - { - this.ConsoleInterceptor.ExclusiveWriteWithoutInterception(() => - { - this.ConsoleWriter.WriteLine(consoleMessage, level); - }); - } + this.ConsoleWriter.WriteLine(this.IgnoreChar + consoleMessage, level); // write to log file this.LogFile.WriteLine(fullMessage); -- 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') 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') 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') 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 915e6d22f199354ef69a20e47f13731379b46306 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 24 Aug 2020 22:23:02 -0400 Subject: minor tweaks --- src/SMAPI/Framework/SCore.cs | 31 +++++++++++++++---------------- src/SMAPI/Framework/SGame.cs | 14 ++++++++++++-- src/SMAPI/Framework/WatcherCore.cs | 5 +++-- 3 files changed, 30 insertions(+), 20 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index c4fb3b5d..a6067867 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -84,7 +84,7 @@ namespace StardewModdingAPI.Framework private readonly CommandManager CommandManager = new CommandManager(); /// The underlying game instance. - private SGame GameInstance; + private SGame Game; /// Manages input visible to the game. private SInputState Input => SGame.Input; @@ -249,7 +249,7 @@ namespace StardewModdingAPI.Framework var multiplayer = new SMultiplayer(this.Monitor, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.Reflection, this.OnModMessageReceived, this.Settings.LogNetworkTraffic); var modHooks = new SModHooks(this.OnNewDayAfterFade); SGame.CreateContentManagerImpl = this.CreateContentManager; // must be static since the game accesses it before the SGame constructor is called - this.GameInstance = new SGame( + this.Game = new SGame( monitor: this.Monitor, reflection: this.Reflection, eventManager: this.EventManager, @@ -259,12 +259,12 @@ namespace StardewModdingAPI.Framework ); // hook game events - this.GameInstance.OnGameContentLoaded += this.OnLoadContent; - this.GameInstance.OnGameUpdating += this.OnGameUpdating; - this.GameInstance.OnGameExiting += this.OnGameExiting; + this.Game.OnGameContentLoaded += this.OnLoadContent; + this.Game.OnGameUpdating += this.OnGameUpdating; + this.Game.OnGameExiting += this.OnGameExiting; this.Translator.SetLocale(this.ContentCore.GetLocale(), this.ContentCore.Language); - StardewValley.Program.gamePtr = this.GameInstance; + StardewValley.Program.gamePtr = this.Game; // apply game patches new GamePatcher(this.Monitor).Apply( @@ -283,12 +283,12 @@ namespace StardewModdingAPI.Framework if (this.IsGameRunning) { this.LogManager.WriteCrashLog(); - this.GameInstance.Exit(); + this.Game.Exit(); } }).Start(); // set window titles - this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; + this.Game.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; this.LogManager.SetConsoleTitle($"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"); } catch (Exception ex) @@ -303,7 +303,7 @@ namespace StardewModdingAPI.Framework this.LogManager.LogSettingsHeader(this.Settings.DeveloperMode, this.Settings.CheckForUpdates); // set window titles - this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; + this.Game.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; this.LogManager.SetConsoleTitle($"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"); // start game @@ -312,7 +312,7 @@ namespace StardewModdingAPI.Framework { this.IsGameRunning = true; StardewValley.Program.releaseBuild = true; // game's debug logic interferes with SMAPI opening the game window - this.GameInstance.Run(); + this.Game.Run(); } catch (Exception ex) { @@ -351,7 +351,7 @@ namespace StardewModdingAPI.Framework this.IsGameRunning = false; this.ContentCore?.Dispose(); this.CancellationToken?.Dispose(); - this.GameInstance?.Dispose(); + this.Game?.Dispose(); this.LogManager?.Dispose(); // dispose last to allow for any last-second log messages // end game (moved from Game1.OnExiting to let us clean up first) @@ -409,7 +409,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"; + this.Game.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; this.LogManager.SetConsoleTitle($"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"); } @@ -420,7 +420,7 @@ namespace StardewModdingAPI.Framework this.Input.TrueUpdate(); // init watchers - this.Watchers = new WatcherCore(this.Input); + this.Watchers = new WatcherCore(this.Input, this.Game.GetObservableLocations()); // validate XNB integrity if (!this.ValidateContentIntegrity()) @@ -477,7 +477,7 @@ namespace StardewModdingAPI.Framework // this too. For example, doing this after mod event suppression would prevent the // user from doing anything on the overnight shipping screen. SInputState inputState = this.Input; - if (this.GameInstance.IsActive) + if (this.Game.IsActive) inputState.TrueUpdate(); /********* @@ -760,7 +760,7 @@ namespace StardewModdingAPI.Framework /********* ** Input events (if window has focus) *********/ - if (this.GameInstance.IsActive) + if (this.Game.IsActive) { // raise events bool isChatInput = Game1.IsChatting || (Context.IsMultiplayer && Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.currentMinigame == null && inputState.IsAnyDown(Game1.options.chatButton)); @@ -1071,7 +1071,6 @@ namespace StardewModdingAPI.Framework private LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) { // Game1._temporaryContent initializing from SGame constructor - // NOTE: this method is called before the SGame constructor runs. Don't depend on anything being initialized at this point. if (this.ContentCore == null) { this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, this.Reflection, this.Toolkit.JsonHelper, this.InitializeBeforeFirstAssetLoaded); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 966fbcdd..9f8a07e6 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -69,7 +69,7 @@ namespace StardewModdingAPI.Framework /********* - ** Protected methods + ** Public methods *********/ /// Construct an instance. /// Encapsulates monitoring and logging for SMAPI. @@ -78,7 +78,7 @@ namespace StardewModdingAPI.Framework /// Handles mod hooks provided by the game. /// The core multiplayer logic. /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. - internal SGame(Monitor monitor, Reflector reflection, EventManager eventManager, SModHooks modHooks, SMultiplayer multiplayer, Action exitGameImmediately) + public SGame(Monitor monitor, Reflector reflection, EventManager eventManager, SModHooks modHooks, SMultiplayer multiplayer, Action exitGameImmediately) { // init XNA Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; @@ -96,6 +96,16 @@ namespace StardewModdingAPI.Framework this.ExitGameImmediately = exitGameImmediately; } + /// Get the observable location list. + public ObservableCollection GetObservableLocations() + { + return (ObservableCollection)Game1.locations; + } + + + /********* + ** Protected methods + *********/ /// Load content when the game is launched. protected override void LoadContent() { diff --git a/src/SMAPI/Framework/WatcherCore.cs b/src/SMAPI/Framework/WatcherCore.cs index c89efa44..2a5d1ee6 100644 --- a/src/SMAPI/Framework/WatcherCore.cs +++ b/src/SMAPI/Framework/WatcherCore.cs @@ -56,7 +56,8 @@ namespace StardewModdingAPI.Framework *********/ /// Construct an instance. /// Manages input visible to the game. - public WatcherCore(SInputState inputState) + /// The observable list of game locations. + public WatcherCore(SInputState inputState, ObservableCollection gameLocations) { // init watchers this.CursorWatcher = WatcherFactory.ForEquatable(() => inputState.CursorPosition); @@ -65,7 +66,7 @@ namespace StardewModdingAPI.Framework this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay); this.ActiveMenuWatcher = WatcherFactory.ForReference(() => Game1.activeClickableMenu); - this.LocationsWatcher = new WorldLocationsTracker((ObservableCollection)Game1.locations, MineShaft.activeMines); + this.LocationsWatcher = new WorldLocationsTracker(gameLocations, MineShaft.activeMines); this.LocaleWatcher = WatcherFactory.ForGenericEquality(() => LocalizedContentManager.CurrentLanguageCode); this.Watchers.AddRange(new IWatcher[] { -- 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') 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') 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') 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 a4938fc4653ad386e9ac88c2e6a0d95347b3f6f7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 25 Aug 2020 23:13:55 -0400 Subject: tweak locale init so it doesn't depend on the game's internal load order --- src/SMAPI/Framework/SCore.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index a6067867..99a809ad 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -257,15 +257,13 @@ namespace StardewModdingAPI.Framework multiplayer: multiplayer, exitGameImmediately: this.ExitGameImmediately ); + StardewValley.Program.gamePtr = this.Game; // hook game events this.Game.OnGameContentLoaded += this.OnLoadContent; this.Game.OnGameUpdating += this.OnGameUpdating; this.Game.OnGameExiting += this.OnGameExiting; - this.Translator.SetLocale(this.ContentCore.GetLocale(), this.ContentCore.Language); - StardewValley.Program.gamePtr = this.Game; - // apply game patches new GamePatcher(this.Monitor).Apply( new EventErrorPatch(this.LogManager.MonitorForGame), @@ -1074,6 +1072,9 @@ namespace StardewModdingAPI.Framework if (this.ContentCore == null) { this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, this.Reflection, this.Toolkit.JsonHelper, this.InitializeBeforeFirstAssetLoaded); + if (this.ContentCore.Language != this.Translator.LocaleEnum) + this.Translator.SetLocale(this.ContentCore.GetLocale(), this.ContentCore.Language); + this.NextContentManagerIsMain = true; return this.ContentCore.CreateGameContentManager("Game1._temporaryContent"); } -- 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') 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') 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') 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') 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') 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 e57fc0eb15ea3976072949453a831d493b9225db Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 26 Aug 2020 23:27:32 -0400 Subject: fix old comment --- src/SMAPI/Framework/Events/EventManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index b5a12a6e..9092669f 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -11,7 +11,7 @@ namespace StardewModdingAPI.Framework.Events internal class EventManager { /********* - ** Events (new) + ** Events *********/ /**** ** Display -- cgit From a1e1b7d10393dc23fa306ef03e392aa03e0b9ee1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 28 Aug 2020 20:03:12 -0400 Subject: fix map tile rotation broken when you return to title --- docs/release-notes.md | 1 + src/SMAPI/Framework/SCore.cs | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 554b9518..6e531dbd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ * 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 map tile rotation broken when you return to the title screen and reload a save. * 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. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 99a809ad..06d9eac1 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -142,6 +142,9 @@ namespace StardewModdingAPI.Framework /// Whether the game is creating the save file and SMAPI has already raised . private bool IsBetweenCreateEvents; + /// Whether the player just returned to the title screen. + private bool JustReturnedToTitle; + /// Asset interceptors added or removed since the last tick. private readonly List ReloadAssetInterceptorsQueue = new List(); @@ -456,9 +459,17 @@ namespace StardewModdingAPI.Framework try { + /********* + ** Safe queued work + *********/ + // print warnings/alerts SCore.DeprecationManager.PrintQueued(); SCore.PerformanceMonitor.PrintQueuedAlerts(); + // reapply overrides + if (this.JustReturnedToTitle && !(Game1.mapDisplayDevice is SDisplayDevice)) + Game1.mapDisplayDevice = new SDisplayDevice(Game1.content, Game1.game1.GraphicsDevice); + /********* ** First-tick initialization *********/ @@ -1039,8 +1050,7 @@ namespace StardewModdingAPI.Framework { // perform cleanup this.Multiplayer.CleanupOnMultiplayerExit(); - if (!(Game1.mapDisplayDevice is SDisplayDevice)) - Game1.mapDisplayDevice = new SDisplayDevice(Game1.content, Game1.game1.GraphicsDevice); + this.JustReturnedToTitle = true; } /// Raised before the game exits. -- cgit From 097df9076d0ab189ca9fe84cc4f61d52b607376a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 29 Aug 2020 23:04:25 -0400 Subject: fix build error on Linux/Mac --- src/SMAPI/Framework/SCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 06d9eac1..53e41afb 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -214,7 +214,7 @@ namespace StardewModdingAPI.Framework 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(); + this.LogManager.PressAnyKeyToExit(); } #endif } -- 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') 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 3a4606164c6ce8d900077b567dc1142f6aad0f4c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 29 Aug 2020 23:06:52 -0400 Subject: tweaks to reduce differences in Android port --- src/SMAPI/Framework/SCore.cs | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 53e41afb..3645fb9c 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -289,8 +289,10 @@ namespace StardewModdingAPI.Framework }).Start(); // set window titles - this.Game.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; - this.LogManager.SetConsoleTitle($"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"); + this.SetWindowTitles( + game: $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}", + smapi: $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}" + ); } catch (Exception ex) { @@ -304,8 +306,10 @@ namespace StardewModdingAPI.Framework this.LogManager.LogSettingsHeader(this.Settings.DeveloperMode, this.Settings.CheckForUpdates); // set window titles - this.Game.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; - this.LogManager.SetConsoleTitle($"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"); + this.SetWindowTitles( + game: $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}", + smapi: $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}" + ); // start game this.Monitor.Log("Starting game...", LogLevel.Debug); @@ -410,8 +414,10 @@ namespace StardewModdingAPI.Framework // update window titles int modsLoaded = this.ModRegistry.GetAll().Count(); - this.Game.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; - this.LogManager.SetConsoleTitle($"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"); + this.SetWindowTitles( + game: $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods", + smapi: $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods" + ); } /// Raised after the game finishes initializing. @@ -818,11 +824,14 @@ namespace StardewModdingAPI.Framework *********/ if (state.ActiveMenu.IsChanged) { + var was = state.ActiveMenu.Old; + var now = state.ActiveMenu.New; + if (this.Monitor.IsVerbose) - this.Monitor.Log($"Context: menu changed from {state.ActiveMenu.Old?.GetType().FullName ?? "none"} to {state.ActiveMenu.New?.GetType().FullName ?? "none"}."); + this.Monitor.Log($"Context: menu changed from {was?.GetType().FullName ?? "none"} to {now?.GetType().FullName ?? "none"}."); // raise menu events - events.MenuChanged.Raise(new MenuChangedEventArgs(state.ActiveMenu.Old, state.ActiveMenu.New)); + events.MenuChanged.Raise(new MenuChangedEventArgs(was, now)); } /********* @@ -1155,6 +1164,15 @@ namespace StardewModdingAPI.Framework return !issuesFound; } + /// Set the window titles for the game and console windows. + /// The game window text. + /// The SMAPI window text. + private void SetWindowTitles(string game, string smapi) + { + this.Game.Window.Title = game; + this.LogManager.SetConsoleTitle(smapi); + } + /// Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available. /// The mods to include in the update check (if eligible). private void CheckForUpdatesAsync(IModMetadata[] mods) -- cgit From 828be405e11dd8bc7f8a3692d2c74517734f67a5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 30 Aug 2020 22:53:19 -0400 Subject: use inheritdoc --- src/SMAPI.Toolkit/SemanticVersion.cs | 47 ++++++++-------------- src/SMAPI/Framework/Content/AssetData.cs | 7 +--- src/SMAPI/Framework/Content/AssetDataForImage.cs | 13 +----- src/SMAPI/Framework/Content/AssetDataForMap.cs | 5 +-- src/SMAPI/Framework/Content/AssetDataForObject.cs | 15 ++----- src/SMAPI/Framework/Content/AssetInfo.cs | 9 ++--- src/SMAPI/Framework/ContentPack.cs | 31 ++++---------- src/SMAPI/Framework/CursorPosition.cs | 11 +++-- src/SMAPI/Framework/GameVersion.cs | 2 +- src/SMAPI/Framework/ModHelpers/BaseHelper.cs | 4 +- src/SMAPI/Framework/ModHelpers/CommandHelper.cs | 13 +----- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 41 +++++-------------- .../Framework/ModHelpers/ContentPackHelper.cs | 13 ++---- src/SMAPI/Framework/ModHelpers/DataHelper.cs | 34 +++------------- src/SMAPI/Framework/ModHelpers/InputHelper.cs | 14 +++---- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 31 +++++++------- .../Framework/ModHelpers/ModRegistryHelper.cs | 15 +++---- .../Framework/ModHelpers/MultiplayerHelper.cs | 19 +++------ src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs | 34 +++------------- .../Framework/ModHelpers/TranslationHelper.cs | 13 +++--- src/SMAPI/Framework/Monitor.cs | 13 ++---- src/SMAPI/Framework/Networking/MultiplayerPeer.cs | 18 ++++----- .../Framework/Networking/MultiplayerPeerMod.cs | 6 +-- src/SMAPI/Framework/Reflection/ReflectedField.cs | 7 ++-- src/SMAPI/Framework/Reflection/ReflectedMethod.cs | 9 ++--- .../Framework/Reflection/ReflectedProperty.cs | 7 ++-- src/SMAPI/Patches/DialogueErrorPatch.cs | 6 +-- src/SMAPI/Patches/EventErrorPatch.cs | 5 +-- src/SMAPI/Patches/LoadContextPatch.cs | 5 +-- src/SMAPI/Patches/LoadErrorPatch.cs | 5 +-- src/SMAPI/Patches/ObjectErrorPatch.cs | 5 +-- src/SMAPI/Patches/ScheduleErrorPatch.cs | 5 +-- src/SMAPI/SemanticVersion.cs | 47 ++++++++-------------- 33 files changed, 160 insertions(+), 349 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI.Toolkit/SemanticVersion.cs b/src/SMAPI.Toolkit/SemanticVersion.cs index 1a76bec3..0f341665 100644 --- a/src/SMAPI.Toolkit/SemanticVersion.cs +++ b/src/SMAPI.Toolkit/SemanticVersion.cs @@ -25,22 +25,22 @@ namespace StardewModdingAPI.Toolkit /********* ** Accessors *********/ - /// The major version incremented for major API changes. + /// public int MajorVersion { get; } - /// The minor version incremented for backwards-compatible changes. + /// public int MinorVersion { get; } - /// The patch version for backwards-compatible bug fixes. + /// public int PatchVersion { get; } /// The platform release. This is a non-standard semver extension used by Stardew Valley on ported platforms to represent platform-specific patches to a ported version, represented as a fourth number in the version string. public int PlatformRelease { get; } - /// An optional prerelease tag. + /// public string PrereleaseTag { get; } - /// Optional build metadata. This is ignored when determining version precedence. + /// public string BuildMetadata { get; } @@ -103,9 +103,7 @@ namespace StardewModdingAPI.Toolkit this.AssertValid(); } - /// Get an integer indicating whether this version precedes (less than 0), supersedes (more than 0), or is equivalent to (0) the specified version. - /// The version to compare with this instance. - /// The value is null. + /// public int CompareTo(ISemanticVersion other) { if (other == null) @@ -113,68 +111,55 @@ namespace StardewModdingAPI.Toolkit return this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, (other as SemanticVersion)?.PlatformRelease ?? 0, other.PrereleaseTag); } - /// Indicates whether the current object is equal to another object of the same type. - /// true if the current object is equal to the parameter; otherwise, false. - /// An object to compare with this object. + /// public bool Equals(ISemanticVersion other) { return other != null && this.CompareTo(other) == 0; } - /// Whether this is a prerelease version. + /// public bool IsPrerelease() { return !string.IsNullOrWhiteSpace(this.PrereleaseTag); } - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. + /// public bool IsOlderThan(ISemanticVersion other) { return this.CompareTo(other) < 0; } - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. + /// public bool IsOlderThan(string other) { return this.IsOlderThan(new SemanticVersion(other, allowNonStandard: true)); } - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. + /// public bool IsNewerThan(ISemanticVersion other) { return this.CompareTo(other) > 0; } - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. + /// public bool IsNewerThan(string other) { return this.IsNewerThan(new SemanticVersion(other, allowNonStandard: true)); } - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. + /// public bool IsBetween(ISemanticVersion min, ISemanticVersion max) { return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0; } - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - /// One of the specified versions is not a valid semantic version. + /// public bool IsBetween(string min, string max) { return this.IsBetween(new SemanticVersion(min, allowNonStandard: true), new SemanticVersion(max, allowNonStandard: true)); } - /// Get a string representation of the version. + /// public override string ToString() { string version = $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}"; @@ -187,7 +172,7 @@ namespace StardewModdingAPI.Toolkit return version; } - /// Whether the version uses non-standard extensions, like four-part game versions on some platforms. + /// public bool IsNonStandard() { return this.PlatformRelease != 0; diff --git a/src/SMAPI/Framework/Content/AssetData.cs b/src/SMAPI/Framework/Content/AssetData.cs index cacc6078..5c90d83b 100644 --- a/src/SMAPI/Framework/Content/AssetData.cs +++ b/src/SMAPI/Framework/Content/AssetData.cs @@ -16,7 +16,7 @@ namespace StardewModdingAPI.Framework.Content /********* ** Accessors *********/ - /// The content data being read. + /// public TValue Data { get; protected set; } @@ -36,10 +36,7 @@ namespace StardewModdingAPI.Framework.Content this.OnDataReplaced = onDataReplaced; } - /// Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game. - /// The new content value. - /// The is null. - /// The 's type is not compatible with the loaded asset's type. + /// public void ReplaceWith(TValue value) { if (value == null) diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 44a97136..5f91610e 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -28,13 +28,7 @@ namespace StardewModdingAPI.Framework.Content public AssetDataForImage(string locale, string assetName, Texture2D data, Func getNormalizedPath, Action onDataReplaced) : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { } - /// Overwrite part of the image. - /// The image to patch into the content. - /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. - /// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet. - /// Indicates how an image should be patched. - /// One of the arguments is null. - /// The is outside the bounds of the spritesheet. + /// public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) { // get texture @@ -104,10 +98,7 @@ namespace StardewModdingAPI.Framework.Content target.SetData(0, targetArea, sourceData, 0, pixelCount); } - /// Extend the image if needed to fit the given size. Note that this is an expensive operation, creates a new texture instance, and that extending a spritesheet horizontally may cause game errors or bugs. - /// The minimum texture width. - /// The minimum texture height. - /// Whether the texture was resized. + /// public bool ExtendImage(int minWidth, int minHeight) { if (this.Data.Width >= minWidth && this.Data.Height >= minHeight) diff --git a/src/SMAPI/Framework/Content/AssetDataForMap.cs b/src/SMAPI/Framework/Content/AssetDataForMap.cs index dee5b034..e80c6e53 100644 --- a/src/SMAPI/Framework/Content/AssetDataForMap.cs +++ b/src/SMAPI/Framework/Content/AssetDataForMap.cs @@ -24,10 +24,7 @@ namespace StardewModdingAPI.Framework.Content public AssetDataForMap(string locale, string assetName, Map data, Func getNormalizedPath, Action onDataReplaced) : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { } - /// Copy layers, tiles, and tilesheets from another map onto the asset. - /// The map from which to copy. - /// The tile area within the source map to copy, or null for the entire source map size. This must be within the bounds of the map. - /// The tile area within the target map to overwrite, or null to patch the whole map. The original content within this area will be erased. This must be within the bounds of the existing map. + /// /// Derived from with a few changes: /// - can be applied directly to the maps when loading, before the location is created; /// - added support for source/target areas; diff --git a/src/SMAPI/Framework/Content/AssetDataForObject.cs b/src/SMAPI/Framework/Content/AssetDataForObject.cs index f00ba124..b7e8dfeb 100644 --- a/src/SMAPI/Framework/Content/AssetDataForObject.cs +++ b/src/SMAPI/Framework/Content/AssetDataForObject.cs @@ -26,32 +26,25 @@ namespace StardewModdingAPI.Framework.Content public AssetDataForObject(IAssetInfo info, object data, Func getNormalizedPath) : this(info.Locale, info.AssetName, data, getNormalizedPath) { } - /// Get a helper to manipulate the data as a dictionary. - /// The expected dictionary key. - /// The expected dictionary balue. - /// The content being read isn't a dictionary. + /// public IAssetDataForDictionary AsDictionary() { return new AssetDataForDictionary(this.Locale, this.AssetName, this.GetData>(), this.GetNormalizedPath, this.ReplaceWith); } - /// Get a helper to manipulate the data as an image. - /// The content being read isn't an image. + /// public IAssetDataForImage AsImage() { return new AssetDataForImage(this.Locale, this.AssetName, this.GetData(), this.GetNormalizedPath, this.ReplaceWith); } - /// Get a helper to manipulate the data as a map. - /// The content being read isn't a map. + /// public IAssetDataForMap AsMap() { return new AssetDataForMap(this.Locale, this.AssetName, this.GetData(), this.GetNormalizedPath, this.ReplaceWith); } - /// Get the data as a given type. - /// The expected data type. - /// The data can't be converted to . + /// public TData GetData() { if (!(this.Data is TData)) diff --git a/src/SMAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs index ed009499..d8106439 100644 --- a/src/SMAPI/Framework/Content/AssetInfo.cs +++ b/src/SMAPI/Framework/Content/AssetInfo.cs @@ -16,13 +16,13 @@ namespace StardewModdingAPI.Framework.Content /********* ** Accessors *********/ - /// The content's locale code, if the content is localized. + /// public string Locale { get; } - /// The normalized asset name being read. The format may change between platforms; see to compare with a known path. + /// public string AssetName { get; } - /// The content data type. + /// public Type DataType { get; } @@ -42,8 +42,7 @@ namespace StardewModdingAPI.Framework.Content this.GetNormalizedPath = getNormalizedPath; } - /// Get whether the asset name being loaded matches a given name after normalization. - /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). + /// public bool AssetNameEquals(string path) { path = this.GetNormalizedPath(path); diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index 9c0bb9d1..43621141 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -24,13 +24,13 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ - /// The full path to the content pack's folder. + /// public string DirectoryPath { get; } - /// The content pack's manifest. + /// public IManifest Manifest { get; } - /// Provides translations stored in the content pack's i18n folder. See for more info. + /// public ITranslationHelper Translation { get; } @@ -52,8 +52,7 @@ namespace StardewModdingAPI.Framework this.JsonHelper = jsonHelper; } - /// Get whether a given file exists in the content pack. - /// The file path to check. + /// public bool HasFile(string path) { this.AssertRelativePath(path, nameof(this.HasFile)); @@ -61,11 +60,7 @@ namespace StardewModdingAPI.Framework return File.Exists(Path.Combine(this.DirectoryPath, path)); } - /// Read a JSON file from the content pack folder. - /// The model type. - /// The file path relative to the content directory. - /// Returns the deserialized model, or null if the file doesn't exist or is empty. - /// The is not relative or contains directory climbing (../). + /// public TModel ReadJsonFile(string path) where TModel : class { this.AssertRelativePath(path, nameof(this.ReadJsonFile)); @@ -76,11 +71,7 @@ namespace StardewModdingAPI.Framework : null; } - /// Save data to a JSON file in the content pack's folder. - /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. - /// The file path relative to the mod folder. - /// The arbitrary data to save. - /// The is not relative or contains directory climbing (../). + /// public void WriteJsonFile(string path, TModel data) where TModel : class { this.AssertRelativePath(path, nameof(this.WriteJsonFile)); @@ -89,19 +80,13 @@ namespace StardewModdingAPI.Framework this.JsonHelper.WriteJsonFile(path, data); } - /// Load content from the content pack folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. - /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline. - /// The local path to a content file relative to the content pack folder. - /// The is empty or contains invalid characters. - /// The content asset couldn't be loaded (e.g. because it doesn't exist). + /// public T LoadAsset(string key) { return this.Content.Load(key, ContentSource.ModFolder); } - /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. - /// The the local path to a content file relative to the content pack folder. - /// The is empty or contains invalid characters. + /// public string GetActualAssetKey(string key) { return this.Content.GetActualAssetKey(key, ContentSource.ModFolder); diff --git a/src/SMAPI/Framework/CursorPosition.cs b/src/SMAPI/Framework/CursorPosition.cs index 2008ccce..80d89994 100644 --- a/src/SMAPI/Framework/CursorPosition.cs +++ b/src/SMAPI/Framework/CursorPosition.cs @@ -8,16 +8,16 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ - /// The pixel position relative to the top-left corner of the in-game map, adjusted for pixel zoom. + /// public Vector2 AbsolutePixels { get; } - /// The pixel position relative to the top-left corner of the visible screen, adjusted for pixel zoom. + /// public Vector2 ScreenPixels { get; } - /// The tile position under the cursor relative to the top-left corner of the map. + /// public Vector2 Tile { get; } - /// The tile position that the game considers under the cursor for purposes of clicking actions. This may be different than if that's too far from the player. + /// public Vector2 GrabTile { get; } @@ -37,8 +37,7 @@ namespace StardewModdingAPI.Framework this.GrabTile = grabTile; } - /// Get whether the current object is equal to another object of the same type. - /// An object to compare with this object. + /// public bool Equals(ICursorPosition other) { return other != null && this.AbsolutePixels == other.AbsolutePixels; diff --git a/src/SMAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs index 3ed60920..b69c6757 100644 --- a/src/SMAPI/Framework/GameVersion.cs +++ b/src/SMAPI/Framework/GameVersion.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI.Framework public GameVersion(string version) : base(GameVersion.GetSemanticVersionString(version), allowNonStandard: true) { } - /// Get a string representation of the version. + /// public override string ToString() { return GameVersion.GetGameVersionString(base.ToString()); diff --git a/src/SMAPI/Framework/ModHelpers/BaseHelper.cs b/src/SMAPI/Framework/ModHelpers/BaseHelper.cs index 16032da1..5a3d4bed 100644 --- a/src/SMAPI/Framework/ModHelpers/BaseHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/BaseHelper.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI.Framework.ModHelpers +namespace StardewModdingAPI.Framework.ModHelpers { /// The common base class for mod helpers. internal abstract class BaseHelper : IModLinked @@ -6,7 +6,7 @@ /********* ** Accessors *********/ - /// The unique ID of the mod for which the helper was created. + /// public string ModID { get; } diff --git a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs index e9d53d84..600f867f 100644 --- a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs @@ -28,23 +28,14 @@ namespace StardewModdingAPI.Framework.ModHelpers this.CommandManager = commandManager; } - /// Add a console command. - /// The command name, which the user must type to trigger it. - /// The human-readable documentation shown when the player runs the built-in 'help' command. - /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. - /// The or is null or empty. - /// The is not a valid format. - /// There's already a command with that name. + /// public ICommandHelper Add(string name, string documentation, Action callback) { this.CommandManager.Add(this.Mod, name, documentation, callback); return this; } - /// Trigger a command. - /// The command name. - /// The command arguments. - /// Returns whether a matching command was triggered. + /// public bool Trigger(string name, string[] arguments) { return this.CommandManager.Trigger(name, arguments); diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 23e45fd1..80f61c13 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -40,10 +40,10 @@ namespace StardewModdingAPI.Framework.ModHelpers /********* ** Accessors *********/ - /// The game's current locale code (like pt-BR). + /// public string CurrentLocale => this.GameContentManager.GetLocale(); - /// The game's current locale as an enum value. + /// public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.GameContentManager.Language; /// The observable implementation of . @@ -52,10 +52,10 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The observable implementation of . internal ObservableCollection ObservableAssetLoaders { get; } = new ObservableCollection(); - /// Interceptors which provide the initial versions of matching content assets. + /// public IList AssetLoaders => this.ObservableAssetLoaders; - /// Interceptors which edit matching content assets after they're loaded. + /// public IList AssetEditors => this.ObservableAssetEditors; @@ -80,12 +80,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Monitor = monitor; } - /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. - /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline. - /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. - /// Where to search for a matching content asset. - /// The is empty or contains invalid characters. - /// The content asset couldn't be loaded (e.g. because it doesn't exist). + /// public T Load(string key, ContentSource source = ContentSource.ModFolder) { try @@ -109,18 +104,14 @@ namespace StardewModdingAPI.Framework.ModHelpers } } - /// Normalize an asset name so it's consistent with those generated by the game. This is mainly useful for string comparisons like on generated asset names, and isn't necessary when passing asset names into other content helper methods. - /// The asset key. + /// [Pure] public string NormalizeAssetName(string assetName) { return this.ModContentManager.AssertAndNormalizeAssetName(assetName); } - /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. - /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. - /// Where to search for a matching content asset. - /// The is empty or contains invalid characters. + /// public string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder) { switch (source) @@ -136,10 +127,7 @@ namespace StardewModdingAPI.Framework.ModHelpers } } - /// Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. - /// The asset key to invalidate in the content folder. - /// The is empty or contains invalid characters. - /// Returns whether the given asset key was cached. + /// public bool InvalidateCache(string key) { string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent); @@ -147,28 +135,21 @@ namespace StardewModdingAPI.Framework.ModHelpers return this.ContentCore.InvalidateCache(asset => asset.AssetNameEquals(actualKey)).Any(); } - /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. - /// The asset type to remove from the cache. - /// Returns whether any assets were invalidated. + /// public bool InvalidateCache() { this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.", LogLevel.Trace); return this.ContentCore.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)).Any(); } - /// Remove matching assets from the content cache so they're reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. - /// A predicate matching the assets to invalidate. - /// Returns whether any cache entries were invalidated. + /// public bool InvalidateCache(Func predicate) { this.Monitor.Log("Requested cache invalidation for all assets matching a predicate.", LogLevel.Trace); return this.ContentCore.InvalidateCache(predicate).Any(); } - /// Get a patch helper for arbitrary data. - /// The data type. - /// The asset data. - /// The asset name. This is only used for tracking purposes and has no effect on the patch helper. + /// public IAssetData GetPatchHelper(T data, string assetName = null) { if (data == null) diff --git a/src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs index acdd82a0..d39abc7d 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs @@ -32,27 +32,20 @@ namespace StardewModdingAPI.Framework.ModHelpers this.CreateContentPack = createContentPack; } - /// Get all content packs loaded for this mod. + /// public IEnumerable GetOwned() { return this.ContentPacks.Value; } - /// Create a temporary content pack to read files from a directory, using randomized manifest fields. This will generate fake manifest data; any manifest.json in the directory will be ignored. Temporary content packs will not appear in the SMAPI log and update checks will not be performed. - /// The absolute directory path containing the content pack files. + /// public IContentPack CreateFake(string directoryPath) { string id = Guid.NewGuid().ToString("N"); return this.CreateTemporary(directoryPath, id, id, id, id, new SemanticVersion(1, 0, 0)); } - /// Create a temporary content pack to read files from a directory. Temporary content packs will not appear in the SMAPI log and update checks will not be performed. - /// The absolute directory path containing the content pack files. - /// The content pack's unique ID. - /// The content pack name. - /// The content pack description. - /// The content pack author's name. - /// The content pack version. + /// public IContentPack CreateTemporary(string directoryPath, string id, string name, string description, string author, ISemanticVersion version) { // validate diff --git a/src/SMAPI/Framework/ModHelpers/DataHelper.cs b/src/SMAPI/Framework/ModHelpers/DataHelper.cs index 6cde849c..c232a6dd 100644 --- a/src/SMAPI/Framework/ModHelpers/DataHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/DataHelper.cs @@ -39,11 +39,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /**** ** JSON file ****/ - /// Read data from a JSON file in the mod's folder. - /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. - /// The file path relative to the mod folder. - /// Returns the deserialized model, or null if the file doesn't exist or is empty. - /// The is not relative or contains directory climbing (../). + /// public TModel ReadJsonFile(string path) where TModel : class { if (!PathUtilities.IsSafeRelativePath(path)) @@ -55,11 +51,7 @@ namespace StardewModdingAPI.Framework.ModHelpers : null; } - /// Save data to a JSON file in the mod's folder. - /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. - /// The file path relative to the mod folder. - /// The arbitrary data to save. - /// The is not relative or contains directory climbing (../). + /// public void WriteJsonFile(string path, TModel data) where TModel : class { if (!PathUtilities.IsSafeRelativePath(path)) @@ -72,11 +64,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /**** ** Save file ****/ - /// Read arbitrary data stored in the current save slot. This is only possible if a save has been loaded. - /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. - /// The unique key identifying the data. - /// Returns the parsed data, or null if the entry doesn't exist or is empty. - /// The player hasn't loaded a save file yet or isn't the main player. + /// public TModel ReadSaveData(string key) where TModel : class { if (Context.LoadStage == LoadStage.None) @@ -94,11 +82,7 @@ namespace StardewModdingAPI.Framework.ModHelpers return null; } - /// Save arbitrary data to the current save slot. This is only possible if a save has been loaded, and the data will be lost if the player exits without saving the current day. - /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. - /// The unique key identifying the data. - /// The arbitrary data to save. - /// The player hasn't loaded a save file yet or isn't the main player. + /// public void WriteSaveData(string key, TModel model) where TModel : class { if (Context.LoadStage == LoadStage.None) @@ -123,10 +107,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /**** ** Global app data ****/ - /// Read arbitrary data stored on the local computer, synchronised by GOG/Steam if applicable. - /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. - /// The unique key identifying the data. - /// Returns the parsed data, or null if the entry doesn't exist or is empty. + /// public TModel ReadGlobalData(string key) where TModel : class { string path = this.GetGlobalDataPath(key); @@ -135,10 +116,7 @@ namespace StardewModdingAPI.Framework.ModHelpers : null; } - /// Save arbitrary data to the local computer, synchronised by GOG/Steam if applicable. - /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. - /// The unique key identifying the data. - /// The arbitrary data to save. + /// public void WriteGlobalData(string key, TModel data) where TModel : class { string path = this.GetGlobalDataPath(key); diff --git a/src/SMAPI/Framework/ModHelpers/InputHelper.cs b/src/SMAPI/Framework/ModHelpers/InputHelper.cs index 134ba8d1..09ce3c65 100644 --- a/src/SMAPI/Framework/ModHelpers/InputHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/InputHelper.cs @@ -24,35 +24,31 @@ namespace StardewModdingAPI.Framework.ModHelpers this.InputState = inputState; } - /// Get the current cursor position. + /// public ICursorPosition GetCursorPosition() { return this.InputState.CursorPosition; } - /// Get whether a button is currently pressed. - /// The button. + /// public bool IsDown(SButton button) { return this.InputState.IsDown(button); } - /// Get whether a button is currently suppressed, so the game won't see it. - /// The button. + /// public bool IsSuppressed(SButton button) { return this.InputState.IsSuppressed(button); } - /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. - /// The button to suppress. + /// public void Suppress(SButton button) { this.InputState.OverrideButton(button, setDown: false); } - /// Get the state of a button. - /// The button to check. + /// public SButtonState GetState(SButton button) { return this.InputState.GetState(button); diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 9fbb6072..d9fc8621 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -11,37 +11,37 @@ namespace StardewModdingAPI.Framework.ModHelpers /********* ** Accessors *********/ - /// The full path to the mod's folder. + /// public string DirectoryPath { get; } - /// Manages access to events raised by SMAPI, which let your mod react when something happens in the game. + /// public IModEvents Events { get; } - /// An API for loading content assets. + /// public IContentHelper Content { get; } - /// An API for managing content packs. + /// public IContentPackHelper ContentPacks { get; } - /// An API for reading and writing persistent mod data. + /// public IDataHelper Data { get; } - /// An API for checking and changing input state. + /// public IInputHelper Input { get; } - /// An API for accessing private game code. + /// public IReflectionHelper Reflection { get; } - /// an API for fetching metadata about loaded mods. + /// public IModRegistry ModRegistry { get; } - /// An API for managing console commands. + /// public ICommandHelper ConsoleCommands { get; } - /// Provides multiplayer utilities. + /// public IMultiplayerHelper Multiplayer { get; } - /// An API for reading translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). + /// public ITranslationHelper Translation { get; } @@ -89,8 +89,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /**** ** Mod config file ****/ - /// Read the mod's configuration file (and create it if needed). - /// The config class type. This should be a plain class that has public properties for the settings you want. These can be complex types. + /// public TConfig ReadConfig() where TConfig : class, new() { @@ -99,9 +98,7 @@ namespace StardewModdingAPI.Framework.ModHelpers return config; } - /// Save to the mod's configuration file. - /// The config class type. - /// The config settings to save. + /// public void WriteConfig(TConfig config) where TConfig : class, new() { @@ -111,7 +108,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /**** ** Disposal ****/ - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// public void Dispose() { // nothing to dispose yet diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index f42cb085..ef1ad30c 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -38,28 +38,25 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Monitor = monitor; } - /// Get metadata for all loaded mods. + /// public IEnumerable GetAll() { return this.Registry.GetAll(); } - /// Get metadata for a loaded mod. - /// The mod's unique ID. - /// Returns the matching mod's metadata, or null if not found. + /// public IModInfo Get(string uniqueID) { return this.Registry.Get(uniqueID); } - /// Get whether a mod has been loaded. - /// The mod's unique ID. + /// public bool IsLoaded(string uniqueID) { return this.Registry.Get(uniqueID) != null; } - /// Get the API provided by a mod, or null if it has none. This signature requires using the API to access the API's properties and methods. + /// public object GetApi(string uniqueID) { // validate ready @@ -76,9 +73,7 @@ namespace StardewModdingAPI.Framework.ModHelpers return mod?.Api; } - /// Get the API provided by a mod, mapped to a given interface which specifies the expected properties and methods. If the mod has no API or it's not compatible with the given interface, get null. - /// The interface which matches the properties and methods you intend to access. - /// The mod's unique ID. + /// public TInterface GetApi(string uniqueID) where TInterface : class { // get raw API diff --git a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs index c62dd121..a7ce8692 100644 --- a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using StardewModdingAPI.Framework.Networking; using StardewValley; @@ -27,21 +26,19 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Multiplayer = multiplayer; } - /// Get a new multiplayer ID. + /// public long GetNewID() { return this.Multiplayer.getNewID(); } - /// Get the locations which are being actively synced from the host. + /// public IEnumerable GetActiveLocations() { return this.Multiplayer.activeLocations(); } - /// Get a connected player. - /// The player's unique ID. - /// Returns the connected player, or null if no such player is connected. + /// public IMultiplayerPeer GetConnectedPlayer(long id) { return this.Multiplayer.Peers.TryGetValue(id, out MultiplayerPeer peer) @@ -49,19 +46,13 @@ namespace StardewModdingAPI.Framework.ModHelpers : null; } - /// Get all connected players. + /// public IEnumerable GetConnectedPlayers() { return this.Multiplayer.Peers.Values; } - /// Send a message to mods installed by connected players. - /// The data type. This can be a class with a default constructor, or a value type. - /// The data to send over the network. - /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. - /// The mod IDs which should receive the message on the destination computers, or null for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast. - /// The values for the players who should receive the message, or null for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency. - /// The or is null. + /// public void SendMessage(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null) { this.Multiplayer.BroadcastModMessage( diff --git a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs index 916c215d..5a4ea742 100644 --- a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -32,11 +32,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Reflector = reflector; } - /// Get an instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the field is not found. + /// public IReflectedField GetField(object obj, string name, bool required = true) { return this.AssertAccessAllowed( @@ -44,11 +40,7 @@ namespace StardewModdingAPI.Framework.ModHelpers ); } - /// Get a static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the field is not found. + /// public IReflectedField GetField(Type type, string name, bool required = true) { return this.AssertAccessAllowed( @@ -56,11 +48,7 @@ namespace StardewModdingAPI.Framework.ModHelpers ); } - /// Get an instance property. - /// The property type. - /// The object which has the property. - /// The property name. - /// Whether to throw an exception if the property is not found. + /// public IReflectedProperty GetProperty(object obj, string name, bool required = true) { return this.AssertAccessAllowed( @@ -68,11 +56,7 @@ namespace StardewModdingAPI.Framework.ModHelpers ); } - /// Get a static property. - /// The property type. - /// The type which has the property. - /// The property name. - /// Whether to throw an exception if the property is not found. + /// public IReflectedProperty GetProperty(Type type, string name, bool required = true) { return this.AssertAccessAllowed( @@ -80,10 +64,7 @@ namespace StardewModdingAPI.Framework.ModHelpers ); } - /// Get an instance method. - /// The object which has the method. - /// The field name. - /// Whether to throw an exception if the field is not found. + /// public IReflectedMethod GetMethod(object obj, string name, bool required = true) { return this.AssertAccessAllowed( @@ -91,10 +72,7 @@ namespace StardewModdingAPI.Framework.ModHelpers ); } - /// Get a static method. - /// The type which has the method. - /// The field name. - /// Whether to throw an exception if the field is not found. + /// public IReflectedMethod GetMethod(Type type, string name, bool required = true) { return this.AssertAccessAllowed( diff --git a/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs index be7768e8..a88ca9c9 100644 --- a/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs @@ -16,10 +16,10 @@ namespace StardewModdingAPI.Framework.ModHelpers /********* ** Accessors *********/ - /// The current locale. + /// public string Locale => this.Translator.Locale; - /// The game's current language code. + /// public LocalizedContentManager.LanguageCode LocaleEnum => this.Translator.LocaleEnum; @@ -37,22 +37,19 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Translator.SetLocale(locale, languageCode); } - /// Get all translations for the current locale. + /// public IEnumerable GetTranslations() { return this.Translator.GetTranslations(); } - /// Get a translation for the current locale. - /// The translation key. + /// public Translation Get(string key) { return this.Translator.Get(key); } - /// Get a translation for the current locale. - /// The translation key. - /// An object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }), a dictionary, or a class instance. + /// public Translation Get(string key, object tokens) { return this.Translator.Get(key, tokens); diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 527cba64..533420a5 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -34,7 +34,7 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ - /// Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed. + /// public bool IsVerbose { get; } /// Whether to show the full log stamps (with time/level/logger) in the console. If false, shows a simplified stamp with only the logger. @@ -70,25 +70,20 @@ namespace StardewModdingAPI.Framework this.IsVerbose = isVerbose; } - /// Log a message for the player or developer. - /// The message to log. - /// The log severity level. + /// public void Log(string message, LogLevel level = LogLevel.Trace) { this.LogImpl(this.Source, message, (ConsoleLogLevel)level); } - /// Log a message for the player or developer, but only if it hasn't already been logged since the last game launch. - /// The message to log. - /// The log severity level. + /// public void LogOnce(string message, LogLevel level = LogLevel.Trace) { if (this.LogOnceCache.Add($"{message}|{level}")) this.LogImpl(this.Source, message, (ConsoleLogLevel)level); } - /// Log a message that only appears when is enabled. - /// The message to log. + /// public void VerboseLog(string message) { if (this.IsVerbose) diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs index 6b45b04a..5eda71f6 100644 --- a/src/SMAPI/Framework/Networking/MultiplayerPeer.cs +++ b/src/SMAPI/Framework/Networking/MultiplayerPeer.cs @@ -18,25 +18,25 @@ namespace StardewModdingAPI.Framework.Networking /********* ** Accessors *********/ - /// The player's unique ID. + /// public long PlayerID { get; } - /// Whether this is a connection to the host player. + /// public bool IsHost { get; } - /// Whether the player has SMAPI installed. + /// public bool HasSmapi => this.ApiVersion != null; - /// The player's OS platform, if is true. + /// public GamePlatform? Platform { get; } - /// The installed version of Stardew Valley, if is true. + /// public ISemanticVersion GameVersion { get; } - /// The installed version of SMAPI, if is true. + /// public ISemanticVersion ApiVersion { get; } - /// The installed mods, if is true. + /// public IEnumerable Mods { get; } @@ -62,9 +62,7 @@ namespace StardewModdingAPI.Framework.Networking this.SendMessageImpl = sendMessage; } - /// Get metadata for a mod installed by the player. - /// The unique mod ID. - /// Returns the mod info, or null if the player doesn't have that mod. + /// public IMultiplayerPeerMod GetMod(string id) { if (string.IsNullOrWhiteSpace(id) || this.Mods == null || !this.Mods.Any()) diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs b/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs index 1b324bcd..8087dc7e 100644 --- a/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs +++ b/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs @@ -5,13 +5,13 @@ namespace StardewModdingAPI.Framework.Networking /********* ** Accessors *********/ - /// The mod's display name. + /// public string Name { get; } - /// The unique mod ID. + /// public string ID { get; } - /// The mod version. + /// public ISemanticVersion Version { get; } diff --git a/src/SMAPI/Framework/Reflection/ReflectedField.cs b/src/SMAPI/Framework/Reflection/ReflectedField.cs index d771422c..3c4da4fc 100644 --- a/src/SMAPI/Framework/Reflection/ReflectedField.cs +++ b/src/SMAPI/Framework/Reflection/ReflectedField.cs @@ -23,7 +23,7 @@ namespace StardewModdingAPI.Framework.Reflection /********* ** Accessors *********/ - /// The reflection metadata. + /// public FieldInfo FieldInfo { get; } @@ -55,7 +55,7 @@ namespace StardewModdingAPI.Framework.Reflection this.FieldInfo = field; } - /// Get the field value. + /// public TValue GetValue() { try @@ -72,8 +72,7 @@ namespace StardewModdingAPI.Framework.Reflection } } - /// Set the field value. - //// The value to set. + /// public void SetValue(TValue value) { try diff --git a/src/SMAPI/Framework/Reflection/ReflectedMethod.cs b/src/SMAPI/Framework/Reflection/ReflectedMethod.cs index 82737a7f..26112806 100644 --- a/src/SMAPI/Framework/Reflection/ReflectedMethod.cs +++ b/src/SMAPI/Framework/Reflection/ReflectedMethod.cs @@ -22,7 +22,7 @@ namespace StardewModdingAPI.Framework.Reflection /********* ** Accessors *********/ - /// The reflection metadata. + /// public MethodInfo MethodInfo { get; } @@ -54,9 +54,7 @@ namespace StardewModdingAPI.Framework.Reflection this.MethodInfo = method; } - /// Invoke the method. - /// The return type. - /// The method arguments to pass in. + /// public TValue Invoke(params object[] arguments) { // invoke method @@ -85,8 +83,7 @@ namespace StardewModdingAPI.Framework.Reflection } } - /// Invoke the method. - /// The method arguments to pass in. + /// public void Invoke(params object[] arguments) { // invoke method diff --git a/src/SMAPI/Framework/Reflection/ReflectedProperty.cs b/src/SMAPI/Framework/Reflection/ReflectedProperty.cs index 8a10ff9a..42d7bb59 100644 --- a/src/SMAPI/Framework/Reflection/ReflectedProperty.cs +++ b/src/SMAPI/Framework/Reflection/ReflectedProperty.cs @@ -23,7 +23,7 @@ namespace StardewModdingAPI.Framework.Reflection /********* ** Accessors *********/ - /// The reflection metadata. + /// public PropertyInfo PropertyInfo { get; } @@ -61,7 +61,7 @@ namespace StardewModdingAPI.Framework.Reflection this.SetMethod = (Action)Delegate.CreateDelegate(typeof(Action), obj, this.PropertyInfo.SetMethod); } - /// Get the property value. + /// public TValue GetValue() { if (this.GetMethod == null) @@ -81,8 +81,7 @@ namespace StardewModdingAPI.Framework.Reflection } } - /// Set the property value. - //// The value to set. + /// public void SetValue(TValue value) { if (this.SetMethod == null) diff --git a/src/SMAPI/Patches/DialogueErrorPatch.cs b/src/SMAPI/Patches/DialogueErrorPatch.cs index 8043eda3..42494390 100644 --- a/src/SMAPI/Patches/DialogueErrorPatch.cs +++ b/src/SMAPI/Patches/DialogueErrorPatch.cs @@ -33,7 +33,7 @@ namespace StardewModdingAPI.Patches /********* ** Accessors *********/ - /// A unique name for this patch. + /// public string Name => nameof(DialogueErrorPatch); @@ -50,8 +50,7 @@ namespace StardewModdingAPI.Patches } - /// Apply the Harmony patch. - /// The Harmony instance. + /// #if HARMONY_2 public void Apply(Harmony harmony) { @@ -78,6 +77,7 @@ namespace StardewModdingAPI.Patches } #endif + /********* ** Private methods *********/ diff --git a/src/SMAPI/Patches/EventErrorPatch.cs b/src/SMAPI/Patches/EventErrorPatch.cs index 4dbb25f3..8fa882d4 100644 --- a/src/SMAPI/Patches/EventErrorPatch.cs +++ b/src/SMAPI/Patches/EventErrorPatch.cs @@ -27,7 +27,7 @@ namespace StardewModdingAPI.Patches /********* ** Accessors *********/ - /// A unique name for this patch. + /// public string Name => nameof(EventErrorPatch); @@ -41,8 +41,7 @@ namespace StardewModdingAPI.Patches EventErrorPatch.MonitorForGame = monitorForGame; } - /// Apply the Harmony patch. - /// The Harmony instance. + /// #if HARMONY_2 public void Apply(Harmony harmony) { diff --git a/src/SMAPI/Patches/LoadContextPatch.cs b/src/SMAPI/Patches/LoadContextPatch.cs index 768ddd6b..ceda061b 100644 --- a/src/SMAPI/Patches/LoadContextPatch.cs +++ b/src/SMAPI/Patches/LoadContextPatch.cs @@ -33,7 +33,7 @@ namespace StardewModdingAPI.Patches /********* ** Accessors *********/ - /// A unique name for this patch. + /// public string Name => nameof(LoadContextPatch); @@ -49,8 +49,7 @@ namespace StardewModdingAPI.Patches LoadContextPatch.OnStageChanged = onStageChanged; } - /// Apply the Harmony patch. - /// The Harmony instance. + /// #if HARMONY_2 public void Apply(Harmony harmony) #else diff --git a/src/SMAPI/Patches/LoadErrorPatch.cs b/src/SMAPI/Patches/LoadErrorPatch.cs index 5e67b169..f5ee5d71 100644 --- a/src/SMAPI/Patches/LoadErrorPatch.cs +++ b/src/SMAPI/Patches/LoadErrorPatch.cs @@ -34,7 +34,7 @@ namespace StardewModdingAPI.Patches /********* ** Accessors *********/ - /// A unique name for this patch. + /// public string Name => nameof(LoadErrorPatch); @@ -51,8 +51,7 @@ namespace StardewModdingAPI.Patches } - /// Apply the Harmony patch. - /// The Harmony instance. + /// #if HARMONY_2 public void Apply(Harmony harmony) #else diff --git a/src/SMAPI/Patches/ObjectErrorPatch.cs b/src/SMAPI/Patches/ObjectErrorPatch.cs index 4edcc64e..64b8e6b6 100644 --- a/src/SMAPI/Patches/ObjectErrorPatch.cs +++ b/src/SMAPI/Patches/ObjectErrorPatch.cs @@ -23,15 +23,14 @@ namespace StardewModdingAPI.Patches /********* ** Accessors *********/ - /// A unique name for this patch. + /// public string Name => nameof(ObjectErrorPatch); /********* ** Public methods *********/ - /// Apply the Harmony patch. - /// The Harmony instance. + /// #if HARMONY_2 public void Apply(Harmony harmony) #else diff --git a/src/SMAPI/Patches/ScheduleErrorPatch.cs b/src/SMAPI/Patches/ScheduleErrorPatch.cs index cc2238b0..17db07a6 100644 --- a/src/SMAPI/Patches/ScheduleErrorPatch.cs +++ b/src/SMAPI/Patches/ScheduleErrorPatch.cs @@ -29,7 +29,7 @@ namespace StardewModdingAPI.Patches /********* ** Accessors *********/ - /// A unique name for this patch. + /// public string Name => nameof(ScheduleErrorPatch); @@ -43,8 +43,7 @@ namespace StardewModdingAPI.Patches ScheduleErrorPatch.MonitorForGame = monitorForGame; } - /// Apply the Harmony patch. - /// The Harmony instance. + /// #if HARMONY_2 public void Apply(Harmony harmony) #else diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index 4a175efe..ae616419 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -16,19 +16,19 @@ namespace StardewModdingAPI /********* ** Accessors *********/ - /// The major version incremented for major API changes. + /// public int MajorVersion => this.Version.MajorVersion; - /// The minor version incremented for backwards-compatible changes. + /// public int MinorVersion => this.Version.MinorVersion; - /// The patch version for backwards-compatible bug fixes. + /// public int PatchVersion => this.Version.PatchVersion; - /// An optional prerelease tag. + /// public string PrereleaseTag => this.Version.PrereleaseTag; - /// Optional build metadata. This is ignored when determining version precedence. + /// public string BuildMetadata => this.Version.BuildMetadata; @@ -83,83 +83,68 @@ namespace StardewModdingAPI this.Version = version; } - /// Whether this is a prerelease version. + /// public bool IsPrerelease() { return this.Version.IsPrerelease(); } - /// Get an integer indicating whether this version precedes (less than 0), supersedes (more than 0), or is equivalent to (0) the specified version. - /// The version to compare with this instance. - /// The value is null. + /// /// The implementation is defined by Semantic Version 2.0 (https://semver.org/). public int CompareTo(ISemanticVersion other) { return this.Version.CompareTo(other); } - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. + /// public bool IsOlderThan(ISemanticVersion other) { return this.Version.IsOlderThan(other); } - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. + /// public bool IsOlderThan(string other) { return this.Version.IsOlderThan(other); } - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. + /// public bool IsNewerThan(ISemanticVersion other) { return this.Version.IsNewerThan(other); } - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. + /// public bool IsNewerThan(string other) { return this.Version.IsNewerThan(other); } - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. + /// public bool IsBetween(ISemanticVersion min, ISemanticVersion max) { return this.Version.IsBetween(min, max); } - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - /// One of the specified versions is not a valid semantic version. + /// public bool IsBetween(string min, string max) { return this.Version.IsBetween(min, max); } - /// Indicates whether the current object is equal to another object of the same type. - /// true if the current object is equal to the parameter; otherwise, false. - /// An object to compare with this object. + /// public bool Equals(ISemanticVersion other) { return other != null && this.CompareTo(other) == 0; } - /// Get a string representation of the version. + /// public override string ToString() { return this.Version.ToString(); } - /// Whether the version uses non-standard extensions, like four-part game versions on some platforms. + /// public bool IsNonStandard() { return this.Version.IsNonStandard(); -- cgit From 685d56894f837fd9739a7dc021adc9b13d56be00 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 31 Aug 2020 20:43:03 -0400 Subject: switch SGame back to callbacks Callbacks are simpler and more efficient in this case. --- src/SMAPI/Framework/SCore.cs | 13 ++++++------- src/SMAPI/Framework/SGame.cs | 32 +++++++++++++++++++------------- 2 files changed, 25 insertions(+), 20 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 3645fb9c..eedbfc64 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -258,15 +258,14 @@ namespace StardewModdingAPI.Framework eventManager: this.EventManager, modHooks: modHooks, multiplayer: multiplayer, - exitGameImmediately: this.ExitGameImmediately + exitGameImmediately: this.ExitGameImmediately, + + onGameContentLoaded: this.OnGameContentLoaded, + onGameUpdating: this.OnGameUpdating, + onGameExiting: this.OnGameExiting ); StardewValley.Program.gamePtr = this.Game; - // hook game events - this.Game.OnGameContentLoaded += this.OnLoadContent; - this.Game.OnGameUpdating += this.OnGameUpdating; - this.Game.OnGameExiting += this.OnGameExiting; - // apply game patches new GamePatcher(this.Monitor).Apply( new EventErrorPatch(this.LogManager.MonitorForGame), @@ -445,7 +444,7 @@ namespace StardewModdingAPI.Framework } /// Raised after the game finishes loading its initial content. - private void OnLoadContent() + private void OnGameContentLoaded() { // override map display device Game1.mapDisplayDevice = new SDisplayDevice(Game1.content, Game1.game1.GraphicsDevice); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 9f8a07e6..ae2c028d 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -41,6 +41,15 @@ namespace StardewModdingAPI.Framework /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. private readonly Action ExitGameImmediately; + /// Raised after the game finishes loading its initial content. + private readonly Action OnGameContentLoaded; + + /// Raised when the game is updating its state (roughly 60 times per second). + private readonly Action OnGameUpdating; + + /// Raised before the game exits. + private readonly Action OnGameExiting; + /********* ** Accessors @@ -58,15 +67,6 @@ namespace StardewModdingAPI.Framework /// This must be static because the game accesses it before the constructor is called. public static Func CreateContentManagerImpl; - /// Raised after the game finishes loading its initial content. - public event Action OnGameContentLoaded; - - /// Raised before the game exits. - public event Action OnGameExiting; - - /// Raised when the game is updating its state (roughly 60 times per second). - public event Action OnGameUpdating; - /********* ** Public methods @@ -78,7 +78,10 @@ namespace StardewModdingAPI.Framework /// Handles mod hooks provided by the game. /// The core multiplayer logic. /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. - public SGame(Monitor monitor, Reflector reflection, EventManager eventManager, SModHooks modHooks, SMultiplayer multiplayer, Action exitGameImmediately) + /// Raised after the game finishes loading its initial content. + /// Raised when the game is updating its state (roughly 60 times per second). + /// Raised before the game exits. + public SGame(Monitor monitor, Reflector reflection, EventManager eventManager, SModHooks modHooks, SMultiplayer multiplayer, Action exitGameImmediately, Action onGameContentLoaded, Action onGameUpdating, Action onGameExiting) { // init XNA Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; @@ -94,6 +97,9 @@ namespace StardewModdingAPI.Framework this.Events = eventManager; this.Reflection = reflection; this.ExitGameImmediately = exitGameImmediately; + this.OnGameContentLoaded = onGameContentLoaded; + this.OnGameUpdating = onGameUpdating; + this.OnGameExiting = onGameExiting; } /// Get the observable location list. @@ -111,7 +117,7 @@ namespace StardewModdingAPI.Framework { base.LoadContent(); - this.OnGameContentLoaded?.Invoke(); + this.OnGameContentLoaded(); } /// Perform cleanup logic when the game exits. @@ -120,7 +126,7 @@ namespace StardewModdingAPI.Framework /// This overrides the logic in to let SMAPI clean up before exit. protected override void OnExiting(object sender, EventArgs args) { - this.OnGameExiting?.Invoke(); + this.OnGameExiting(); } /// Construct a content manager to read game content files. @@ -138,7 +144,7 @@ namespace StardewModdingAPI.Framework /// A snapshot of the game timing state. protected override void Update(GameTime gameTime) { - this.OnGameUpdating?.Invoke(gameTime, () => base.Update(gameTime)); + this.OnGameUpdating(gameTime, () => base.Update(gameTime)); } /// The method called to draw everything to the screen. -- cgit From f57feb7319725513fadde8b14d55f4e8e4b82c24 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 4 Sep 2020 20:56:27 -0400 Subject: extend game's input logic instead of replacing it --- docs/release-notes.md | 2 +- src/SMAPI/Framework/Input/GamePadStateBuilder.cs | 31 ++--- src/SMAPI/Framework/Input/IInputStateBuilder.cs | 4 - src/SMAPI/Framework/Input/KeyboardStateBuilder.cs | 17 +-- src/SMAPI/Framework/Input/MouseStateBuilder.cs | 39 +++--- src/SMAPI/Framework/Input/SInputState.cs | 157 ++++++++++------------ src/SMAPI/Framework/SCore.cs | 6 +- src/SMAPI/Framework/WatcherCore.cs | 2 +- 8 files changed, 109 insertions(+), 149 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 6e531dbd..ae636153 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,7 +9,7 @@ ## Upcoming release * For players: - * Added heuristic compatibility rewrites, which fix some mods previously incompatible with Android or newer game versions. + * Added heuristic compatibility rewrites. (This fixes some mods previously broken on Android, and improves compatibility with future game updates.) * 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 map tile rotation broken when you return to the title screen and reload a save. diff --git a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs index 2657fd12..f5f2d916 100644 --- a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs +++ b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs @@ -20,7 +20,7 @@ namespace StardewModdingAPI.Framework.Input private GamePadState? State; /// The current button states. - private IDictionary ButtonStates; + private readonly IDictionary ButtonStates; /// The left trigger value. private float LeftTrigger; @@ -39,33 +39,26 @@ namespace StardewModdingAPI.Framework.Input ** Accessors *********/ /// Whether the gamepad is currently connected. - public bool IsConnected { get; private set; } + public bool IsConnected { get; } /********* ** Public methods *********/ /// Construct an instance. - /// The initial state, or null to get the latest state. - public GamePadStateBuilder(GamePadState? state = null) + /// The initial state. + public GamePadStateBuilder(GamePadState state) { - this.Reset(state); - } - - /// Reset the tracked state. - /// The state from which to reset, or null to get the latest state. - public GamePadStateBuilder Reset(GamePadState? state = null) - { - this.State = state ??= GamePad.GetState(PlayerIndex.One); - this.IsConnected = state.Value.IsConnected; + this.State = state; + this.IsConnected = state.IsConnected; if (!this.IsConnected) - return this; + return; - GamePadDPad pad = state.Value.DPad; - GamePadButtons buttons = state.Value.Buttons; - GamePadTriggers triggers = state.Value.Triggers; - GamePadThumbSticks sticks = state.Value.ThumbSticks; + GamePadDPad pad = state.DPad; + GamePadButtons buttons = state.Buttons; + GamePadTriggers triggers = state.Triggers; + GamePadThumbSticks sticks = state.ThumbSticks; this.ButtonStates = new Dictionary { [SButton.DPadUp] = pad.Up, @@ -89,8 +82,6 @@ namespace StardewModdingAPI.Framework.Input this.RightTrigger = triggers.Right; this.LeftStickPos = sticks.Left; this.RightStickPos = sticks.Right; - - return this; } /// Override the states for a set of buttons. diff --git a/src/SMAPI/Framework/Input/IInputStateBuilder.cs b/src/SMAPI/Framework/Input/IInputStateBuilder.cs index 193e5216..28d62439 100644 --- a/src/SMAPI/Framework/Input/IInputStateBuilder.cs +++ b/src/SMAPI/Framework/Input/IInputStateBuilder.cs @@ -12,10 +12,6 @@ namespace StardewModdingAPI.Framework.Input /********* ** Methods *********/ - /// Reset the tracked state. - /// The state from which to reset, or null to get the latest state. - THandler Reset(TState? state = null); - /// Override the states for a set of buttons. /// The button state overrides. THandler OverrideButtons(IDictionary overrides); diff --git a/src/SMAPI/Framework/Input/KeyboardStateBuilder.cs b/src/SMAPI/Framework/Input/KeyboardStateBuilder.cs index f95a28bf..620ad442 100644 --- a/src/SMAPI/Framework/Input/KeyboardStateBuilder.cs +++ b/src/SMAPI/Framework/Input/KeyboardStateBuilder.cs @@ -21,23 +21,14 @@ namespace StardewModdingAPI.Framework.Input ** Public methods *********/ /// Construct an instance. - /// The initial state, or null to get the latest state. - public KeyboardStateBuilder(KeyboardState? state = null) + /// The initial state. + public KeyboardStateBuilder(KeyboardState state) { - this.Reset(state); - } - - /// Reset the tracked state. - /// The state from which to reset, or null to get the latest state. - public KeyboardStateBuilder Reset(KeyboardState? state = null) - { - this.State = state ??= Keyboard.GetState(); + this.State = state; this.PressedButtons.Clear(); - foreach (var button in state.Value.GetPressedKeys()) + foreach (var button in state.GetPressedKeys()) this.PressedButtons.Add(button); - - return this; } /// Override the states for a set of buttons. diff --git a/src/SMAPI/Framework/Input/MouseStateBuilder.cs b/src/SMAPI/Framework/Input/MouseStateBuilder.cs index 1cc16ca9..a1ac5492 100644 --- a/src/SMAPI/Framework/Input/MouseStateBuilder.cs +++ b/src/SMAPI/Framework/Input/MouseStateBuilder.cs @@ -13,51 +13,42 @@ namespace StardewModdingAPI.Framework.Input private MouseState? State; /// The current button states. - private IDictionary ButtonStates; + private readonly IDictionary ButtonStates; /// The mouse wheel scroll value. - private int ScrollWheelValue; + private readonly int ScrollWheelValue; /********* ** Accessors *********/ /// The X cursor position. - public int X { get; private set; } + public int X { get; } /// The Y cursor position. - public int Y { get; private set; } + public int Y { get; } /********* ** Public methods *********/ /// Construct an instance. - /// The initial state, or null to get the latest state. - public MouseStateBuilder(MouseState? state = null) + /// The initial state. + public MouseStateBuilder(MouseState state) { - this.Reset(state); - } - - /// Reset the tracked state. - /// The state from which to reset, or null to get the latest state. - public MouseStateBuilder Reset(MouseState? state = null) - { - this.State = state ??= Mouse.GetState(); + this.State = state; this.ButtonStates = new Dictionary { - [SButton.MouseLeft] = state.Value.LeftButton, - [SButton.MouseMiddle] = state.Value.MiddleButton, - [SButton.MouseRight] = state.Value.RightButton, - [SButton.MouseX1] = state.Value.XButton1, - [SButton.MouseX2] = state.Value.XButton2 + [SButton.MouseLeft] = state.LeftButton, + [SButton.MouseMiddle] = state.MiddleButton, + [SButton.MouseRight] = state.RightButton, + [SButton.MouseX1] = state.XButton1, + [SButton.MouseX2] = state.XButton2 }; - this.X = state.Value.X; - this.Y = state.Value.Y; - this.ScrollWheelValue = state.Value.ScrollWheelValue; - - return this; + this.X = state.X; + this.Y = state.Y; + this.ScrollWheelValue = state.ScrollWheelValue; } /// Override the states for a set of buttons. diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index 333f5726..3dfeb152 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -29,21 +29,24 @@ namespace StardewModdingAPI.Framework.Input /// Whether there are new overrides in or that haven't been applied to the previous state. private bool HasNewOverrides; + /// The game tick when the input state was last updated. + private uint? LastUpdateTick; + /********* ** Accessors *********/ - /// The controller state as of the last update. - public GamePadState LastController { get; private set; } + /// The controller state as of the last update, with overrides applied. + public GamePadState ControllerState { get; private set; } - /// The keyboard state as of the last update. - public KeyboardState LastKeyboard { get; private set; } + /// The keyboard state as of the last update, with overrides applied. + public KeyboardState KeyboardState { get; private set; } - /// The mouse state as of the last update. - public MouseState LastMouse { get; private set; } + /// The mouse state as of the last update, with overrides applied. + public MouseState MouseState { get; private set; } /// The buttons which were pressed, held, or released as of the last update. - public IDictionary LastButtonStates { get; private set; } = new Dictionary(); + public IDictionary ButtonStates { get; private set; } = new Dictionary(); /// The cursor position on the screen adjusted for the zoom level. public ICursorPosition CursorPosition => this.CursorPositionImpl; @@ -52,54 +55,26 @@ namespace StardewModdingAPI.Framework.Input /********* ** Public methods *********/ - /// Get a copy of the current state. - public SInputState Clone() - { - return new SInputState - { - LastButtonStates = this.LastButtonStates, - LastController = this.LastController, - LastKeyboard = this.LastKeyboard, - LastMouse = this.LastMouse, - CursorPositionImpl = this.CursorPositionImpl - }; - } - - /// Override the state for a button. - /// The button to override. - /// Whether to mark it pressed; else mark it released. - public void OverrideButton(SButton button, bool setDown) + /// Update the current button states for the given tick. This does nothing if the input has already been updated for this tick (e.g. because SMAPI updated it before the game update). + public override void Update() { - bool changed = setDown - ? this.CustomPressedKeys.Add(button) | this.CustomReleasedKeys.Remove(button) - : this.CustomPressedKeys.Remove(button) | this.CustomReleasedKeys.Add(button); + // skip if already updated + if (this.LastUpdateTick == SCore.TicksElapsed) + return; + this.LastUpdateTick = SCore.TicksElapsed; - if (changed) - this.HasNewOverrides = true; - } + // update base state + base.Update(); - /// Get whether a mod has indicated the key was already handled, so the game shouldn't handle it. - /// The button to check. - public bool IsSuppressed(SButton button) - { - return this.CustomReleasedKeys.Contains(button); - } - - /// This method is called by the game, and does nothing since SMAPI will already have updated by that point. - [Obsolete("This method should only be called by the game itself.")] - public override void Update() { } - - /// Update the current button states for the given tick. - public void TrueUpdate() - { + // update SMAPI extended data try { float zoomMultiplier = (1f / Game1.options.zoomLevel); // get real values - var controller = new GamePadStateBuilder(); - var keyboard = new KeyboardStateBuilder(); - var mouse = new MouseStateBuilder(); + var controller = new GamePadStateBuilder(base.GetGamePadState()); + var keyboard = new KeyboardStateBuilder(base.GetKeyboardState()); + var mouse = new MouseStateBuilder(base.GetMouseState()); Vector2 cursorAbsolutePos = new Vector2((mouse.X * zoomMultiplier) + Game1.viewport.X, (mouse.Y * zoomMultiplier) + Game1.viewport.Y); Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : (Vector2?)null; HashSet reallyDown = new HashSet(this.GetPressedButtons(keyboard, mouse, controller)); @@ -124,18 +99,18 @@ namespace StardewModdingAPI.Framework.Input var pressedButtons = hasOverrides ? new HashSet(this.GetPressedButtons(keyboard, mouse, controller)) : reallyDown; - var activeButtons = this.DeriveStates(this.LastButtonStates, pressedButtons); + var activeButtons = this.DeriveStates(this.ButtonStates, pressedButtons); // update this.HasNewOverrides = false; - this.LastController = controller.GetState(); - this.LastKeyboard = keyboard.GetState(); - this.LastMouse = mouse.GetState(); - this.LastButtonStates = activeButtons; + this.ControllerState = controller.GetState(); + this.KeyboardState = keyboard.GetState(); + this.MouseState = mouse.GetState(); + this.ButtonStates = activeButtons; if (cursorAbsolutePos != this.CursorPositionImpl?.AbsolutePixels || playerTilePos != this.LastPlayerTile) { this.LastPlayerTile = playerTilePos; - this.CursorPositionImpl = this.GetCursorPosition(this.LastMouse, cursorAbsolutePos, zoomMultiplier); + this.CursorPositionImpl = this.GetCursorPosition(this.MouseState, cursorAbsolutePos, zoomMultiplier); } } catch (InvalidOperationException) @@ -144,53 +119,67 @@ namespace StardewModdingAPI.Framework.Input } } - /// Apply input overrides to the current state. - public void ApplyOverrides() - { - if (this.HasNewOverrides) - { - var controller = new GamePadStateBuilder(this.LastController); - var keyboard = new KeyboardStateBuilder(this.LastKeyboard); - var mouse = new MouseStateBuilder(this.LastMouse); - - if (this.ApplyOverrides(pressed: this.CustomPressedKeys, released: this.CustomReleasedKeys, controller, keyboard, mouse)) - { - this.LastController = controller.GetState(); - this.LastKeyboard = keyboard.GetState(); - this.LastMouse = mouse.GetState(); - } - } - } - /// Get the gamepad state visible to the game. - [Obsolete("This method should only be called by the game itself.")] public override GamePadState GetGamePadState() { - if (Game1.options.gamepadMode == Options.GamepadModes.ForceOff) - return new GamePadState(); - - return this.LastController; + return this.ControllerState; } /// Get the keyboard state visible to the game. - [Obsolete("This method should only be called by the game itself.")] public override KeyboardState GetKeyboardState() { - return this.LastKeyboard; + return this.KeyboardState; } /// Get the keyboard state visible to the game. - [Obsolete("This method should only be called by the game itself.")] public override MouseState GetMouseState() { - return this.LastMouse; + return this.MouseState; + } + + /// Override the state for a button. + /// The button to override. + /// Whether to mark it pressed; else mark it released. + public void OverrideButton(SButton button, bool setDown) + { + bool changed = setDown + ? this.CustomPressedKeys.Add(button) | this.CustomReleasedKeys.Remove(button) + : this.CustomPressedKeys.Remove(button) | this.CustomReleasedKeys.Add(button); + + if (changed) + this.HasNewOverrides = true; + } + + /// Get whether a mod has indicated the key was already handled, so the game shouldn't handle it. + /// The button to check. + public bool IsSuppressed(SButton button) + { + return this.CustomReleasedKeys.Contains(button); + } + + /// Apply input overrides to the current state. + public void ApplyOverrides() + { + if (this.HasNewOverrides) + { + var controller = new GamePadStateBuilder(this.ControllerState); + var keyboard = new KeyboardStateBuilder(this.KeyboardState); + var mouse = new MouseStateBuilder(this.MouseState); + + if (this.ApplyOverrides(pressed: this.CustomPressedKeys, released: this.CustomReleasedKeys, controller, keyboard, mouse)) + { + this.ControllerState = controller.GetState(); + this.KeyboardState = keyboard.GetState(); + this.MouseState = mouse.GetState(); + } + } } /// Get whether a given button was pressed or held. /// The button to check. public bool IsDown(SButton button) { - return this.GetState(this.LastButtonStates, button).IsDown(); + return this.GetState(this.ButtonStates, button).IsDown(); } /// Get whether any of the given buttons were pressed or held. @@ -204,7 +193,7 @@ namespace StardewModdingAPI.Framework.Input /// The button to check. public SButtonState GetState(SButton button) { - return this.GetState(this.LastButtonStates, button); + return this.GetState(this.ButtonStates, button); } @@ -305,7 +294,9 @@ namespace StardewModdingAPI.Framework.Input /// The button to check. private SButtonState GetState(IDictionary activeButtons, SButton button) { - return activeButtons.TryGetValue(button, out SButtonState state) ? state : SButtonState.None; + return activeButtons.TryGetValue(button, out SButtonState state) + ? state + : SButtonState.None; } /// Get the buttons pressed in the given stats. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index eedbfc64..bfe6e277 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -423,7 +423,7 @@ namespace StardewModdingAPI.Framework private void OnGameInitialized() { // set initial state - this.Input.TrueUpdate(); + this.Input.Update(); // init watchers this.Watchers = new WatcherCore(this.Input, this.Game.GetObservableLocations()); @@ -492,7 +492,7 @@ namespace StardewModdingAPI.Framework // user from doing anything on the overnight shipping screen. SInputState inputState = this.Input; if (this.Game.IsActive) - inputState.TrueUpdate(); + inputState.Update(); /********* ** Special cases @@ -795,7 +795,7 @@ namespace StardewModdingAPI.Framework } // raise input button events - foreach (var pair in inputState.LastButtonStates) + foreach (var pair in inputState.ButtonStates) { SButton button = pair.Key; SButtonState status = pair.Value; diff --git a/src/SMAPI/Framework/WatcherCore.cs b/src/SMAPI/Framework/WatcherCore.cs index 2a5d1ee6..393f6a37 100644 --- a/src/SMAPI/Framework/WatcherCore.cs +++ b/src/SMAPI/Framework/WatcherCore.cs @@ -61,7 +61,7 @@ namespace StardewModdingAPI.Framework { // init watchers this.CursorWatcher = WatcherFactory.ForEquatable(() => inputState.CursorPosition); - this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => inputState.LastMouse.ScrollWheelValue); + this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => inputState.MouseState.ScrollWheelValue); this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0); this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay); -- 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') 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') 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