Compare commits

...

2 Commits

Author SHA1 Message Date
Jesse Plamondon-Willard e16584527c
fix changes to save handling
The postfix for an enumerable method is raised when the enumerable is returned, not when it finishes enumerating.
2018-12-04 23:50:30 -05:00
Jesse Plamondon-Willard 90ca3d6ba1
change save handling so events work on the end-of-day screens 2018-12-04 23:50:29 -05:00
5 changed files with 131 additions and 57 deletions

View File

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

View File

@ -166,11 +166,6 @@ namespace StardewModdingAPI.Framework
return;
}
#endif
// apply game patches
new GamePatcher(this.Monitor).Apply(
new DialogueErrorPatch(this.MonitorForGame, this.Reflection)
);
}
/// <summary>Launch SMAPI.</summary>
@ -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)
);
// start game
this.Monitor.Log("Starting game...", LogLevel.Debug);
try

View File

@ -199,11 +199,61 @@ namespace StardewModdingAPI.Framework
}
/// <summary>A callback invoked before <see cref="Game1.newDayAfterFade"/> runs.</summary>
protected void OnNewDayAfterFade()
private void OnNewDayAfterFade()
{
this.Events.DayEnding.RaiseEmpty();
}
/// <summary>A callback invoked before <see cref="SaveGame.Save"/> runs.</summary>
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
}
}
/// <summary>A callback invoked after <see cref="SaveGame.Save"/> runs.</summary>
private 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
}
}
/// <summary>A callback invoked when a mod message is received.</summary>
/// <param name="message">The message to deliver to applicable mods.</param>
private void OnModMessageReceived(ModMessageModel message)
@ -362,67 +412,26 @@ 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)
if (!Context.IsSaving)
this.OnSaved();
else
{
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
}
/*********

View File

@ -0,0 +1,62 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Harmony;
using StardewModdingAPI.Framework.Patching;
using StardewValley;
namespace StardewModdingAPI.Patches
{
/// <summary>A Harmony patch for <see cref="SaveGame.Save"/> to detect when the game is saving.</summary>
internal class SaveGamePatch : IHarmonyPatch
{
/*********
** Private methods
*********/
/// <summary>A callback to invoke before <see cref="SaveGame.Save"/> runs.</summary>
private static Action OnSaving;
/*********
** Accessors
*********/
/// <summary>A unique name for this patch.</summary>
public string Name => $"{nameof(SaveGamePatch)}";
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="onSaving">A callback to invoke before <see cref="SaveGame.Save"/> runs.</param>
public SaveGamePatch(Action onSaving)
{
SaveGamePatch.OnSaving = onSaving;
}
/// <summary>Apply the Harmony patch.</summary>
/// <param name="harmony">The Harmony instance.</param>
public void Apply(HarmonyInstance harmony)
{
MethodInfo method = AccessTools.Method(typeof(SaveGame), nameof(SaveGame.Save));
MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(SaveGamePatch.Prefix));
harmony.Patch(method, prefix: new HarmonyMethod(prefix));
}
/*********
** Private methods
*********/
/// <summary>The method to call before <see cref="SaveGame.Save"/>.</summary>
/// <returns>Returns whether to execute the original method.</returns>
/// <remarks>This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.</remarks>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony.")]
private static bool Prefix()
{
SaveGamePatch.OnSaving();
return true;
}
}
}

View File

@ -323,6 +323,7 @@
<Compile Include="Framework\Monitor.cs" />
<Compile Include="Metadata\InstructionMetadata.cs" />
<Compile Include="Mod.cs" />
<Compile Include="Patches\SaveGamePatch.cs" />
<Compile Include="Patches\DialogueErrorPatch.cs" />
<Compile Include="PatchMode.cs" />
<Compile Include="GamePlatform.cs" />