diff --git a/docs/release-notes.md b/docs/release-notes.md index 5788fa9f..baec72f7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,7 @@ * Added `IsLocalPlayer` to new player events. * Reloading a map asset will now update affected locations. * Reloading the `Data\NPCDispositions` asset will now update affected NPCs. + * Improved how SMAPI events work at the end of day. Events are now raised correctly while end-of-day menus like the shipping menu are shown, and `BeforeSave` is now raised immediately before save instead of when the shipping menu is opened. * Fixed some map tilesheets not editable if not playing in English. * Fixed newlines in most manifest fields not being ignored. * Fixed `Display.RenderedWorld` event not invoked before overlays are rendered. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index eff7cb3b..89f2d6c4 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -166,11 +166,6 @@ namespace StardewModdingAPI.Framework return; } #endif - - // apply game patches - new GamePatcher(this.Monitor).Apply( - new DialogueErrorPatch(this.MonitorForGame, this.Reflection) - ); } /// Launch SMAPI. @@ -284,6 +279,12 @@ namespace StardewModdingAPI.Framework File.Delete(Constants.FatalCrashMarker); } + // apply game patches + new GamePatcher(this.Monitor).Apply( + new DialogueErrorPatch(this.MonitorForGame, this.Reflection), + new SaveGamePatch(onSaving: this.GameInstance.OnSaving, onSaved: this.GameInstance.OnSaved) + ); + // start game this.Monitor.Log("Starting game...", LogLevel.Debug); try diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 9ad8d188..ebf2872d 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -204,6 +204,56 @@ namespace StardewModdingAPI.Framework this.Events.DayEnding.RaiseEmpty(); } + /// A callback invoked before runs. + internal void OnSaving() + { + // raise events + if (!Context.IsWorldReady) + { + this.IsBetweenCreateEvents = true; + this.Monitor.Log("Context: before save creation.", LogLevel.Trace); + this.Events.SaveCreating.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + this.Events.Legacy_BeforeCreateSave.Raise(); +#endif + } + else + { + this.IsBetweenSaveEvents = true; + this.Monitor.Log("Context: before save.", LogLevel.Trace); + this.Events.Saving.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + this.Events.Legacy_BeforeSave.Raise(); +#endif + } + } + + /// A callback invoked after runs. + internal void OnSaved() + { + // reset flags + this.IsBetweenCreateEvents = false; + this.IsBetweenSaveEvents = false; + + // raise events + if (!Context.IsWorldReady) + { + this.Monitor.Log($"Context: after save creation, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + this.Events.SaveCreated.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + this.Events.Legacy_AfterCreateSave.Raise(); +#endif + } + else + { + this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + this.Events.Saved.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + this.Events.Legacy_AfterSave.Raise(); +#endif + } + } + /// A callback invoked when a mod message is received. /// The message to deliver to applicable mods. private void OnModMessageReceived(ModMessageModel message) @@ -362,68 +412,22 @@ namespace StardewModdingAPI.Framework inputState.TrueUpdate(); /********* - ** Save events + suppress events during save + ** Suppress events during save *********/ // 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) + // is in progress. + if (this.IsBetweenCreateEvents || this.IsBetweenSaveEvents) { - // raise before-create - if (!Context.IsWorldReady && !this.IsBetweenCreateEvents) - { - this.IsBetweenCreateEvents = true; - this.Monitor.Log("Context: before save creation.", LogLevel.Trace); - this.Events.SaveCreating.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - this.Events.Legacy_BeforeCreateSave.Raise(); -#endif - } - - // raise before-save - if (Context.IsWorldReady && !this.IsBetweenSaveEvents) - { - this.IsBetweenSaveEvents = true; - this.Monitor.Log("Context: before save.", LogLevel.Trace); - this.Events.Saving.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - this.Events.Legacy_BeforeSave.Raise(); -#endif - } - - // suppress non-save events this.Events.UnvalidatedUpdateTicking.Raise(new UnvalidatedUpdateTickingEventArgs(this.TicksElapsed)); base.Update(gameTime); this.Events.UnvalidatedUpdateTicked.Raise(new UnvalidatedUpdateTickedEventArgs(this.TicksElapsed)); #if !SMAPI_3_0_STRICT - this.Events.Legacy_UnvalidatedUpdateTick.Raise(); + this.Events.Legacy_BeforeCreateSave.Raise(); #endif return; } - 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.Events.SaveCreated.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - this.Events.Legacy_AfterCreateSave.Raise(); -#endif - } - 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); - this.Events.Saved.RaiseEmpty(); - this.Events.DayStarted.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - this.Events.Legacy_AfterSave.Raise(); - this.Events.Legacy_AfterDayStarted.Raise(); -#endif - } /********* ** Update context diff --git a/src/SMAPI/Patches/SaveGamePatch.cs b/src/SMAPI/Patches/SaveGamePatch.cs new file mode 100644 index 00000000..96b23d71 --- /dev/null +++ b/src/SMAPI/Patches/SaveGamePatch.cs @@ -0,0 +1,77 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Harmony; +using StardewModdingAPI.Framework.Patching; +using StardewValley; + +namespace StardewModdingAPI.Patches +{ + /// A Harmony patch for to detect when the game is saving. + internal class SaveGamePatch : IHarmonyPatch + { + /********* + ** Private methods + *********/ + /// A callback to invoke before runs. + private static Action OnSaving; + + /// A callback to invoke before runs. + private static Action OnSaved; + + + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => $"{nameof(SaveGamePatch)}"; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A callback to invoke before runs. + /// A callback to invoke after runs. + public SaveGamePatch(Action onSaving, Action onSaved) + { + SaveGamePatch.OnSaving = onSaving; + SaveGamePatch.OnSaved = onSaved; + } + + + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + MethodInfo method = AccessTools.Method(typeof(SaveGame), nameof(SaveGame.Save)); + MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(SaveGamePatch.Prefix)); + MethodInfo postfix = AccessTools.Method(this.GetType(), nameof(SaveGamePatch.Postfix)); + + harmony.Patch(method, prefix: new HarmonyMethod(prefix)); + harmony.Patch(method, postfix: new HarmonyMethod(postfix)); + } + + + /********* + ** Private methods + *********/ + /// The method to call before . + /// Returns whether to execute the original method. + /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony.")] + private static bool Prefix() + { + SaveGamePatch.OnSaving(); + return true; + } + + /// The method to call after . + /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony.")] + private static void Postfix() + { + SaveGamePatch.OnSaved(); + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 3696b54d..1864bed9 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -323,6 +323,7 @@ +