diff --git a/src/BizHawk.Client.Common/FilesystemFilter.cs b/src/BizHawk.Client.Common/FilesystemFilter.cs
index e05a06c4c67..d6cea1647eb 100644
--- a/src/BizHawk.Client.Common/FilesystemFilter.cs
+++ b/src/BizHawk.Client.Common/FilesystemFilter.cs
@@ -65,6 +65,8 @@ public FilesystemFilter(
public static readonly FilesystemFilter PNGs = new FilesystemFilter("PNG Files", new[] { "png" });
+ public static readonly FilesystemFilter SaveRams = new FilesystemFilter("SaveRAM Files", new[] { "SaveRAM", "bin" });
+
public static readonly FilesystemFilter TAStudioProjects = new FilesystemFilter("TAS Project Files", new[] { MovieService.TasMovieExtension });
public static readonly FilesystemFilter TextFiles = new FilesystemFilter("Text Files", new[] { "txt" });
diff --git a/src/BizHawk.Client.Common/LoadRomArgs.cs b/src/BizHawk.Client.Common/LoadRomArgs.cs
index e68a1356cc8..90ecaa02ca9 100644
--- a/src/BizHawk.Client.Common/LoadRomArgs.cs
+++ b/src/BizHawk.Client.Common/LoadRomArgs.cs
@@ -1,7 +1,10 @@
+#nullable enable
+
namespace BizHawk.Client.Common
{
public sealed record class LoadRomArgs(
IOpenAdvanced OpenAdvanced,
- string/*?*/ ForcedSysID = null,
- bool? Deterministic = null);
+ string? ForcedSysID = null,
+ bool? Deterministic = null,
+ string? SaveRamPath = null);
}
diff --git a/src/BizHawk.Client.Common/config/Config.cs b/src/BizHawk.Client.Common/config/Config.cs
index 8316338b5ab..b77d6f6ee0b 100644
--- a/src/BizHawk.Client.Common/config/Config.cs
+++ b/src/BizHawk.Client.Common/config/Config.cs
@@ -190,12 +190,14 @@ public void SetWindowScaleFor(string sysID, int windowScale)
///
/// Whether to make AutoSave files at periodic intervals
///
- public bool AutosaveSaveRAM { get; set; }
+ public bool AutosaveSaveRAM { get; set; } = true;
///
/// Intervals at which to make AutoSave files
///
- public int FlushSaveRamFrames { get; set; }
+ public int FlushSaveRamFrames { get; set; } = 5 * 60 * 60; // 5 minutes (it assumes 60 fps)
+
+ public bool WarnLoadSramReboots { get; set; } = true;
public bool TurboSeek { get; set; }
diff --git a/src/BizHawk.Client.Common/config/PathEntryCollectionExtensions.cs b/src/BizHawk.Client.Common/config/PathEntryCollectionExtensions.cs
index ff7306ecd33..c8a74b1188c 100644
--- a/src/BizHawk.Client.Common/config/PathEntryCollectionExtensions.cs
+++ b/src/BizHawk.Client.Common/config/PathEntryCollectionExtensions.cs
@@ -230,13 +230,9 @@ public static string RomAbsolutePath(this PathEntryCollection collection, string
return collection.AbsolutePathFor(path.Path, systemId);
}
- public static string SaveRamAbsolutePath(this PathEntryCollection collection, IGameInfo game, IMovie movie)
+ public static string SaveRamAbsolutePath(this PathEntryCollection collection, IGameInfo game)
{
var name = game.FilesystemSafeName();
- if (movie.IsActive())
- {
- name += $".{Path.GetFileNameWithoutExtension(movie.Filename)}";
- }
var pathEntry = collection[game.System, "Save RAM"]
?? collection[game.System, "Base"];
@@ -244,6 +240,14 @@ public static string SaveRamAbsolutePath(this PathEntryCollection collection, IG
return $"{Path.Combine(collection.AbsolutePathFor(pathEntry.Path, game.System), name)}.SaveRAM";
}
+ public static string SaveRamAbsolutePath(this PathEntryCollection collection, string system)
+ {
+ var pathEntry = collection[system, "Save RAM"]
+ ?? collection[system, "Base"];
+
+ return collection.AbsolutePathFor(pathEntry.Path, system);
+ }
+
// Shenanigans
public static string RetroSaveRamAbsolutePath(this PathEntryCollection collection, string coreName)
{
@@ -260,12 +264,6 @@ public static string RetroSystemAbsolutePath(this PathEntryCollection collection
return Path.Combine(collection.AbsolutePathFor(pathEntry.Path, VSystemID.Raw.Libretro), coreName);
}
- public static string AutoSaveRamAbsolutePath(this PathEntryCollection collection, IGameInfo game, IMovie movie)
- {
- var path = collection.SaveRamAbsolutePath(game, movie);
- return path.Insert(path.Length - 8, ".AutoSaveRAM");
- }
-
public static string CheatsAbsolutePath(this PathEntryCollection collection, string systemId)
{
var pathEntry = collection[systemId, "Cheats"]
diff --git a/src/BizHawk.Client.EmuHawk/CustomControls/MsgBox.cs b/src/BizHawk.Client.EmuHawk/CustomControls/MsgBox.cs
index e8dd25fd52e..9ee92ddf9ce 100644
--- a/src/BizHawk.Client.EmuHawk/CustomControls/MsgBox.cs
+++ b/src/BizHawk.Client.EmuHawk/CustomControls/MsgBox.cs
@@ -18,13 +18,16 @@ internal partial class MsgBox : Form
// The min required width of the button and checkbox row. Sum of button widths + checkbox width + margins.
private int _minButtonRowWidth;
+ public bool UserSaysDontShowAgain => chkBx.Checked;
+
///
/// Create a new instance of the dialog box with a message and title and a standard windows MessageBox icon.
///
/// Message text.
/// Dialog Box title.
/// Standard system MessageBox icon.
- public MsgBox(string message, string title, MessageBoxIcon boxIcon)
+ /// Show a checkbox letting the user say "don't show this again".
+ public MsgBox(string message, string title, MessageBoxIcon boxIcon, bool showCheckbox = false)
{
var icon = GetMessageBoxIcon(boxIcon);
InitializeComponent();
@@ -39,6 +42,8 @@ public MsgBox(string message, string title, MessageBoxIcon boxIcon)
{
messageLbl.Location = new Point(FormXMargin, FormYMargin);
}
+
+ chkBx.Visible = showCheckbox;
}
///
diff --git a/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs b/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs
index 29b8f68aa72..ac607e837c1 100644
--- a/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs
+++ b/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs
@@ -74,6 +74,8 @@ private void InitializeComponent()
this.LoadCurrentSlotMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx();
this.SaveRAMSubMenu = new BizHawk.WinForms.Controls.ToolStripMenuItemEx();
this.FlushSaveRAMMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx();
+ this.SaveSramAsMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx();
+ this.LoadSramMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx();
this.toolStripMenuItem2 = new BizHawk.WinForms.Controls.ToolStripSeparatorEx();
this.MovieSubMenu = new BizHawk.WinForms.Controls.ToolStripMenuItemEx();
this.ReadonlyMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx();
@@ -709,7 +711,9 @@ private void InitializeComponent()
// SaveRAMSubMenu
//
this.SaveRAMSubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
- this.FlushSaveRAMMenuItem});
+ this.FlushSaveRAMMenuItem,
+ this.SaveSramAsMenuItem,
+ this.LoadSramMenuItem});
this.SaveRAMSubMenu.Text = "Save &RAM";
this.SaveRAMSubMenu.DropDownOpened += new System.EventHandler(this.SaveRamSubMenu_DropDownOpened);
//
@@ -718,6 +722,16 @@ private void InitializeComponent()
this.FlushSaveRAMMenuItem.Text = "&Flush Save Ram";
this.FlushSaveRAMMenuItem.Click += new System.EventHandler(this.FlushSaveRAMMenuItem_Click);
//
+ // SaveSramAsMenuItem
+ //
+ this.SaveSramAsMenuItem.Text = "Save SRAM &As";
+ this.SaveSramAsMenuItem.Click += new System.EventHandler(this.SaveSramAsMenuItem_Click);
+ //
+ // LoadSramMenuItem
+ //
+ this.LoadSramMenuItem.Text = "L&oad SRAM";
+ this.LoadSramMenuItem.Click += new System.EventHandler(this.LoadSramMenuItem_Click);
+ //
// MovieSubMenu
//
this.MovieSubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
@@ -2716,6 +2730,8 @@ private void InitializeComponent()
private BizHawk.WinForms.Controls.ToolStripMenuItemEx PSXOptionsMenuItem;
private BizHawk.WinForms.Controls.ToolStripMenuItemEx SaveRAMSubMenu;
private BizHawk.WinForms.Controls.ToolStripMenuItemEx FlushSaveRAMMenuItem;
+ private BizHawk.WinForms.Controls.ToolStripMenuItemEx SaveSramAsMenuItem;
+ private BizHawk.WinForms.Controls.ToolStripMenuItemEx LoadSramMenuItem;
private BizHawk.WinForms.Controls.ToolStripMenuItemEx PSXDiscControlsMenuItem;
private BizHawk.WinForms.Controls.StatusLabelEx UpdateNotification;
private BizHawk.WinForms.Controls.ToolStripMenuItemEx PSXControllerSettingsMenuItem;
diff --git a/src/BizHawk.Client.EmuHawk/MainForm.Events.cs b/src/BizHawk.Client.EmuHawk/MainForm.Events.cs
index c721343904b..18874cf3e66 100644
--- a/src/BizHawk.Client.EmuHawk/MainForm.Events.cs
+++ b/src/BizHawk.Client.EmuHawk/MainForm.Events.cs
@@ -318,6 +318,70 @@ private void FlushSaveRAMMenuItem_Click(object sender, EventArgs e)
ShowMessageIfError(() => FlushSaveRAM(), "Failed to flush saveram!");
}
+ private void SaveSramAsMenuItem_Click(object sender, EventArgs e)
+ {
+ string sramFolderPath = Config.PathEntries.SaveRamAbsolutePath(Game.System);
+
+ // Create folder if it doesn't already exist
+ try
+ {
+ Directory.CreateDirectory(sramFolderPath);
+ }
+ catch (IOException) { /* ignored */ }
+ catch (UnauthorizedAccessException) { /* ignored */ }
+
+ FilesystemFilterSet filterset = new(FilesystemFilter.SaveRams);
+ var shouldSaveResult = this.ShowFileSaveDialog(
+ initDir: sramFolderPath,
+ discardCWDChange: true,
+ fileExt: $".{filterset.Filters[0].Extensions.First()}",
+ filter: filterset);
+ if (shouldSaveResult is not null)
+ {
+ string normalPath = CurrentlyOpenRomArgs.SaveRamPath ?? Config.PathEntries.SaveRamAbsolutePath(Game);
+ string oldAutoPath = MakeSaveRamAutosavePath(normalPath);
+ CurrentlyOpenRomArgs = CurrentlyOpenRomArgs with { SaveRamPath = shouldSaveResult };
+ FlushSaveRAMMenuItem_Click(sender, e);
+ try { File.Delete(oldAutoPath); } catch { /* nothing */ }
+ }
+ }
+
+ private void LoadSramMenuItem_Click(object sender, EventArgs e)
+ {
+ if (Config.WarnLoadSramReboots)
+ {
+ MsgBox box = new(
+ message: "Loading Save RAM requires a core reboot. Proceed?",
+ title: "Reboot?",
+ boxIcon: MessageBoxIcon.Warning,
+ showCheckbox: true);
+ box.SetButtons([ "Yes", "No" ], [ DialogResult.Yes, DialogResult.Cancel ]);
+ DialogResult result = box.ShowDialog();
+ if (box.UserSaysDontShowAgain) Config.WarnLoadSramReboots = false;
+ if (result == DialogResult.Cancel) return;
+ }
+
+ // get the file
+ string sramFolderPath = Config.PathEntries.SaveRamAbsolutePath(Game.System);
+ // Create folder if it doesn't already exist
+ try
+ {
+ Directory.CreateDirectory(sramFolderPath);
+ }
+ catch (IOException) { /* ignored */ }
+ catch (UnauthorizedAccessException) { /* ignored */ }
+
+ FilesystemFilterSet filterset = new(FilesystemFilter.SaveRams);
+ string fileToLoad = this.ShowFileOpenDialog(
+ initDir: sramFolderPath,
+ discardCWDChange: true,
+ filter: filterset);
+ if (fileToLoad == null) return;
+
+ // reboot
+ RebootCore(fileToLoad);
+ }
+
private void ReadonlyMenuItem_Click(object sender, EventArgs e)
{
ToggleReadOnly();
@@ -1270,7 +1334,7 @@ private void MainFormContextMenu_Opening(object sender, System.ComponentModel.Ca
ConfigContextMenuItem.Visible = _inFullscreen;
- ClearSRAMContextMenuItem.Visible = File.Exists(Config.PathEntries.SaveRamAbsolutePath(Game, MovieSession.Movie));
+ ClearSRAMContextMenuItem.Visible = File.Exists(Config.PathEntries.SaveRamAbsolutePath(Game));
ContextSeparator_AfterROM.Visible = OpenRomContextMenuItem.Visible || LoadLastRomContextMenuItem.Visible;
diff --git a/src/BizHawk.Client.EmuHawk/MainForm.Movie.cs b/src/BizHawk.Client.EmuHawk/MainForm.Movie.cs
index b9e7f04f946..11fe212c449 100644
--- a/src/BizHawk.Client.EmuHawk/MainForm.Movie.cs
+++ b/src/BizHawk.Client.EmuHawk/MainForm.Movie.cs
@@ -21,6 +21,8 @@ namespace BizHawk.Client.EmuHawk
{
public partial class MainForm
{
+ private bool _hadMovie = false;
+
public bool StartNewMovie(IMovie movie, bool newMovie)
{
if (movie is null) throw new ArgumentNullException(paramName: nameof(movie));
@@ -63,6 +65,7 @@ public bool StartNewMovie(IMovie movie, bool newMovie)
Config.RecentMovies.Add(movie.Filename);
MovieSession.RunQueuedMovie(newMovie, Emulator);
+ _hadMovie = true;
if (newMovie)
{
PopulateWithDefaultHeaderValues(movie);
diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs
index 54b59ca2172..55c15e23c40 100644
--- a/src/BizHawk.Client.EmuHawk/MainForm.cs
+++ b/src/BizHawk.Client.EmuHawk/MainForm.cs
@@ -1311,7 +1311,9 @@ protected override void OnDeactivate(EventArgs e)
base.OnDeactivate(e);
}
- public bool RebootCore()
+ public bool RebootCore() => RebootCore(CurrentlyOpenRomArgs?.SaveRamPath);
+
+ private bool RebootCore(string/*?*/ saveRamPath)
{
if (ToolControllingReboot is { } tool)
{
@@ -1323,7 +1325,7 @@ public bool RebootCore()
if (CurrentlyOpenRomArgs == null) return true;
return LoadRom(
CurrentlyOpenRomArgs.OpenAdvanced.SimplePath,
- CurrentlyOpenRomArgs with { ForcedSysID = Emulator.SystemId });
+ CurrentlyOpenRomArgs with { ForcedSysID = Emulator.SystemId, SaveRamPath = saveRamPath });
}
}
@@ -1740,6 +1742,8 @@ public bool RunLibretroCoreChooser()
public int AutoFlushSaveRamIn { get; set; }
private bool AutoFlushSaveRamFailed;
+ private int _lastSaveRamFlush = -1;
+
private void SetStatusBar()
{
if (!_inFullscreen)
@@ -1861,12 +1865,17 @@ public void UpdateDumpInfo(RomStatus? newStatus = null)
// Better is to just keep the game and rom hashes as properties and then generate the rom info from this
private string _defaultRomDetails = "";
+ private static string MakeSaveRamAutosavePath(string sramPath)
+ {
+ return sramPath.Insert(sramPath.Length - 8, ".AutoSaveRAM");
+ }
+
private void LoadSaveRam()
{
if (Emulator.HasSaveRam())
{
- var saveRam = new FileInfo(Config.PathEntries.SaveRamAbsolutePath(Game, MovieSession.Movie));
- var autoSaveRam = new FileInfo(Config.PathEntries.AutoSaveRamAbsolutePath(Game, MovieSession.Movie));
+ var saveRam = new FileInfo(CurrentlyOpenRomArgs.SaveRamPath ?? Config.PathEntries.SaveRamAbsolutePath(Game));
+ var autoSaveRam = new FileInfo(MakeSaveRamAutosavePath(saveRam.FullName));
FileInfo saveramToLoad;
if (saveRam.Exists && (!autoSaveRam.Exists || autoSaveRam.LastWriteTimeUtc <= saveRam.LastWriteTimeUtc))
@@ -1935,20 +1944,25 @@ public FileWriteResult FlushSaveRAM(bool autosave = false)
{
if (Emulator.HasSaveRam())
{
- string path;
+ string normalPath = CurrentlyOpenRomArgs.SaveRamPath ?? Config.PathEntries.SaveRamAbsolutePath(Game);
+ string autoPath = MakeSaveRamAutosavePath(normalPath);
+
+ var saveram = Emulator.AsSaveRam().CloneSaveRam();
+ if (saveram == null)
+ return new();
+
if (autosave)
{
- path = Config.PathEntries.AutoSaveRamAbsolutePath(Game, MovieSession.Movie);
+ // No backup: autosave is already a backup
+ return FileWriter.Write(autoPath, saveram);
}
else
{
- path = Config.PathEntries.SaveRamAbsolutePath(Game, MovieSession.Movie);
+ FileWriteResult result = FileWriter.Write(normalPath, saveram, Config.BackupSaveram ? $"{normalPath}.bak" : null);
+ if (!result.IsError) _lastSaveRamFlush = Emulator.Frame;
+ try { File.Delete(autoPath); } catch { /* nothing */ }
+ return result;
}
-
- var saveram = Emulator.AsSaveRam().CloneSaveRam();
- if (saveram == null)
- return new();
- return FileWriter.Write(path, saveram, $"{path}.bak");
}
return new();
@@ -2977,7 +2991,7 @@ private void StepRunLoop_Core(bool force = false)
RA?.OnFrameAdvance();
- if (Config.AutosaveSaveRAM)
+ if (Config.AutosaveSaveRAM && !_hadMovie)
{
AutoFlushSaveRamIn--;
if (AutoFlushSaveRamIn <= 0)
@@ -3777,16 +3791,17 @@ private bool LoadRomInternal(string path, LoadRomArgs args, out bool failureIsFr
Console.WriteLine("Core reported BoardID: \"{0}\"", Emulator.AsBoardInfo().BoardName);
}
+ var previousRom = CurrentlyOpenRom;
+ CurrentlyOpenRom = oaOpenrom?.Path ?? openAdvancedArgs;
+ CurrentlyOpenRomArgs = args;
+
// Don't load Save Ram if a movie is being loaded
if (!MovieSession.NewMovieQueued)
{
LoadSaveRam();
AutoFlushSaveRamIn = Config.FlushSaveRamFrames;
}
-
- var previousRom = CurrentlyOpenRom;
- CurrentlyOpenRom = oaOpenrom?.Path ?? openAdvancedArgs;
- CurrentlyOpenRomArgs = args;
+ _lastSaveRamFlush = -1;
Tools.Restart(Config, Emulator, Game);
@@ -3931,7 +3946,7 @@ private bool CloseGame(bool clearSram = false)
if (clearSram)
{
- var path = Config.PathEntries.SaveRamAbsolutePath(Game, MovieSession.Movie);
+ var path = Config.PathEntries.SaveRamAbsolutePath(Game);
if (File.Exists(path))
{
TryAgainResult clearResult = this.DoWithTryAgainBox(() => {
@@ -3952,13 +3967,29 @@ private bool CloseGame(bool clearSram = false)
}
}
}
- else if (Emulator.HasSaveRam())
+ else if (!_hadMovie)
{
- TryAgainResult flushResult = this.DoWithTryAgainBox(
- () => FlushSaveRAM(),
- "Failed flushing the game's Save RAM to your disk.");
- if (flushResult == TryAgainResult.Canceled) return false;
+ ISaveRam sramService = Emulator.AsSaveRam();
+ // if (sramService.SaveRamModified) // not a good idea because some core always return true (do any return false negative?)
+ if (sramService != null && _lastSaveRamFlush < Emulator.Frame)
+ {
+ if (this.ShowMessageBox2(
+ text: "Flsuh Save RAM?",
+ caption: "Save?",
+ icon: EMsgBoxIcon.Question))
+ {
+ TryAgainResult saveSramResult = this.DoWithTryAgainBox(() => FlushSaveRAM(), "Failed to save Save RAM.");
+ if (saveSramResult == TryAgainResult.Canceled) return false;
+ }
+ else
+ {
+ // either way, we don't need to keep a backup if the user explicitly said they don't want a save
+ string normalPath = CurrentlyOpenRomArgs.SaveRamPath ?? Config.PathEntries.SaveRamAbsolutePath(Game);
+ try { File.Delete(MakeSaveRamAutosavePath(normalPath)); } catch { /* nothing */ }
+ }
+ }
}
+ _hadMovie = false;
TryAgainResult stateSaveResult = this.DoWithTryAgainBox(AutoSaveStateIfConfigured, "Failed to auto-save state.");
if (stateSaveResult == TryAgainResult.Canceled) return false;