From c28d0fe01c12dcace44397cd9c507b5c5af6a975 Mon Sep 17 00:00:00 2001 From: SuuperW Date: Sun, 9 Aug 2026 16:19:25 -0500 Subject: [PATCH 1/8] allow users to make a named SaveRAM file --- src/BizHawk.Client.Common/FilesystemFilter.cs | 2 ++ .../config/PathEntryCollectionExtensions.cs | 8 +++++ .../MainForm.Designer.cs | 10 ++++++- src/BizHawk.Client.EmuHawk/MainForm.Events.cs | 29 +++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) 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/config/PathEntryCollectionExtensions.cs b/src/BizHawk.Client.Common/config/PathEntryCollectionExtensions.cs index ff7306ecd33..c51ac76a711 100644 --- a/src/BizHawk.Client.Common/config/PathEntryCollectionExtensions.cs +++ b/src/BizHawk.Client.Common/config/PathEntryCollectionExtensions.cs @@ -244,6 +244,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) { diff --git a/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs b/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs index 29b8f68aa72..7d360d5bedc 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs @@ -74,6 +74,7 @@ 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.toolStripMenuItem2 = new BizHawk.WinForms.Controls.ToolStripSeparatorEx(); this.MovieSubMenu = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); this.ReadonlyMenuItem = new BizHawk.WinForms.Controls.ToolStripMenuItemEx(); @@ -709,7 +710,8 @@ private void InitializeComponent() // SaveRAMSubMenu // this.SaveRAMSubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.FlushSaveRAMMenuItem}); + this.FlushSaveRAMMenuItem, + this.SaveSramAsMenuItem}); this.SaveRAMSubMenu.Text = "Save &RAM"; this.SaveRAMSubMenu.DropDownOpened += new System.EventHandler(this.SaveRamSubMenu_DropDownOpened); // @@ -718,6 +720,11 @@ 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); + // // MovieSubMenu // this.MovieSubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { @@ -2716,6 +2723,7 @@ 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 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..94b9fdf0997 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.Events.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.Events.cs @@ -318,6 +318,35 @@ 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) + { + byte[] saveram = Emulator.AsSaveRam().CloneSaveRam(); + if (saveram == null) + return; + ShowMessageIfError( + () => FileWriter.Write(shouldSaveResult, saveram), + "Unable to save Save RAM."); + } + } + private void ReadonlyMenuItem_Click(object sender, EventArgs e) { ToggleReadOnly(); From 0eac5899a01ac5f871ecd160c5b6759d9f0059bc Mon Sep 17 00:00:00 2001 From: SuuperW Date: Sun, 9 Aug 2026 16:51:52 -0500 Subject: [PATCH 2/8] allow user to manually load a SaveRAM file --- src/BizHawk.Client.Common/config/Config.cs | 2 + .../CustomControls/MsgBox.cs | 7 +++- .../MainForm.Designer.cs | 10 ++++- src/BizHawk.Client.EmuHawk/MainForm.Events.cs | 42 +++++++++++++++++++ src/BizHawk.Client.EmuHawk/MainForm.cs | 22 +++++++--- 5 files changed, 76 insertions(+), 7 deletions(-) diff --git a/src/BizHawk.Client.Common/config/Config.cs b/src/BizHawk.Client.Common/config/Config.cs index 8316338b5ab..a1aae30e084 100644 --- a/src/BizHawk.Client.Common/config/Config.cs +++ b/src/BizHawk.Client.Common/config/Config.cs @@ -197,6 +197,8 @@ public void SetWindowScaleFor(string sysID, int windowScale) /// public int FlushSaveRamFrames { get; set; } + public bool WarnLoadSramReboots { get; set; } = true; + public bool TurboSeek { get; set; } public ClientProfile SelectedProfile { get; set; } = ClientProfile.Unknown; 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 7d360d5bedc..ac607e837c1 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs @@ -75,6 +75,7 @@ private void InitializeComponent() 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(); @@ -711,7 +712,8 @@ private void InitializeComponent() // this.SaveRAMSubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.FlushSaveRAMMenuItem, - this.SaveSramAsMenuItem}); + this.SaveSramAsMenuItem, + this.LoadSramMenuItem}); this.SaveRAMSubMenu.Text = "Save &RAM"; this.SaveRAMSubMenu.DropDownOpened += new System.EventHandler(this.SaveRamSubMenu_DropDownOpened); // @@ -725,6 +727,11 @@ private void InitializeComponent() 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[] { @@ -2724,6 +2731,7 @@ private void InitializeComponent() 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 94b9fdf0997..c1bc77ba2bf 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.Events.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.Events.cs @@ -347,6 +347,48 @@ private void SaveSramAsMenuItem_Click(object sender, EventArgs e) } } + 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; + + // copy the file + File.Copy( + sourceFileName: fileToLoad, + destFileName: Config.PathEntries.SaveRamAbsolutePath(Game, movie: null), + overwrite: true); + + // reboot + RebootCore(false); + } + private void ReadonlyMenuItem_Click(object sender, EventArgs e) { ToggleReadOnly(); diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 54b59ca2172..277e03fe02f 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -1138,6 +1138,8 @@ private set public bool IsFastForwarding => InputManager.ClientControls["Fast Forward"] || IsTurboing; public bool IsRewinding { get; private set; } + private bool _saveSramOnReboot = true; + /// /// Used to disable secondary throttling (e.g. vsync, audio) for unthrottled modes or when the primary (clock) throttle is taking over (e.g. during fast forward/rewind). /// @@ -1311,7 +1313,9 @@ protected override void OnDeactivate(EventArgs e) base.OnDeactivate(e); } - public bool RebootCore() + public bool RebootCore() => RebootCore(true); + + private bool RebootCore(bool saveSram) { if (ToolControllingReboot is { } tool) { @@ -1321,9 +1325,17 @@ public bool RebootCore() else { if (CurrentlyOpenRomArgs == null) return true; - return LoadRom( - CurrentlyOpenRomArgs.OpenAdvanced.SimplePath, - CurrentlyOpenRomArgs with { ForcedSysID = Emulator.SystemId }); + if (!saveSram) _saveSramOnReboot = false; + try + { + return LoadRom( + CurrentlyOpenRomArgs.OpenAdvanced.SimplePath, + CurrentlyOpenRomArgs with { ForcedSysID = Emulator.SystemId }); + } + finally + { + _saveSramOnReboot = true; + } } } @@ -3952,7 +3964,7 @@ private bool CloseGame(bool clearSram = false) } } } - else if (Emulator.HasSaveRam()) + else if (Emulator.HasSaveRam() && _saveSramOnReboot) { TryAgainResult flushResult = this.DoWithTryAgainBox( () => FlushSaveRAM(), From 64e12386ce8f006df6502e701c3a09a07cece324 Mon Sep 17 00:00:00 2001 From: SuuperW Date: Sun, 9 Aug 2026 18:07:32 -0500 Subject: [PATCH 3/8] do not automatically save SRAM if a movie is or was loaded --- .../config/PathEntryCollectionExtensions.cs | 10 +++------- src/BizHawk.Client.EmuHawk/MainForm.Events.cs | 4 ++-- src/BizHawk.Client.EmuHawk/MainForm.Movie.cs | 3 +++ src/BizHawk.Client.EmuHawk/MainForm.cs | 13 +++++++------ 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/BizHawk.Client.Common/config/PathEntryCollectionExtensions.cs b/src/BizHawk.Client.Common/config/PathEntryCollectionExtensions.cs index c51ac76a711..deadfc22595 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"]; @@ -268,9 +264,9 @@ 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) + public static string AutoSaveRamAbsolutePath(this PathEntryCollection collection, IGameInfo game) { - var path = collection.SaveRamAbsolutePath(game, movie); + var path = collection.SaveRamAbsolutePath(game); return path.Insert(path.Length - 8, ".AutoSaveRAM"); } diff --git a/src/BizHawk.Client.EmuHawk/MainForm.Events.cs b/src/BizHawk.Client.EmuHawk/MainForm.Events.cs index c1bc77ba2bf..7a1a7dad469 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.Events.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.Events.cs @@ -382,7 +382,7 @@ private void LoadSramMenuItem_Click(object sender, EventArgs e) // copy the file File.Copy( sourceFileName: fileToLoad, - destFileName: Config.PathEntries.SaveRamAbsolutePath(Game, movie: null), + destFileName: Config.PathEntries.SaveRamAbsolutePath(Game), overwrite: true); // reboot @@ -1341,7 +1341,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 277e03fe02f..e835e01b4e4 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -1877,8 +1877,8 @@ 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(Config.PathEntries.SaveRamAbsolutePath(Game)); + var autoSaveRam = new FileInfo(Config.PathEntries.AutoSaveRamAbsolutePath(Game)); FileInfo saveramToLoad; if (saveRam.Exists && (!autoSaveRam.Exists || autoSaveRam.LastWriteTimeUtc <= saveRam.LastWriteTimeUtc)) @@ -1950,11 +1950,11 @@ public FileWriteResult FlushSaveRAM(bool autosave = false) string path; if (autosave) { - path = Config.PathEntries.AutoSaveRamAbsolutePath(Game, MovieSession.Movie); + path = Config.PathEntries.AutoSaveRamAbsolutePath(Game); } else { - path = Config.PathEntries.SaveRamAbsolutePath(Game, MovieSession.Movie); + path = Config.PathEntries.SaveRamAbsolutePath(Game); } var saveram = Emulator.AsSaveRam().CloneSaveRam(); @@ -3943,7 +3943,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(() => { @@ -3964,13 +3964,14 @@ private bool CloseGame(bool clearSram = false) } } } - else if (Emulator.HasSaveRam() && _saveSramOnReboot) + else if (Emulator.HasSaveRam() && _saveSramOnReboot && !_hadMovie) { TryAgainResult flushResult = this.DoWithTryAgainBox( () => FlushSaveRAM(), "Failed flushing the game's Save RAM to your disk."); if (flushResult == TryAgainResult.Canceled) return false; } + _hadMovie = false; TryAgainResult stateSaveResult = this.DoWithTryAgainBox(AutoSaveStateIfConfigured, "Failed to auto-save state."); if (stateSaveResult == TryAgainResult.Canceled) return false; From 9fb751b7342a83b9833c33e5ec2945aca2c54fff Mon Sep 17 00:00:00 2001 From: SuuperW Date: Sun, 9 Aug 2026 18:29:53 -0500 Subject: [PATCH 4/8] do not overwrite the default SRAM file; make FlushSaveRAM write to whatever file was opened --- src/BizHawk.Client.Common/LoadRomArgs.cs | 7 ++- .../config/PathEntryCollectionExtensions.cs | 6 --- src/BizHawk.Client.EmuHawk/MainForm.Events.cs | 8 +--- src/BizHawk.Client.EmuHawk/MainForm.cs | 47 ++++++++----------- 4 files changed, 25 insertions(+), 43 deletions(-) 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/PathEntryCollectionExtensions.cs b/src/BizHawk.Client.Common/config/PathEntryCollectionExtensions.cs index deadfc22595..c8a74b1188c 100644 --- a/src/BizHawk.Client.Common/config/PathEntryCollectionExtensions.cs +++ b/src/BizHawk.Client.Common/config/PathEntryCollectionExtensions.cs @@ -264,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) - { - var path = collection.SaveRamAbsolutePath(game); - 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/MainForm.Events.cs b/src/BizHawk.Client.EmuHawk/MainForm.Events.cs index 7a1a7dad469..3590f532691 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.Events.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.Events.cs @@ -379,14 +379,8 @@ private void LoadSramMenuItem_Click(object sender, EventArgs e) filter: filterset); if (fileToLoad == null) return; - // copy the file - File.Copy( - sourceFileName: fileToLoad, - destFileName: Config.PathEntries.SaveRamAbsolutePath(Game), - overwrite: true); - // reboot - RebootCore(false); + RebootCore(fileToLoad); } private void ReadonlyMenuItem_Click(object sender, EventArgs e) diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index e835e01b4e4..9d5895204d4 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -1138,8 +1138,6 @@ private set public bool IsFastForwarding => InputManager.ClientControls["Fast Forward"] || IsTurboing; public bool IsRewinding { get; private set; } - private bool _saveSramOnReboot = true; - /// /// Used to disable secondary throttling (e.g. vsync, audio) for unthrottled modes or when the primary (clock) throttle is taking over (e.g. during fast forward/rewind). /// @@ -1313,9 +1311,9 @@ protected override void OnDeactivate(EventArgs e) base.OnDeactivate(e); } - public bool RebootCore() => RebootCore(true); + public bool RebootCore() => RebootCore(CurrentlyOpenRomArgs?.SaveRamPath); - private bool RebootCore(bool saveSram) + private bool RebootCore(string/*?*/ saveRamPath) { if (ToolControllingReboot is { } tool) { @@ -1325,17 +1323,9 @@ private bool RebootCore(bool saveSram) else { if (CurrentlyOpenRomArgs == null) return true; - if (!saveSram) _saveSramOnReboot = false; - try - { - return LoadRom( - CurrentlyOpenRomArgs.OpenAdvanced.SimplePath, - CurrentlyOpenRomArgs with { ForcedSysID = Emulator.SystemId }); - } - finally - { - _saveSramOnReboot = true; - } + return LoadRom( + CurrentlyOpenRomArgs.OpenAdvanced.SimplePath, + CurrentlyOpenRomArgs with { ForcedSysID = Emulator.SystemId, SaveRamPath = saveRamPath }); } } @@ -1873,12 +1863,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)); - var autoSaveRam = new FileInfo(Config.PathEntries.AutoSaveRamAbsolutePath(Game)); + 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)) @@ -1947,14 +1942,10 @@ public FileWriteResult FlushSaveRAM(bool autosave = false) { if (Emulator.HasSaveRam()) { - string path; + string path = CurrentlyOpenRomArgs.SaveRamPath ?? Config.PathEntries.SaveRamAbsolutePath(Game); if (autosave) { - path = Config.PathEntries.AutoSaveRamAbsolutePath(Game); - } - else - { - path = Config.PathEntries.SaveRamAbsolutePath(Game); + path = MakeSaveRamAutosavePath(path); } var saveram = Emulator.AsSaveRam().CloneSaveRam(); @@ -3789,6 +3780,10 @@ 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) { @@ -3796,10 +3791,6 @@ private bool LoadRomInternal(string path, LoadRomArgs args, out bool failureIsFr AutoFlushSaveRamIn = Config.FlushSaveRamFrames; } - var previousRom = CurrentlyOpenRom; - CurrentlyOpenRom = oaOpenrom?.Path ?? openAdvancedArgs; - CurrentlyOpenRomArgs = args; - Tools.Restart(Config, Emulator, Game); if (previousRom != CurrentlyOpenRom) @@ -3964,7 +3955,7 @@ private bool CloseGame(bool clearSram = false) } } } - else if (Emulator.HasSaveRam() && _saveSramOnReboot && !_hadMovie) + else if (Emulator.HasSaveRam() && !_hadMovie) { TryAgainResult flushResult = this.DoWithTryAgainBox( () => FlushSaveRAM(), From 522940178037a61d0263a51d976f60056e2771a9 Mon Sep 17 00:00:00 2001 From: SuuperW Date: Mon, 10 Aug 2026 21:54:24 -0500 Subject: [PATCH 5/8] also don't do recurring autosave SRAM when movie --- src/BizHawk.Client.EmuHawk/MainForm.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 9d5895204d4..1ac56e4e104 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -2980,7 +2980,7 @@ private void StepRunLoop_Core(bool force = false) RA?.OnFrameAdvance(); - if (Config.AutosaveSaveRAM) + if (Config.AutosaveSaveRAM && !_hadMovie) { AutoFlushSaveRamIn--; if (AutoFlushSaveRamIn <= 0) From fbc30ea64f60444ae927280a167b128d7f3beb98 Mon Sep 17 00:00:00 2001 From: SuuperW Date: Mon, 10 Aug 2026 22:14:12 -0500 Subject: [PATCH 6/8] enable periodic autosave SRAM, remove automatic save on close respect user's .bak setting --- src/BizHawk.Client.Common/config/Config.cs | 4 ++-- src/BizHawk.Client.EmuHawk/MainForm.cs | 25 +++++++++++----------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/BizHawk.Client.Common/config/Config.cs b/src/BizHawk.Client.Common/config/Config.cs index a1aae30e084..b77d6f6ee0b 100644 --- a/src/BizHawk.Client.Common/config/Config.cs +++ b/src/BizHawk.Client.Common/config/Config.cs @@ -190,12 +190,12 @@ 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; diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 1ac56e4e104..2db32ead8cb 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -1942,16 +1942,22 @@ public FileWriteResult FlushSaveRAM(bool autosave = false) { if (Emulator.HasSaveRam()) { - string path = CurrentlyOpenRomArgs.SaveRamPath ?? Config.PathEntries.SaveRamAbsolutePath(Game); - if (autosave) - { - path = MakeSaveRamAutosavePath(path); - } + string normalPath = CurrentlyOpenRomArgs.SaveRamPath ?? Config.PathEntries.SaveRamAbsolutePath(Game); + string autoPath = MakeSaveRamAutosavePath(normalPath); var saveram = Emulator.AsSaveRam().CloneSaveRam(); if (saveram == null) return new(); - return FileWriter.Write(path, saveram, $"{path}.bak"); + + if (autosave) + { + // No backup: autosave is already a backup + return FileWriter.Write(autoPath, saveram); + } + else + { + return FileWriter.Write(normalPath, saveram, Config.BackupSaveram ? $"{normalPath}.bak" : null); + } } return new(); @@ -3955,13 +3961,6 @@ private bool CloseGame(bool clearSram = false) } } } - else if (Emulator.HasSaveRam() && !_hadMovie) - { - TryAgainResult flushResult = this.DoWithTryAgainBox( - () => FlushSaveRAM(), - "Failed flushing the game's Save RAM to your disk."); - if (flushResult == TryAgainResult.Canceled) return false; - } _hadMovie = false; TryAgainResult stateSaveResult = this.DoWithTryAgainBox(AutoSaveStateIfConfigured, "Failed to auto-save state."); From a7b93451f7a704eb6e0a1d06ee4880b905267ee4 Mon Sep 17 00:00:00 2001 From: SuuperW Date: Mon, 10 Aug 2026 22:48:08 -0500 Subject: [PATCH 7/8] ask if user wants to flush SRAM on close; delete the autosave if they say no or any time SRAM is manually saved --- src/BizHawk.Client.EmuHawk/MainForm.cs | 30 +++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 2db32ead8cb..55c15e23c40 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -1742,6 +1742,8 @@ public bool RunLibretroCoreChooser() public int AutoFlushSaveRamIn { get; set; } private bool AutoFlushSaveRamFailed; + private int _lastSaveRamFlush = -1; + private void SetStatusBar() { if (!_inFullscreen) @@ -1956,7 +1958,10 @@ public FileWriteResult FlushSaveRAM(bool autosave = false) } else { - return FileWriter.Write(normalPath, saveram, Config.BackupSaveram ? $"{normalPath}.bak" : null); + 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; } } @@ -3796,6 +3801,7 @@ private bool LoadRomInternal(string path, LoadRomArgs args, out bool failureIsFr LoadSaveRam(); AutoFlushSaveRamIn = Config.FlushSaveRamFrames; } + _lastSaveRamFlush = -1; Tools.Restart(Config, Emulator, Game); @@ -3961,6 +3967,28 @@ private bool CloseGame(bool clearSram = false) } } } + else if (!_hadMovie) + { + 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."); From bb048e2f6b70ca66c68192be828684108cf64464 Mon Sep 17 00:00:00 2001 From: SuuperW Date: Mon, 10 Aug 2026 22:49:54 -0500 Subject: [PATCH 8/8] when the user does save SRAM as, make that the current SRAM file --- src/BizHawk.Client.EmuHawk/MainForm.Events.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/BizHawk.Client.EmuHawk/MainForm.Events.cs b/src/BizHawk.Client.EmuHawk/MainForm.Events.cs index 3590f532691..18874cf3e66 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.Events.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.Events.cs @@ -338,12 +338,11 @@ private void SaveSramAsMenuItem_Click(object sender, EventArgs e) filter: filterset); if (shouldSaveResult is not null) { - byte[] saveram = Emulator.AsSaveRam().CloneSaveRam(); - if (saveram == null) - return; - ShowMessageIfError( - () => FileWriter.Write(shouldSaveResult, saveram), - "Unable to save Save RAM."); + 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 */ } } }