Skip to content

Commit 5559b48

Browse files
committed
Handle SRAM saving failure.
1 parent 1e313b7 commit 5559b48

File tree

7 files changed

+105
-89
lines changed

7 files changed

+105
-89
lines changed

src/BizHawk.Client.Common/Api/Classes/EmuClientApi.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ public bool OpenRom(string path)
158158

159159
public void RebootCore() => _mainForm.RebootCore();
160160

161+
// TODO: Change return type to FileWriteResult.
161162
public void SaveRam() => _mainForm.FlushSaveRAM();
162163

163164
// TODO: Change return type to FileWriteResult.

src/BizHawk.Client.Common/DialogControllerExtensions.cs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,40 @@ public static void ErrorMessageBox(
6868
{
6969
Debug.Assert(fileResult.IsError && fileResult.Exception != null, "Error box must have an error.");
7070

71-
string prefix = prefixMessage == null ? "" : prefixMessage + "\n";
71+
string prefix = prefixMessage ?? "";
7272
dialogParent.ModalMessageBox(
73-
text: $"{prefix}{fileResult.UserFriendlyErrorMessage()}\n{fileResult.Exception!.Message}",
73+
text: $"{prefix}\n{fileResult.UserFriendlyErrorMessage()}\n{fileResult.Exception!.Message}",
7474
caption: "Error",
7575
icon: EMsgBoxIcon.Error);
7676
}
7777

78+
/// <summary>
79+
/// If the action fails, asks the user if they want to try again.
80+
/// The user will be repeatedly asked if they want to try again until either success or the user says no.
81+
/// </summary>
82+
/// <returns>Returns true on success or if the user said no. Returns false if the user said cancel.</returns>
83+
public static bool DoWithTryAgainBox(
84+
this IDialogParent dialogParent,
85+
Func<FileWriteResult> action,
86+
string message)
87+
{
88+
FileWriteResult fileResult = action();
89+
while (fileResult.IsError)
90+
{
91+
string prefix = message ?? "";
92+
bool? askResult = dialogParent.ModalMessageBox3(
93+
text: $"{prefix} Do you want to try again?\n\nError details:" +
94+
$"{fileResult.UserFriendlyErrorMessage()}\n{fileResult.Exception!.Message}",
95+
caption: "Error",
96+
icon: EMsgBoxIcon.Error);
97+
if (askResult == null) return false;
98+
if (askResult == false) return true;
99+
if (askResult == true) fileResult = action();
100+
}
101+
102+
return true;
103+
}
104+
78105
/// <summary>Creates and shows a <c>System.Windows.Forms.OpenFileDialog</c> or equivalent with the receiver (<paramref name="dialogParent"/>) as its parent</summary>
79106
/// <param name="discardCWDChange"><c>OpenFileDialog.RestoreDirectory</c> (isn't this useless when specifying <paramref name="initDir"/>? keeping it for backcompat)</param>
80107
/// <param name="filter"><c>OpenFileDialog.Filter</c></param>

src/BizHawk.Client.Common/FileWriter.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,23 @@ private FileWriter(FileWritePaths paths, FileStream stream)
3939
_stream = stream;
4040
}
4141

42+
public static FileWriteResult Write(string path, byte[] bytes, string? backupPath = null)
43+
{
44+
FileWriteResult<FileWriter> createResult = Create(path);
45+
if (createResult.IsError) return createResult;
46+
47+
try
48+
{
49+
createResult.Value!.Stream.Write(bytes);
50+
}
51+
catch (Exception ex)
52+
{
53+
return new(FileWriteEnum.FailedDuringWrite, createResult.Value!.Paths, ex);
54+
}
55+
56+
return createResult.Value.CloseAndDispose(backupPath);
57+
}
58+
4259

4360
/// <summary>
4461
/// Create a FileWriter instance, or return an error if unable to access the file.

src/BizHawk.Client.Common/IMainFormForApi.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public interface IMainFormForApi
4848
void EnableRewind(bool enabled);
4949

5050
/// <remarks>only referenced from <c>EmuClientApi</c></remarks>
51-
bool FlushSaveRAM(bool autosave = false);
51+
FileWriteResult FlushSaveRAM(bool autosave = false);
5252

5353
/// <remarks>only referenced from <c>EmuClientApi</c></remarks>
5454
void FrameAdvance(bool discardApiHawkSurfaces = true);

src/BizHawk.Client.EmuHawk/MainForm.Events.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ private bool LoadstateCurrentSlot()
316316

