From 0e304e4d51857e3c7dc9cd18141176e44934755c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 7 May 2017 01:56:39 -0400 Subject: added basic context logging to simplify troubleshooting --- src/StardewModdingAPI/Framework/SGame.cs | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 1f2bf3ac..dbc257e1 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -1118,6 +1118,8 @@ namespace StardewModdingAPI.Framework { if (this.AfterLoadTimer == 0) { + this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}'.", LogLevel.Trace); + SaveEvents.InvokeAfterLoad(this.Monitor); PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame)); TimeEvents.InvokeAfterDayStarted(this.Monitor); @@ -1132,6 +1134,7 @@ namespace StardewModdingAPI.Framework // after exit to title if (this.IsWorldReady && this.IsExiting && Game1.activeClickableMenu is TitleMenu) { + this.Monitor.Log("Context: returned to title", LogLevel.Trace); SaveEvents.InvokeAfterReturnToTitle(this.Monitor); this.AfterLoadTimer = 5; this.IsExiting = false; @@ -1199,9 +1202,13 @@ namespace StardewModdingAPI.Framework // raise save events // (saving is performed by SaveGameMenu; on days when the player shipping something, ShippingMenu wraps SaveGameMenu) if (newMenu is SaveGameMenu || newMenu is ShippingMenu) + { + this.Monitor.Log("Context: before save.", LogLevel.Trace); SaveEvents.InvokeBeforeSave(this.Monitor); + } else if (previousMenu is SaveGameMenu || previousMenu is ShippingMenu) { + this.Monitor.Log("Context: after save, starting new day.", LogLevel.Trace); SaveEvents.InvokeAfterSave(this.Monitor); TimeEvents.InvokeAfterDayStarted(this.Monitor); } -- cgit From 8963793bf854bee368b5cd08a3e0eb410ee8e1a9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 7 May 2017 02:50:36 -0400 Subject: exit game after many consecutive unrecoverable draw errors (#283) --- src/StardewModdingAPI/Framework/SGame.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index dbc257e1..f8226529 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -34,6 +34,12 @@ namespace StardewModdingAPI.Framework /// Skipping a few frames ensures the game finishes initialising the world before mods try to change it. private int AfterLoadTimer = 5; + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. + private readonly int MaxFailedDraws = 120; // roughly two seconds + + /// The number of consecutive failed draws. + private int FailedDraws = 0; + /// Whether the player has loaded a save and the world has finished initialising. private bool IsWorldReady => this.AfterLoadTimer < 0; @@ -944,9 +950,20 @@ namespace StardewModdingAPI.Framework } } } + + // reset failed draw count + this.FailedDraws = 0; } catch (Exception ex) { + // exit if irrecoverable + if (this.FailedDraws >= this.MaxFailedDraws) + { + this.Monitor.ExitGameImmediately("the game crashed when drawing, and SMAPI was unable to recover the game."); + return; + } + this.FailedDraws++; + // log error this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error); -- cgit From 624840efe5f3d4135dafeb2939b182cfeb4ec6c3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 7 May 2017 13:09:32 -0400 Subject: use more robust sprite batch recovery logic (#283) --- .../Framework/InternalExtensions.cs | 22 ++++++++++++++++++ src/StardewModdingAPI/Framework/SGame.cs | 27 +++++++++++----------- 2 files changed, 36 insertions(+), 13 deletions(-) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/StardewModdingAPI/Framework/InternalExtensions.cs index 5199c72d..cadf6598 100644 --- a/src/StardewModdingAPI/Framework/InternalExtensions.cs +++ b/src/StardewModdingAPI/Framework/InternalExtensions.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; namespace StardewModdingAPI.Framework { @@ -128,5 +130,25 @@ namespace StardewModdingAPI.Framework deprecationManager.Warn(modName, nounPhrase, version, severity); } } + + /**** + ** Sprite batch + ****/ + /// Get whether the sprite batch is between a begin and end pair. + /// The sprite batch to check. + /// The reflection helper with which to access private fields. + public static bool IsOpen(this SpriteBatch spriteBatch, IReflectionHelper reflection) + { + // get field name + const string fieldName = +#if SMAPI_FOR_WINDOWS + "inBeginEndPair"; +#else + "_beginCalled"; +#endif + + // get result + return reflection.GetPrivateValue(Game1.spriteBatch, fieldName); + } } } diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index f8226529..7dae937b 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI.Framework private readonly int MaxFailedDraws = 120; // roughly two seconds /// The number of consecutive failed draws. - private int FailedDraws = 0; + private int FailedDraws; /// Whether the player has loaded a save and the world has finished initialising. private bool IsWorldReady => this.AfterLoadTimer < 0; @@ -956,6 +956,9 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { + // log error + this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error); + // exit if irrecoverable if (this.FailedDraws >= this.MaxFailedDraws) { @@ -964,22 +967,20 @@ namespace StardewModdingAPI.Framework } this.FailedDraws++; - // log error - this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error); - - // fix sprite batch + // recover sprite batch try { - bool isSpriteBatchOpen = -#if SMAPI_FOR_WINDOWS - SGame.Reflection.GetPrivateValue(Game1.spriteBatch, "inBeginEndPair"); -#else - SGame.Reflection.GetPrivateValue(Game1.spriteBatch, "_beginCalled"); -#endif - if (isSpriteBatchOpen) + if (Game1.spriteBatch.IsOpen(SGame.Reflection)) { this.Monitor.Log("Recovering sprite batch from error...", LogLevel.Trace); - Game1.spriteBatch.End(); + try + { + Game1.spriteBatch.End(); + } + catch + { + Game1.spriteBatch = new SpriteBatch(this.GraphicsDevice); // sprite batch is broken, try replacing it + } } } catch (Exception innerEx) -- cgit From 72a0b4fc6d268c67c3fba171793d5864f9710276 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 8 May 2017 01:57:07 -0400 Subject: detect unrecoverable draw errors (#283) --- src/StardewModdingAPI/Framework/SGame.cs | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 7dae937b..6932af2a 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -967,6 +967,13 @@ namespace StardewModdingAPI.Framework } this.FailedDraws++; + // abort in known unrecoverable cases + if (Game1.toolSpriteSheet?.IsDisposed == true) + { + this.Monitor.ExitGameImmediately("the game unexpectedly disposed the tool spritesheet, so it crashed trying to draw a tool. This is a known bug in Stardew Valley 1.2.29, and there's no way to recover from it."); + return; + } + // recover sprite batch try { -- cgit From 85f609dc6c2f02d89b9fccaacfe837f8822d6b7c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 8 May 2017 02:18:58 -0400 Subject: add optional verbose context logging --- src/StardewModdingAPI/Framework/Models/SConfig.cs | 3 ++ src/StardewModdingAPI/Framework/SGame.cs | 32 ++++++++++++++++++---- src/StardewModdingAPI/Program.cs | 3 ++ .../StardewModdingAPI.config.json | 5 ++++ 4 files changed, 38 insertions(+), 5 deletions(-) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Framework/Models/SConfig.cs b/src/StardewModdingAPI/Framework/Models/SConfig.cs index 0de96297..c3f0816e 100644 --- a/src/StardewModdingAPI/Framework/Models/SConfig.cs +++ b/src/StardewModdingAPI/Framework/Models/SConfig.cs @@ -12,6 +12,9 @@ /// Whether to check if a newer version of SMAPI is available on startup. public bool CheckForUpdates { get; set; } = true; + /// Whether SMAPI should log more information about the game context. + public bool VerboseLogging { get; set; } = false; + /// A list of mod versions which should be considered compatible or incompatible regardless of whether SMAPI detects incompatible code. public ModCompatibility[] ModCompatibility { get; set; } } diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 6932af2a..d248c3ca 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -30,13 +30,13 @@ namespace StardewModdingAPI.Framework /**** ** SMAPI state ****/ + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. + private readonly int MaxFailedDraws = 120; // roughly two seconds + /// The number of ticks until SMAPI should notify mods that the game has loaded. /// Skipping a few frames ensures the game finishes initialising the world before mods try to change it. private int AfterLoadTimer = 5; - /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. - private readonly int MaxFailedDraws = 120; // roughly two seconds - /// The number of consecutive failed draws. private int FailedDraws; @@ -176,6 +176,12 @@ namespace StardewModdingAPI.Framework private readonly Action renderScreenBuffer = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(new object[0]); // ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming + /********* + ** Accessors + *********/ + /// Whether SMAPI should log more information about the game context. + public bool VerboseLogging { get; set; } + /********* ** Protected methods @@ -1133,6 +1139,9 @@ namespace StardewModdingAPI.Framework var oldValue = this.PreviousLocale; var newValue = LocalizedContentManager.CurrentLanguageCode; + if (this.VerboseLogging) + this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); + if (oldValue != null) ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString()); this.PreviousLocale = newValue; @@ -1143,7 +1152,7 @@ namespace StardewModdingAPI.Framework { if (this.AfterLoadTimer == 0) { - this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}'.", LogLevel.Trace); + this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); SaveEvents.InvokeAfterLoad(this.Monitor); PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame)); @@ -1224,6 +1233,17 @@ namespace StardewModdingAPI.Framework IClickableMenu previousMenu = this.PreviousActiveMenu; IClickableMenu newMenu = Game1.activeClickableMenu; + // log context + if (this.VerboseLogging) + { + if (previousMenu == null) + this.Monitor.Log($"Context: opened menu {newMenu?.GetType().FullName ?? "(none)"}.", LogLevel.Trace); + else if (newMenu == null) + this.Monitor.Log($"Context: closed menu {previousMenu.GetType().FullName}.", LogLevel.Trace); + else + this.Monitor.Log($"Context: changed menu from {previousMenu.GetType().FullName} to {newMenu.GetType().FullName}.", LogLevel.Trace); + } + // raise save events // (saving is performed by SaveGameMenu; on days when the player shipping something, ShippingMenu wraps SaveGameMenu) if (newMenu is SaveGameMenu || newMenu is ShippingMenu) @@ -1233,7 +1253,7 @@ namespace StardewModdingAPI.Framework } else if (previousMenu is SaveGameMenu || previousMenu is ShippingMenu) { - this.Monitor.Log("Context: after save, starting new day.", LogLevel.Trace); + this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); SaveEvents.InvokeAfterSave(this.Monitor); TimeEvents.InvokeAfterDayStarted(this.Monitor); } @@ -1262,6 +1282,8 @@ namespace StardewModdingAPI.Framework // raise current location changed if (Game1.currentLocation != this.PreviousGameLocation) { + if (this.VerboseLogging) + this.Monitor.Log($"Context: set location to {Game1.currentLocation?.Name ?? "(none)"}.", LogLevel.Trace); LocationEvents.InvokeCurrentLocationChanged(this.Monitor, this.PreviousGameLocation, Game1.currentLocation); this.PreviousGameLocation = Game1.currentLocation; } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 1e5fcfc3..1913544f 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -230,6 +230,7 @@ namespace StardewModdingAPI { // load settings this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); + this.GameInstance.VerboseLogging = this.Settings.VerboseLogging; // load core components this.ModRegistry = new ModRegistry(this.Settings.ModCompatibility); @@ -266,6 +267,8 @@ namespace StardewModdingAPI 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); + if (this.Settings.VerboseLogging) + this.Monitor.Log("Verbose logging enabled.", LogLevel.Trace); // validate XNB integrity if (!this.ValidateContentIntegrity()) diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 9438c621..f42a4dfc 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -21,6 +21,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha */ "CheckForUpdates": true, + /** + * Whether SMAPI should log more information about the game context. + */ + "VerboseLogging": false, + /** * A list of mod versions SMAPI should consider compatible or broken regardless of whether it * detects incompatible code. Each record can be set to `AssumeCompatible` or `AssumeBroken`. -- cgit From 486ac29796586e09540723dcae8070cf3e60285b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 9 May 2017 00:11:39 -0400 Subject: use shared reflection helper --- src/StardewModdingAPI/Framework/ModHelper.cs | 7 ++++--- src/StardewModdingAPI/Framework/SGame.cs | 10 +++++++--- src/StardewModdingAPI/Program.cs | 8 ++++++-- 3 files changed, 17 insertions(+), 8 deletions(-) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Framework/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelper.cs index 09297a65..7810148c 100644 --- a/src/StardewModdingAPI/Framework/ModHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelper.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; namespace StardewModdingAPI.Framework @@ -25,7 +24,7 @@ namespace StardewModdingAPI.Framework public IContentHelper Content { get; } /// Simplifies access to private game code. - public IReflectionHelper Reflection { get; } = new ReflectionHelper(); + public IReflectionHelper Reflection { get; } /// Metadata about loaded mods. public IModRegistry ModRegistry { get; } @@ -44,9 +43,10 @@ namespace StardewModdingAPI.Framework /// Metadata about loaded mods. /// Manages console commands. /// The content manager which loads content assets. + /// Simplifies access to private game code. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(IManifest manifest, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager, SContentManager contentManager) + public ModHelper(IManifest manifest, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager, SContentManager contentManager, IReflectionHelper reflection) { // validate if (string.IsNullOrWhiteSpace(modDirectory)) @@ -64,6 +64,7 @@ namespace StardewModdingAPI.Framework this.Content = new ContentHelper(contentManager, modDirectory, manifest.Name); this.ModRegistry = modRegistry; this.ConsoleCommands = new CommandHelper(manifest.Name, commandManager); + this.Reflection = reflection; } /**** diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index d248c3ca..8786010e 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -9,7 +9,6 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; -using StardewModdingAPI.Framework.Reflection; using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Locations; @@ -157,9 +156,11 @@ namespace StardewModdingAPI.Framework /**** ** Private wrappers ****/ + /// Simplifies access to private game code. + private static IReflectionHelper Reflection; + // ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming /// Used to access private fields and methods. - private static readonly IReflectionHelper Reflection = new ReflectionHelper(); private static List _fpsList => SGame.Reflection.GetPrivateField>(typeof(Game1), nameof(_fpsList)).GetValue(); private static Stopwatch _fpsStopwatch => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue(); private static float _fps @@ -176,6 +177,7 @@ namespace StardewModdingAPI.Framework private readonly Action renderScreenBuffer = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(new object[0]); // ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming + /********* ** Accessors *********/ @@ -188,11 +190,13 @@ namespace StardewModdingAPI.Framework *********/ /// Construct an instance. /// Encapsulates monitoring and logging. - internal SGame(IMonitor monitor) + /// Simplifies access to private game code. + internal SGame(IMonitor monitor, IReflectionHelper reflection) { this.Monitor = monitor; this.FirstUpdate = true; SGame.Instance = this; + SGame.Reflection = reflection; Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; // required by Stardew Valley } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 1913544f..aa78ff41 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -15,6 +15,7 @@ using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; @@ -40,6 +41,9 @@ namespace StardewModdingAPI /// Tracks whether the game should exit immediately and any pending initialisation should be cancelled. private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); + /// Simplifies access to private game code. + private readonly IReflectionHelper Reflection = new ReflectionHelper(); + /// The underlying game instance. private SGame GameInstance; @@ -141,7 +145,7 @@ namespace StardewModdingAPI AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); // override game - this.GameInstance = new SGame(this.Monitor); + this.GameInstance = new SGame(this.Monitor, this.Reflection); StardewValley.Program.gamePtr = this.GameInstance; // add exit handler @@ -599,7 +603,7 @@ namespace StardewModdingAPI // inject data // get helper mod.ModManifest = manifest; - mod.Helper = new ModHelper(manifest, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager, (SContentManager)Game1.content); + mod.Helper = new ModHelper(manifest, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager, (SContentManager)Game1.content, this.Reflection); mod.Monitor = this.GetSecondaryMonitor(manifest.Name); mod.PathOnDisk = directory.FullName; -- cgit From fa729fa70021106bfb8541695f5fb7c5654e31b7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 9 May 2017 00:14:59 -0400 Subject: don't try to recover from a completely broken sprite batch, which can cause a whole new set of problems (#283) --- src/StardewModdingAPI/Framework/SGame.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 8786010e..b1ac7c58 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -990,14 +990,7 @@ namespace StardewModdingAPI.Framework if (Game1.spriteBatch.IsOpen(SGame.Reflection)) { this.Monitor.Log("Recovering sprite batch from error...", LogLevel.Trace); - try - { - Game1.spriteBatch.End(); - } - catch - { - Game1.spriteBatch = new SpriteBatch(this.GraphicsDevice); // sprite batch is broken, try replacing it - } + Game1.spriteBatch.End(); } } catch (Exception innerEx) -- cgit From 467d4a27ee565433559c9dc374f5c95107938498 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 9 May 2017 00:16:04 -0400 Subject: reduce max consecutive draw crashes (#283) --- src/StardewModdingAPI/Framework/SGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index b1ac7c58..81dae754 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Framework ** SMAPI state ****/ /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. - private readonly int MaxFailedDraws = 120; // roughly two seconds + private readonly int MaxFailedDraws = 60; // 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 initialising the world before mods try to change it. -- cgit From 3d732275874e2f36e13f10813e08b7ed2fc143f8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 9 May 2017 01:46:40 -0400 Subject: when a fatal crash happens, keep a copy of the log and notify the player on relaunch --- src/StardewModdingAPI/Constants.cs | 6 ++++++ src/StardewModdingAPI/Program.cs | 22 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 1860795d..1d6c9fa1 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -71,6 +71,12 @@ namespace StardewModdingAPI /// The file path to the log where the latest output should be saved. internal static string DefaultLogPath => Path.Combine(Constants.LogDir, "SMAPI-latest.txt"); + /// A copy of the log leading up to the previous fatal crash, if any. + internal static string FatalCrashLog => Path.Combine(Constants.LogDir, "SMAPI-crash.txt"); + + /// The file path which stores a fatal crash message for the next run. + internal static string FatalCrashMarker => Path.Combine(Constants.ExecutionPath, "StardewModdingAPI.crash.marker"); + /// The full path to the folder containing mods. internal static string ModPath { get; } = Path.Combine(Constants.ExecutionPath, "Mods"); diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index aa78ff41..725ac94f 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -154,7 +154,16 @@ namespace StardewModdingAPI this.CancellationTokenSource.Token.WaitHandle.WaitOne(); if (this.IsGameRunning) { - this.GameInstance.Exiting += (sender, e) => this.PressAnyKeyToExit(); + try + { + File.WriteAllText(Constants.FatalCrashMarker, string.Empty); + File.Copy(Constants.DefaultLogPath, Constants.FatalCrashLog, overwrite: true); + } + catch (Exception ex) + { + this.Monitor.Log($"SMAPI failed trying to track the crash details: {ex.GetLogSummary()}"); + } + this.GameInstance.Exit(); } }).Start(); @@ -179,6 +188,17 @@ namespace StardewModdingAPI return; } + // show details if game crashed during last session + if (File.Exists(Constants.FatalCrashMarker)) + { + this.Monitor.Log("The game crashed last time you played. That can be due to bugs in the game, but if it happens repeatedly you can ask for help here: http://community.playstarbound.com/threads/108375/.", LogLevel.Error); + this.Monitor.Log($"If you ask for help, make sure to attach this file: {Constants.FatalCrashLog}", 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); + } + // start game this.Monitor.Log("Starting game..."); try -- cgit From 494f9366a8e74f40e085581e8f085a03e3fc9e49 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 9 May 2017 22:02:17 -0400 Subject: let mods dispose unmanaged resources when SMAPI is disposing (#282) --- src/StardewModdingAPI/Mod.cs | 20 +++++++++++++++++++- src/StardewModdingAPI/Program.cs | 17 ++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs index 8033e1fd..a3169fb3 100644 --- a/src/StardewModdingAPI/Mod.cs +++ b/src/StardewModdingAPI/Mod.cs @@ -5,7 +5,7 @@ using StardewModdingAPI.Framework; namespace StardewModdingAPI { /// The base class for a mod. - public class Mod : IMod + public class Mod : IMod, IDisposable { /********* ** Properties @@ -88,6 +88,14 @@ namespace StardewModdingAPI /// Provides simplified APIs for writing mods. public virtual void Entry(IModHelper helper) { } + /// Release or reset unmanaged resources. + public void Dispose() + { + (this.Helper as IDisposable)?.Dispose(); // deliberate do this outside overridable dispose method so mods don't accidentally suppress it + this.Dispose(true); + GC.SuppressFinalize(this); + } + /********* ** Private methods @@ -106,5 +114,15 @@ namespace StardewModdingAPI } return Path.Combine(this.PathOnDisk, "psconfigs"); } + + /// Release or reset unmanaged resources. + /// Whether the instance is being disposed explicitly rather than finalised. If this is false, the instance shouldn't dispose other objects since they may already be finalised. + protected virtual void Dispose(bool disposing) { } + + /// Destruct the instance. + ~Mod() + { + this.Dispose(false); + } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 725ac94f..a5905805 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -228,14 +228,25 @@ namespace StardewModdingAPI /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { + this.Monitor.Log("Disposing...", LogLevel.Trace); + // skip if already disposed if (this.IsDisposed) return; this.IsDisposed = true; - // dispose mod helpers - foreach (var mod in this.ModRegistry.GetMods()) - (mod.Helper as IDisposable)?.Dispose(); + // dispose mod data + foreach (IMod mod in this.ModRegistry.GetMods()) + { + try + { + (mod as IDisposable)?.Dispose(); + } + catch (Exception ex) + { + this.Monitor.Log($"The {mod.ModManifest.Name} mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); + } + } // dispose core components this.IsGameRunning = false; -- cgit From 3fa71385e57ba45cde17bf70ad7f027a9734665f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 9 May 2017 22:12:02 -0400 Subject: add warning for mods that don't set the UniqueID manifest field --- src/StardewModdingAPI/Program.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index a5905805..70b2fbc1 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -501,11 +501,15 @@ namespace StardewModdingAPI this.Monitor.Log($"{skippedPrefix} because its manifest is invalid.", LogLevel.Error); continue; } - if (string.IsNullOrEmpty(manifest.EntryDll)) + + // validate manifest + if (string.IsNullOrWhiteSpace(manifest.EntryDll)) { this.Monitor.Log($"{skippedPrefix} because its manifest doesn't specify an entry DLL.", LogLevel.Error); continue; } + if (string.IsNullOrWhiteSpace(manifest.UniqueID)) + deprecationWarnings.Add(() => this.Monitor.Log($"{manifest.Name} doesn't have a {nameof(IManifest.UniqueID)} in its manifest. This will be required in an upcoming SMAPI release.", LogLevel.Warn)); } catch (Exception ex) { -- cgit From b4584afda897b695c5fc306db9facc0690b5ab75 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 9 May 2017 22:37:53 -0400 Subject: trace locale changes as non-verbose context --- src/StardewModdingAPI/Framework/SGame.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 81dae754..f0450306 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -1136,8 +1136,7 @@ namespace StardewModdingAPI.Framework var oldValue = this.PreviousLocale; var newValue = LocalizedContentManager.CurrentLanguageCode; - if (this.VerboseLogging) - this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); + this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); if (oldValue != null) ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString()); -- cgit From d88050fceeba58f8357bbd20d3acbd97d05c18e4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 10 May 2017 23:44:58 -0400 Subject: deprecate GameEvents.GameLoaded and GameEvents.FirstUpdateTick --- src/StardewModdingAPI/Events/GameEvents.cs | 32 ++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Events/GameEvents.cs b/src/StardewModdingAPI/Events/GameEvents.cs index 029ec1f9..be06a03b 100644 --- a/src/StardewModdingAPI/Events/GameEvents.cs +++ b/src/StardewModdingAPI/Events/GameEvents.cs @@ -28,9 +28,11 @@ namespace StardewModdingAPI.Events public static event EventHandler LoadContent; /// Raised during launch after configuring Stardew Valley, loading it into memory, and opening the game window. The window is still blank by this point. + [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the game loads, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] public static event EventHandler GameLoaded; /// Raised during the first game update tick. + [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the game loads, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] public static event EventHandler FirstUpdateTick; /// Raised when the game updates its state (≈60 times per second). @@ -99,7 +101,28 @@ namespace StardewModdingAPI.Events /// Encapsulates monitoring and logging. internal static void InvokeGameLoaded(IMonitor monitor) { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoaded)}", GameEvents.GameLoaded?.GetInvocationList()); + if (GameEvents.GameLoaded == null) + return; + + string name = $"{nameof(GameEvents)}.{nameof(GameEvents.GameLoaded)}"; + Delegate[] handlers = GameEvents.GameLoaded.GetInvocationList(); + + GameEvents.DeprecationManager.WarnForEvent(handlers, name, "1.12", DeprecationLevel.Info); + monitor.SafelyRaisePlainEvent(name, handlers); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + internal static void InvokeFirstUpdateTick(IMonitor monitor) + { + if (GameEvents.FirstUpdateTick == null) + return; + + string name = $"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}"; + Delegate[] handlers = GameEvents.FirstUpdateTick.GetInvocationList(); + + GameEvents.DeprecationManager.WarnForEvent(handlers, name, "1.12", DeprecationLevel.Info); + monitor.SafelyRaisePlainEvent(name, handlers); } /// Raise an event. @@ -150,12 +173,5 @@ namespace StardewModdingAPI.Events { monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList()); } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeFirstUpdateTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", GameEvents.FirstUpdateTick?.GetInvocationList()); - } } } -- cgit From 86c60c971ae533321b6550f140af4f01351cb2e3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 10 May 2017 23:49:58 -0400 Subject: merge SGame::UpdateEventCalls into Update The method was misleadingly named (since only some of the events were in the method), and unnecessarily limited the possible flows. --- src/StardewModdingAPI/Framework/SGame.cs | 501 +++++++++++++++---------------- 1 file changed, 247 insertions(+), 254 deletions(-) (limited to 'src/StardewModdingAPI') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index f0450306..ed1ff647 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -257,8 +257,253 @@ namespace StardewModdingAPI.Framework GameEvents.InvokeGameLoaded(this.Monitor); } - // update SMAPI events - this.UpdateEventCalls(); + // content locale changed event + if (this.PreviousLocale != LocalizedContentManager.CurrentLanguageCode) + { + var oldValue = this.PreviousLocale; + var newValue = LocalizedContentManager.CurrentLanguageCode; + + this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); + + if (oldValue != null) + ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString()); + this.PreviousLocale = newValue; + } + + // save loaded event + if (Context.IsSaveLoaded && !SaveGame.IsProcessing/*still loading save*/ && this.AfterLoadTimer >= 0) + { + if (this.AfterLoadTimer == 0) + { + this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + + SaveEvents.InvokeAfterLoad(this.Monitor); + PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame)); + TimeEvents.InvokeAfterDayStarted(this.Monitor); + } + this.AfterLoadTimer--; + } + + // before exit to title + if (Game1.exitToTitle) + this.IsExiting = true; + + // after exit to title + if (this.IsWorldReady && this.IsExiting && Game1.activeClickableMenu is TitleMenu) + { + this.Monitor.Log("Context: returned to title", LogLevel.Trace); + SaveEvents.InvokeAfterReturnToTitle(this.Monitor); + this.AfterLoadTimer = 5; + this.IsExiting = false; + } + + // input events + { + // get latest state + this.KStateNow = Keyboard.GetState(); + this.MStateNow = Mouse.GetState(); + this.MPositionNow = new Point(Game1.getMouseX(), Game1.getMouseY()); + + // raise key pressed + foreach (var key in this.FramePressedKeys) + ControlEvents.InvokeKeyPressed(this.Monitor, key); + + // raise key released + foreach (var key in this.FrameReleasedKeys) + ControlEvents.InvokeKeyReleased(this.Monitor, key); + + // raise controller button pressed + for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++) + { + var buttons = this.GetFramePressedButtons(i); + foreach (var button in buttons) + { + if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) + ControlEvents.InvokeTriggerPressed(this.Monitor, i, button, button == Buttons.LeftTrigger ? GamePad.GetState(i).Triggers.Left : GamePad.GetState(i).Triggers.Right); + else + ControlEvents.InvokeButtonPressed(this.Monitor, i, button); + } + } + + // raise controller button released + for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++) + { + foreach (var button in this.GetFrameReleasedButtons(i)) + { + if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) + ControlEvents.InvokeTriggerReleased(this.Monitor, i, button, button == Buttons.LeftTrigger ? GamePad.GetState(i).Triggers.Left : GamePad.GetState(i).Triggers.Right); + else + ControlEvents.InvokeButtonReleased(this.Monitor, i, button); + } + } + + // raise keyboard state changed + if (this.KStateNow != this.KStatePrior) + ControlEvents.InvokeKeyboardChanged(this.Monitor, this.KStatePrior, this.KStateNow); + + // raise mouse state changed + if (this.MStateNow != this.MStatePrior) + { + ControlEvents.InvokeMouseChanged(this.Monitor, this.MStatePrior, this.MStateNow, this.MPositionPrior, this.MPositionNow); + this.MStatePrior = this.MStateNow; + this.MPositionPrior = this.MPositionNow; + } + } + + // menu events + if (Game1.activeClickableMenu != this.PreviousActiveMenu) + { + IClickableMenu previousMenu = this.PreviousActiveMenu; + IClickableMenu newMenu = Game1.activeClickableMenu; + + // log context + if (this.VerboseLogging) + { + if (previousMenu == null) + this.Monitor.Log($"Context: opened menu {newMenu?.GetType().FullName ?? "(none)"}.", LogLevel.Trace); + else if (newMenu == null) + this.Monitor.Log($"Context: closed menu {previousMenu.GetType().FullName}.", LogLevel.Trace); + else + this.Monitor.Log($"Context: changed menu from {previousMenu.GetType().FullName} to {newMenu.GetType().FullName}.", LogLevel.Trace); + } + + // raise save events + // (saving is performed by SaveGameMenu; on days when the player shipping something, ShippingMenu wraps SaveGameMenu) + if (newMenu is SaveGameMenu || newMenu is ShippingMenu) + { + this.Monitor.Log("Context: before save.", LogLevel.Trace); + SaveEvents.InvokeBeforeSave(this.Monitor); + } + else if (previousMenu is SaveGameMenu || previousMenu is ShippingMenu) + { + this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + SaveEvents.InvokeAfterSave(this.Monitor); + TimeEvents.InvokeAfterDayStarted(this.Monitor); + } + + // raise menu events + if (newMenu != null) + MenuEvents.InvokeMenuChanged(this.Monitor, previousMenu, newMenu); + else + MenuEvents.InvokeMenuClosed(this.Monitor, previousMenu); + + // update previous menu + // (if the menu was changed in one of the handlers, deliberately defer detection until the next update so mods can be notified of the new menu change) + this.PreviousActiveMenu = newMenu; + } + + // world & player events + if (this.IsWorldReady) + { + // raise location list changed + if (this.GetHash(Game1.locations) != this.PreviousGameLocations) + { + LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); + this.PreviousGameLocations = this.GetHash(Game1.locations); + } + + // raise current location changed + if (Game1.currentLocation != this.PreviousGameLocation) + { + if (this.VerboseLogging) + this.Monitor.Log($"Context: set location to {Game1.currentLocation?.Name ?? "(none)"}.", LogLevel.Trace); + LocationEvents.InvokeCurrentLocationChanged(this.Monitor, this.PreviousGameLocation, Game1.currentLocation); + this.PreviousGameLocation = Game1.currentLocation; + } + + // raise player changed + if (Game1.player != this.PreviousFarmer) + { + PlayerEvents.InvokeFarmerChanged(this.Monitor, this.PreviousFarmer, Game1.player); + this.PreviousFarmer = Game1.player; + } + + // raise player leveled up a skill + if (Game1.player.combatLevel != this.PreviousCombatLevel) + { + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel); + this.PreviousCombatLevel = Game1.player.combatLevel; + } + if (Game1.player.farmingLevel != this.PreviousFarmingLevel) + { + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel); + this.PreviousFarmingLevel = Game1.player.farmingLevel; + } + if (Game1.player.fishingLevel != this.PreviousFishingLevel) + { + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Fishing, Game1.player.fishingLevel); + this.PreviousFishingLevel = Game1.player.fishingLevel; + } + if (Game1.player.foragingLevel != this.PreviousForagingLevel) + { + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Foraging, Game1.player.foragingLevel); + this.PreviousForagingLevel = Game1.player.foragingLevel; + } + if (Game1.player.miningLevel != this.PreviousMiningLevel) + { + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Mining, Game1.player.miningLevel); + this.PreviousMiningLevel = Game1.player.miningLevel; + } + if (Game1.player.luckLevel != this.PreviousLuckLevel) + { + PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Luck, Game1.player.luckLevel); + this.PreviousLuckLevel = Game1.player.luckLevel; + } + + // raise player inventory changed + ItemStackChange[] changedItems = this.GetInventoryChanges(Game1.player.items, this.PreviousItems).ToArray(); + if (changedItems.Any()) + { + PlayerEvents.InvokeInventoryChanged(this.Monitor, Game1.player.items, changedItems); + this.PreviousItems = Game1.player.items.Where(n => n != null).ToDictionary(n => n, n => n.Stack); + } + + // raise current location's object list changed + { + int? objectHash = Game1.currentLocation?.objects != null ? this.GetHash(Game1.currentLocation.objects) : (int?)null; + if (objectHash != null && this.PreviousLocationObjects != objectHash) + { + LocationEvents.InvokeOnNewLocationObject(this.Monitor, Game1.currentLocation.objects); + this.PreviousLocationObjects = objectHash.Value; + } + } + + // raise time changed + if (Game1.timeOfDay != this.PreviousTime) + { + TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay); + this.PreviousTime = Game1.timeOfDay; + } + if (Game1.dayOfMonth != this.PreviousDay) + { + TimeEvents.InvokeDayOfMonthChanged(this.Monitor, this.PreviousDay, Game1.dayOfMonth); + this.PreviousDay = Game1.dayOfMonth; + } + if (Game1.currentSeason != this.PreviousSeason) + { + TimeEvents.InvokeSeasonOfYearChanged(this.Monitor, this.PreviousSeason, Game1.currentSeason); + this.PreviousSeason = Game1.currentSeason; + } + if (Game1.year != this.PreviousYear) + { + TimeEvents.InvokeYearOfGameChanged(this.Monitor, this.PreviousYear, Game1.year); + this.PreviousYear = Game1.year; + } + + // raise mine level changed + if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) + { + MineEvents.InvokeMineLevelChanged(this.Monitor, this.PreviousMineLevel, Game1.mine.mineLevel); + this.PreviousMineLevel = Game1.mine.mineLevel; + } + } + + // raise game day transition event (obsolete) + if (Game1.newDay != this.PreviousIsNewDay) + { + TimeEvents.InvokeOnNewDay(this.Monitor, this.PreviousDay, Game1.dayOfMonth, Game1.newDay); + this.PreviousIsNewDay = Game1.newDay; + } // let game update try @@ -1127,258 +1372,6 @@ namespace StardewModdingAPI.Framework return this.WasButtonJustReleased(button, value > 0.2f ? ButtonState.Pressed : ButtonState.Released, stateIndex); } - /// Detect changes since the last update ticket and trigger mod events. - private void UpdateEventCalls() - { - // content locale changed event - if (this.PreviousLocale != LocalizedContentManager.CurrentLanguageCode) - { - var oldValue = this.PreviousLocale; - var newValue = LocalizedContentManager.CurrentLanguageCode; - - this.Monitor.Log($"Context: locale set to {newValue}.", LogLevel.Trace); - - if (oldValue != null) - ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString()); - this.PreviousLocale = newValue; - } - - // save loaded event - if (Context.IsSaveLoaded && !SaveGame.IsProcessing/*still loading save*/ && this.AfterLoadTimer >= 0) - { - if (this.AfterLoadTimer == 0) - { - this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - - SaveEvents.InvokeAfterLoad(this.Monitor); - PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame)); - TimeEvents.InvokeAfterDayStarted(this.Monitor); - } - this.AfterLoadTimer--; - } - - // before exit to title - if (Game1.exitToTitle) - this.IsExiting = true; - - // after exit to title - if (this.IsWorldReady && this.IsExiting && Game1.activeClickableMenu is TitleMenu) - { - this.Monitor.Log("Context: returned to title", LogLevel.Trace); - SaveEvents.InvokeAfterReturnToTitle(this.Monitor); - this.AfterLoadTimer = 5; - this.IsExiting = false; - } - - // input events - { - // get latest state - this.KStateNow = Keyboard.GetState(); - this.MStateNow = Mouse.GetState(); - this.MPositionNow = new Point(Game1.getMouseX(), Game1.getMouseY()); - - // raise key pressed - foreach (var key in this.FramePressedKeys) - ControlEvents.InvokeKeyPressed(this.Monitor, key); - - // raise key released - foreach (var key in this.FrameReleasedKeys) - ControlEvents.InvokeKeyReleased(this.Monitor, key); - - // raise controller button pressed - for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++) - { - var buttons = this.GetFramePressedButtons(i); - foreach (var button in buttons) - { - if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) - ControlEvents.InvokeTriggerPressed(this.Monitor, i, button, button == Buttons.LeftTrigger ? GamePad.GetState(i).Triggers.Left : GamePad.GetState(i).Triggers.Right); - else - ControlEvents.InvokeButtonPressed(this.Monitor, i, button); - } - } - - // raise controller button released - for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++) - { - foreach (var button in this.GetFrameReleasedButtons(i)) - { - if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) - ControlEvents.InvokeTriggerReleased(this.Monitor, i, button, button == Buttons.LeftTrigger ? GamePad.GetState(i).Triggers.Left : GamePad.GetState(i).Triggers.Right); - else - ControlEvents.InvokeButtonReleased(this.Monitor, i, button); - } - } - - // raise keyboard state changed - if (this.KStateNow != this.KStatePrior) - ControlEvents.InvokeKeyboardChanged(this.Monitor, this.KStatePrior, this.KStateNow); - - // raise mouse state changed - if (this.MStateNow != this.MStatePrior) - { - ControlEvents.InvokeMouseChanged(this.Monitor, this.MStatePrior, this.MStateNow, this.MPositionPrior, this.MPositionNow); - this.MStatePrior = this.MStateNow; - this.MPositionPrior = this.MPositionNow; - } - } - - // menu events - if (Game1.activeClickableMenu != this.PreviousActiveMenu) - { - IClickableMenu previousMenu = this.PreviousActiveMenu; - IClickableMenu newMenu = Game1.activeClickableMenu; - - // log context - if (this.VerboseLogging) - { - if (previousMenu == null) - this.Monitor.Log($"Context: opened menu {newMenu?.GetType().FullName ?? "(none)"}.", LogLevel.Trace); - else if (newMenu == null) - this.Monitor.Log($"Context: closed menu {previousMenu.GetType().FullName}.", LogLevel.Trace); - else - this.Monitor.Log($"Context: changed menu from {previousMenu.GetType().FullName} to {newMenu.GetType().FullName}.", LogLevel.Trace); - } - - // raise save events - // (saving is performed by SaveGameMenu; on days when the player shipping something, ShippingMenu wraps SaveGameMenu) - if (newMenu is SaveGameMenu || newMenu is ShippingMenu) - { - this.Monitor.Log("Context: before save.", LogLevel.Trace); - SaveEvents.InvokeBeforeSave(this.Monitor); - } - else if (previousMenu is SaveGameMenu || previousMenu is ShippingMenu) - { - this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - SaveEvents.InvokeAfterSave(this.Monitor); - TimeEvents.InvokeAfterDayStarted(this.Monitor); - } - - // raise menu events - if (newMenu != null) - MenuEvents.InvokeMenuChanged(this.Monitor, previousMenu, newMenu); - else - MenuEvents.InvokeMenuClosed(this.Monitor, previousMenu); - - // update previous menu - // (if the menu was changed in one of the handlers, deliberately defer detection until the next update so mods can be notified of the new menu change) - this.PreviousActiveMenu = newMenu; - } - - // world & player events - if (this.IsWorldReady) - { - // raise location list changed - if (this.GetHash(Game1.locations) != this.PreviousGameLocations) - { - LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); - this.PreviousGameLocations = this.GetHash(Game1.locations); - } - - // raise current location changed - if (Game1.currentLocation != this.PreviousGameLocation) - { - if (this.VerboseLogging) - this.Monitor.Log($"Context: set location to {Game1.currentLocation?.Name ?? "(none)"}.", LogLevel.Trace); - LocationEvents.InvokeCurrentLocationChanged(this.Monitor, this.PreviousGameLocation, Game1.currentLocation); - this.PreviousGameLocation = Game1.currentLocation; - } - - // raise player changed - if (Game1.player != this.PreviousFarmer) - { - PlayerEvents.InvokeFarmerChanged(this.Monitor, this.PreviousFarmer, Game1.player); - this.PreviousFarmer = Game1.player; - } - - // raise player leveled up a skill - if (Game1.player.combatLevel != this.PreviousCombatLevel) - { - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Combat, Game1.player.combatLevel); - this.PreviousCombatLevel = Game1.player.combatLevel; - } - if (Game1.player.farmingLevel != this.PreviousFarmingLevel) - { - PlayerEvents.InvokeLeveledUp(this.Monitor, EventArgsLevelUp.LevelType.Farming, Game1.player.farmingLevel); - this.PreviousFarmingLevel = Game1.player.farmingLevel; -