fix errors during overridden update loop immediately crashing the game with no log entry

This commit is contained in:
Jesse Plamondon-Willard 2017-05-17 11:45:17 -04:00
parent b977a8e48f
commit 8439594b10
4 changed files with 392 additions and 333 deletions

View File

@ -15,6 +15,7 @@ See [log](https://github.com/Pathoschild/SMAPI/compare/1.12...1.13).
For players: For players:
* SMAPI now recovers better from mod draw errors and detects when the error is irrecoverable. * SMAPI now recovers better from mod draw errors and detects when the error is irrecoverable.
* SMAPI now recovers automatically from update errors if possible.
* SMAPI now remembers if your game crashed and offers help next time you launch it. * SMAPI now remembers if your game crashed and offers help next time you launch it.
* Fixed installer finding redundant game paths on Linux. * Fixed installer finding redundant game paths on Linux.
* Fixed save events not being raised after the first day on Linux/Mac. * Fixed save events not being raised after the first day on Linux/Mac.

View File

@ -0,0 +1,44 @@
namespace StardewModdingAPI.Framework
{
/// <summary>Counts down from a baseline value.</summary>
internal class Countdown
{
/*********
** Accessors
*********/
/// <summary>The initial value from which to count down.</summary>
public int Initial { get; }
/// <summary>The current value.</summary>
public int Current { get; private set; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="initial">The initial value from which to count down.</param>
public Countdown(int initial)
{
this.Initial = initial;
this.Current = initial;
}
/// <summary>Reduce the current value by one.</summary>
/// <returns>Returns whether the value was decremented (i.e. wasn't already zero).</returns>
public bool Decrement()
{
if (this.Current <= 0)
return false;
this.Current--;
return true;
}
/// <summary>Restart the countdown.</summary>
public void Reset()
{
this.Current = this.Initial;
}
}
}

View File

@ -30,15 +30,15 @@ namespace StardewModdingAPI.Framework
** SMAPI state ** SMAPI state
****/ ****/
/// <summary>The maximum number of consecutive attempts SMAPI should make to recover from a draw error.</summary> /// <summary>The maximum number of consecutive attempts SMAPI should make to recover from a draw error.</summary>
private readonly int MaxFailedDraws = 60; // roughly one second private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second
/// <summary>The maximum number of consecutive attempts SMAPI should make to recover from an update error.</summary>
private readonly Countdown UpdateCrashTimer = new Countdown(60); // 60 ticks = roughly one second
/// <summary>The number of ticks until SMAPI should notify mods that the game has loaded.</summary> /// <summary>The number of ticks until SMAPI should notify mods that the game has loaded.</summary>
/// <remarks>Skipping a few frames ensures the game finishes initialising the world before mods try to change it.</remarks> /// <remarks>Skipping a few frames ensures the game finishes initialising the world before mods try to change it.</remarks>
private int AfterLoadTimer = 5; private int AfterLoadTimer = 5;
/// <summary>The number of consecutive failed draws.</summary>
private int FailedDraws;
/// <summary>Whether the game is returning to the menu.</summary> /// <summary>Whether the game is returning to the menu.</summary>
private bool IsExiting; private bool IsExiting;
@ -233,6 +233,8 @@ namespace StardewModdingAPI.Framework
/// <summary>The method called when the game is updating its state. This happens roughly 60 times per second.</summary> /// <summary>The method called when the game is updating its state. This happens roughly 60 times per second.</summary>
/// <param name="gameTime">A snapshot of the game timing state.</param> /// <param name="gameTime">A snapshot of the game timing state.</param>
protected override void Update(GameTime gameTime) protected override void Update(GameTime gameTime)
{
try
{ {
/********* /*********
** Skip conditions ** Skip conditions
@ -316,7 +318,7 @@ namespace StardewModdingAPI.Framework
/********* /*********
** After load events ** After load events
*********/ *********/
if (Context.IsSaveLoaded && !SaveGame.IsProcessing/*still loading save*/ && this.AfterLoadTimer >= 0) if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0)
{ {
if (this.AfterLoadTimer == 0) if (this.AfterLoadTimer == 0)
{ {
@ -567,6 +569,18 @@ namespace StardewModdingAPI.Framework
this.KStatePrior = this.KStateNow; this.KStatePrior = this.KStateNow;
for (PlayerIndex i = PlayerIndex.One; i <= PlayerIndex.Four; i++) for (PlayerIndex i = PlayerIndex.One; i <= PlayerIndex.Four; i++)
this.PreviouslyPressedButtons[(int)i] = this.GetButtonsDown(i); this.PreviouslyPressedButtons[(int)i] = this.GetButtonsDown(i);
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.Monitor.ExitGameImmediately("the game crashed when updating, and SMAPI was unable to recover the game.");
}
} }
/// <summary>The method called to draw everything to the screen.</summary> /// <summary>The method called to draw everything to the screen.</summary>
@ -577,7 +591,7 @@ namespace StardewModdingAPI.Framework
try try
{ {
this.DrawImpl(gameTime); this.DrawImpl(gameTime);
this.FailedDraws = 0; this.DrawCrashTimer.Reset();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -585,12 +599,11 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error); this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error);
// exit if irrecoverable // exit if irrecoverable
if (this.FailedDraws >= this.MaxFailedDraws) if (!this.DrawCrashTimer.Decrement())
{ {
this.Monitor.ExitGameImmediately("the game crashed when drawing, and SMAPI was unable to recover the game."); this.Monitor.ExitGameImmediately("the game crashed when drawing, and SMAPI was unable to recover the game.");
return; return;
} }
this.FailedDraws++;
// abort in known unrecoverable cases // abort in known unrecoverable cases
if (Game1.toolSpriteSheet?.IsDisposed == true) if (Game1.toolSpriteSheet?.IsDisposed == true)

View File

@ -121,6 +121,7 @@
<Compile Include="Events\EventArgsStringChanged.cs" /> <Compile Include="Events\EventArgsStringChanged.cs" />
<Compile Include="Events\GameEvents.cs" /> <Compile Include="Events\GameEvents.cs" />
<Compile Include="Events\GraphicsEvents.cs" /> <Compile Include="Events\GraphicsEvents.cs" />
<Compile Include="Framework\Countdown.cs" />
<Compile Include="Framework\ModLoading\IModMetadata.cs" /> <Compile Include="Framework\ModLoading\IModMetadata.cs" />
<Compile Include="Framework\ModLoading\InvalidModStateException.cs" /> <Compile Include="Framework\ModLoading\InvalidModStateException.cs" />
<Compile Include="Framework\ModLoading\ModDependencyStatus.cs" /> <Compile Include="Framework\ModLoading\ModDependencyStatus.cs" />