diff --git a/PKHeX.Core/Saves/SAV3GCMemoryCard.cs b/PKHeX.Core/Saves/SAV3GCMemoryCard.cs
index 4d5ce7d6d..3ccc63d50 100644
--- a/PKHeX.Core/Saves/SAV3GCMemoryCard.cs
+++ b/PKHeX.Core/Saves/SAV3GCMemoryCard.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Text;
using static System.Buffers.Binary.BinaryPrimitives;
@@ -242,7 +242,8 @@ public sealed class SAV3GCMemoryCard
SaveGameCount = 0;
var gameCode = EncodingType.GetString(Data, offset, 4);
- var ver = SaveHandlerGCI.GetGameCode(gameCode);
+ var header = Data.AsSpan(0, 4);
+ var ver = SaveHandlerGCI.GetGameCode(header);
if (ver == GameVersion.COLO)
{
if (HasCOLO) // another entry already exists
diff --git a/PKHeX.Core/Saves/Util/Recognition/ISaveHandler.cs b/PKHeX.Core/Saves/Util/Recognition/ISaveHandler.cs
index f3b56fa6a..03260bbcb 100644
--- a/PKHeX.Core/Saves/Util/Recognition/ISaveHandler.cs
+++ b/PKHeX.Core/Saves/Util/Recognition/ISaveHandler.cs
@@ -1,4 +1,6 @@
-namespace PKHeX.Core;
+using System;
+
+namespace PKHeX.Core;
#if !(EXCLUDE_EMULATOR_FORMATS && EXCLUDE_HACKS)
///
/// Provides handling for recognizing atypical save file formats.
@@ -17,7 +19,7 @@ public interface ISaveHandler
///
/// Combined data
/// Null if not a valid save file for this handler's format. Returns an object containing header, footer, and inner data references.
- SaveHandlerSplitResult? TrySplit(byte[] input);
+ SaveHandlerSplitResult? TrySplit(ReadOnlySpan input);
}
#endif
@@ -35,4 +37,4 @@ public interface ISaveReader : ISaveHandler
/// Save File object, or null if invalid. Check if it is compatible first.
SaveFile? ReadSaveFile(byte[] data, string? path = null);
}
-#endif
\ No newline at end of file
+#endif
diff --git a/PKHeX.Core/Saves/Util/Recognition/SaveHandlerARDS.cs b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerARDS.cs
index c7adc1ab2..ed0bf5aa1 100644
--- a/PKHeX.Core/Saves/Util/Recognition/SaveHandlerARDS.cs
+++ b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerARDS.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
namespace PKHeX.Core;
@@ -12,11 +12,11 @@ public sealed class SaveHandlerARDS : ISaveHandler
public bool IsRecognized(int size) => size is ExpectedSize;
- public SaveHandlerSplitResult TrySplit(byte[] input)
+ public SaveHandlerSplitResult TrySplit(ReadOnlySpan input)
{
// No authentication to see if it actually is a header; no size collisions expected.
- var header = input.Slice(0, sizeHeader);
- input = input.SliceEnd(sizeHeader);
- return new SaveHandlerSplitResult(input, header, Array.Empty());
+ var header = input[..sizeHeader].ToArray();
+ var data = input[sizeHeader..].ToArray();
+ return new SaveHandlerSplitResult(data, header, Array.Empty());
}
}
diff --git a/PKHeX.Core/Saves/Util/Recognition/SaveHandlerBizHawk.cs b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerBizHawk.cs
new file mode 100644
index 000000000..5f39a43b5
--- /dev/null
+++ b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerBizHawk.cs
@@ -0,0 +1,35 @@
+using System;
+using static System.Buffers.Binary.BinaryPrimitives;
+
+namespace PKHeX.Core;
+
+///
+/// Logic for recognizing .dsv save files from DeSmuME.
+///
+public sealed class SaveHandlerBizHawk : ISaveHandler
+{
+ private const int sizeFooter = 0x16;
+
+ private static bool GetHasFooter(ReadOnlySpan input)
+ {
+ var start = input.Length - sizeFooter;
+ var footer = input[start..];
+ var _0x0b = ReadUInt16LittleEndian(footer[0x0B..]);
+ var _0x14 = ReadUInt16LittleEndian(footer[0x14..]);
+ return _0x0b == _0x14;
+ }
+
+ public bool IsRecognized(int size) => SaveUtil.IsSizeValidNoHandler(size - sizeFooter);
+
+ public SaveHandlerSplitResult? TrySplit(ReadOnlySpan input)
+ {
+ if (!GetHasFooter(input))
+ return null;
+
+ var realSize = input.Length - sizeFooter;
+ var footer = input[^realSize..].ToArray();
+ var data = input[..realSize].ToArray();
+
+ return new SaveHandlerSplitResult(data, Array.Empty(), footer);
+ }
+}
diff --git a/PKHeX.Core/Saves/Util/Recognition/SaveHandlerDeSmuME.cs b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerDeSmuME.cs
index 70d5589fe..1d76e3efa 100644
--- a/PKHeX.Core/Saves/Util/Recognition/SaveHandlerDeSmuME.cs
+++ b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerDeSmuME.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Text;
+using System;
namespace PKHeX.Core;
@@ -9,27 +8,34 @@ namespace PKHeX.Core;
public sealed class SaveHandlerDeSmuME : ISaveHandler
{
private const int sizeFooter = 0x7A;
- private const int ExpectedSize = SaveUtil.SIZE_G4RAW + sizeFooter;
+ private const int RealSize = SaveUtil.SIZE_G4RAW;
+ private const int ExpectedSize = RealSize + sizeFooter;
- private static readonly byte[] FOOTER_DSV = Encoding.ASCII.GetBytes("|-DESMUME SAVE-|");
+ private const string SignatureDSV = "|-DESMUME SAVE-|";
- private static bool GetHasFooterDSV(byte[] input)
+ private static bool GetHasFooter(ReadOnlySpan input)
{
- var signature = FOOTER_DSV;
- var start = input.Length - signature.Length;
- return input.AsSpan(start).SequenceEqual(signature);
+ var start = input.Length - SignatureDSV.Length;
+ var footer = input[start..];
+ for (int i = SignatureDSV.Length - 1; i >= 0; i--)
+ {
+ byte c = (byte)SignatureDSV[i];
+ if (footer[i] != c)
+ return false;
+ }
+ return true;
}
public bool IsRecognized(int size) => size is ExpectedSize;
- public SaveHandlerSplitResult? TrySplit(byte[] input)
+ public SaveHandlerSplitResult? TrySplit(ReadOnlySpan input)
{
- if (!GetHasFooterDSV(input))
+ if (!GetHasFooter(input))
return null;
- var footer = input.SliceEnd(SaveUtil.SIZE_G4RAW);
- input = input.Slice(0, SaveUtil.SIZE_G4RAW);
+ var footer = input[^RealSize..].ToArray();
+ var data = input[..RealSize].ToArray();
- return new SaveHandlerSplitResult(input, Array.Empty(), footer);
+ return new SaveHandlerSplitResult(data, Array.Empty(), footer);
}
}
diff --git a/PKHeX.Core/Saves/Util/Recognition/SaveHandlerGCI.cs b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerGCI.cs
index 98409901a..bd415b3c0 100644
--- a/PKHeX.Core/Saves/Util/Recognition/SaveHandlerGCI.cs
+++ b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerGCI.cs
@@ -1,7 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
+using System;
namespace PKHeX.Core;
@@ -19,11 +16,31 @@ public sealed class SaveHandlerGCI : ISaveHandler
private static readonly string[] HEADER_XD = { "GXXJ", "GXXE", "GXXP" }; // NTSC-J, NTSC-U, PAL
private static readonly string[] HEADER_RSBOX = { "GPXJ", "GPXE", "GPXP" }; // NTSC-J, NTSC-U, PAL
- private static bool IsGameMatchHeader(IEnumerable headers, byte[] data) => headers.Contains(Encoding.ASCII.GetString(data, 0, 4));
+ private static bool IsGameMatchHeader(ReadOnlySpan headers, ReadOnlySpan data)
+ {
+ foreach (var header in headers)
+ {
+ if (!IsGameMatchHeader(data, header.AsSpan()))
+ return false;
+ }
+ return true;
+ }
+
+ private static bool IsGameMatchHeader(ReadOnlySpan data, ReadOnlySpan header)
+ {
+ for (int i = 0; i < header.Length; i++)
+ {
+ var c = (byte)header[i];
+ if (data[i] != c)
+ return false;
+ }
+
+ return true;
+ }
public bool IsRecognized(int size) => size is SIZE_G3BOXGCI or SIZE_G3COLOGCI or SIZE_G3XDGCI;
- public SaveHandlerSplitResult? TrySplit(byte[] input)
+ public SaveHandlerSplitResult? TrySplit(ReadOnlySpan input)
{
switch (input.Length)
{
@@ -35,10 +52,10 @@ public sealed class SaveHandlerGCI : ISaveHandler
return null;
}
- byte[] header = input.Slice(0, headerSize);
- input = input.SliceEnd(headerSize);
+ var header = input[..headerSize].ToArray();
+ var data = input[headerSize..].ToArray();
- return new SaveHandlerSplitResult(input, header, Array.Empty());
+ return new SaveHandlerSplitResult(data, header, Array.Empty());
}
///
@@ -46,13 +63,13 @@ public sealed class SaveHandlerGCI : ISaveHandler
///
/// 4 character game code string
/// Magic version ID enumeration; if no match.
- public static GameVersion GetGameCode(string gameCode)
+ public static GameVersion GetGameCode(ReadOnlySpan gameCode)
{
- if (HEADER_COLO.Contains(gameCode))
+ if (IsGameMatchHeader(HEADER_COLO, gameCode))
return GameVersion.COLO;
- if (HEADER_XD.Contains(gameCode))
+ if (IsGameMatchHeader(HEADER_XD, gameCode))
return GameVersion.XD;
- if (HEADER_RSBOX.Contains(gameCode))
+ if (IsGameMatchHeader(HEADER_RSBOX, gameCode))
return GameVersion.RSBOX;
return GameVersion.Unknown;
diff --git a/PKHeX.Core/Saves/Util/SaveUtil.cs b/PKHeX.Core/Saves/Util/SaveUtil.cs
index def38ba88..5d2cc9eb1 100644
--- a/PKHeX.Core/Saves/Util/SaveUtil.cs
+++ b/PKHeX.Core/Saves/Util/SaveUtil.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -88,6 +88,7 @@ public static class SaveUtil
{
DolphinHandler,
new SaveHandlerDeSmuME(),
+ new SaveHandlerBizHawk(),
new SaveHandlerARDS(),
};
#endif
@@ -840,5 +841,17 @@ public static class SaveUtil
///
/// Size in bytes of the save data
/// A boolean indicating whether or not the save data size is valid.
- public static bool IsSizeValid(int size) => Sizes.Contains(size) || Handlers.Any(z => z.IsRecognized(size));
+ public static bool IsSizeValid(int size) => IsSizeValidNoHandler(size) || IsSizeValidHandler(size);
+
+ ///
+ /// Determines whether the save data size is valid for automatically detecting saves.
+ ///
+ /// Only checks the list.
+ public static bool IsSizeValidHandler(int size) => Handlers.Any(z => z.IsRecognized(size));
+
+ ///
+ /// Determines whether the save data size is valid for automatically detecting saves.
+ ///
+ /// Does not check the list.
+ public static bool IsSizeValidNoHandler(int size) => Sizes.Contains(size);
}
diff --git a/PKHeX.WinForms/Util/WinFormsUtil.cs b/PKHeX.WinForms/Util/WinFormsUtil.cs
index 1f2d2fbd5..d8edc1f13 100644
--- a/PKHeX.WinForms/Util/WinFormsUtil.cs
+++ b/PKHeX.WinForms/Util/WinFormsUtil.cs
@@ -246,7 +246,7 @@ public static class WinFormsUtil
"gci", // Dolphin GameCubeImage
"dsv", // DeSmuME
"srm", // RetroArch save files
- "fla", // flashcard
+ "fla", // flash
"SaveRAM", // BizHawk
};