317317
private void FlushSaveRAMMenuItem_Click(object sender, EventArgs e)
318318
{
319-
FlushSaveRAM();
319+
ShowMessageIfError(() => FlushSaveRAM(), "Failed to flush saveram!");
320320
}
321321

322322
private void ReadonlyMenuItem_Click(object sender, EventArgs e)

src/BizHawk.Client.EmuHawk/MainForm.Hotkey.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ void SelectAndLoadFromSlot(int slot)
7676
LoadMostRecentROM();
7777
break;
7878
case "Flush SaveRAM":
79-
FlushSaveRAM();
79+
FlushSaveRAMMenuItem_Click(null, null);
8080
break;
8181
case "Display FPS":
8282
ToggleFps();

src/BizHawk.Client.EmuHawk/MainForm.cs

Lines changed: 55 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1840,6 +1840,7 @@ public bool RunLibretroCoreChooser()
18401840

18411841
// countdown for saveram autoflushing
18421842
public int AutoFlushSaveRamIn { get; set; }
1843+
private bool AutoFlushSaveRamFailed;
18431844

18441845
private void SetStatusBar()
18451846
{
@@ -2063,7 +2064,7 @@ private void LoadSaveRam()
20632064
}
20642065
}
20652066

2066-
public bool FlushSaveRAM(bool autosave = false)
2067+
public FileWriteResult FlushSaveRAM(bool autosave = false)
20672068
{
20682069
if (Emulator.HasSaveRam())
20692070
{
@@ -2077,51 +2078,11 @@ public bool FlushSaveRAM(bool autosave = false)
20772078
path = Config.PathEntries.SaveRamAbsolutePath(Game, MovieSession.Movie);
20782079
}
20792080

2080-
var file = new FileInfo(path);
2081-
var newPath = $"{path}.new";
2082-
var newFile = new FileInfo(newPath);
2083-
var backupPath = $"{path}.bak";
2084-
var backupFile = new FileInfo(backupPath);
2085-
20862081
var saveram = Emulator.AsSaveRam().CloneSaveRam()!;
2087-
2088-
try
2089-
{
2090-
Directory.CreateDirectory(file.DirectoryName!);
2091-
using (var fs = File.Create(newPath))
2092-
{
2093-
fs.Write(saveram, 0, saveram.Length);
2094-
fs.Flush(flushToDisk: true);
2095-
}
2096-
2097-
if (file.Exists)
2098-
{
2099-
if (Config.BackupSaveram)
2100-
{
2101-
if (backupFile.Exists)
2102-
{
2103-
backupFile.Delete();
2104-
}
2105-
2106-
file.MoveTo(backupPath);
2107-
}
2108-
else
2109-
{
2110-
file.Delete();
2111-
}
2112-
}
2113-
2114-
newFile.MoveTo(path);
2115-
}
2116-
catch (IOException e)
2117-
{
2118-
AddOnScreenMessage("Failed to flush saveram!");
2119-
Console.Error.WriteLine(e);
2120-
return false;
2121-
}
2082+
return FileWriter.Write(path, saveram, $"{path}.bak");
21222083
}
21232084

2124-
return true;
2085+
return new();
21252086
}
21262087

