Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .global.editorconfig.ini
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,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
Expand Down
14 changes: 12 additions & 2 deletions src/BizHawk.Client.Common/Api/Classes/EmuClientApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ private void CallStateSaved(object sender, StateSavedEventArgs args)

public void CloseEmulator(int? exitCode = null) => _mainForm.CloseEmulator(exitCode);

public void CloseRom() => _mainForm.CloseRom();
public void CloseRom() => _mainForm.LoadNullRom();

public void DisplayMessages(bool value) => _config.DisplayMessages = value;

Expand Down Expand Up @@ -158,9 +158,19 @@ public bool OpenRom(string path)

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;

Expand Down
4 changes: 3 additions & 1 deletion src/BizHawk.Client.Common/Api/Classes/MovieApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public string GetInputAsMnemonic(int frame)
return Bk2LogEntryGenerator.GenerateLogEntry(_movieSession.Movie.GetInputState(frame));
}

// TODO: Change return type to FileWriteResult
public void Save(string filename)
{
if (_movieSession.Movie.NotActive())
Expand All @@ -69,7 +70,8 @@ public void Save(string filename)
}
_movieSession.Movie.Filename = filename;
}
_movieSession.Movie.Save();
FileWriteResult result = _movieSession.Movie.Save();
if (result.Exception != null) throw result.Exception;
}

public IReadOnlyDictionary<string, string> GetHeader()
Expand Down
17 changes: 15 additions & 2 deletions src/BizHawk.Client.Common/Api/Classes/SaveStateApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,17 @@ public bool LoadSlot(int slotNum, bool suppressOSD)
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);
Expand All @@ -49,7 +58,11 @@ public void SaveSlot(int slotNum, bool suppressOSD)
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;
}
}
}
}
49 changes: 49 additions & 0 deletions src/BizHawk.Client.Common/DialogControllerExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
#nullable enable

using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

namespace BizHawk.Client.Common
{
public enum TryAgainResult
{
Saved,
IgnoredFailure,
Canceled,
}

public static class DialogControllerExtensions
{
public static void AddOnScreenMessage(
Expand Down Expand Up @@ -64,6 +72,47 @@ public static bool ModalMessageBox2(
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>
Expand Down
131 changes: 131 additions & 0 deletions src/BizHawk.Client.Common/FileWriteResult.cs
Original file line number Diff line number Diff line change
@@ -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) { }
}
}
Loading
Loading