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;