Merge 629df122b5
into 9d2bc341b3
This commit is contained in:
commit
777492c510
|
@ -120,6 +120,15 @@ dotnet_diagnostic.CA2229.severity = silent
|
|||
# Opt in to preview features before using them
|
||||
dotnet_diagnostic.CA2252.severity = silent # CSharpDetectPreviewFeatureAnalyzer very slow
|
||||
|
||||
## Nullable rules; generics are a bit wonky and I have no idea why we're allowed to configure these to be not errors or why they aren't errors by default.
|
||||
|
||||
# Nullability of reference types in value doesn't match target type.
|
||||
dotnet_diagnostic.CS8619.severity = error
|
||||
# Make Foo<string?> an error for class Foo<T> where T : class. Use `where T : class?` if Foo<string?> should be allowed.
|
||||
dotnet_diagnostic.CS8634.severity = error
|
||||
# Nullability of type argument doesn't match 'notnull' constraint.
|
||||
dotnet_diagnostic.CS8714.severity = error
|
||||
|
||||
## .NET DocumentationAnalyzers style rules
|
||||
|
||||
# Place text in paragraphs
|
||||
|
|
|
@ -158,9 +158,19 @@ namespace BizHawk.Client.Common
|
|||
|
||||
public void RebootCore() => _mainForm.RebootCore();
|
||||
|
||||
// TODO: Change return type to FileWriteResult.
|
||||
public void SaveRam() => _mainForm.FlushSaveRAM();
|
||||
|
||||
public void SaveState(string name) => _mainForm.SaveState(Path.Combine(_config.PathEntries.SaveStateAbsolutePath(Game.System), $"{name}.State"), name, fromLua: false);
|
||||
// TODO: Change return type to FileWriteResult.
|
||||
// We may wish to change more than that, since we have a mostly-dupicate ISaveStateApi.Save, neither has documentation indicating what the differences are.
|
||||
public void SaveState(string name)
|
||||
{
|
||||
FileWriteResult result = _mainForm.SaveState(Path.Combine(_config.PathEntries.SaveStateAbsolutePath(Game.System), $"{name}.State"), name);
|
||||
if (result.Exception != null && result.Exception is not UnlessUsingApiException)
|
||||
{
|
||||
throw result.Exception;
|
||||
}
|
||||
}
|
||||
|
||||
public int ScreenHeight() => _displayManager.GetPanelNativeSize().Height;
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ namespace BizHawk.Client.Common
|
|||
return Bk2LogEntryGenerator.GenerateLogEntry(_movieSession.Movie.GetInputState(frame));
|
||||
}
|
||||
|
||||
// TODO: Change return type to FileWriteResult
|
||||
public void Save(string filename)
|
||||
{
|
||||
if (_movieSession.Movie.NotActive())
|
||||
|
@ -70,7 +71,8 @@ namespace BizHawk.Client.Common
|
|||
}
|
||||
_movieSession.Movie.Filename = filename;
|
||||
}
|
||||
_movieSession.Movie.Save();
|
||||
FileWriteResult result = _movieSession.Movie.Save();
|
||||
if (result.Exception != null) throw result.Exception;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, string> GetHeader()
|
||||
|
|
|
@ -39,8 +39,17 @@ namespace BizHawk.Client.Common
|
|||
return _mainForm.LoadQuickSave(slotNum, suppressOSD: suppressOSD);
|
||||
}
|
||||
|
||||
public void Save(string path, bool suppressOSD) => _mainForm.SaveState(path, path, true, suppressOSD);
|
||||
// TODO: Change return type FileWriteResult.
|
||||
public void Save(string path, bool suppressOSD)
|
||||
{
|
||||
FileWriteResult result = _mainForm.SaveState(path, path, suppressOSD);
|
||||
if (result.Exception != null && result.Exception is not UnlessUsingApiException)
|
||||
{
|
||||
throw result.Exception;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Change return type to FileWriteResult.
|
||||
public void SaveSlot(int slotNum, bool suppressOSD)
|
||||
{
|
||||
if (slotNum is < 0 or > 10) throw new ArgumentOutOfRangeException(paramName: nameof(slotNum), message: ERR_MSG_NOT_A_SLOT);
|
||||
|
@ -49,7 +58,11 @@ namespace BizHawk.Client.Common
|
|||
LogCallback(ERR_MSG_USE_SLOT_10);
|
||||
slotNum = 10;
|
||||
}
|
||||
_mainForm.SaveQuickSave(slotNum, suppressOSD: suppressOSD, fromLua: true);
|
||||
FileWriteResult result = _mainForm.SaveQuickSave(slotNum, suppressOSD: suppressOSD);
|
||||
if (result.Exception != null && result.Exception is not UnlessUsingApiException)
|
||||
{
|
||||
throw result.Exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
#nullable enable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace BizHawk.Client.Common
|
||||
{
|
||||
public enum TryAgainResult
|
||||
{
|
||||
Saved,
|
||||
IgnoredFailure,
|
||||
Canceled,
|
||||
}
|
||||
|
||||
public static class DialogControllerExtensions
|
||||
{
|
||||
public static void AddOnScreenMessage(this IDialogParent dialogParent, string message, int? duration = null)
|
||||
|
@ -60,6 +68,47 @@ namespace BizHawk.Client.Common
|
|||
EMsgBoxIcon? icon = null)
|
||||
=> dialogParent.DialogController.ShowMessageBox3(owner: dialogParent, text: text, caption: caption, icon: icon);
|
||||
|
||||
public static void ErrorMessageBox(
|
||||
this IDialogParent dialogParent,
|
||||
FileWriteResult fileResult,
|
||||
string? prefixMessage = null)
|
||||
{
|
||||
Debug.Assert(fileResult.IsError && fileResult.Exception != null, "Error box must have an error.");
|
||||
|
||||
string prefix = prefixMessage ?? "";
|
||||
dialogParent.ModalMessageBox(
|
||||
text: $"{prefix}\n{fileResult.UserFriendlyErrorMessage()}\n{fileResult.Exception!.Message}",
|
||||
caption: "Error",
|
||||
icon: EMsgBoxIcon.Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the action fails, asks the user if they want to try again.
|
||||
/// The user will be repeatedly asked if they want to try again until either success or the user says no.
|
||||
/// </summary>
|
||||
/// <returns>Returns true on success or if the user said no. Returns false if the user said cancel.</returns>
|
||||
public static TryAgainResult DoWithTryAgainBox(
|
||||
this IDialogParent dialogParent,
|
||||
Func<FileWriteResult> action,
|
||||
string message)
|
||||
{
|
||||
FileWriteResult fileResult = action();
|
||||
while (fileResult.IsError)
|
||||
{
|
||||
string prefix = message ?? "";
|
||||
bool? askResult = dialogParent.ModalMessageBox3(
|
||||
text: $"{prefix} Do you want to try again?\n\nError details:" +
|
||||
$"{fileResult.UserFriendlyErrorMessage()}\n{fileResult.Exception!.Message}",
|
||||
caption: "Error",
|
||||
icon: EMsgBoxIcon.Error);
|
||||
if (askResult == null) return TryAgainResult.Canceled;
|
||||
if (askResult == false) return TryAgainResult.IgnoredFailure;
|
||||
if (askResult == true) fileResult = action();
|
||||
}
|
||||
|
||||
return TryAgainResult.Saved;
|
||||
}
|
||||
|
||||
/// <summary>Creates and shows a <c>System.Windows.Forms.OpenFileDialog</c> or equivalent with the receiver (<paramref name="dialogParent"/>) as its parent</summary>
|
||||
/// <param name="discardCWDChange"><c>OpenFileDialog.RestoreDirectory</c> (isn't this useless when specifying <paramref name="initDir"/>? keeping it for backcompat)</param>
|
||||
/// <param name="filter"><c>OpenFileDialog.Filter</c></param>
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
#nullable enable
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace BizHawk.Client.Common
|
||||
{
|
||||
public enum FileWriteEnum
|
||||
{
|
||||
Success,
|
||||
// Failures during a FileWriter write.
|
||||
FailedToOpen,
|
||||
FailedDuringWrite,
|
||||
FailedToDeleteOldBackup,
|
||||
FailedToMakeBackup,
|
||||
FailedToDeleteOldFile,
|
||||
FailedToRename,
|
||||
Aborted,
|
||||
// Failures from other sources
|
||||
FailedToDeleteGeneric,
|
||||
FailedToMoveForSwap,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides information about the success or failure of an attempt to write to a file.
|
||||
/// </summary>
|
||||
public class FileWriteResult
|
||||
{
|
||||
public readonly FileWriteEnum Error = FileWriteEnum.Success;
|
||||
public readonly Exception? Exception;
|
||||
internal readonly FileWritePaths Paths;
|
||||
|
||||
public bool IsError => Error != FileWriteEnum.Success;
|
||||
|
||||
public FileWriteResult(FileWriteEnum error, FileWritePaths writer, Exception? exception)
|
||||
{
|
||||
Error = error;
|
||||
Exception = exception;
|
||||
Paths = writer;
|
||||
}
|
||||
|
||||
public FileWriteResult() : this(FileWriteEnum.Success, new("", ""), null) { }
|
||||
|
||||
/// <summary>
|
||||
/// Converts this instance to a different generic type.
|
||||
/// The new instance will take the value given only if this instance has no error.
|
||||
/// </summary>
|
||||
/// <param name="value">The value of the new instance. Ignored if this instance has an error.</param>
|
||||
public FileWriteResult<T> Convert<T>(T value) where T : class
|
||||
{
|
||||
if (Error == FileWriteEnum.Success) return new(value, Paths);
|
||||
else return new(this);
|
||||
}
|
||||
|
||||
public FileWriteResult(FileWriteResult other) : this(other.Error, other.Paths, other.Exception) { }
|
||||
|
||||
public string UserFriendlyErrorMessage()
|
||||
{
|
||||
Debug.Assert(!IsError || (Exception != null), "FileWriteResult with an error should have an exception.");
|
||||
|
||||
switch (Error)
|
||||
{
|
||||
// We include the full path since the user may not have explicitly given a directory and may not know what it is.
|
||||
case FileWriteEnum.Success:
|
||||
return $"The file \"{Paths.Final}\" was written successfully.";
|
||||
case FileWriteEnum.FailedToOpen:
|
||||
if (Paths.Final != Paths.Temp)
|
||||
{
|
||||
return $"The temporary file \"{Paths.Temp}\" could not be opened.";
|
||||
}
|
||||
return $"The file \"{Paths.Final}\" could not be created.";
|
||||
case FileWriteEnum.FailedDuringWrite:
|
||||
return $"An error occurred while writing the file."; // No file name here; it should be deleted.
|
||||
case FileWriteEnum.Aborted:
|
||||
return "The operation was aborted.";
|
||||
|
||||
case FileWriteEnum.FailedToDeleteGeneric:
|
||||
return $"The file \"{Paths.Final}\" could not be deleted.";
|
||||
//case FileWriteEnum.FailedToDeleteForSwap:
|
||||
// return $"Failed to swap files. Unable to write to \"{Paths.Final}\"";
|
||||
case FileWriteEnum.FailedToMoveForSwap:
|
||||
return $"Failed to swap files. Unable to rename \"{Paths.Temp}\" to \"{Paths.Final}\"";
|
||||
}
|
||||
|
||||
string success = $"The file was created successfully at \"{Paths.Temp}\" but could not be moved";
|
||||
switch (Error)
|
||||
{
|
||||
case FileWriteEnum.FailedToDeleteOldBackup:
|
||||
return $"{success}. Unable to remove old backup file \"{Paths.Backup}\".";
|
||||
case FileWriteEnum.FailedToMakeBackup:
|
||||
return $"{success}. Unable to create backup. Failed to move \"{Paths.Final}\" to \"{Paths.Backup}\".";
|
||||
case FileWriteEnum.FailedToDeleteOldFile:
|
||||
return $"{success}. Unable to remove the old file \"{Paths.Final}\".";
|
||||
case FileWriteEnum.FailedToRename:
|
||||
return $"{success} to \"{Paths.Final}\".";
|
||||
default:
|
||||
return "unreachable";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides information about the success or failure of an attempt to write to a file.
|
||||
/// If successful, also provides a related object instance.
|
||||
/// </summary>
|
||||
public class FileWriteResult<T> : FileWriteResult where T : class // Note: "class" also means "notnull".
|
||||
{
|
||||
/// <summary>
|
||||
/// Value will be null if <see cref="FileWriteResult.IsError"/> is true.
|
||||
/// Otherwise, Value will not be null.
|
||||
/// </summary>
|
||||
public readonly T? Value = default;
|
||||
|
||||
internal FileWriteResult(FileWriteEnum error, FileWritePaths paths, Exception? exception) : base(error, paths, exception) { }
|
||||
|
||||
internal FileWriteResult(T value, FileWritePaths paths) : base(FileWriteEnum.Success, paths, null)
|
||||
{
|
||||
Debug.Assert(value != null, "Should not give a null value on success. Use the non-generic type if there is no value.");
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public FileWriteResult(FileWriteResult other) : base(other.Error, other.Paths, other.Exception) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This only exists as a way to avoid changing the API behavior.
|
||||
/// </summary>
|
||||
public class UnlessUsingApiException : Exception
|
||||
{
|
||||
public UnlessUsingApiException(string message) : base(message) { }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
#nullable enable
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
using BizHawk.Common.StringExtensions;
|
||||
|
||||
namespace BizHawk.Client.Common
|
||||
{
|
||||
public class FileWritePaths(string final, string temp)
|
||||
{
|
||||
public readonly string Final = final;
|
||||
public readonly string Temp = temp;
|
||||
public string? Backup;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a mechanism for safely overwriting files, by using a temporary file that only replaces the original after writing has been completed.
|
||||
/// Optionally makes a backup of the original file.
|
||||
/// </summary>
|
||||
public class FileWriter : IDisposable
|
||||
{
|
||||
|
||||
private FileStream? _stream; // is never null until this.Dispose()
|
||||
public FileStream Stream
|
||||
{
|
||||
get => _stream ?? throw new ObjectDisposedException("Cannot access a disposed FileStream.");
|
||||
}
|
||||
public FileWritePaths Paths;
|
||||
|
||||
public bool UsingTempFile => Paths.Temp != Paths.Final;
|
||||
|
||||
private bool _finished = false;
|
||||
|
||||
private FileWriter(FileWritePaths paths, FileStream stream)
|
||||
{
|
||||
Paths = paths;
|
||||
_stream = stream;
|
||||
}
|
||||
|
||||
public static FileWriteResult Write(string path, byte[] bytes, string? backupPath = null)
|
||||
{
|
||||
FileWriteResult<FileWriter> createResult = Create(path);
|
||||
if (createResult.IsError) return createResult;
|
||||
|
||||
try
|
||||
{
|
||||
createResult.Value!.Stream.Write(bytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new(FileWriteEnum.FailedDuringWrite, createResult.Value!.Paths, ex);
|
||||
}
|
||||
|
||||
return createResult.Value.CloseAndDispose(backupPath);
|
||||
}
|
||||
|
||||
public static FileWriteResult Write(string path, Action<Stream> writeCallback, string? backupPath = null)
|
||||
{
|
||||
FileWriteResult<FileWriter> createResult = Create(path);
|
||||
if (createResult.IsError) return createResult;
|
||||
|
||||
try
|
||||
{
|
||||
writeCallback(createResult.Value!.Stream);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new(FileWriteEnum.FailedDuringWrite, createResult.Value!.Paths, ex);
|
||||
}
|
||||
|
||||
return createResult.Value.CloseAndDispose(backupPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a FileWriter instance, or return an error if unable to access the file.
|
||||
/// </summary>
|
||||
public static FileWriteResult<FileWriter> Create(string path)
|
||||
{
|
||||
string writePath = path;
|
||||
// If the file already exists, we will write to a temporary location first and preserve the old one until we're done.
|
||||
if (File.Exists(path))
|
||||
{
|
||||
writePath = path.InsertBeforeLast('.', ".saving", out bool inserted);
|
||||
if (!inserted) writePath = $"{path}.saving";
|
||||
|
||||
// The file might already exist, if a prior file write failed.
|
||||
// Maybe the user should have dealt with this on the previously failed save.
|
||||
// But we want to support plain old "try again", so let's ignore that.
|
||||
}
|
||||
FileWritePaths paths = new(path, writePath);
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
FileStream fs = new(writePath, FileMode.Create, FileAccess.Write);
|
||||
return new(new FileWriter(paths, fs), paths);
|
||||
}
|
||||
catch (Exception ex) // There are many exception types that file operations might raise.
|
||||
{
|
||||
return new(FileWriteEnum.FailedToOpen, paths, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method must be called after writing has finished and must not be called twice.
|
||||
/// Dispose will be called regardless of the result.
|
||||
/// </summary>
|
||||
/// <param name="backupPath">If not null, renames the original file to this path.</param>
|
||||
/// <exception cref="InvalidOperationException">If called twice.</exception>
|
||||
public FileWriteResult CloseAndDispose(string? backupPath = null)
|
||||
{
|
||||
// In theory it might make sense to allow the user to try again if we fail inside this method.
|
||||
// If we implement that, it is probably best to make a static method that takes a FileWriteResult.
|
||||
// So even then, this method should not ever be called twice.
|
||||
if (_finished) throw new InvalidOperationException("Cannot close twice.");
|
||||
|
||||
_finished = true;
|
||||
Dispose();
|
||||
|
||||
Paths.Backup = backupPath;
|
||||
if (!UsingTempFile)
|
||||
{
|
||||
// The chosen file did not already exist, so there is nothing to back up and nothing to rename.
|
||||
return new(FileWriteEnum.Success, Paths, null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// When everything goes right, this is all we need.
|
||||
File.Replace(Paths.Temp, Paths.Final, backupPath);
|
||||
return new(FileWriteEnum.Success, Paths, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// When things go wrong, we have to do a lot of work in order to
|
||||
// figure out what went wrong and tell the user.
|
||||
return FindTheError();
|
||||
}
|
||||
}
|
||||
|
||||
private FileWriteResult FindTheError()
|
||||
{
|
||||
// It is an unfortunate reality that .NET provides horrible exception messages
|
||||
// when using File.Replace(source, destination, backup). They are not only
|
||||
// unhelpful by not telling which file operation failed, but can also be a lie.
|
||||
// File.Move isn't great either.
|
||||
// So, we will split this into multiple parts and subparts.
|
||||
|
||||
// 1) Handle backup file, if necessary
|
||||
// a) Delete the old backup, if it exists. We check existence here to avoid DirectoryNotFound errors.
|
||||
// If this fails, return that failure.
|
||||
// If it succeeded but the file somehow still exists, report that error.
|
||||
// b) Ensure the target directory exists.
|
||||
// Rename the original file, and similarly report any errors.
|
||||
// 2) Handle renaming of temp file, the same way renaming of original for backup was done.
|
||||
|
||||
if (Paths.Backup != null)
|
||||
{
|
||||
try { DeleteIfExists(Paths.Backup); }
|
||||
catch (Exception ex) { return new(FileWriteEnum.FailedToDeleteOldBackup, Paths, ex); }
|
||||
if (!TryWaitForFileToVanish(Paths.Backup)) return new(FileWriteEnum.FailedToDeleteOldBackup, Paths, new Exception("The file was supposedly deleted but is still there."));
|
||||
|
||||
try { MoveFile(Paths.Final, Paths.Backup); }
|
||||
catch (Exception ex) { return new(FileWriteEnum.FailedToMakeBackup, Paths, ex); }
|
||||
if (!TryWaitForFileToVanish(Paths.Final)) return new(FileWriteEnum.FailedToMakeBackup, Paths, new Exception("The file was supposedly moved but is still in the orignal location."));
|
||||
}
|
||||
|
||||
try { DeleteIfExists(Paths.Final); }
|
||||
catch (Exception ex) { return new(FileWriteEnum.FailedToDeleteOldFile, Paths, ex); }
|
||||
if (!TryWaitForFileToVanish(Paths.Final)) return new(FileWriteEnum.FailedToDeleteOldFile, Paths, new Exception("The file was supposedly deleted but is still there."));
|
||||
|
||||
try { MoveFile(Paths.Temp, Paths.Final); }
|
||||
catch (Exception ex) { return new(FileWriteEnum.FailedToRename, Paths, ex); }
|
||||
if (!TryWaitForFileToVanish(Paths.Temp)) return new(FileWriteEnum.FailedToRename, Paths, new Exception("The file was supposedly moved but is still in the orignal location."));
|
||||
|
||||
return new(FileWriteEnum.Success, Paths, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes and deletes the file. Use if there was an error while writing.
|
||||
/// Do not call <see cref="CloseAndDispose"/> after this.
|
||||
/// </summary>
|
||||
public void Abort()
|
||||
{
|
||||
if (_dispoed) throw new ObjectDisposedException("Cannot use a disposed file stream.");
|
||||
_finished = true;
|
||||
Dispose();
|
||||
|
||||
try
|
||||
{
|
||||
// Delete because the file is almost certainly useless and just clutter.
|
||||
File.Delete(Paths.Temp);
|
||||
}
|
||||
catch { /* eat? this is probably not very important */ }
|
||||
}
|
||||
|
||||
private bool _dispoed;
|
||||
public void Dispose()
|
||||
{
|
||||
if (_dispoed) return;
|
||||
_dispoed = true;
|
||||
|
||||
_stream!.Dispose();
|
||||
_stream = null;
|
||||
|
||||
// The caller should call CloseAndDispose and handle potential failure.
|
||||
Debug.Assert(_finished, $"{nameof(FileWriteResult)} should not be disposed before calling {nameof(CloseAndDispose)}");
|
||||
}
|
||||
|
||||
|
||||
private static void DeleteIfExists(string path)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static void MoveFile(string source, string destination)
|
||||
{
|
||||
FileInfo file = new(destination);
|
||||
file.Directory.Create();
|
||||
File.Move(source, destination);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supposedly it is possible for File.Delete to return before the file has actually been deleted.
|
||||
/// And File.Move too, I guess.
|
||||
/// </summary>
|
||||
private static bool TryWaitForFileToVanish(string path)
|
||||
{
|
||||
for (var i = 25; i != 0; i--)
|
||||
{
|
||||
if (!File.Exists(path)) return true;
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
#nullable enable
|
||||
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
|
||||
|
@ -7,18 +9,19 @@ namespace BizHawk.Client.Common
|
|||
{
|
||||
public class FrameworkZipWriter : IZipWriter
|
||||
{
|
||||
private ZipArchive _archive;
|
||||
private ZipArchive? _archive;
|
||||
|
||||
private FileStream _fs;
|
||||
private FileWriter? _fs;
|
||||
|
||||
private Zstd _zstd;
|
||||
private Zstd? _zstd;
|
||||
private readonly CompressionLevel _level;
|
||||
private readonly int _zstdCompressionLevel;
|
||||
|
||||
public FrameworkZipWriter(string path, int compressionLevel)
|
||||
private Exception? _writeException = null;
|
||||
private bool _disposed;
|
||||
|
||||
private FrameworkZipWriter(int compressionLevel)
|
||||
{
|
||||
_fs = new(path, FileMode.Create, FileAccess.Write);
|
||||
_archive = new(_fs, ZipArchiveMode.Create, leaveOpen: true);
|
||||
if (compressionLevel == 0)
|
||||
_level = CompressionLevel.NoCompression;
|
||||
else if (compressionLevel < 5)
|
||||
|
@ -32,38 +35,95 @@ namespace BizHawk.Client.Common
|
|||
_zstdCompressionLevel = compressionLevel * 2 + 1;
|
||||
}
|
||||
|
||||
public void WriteItem(string name, Action<Stream> callback, bool zstdCompress)
|
||||
public static FileWriteResult<FrameworkZipWriter> Create(string path, int compressionLevel)
|
||||
{
|
||||
// don't compress with deflate if we're already compressing with zstd
|
||||
// this won't produce meaningful compression, and would just be a timesink
|
||||
using var stream = _archive.CreateEntry(name, zstdCompress ? CompressionLevel.NoCompression : _level).Open();
|
||||
FileWriteResult<FileWriter> fs = FileWriter.Create(path);
|
||||
if (fs.IsError) return new(fs);
|
||||
|
||||
if (zstdCompress)
|
||||
FrameworkZipWriter ret = new(compressionLevel);
|
||||
ret._fs = fs.Value!;
|
||||
ret._archive = new(ret._fs.Stream, ZipArchiveMode.Create, leaveOpen: true);
|
||||
|
||||
return fs.Convert(ret);
|
||||
}
|
||||
|
||||
public FileWriteResult CloseAndDispose(string? backupPath = null)
|
||||
{
|
||||
if (_archive == null || _fs == null) throw new ObjectDisposedException("Cannot use disposed ZipWriter.");
|
||||
|
||||
// We actually have to do this here since it has to be done before the file stream is closed.
|
||||
_archive.Dispose();
|
||||
_archive = null;
|
||||
|
||||
FileWriteResult ret;
|
||||
if (_writeException == null)
|
||||
{
|
||||
using var z = _zstd.CreateZstdCompressionStream(stream, _zstdCompressionLevel);
|
||||
callback(z);
|
||||
ret = _fs.CloseAndDispose(backupPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
callback(stream);
|
||||
ret = new(FileWriteEnum.FailedDuringWrite, _fs.Paths, _writeException);
|
||||
_fs.Abort();
|
||||
}
|
||||
|
||||
// And since we have to close stuff, there's really no point in not disposing here.
|
||||
Dispose();
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void Abort()
|
||||
{
|
||||
if (_archive == null || _fs == null) throw new ObjectDisposedException("Cannot use disposed ZipWriter.");
|
||||
|
||||
_archive.Dispose();
|
||||
_archive = null;
|
||||
|
||||
_fs.Abort();
|
||||
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public void WriteItem(string name, Action<Stream> callback, bool zstdCompress)
|
||||
{
|
||||
if (_archive == null || _zstd == null) throw new ObjectDisposedException("Cannot use disposed ZipWriter.");
|
||||
if (_writeException != null) return;
|
||||
|
||||
try
|
||||
{
|
||||
// don't compress with deflate if we're already compressing with zstd
|
||||
// this won't produce meaningful compression, and would just be a timesink
|
||||
using var stream = _archive.CreateEntry(name, zstdCompress ? CompressionLevel.NoCompression : _level).Open();
|
||||
|
||||
if (zstdCompress)
|
||||
{
|
||||
using var z = _zstd.CreateZstdCompressionStream(stream, _zstdCompressionLevel);
|
||||
callback(z);
|
||||
}
|
||||
else
|
||||
{
|
||||
callback(stream);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_writeException = ex;
|
||||
// We aren't returning the failure until closing. Should we? I don't want to refactor that much calling code without a good reason.
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_archive != null)
|
||||
{
|
||||
_archive.Dispose();
|
||||
_archive = null;
|
||||
}
|
||||
_fs?.Flush(flushToDisk: true);
|
||||
_fs?.Dispose();
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
// _archive should already be disposed by CloseAndDispose, but just in case
|
||||
_archive?.Dispose();
|
||||
_archive = null;
|
||||
_zstd!.Dispose();
|
||||
_zstd = null;
|
||||
|
||||
_fs!.Dispose();
|
||||
_fs = null;
|
||||
if (_zstd != null)
|
||||
{
|
||||
_zstd.Dispose();
|
||||
_zstd = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ namespace BizHawk.Client.Common
|
|||
void EnableRewind(bool enabled);
|
||||
|
||||
/// <remarks>only referenced from <c>EmuClientApi</c></remarks>
|
||||
bool FlushSaveRAM(bool autosave = false);
|
||||
FileWriteResult FlushSaveRAM(bool autosave = false);
|
||||
|
||||
/// <remarks>only referenced from <c>EmuClientApi</c></remarks>
|
||||
void FrameAdvance(bool discardApiHawkSurfaces = true);
|
||||
|
@ -89,10 +89,16 @@ namespace BizHawk.Client.Common
|
|||
/// <remarks>only referenced from <see cref="MovieApi"/></remarks>
|
||||
bool RestartMovie();
|
||||
|
||||
/// <remarks>only referenced from <see cref="SaveStateApi"/></remarks>
|
||||
void SaveQuickSave(int slot, bool suppressOSD = false, bool fromLua = false);
|
||||
FileWriteResult SaveQuickSave(int slot, bool suppressOSD = false);
|
||||
|
||||
void SaveState(string path, string userFriendlyStateName, bool fromLua = false, bool suppressOSD = false);
|
||||
/// <summary>
|
||||
/// Creates a savestate and writes it to a file.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the file to write.</param>
|
||||
/// <param name="userFriendlyStateName">The name to use for the state on the client's HUD.</param>
|
||||
/// <param name="suppressOSD">If true, the client HUD will not show a success message.</param>
|
||||
/// <returns>Returns a value indicating if there was an error and (if there was) why.</returns>
|
||||
FileWriteResult SaveState(string path, string userFriendlyStateName, bool suppressOSD = false);
|
||||
|
||||
void SeekFrameAdvance();
|
||||
|
||||
|
|
|
@ -5,5 +5,18 @@ namespace BizHawk.Client.Common
|
|||
public interface IZipWriter : IDisposable
|
||||
{
|
||||
void WriteItem(string name, Action<Stream> callback, bool zstdCompress);
|
||||
|
||||
/// <summary>
|
||||
/// This method must be called after writing has finished and must not be called twice.
|
||||
/// Dispose will be called regardless of the result.
|
||||
/// </summary>
|
||||
/// <param name="backupPath">If not null, renames the original file to this path.</param>
|
||||
FileWriteResult CloseAndDispose(string backupPath = null);
|
||||
|
||||
/// <summary>
|
||||
/// Closes and deletes the file. Use if there was an error while writing.
|
||||
/// Do not call <see cref="CloseAndDispose"/> after this.
|
||||
/// </summary>
|
||||
void Abort();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,31 +69,65 @@ namespace BizHawk.Client.Common
|
|||
public bool IsRedo(IMovie movie, int slot)
|
||||
=> slot is >= 1 and <= 10 && movie is not ITasMovie && _redo[slot - 1];
|
||||
|
||||
public void SwapBackupSavestate(IMovie movie, string path, int currentSlot)
|
||||
/// <summary>
|
||||
/// Takes the .state and .bak files and swaps them
|
||||
/// </summary>
|
||||
public FileWriteResult SwapBackupSavestate(IMovie movie, string path, int currentSlot)
|
||||
{
|
||||
// Takes the .state and .bak files and swaps them
|
||||
string backupPath = $"{path}.bak";
|
||||
string tempPath = $"{path}.bak.tmp";
|
||||
|
||||
var state = new FileInfo(path);
|
||||
var backup = new FileInfo($"{path}.bak");
|
||||
var temp = new FileInfo($"{path}.bak.tmp");
|
||||
var backup = new FileInfo(backupPath);
|
||||
|
||||
if (!state.Exists || !backup.Exists)
|
||||
{
|
||||
return;
|
||||
return new();
|
||||
}
|
||||
|
||||
if (temp.Exists)
|
||||
// Delete old temp file if it exists.
|
||||
try
|
||||
{
|
||||
temp.Delete();
|
||||
if (File.Exists(tempPath)) File.Delete(tempPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new(FileWriteEnum.FailedToDeleteGeneric, new(tempPath, ""), ex);
|
||||
}
|
||||
|
||||
backup.CopyTo($"{path}.bak.tmp");
|
||||
backup.Delete();
|
||||
state.CopyTo($"{path}.bak");
|
||||
state.Delete();
|
||||
temp.CopyTo(path);
|
||||
temp.Delete();
|
||||
// Move backup to temp.
|
||||
try
|
||||
{
|
||||
backup.MoveTo(tempPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new(FileWriteEnum.FailedToMoveForSwap, new(tempPath, backupPath), ex);
|
||||
}
|
||||
// Move current to backup.
|
||||
try
|
||||
{
|
||||
state.MoveTo(backupPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Attempt to restore the backup
|
||||
try { backup.MoveTo(backupPath); } catch { /* eat? unlikely to fail here */ }
|
||||
return new(FileWriteEnum.FailedToMoveForSwap, new(backupPath, path), ex);
|
||||
}
|
||||
// Move backup to current.
|
||||
try
|
||||
{
|
||||
backup.MoveTo(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Should we attempt to restore? Unlikely to fail here since we've already touched all files.
|
||||
return new(FileWriteEnum.FailedToMoveForSwap, new(path, tempPath), ex);
|
||||
}
|
||||
|
||||
ToggleRedo(movie, currentSlot);
|
||||
return new();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,19 +104,14 @@ namespace BizHawk.Client.Common
|
|||
return config ?? new T();
|
||||
}
|
||||
|
||||
public static void Save(string filepath, object config)
|
||||
public static FileWriteResult Save(string filepath, object config)
|
||||
{
|
||||
var file = new FileInfo(filepath);
|
||||
try
|
||||
return FileWriter.Write(filepath, (fs) =>
|
||||
{
|
||||
using var writer = file.CreateText();
|
||||
using var writer = new StreamWriter(fs);
|
||||
var w = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
|
||||
Serializer.Serialize(w, config);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* Eat it */
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// movie 1.0 header stuff
|
||||
|
|
|
@ -102,9 +102,8 @@ namespace BizHawk.Client.Common
|
|||
return true;
|
||||
}
|
||||
|
||||
public void Save(string path)
|
||||
public FileWriteResult Save(string path)
|
||||
{
|
||||
using var sw = new StreamWriter(path);
|
||||
var sb = new StringBuilder();
|
||||
var saveDirectory = Path.GetDirectoryName(Path.GetFullPath(path));
|
||||
foreach (var file in this)
|
||||
|
@ -123,10 +122,19 @@ namespace BizHawk.Client.Common
|
|||
}
|
||||
}
|
||||
|
||||
sw.Write(sb.ToString());
|
||||
FileWriteResult result = FileWriter.Write(path, (fs) =>
|
||||
{
|
||||
using var sw = new StreamWriter(fs);
|
||||
sw.Write(sb.ToString());
|
||||
});
|
||||
|
||||
Filename = path;
|
||||
Changes = false;
|
||||
if (!result.IsError)
|
||||
{
|
||||
Filename = path;
|
||||
Changes = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ namespace BizHawk.Client.Common
|
|||
return bk2;
|
||||
}
|
||||
|
||||
public static ITasMovie ConvertToSavestateAnchoredMovie(this ITasMovie old, int frame, byte[] savestate)
|
||||
public static FileWriteResult<ITasMovie> ConvertToSavestateAnchoredMovie(this ITasMovie old, int frame, byte[] savestate)
|
||||
{
|
||||
string newFilename = ConvertFileNameToTasMovie(old.Filename);
|
||||
|
||||
|
@ -115,11 +115,11 @@ namespace BizHawk.Client.Common
|
|||
}
|
||||
}
|
||||
|
||||
tas.Save();
|
||||
return tas;
|
||||
FileWriteResult saveResult = tas.Save();
|
||||
return saveResult.Convert(tas);
|
||||
}
|
||||
|
||||
public static ITasMovie ConvertToSaveRamAnchoredMovie(this ITasMovie old, byte[] saveRam)
|
||||
public static FileWriteResult<ITasMovie> ConvertToSaveRamAnchoredMovie(this ITasMovie old, byte[] saveRam)
|
||||
{
|
||||
string newFilename = ConvertFileNameToTasMovie(old.Filename);
|
||||
|
||||
|
@ -133,7 +133,6 @@ namespace BizHawk.Client.Common
|
|||
|
||||
foreach (var (k, v) in old.HeaderEntries) tas.HeaderEntries[k] = v;
|
||||
|
||||
tas.StartsFromSaveRam = true;
|
||||
tas.SyncSettingsJson = old.SyncSettingsJson;
|
||||
|
||||
foreach (string comment in old.Comments)
|
||||
|
@ -146,8 +145,8 @@ namespace BizHawk.Client.Common
|
|||
tas.Subtitles.Add(sub);
|
||||
}
|
||||
|
||||
tas.Save();
|
||||
return tas;
|
||||
FileWriteResult saveResult = tas.Save();
|
||||
return saveResult.Convert(tas);
|
||||
}
|
||||
|
||||
#pragma warning disable RCS1224 // private but for unit test
|
||||
|
|
|
@ -244,8 +244,9 @@ namespace BizHawk.Client.Common
|
|||
public void AbortQueuedMovie()
|
||||
=> _queuedMovie = null;
|
||||
|
||||
public void StopMovie(bool saveChanges = true)
|
||||
public FileWriteResult StopMovie(bool saveChanges = true)
|
||||
{
|
||||
FileWriteResult/*?*/ result = null;
|
||||
if (Movie.IsActive())
|
||||
{
|
||||
var message = "Movie ";
|
||||
|
@ -262,8 +263,17 @@ namespace BizHawk.Client.Common
|
|||
|
||||
if (saveChanges && Movie.Changes)
|
||||
{
|
||||
Movie.Save();
|
||||
Output($"{Path.GetFileName(Movie.Filename)} written to disk.");
|
||||
result = Movie.Save();
|
||||
if (result.IsError)
|
||||
{
|
||||
Output($"Failed to write {Path.GetFileName(Movie.Filename)} to disk.");
|
||||
Output(result.UserFriendlyErrorMessage());
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
Output($"{Path.GetFileName(Movie.Filename)} written to disk.");
|
||||
}
|
||||
}
|
||||
Movie.Stop();
|
||||
|
||||
|
@ -279,6 +289,8 @@ namespace BizHawk.Client.Common
|
|||
}
|
||||
|
||||
Movie = null;
|
||||
|
||||
return result ?? new();
|
||||
}
|
||||
|
||||
public IMovie Get(string path, bool loadMovie)
|
||||
|
@ -373,6 +385,8 @@ namespace BizHawk.Client.Common
|
|||
switch (Settings.MovieEndAction)
|
||||
{
|
||||
case MovieEndAction.Stop:
|
||||
// Technically this can save the movie, but it'd be weird to be in that situation.
|
||||
// Do we want that?
|
||||
StopMovie();
|
||||
break;
|
||||
case MovieEndAction.Record:
|
||||
|
|
|
@ -49,26 +49,7 @@ namespace BizHawk.Client.Common
|
|||
}
|
||||
}
|
||||
|
||||
public bool StartsFromSaveRam
|
||||
{
|
||||
// ReSharper disable SimplifyConditionalTernaryExpression
|
||||
get => Header.TryGetValue(HeaderKeys.StartsFromSaveram, out var s) ? bool.Parse(s) : false;
|
||||
// ReSharper restore SimplifyConditionalTernaryExpression
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
if (!Header.ContainsKey(HeaderKeys.StartsFromSaveram))
|
||||
{
|
||||
Header.Add(HeaderKeys.StartsFromSaveram, "True");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Header.Remove(HeaderKeys.StartsFromSaveram);
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool StartsFromSaveRam => SaveRam != null;
|
||||
|
||||
public override string GameName
|
||||
{
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable enable
|
||||
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
|
@ -10,25 +12,25 @@ namespace BizHawk.Client.Common
|
|||
{
|
||||
public partial class Bk2Movie
|
||||
{
|
||||
public void Save()
|
||||
public FileWriteResult Save()
|
||||
{
|
||||
Write(Filename);
|
||||
return Write(Filename);
|
||||
}
|
||||
|
||||
public void SaveBackup()
|
||||
public FileWriteResult SaveBackup()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Filename))
|
||||
{
|
||||
return;
|
||||
return new();
|
||||
}
|
||||
|
||||
var backupName = Filename.InsertBeforeLast('.', insert: $".{DateTime.Now:yyyy-MM-dd HH.mm.ss}", out _);
|
||||
string backupName = Filename.InsertBeforeLast('.', insert: $".{DateTime.Now:yyyy-MM-dd HH.mm.ss}", out _);
|
||||
backupName = Path.Combine(Session.BackupDirectory, Path.GetFileName(backupName));
|
||||
|
||||
Write(backupName, isBackup: true);
|
||||
return Write(backupName, isBackup: true);
|
||||
}
|
||||
|
||||
protected virtual void Write(string fn, bool isBackup = false)
|
||||
protected virtual FileWriteResult Write(string fn, bool isBackup = false)
|
||||
{
|
||||
SetCycleValues();
|
||||
// EmulatorVersion used to store the unchanging original emulator version.
|
||||
|
@ -39,13 +41,27 @@ namespace BizHawk.Client.Common
|
|||
Header[HeaderKeys.EmulatorVersion] = VersionInfo.GetEmuVersion();
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fn)!);
|
||||
|
||||
using var bs = new ZipStateSaver(fn, Session.Settings.MovieCompressionLevel);
|
||||
AddLumps(bs, isBackup);
|
||||
var createResult = ZipStateSaver.Create(fn, Session.Settings.MovieCompressionLevel);
|
||||
if (createResult.IsError) return createResult;
|
||||
|
||||
if (!isBackup)
|
||||
ZipStateSaver saver = createResult.Value!;
|
||||
try
|
||||
{
|
||||
AddLumps(saver, isBackup);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
saver.Abort();
|
||||
return new(FileWriteEnum.FailedDuringWrite, createResult.Paths, ex);
|
||||
}
|
||||
|
||||
FileWriteResult result = saver.CloseAndDispose();
|
||||
if (!isBackup && !result.IsError)
|
||||
{
|
||||
Changes = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void SetCycleValues() //TODO IEmulator should not be an instance prop of movies, it should be passed in to every call (i.e. from MovieService) --yoshi
|
||||
|
@ -129,7 +145,7 @@ namespace BizHawk.Client.Common
|
|||
|
||||
bl.GetLump(BinaryStateLump.SyncSettings, abort: false, tr =>
|
||||
{
|
||||
string line;
|
||||
string? line;
|
||||
while ((line = tr.ReadLine()) != null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(line))
|
||||
|
|
|
@ -67,7 +67,11 @@ namespace BizHawk.Client.Common
|
|||
Result.Movie.Hash = hash;
|
||||
}
|
||||
|
||||
Result.Movie.Save();
|
||||
if (Result.Movie.Save().IsError)
|
||||
{
|
||||
Result.Errors.Add($"Could not write the file {newFileName}");
|
||||
return Result;
|
||||
}
|
||||
}
|
||||
|
||||
return Result;
|
||||
|
|
|
@ -65,19 +65,19 @@ namespace BizHawk.Client.Common
|
|||
byte[] SaveRam { get; set; }
|
||||
|
||||
bool StartsFromSavestate { get; set; }
|
||||
bool StartsFromSaveRam { get; set; }
|
||||
bool StartsFromSaveRam { get; }
|
||||
|
||||
string LogKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Forces the creation of a backup file of the current movie state
|
||||
/// </summary>
|
||||
void SaveBackup();
|
||||
FileWriteResult SaveBackup();
|
||||
|
||||
/// <summary>
|
||||
/// Instructs the movie to save the current contents to Filename
|
||||
/// </summary>
|
||||
void Save();
|
||||
FileWriteResult Save();
|
||||
|
||||
/// <summary>updates the <see cref="HeaderKeys.CycleCount"/> and <see cref="HeaderKeys.ClockRate"/> headers from the currently loaded core</summary>
|
||||
void SetCycleValues();
|
||||
|
|
|
@ -83,7 +83,7 @@ namespace BizHawk.Client.Common
|
|||
/// <summary>clears the queued movie</summary>
|
||||
void AbortQueuedMovie();
|
||||
|
||||
void StopMovie(bool saveChanges = true);
|
||||
FileWriteResult StopMovie(bool saveChanges = true);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new (Tas)Movie with the given path as filename. If <paramref name="loadMovie"/> is true,
|
||||
|
|
|
@ -207,7 +207,9 @@ namespace BizHawk.Client.Common
|
|||
// We are in record mode so replace the movie log with the one from the savestate
|
||||
if (Session.Settings.EnableBackupMovies && MakeBackup && Log.Count != 0)
|
||||
{
|
||||
SaveBackup();
|
||||
// TODO: This isn't ideal, but making it ideal would mean a big refactor.
|
||||
FileWriteResult saveResult = SaveBackup();
|
||||
if (saveResult.Exception != null) throw saveResult.Exception;
|
||||
MakeBackup = false;
|
||||
}
|
||||
|
||||
|
|
|
@ -50,14 +50,16 @@ namespace BizHawk.Client.Common
|
|||
_userBag = userBag;
|
||||
}
|
||||
|
||||
public void Create(string filename, SaveStateConfig config)
|
||||
public FileWriteResult Create(string filename, SaveStateConfig config, bool makeBackup)
|
||||
{
|
||||
// the old method of text savestate save is now gone.
|
||||
// a text savestate is just like a binary savestate, but with a different core lump
|
||||
using var bs = new ZipStateSaver(filename, config.CompressionLevelNormal);
|
||||
FileWriteResult<ZipStateSaver> createResult = ZipStateSaver.Create(filename, config.CompressionLevelNormal);
|
||||
if (createResult.IsError) return createResult;
|
||||
var bs = createResult.Value!;
|
||||
|
||||
using (new SimpleTime("Save Core"))
|
||||
{
|
||||
// the old method of text savestate save is now gone.
|
||||
// a text savestate is just like a binary savestate, but with a different core lump
|
||||
if (config.Type == SaveStateType.Text)
|
||||
{
|
||||
bs.PutLump(BinaryStateLump.CorestateText, tw => _statable.SaveStateText(tw));
|
||||
|
@ -112,6 +114,9 @@ namespace BizHawk.Client.Common
|
|||
{
|
||||
bs.PutLump(BinaryStateLump.LagLog, tw => ((ITasMovie) _movieSession.Movie).LagLog.Save(tw));
|
||||
}
|
||||
|
||||
makeBackup = makeBackup && config.MakeBackups;
|
||||
return bs.CloseAndDispose(makeBackup ? $"{filename}.bak" : null);
|
||||
}
|
||||
|
||||
public bool Load(string path, IDialogParent dialogParent)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#nullable enable
|
||||
|
||||
using System.IO;
|
||||
|
||||
using BizHawk.Common;
|
||||
|
@ -21,9 +23,9 @@ namespace BizHawk.Client.Common
|
|||
sw.WriteLine(VersionInfo.GetEmuVersion());
|
||||
}
|
||||
|
||||
public ZipStateSaver(string path, int compressionLevel)
|
||||
private ZipStateSaver(FrameworkZipWriter zip)
|
||||
{
|
||||
_zip = new FrameworkZipWriter(path, compressionLevel);
|
||||
_zip = zip;
|
||||
|
||||
// we put these in every zip, so we know where they came from
|
||||
// a bit redundant for movie files given their headers, but w/e
|
||||
|
@ -31,6 +33,35 @@ namespace BizHawk.Client.Common
|
|||
PutLump(BinaryStateLump.BizVersion, WriteEmuVersion, false);
|
||||
}
|
||||
|
||||
public static FileWriteResult<ZipStateSaver> Create(string path, int compressionLevel)
|
||||
{
|
||||
FileWriteResult<FrameworkZipWriter> result = FrameworkZipWriter.Create(path, compressionLevel);
|
||||
if (result.IsError) return new(result);
|
||||
else return result.Convert(new ZipStateSaver(result.Value!));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method must be called after writing has finished and must not be called twice.
|
||||
/// Dispose will be called regardless of the result.
|
||||
/// </summary>
|
||||
/// <param name="backupPath">If not null, renames the original file to this path.</param>
|
||||
public FileWriteResult CloseAndDispose(string? backupPath = null)
|
||||
{
|
||||
FileWriteResult result = _zip.CloseAndDispose(backupPath);
|
||||
Dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes and deletes the file. Use if there was an error while writing.
|
||||
/// Do not call <see cref="CloseAndDispose"/> after this.
|
||||
/// </summary>
|
||||
public void Abort()
|
||||
{
|
||||
_zip.Abort();
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public void PutLump(BinaryStateLump lump, Action<Stream> callback, bool zstdCompress = true)
|
||||
{
|
||||
_zip.WriteItem(lump.WriteName, callback, zstdCompress);
|
||||
|
|
|
@ -88,20 +88,10 @@ namespace BizHawk.Client.Common
|
|||
return file.Exists && Load(domains, file.FullName, false);
|
||||
}
|
||||
|
||||
public void NewList(string defaultFileName, bool autosave = false)
|
||||
public void NewList(string defaultFileName)
|
||||
{
|
||||
_defaultFileName = defaultFileName;
|
||||
|
||||
if (autosave && _changes && _cheatList.Count is not 0)
|
||||
{
|
||||
if (string.IsNullOrEmpty(CurrentFileName))
|
||||
{
|
||||
CurrentFileName = _defaultFileName;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
|
||||
_cheatList.Clear();
|
||||
CurrentFileName = "";
|
||||
Changes = false;
|
||||
|
@ -220,7 +210,7 @@ namespace BizHawk.Client.Common
|
|||
public bool IsActive(MemoryDomain domain, long address)
|
||||
=> _cheatList.Exists(cheat => !cheat.IsSeparator && cheat.Enabled && cheat.Domain == domain && cheat.Contains(address));
|
||||
|
||||
public void SaveOnClose()
|
||||
public FileWriteResult SaveOnClose()
|
||||
{
|
||||
if (_config.AutoSaveOnClose)
|
||||
{
|
||||
|
@ -231,17 +221,27 @@ namespace BizHawk.Client.Common
|
|||
CurrentFileName = _defaultFileName;
|
||||
}
|
||||
|
||||
SaveFile(CurrentFileName);
|
||||
return SaveFile(CurrentFileName);
|
||||
}
|
||||
else if (_cheatList.Count is 0 && !string.IsNullOrWhiteSpace(CurrentFileName))
|
||||
{
|
||||
File.Delete(CurrentFileName);
|
||||
try
|
||||
{
|
||||
File.Delete(CurrentFileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new(FileWriteEnum.FailedToDeleteGeneric, new(CurrentFileName, ""), ex);
|
||||
}
|
||||
_config.Recent.Remove(CurrentFileName);
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
||||
return new();
|
||||
}
|
||||
|
||||
public bool Save()
|
||||
public FileWriteResult Save()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(CurrentFileName))
|
||||
{
|
||||
|
@ -251,54 +251,51 @@ namespace BizHawk.Client.Common
|
|||
return SaveFile(CurrentFileName);
|
||||
}
|
||||
|
||||
public bool SaveFile(string path)
|
||||
public FileWriteResult SaveFile(string path)
|
||||
{
|
||||
try
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var cheat in _cheatList)
|
||||
{
|
||||
new FileInfo(path).Directory?.Create();
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var cheat in _cheatList)
|
||||
if (cheat.IsSeparator)
|
||||
{
|
||||
if (cheat.IsSeparator)
|
||||
{
|
||||
sb.AppendLine("----");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set to hex for saving
|
||||
var tempCheatType = cheat.Type;
|
||||
|
||||
cheat.SetType(WatchDisplayType.Hex);
|
||||
|
||||
sb
|
||||
.Append(cheat.AddressStr).Append('\t')
|
||||
.Append(cheat.ValueStr).Append('\t')
|
||||
.Append(cheat.Compare is null ? "N" : cheat.CompareStr).Append('\t')
|
||||
.Append(cheat.Domain != null ? cheat.Domain.Name : "").Append('\t')
|
||||
.Append(cheat.Enabled ? '1' : '0').Append('\t')
|
||||
.Append(cheat.Name).Append('\t')
|
||||
.Append(cheat.SizeAsChar).Append('\t')
|
||||
.Append(cheat.TypeAsChar).Append('\t')
|
||||
.Append(cheat.BigEndian is true ? '1' : '0').Append('\t')
|
||||
.Append(cheat.ComparisonType).Append('\t')
|
||||
.AppendLine();
|
||||
|
||||
cheat.SetType(tempCheatType);
|
||||
}
|
||||
sb.AppendLine("----");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set to hex for saving
|
||||
var tempCheatType = cheat.Type;
|
||||
|
||||
File.WriteAllText(path, sb.ToString());
|
||||
cheat.SetType(WatchDisplayType.Hex);
|
||||
|
||||
sb
|
||||
.Append(cheat.AddressStr).Append('\t')
|
||||
.Append(cheat.ValueStr).Append('\t')
|
||||
.Append(cheat.Compare is null ? "N" : cheat.CompareStr).Append('\t')
|
||||
.Append(cheat.Domain != null ? cheat.Domain.Name : "").Append('\t')
|
||||
.Append(cheat.Enabled ? '1' : '0').Append('\t')
|
||||
.Append(cheat.Name).Append('\t')
|
||||
.Append(cheat.SizeAsChar).Append('\t')
|
||||
.Append(cheat.TypeAsChar).Append('\t')
|
||||
.Append(cheat.BigEndian is true ? '1' : '0').Append('\t')
|
||||
.Append(cheat.ComparisonType).Append('\t')
|
||||
.AppendLine();
|
||||
|
||||
cheat.SetType(tempCheatType);
|
||||
}
|
||||
}
|
||||
FileWriteResult result = FileWriter.Write(path, (fs) =>
|
||||
{
|
||||
StreamWriter sw = new(fs);
|
||||
sw.Write(sb.ToString());
|
||||
});
|
||||
if (!result.IsError)
|
||||
{
|
||||
CurrentFileName = path;
|
||||
_config.Recent.Add(CurrentFileName);
|
||||
Changes = false;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool Load(IMemoryDomains domains, string path, bool append)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
@ -338,39 +339,37 @@ namespace BizHawk.Client.Common
|
|||
}
|
||||
}
|
||||
|
||||
public bool Save()
|
||||
public FileWriteResult Save()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(CurrentFileName))
|
||||
{
|
||||
return false;
|
||||
return new();
|
||||
}
|
||||
|
||||
using (var sw = new StreamWriter(CurrentFileName))
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("SystemID ").AppendLine(_systemId);
|
||||
|
||||
foreach (var watch in _watchList)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("SystemID ").AppendLine(_systemId);
|
||||
|
||||
foreach (var watch in _watchList)
|
||||
{
|
||||
sb.AppendLine(watch.ToString());
|
||||
}
|
||||
|
||||
sw.WriteLine(sb.ToString());
|
||||
sb.AppendLine(watch.ToString());
|
||||
}
|
||||
|
||||
Changes = false;
|
||||
return true;
|
||||
FileWriteResult result = FileWriter.Write(CurrentFileName, (fs) =>
|
||||
{
|
||||
using var sw = new StreamWriter(fs);
|
||||
sw.WriteLine(sb.ToString());
|
||||
});
|
||||
|
||||
if (!result.IsError) Changes = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool SaveAs(FileInfo file)
|
||||
public FileWriteResult SaveAs(FileInfo file)
|
||||
{
|
||||
if (file != null)
|
||||
{
|
||||
CurrentFileName = file.FullName;
|
||||
return Save();
|
||||
}
|
||||
Debug.Assert(file != null, "Cannot save as without a file name.");
|
||||
|
||||
return false;
|
||||
CurrentFileName = file.FullName;
|
||||
return Save();
|
||||
}
|
||||
|
||||
private bool LoadFile(string path, bool append)
|
||||
|
|
|
@ -20,9 +20,6 @@ namespace BizHawk.Client.EmuHawk
|
|||
// TODO: remove? or does anything ever need access to the FirmwareManager
|
||||
FirmwareManager FirmwareManager { get; }
|
||||
|
||||
/// <remarks>only referenced from <see cref="TAStudio"/></remarks>
|
||||
bool GameIsClosing { get; }
|
||||
|
||||
/// <remarks>only referenced from <see cref="PlaybackBox"/></remarks>
|
||||
bool HoldFrameAdvance { get; set; }
|
||||
|
||||
|
|
|
@ -270,7 +270,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
}
|
||||
|
||||
private void QuickSavestateMenuItem_Click(object sender, EventArgs e)
|
||||
=> SaveQuickSave(int.Parse(((ToolStripMenuItem) sender).Text));
|
||||
=> SaveQuickSaveAndShowError(int.Parse(((ToolStripMenuItem) sender).Text));
|
||||
|
||||
private void SaveNamedStateMenuItem_Click(object sender, EventArgs e) => SaveStateAs();
|
||||
|
||||
|
@ -306,7 +306,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
=> SavestateCurrentSlot();
|
||||
|
||||
private void SavestateCurrentSlot()
|
||||
=> SaveQuickSave(Config.SaveSlot);
|
||||
=> SaveQuickSaveAndShowError(Config.SaveSlot);
|
||||
|
||||
private void LoadCurrentSlotMenuItem_Click(object sender, EventArgs e)
|
||||
=> LoadstateCurrentSlot();
|
||||
|
@ -316,7 +316,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
|
||||
private void FlushSaveRAMMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
FlushSaveRAM();
|
||||
ShowMessageIfError(() => FlushSaveRAM(), "Failed to flush saveram!");
|
||||
}
|
||||
|
||||
private void ReadonlyMenuItem_Click(object sender, EventArgs e)
|
||||
|
@ -976,8 +976,15 @@ namespace BizHawk.Client.EmuHawk
|
|||
|
||||
private void SaveConfigMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
SaveConfig();
|
||||
AddOnScreenMessage("Saved settings");
|
||||
FileWriteResult result = SaveConfig();
|
||||
if (result.IsError)
|
||||
{
|
||||
this.ErrorMessageBox(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddOnScreenMessage("Saved settings");
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveConfigAsMenuItem_Click(object sender, EventArgs e)
|
||||
|
@ -989,8 +996,15 @@ namespace BizHawk.Client.EmuHawk
|
|||
initFileName: file);
|
||||
if (result is not null)
|
||||
{
|
||||
SaveConfig(result);
|
||||
AddOnScreenMessage("Copied settings");
|
||||
FileWriteResult saveResult = SaveConfig(result);
|
||||
if (saveResult.IsError)
|
||||
{
|
||||
this.ErrorMessageBox(saveResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddOnScreenMessage("Copied settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1375,8 +1389,15 @@ namespace BizHawk.Client.EmuHawk
|
|||
private void UndoSavestateContextMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
var slot = Config.SaveSlot;
|
||||
_stateSlots.SwapBackupSavestate(MovieSession.Movie, $"{SaveStatePrefix()}.QuickSave{slot % 10}.State", slot);
|
||||
AddOnScreenMessage($"Save slot {slot} restored.");
|
||||
FileWriteResult swapResult = _stateSlots.SwapBackupSavestate(MovieSession.Movie, $"{SaveStatePrefix()}.QuickSave{slot % 10}.State", slot);
|
||||
if (swapResult.IsError)
|
||||
{
|
||||
this.ErrorMessageBox(swapResult, "Failed to swap state files.");
|
||||
}
|
||||
else
|
||||
{
|
||||
AddOnScreenMessage($"Save slot {slot} restored.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearSramContextMenuItem_Click(object sender, EventArgs e)
|
||||
|
@ -1446,7 +1467,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
if (sender == Slot9StatusButton) slot = 9;
|
||||
if (sender == Slot0StatusButton) slot = 10;
|
||||
|
||||
if (e.Button is MouseButtons.Right) SaveQuickSave(slot);
|
||||
if (e.Button is MouseButtons.Right) SaveQuickSaveAndShowError(slot);
|
||||
else if (e.Button is MouseButtons.Left && HasSlot(slot)) _ = LoadQuickSave(slot);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
{
|
||||
void SelectAndSaveToSlot(int slot)
|
||||
{
|
||||
SaveQuickSave(slot);
|
||||
SaveQuickSaveAndShowError(slot);
|
||||
Config.SaveSlot = slot;
|
||||
UpdateStatusSlots();
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
LoadMostRecentROM();
|
||||
break;
|
||||
case "Flush SaveRAM":
|
||||
FlushSaveRAM();
|
||||
FlushSaveRAMMenuItem_Click(null, null);
|
||||
break;
|
||||
case "Display FPS":
|
||||
ToggleFps();
|
||||
|
|
|
@ -125,7 +125,14 @@ namespace BizHawk.Client.EmuHawk
|
|||
}
|
||||
else
|
||||
{
|
||||
MovieSession.StopMovie(saveChanges);
|
||||
FileWriteResult saveResult = MovieSession.StopMovie(saveChanges);
|
||||
if (saveResult.IsError)
|
||||
{
|
||||
this.ShowMessageBox(
|
||||
$"Failed to save movie.\n{saveResult.UserFriendlyErrorMessage()}\n{saveResult.Exception.Message}",
|
||||
"Error",
|
||||
EMsgBoxIcon.Error);
|
||||
}
|
||||
SetMainformMovieInfo();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,12 +26,8 @@ using BizHawk.Client.Common;
|
|||
|
||||
using BizHawk.Emulation.Common;
|
||||
using BizHawk.Emulation.Cores;
|
||||
using BizHawk.Emulation.Cores.Computers.AppleII;
|
||||
using BizHawk.Emulation.Cores.Computers.Commodore64;
|
||||
using BizHawk.Emulation.Cores.Computers.DOS;
|
||||
using BizHawk.Emulation.Cores.Consoles.Nintendo.QuickNES;
|
||||
using BizHawk.Emulation.Cores.Consoles.SNK;
|
||||
using BizHawk.Emulation.Cores.Nintendo.GBA;
|
||||
using BizHawk.Emulation.Cores.Nintendo.NES;
|
||||
using BizHawk.Emulation.Cores.Nintendo.SNES;
|
||||
|
||||
|
@ -867,20 +863,23 @@ namespace BizHawk.Client.EmuHawk
|
|||
closingArgs.Cancel = true;
|
||||
return;
|
||||
}
|
||||
// StopAv would be handled in CloseGame, but since we've asked the user about it, best to handle it now.
|
||||
StopAv();
|
||||
}
|
||||
|
||||
if (!Tools.AskSave())
|
||||
TryAgainResult configSaveResult = this.DoWithTryAgainBox(() => SaveConfig(), "Failed to save config file.");
|
||||
if (configSaveResult == TryAgainResult.Canceled)
|
||||
{
|
||||
closingArgs.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CloseGame())
|
||||
{
|
||||
closingArgs.Cancel = true;
|
||||
return;
|
||||
}
|
||||
Tools.Close();
|
||||
MovieSession.StopMovie();
|
||||
// zero 03-nov-2015 - close game after other steps. tools might need to unhook themselves from a core.
|
||||
CloseGame();
|
||||
SaveConfig();
|
||||
}
|
||||
|
||||
private readonly bool _suppressSyncSettingsWarning;
|
||||
|
@ -1846,6 +1845,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
|
||||
// countdown for saveram autoflushing
|
||||
public int AutoFlushSaveRamIn { get; set; }
|
||||
private bool AutoFlushSaveRamFailed;
|
||||
|
||||
private void SetStatusBar()
|
||||
{
|
||||
|
@ -2046,42 +2046,30 @@ namespace BizHawk.Client.EmuHawk
|
|||
return;
|
||||
}
|
||||
|
||||
byte[] sram = null;
|
||||
try
|
||||
{
|
||||
byte[] sram;
|
||||
|
||||
// some cores might not know how big the saveram ought to be, so just send it the whole file
|
||||
if (Emulator is AppleII or C64 or DOSBox or MGBAHawk or NeoGeoPort or NES { BoardName: "FDS" })
|
||||
{
|
||||
sram = File.ReadAllBytes(saveramToLoad.FullName);
|
||||
}
|
||||
else
|
||||
{
|
||||
var oldRam = Emulator.AsSaveRam().CloneSaveRam();
|
||||
if (oldRam is null)
|
||||
{
|
||||
// we have a SaveRAM file, but the current core does not have save ram.
|
||||
// just skip loading the saveram file in that case
|
||||
return;
|
||||
}
|
||||
|
||||
// why do we silently truncate\pad here instead of warning\erroring?
|
||||
sram = new byte[oldRam.Length];
|
||||
using var fs = saveramToLoad.OpenRead();
|
||||
_ = fs.Read(sram, 0, sram.Length);
|
||||
}
|
||||
|
||||
Emulator.AsSaveRam().StoreSaveRam(sram);
|
||||
sram = File.ReadAllBytes(saveramToLoad.FullName);
|
||||
}
|
||||
catch (IOException e)
|
||||
catch (Exception e)
|
||||
{
|
||||
AddOnScreenMessage("An error occurred while loading Sram");
|
||||
AddOnScreenMessage("An IO error occurred while loading Sram");
|
||||
Console.Error.WriteLine(e);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (sram != null) Emulator.AsSaveRam().StoreSaveRam(sram);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
AddOnScreenMessage("The core threw an error while loading Sram");
|
||||
Console.Error.WriteLine(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool FlushSaveRAM(bool autosave = false)
|
||||
public FileWriteResult FlushSaveRAM(bool autosave = false)
|
||||
{
|
||||
if (Emulator.HasSaveRam())
|
||||
{
|
||||
|
@ -2095,53 +2083,11 @@ namespace BizHawk.Client.EmuHawk
|
|||
path = Config.PathEntries.SaveRamAbsolutePath(Game, MovieSession.Movie);
|
||||
}
|
||||
|
||||
var file = new FileInfo(path);
|
||||
var newPath = $"{path}.new";
|
||||
var newFile = new FileInfo(newPath);
|
||||
var backupPath = $"{path}.bak";
|
||||
var backupFile = new FileInfo(backupPath);
|
||||
|
||||
var saveram = Emulator.AsSaveRam().CloneSaveRam();
|
||||
if (saveram == null)
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(file.DirectoryName!);
|
||||
using (var fs = File.Create(newPath))
|
||||
{
|
||||
fs.Write(saveram, 0, saveram.Length);
|
||||
fs.Flush(flushToDisk: true);
|
||||
}
|
||||
|
||||
if (file.Exists)
|
||||
{
|
||||
if (Config.BackupSaveram)
|
||||
{
|
||||
if (backupFile.Exists)
|
||||
{
|
||||
backupFile.Delete();
|
||||
}
|
||||
|
||||
file.MoveTo(backupPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
file.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
newFile.MoveTo(path);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
AddOnScreenMessage("Failed to flush saveram!");
|
||||
Console.Error.WriteLine(e);
|
||||
return false;
|
||||
}
|
||||
var saveram = Emulator.AsSaveRam().CloneSaveRam()!;
|
||||
return FileWriter.Write(path, saveram, $"{path}.bak");
|
||||
}
|
||||
|
||||
return true;
|
||||
return new();
|
||||
}
|
||||
|
||||
private void RewireSound()
|
||||
|
@ -2255,7 +2201,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
|
||||
if (!LoadRom(romPath, new LoadRomArgs(ioa), out var failureIsFromAskSave))
|
||||
{
|
||||
if (failureIsFromAskSave) AddOnScreenMessage("ROM loading cancelled; a tool had unsaved changes");
|
||||
if (failureIsFromAskSave) AddOnScreenMessage("ROM loading cancelled due to unsaved changes");
|
||||
else if (ioa is OpenAdvanced_LibretroNoGame || File.Exists(romPath)) AddOnScreenMessage("ROM loading failed");
|
||||
else Config.RecentRoms.HandleLoadError(this, romPath, rom);
|
||||
}
|
||||
|
@ -2584,7 +2530,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
public SettingsAdapter GetSettingsAdapterForLoadedCoreUntyped()
|
||||
=> new(Emulator, static () => true, HandlePutCoreSettings, MayPutCoreSyncSettings, HandlePutCoreSyncSettings);
|
||||
|
||||
private void SaveConfig(string path = "")
|
||||
private FileWriteResult SaveConfig(string path = "")
|
||||
{
|
||||
if (Config.SaveWindowPosition)
|
||||
{
|
||||
|
@ -2610,7 +2556,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
}
|
||||
|
||||
CommitCoreSettingsToConfig();
|
||||
ConfigService.Save(path, Config);
|
||||
return ConfigService.Save(path, Config);
|
||||
}
|
||||
|
||||
private void ToggleFps()
|
||||
|
@ -2831,8 +2777,16 @@ namespace BizHawk.Client.EmuHawk
|
|||
{
|
||||
if (MovieSession.Movie.IsActive())
|
||||
{
|
||||
MovieSession.Movie.Save();
|
||||
AddOnScreenMessage($"{MovieSession.Movie.Filename} saved.");
|
||||
FileWriteResult result = MovieSession.Movie.Save();
|
||||
if (result.IsError)
|
||||
{
|
||||
AddOnScreenMessage($"Failed to save {MovieSession.Movie.Filename}.");
|
||||
AddOnScreenMessage(result.UserFriendlyErrorMessage());
|
||||
}
|
||||
else
|
||||
{
|
||||
AddOnScreenMessage($"{MovieSession.Movie.Filename} saved.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3192,8 +3146,22 @@ namespace BizHawk.Client.EmuHawk
|
|||
AutoFlushSaveRamIn--;
|
||||
if (AutoFlushSaveRamIn <= 0)
|
||||
{
|
||||
FlushSaveRAM(true);
|
||||
AutoFlushSaveRamIn = Config.FlushSaveRamFrames;
|
||||
FileWriteResult result = FlushSaveRAM(true);
|
||||
if (result.IsError)
|
||||
{
|
||||
// For autosave, allow one failure before bothering the user.
|
||||
if (AutoFlushSaveRamFailed)
|
||||
{
|
||||
this.ErrorMessageBox(result, "Failed to flush saveram!");
|
||||
}
|
||||
AutoFlushSaveRamFailed = true;
|
||||
AutoFlushSaveRamIn = Math.Min(600, Config.FlushSaveRamFrames);
|
||||
}
|
||||
else
|
||||
{
|
||||
AutoFlushSaveRamFailed = false;
|
||||
AutoFlushSaveRamIn = Config.FlushSaveRamFrames;
|
||||
}
|
||||
}
|
||||
}
|
||||
// why not skip audio if the user doesn't want sound
|
||||
|
@ -3747,6 +3715,12 @@ namespace BizHawk.Client.EmuHawk
|
|||
private bool LoadRomInternal(string path, LoadRomArgs args, out bool failureIsFromAskSave)
|
||||
{
|
||||
failureIsFromAskSave = false;
|
||||
if (!CloseGame())
|
||||
{
|
||||
failureIsFromAskSave = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (path == null)
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
if (args == null)
|
||||
|
@ -3774,12 +3748,6 @@ namespace BizHawk.Client.EmuHawk
|
|||
// it is then up to the core itself to override its own local DeterministicEmulation setting
|
||||
bool deterministic = args.Deterministic ?? MovieSession.NewMovieQueued;
|
||||
|
||||
if (!Tools.AskSave())
|
||||
{
|
||||
failureIsFromAskSave = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
var loader = new RomLoader(Config, this)
|
||||
{
|
||||
ChooseArchive = LoadArchiveChooser,
|
||||
|
@ -3794,12 +3762,6 @@ namespace BizHawk.Client.EmuHawk
|
|||
loader.OnLoadSettings += CoreSettings;
|
||||
loader.OnLoadSyncSettings += CoreSyncSettings;
|
||||
|
||||
// this also happens in CloseGame(). But it needs to happen here since if we're restarting with the same core,
|
||||
// any settings changes that we made need to make it back to config before we try to instantiate that core with
|
||||
// the new settings objects
|
||||
CommitCoreSettingsToConfig(); // adelikat: I Think by reordering things, this isn't necessary anymore
|
||||
CloseGame();
|
||||
|
||||
var nextComm = CreateCoreComm();
|
||||
|
||||
IOpenAdvanced ioa = args.OpenAdvanced;
|
||||
|
@ -3977,7 +3939,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
|
||||
if (previousRom != CurrentlyOpenRom)
|
||||
{
|
||||
CheatList.NewList(Tools.GenerateDefaultCheatFilename(), autosave: true);
|
||||
CheatList.NewList(Tools.GenerateDefaultCheatFilename());
|
||||
if (Config.Cheats.LoadFileByGame && Emulator.HasMemoryDomains())
|
||||
{
|
||||
if (CheatList.AttemptToLoadCheatFile(Emulator.AsMemoryDomains()))
|
||||
|
@ -3994,7 +3956,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
}
|
||||
else
|
||||
{
|
||||
CheatList.NewList(Tools.GenerateDefaultCheatFilename(), autosave: true);
|
||||
CheatList.NewList(Tools.GenerateDefaultCheatFilename());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4031,7 +3993,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
DisplayManager.UpdateGlobals(Config, Emulator);
|
||||
DisplayManager.Blank();
|
||||
ExtToolManager.BuildToolStrip();
|
||||
CheatList.NewList("", autosave: true);
|
||||
CheatList.NewList("");
|
||||
OnRomChanged();
|
||||
return false;
|
||||
}
|
||||
|
@ -4084,74 +4046,96 @@ namespace BizHawk.Client.EmuHawk
|
|||
}
|
||||
}
|
||||
|
||||
// whats the difference between these two methods??
|
||||
// its very tricky. rename to be more clear or combine them.
|
||||
// This gets called whenever a core related thing is changed.
|
||||
// Like reboot core.
|
||||
private void CloseGame(bool clearSram = false)
|
||||
/// <summary>
|
||||
/// This closes the game but does not set things up for using the client with the new null emulator.
|
||||
/// This method should only be called (outside of <see cref="CloseRom(bool)"/>) if the caller is about to load a new game with no user interaction between close and load.
|
||||
/// </summary>
|
||||
/// <returns>True if the game was closed. False if the user cancelled due to unsaved changes.</returns>
|
||||
private bool CloseGame(bool clearSram = false)
|
||||
{
|
||||
GameIsClosing = true;
|
||||
CommitCoreSettingsToConfig(); // Must happen before stopping the movie, since it checks for active movie.
|
||||
|
||||
if (!Tools.AskSave())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// There is a cheats tool, but cheats can be active while the "cheats tool" is not. And have auto-save option.
|
||||
TryAgainResult cheatSaveResult = this.DoWithTryAgainBox(CheatList.SaveOnClose, "Failed to save cheats.");
|
||||
if (cheatSaveResult == TryAgainResult.Canceled) return false;
|
||||
|
||||
// If TAStudio is open, we already asked about saving the movie.
|
||||
if (!Tools.IsLoaded<TAStudio>())
|
||||
{
|
||||
TryAgainResult saveMovieResult = this.DoWithTryAgainBox(() => MovieSession.StopMovie(), "Failed to save movie.");
|
||||
if (saveMovieResult == TryAgainResult.Canceled) return false;
|
||||
}
|
||||
|
||||
if (clearSram)
|
||||
{
|
||||
var path = Config.PathEntries.SaveRamAbsolutePath(Game, MovieSession.Movie);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
AddOnScreenMessage("SRAM cleared.");
|
||||
TryAgainResult clearResult = this.DoWithTryAgainBox(() => {
|
||||
try
|
||||
{
|
||||
File.Delete(path);
|
||||
AddOnScreenMessage("SRAM cleared.");
|
||||
return new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new(FileWriteEnum.FailedToDeleteGeneric, new(path, ""), ex);
|
||||
}
|
||||
}, "Failed to clear SRAM.");
|
||||
if (clearResult == TryAgainResult.Canceled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Emulator.HasSaveRam())
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (FlushSaveRAM()) break;
|
||||
|
||||
var result = ShowMessageBox3(
|
||||
owner: this,
|
||||
"Failed flushing the game's Save RAM to your disk.\n" +
|
||||
"Do you want to try again?",
|
||||
"IOError while writing SaveRAM",
|
||||
EMsgBoxIcon.Error);
|
||||
|
||||
if (result is false) break;
|
||||
if (result is null) return;
|
||||
}
|
||||
TryAgainResult flushResult = this.DoWithTryAgainBox(
|
||||
() => FlushSaveRAM(),
|
||||
"Failed flushing the game's Save RAM to your disk.");
|
||||
if (flushResult == TryAgainResult.Canceled) return false;
|
||||
}
|
||||
|
||||
TryAgainResult stateSaveResult = this.DoWithTryAgainBox(AutoSaveStateIfConfigured, "Failed to auto-save state.");
|
||||
if (stateSaveResult == TryAgainResult.Canceled) return false;
|
||||
|
||||
StopAv();
|
||||
AutoSaveStateIfConfigured();
|
||||
|
||||
CommitCoreSettingsToConfig();
|
||||
DisableRewind();
|
||||
|
||||
if (MovieSession.Movie.IsActive()) // Note: this must be called after CommitCoreSettingsToConfig()
|
||||
{
|
||||
StopMovie();
|
||||
}
|
||||
|
||||
RA?.Stop();
|
||||
|
||||
CheatList.SaveOnClose();
|
||||
Emulator.Dispose();
|
||||
Emulator = new NullEmulator();
|
||||
Game = GameInfo.NullInstance;
|
||||
InputManager.SyncControls(Emulator, MovieSession, Config);
|
||||
RewireSound();
|
||||
RebootStatusBarIcon.Visible = false;
|
||||
GameIsClosing = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void AutoSaveStateIfConfigured()
|
||||
private FileWriteResult AutoSaveStateIfConfigured()
|
||||
{
|
||||
if (Config.AutoSaveLastSaveSlot && Emulator.HasSavestates()) SavestateCurrentSlot();
|
||||
if (Config.AutoSaveLastSaveSlot && Emulator.HasSavestates())
|
||||
{
|
||||
return SaveQuickSave(Config.SaveSlot);
|
||||
}
|
||||
|
||||
return new();
|
||||
}
|
||||
|
||||
public bool GameIsClosing { get; private set; } // Lets tools make better decisions when being called by CloseGame
|
||||
|
||||
/// <summary>
|
||||
/// This closes the current ROM, closes tools that require emulator services, and sets things up for the user to interact with the client having no loaded ROM.
|
||||
/// </summary>
|
||||
/// <param name="clearSram">True if SRAM should be deleted instead of saved.</param>
|
||||
public void CloseRom(bool clearSram = false)
|
||||
{
|
||||
// This gets called after Close Game gets called.
|
||||
// Tested with NESHawk and SMB3 (U)
|
||||
if (Tools.AskSave())
|
||||
{
|
||||
CloseGame(clearSram);
|
||||
|
@ -4161,7 +4145,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
PauseOnFrame = null;
|
||||
CurrentlyOpenRom = null;
|
||||
CurrentlyOpenRomArgs = null;
|
||||
CheatList.NewList("", autosave: true);
|
||||
CheatList.NewList("");
|
||||
OnRomChanged();
|
||||
}
|
||||
}
|
||||
|
@ -4308,29 +4292,53 @@ namespace BizHawk.Client.EmuHawk
|
|||
return LoadState(path: path, userFriendlyStateName: quickSlotName, suppressOSD: suppressOSD);
|
||||
}
|
||||
|
||||
public void SaveState(string path, string userFriendlyStateName, bool fromLua = false, bool suppressOSD = false)
|
||||
private FileWriteResult SaveStateInternal(string path, string userFriendlyStateName, bool suppressOSD, bool isQuickSave)
|
||||
{
|
||||
if (!Emulator.HasSavestates())
|
||||
{
|
||||
return;
|
||||
return new(FileWriteEnum.Aborted, new("", ""), new UnlessUsingApiException("The current emulator does not support savestates."));
|
||||
}
|
||||
|
||||
if (isQuickSave)
|
||||
{
|
||||
var handled = false;
|
||||
if (QuicksaveSave is not null)
|
||||
{
|
||||
BeforeQuickSaveEventArgs args = new(userFriendlyStateName);
|
||||
QuicksaveSave(this, args);
|
||||
handled = args.Handled;
|
||||
}
|
||||
if (handled)
|
||||
{
|
||||
// I suppose this is a success? But we have no path.
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
||||
if (ToolControllingSavestates is { } tool)
|
||||
{
|
||||
tool.SaveState();
|
||||
return;
|
||||
if (isQuickSave) tool.SaveQuickSave(SlotToInt(userFriendlyStateName));
|
||||
else tool.SaveState();
|
||||
// assume success by the tool: state was created, but not as a file. So no path.
|
||||
return new();
|
||||
}
|
||||
|
||||
if (MovieSession.Movie.IsActive() && Emulator.Frame > MovieSession.Movie.FrameCount)
|
||||
{
|
||||
AddOnScreenMessage("Cannot savestate after movie end!");
|
||||
return;
|
||||
const string errmsg = "Cannot savestate after movie end!";
|
||||
AddOnScreenMessage(errmsg);
|
||||
// Failed to create state due to limitations of our movie handling code.
|
||||
return new(FileWriteEnum.Aborted, new("", ""), new UnlessUsingApiException(errmsg));
|
||||
}
|
||||
|
||||
try
|
||||
FileWriteResult result = new SavestateFile(Emulator, MovieSession, MovieSession.UserBag)
|
||||
.Create(path, Config.Savestates, isQuickSave);
|
||||
if (result.IsError)
|
||||
{
|
||||
AddOnScreenMessage($"Unable to save state {path}");
|
||||
}
|
||||
else
|
||||
{
|
||||
new SavestateFile(Emulator, MovieSession, MovieSession.UserBag).Create(path, Config.Savestates);
|
||||
|
||||
if (SavestateSaved is not null)
|
||||
{
|
||||
StateSavedEventArgs args = new(userFriendlyStateName);
|
||||
|
@ -4338,63 +4346,42 @@ namespace BizHawk.Client.EmuHawk
|
|||
}
|
||||
RA?.OnSaveState(path);
|
||||
|
||||
if (Tools.Has<LuaConsole>())
|
||||
{
|
||||
Tools.LuaConsole.LuaImp.CallSaveStateEvent(userFriendlyStateName);
|
||||
}
|
||||
|
||||
if (!suppressOSD)
|
||||
{
|
||||
AddOnScreenMessage($"Saved state: {userFriendlyStateName}");
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
AddOnScreenMessage($"Unable to save state {path}");
|
||||
}
|
||||
|
||||
if (!fromLua)
|
||||
{
|
||||
UpdateStatusSlots();
|
||||
}
|
||||
UpdateStatusSlots();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public FileWriteResult SaveState(string path, string userFriendlyStateName, bool suppressOSD = false)
|
||||
{
|
||||
return SaveStateInternal(path, userFriendlyStateName, suppressOSD, false);
|
||||
}
|
||||
|
||||
// TODO: should backup logic be stuffed in into Client.Common.SaveStateManager?
|
||||
public void SaveQuickSave(int slot, bool suppressOSD = false, bool fromLua = false)
|
||||
public FileWriteResult SaveQuickSave(int slot, bool suppressOSD = false)
|
||||
{
|
||||
if (!Emulator.HasSavestates())
|
||||
{
|
||||
return;
|
||||
}
|
||||
var quickSlotName = $"QuickSave{slot % 10}";
|
||||
var handled = false;
|
||||
if (QuicksaveSave is not null)
|
||||
{
|
||||
BeforeQuickSaveEventArgs args = new(quickSlotName);
|
||||
QuicksaveSave(this, args);
|
||||
handled = args.Handled;
|
||||
}
|
||||
if (handled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ToolControllingSavestates is { } tool)
|
||||
{
|
||||
tool.SaveQuickSave(SlotToInt(quickSlotName));
|
||||
return;
|
||||
}
|
||||
|
||||
var path = $"{SaveStatePrefix()}.{quickSlotName}.State";
|
||||
new FileInfo(path).Directory?.Create();
|
||||
|
||||
// Make backup first
|
||||
if (Config.Savestates.MakeBackups)
|
||||
{
|
||||
Util.TryMoveBackupFile(path, $"{path}.bak");
|
||||
}
|
||||
return SaveStateInternal(path, quickSlotName, suppressOSD, true);
|
||||
}
|
||||
|
||||
SaveState(path, quickSlotName, fromLua, suppressOSD);
|
||||
|
||||
if (Tools.Has<LuaConsole>())
|
||||
{
|
||||
Tools.LuaConsole.LuaImp.CallSaveStateEvent(quickSlotName);
|
||||
}
|
||||
/// <summary>
|
||||
/// Runs <see cref="SaveQuickSave(int, bool)"/> and displays a pop up message if there was an error.
|
||||
/// </summary>
|
||||
private void SaveQuickSaveAndShowError(int slot)
|
||||
{
|
||||
ShowMessageIfError(() => SaveQuickSave(slot), "Quick save failed.");
|
||||
}
|
||||
|
||||
public bool EnsureCoreIsAccurate()
|
||||
|
@ -4457,12 +4444,17 @@ namespace BizHawk.Client.EmuHawk
|
|||
var path = Config.PathEntries.SaveStateAbsolutePath(Game.System);
|
||||
new FileInfo(path).Directory?.Create();
|
||||
|
||||
var result = this.ShowFileSaveDialog(
|
||||
var shouldSaveResult = this.ShowFileSaveDialog(
|
||||
fileExt: "State",
|
||||
filter: EmuHawkSaveStatesFSFilterSet,
|
||||
initDir: path,
|
||||
initFileName: $"{SaveStatePrefix()}.QuickSave0.State");
|
||||
if (result is not null) SaveState(path: result, userFriendlyStateName: result);
|
||||
if (shouldSaveResult is not null)
|
||||
{
|
||||
ShowMessageIfError(
|
||||
() => SaveState(path: shouldSaveResult, userFriendlyStateName: shouldSaveResult),
|
||||
"Unable to save state.");
|
||||
}
|
||||
|
||||
if (Tools.IsLoaded<TAStudio>())
|
||||
{
|
||||
|
@ -4752,6 +4744,15 @@ namespace BizHawk.Client.EmuHawk
|
|||
_ => null,
|
||||
};
|
||||
|
||||
public void ShowMessageIfError(Func<FileWriteResult> action, string message)
|
||||
{
|
||||
FileWriteResult result = action();
|
||||
if (result.IsError)
|
||||
{
|
||||
this.ErrorMessageBox(result, message);
|
||||
}
|
||||
}
|
||||
|
||||
public void StartSound() => Sound.StartSound();
|
||||
public void StopSound() => Sound.StopSound();
|
||||
|
||||
|
|
|
@ -475,7 +475,11 @@ namespace BizHawk.Client.EmuHawk
|
|||
|
||||
SaveToDefaults(cd);
|
||||
|
||||
ConfigService.Save(Config.ControlDefaultPath, cd);
|
||||
FileWriteResult saveResult = ConfigService.Save(Config.ControlDefaultPath, cd);
|
||||
if (saveResult.IsError)
|
||||
{
|
||||
this.ErrorMessageBox(saveResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,8 @@ namespace BizHawk.Client.EmuHawk
|
|||
_movie.Comments.Add(c.Value.ToString());
|
||||
}
|
||||
|
||||
_movie.Save();
|
||||
FileWriteResult result = _movie.Save();
|
||||
if (result.IsError) throw result.Exception!;
|
||||
}
|
||||
|
||||
private void Cancel_Click(object sender, EventArgs e)
|
||||
|
|
|
@ -102,7 +102,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
MaxDropDownItems = 32,
|
||||
Size = new(152, 21),
|
||||
};
|
||||
if (_emulator.HasSaveRam() && _emulator.AsSaveRam().CloneSaveRam(clearDirty: false) is not null) StartFromCombo.Items.Add(START_FROM_SAVERAM);
|
||||
if (_emulator.HasSaveRam()) StartFromCombo.Items.Add(START_FROM_SAVERAM);
|
||||
if (_emulator.HasSavestates()) StartFromCombo.Items.Add(START_FROM_SAVESTATE);
|
||||
|
||||
DefaultAuthorCheckBox = new()
|
||||
|
@ -242,7 +242,6 @@ namespace BizHawk.Client.EmuHawk
|
|||
else if (selectedStartFromValue is START_FROM_SAVERAM && _emulator.HasSaveRam())
|
||||
{
|
||||
var core = _emulator.AsSaveRam();
|
||||
movieToRecord.StartsFromSaveRam = true;
|
||||
movieToRecord.SaveRam = core.CloneSaveRam(clearDirty: false);
|
||||
}
|
||||
|
||||
|
|
|
@ -214,19 +214,23 @@ namespace BizHawk.Client.EmuHawk
|
|||
{
|
||||
if (_currentFilename != null)
|
||||
{
|
||||
RunSave();
|
||||
bool saveResult2 = RunSave();
|
||||
ShutdownCDL();
|
||||
return true;
|
||||
return saveResult2;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - I don't like this system. It's hard to figure out how to use it. It should be done in multiple passes.
|
||||
var result = DialogController.ShowMessageBox2("Save changes to CDL session?", "CDL Auto Save", EMsgBoxIcon.Question);
|
||||
if (!result)
|
||||
var result = DialogController.ShowMessageBox3("Save changes to CDL session?", "CDL Save", EMsgBoxIcon.Question);
|
||||
if (result == false)
|
||||
{
|
||||
ShutdownCDL();
|
||||
return true;
|
||||
}
|
||||
else if (result == null)
|
||||
{
|
||||
ShutdownCDL();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_currentFilename))
|
||||
{
|
||||
|
@ -240,9 +244,9 @@ namespace BizHawk.Client.EmuHawk
|
|||
return false;
|
||||
}
|
||||
|
||||
RunSave();
|
||||
bool saveResult = RunSave();
|
||||
ShutdownCDL();
|
||||
return true;
|
||||
return saveResult;
|
||||
}
|
||||
|
||||
private bool _autoloading;
|
||||
|
@ -341,11 +345,20 @@ namespace BizHawk.Client.EmuHawk
|
|||
LoadFile(file.FullName);
|
||||
}
|
||||
|
||||
private void RunSave()
|
||||
/// <summary>
|
||||
/// returns false if the operation was canceled
|
||||
/// </summary>
|
||||
private bool RunSave()
|
||||
{
|
||||
_recent.Add(_currentFilename);
|
||||
using var fs = new FileStream(_currentFilename, FileMode.Create, FileAccess.Write);
|
||||
_cdl.Save(fs);
|
||||
TryAgainResult result = this.DoWithTryAgainBox(
|
||||
() => FileWriter.Write(_currentFilename, _cdl.Save),
|
||||
"Failed to save CDL session.");
|
||||
if (result == TryAgainResult.Saved)
|
||||
{
|
||||
_recent.Add(_currentFilename);
|
||||
return true;
|
||||
}
|
||||
return result != TryAgainResult.Canceled;
|
||||
}
|
||||
|
||||
private void SaveMenuItem_Click(object sender, EventArgs e)
|
||||
|
@ -386,8 +399,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
return false;
|
||||
|
||||
SetCurrentFilename(file.FullName);
|
||||
RunSave();
|
||||
return true;
|
||||
return RunSave();
|
||||
}
|
||||
|
||||
private void SaveAsMenuItem_Click(object sender, EventArgs e)
|
||||
|
|
|
@ -142,7 +142,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
}
|
||||
}
|
||||
|
||||
private bool SaveAs()
|
||||
private FileWriteResult SaveAs()
|
||||
{
|
||||
var fileName = MainForm.CheatList.CurrentFileName;
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
|
@ -156,7 +156,8 @@ namespace BizHawk.Client.EmuHawk
|
|||
CheatsFSFilterSet,
|
||||
this);
|
||||
|
||||
return file != null && MainForm.CheatList.SaveFile(file.FullName);
|
||||
if (file == null) return new();
|
||||
else return MainForm.CheatList.SaveFile(file.FullName);
|
||||
}
|
||||
|
||||
private void Cheats_Load(object sender, EventArgs e)
|
||||
|
@ -361,7 +362,12 @@ namespace BizHawk.Client.EmuHawk
|
|||
{
|
||||
if (MainForm.CheatList.Changes)
|
||||
{
|
||||
if (MainForm.CheatList.Save())
|
||||
FileWriteResult result = MainForm.CheatList.Save();
|
||||
if (result.IsError)
|
||||
{
|
||||
this.ErrorMessageBox(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateMessageLabel(saved: true);
|
||||
}
|
||||
|
@ -374,7 +380,12 @@ namespace BizHawk.Client.EmuHawk
|
|||
|
||||
private void SaveAsMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (SaveAs())
|
||||
FileWriteResult result = SaveAs();
|
||||
if (result.IsError)
|
||||
{
|
||||
this.ErrorMessageBox(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateMessageLabel(saved: true);
|
||||
}
|
||||
|
|
|
@ -681,15 +681,25 @@ namespace BizHawk.Client.EmuHawk
|
|||
return result is not null ? new FileInfo(result) : null;
|
||||
}
|
||||
|
||||
private void SaveSessionAs()
|
||||
private FileWriteResult SaveSessionAs()
|
||||
{
|
||||
var file = GetSaveFileFromUser();
|
||||
if (file != null)
|
||||
{
|
||||
LuaImp.ScriptList.Save(file.FullName);
|
||||
Config.RecentLuaSession.Add(file.FullName);
|
||||
OutputMessages.Text = $"{file.Name} saved.";
|
||||
FileWriteResult saveResult = LuaImp.ScriptList.Save(file.FullName);
|
||||
if (saveResult.IsError)
|
||||
{
|
||||
this.ErrorMessageBox(saveResult);
|
||||
OutputMessages.Text = $"Lua session could not be saved to {file.Name}";
|
||||
}
|
||||
else
|
||||
{
|
||||
Config.RecentLuaSession.Add(file.FullName);
|
||||
OutputMessages.Text = $"{file.Name} saved.";
|
||||
}
|
||||
return saveResult;
|
||||
}
|
||||
return new();
|
||||
}
|
||||
|
||||
private void LoadSessionFromRecent(string path)
|
||||
|
@ -717,7 +727,11 @@ namespace BizHawk.Client.EmuHawk
|
|||
icon: EMsgBoxIcon.Question,
|
||||
text: $"Save {WindowTitleStatic} session?"));
|
||||
if (result is null) return false;
|
||||
if (result.Value) SaveOrSaveAs();
|
||||
if (result.Value)
|
||||
{
|
||||
TryAgainResult saveResult = this.DoWithTryAgainBox(SaveOrSaveAs, "Failed to save Lua session.");
|
||||
return saveResult != TryAgainResult.Canceled;
|
||||
}
|
||||
else LuaImp.ScriptList.Changes = false;
|
||||
return true;
|
||||
}
|
||||
|
@ -732,16 +746,20 @@ namespace BizHawk.Client.EmuHawk
|
|||
}
|
||||
}
|
||||
|
||||
private void SaveOrSaveAs()
|
||||
private FileWriteResult SaveOrSaveAs()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(LuaImp.ScriptList.Filename))
|
||||
{
|
||||
LuaImp.ScriptList.Save(LuaImp.ScriptList.Filename);
|
||||
Config.RecentLuaSession.Add(LuaImp.ScriptList.Filename);
|
||||
FileWriteResult result = LuaImp.ScriptList.Save(LuaImp.ScriptList.Filename);
|
||||
if (!result.IsError)
|
||||
{
|
||||
Config.RecentLuaSession.Add(LuaImp.ScriptList.Filename);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
SaveSessionAs();
|
||||
return SaveSessionAs();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -784,8 +802,16 @@ namespace BizHawk.Client.EmuHawk
|
|||
{
|
||||
if (LuaImp.ScriptList.Changes)
|
||||
{
|
||||
SaveOrSaveAs();
|
||||
OutputMessages.Text = $"{Path.GetFileName(LuaImp.ScriptList.Filename)} saved.";
|
||||
FileWriteResult result = SaveOrSaveAs();
|
||||
if (result.IsError)
|
||||
{
|
||||
this.ErrorMessageBox(result, "Failed to save Lua session.");
|
||||
OutputMessages.Text = $"Failed to save {Path.GetFileName(LuaImp.ScriptList.Filename)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
OutputMessages.Text = $"{Path.GetFileName(LuaImp.ScriptList.Filename)} saved.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -112,6 +112,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
return true;
|
||||
}
|
||||
|
||||
// Intentionally not updating this to use FileWriter because this tool is going to be removed later.
|
||||
foreach (var zone in _unsavedZones)
|
||||
{
|
||||
SaveMacroAs(_zones[zone]);
|
||||
|
|
|
@ -59,13 +59,10 @@
|
|||
|
||||
public void StopMovie(bool suppressSave)
|
||||
{
|
||||
if (!MainForm.GameIsClosing)
|
||||
{
|
||||
Activate();
|
||||
_suppressAskSave = suppressSave;
|
||||
StartNewTasMovie();
|
||||
_suppressAskSave = false;
|
||||
}
|
||||
Activate();
|
||||
_suppressAskSave = suppressSave;
|
||||
StartNewTasMovie();
|
||||
_suppressAskSave = false;
|
||||
}
|
||||
|
||||
public bool WantsToControlRewind { get; private set; } = true;
|
||||
|
|
|
@ -155,12 +155,16 @@ namespace BizHawk.Client.EmuHawk
|
|||
|
||||
StopSeeking();
|
||||
if (CurrentTasMovie?.Changes is not true) return true;
|
||||
var result = DialogController.DoWithTempMute(() => this.ModalMessageBox3(
|
||||
var shouldSaveResult = DialogController.DoWithTempMute(() => this.ModalMessageBox3(
|
||||
caption: "Closing with Unsaved Changes",
|
||||
icon: EMsgBoxIcon.Question,
|
||||
text: $"Save {WindowTitleStatic} project?"));
|
||||
if (result is null) return false;
|
||||
if (result.Value) SaveTas();
|
||||
if (shouldSaveResult == true)
|
||||
{
|
||||
TryAgainResult saveResult = this.DoWithTryAgainBox(() => SaveTas(), "Failed to save movie.");
|
||||
return saveResult != TryAgainResult.Canceled;
|
||||
}
|
||||
if (shouldSaveResult is null) return false;
|
||||
else CurrentTasMovie.ClearChanges();
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -36,18 +36,22 @@ namespace BizHawk.Client.EmuHawk
|
|||
|
||||
NewFromCurrentSaveRamMenuItem.Enabled =
|
||||
CurrentTasMovie.InputLogLength > 0
|
||||
&& SaveRamEmulator != null;
|
||||
&& SaveRamEmulator?.SupportsSaveRam == true;
|
||||
}
|
||||
|
||||
private void StartNewProjectFromNowMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (AskSaveChanges())
|
||||
{
|
||||
var newProject = CurrentTasMovie.ConvertToSavestateAnchoredMovie(
|
||||
var result = CurrentTasMovie.ConvertToSavestateAnchoredMovie(
|
||||
Emulator.Frame, StatableEmulator.CloneSavestate());
|
||||
DisplayMessageIfFailed(() => result, "Failed to create movie.");
|
||||
|
||||
MainForm.PauseEmulator();
|
||||
LoadMovie(newProject, true);
|
||||
if (result.Value is ITasMovie newProject)
|
||||
{
|
||||
MainForm.PauseEmulator();
|
||||
LoadMovie(newProject, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,11 +59,16 @@ namespace BizHawk.Client.EmuHawk
|
|||
{
|
||||
if (AskSaveChanges())
|
||||
{
|
||||
var saveRam = SaveRamEmulator?.CloneSaveRam(clearDirty: false) ?? throw new Exception("No SaveRam");
|
||||
var saveRam = SaveRamEmulator?.CloneSaveRam(clearDirty: false) ?? throw new Exception("No SaveRam; this button should have been disabled.");
|
||||
GoToFrame(TasView.AnyRowsSelected ? TasView.FirstSelectedRowIndex : 0);
|
||||
var newProject = CurrentTasMovie.ConvertToSaveRamAnchoredMovie(saveRam);
|
||||
MainForm.PauseEmulator();
|
||||
LoadMovie(newProject, true);
|
||||
var result = CurrentTasMovie.ConvertToSaveRamAnchoredMovie(saveRam);
|
||||
DisplayMessageIfFailed(() => result, "Failed to create movie.");
|
||||
|
||||
if (result.Value is ITasMovie newProject)
|
||||
{
|
||||
MainForm.PauseEmulator();
|
||||
LoadMovie(newProject, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,30 +124,30 @@ namespace BizHawk.Client.EmuHawk
|
|||
|
||||
private void SaveTasMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
SaveTas();
|
||||
DisplayMessageIfFailed(() => SaveTas(), "Failed to save movie.");
|
||||
if (Settings.BackupPerFileSave)
|
||||
{
|
||||
SaveTas(saveBackup: true);
|
||||
DisplayMessageIfFailed(() => SaveTas(saveBackup: true), "Failed to save backup.");
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveAsTasMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
SaveAsTas();
|
||||
DisplayMessageIfFailed(() => SaveAsTas(), "Failed to save movie.");
|
||||
if (Settings.BackupPerFileSave)
|
||||
{
|
||||
SaveTas(saveBackup: true);
|
||||
DisplayMessageIfFailed(() => SaveTas(saveBackup: true), "Failed to save backup.");
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveBackupMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
SaveTas(saveBackup: true);
|
||||
DisplayMessageIfFailed(() => SaveTas(saveBackup: true), "Failed to save backup.");
|
||||
}
|
||||
|
||||
private void SaveBk2BackupMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
SaveTas(saveAsBk2: true, saveBackup: true);
|
||||
DisplayMessageIfFailed(() => SaveTas(saveAsBk2: true, saveBackup: true), "Failed to save backup.");
|
||||
}
|
||||
|
||||
private void SaveSelectionToMacroMenuItem_Click(object sender, EventArgs e)
|
||||
|
@ -220,13 +229,25 @@ namespace BizHawk.Client.EmuHawk
|
|||
{
|
||||
MessageStatusLabel.Text = "Exporting to .bk2...";
|
||||
MessageStatusLabel.Owner.Update();
|
||||
|
||||
Cursor = Cursors.WaitCursor;
|
||||
var bk2 = CurrentTasMovie.ToBk2();
|
||||
bk2.Filename = fileInfo.FullName;
|
||||
bk2.Attach(Emulator); // required to be able to save the cycle count for ICycleTiming emulators
|
||||
bk2.Save();
|
||||
MessageStatusLabel.Text = $"{bk2.Name} exported.";
|
||||
FileWriteResult saveResult = bk2.Save();
|
||||
Cursor = Cursors.Default;
|
||||
|
||||
while (saveResult.IsError)
|
||||
{
|
||||
DialogResult d = MessageBox.Show(
|
||||
$"Failed to save .bk2. {saveResult.UserFriendlyErrorMessage()}\nTry again?",
|
||||
"Error",
|
||||
MessageBoxButtons.YesNo);
|
||||
if (d == DialogResult.Yes) saveResult = bk2.Save();
|
||||
else break;
|
||||
}
|
||||
if (!saveResult.IsError) MessageStatusLabel.Text = $"{bk2.Name} exported.";
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1320,7 +1341,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
|
||||
StartANewProjectFromSaveRamMenuItem.Visible =
|
||||
selectionIsSingleRow
|
||||
&& SaveRamEmulator != null
|
||||
&& SaveRamEmulator?.SupportsSaveRam == true
|
||||
&& !CurrentTasMovie.StartsFromSavestate;
|
||||
|
||||
StartFromNowSeparator.Visible = StartNewProjectFromNowMenuItem.Visible || StartANewProjectFromSaveRamMenuItem.Visible;
|
||||
|
|
|
@ -36,6 +36,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
private const string FrameColumnName = "FrameColumn";
|
||||
private UndoHistoryForm _undoForm;
|
||||
private Timer _autosaveTimer;
|
||||
private bool _lastAutoSaveSuccess = true;
|
||||
|
||||
private readonly int _defaultMainSplitDistance;
|
||||
private readonly int _defaultBranchMarkerSplitDistance;
|
||||
|
@ -194,11 +195,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
|
||||
_autosaveTimer = new Timer(components);
|
||||
_autosaveTimer.Tick += AutosaveTimerEventProcessor;
|
||||
if (Settings.AutosaveInterval > 0)
|
||||
{
|
||||
_autosaveTimer.Interval = (int)Settings.AutosaveInterval;
|
||||
_autosaveTimer.Start();
|
||||
}
|
||||
ScheduleAutoSave(Settings.AutosaveInterval);
|
||||
|
||||
MainVertialSplit.SetDistanceOrDefault(
|
||||
Settings.MainVerticalSplitDistance,
|
||||
|
@ -240,20 +237,27 @@ namespace BizHawk.Client.EmuHawk
|
|||
{
|
||||
changesString = "The current movie has unsaved changes. Would you like to save before closing it?";
|
||||
}
|
||||
var result = DialogController.ShowMessageBox3(
|
||||
var shouldSaveResult = DialogController.ShowMessageBox3(
|
||||
"TAStudio will create a new project file from the current movie.\n\n" + changesString,
|
||||
"Convert movie",
|
||||
EMsgBoxIcon.Question);
|
||||
if (result == true)
|
||||
if (shouldSaveResult == true)
|
||||
{
|
||||
MovieSession.Movie.Save();
|
||||
FileWriteResult saveResult = MovieSession.Movie.Save();
|
||||
if (saveResult.IsError)
|
||||
{
|
||||
DisplayMessageIfFailed(() => saveResult, "Failed to save movie.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (result == null)
|
||||
else if (shouldSaveResult == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var tasMovie = ConvertCurrentMovieToTasproj();
|
||||
var tasMovie = MovieSession.Movie.ToTasMovie();
|
||||
// No need to save new movie, as there are no changes.
|
||||
// User will save future changes if they want (potentially via auto-save).
|
||||
success = LoadMovie(tasMovie);
|
||||
}
|
||||
|
||||
|
@ -294,6 +298,16 @@ namespace BizHawk.Client.EmuHawk
|
|||
return true;
|
||||
}
|
||||
|
||||
private void ScheduleAutoSave(uint secondsUntil)
|
||||
{
|
||||
_autosaveTimer.Stop();
|
||||
if (secondsUntil != 0)
|
||||
{
|
||||
_autosaveTimer.Interval = (int)secondsUntil;
|
||||
_autosaveTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void AutosaveTimerEventProcessor(object sender, EventArgs e)
|
||||
{
|
||||
if (CurrentTasMovie == null)
|
||||
|
@ -307,28 +321,55 @@ namespace BizHawk.Client.EmuHawk
|
|||
return;
|
||||
}
|
||||
|
||||
FileWriteResult saveResult;
|
||||
if (Settings.AutosaveAsBackupFile)
|
||||
{
|
||||
if (Settings.AutosaveAsBk2)
|
||||
{
|
||||
SaveTas(saveAsBk2: true, saveBackup: true);
|
||||
saveResult = SaveTas(saveAsBk2: true, saveBackup: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
SaveTas(saveBackup: true);
|
||||
saveResult = SaveTas(saveBackup: true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Settings.AutosaveAsBk2)
|
||||
{
|
||||
SaveTas(saveAsBk2: true);
|
||||
saveResult = SaveTas(saveAsBk2: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
SaveTas();
|
||||
saveResult = SaveTas();
|
||||
}
|
||||
}
|
||||
|
||||
if (saveResult.IsError && _lastAutoSaveSuccess)
|
||||
{
|
||||
// Should we alert the user?
|
||||
// Let's try again once after a bit, then alert if it fails again.
|
||||
ScheduleAutoSave(60);
|
||||
}
|
||||
else if (saveResult.IsError)
|
||||
{
|
||||
_autosaveTimer.Stop();
|
||||
bool tryAgain = DialogController.ShowMessageBox2(
|
||||
$"Failed to auto-save. {saveResult.UserFriendlyErrorMessage()}\n{saveResult.Exception.Message}\n\nTry again?",
|
||||
"Error");
|
||||
if (tryAgain)
|
||||
{
|
||||
AutosaveTimerEventProcessor(null, null);
|
||||
return;
|
||||
}
|
||||
ScheduleAutoSave(Settings.AutosaveInterval);
|
||||
}
|
||||
else
|
||||
{
|
||||
ScheduleAutoSave(Settings.AutosaveInterval);
|
||||
}
|
||||
|
||||
_lastAutoSaveSuccess = !saveResult.IsError;
|
||||
}
|
||||
|
||||
private static readonly string[] N64CButtonSuffixes = { " C Up", " C Down", " C Left", " C Right" };
|
||||
|
@ -481,13 +522,6 @@ namespace BizHawk.Client.EmuHawk
|
|||
}
|
||||
}
|
||||
|
||||
private ITasMovie ConvertCurrentMovieToTasproj()
|
||||
{
|
||||
var tasMovie = MovieSession.Movie.ToTasMovie();
|
||||
tasMovie.Save(); // should this be done?
|
||||
return tasMovie;
|
||||
}
|
||||
|
||||
private bool LoadMovie(ITasMovie tasMovie, bool startsFromSavestate = false, int gotoFrame = 0)
|
||||
{
|
||||
_engaged = false;
|
||||
|
@ -694,12 +728,23 @@ namespace BizHawk.Client.EmuHawk
|
|||
$"{Game.FilesystemSafeName()}.{MovieService.TasMovieExtension}");
|
||||
}
|
||||
|
||||
private void SaveTas(bool saveAsBk2 = false, bool saveBackup = false)
|
||||
private void DisplayMessageIfFailed(Func<FileWriteResult> action, string message)
|
||||
{
|
||||
FileWriteResult result = action();
|
||||
if (result.IsError)
|
||||
{
|
||||
DialogController.ShowMessageBox(
|
||||
$"{message}\n{result.UserFriendlyErrorMessage()}\n{result.Exception.Message}",
|
||||
"Error",
|
||||
EMsgBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private FileWriteResult SaveTas(bool saveAsBk2 = false, bool saveBackup = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(CurrentTasMovie.Filename) || CurrentTasMovie.Filename == DefaultTasProjName())
|
||||
{
|
||||
SaveAsTas();
|
||||
return;
|
||||
return SaveAsTas();
|
||||
}
|
||||
|
||||
_autosaveTimer.Stop();
|
||||
|
@ -716,22 +761,29 @@ namespace BizHawk.Client.EmuHawk
|
|||
movieToSave.Attach(Emulator);
|
||||
}
|
||||
|
||||
FileWriteResult result;
|
||||
if (saveBackup)
|
||||
movieToSave.SaveBackup();
|
||||
result = movieToSave.SaveBackup();
|
||||
else
|
||||
movieToSave.Save();
|
||||
result = movieToSave.Save();
|
||||
|
||||
MessageStatusLabel.Text = saveBackup
|
||||
? $"Backup .{(saveAsBk2 ? MovieService.StandardMovieExtension : MovieService.TasMovieExtension)} saved to \"Movie backups\" path."
|
||||
: "File saved.";
|
||||
Cursor = Cursors.Default;
|
||||
if (Settings.AutosaveInterval > 0)
|
||||
if (!result.IsError)
|
||||
{
|
||||
_autosaveTimer.Start();
|
||||
MessageStatusLabel.Text = saveBackup
|
||||
? $"Backup .{(saveAsBk2 ? MovieService.StandardMovieExtension : MovieService.TasMovieExtension)} saved to \"Movie backups\" path."
|
||||
: "File saved.";
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageStatusLabel.Text = "Failed to save movie.";
|
||||
}
|
||||
|
||||
Cursor = Cursors.Default;
|
||||
ScheduleAutoSave(Settings.AutosaveInterval);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void SaveAsTas()
|
||||
private FileWriteResult SaveAsTas()
|
||||
{
|
||||
_autosaveTimer.Stop();
|
||||
|
||||
|
@ -747,25 +799,33 @@ namespace BizHawk.Client.EmuHawk
|
|||
TAStudioProjectsFSFilterSet,
|
||||
this);
|
||||
|
||||
FileWriteResult saveResult = null;
|
||||
if (fileInfo != null)
|
||||
{
|
||||
MessageStatusLabel.Text = "Saving...";
|
||||
MessageStatusLabel.Owner.Update();
|
||||
Cursor = Cursors.WaitCursor;
|
||||
CurrentTasMovie.Filename = fileInfo.FullName;
|
||||
CurrentTasMovie.Save();
|
||||
saveResult = CurrentTasMovie.Save();
|
||||
Settings.RecentTas.Add(CurrentTasMovie.Filename);
|
||||
MessageStatusLabel.Text = "File saved.";
|
||||
Cursor = Cursors.Default;
|
||||
}
|
||||
|
||||
if (Settings.AutosaveInterval > 0)
|
||||
{
|
||||
_autosaveTimer.Start();
|
||||
if (!saveResult.IsError)
|
||||
{
|
||||
MessageStatusLabel.Text = "File saved.";
|
||||
ScheduleAutoSave(Settings.AutosaveInterval);
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageStatusLabel.Text = "Failed to save.";
|
||||
}
|
||||
}
|
||||
|
||||
UpdateWindowTitle(); // changing the movie's filename does not flag changes, so we need to ensure the window title is always updated
|
||||
MainForm.UpdateWindowTitle();
|
||||
|
||||
if (fileInfo != null) return saveResult;
|
||||
else return new(); // user cancelled, so we were successful in not saving
|
||||
}
|
||||
|
||||
protected override string WindowTitle
|
||||
|
|
|
@ -1008,7 +1008,13 @@ namespace BizHawk.Client.EmuHawk
|
|||
|
||||
if (!string.IsNullOrWhiteSpace(watches.CurrentFileName))
|
||||
{
|
||||
if (watches.Save())
|
||||
FileWriteResult saveResult = watches.Save();
|
||||
if (saveResult.IsError)
|
||||
{
|
||||
this.ErrorMessageBox(saveResult);
|
||||
MessageLabel.Text = $"Failed to save {Path.GetFileName(_currentFileName)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentFileName = watches.CurrentFileName;
|
||||
MessageLabel.Text = $"{Path.GetFileName(_currentFileName)} saved";
|
||||
|
@ -1017,11 +1023,20 @@ namespace BizHawk.Client.EmuHawk
|
|||
}
|
||||
else
|
||||
{
|
||||
var result = watches.SaveAs(GetWatchSaveFileFromUser(CurrentFileName()));
|
||||
if (result)
|
||||
FileInfo/*?*/ file = GetWatchSaveFileFromUser(CurrentFileName());
|
||||
if (file != null)
|
||||
{
|
||||
MessageLabel.Text = $"{Path.GetFileName(_currentFileName)} saved";
|
||||
Settings.RecentSearches.Add(watches.CurrentFileName);
|
||||
var result = watches.SaveAs(file);
|
||||
if (result.IsError)
|
||||
{
|
||||
this.ErrorMessageBox(result);
|
||||
MessageLabel.Text = $"Failed to save {Path.GetFileName(_currentFileName)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageLabel.Text = $"{Path.GetFileName(_currentFileName)} saved";
|
||||
Settings.RecentSearches.Add(watches.CurrentFileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1035,7 +1050,15 @@ namespace BizHawk.Client.EmuHawk
|
|||
watches.Add(_searches[i]);
|
||||
}
|
||||
|
||||
if (watches.SaveAs(GetWatchSaveFileFromUser(CurrentFileName())))
|
||||
FileInfo/*?*/ file = GetWatchSaveFileFromUser(CurrentFileName());
|
||||
if (file == null) return;
|
||||
FileWriteResult result = watches.SaveAs(file);
|
||||
if (result.IsError)
|
||||
{
|
||||
this.ErrorMessageBox(result);
|
||||
MessageLabel.Text = $"Failed to save {Path.GetFileName(_currentFileName)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentFileName = watches.CurrentFileName;
|
||||
MessageLabel.Text = $"{Path.GetFileName(_currentFileName)} saved";
|
||||
|
|
|
@ -213,15 +213,8 @@ namespace BizHawk.Client.EmuHawk
|
|||
if (result is null) return false;
|
||||
if (result.Value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_watches.CurrentFileName))
|
||||
{
|
||||
SaveAs();
|
||||
}
|
||||
else
|
||||
{
|
||||
_watches.Save();
|
||||
Config.RecentWatches.Add(_watches.CurrentFileName);
|
||||
}
|
||||
TryAgainResult saveResult = this.DoWithTryAgainBox(Save, "Failed to save watch list.");
|
||||
return saveResult != TryAgainResult.Canceled;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -591,14 +584,47 @@ namespace BizHawk.Client.EmuHawk
|
|||
: Game.FilesystemSafeName();
|
||||
}
|
||||
|
||||
private void SaveAs()
|
||||
private FileWriteResult SaveAs()
|
||||
{
|
||||
var result = _watches.SaveAs(GetWatchSaveFileFromUser(CurrentFileName()));
|
||||
if (result)
|
||||
FileInfo/*?*/ file = GetWatchSaveFileFromUser(CurrentFileName());
|
||||
if (file == null) return new();
|
||||
|
||||
FileWriteResult result = _watches.SaveAs(file);
|
||||
if (result.IsError)
|
||||
{
|
||||
UpdateStatusBar(saved: true);
|
||||
Config.RecentWatches.Add(_watches.CurrentFileName);
|
||||
MessageLabel.Text = $"Failed to save {Path.GetFileName(_watches.CurrentFileName)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageLabel.Text = $"{Path.GetFileName(_watches.CurrentFileName)} saved";
|
||||
Config.RecentWatches.Add(_watches.CurrentFileName);
|
||||
UpdateStatusBar(saved: true);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private FileWriteResult Save()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_watches.CurrentFileName))
|
||||
{
|
||||
return SaveAs();
|
||||
}
|
||||
else
|
||||
{
|
||||
FileWriteResult saveResult = _watches.Save();
|
||||
if (saveResult.IsError)
|
||||
{
|
||||
MessageLabel.Text = $"Failed to save {Path.GetFileName(_watches.CurrentFileName)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageLabel.Text = $"{Path.GetFileName(_watches.CurrentFileName)} saved";
|
||||
Config.RecentWatches.Add(_watches.CurrentFileName);
|
||||
UpdateStatusBar(saved: true);
|
||||
}
|
||||
return saveResult;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void SaveConfigSettings()
|
||||
|
@ -727,23 +753,20 @@ namespace BizHawk.Client.EmuHawk
|
|||
|
||||
private void SaveMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_watches.CurrentFileName))
|
||||
FileWriteResult saveResult = Save();
|
||||
if (saveResult.IsError)
|
||||
{
|
||||
if (_watches.Save())
|
||||
{
|
||||
Config.RecentWatches.Add(_watches.CurrentFileName);
|
||||
UpdateStatusBar(saved: true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SaveAs();
|
||||
this.ErrorMessageBox(saveResult, "Failed to save watch list.");
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveAsMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
SaveAs();
|
||||
FileWriteResult saveResult = SaveAs();
|
||||
if (saveResult.IsError)
|
||||
{
|
||||
this.ErrorMessageBox(saveResult, "Failed to save watch list.");
|
||||
}
|
||||
}
|
||||
|
||||
private void RecentSubMenu_DropDownOpened(object sender, EventArgs e)
|
||||
|
|
|
@ -55,13 +55,17 @@ namespace BizHawk.Emulation.Common
|
|||
int pos = 0;
|
||||
for (int i = 0; i < _numCores; i++)
|
||||
{
|
||||
var toCopy = _linkedCores[i].AsSaveRam().CloneSaveRam(); // wait CloneSaveRam is already a copy, why are we copying it again
|
||||
if (toCopy is null) continue;
|
||||
var b = new byte[toCopy.Length];
|
||||
var numberBytesToCopy = _linkedCores[i].AsSaveRam().CloneSaveRam()?.Length;
|
||||
if (numberBytesToCopy is null) continue;
|
||||
var b = new byte[numberBytesToCopy.Value];
|
||||
Buffer.BlockCopy(data, pos, b, 0, b.Length);
|
||||
pos += b.Length;
|
||||
_linkedCores[i].AsSaveRam().StoreSaveRam(b);
|
||||
}
|
||||
|
||||
if (data.Length != pos) throw new InvalidOperationException("Incorrect sram size.");
|
||||
}
|
||||
|
||||
public bool SupportsSaveRam => true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -134,7 +134,7 @@ namespace BizHawk.Emulation.Common
|
|||
|
||||
public static bool HasSaveRam(this IEmulator core)
|
||||
{
|
||||
return core != null && core.ServiceProvider.HasService<ISaveRam>();
|
||||
return core != null && core.ServiceProvider.HasService<ISaveRam>() && core.AsSaveRam()!.SupportsSaveRam;
|
||||
}
|
||||
|
||||
public static ISaveRam AsSaveRam(this IEmulator core)
|
||||
|
|
|
@ -10,16 +10,16 @@
|
|||
{
|
||||
/// <summary>
|
||||
/// Returns a copy of the SaveRAM. Editing it won't do you any good unless you later call StoreSaveRam()
|
||||
/// This IS allowed to return null.
|
||||
/// Unfortunately, the core may think differently of a nonexisting (null) saveram vs a 0 size saveram.
|
||||
/// Frontend users of the ISaveRam should treat null as nonexisting (and thus not even write the file, so that the "does not exist" condition can be roundtripped and not confused with an empty file)
|
||||
/// This method must return null if and only if <see cref="SupportsSaveRam"/> is false.
|
||||
/// </summary>
|
||||
/// <param name="clearDirty">Whether the saveram should be considered in a clean state after this call for purposes of <see cref="SaveRamModified"/></param>
|
||||
byte[]? CloneSaveRam(bool clearDirty = true);
|
||||
|
||||
/// <summary>
|
||||
/// store new SaveRAM to the emu core. the data should be the same size as the return from ReadSaveRam()
|
||||
/// Store new SaveRAM to the emu core.
|
||||
/// The core must ignore calls to this method if <see cref="SupportsSaveRam"/> is false.
|
||||
/// </summary>
|
||||
/// <exception cref="Exception">The core may throw an exception if the given data is invalid.</exception>
|
||||
void StoreSaveRam(byte[] data);
|
||||
|
||||
/// <summary>
|
||||
|
@ -28,5 +28,10 @@
|
|||
/// This value should be considered a hint more than an absolute truth.
|
||||
/// </summary>
|
||||
bool SaveRamModified { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Certain cores may support SaveRam only in certain situations, for example only for certain games.
|
||||
/// </summary>
|
||||
bool SupportsSaveRam { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@ namespace BizHawk.Emulation.Cores.Arcades.MAME
|
|||
|
||||
public bool SaveRamModified => _nvramFilenames.Count > 0;
|
||||
|
||||
public bool SupportsSaveRam => _nvramFilenames.Count != 0;
|
||||
|
||||
public byte[] CloneSaveRam(bool clearDirty)
|
||||
{
|
||||
if (_nvramFilenames.Count == 0)
|
||||
|
|
|
@ -15,6 +15,7 @@ namespace BizHawk.Emulation.Cores.Computers.AppleII
|
|||
}
|
||||
|
||||
public bool SaveRamModified => true;
|
||||
public bool SupportsSaveRam => true;
|
||||
|
||||
public byte[] CloneSaveRam(bool clearDirty)
|
||||
{
|
||||
|
|
|
@ -302,11 +302,15 @@ namespace BizHawk.Emulation.Cores.Computers.Commodore64.Cartridge
|
|||
var deltaBSize = reader.ReadInt32();
|
||||
_deltaB = reader.ReadBytes(deltaBSize);
|
||||
|
||||
if (reader.BaseStream.Position != deltaASize + deltaBSize + 8) throw new InvalidOperationException("Incorrect sram size.");
|
||||
|
||||
DeltaSerializer.ApplyDelta(_originalMediaA, _chipA.Data, _deltaA);
|
||||
DeltaSerializer.ApplyDelta(_originalMediaB, _chipB.Data, _deltaB);
|
||||
_saveRamDirty = false;
|
||||
}
|
||||
|
||||
public bool SaveRamModified => _saveRamDirty;
|
||||
|
||||
public bool SupportsSaveRam => true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,8 @@ public sealed partial class Drive1541 : ISaveRam
|
|||
|
||||
public bool SaveRamModified { get; private set; } = false;
|
||||
|
||||
public bool SupportsSaveRam => true;
|
||||
|
||||
public byte[] CloneSaveRam(bool clearDirty)
|
||||
{
|
||||
SaveDeltas();
|
||||
|
|
|
@ -13,12 +13,15 @@ namespace BizHawk.Emulation.Cores.Computers.MSX
|
|||
{
|
||||
if (SaveRAM != null)
|
||||
{
|
||||
if (data.Length != SaveRAM.Length) throw new InvalidOperationException("Incorrect sram size.");
|
||||
Array.Copy(data, SaveRAM, data.Length);
|
||||
}
|
||||
}
|
||||
|
||||
public bool SaveRamModified { get; private set; }
|
||||
|
||||
public bool SupportsSaveRam => SaveRAM != null;
|
||||
|
||||
public byte[] SaveRAM;
|
||||
private byte SaveRamBank;
|
||||
}
|
||||
|
|
|
@ -11,9 +11,12 @@ namespace BizHawk.Emulation.Cores.Atari.A7800Hawk
|
|||
|
||||
public void StoreSaveRam(byte[] data)
|
||||
{
|
||||
if (data.Length != _hsram.Length) throw new InvalidOperationException("Incorrect sram size.");
|
||||
Buffer.BlockCopy(data, 0, _hsram, 0, data.Length);
|
||||
}
|
||||
|
||||
public bool SaveRamModified => (_hsbios != null);
|
||||
|
||||
public bool SupportsSaveRam => true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ namespace BizHawk.Emulation.Cores.Atari.Jaguar
|
|||
|
||||
public new bool SaveRamModified => _saveRamSize > 0 && _core.SaveRamIsDirty();
|
||||
|
||||
public new bool SupportsSaveRam => _saveRamSize != 0;
|
||||
|
||||
public new byte[] CloneSaveRam(bool clearDirty)
|
||||
{
|
||||
if (_saveRamSize == 0)
|
||||
|
|
|
@ -22,7 +22,7 @@ namespace BizHawk.Emulation.Cores.Atari.Lynx
|
|||
{
|
||||
if (!LibLynx.GetSaveRamPtr(Core, out var size, out var data))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
return;
|
||||
}
|
||||
|
||||
if (srcData.Length != size) throw new ArgumentException(message: "buffer too small", paramName: nameof(srcData));
|
||||
|
@ -31,5 +31,7 @@ namespace BizHawk.Emulation.Cores.Atari.Lynx
|
|||
}
|
||||
|
||||
public bool SaveRamModified => LibLynx.GetSaveRamPtr(Core, out int unused, out IntPtr unused2);
|
||||
|
||||
public bool SupportsSaveRam => LibLynx.GetSaveRamPtr(Core, out int _, out IntPtr _);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,5 +15,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Vectrex
|
|||
}
|
||||
|
||||
public bool SaveRamModified => false;
|
||||
|
||||
public bool SupportsSaveRam => false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,13 +11,16 @@ namespace BizHawk.Emulation.Cores.Consoles.O2Hawk
|
|||
|
||||
public void StoreSaveRam(byte[] data)
|
||||
{
|
||||
if (_syncSettings.Use_SRAM)
|
||||
if (cart_RAM != null && _syncSettings.Use_SRAM)
|
||||
{
|
||||
if (data.Length != cart_RAM.Length) throw new InvalidOperationException("Incorrect sram size.");
|
||||
Buffer.BlockCopy(data, 0, cart_RAM, 0, data.Length);
|
||||
Console.WriteLine("loading SRAM here");
|
||||
}
|
||||
}
|
||||
|
||||
public bool SaveRamModified => has_bat & _syncSettings.Use_SRAM;
|
||||
|
||||
public bool SupportsSaveRam => cart_RAM != null; // The Use_SRAM setting implements behavior not officially supported by BizHawk.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ namespace BizHawk.Emulation.Cores.Nintendo.BSNES
|
|||
|
||||
public bool SaveRamModified => _saveRamSize != 0;
|
||||
|
||||
public bool SupportsSaveRam => _saveRamSize != 0;
|
||||
|
||||
public byte[] CloneSaveRam(bool clearDirty)
|
||||
{
|
||||
if (_saveRamSize == 0) return null;
|
||||
|
|
|
@ -33,6 +33,8 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBA
|
|||
|
||||
public void StoreSaveRam(byte[] data)
|
||||
{
|
||||
if (!SupportsSaveRam) return;
|
||||
|
||||
if (data.AsSpan().Slice(0, 8).SequenceEqual(_legacyHeader))
|
||||
{
|
||||
data = LegacyFix(data);
|
||||
|
@ -43,6 +45,17 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBA
|
|||
|
||||
public bool SaveRamModified => LibmGBA.BizGetSaveRam(Core, _saveScratch, _saveScratch.Length) > 0;
|
||||
|
||||
public bool SupportsSaveRam
|
||||
{
|
||||
get
|
||||
{
|
||||
// Is all of this necessary? Someone who knows how the core works might know.
|
||||
int len = LibmGBA.BizGetSaveRam(Core, _saveScratch, _saveScratch.Length);
|
||||
len = TruncateRTCIfUsingDeterministicTime(len);
|
||||
return len != 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] LegacyFix(byte[] saveram)
|
||||
{
|
||||
// at one point vbanext-hawk had a special saveram format which we want to load.
|
||||
|
|
|
@ -11,13 +11,16 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawk
|
|||
|
||||
public void StoreSaveRam(byte[] data)
|
||||
{
|
||||
if (_syncSettings.Use_SRAM)
|
||||
if (cart_RAM != null && _syncSettings.Use_SRAM)
|
||||
{
|
||||
if (data.Length != cart_RAM.Length) throw new InvalidOperationException("Incorrect sram size.");
|
||||
Buffer.BlockCopy(data, 0, cart_RAM, 0, data.Length);
|
||||
Console.WriteLine("loading SRAM here");
|
||||
}
|
||||
}
|
||||
|
||||
public bool SaveRamModified => has_bat & _syncSettings.Use_SRAM;
|
||||
|
||||
public bool SupportsSaveRam => cart_RAM != null; // The Use_SRAM setting implements behavior not officially supported by BizHawk.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,14 +54,17 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink
|
|||
{
|
||||
if (L.cart_RAM != null && R.cart_RAM == null)
|
||||
{
|
||||
if (data.Length != L.cart_RAM.Length) throw new InvalidOperationException("Incorrect sram size.");
|
||||
Buffer.BlockCopy(data, 0, L.cart_RAM, 0, L.cart_RAM.Length);
|
||||
}
|
||||
else if (R.cart_RAM != null && L.cart_RAM == null)
|
||||
{
|
||||
if (data.Length != R.cart_RAM.Length) throw new InvalidOperationException("Incorrect sram size.");
|
||||
Buffer.BlockCopy(data, 0, R.cart_RAM, 0, R.cart_RAM.Length);
|
||||
}
|
||||
else if (R.cart_RAM != null && L.cart_RAM != null)
|
||||
{
|
||||
if (data.Length != L.cart_RAM.Length + R.cart_RAM.Length) throw new InvalidOperationException("Incorrect sram size.");
|
||||
Buffer.BlockCopy(data, 0, L.cart_RAM, 0, L.cart_RAM.Length);
|
||||
Buffer.BlockCopy(data, L.cart_RAM.Length, R.cart_RAM, 0, R.cart_RAM.Length);
|
||||
}
|
||||
|
@ -71,5 +74,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink
|
|||
}
|
||||
|
||||
public bool SaveRamModified => (L.has_bat || R.has_bat) & linkSyncSettings.Use_SRAM;
|
||||
|
||||
public bool SupportsSaveRam => L.cart_RAM != null || R.cart_RAM != null; // The Use_SRAM setting implements behavior not officially supported by BizHawk.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,12 +84,17 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink3x
|
|||
if (R.cart_RAM != null)
|
||||
{
|
||||
Buffer.BlockCopy(data, temp, R.cart_RAM, 0, R.cart_RAM.Length);
|
||||
temp += R.cart_RAM.Length;
|
||||
}
|
||||
|
||||
if (data.Length != temp) throw new InvalidOperationException("Incorrect sram size.");
|
||||
|
||||
Console.WriteLine("loading SRAM here");
|
||||
}
|
||||
}
|
||||
|
||||
public bool SaveRamModified => (L.has_bat || C.has_bat || R.has_bat) & Link3xSyncSettings.Use_SRAM;
|
||||
|
||||
public bool SupportsSaveRam => L.cart_RAM != null || C.cart_RAM != null || R.cart_RAM != null; // The Use_SRAM setting implements behavior not officially supported by BizHawk.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,12 +105,17 @@ namespace BizHawk.Emulation.Cores.Nintendo.GBHawkLink4x
|
|||
if (D.cart_RAM != null)
|
||||
{
|
||||
Buffer.BlockCopy(data, temp, D.cart_RAM, 0, D.cart_RAM.Length);
|
||||
temp += D.cart_RAM.Length;
|
||||
}
|
||||
|
||||
if (data.Length != temp) throw new InvalidOperationException("Incorrect sram size.");
|
||||
|
||||
Console.WriteLine("loading SRAM here");
|
||||
}
|
||||
}
|
||||
|
||||
public bool SaveRamModified => (A.has_bat || B.has_bat || C.has_bat || D.has_bat) & Link4xSyncSettings.Use_SRAM;
|
||||
|
||||
public bool SupportsSaveRam => A.cart_RAM != null || B.cart_RAM != null || C.cart_RAM != null || D.cart_RAM != null; // The Use_SRAM setting implements behavior not officially supported by BizHawk.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
|
|||
// need to wire more stuff into the core to actually know this
|
||||
public bool SaveRamModified => LibGambatte.gambatte_getsavedatalength(GambatteState) != 0;
|
||||
|
||||
public bool SupportsSaveRam => LibGambatte.gambatte_getsavedatalength(GambatteState) != 0;
|
||||
|
||||
public byte[] CloneSaveRam(bool clearDirty)
|
||||
{
|
||||
var length = LibGambatte.gambatte_getsavedatalength(GambatteState);
|
||||
|
@ -24,6 +26,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
|
|||
public void StoreSaveRam(byte[] data)
|
||||
{
|
||||
var expected = LibGambatte.gambatte_getsavedatalength(GambatteState);
|
||||
if (expected == 0) return;
|
||||
if (data.Length != expected) throw new ArgumentException(message: "Size of saveram data does not match expected!", paramName: nameof(data));
|
||||
|
||||
LibGambatte.gambatte_loadsavedata(GambatteState, data);
|
||||
|
|
|
@ -15,5 +15,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.N64
|
|||
}
|
||||
|
||||
public bool SaveRamModified => true;
|
||||
|
||||
public bool SupportsSaveRam => true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS
|
|||
|
||||
public new bool SaveRamModified => IsDSiWare ? DSiWareSaveLength != 0 : _core.SaveRamIsDirty();
|
||||
|
||||
public new bool SupportsSaveRam => IsDSiWare ? DSiWareSaveLength != 0 : _core.GetSaveRamLength(_console) != 0;
|
||||
|
||||
public new byte[] CloneSaveRam(bool clearDirty)
|
||||
{
|
||||
if (IsDSiWare)
|
||||
|
@ -54,6 +56,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS
|
|||
{
|
||||
if (IsDSiWare)
|
||||
{
|
||||
if (DSiWareSaveLength == 0) return;
|
||||
if (data.Length == DSiWareSaveLength)
|
||||
{
|
||||
if (PublicSavSize > 0) _exe.AddReadonlyFile(data.AsSpan().Slice(0, PublicSavSize).ToArray(), "public.sav");
|
||||
|
@ -66,6 +69,10 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS
|
|||
if (PrivateSavSize > 0) _exe.RemoveReadonlyFile("private.sav");
|
||||
if (BannerSavSize > 0) _exe.RemoveReadonlyFile("banner.sav");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Incorrect sram size.");
|
||||
}
|
||||
}
|
||||
else if (data.Length > 0)
|
||||
{
|
||||
|
|
|
@ -15,6 +15,17 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES
|
|||
}
|
||||
}
|
||||
|
||||
public bool SupportsSaveRam
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Board == null) return false;
|
||||
if (Board is FDS) return true;
|
||||
if (Board.SaveRam == null) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] CloneSaveRam(bool clearDirty)
|
||||
{
|
||||
if (Board is FDS fds)
|
||||
|
@ -38,6 +49,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES
|
|||
return;
|
||||
}
|
||||
|
||||
if (data.Length != Board.SaveRam.Length) throw new InvalidOperationException("Incorrect sram size.");
|
||||
Array.Copy(data, Board.SaveRam, data.Length);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.QuickNES
|
|||
|
||||
public bool SaveRamModified => QN.qn_has_battery_ram(Context);
|
||||
|
||||
public bool SupportsSaveRam => QN.qn_has_battery_ram(Context);
|
||||
|
||||
private byte[] _saveRamBuff;
|
||||
|
||||
private void InitSaveRamBuff()
|
||||
|
|
|
@ -11,6 +11,10 @@ namespace BizHawk.Emulation.Cores.Nintendo.SNES
|
|||
Api.QUERY_get_memory_size(LibsnesApi.SNES_MEMORY.CARTRIDGE_RAM) != 0
|
||||
|| Api.QUERY_get_memory_size(LibsnesApi.SNES_MEMORY.SGB_CARTRAM) != 0;
|
||||
|
||||
public bool SupportsSaveRam =>
|
||||
Api.QUERY_get_memory_data(LibsnesApi.SNES_MEMORY.CARTRIDGE_RAM) != null
|
||||
|| Api.QUERY_get_memory_data(LibsnesApi.SNES_MEMORY.SGB_CARTRAM) != null;
|
||||
|
||||
public byte[] CloneSaveRam(bool clearDirty)
|
||||
{
|
||||
using (Api.EnterExit())
|
||||
|
@ -46,7 +50,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.SNES
|
|||
size = Api.QUERY_get_memory_size(LibsnesApi.SNES_MEMORY.SGB_CARTRAM);
|
||||
}
|
||||
|
||||
if (size == 0)
|
||||
if (size == 0 || buf == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ namespace BizHawk.Emulation.Cores.Nintendo.Sameboy
|
|||
{
|
||||
public bool SaveRamModified => LibSameboy.sameboy_sramlen(SameboyState) != 0;
|
||||
|
||||
public bool SupportsSaveRam => LibSameboy.sameboy_sramlen(SameboyState) != 0;
|
||||
|
||||
public byte[] CloneSaveRam(bool clearDirty)
|
||||
{
|
||||
int length = LibSameboy.sameboy_sramlen(SameboyState);
|
||||
|
@ -23,12 +25,10 @@ namespace BizHawk.Emulation.Cores.Nintendo.Sameboy
|
|||
public void StoreSaveRam(byte[] data)
|
||||
{
|
||||
int expected = LibSameboy.sameboy_sramlen(SameboyState);
|
||||
if (expected == 0) return;
|
||||
if (data.Length != expected) throw new ArgumentException(message: "Size of saveram data does not match expected!", paramName: nameof(data));
|
||||
|
||||
if (expected > 0)
|
||||
{
|
||||
LibSameboy.sameboy_loadsram(SameboyState, data, data.Length);
|
||||
}
|
||||
LibSameboy.sameboy_loadsram(SameboyState, data, data.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ namespace BizHawk.Emulation.Cores.PCEngine
|
|||
{
|
||||
public bool SaveRamModified { get; private set; }
|
||||
|
||||
public bool SupportsSaveRam => BRAM != null;
|
||||
|
||||
public byte[] CloneSaveRam(bool clearDirty)
|
||||
{
|
||||
if (clearDirty) SaveRamModified = false;
|
||||
|
@ -16,6 +18,7 @@ namespace BizHawk.Emulation.Cores.PCEngine
|
|||
{
|
||||
if (BRAM != null)
|
||||
{
|
||||
if (data.Length != BRAM.Length) throw new InvalidOperationException("Incorrect sram size.");
|
||||
Array.Copy(data, BRAM, data.Length);
|
||||
}
|
||||
|
||||
|
|
|
@ -51,6 +51,8 @@ namespace BizHawk.Emulation.Cores.Consoles.SNK
|
|||
}
|
||||
}
|
||||
|
||||
public new bool SupportsSaveRam => true;
|
||||
|
||||
public new byte[] CloneSaveRam(bool clearDirty)
|
||||
{
|
||||
_exe.AddTransientFile(new byte[0], "SAV:flash");
|
||||
|
|
|
@ -52,14 +52,17 @@ namespace BizHawk.Emulation.Cores.Sega.GGHawkLink
|
|||
{
|
||||
if (L.SaveRAM != null && R.SaveRAM == null)
|
||||
{
|
||||
if (data.Length != L.SaveRAM.Length) throw new InvalidOperationException("Incorrect sram size.");
|
||||
Buffer.BlockCopy(data, 0, L.SaveRAM, 0, L.SaveRAM.Length);
|
||||
}
|
||||
else if (R.SaveRAM != null && L.SaveRAM == null)
|
||||
{
|
||||
if (data.Length != R.SaveRAM.Length) throw new InvalidOperationException("Incorrect sram size.");
|
||||
Buffer.BlockCopy(data, 0, R.SaveRAM, 0, R.SaveRAM.Length);
|
||||
}
|
||||
else if (R.SaveRAM != null && L.SaveRAM != null)
|
||||
{
|
||||
if (data.Length != L.SaveRAM.Length + R.SaveRAM.Length) throw new InvalidOperationException("Incorrect sram size.");
|
||||
Buffer.BlockCopy(data, 0, L.SaveRAM, 0, L.SaveRAM.Length);
|
||||
Buffer.BlockCopy(data, L.SaveRAM.Length, R.SaveRAM, 0, R.SaveRAM.Length);
|
||||
}
|
||||
|
@ -68,5 +71,7 @@ namespace BizHawk.Emulation.Cores.Sega.GGHawkLink
|
|||
}
|
||||
|
||||
public bool SaveRamModified => linkSyncSettings.Use_SRAM;
|
||||
|
||||
public bool SupportsSaveRam => L.SaveRAM != null || R.SaveRAM != null; // The Use_SRAM setting implements behavior not officially supported by BizHawk.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ namespace BizHawk.Emulation.Cores.Sega.MasterSystem
|
|||
{
|
||||
if (SaveRAM != null)
|
||||
{
|
||||
if (data.Length != SaveRAM.Length) throw new InvalidOperationException("Incorrect sram size.");
|
||||
Array.Copy(data, SaveRAM, data.Length);
|
||||
}
|
||||
|
||||
|
@ -22,6 +23,8 @@ namespace BizHawk.Emulation.Cores.Sega.MasterSystem
|
|||
|
||||
public bool SaveRamModified { get; private set; }
|
||||
|
||||
public bool SupportsSaveRam => SaveRAM != null;
|
||||
|
||||
public byte[] SaveRAM;
|
||||
private byte SaveRamBank;
|
||||
}
|
||||
|
|
|
@ -118,6 +118,8 @@ namespace BizHawk.Emulation.Cores.Consoles.Sega.Saturn
|
|||
|
||||
public new bool SaveRamModified => true;
|
||||
|
||||
public new bool SupportsSaveRam => true;
|
||||
|
||||
public new byte[] CloneSaveRam(bool clearDirty)
|
||||
{
|
||||
var data = new byte[_saturnus.GetSaveRamLength()];
|
||||
|
@ -126,7 +128,9 @@ namespace BizHawk.Emulation.Cores.Consoles.Sega.Saturn
|
|||
}
|
||||
|
||||
public new void StoreSaveRam(byte[] data)
|
||||
=> _saturnus.PutSaveRam(data, data.Length);
|
||||
{
|
||||
_saturnus.PutSaveRam(data, data.Length);
|
||||
}
|
||||
|
||||
public bool IsSTV => _isArcade;
|
||||
}
|
||||
|
|
|
@ -33,6 +33,11 @@ namespace BizHawk.Emulation.Cores.Consoles.Sega.gpgx
|
|||
return;
|
||||
}
|
||||
|
||||
if (!SupportsSaveRam)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Core.gpgx_put_sram(data, data.Length))
|
||||
{
|
||||
throw new Exception("Core rejected saveram");
|
||||
|
@ -48,5 +53,15 @@ namespace BizHawk.Emulation.Cores.Consoles.Sega.gpgx
|
|||
return size > 0 && area != IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
public bool SupportsSaveRam
|
||||
{
|
||||
get
|
||||
{
|
||||
var size = 0;
|
||||
var area = Core.gpgx_get_sram(ref size);
|
||||
return size == 0 || area == IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -986,6 +986,8 @@ namespace BizHawk.Emulation.Cores.Sony.PSX
|
|||
}
|
||||
}
|
||||
|
||||
public bool SupportsSaveRam => true;
|
||||
|
||||
|
||||
//THIS IS STILL AWFUL
|
||||
|
||||
|
|
|
@ -30,5 +30,7 @@ namespace BizHawk.Emulation.Cores.WonderSwan
|
|||
}
|
||||
|
||||
public bool SaveRamModified => BizSwan.bizswan_saveramsize(Core) > 0;
|
||||
|
||||
public bool SupportsSaveRam => true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ namespace BizHawk.Emulation.Cores.Libretro
|
|||
|
||||
public bool SaveRamModified => _saveramSize > 0;
|
||||
|
||||
public bool SupportsSaveRam => _saveramSize > 0;
|
||||
|
||||
public byte[] CloneSaveRam(bool clearDirty)
|
||||
{
|
||||
if (_saveramSize > 0)
|
||||
|
@ -39,6 +41,8 @@ namespace BizHawk.Emulation.Cores.Libretro
|
|||
Marshal.Copy(data, index, m.Data, (int)m.Size);
|
||||
index += (int)m.Size;
|
||||
}
|
||||
|
||||
if (data.Length != index) throw new InvalidOperationException("Incorrect sram size.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -158,6 +158,8 @@ namespace BizHawk.Emulation.Cores.Waterbox
|
|||
}
|
||||
}
|
||||
|
||||
public bool SupportsSaveRam => _saveramSize != 0;
|
||||
|
||||
public byte[] CloneSaveRam(bool clearDirty)
|
||||
{
|
||||
if (_saveramSize == 0)
|
||||
|
@ -176,6 +178,8 @@ namespace BizHawk.Emulation.Cores.Waterbox
|
|||
|
||||
public void StoreSaveRam(byte[] data)
|
||||
{
|
||||
if (!SupportsSaveRam) return;
|
||||
|
||||
// Checking if the size of the SaveRAM provided coincides with that expected. This is important for cores whose SaveRAM size can vary depending on their configuration.
|
||||
if (data.Length != _saveramSize)
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue