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 .