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 };