21272088
private void RewireSound()
@@ -3180,8 +3141,22 @@ private void StepRunLoop_Core(bool force = false)
31803141
AutoFlushSaveRamIn--;
31813142
if (AutoFlushSaveRamIn <= 0)
31823143
{
3183-
FlushSaveRAM(true);
3184-
AutoFlushSaveRamIn = Config.FlushSaveRamFrames;
3144+
FileWriteResult result = FlushSaveRAM(true);
3145+
if (result.IsError)
3146+
{
3147+
// For autosave, allow one failure before bothering the user.
3148+
if (AutoFlushSaveRamFailed)
3149+
{
3150+
this.ErrorMessageBox(result, "Failed to flush saveram!");
3151+
}
3152+
AutoFlushSaveRamFailed = true;
3153+
AutoFlushSaveRamIn = Math.Min(600, Config.FlushSaveRamFrames);
3154+
}
3155+
else
3156+
{
3157+
AutoFlushSaveRamFailed = false;
3158+
AutoFlushSaveRamIn = Config.FlushSaveRamFrames;
3159+
}
31853160
}
31863161
}
31873162
// why not skip audio if the user doesn't want sound
@@ -4101,41 +4076,34 @@ private bool CloseGame(bool clearSram = false)
41014076
var path = Config.PathEntries.SaveRamAbsolutePath(Game, MovieSession.Movie);
41024077
if (File.Exists(path))
41034078
{
4104-
File.Delete(path);
4105-
AddOnScreenMessage("SRAM cleared.");
4079+
bool clearResult = this.DoWithTryAgainBox(() => {
4080+
try
4081+
{
4082+
File.Delete(path);
4083+
AddOnScreenMessage("SRAM cleared.");
4084+
return new();
4085+
}
4086+
catch (Exception ex)
4087+
{
4088+
return new(FileWriteEnum.FailedToDeleteGeneric, new(path, ""), ex);
4089+
}
4090+
}, "Failed to clear SRAM.");
4091+
if (!clearResult)
4092+
{
4093+
return false;
4094+
}
41064095
}
41074096
}
41084097
else if (Emulator.HasSaveRam())
41094098
{
4110-
while (true)
4111-
{
4112-
if (FlushSaveRAM()) break;
4113-
4114-
var result = ShowMessageBox3(
4115-
owner: this,
4116-
"Failed flushing the game's Save RAM to your disk.\n" +
4117-
"Do you want to try again?",
4118-
"IOError while writing SaveRAM",
4119-
EMsgBoxIcon.Error);
4120-
4121-
if (result is false) break;
4122-
if (result is null) return false;
4123-
}
4099+
bool flushResult = this.DoWithTryAgainBox(
4100+
() => FlushSaveRAM(),
4101+
"Failed flushing the game's Save RAM to your disk.");
4102+
if (!flushResult) return false;
41244103
}
41254104

4126-
bool? tryAgain = null;
4127-
do
4128-
{
4129-
FileWriteResult stateSaveResult = AutoSaveStateIfConfigured();
4130-
if (stateSaveResult.IsError)
4131-
{
4132-
tryAgain = this.ShowMessageBox3(
4133-
$"Failed to auto-save state. Do you want to try again?\n\nError details:\n{stateSaveResult.UserFriendlyErrorMessage()}\n{stateSaveResult.Exception.Message}",
4134-
"IOError while writing savestate",
4135-
EMsgBoxIcon.Error);
4136-
if (tryAgain == null) return false;
4137-
}
4138-
} while (tryAgain == true);
4105+
bool stateSaveResult = this.DoWithTryAgainBox(AutoSaveStateIfConfigured, "Failed to auto-save state.");
4106+
if (!stateSaveResult) return false;
41394107

41404108
StopAv();
41414109

@@ -4414,11 +4382,7 @@ public FileWriteResult SaveQuickSave(int slot, bool suppressOSD = false)
44144382
/// </summary>
44154383
private void SaveQuickSaveAndShowError(int slot)
44164384
{
4417-
FileWriteResult result = SaveQuickSave(slot);
4418-
if (result.IsError)
4419-
{
4420-
this.ErrorMessageBox(result, "Quick save failed.");
4421-
}
4385+
ShowMessageIfError(() => SaveQuickSave(slot), "Quick save failed.");
44224386
}
44234387

44244388
public bool EnsureCoreIsAccurate()
@@ -4488,11 +4452,9 @@ private void SaveStateAs()
44884452
initFileName: $"{SaveStatePrefix()}.QuickSave0.State");
44894453
if (shouldSaveResult is not null)
44904454
{
4491-
FileWriteResult saveResult = SaveState(path: shouldSaveResult, userFriendlyStateName: shouldSaveResult);
4492-
if (saveResult.IsError)
4493-
{
4494-
this.ErrorMessageBox(saveResult, "Unable to save.");
4495-
}
4455+
ShowMessageIfError(
4456+
() => SaveState(path: shouldSaveResult, userFriendlyStateName: shouldSaveResult),
4457+
"Unable to save state.");
44964458
}
44974459

44984460
if (Tools.IsLoaded<TAStudio>())
@@ -4783,6 +4745,15 @@ public bool ShowMessageBox2(
47834745
_ => null,
47844746
};
47854747

4748+
public void ShowMessageIfError(Func<FileWriteResult> action, string message)
4749+
{
4750+
FileWriteResult result = action();
4751+
if (result.IsError)
4752+
{
4753+
this.ErrorMessageBox(result, message);
4754+
}
4755+
}
4756+
47864757
public void StartSound() => Sound.StartSound();
47874758
public void StopSound() => Sound.StopSound();
47884759

0 commit comments

Comments
 (0)