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 @@
+