diff --git a/PKHeX.Core/Saves/SAV3GCMemoryCard.cs b/PKHeX.Core/Saves/SAV3GCMemoryCard.cs index 349907639..3a34a28c3 100644 --- a/PKHeX.Core/Saves/SAV3GCMemoryCard.cs +++ b/PKHeX.Core/Saves/SAV3GCMemoryCard.cs @@ -237,21 +237,22 @@ public GCMemoryCardState GetMemoryCardState() if (FirstBlock + BlockCount > NumBlocks) continue; - if (SaveUtil.HEADER_COLO.Contains(GameCode)) + var ver = SaveHandlerGCI.GetGameCode(GameCode); + if (ver == GameVersion.COLO) { if (HasCOLO) // another entry already exists return GCMemoryCardState.DuplicateCOLO; EntryCOLO = i; SaveGameCount++; } - if (SaveUtil.HEADER_XD.Contains(GameCode)) + if (ver == GameVersion.XD) { if (HasXD) // another entry already exists return GCMemoryCardState.DuplicateXD; EntryXD = i; SaveGameCount++; } - if (SaveUtil.HEADER_RSBOX.Contains(GameCode)) + if (ver == GameVersion.RSBOX) { if (HasRSBOX) // another entry already exists return GCMemoryCardState.DuplicateRSBOX; diff --git a/PKHeX.Core/Saves/Util/Recognition/ISaveHandler.cs b/PKHeX.Core/Saves/Util/Recognition/ISaveHandler.cs new file mode 100644 index 000000000..3a94b65c2 --- /dev/null +++ b/PKHeX.Core/Saves/Util/Recognition/ISaveHandler.cs @@ -0,0 +1,22 @@ +namespace PKHeX.Core +{ + /// + /// Provides handling for recognizing atypical save file formats. + /// + public interface ISaveHandler + { + /// + /// Checks if the requested file size is one that can be recognized by this handler. + /// + /// File size + /// True if recognized, false if not recognized. + bool IsRecognized(int size); + + /// + /// Tries splitting up the into header/footer/data components. Returns null if not a valid save file for this handler. + /// + /// 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); + } +} diff --git a/PKHeX.Core/Saves/Util/Recognition/SaveHandlerARDS.cs b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerARDS.cs new file mode 100644 index 000000000..ca73135bf --- /dev/null +++ b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerARDS.cs @@ -0,0 +1,23 @@ +using System; + +namespace PKHeX.Core +{ + /// + /// Logic for recognizing .duc save files dumped via an ARDS. + /// + public class SaveHandlerARDS : ISaveHandler + { + private const int sizeHeader = 0xA4; + private const int ExpectedSize = SaveUtil.SIZE_G4RAW + sizeHeader; // 0x800A4 + + public bool IsRecognized(int size) => size is ExpectedSize; + + public SaveHandlerSplitResult TrySplit(byte[] 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()); + } + } +} diff --git a/PKHeX.Core/Saves/Util/Recognition/SaveHandlerDeSmuME.cs b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerDeSmuME.cs new file mode 100644 index 000000000..25a78c67f --- /dev/null +++ b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerDeSmuME.cs @@ -0,0 +1,45 @@ +using System; +using System.Text; + +namespace PKHeX.Core +{ + /// + /// Logic for recognizing .dsv save files from DeSmuME. + /// + public class SaveHandlerDeSmuME : ISaveHandler + { + private const int sizeFooter = 0x7A; + private const int ExpectedSize = SaveUtil.SIZE_G4RAW + sizeFooter; + + private static readonly byte[] FOOTER_DSV = Encoding.ASCII.GetBytes("|-DESMUME SAVE-|"); + + private static bool GetHasSignature(byte[] input, byte[] signature, int start) + { + for (int i = 0; i < signature.Length; i++) + { + if (signature[i] != input[start + i]) + return false; + } + return true; + } + + private static bool GetHasFooterDSV(byte[] input) + { + var signature = FOOTER_DSV; + return GetHasSignature(input, signature, input.Length - signature.Length); + } + + public bool IsRecognized(int size) => size is ExpectedSize; + + public SaveHandlerSplitResult? TrySplit(byte[] input) + { + if (!GetHasFooterDSV(input)) + return null; + + var footer = input.SliceEnd(SaveUtil.SIZE_G4RAW); + input = input.Slice(0, SaveUtil.SIZE_G4RAW); + + return new SaveHandlerSplitResult(input, Array.Empty(), footer); + } + } +} diff --git a/PKHeX.Core/Saves/Util/Recognition/SaveHandlerGCI.cs b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerGCI.cs new file mode 100644 index 000000000..fc1c1e424 --- /dev/null +++ b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerGCI.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace PKHeX.Core +{ + /// + /// Logic for recognizing .gci save files. + /// + public class SaveHandlerGCI : ISaveHandler + { + private const int headerSize = 0x40; + private const int SIZE_G3BOXGCI = headerSize + SaveUtil.SIZE_G3BOX; // GCI data + private const int SIZE_G3COLOGCI = headerSize + SaveUtil.SIZE_G3COLO; // GCI data + private const int SIZE_G3XDGCI = headerSize + SaveUtil.SIZE_G3XD; // GCI data + + private static readonly string[] HEADER_COLO = { "GC6J", "GC6E", "GC6P" }; // NTSC-J, NTSC-U, PAL + 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)); + + public bool IsRecognized(int size) => size is SIZE_G3BOXGCI or SIZE_G3COLOGCI or SIZE_G3XDGCI; + + public SaveHandlerSplitResult? TrySplit(byte[] input) + { + switch (input.Length) + { + case SIZE_G3COLOGCI when IsGameMatchHeader(HEADER_COLO , input): + case SIZE_G3XDGCI when IsGameMatchHeader(HEADER_XD , input): + case SIZE_G3BOXGCI when IsGameMatchHeader(HEADER_RSBOX, input): + break; + default: + return null; + } + + byte[] header = input.Slice(0, headerSize); + input = input.SliceEnd(headerSize); + + return new SaveHandlerSplitResult(input, header, Array.Empty()); + } + + /// + /// Checks if the game code is one of the recognizable versions. + /// + /// 4 character game code string + /// Magic version ID enumeration; if no match. + public static GameVersion GetGameCode(string gameCode) + { + if (HEADER_COLO.Contains(gameCode)) + return GameVersion.COLO; + if (HEADER_XD.Contains(gameCode)) + return GameVersion.XD; + if (HEADER_RSBOX.Contains(gameCode)) + return GameVersion.RSBOX; + + return GameVersion.Unknown; + } + } +} diff --git a/PKHeX.Core/Saves/Util/Recognition/SaveHandlerSplitResult.cs b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerSplitResult.cs new file mode 100644 index 000000000..85f7a2fcb --- /dev/null +++ b/PKHeX.Core/Saves/Util/Recognition/SaveHandlerSplitResult.cs @@ -0,0 +1,16 @@ +namespace PKHeX.Core +{ + public sealed class SaveHandlerSplitResult + { + public readonly byte[] Header; + public readonly byte[] Footer; + public readonly byte[] Data; + + public SaveHandlerSplitResult(byte[] data, byte[] header, byte[] footer) + { + Data = data; + Header = header; + Footer = footer; + } + } +} diff --git a/PKHeX.Core/Saves/Util/SaveUtil.cs b/PKHeX.Core/Saves/Util/SaveUtil.cs index 33a516a36..a456db516 100644 --- a/PKHeX.Core/Saves/Util/SaveUtil.cs +++ b/PKHeX.Core/Saves/Util/SaveUtil.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; - using static PKHeX.Core.MessageStrings; using static PKHeX.Core.GameVersion; @@ -39,9 +37,6 @@ public static class SaveUtil public const int SIZE_G3BOX = 0x76000; public const int SIZE_G3COLO = 0x60000; public const int SIZE_G3XD = 0x56000; - public const int SIZE_G3BOXGCI = SIZE_G3BOX + 0x40; // GCI data - public const int SIZE_G3COLOGCI = SIZE_G3COLO + 0x40; // GCI data - public const int SIZE_G3XDGCI = SIZE_G3XD + 0x40; // GCI data public const int SIZE_G3RAW = 0x20000; public const int SIZE_G3RAWHALF = 0x10000; public const int SIZE_G2STAD = 0x20000; // same as G3RAW_U @@ -64,6 +59,18 @@ public static class SaveUtil public const int SIZE_G4RANCH = 0x54000; public const int SIZE_G4RANCH_PLAT = 0x7C000; + private static readonly SaveHandlerGCI DolphinHandler = new(); + + /// + /// Pre-formatters for loading save files from non-standard formats (e.g. emulators). + /// + public static readonly ICollection Handlers = new List + { + DolphinHandler, + new SaveHandlerDeSmuME(), + new SaveHandlerARDS(), + }; + internal static readonly HashSet SizesSWSH = new() { SIZE_G8SWSH, SIZE_G8SWSH_1, SIZE_G8SWSH_2, SIZE_G8SWSH_2B, SIZE_G8SWSH_3, SIZE_G8SWSH_3A, SIZE_G8SWSH_3B, SIZE_G8SWSH_3C, @@ -81,18 +88,13 @@ public static class SaveUtil SIZE_G6XY, SIZE_G6ORAS, SIZE_G6ORASDEMO, SIZE_G5RAW, SIZE_G5BW, SIZE_G5B2W2, SIZE_G4BR, SIZE_G4RAW, - SIZE_G3BOX, SIZE_G3BOXGCI, SIZE_G3COLO, SIZE_G3COLOGCI, SIZE_G3XD, SIZE_G3XDGCI, SIZE_G3RAW, SIZE_G3RAWHALF, + SIZE_G3BOX, SIZE_G3COLO, SIZE_G3XD, SIZE_G3RAW, SIZE_G3RAWHALF, // SizesGen2 covers gen2 sizes since there's so many SIZE_G1RAW, SIZE_G1BAT, SIZE_G7BANK, SIZE_G4BANK, SIZE_G4RANCH, SIZE_G4RANCH_PLAT, }; - private static readonly byte[] FOOTER_DSV = Encoding.ASCII.GetBytes("|-DESMUME SAVE-|"); - internal static readonly string[] HEADER_COLO = { "GC6J","GC6E","GC6P" }; // NTSC-J, NTSC-U, PAL - internal static readonly string[] HEADER_XD = { "GXXJ","GXXE","GXXP" }; // NTSC-J, NTSC-U, PAL - internal static readonly string[] HEADER_RSBOX = { "GPXJ","GPXE","GPXP" }; // NTSC-J, NTSC-U, PAL - /// Determines the type of the provided save data. /// Save data of which to determine the origins of /// Version Identifier or Invalid if type cannot be determined. @@ -287,7 +289,7 @@ internal static GameVersion GetIsG3SAV(byte[] data) /// Version Identifier or Invalid if type cannot be determined. internal static GameVersion GetIsG3BOXSAV(byte[] data) { - if (data.Length is not (SIZE_G3BOX or SIZE_G3BOXGCI)) + if (data.Length is not SIZE_G3BOX) return Invalid; byte[] sav = data; @@ -312,7 +314,7 @@ internal static GameVersion GetIsG3BOXSAV(byte[] data) /// Version Identifier or Invalid if type cannot be determined. internal static GameVersion GetIsG3COLOSAV(byte[] data) { - if (data.Length is not (SIZE_G3COLO or SIZE_G3COLOGCI)) + if (data.Length is not SIZE_G3COLO) return Invalid; // Check the intro bytes for each save slot @@ -331,7 +333,7 @@ internal static GameVersion GetIsG3COLOSAV(byte[] data) /// Version Identifier or Invalid if type cannot be determined. internal static GameVersion GetIsG3XDSAV(byte[] data) { - if (data.Length is not (SIZE_G3XD or SIZE_G3XDGCI)) + if (data.Length is not SIZE_G3XD) return Invalid; // Check the intro bytes for each save slot @@ -497,14 +499,29 @@ private static GameVersion GetIsG8SAV(byte[] data) /// An appropriate type of save file for the given data, or null if the save data is invalid. public static SaveFile? GetVariantSAV(byte[] data) { - // Pre-check for header/footer signatures - CheckHeaderFooter(ref data, out var header, out var footer); var sav = GetVariantSAVInternal(data); - if (sav == null) - return null; + if (sav != null) + return sav; - sav.Metadata.SetExtraInfo(header, footer); - return sav; + foreach (var h in Handlers) + { + if (!h.IsRecognized(data.Length)) + continue; + + var split = h.TrySplit(data); + if (split == null) + continue; + + sav = GetVariantSAVInternal(split.Data); + if (sav == null) + continue; + + sav.Metadata.SetExtraInfo(split.Header, split.Footer); + return sav; + } + + // unrecognized. + return null; } private static SaveFile? GetVariantSAVInternal(byte[] data) @@ -559,7 +576,11 @@ private static GameVersion GetIsG8SAV(byte[] data) // Pre-check for header/footer signatures SaveFile sav; byte[] data = memCard.SelectedSaveData; - CheckHeaderFooter(ref data, out var header, out var footer); + var split = DolphinHandler.TrySplit(data); + if (split == null) + return null; + + data = split.Data; switch (memCard.SelectedGameVersion) { @@ -572,7 +593,7 @@ private static GameVersion GetIsG8SAV(byte[] data) default: return null; } - sav.Metadata.SetExtraInfo(header, footer); + sav.Metadata.SetExtraInfo(split.Header, split.Footer); return sav; } @@ -638,7 +659,7 @@ public static SaveFile GetBlankSAV(GameVersion game, string trainerName, Languag /// Blank save file from the requested game, null if no game exists for that . private static SaveFile GetBlankSAV(GameVersion game, LanguageID language) => game switch { - RD or BU or GN or YW or RBY => new SAV1(version: game, japanese: language == LanguageID.Japanese || game == GameVersion.BU), + RD or BU or GN or YW or RBY => new SAV1(version: game, japanese: language == LanguageID.Japanese || game == BU), StadiumJ => new SAV1StadiumJ(), Stadium => new SAV1Stadium(language == LanguageID.Japanese), @@ -730,73 +751,7 @@ public static bool GetSavesFromFolder(string folderPath, bool deep, out IEnumera /// /// 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); - - /// - /// Checks the provided and pulls out any and/or arrays. - /// - /// Input byte array to strip - /// Header data - /// Footer data - private static void CheckHeaderFooter(ref byte[] input, out byte[] header, out byte[] footer) - { - header = Array.Empty(); footer = Array.Empty(); - if ((input.Length & 0xFF) == 0) // catch most non-header/footers - return; - if (input.Length > SIZE_G4RAW) // DeSmuME Gen4/5 DSV - { - if (input.Length == 0x800A4) // Action Replay - { - header = input.Slice(0, 0xA4); - input = input.SliceEnd(0xA4); - return; - } - - if (!GetHasFooterDSV(input)) - return; - - footer = input.SliceEnd(SIZE_G4RAW); - input = input.Slice(0, SIZE_G4RAW); - } - else if (input.Length == SIZE_G3BOXGCI) - { - if (!IsGameMatchHeader(HEADER_RSBOX, input)) - return; // not gci - header = input.Slice(0, SIZE_G3BOXGCI - SIZE_G3BOX); - input = input.SliceEnd(header.Length); - } - else if (input.Length == SIZE_G3COLOGCI) - { - if (!IsGameMatchHeader(HEADER_COLO, input)) - return; // not gci - header = input.Slice(0, SIZE_G3COLOGCI - SIZE_G3COLO); - input = input.SliceEnd(header.Length); - } - else if (input.Length == SIZE_G3XDGCI) - { - if (!IsGameMatchHeader(HEADER_XD, input)) - return; // not gci - header = input.Slice(0, SIZE_G3XDGCI - SIZE_G3XD); - input = input.SliceEnd(header.Length); - } - static bool IsGameMatchHeader(IEnumerable headers, byte[] data) => headers.Contains(Encoding.ASCII.GetString(data, 0, 4)); - } - - private static bool GetHasFooterDSV(byte[] input) - { - var signature = FOOTER_DSV; - return GetHasSignature(input, signature, input.Length - signature.Length); - } - - private static bool GetHasSignature(byte[] input, byte[] signature, int start) - { - for (int i = 0; i < signature.Length; i++) - { - if (signature[i] != input[start + i]) - return false; - } - return true; - } + public static bool IsSizeValid(int size) => Sizes.Contains(size) || Handlers.Any(z => z.IsRecognized(size)); /// /// Force loads the provided to the requested .