From 9718d1d2aa9708823820cde56ffc929d74aebfb5 Mon Sep 17 00:00:00 2001 From: Kurt Date: Wed, 6 Jan 2021 15:46:43 -0800 Subject: [PATCH] Add save handler abstraction for detecting atypical save formats GCI, DSV, DUC are already supported, so I've written the abstraction for those and seed the Handler list on startup. Can add a new class with recognition via SaveUtil.Handlers.Add(myHandler); --- PKHeX.Core/Saves/SAV3GCMemoryCard.cs | 7 +- .../Saves/Util/Recognition/ISaveHandler.cs | 22 +++ .../Saves/Util/Recognition/SaveHandlerARDS.cs | 23 +++ .../Util/Recognition/SaveHandlerDeSmuME.cs | 45 ++++++ .../Saves/Util/Recognition/SaveHandlerGCI.cs | 61 ++++++++ .../Recognition/SaveHandlerSplitResult.cs | 16 +++ PKHeX.Core/Saves/Util/SaveUtil.cs | 135 ++++++------------ 7 files changed, 216 insertions(+), 93 deletions(-) create mode 100644 PKHeX.Core/Saves/Util/Recognition/ISaveHandler.cs create mode 100644 PKHeX.Core/Saves/Util/Recognition/SaveHandlerARDS.cs create mode 100644 PKHeX.Core/Saves/Util/Recognition/SaveHandlerDeSmuME.cs create mode 100644 PKHeX.Core/Saves/Util/Recognition/SaveHandlerGCI.cs create mode 100644 PKHeX.Core/Saves/Util/Recognition/SaveHandlerSplitResult.cs 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 .