diff --git a/PKHeX.Core/Saves/MemoryCard/DEntry.cs b/PKHeX.Core/Saves/MemoryCard/DEntry.cs
new file mode 100644
index 000000000..52c6902b3
--- /dev/null
+++ b/PKHeX.Core/Saves/MemoryCard/DEntry.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Buffers.Binary;
+using System.Text;
+
+namespace PKHeX.Core;
+
+///
+/// GameCube Directory Entry structure
+///
+/// 0x40 "header" of a GCI file.
+public readonly ref struct DEntry(Span data)
+{
+ public const int SIZE = 0x40;
+ private readonly Span Data = data;
+
+ public Span GameCode => Data[..4];
+ public Span MakerCode => Data[4..6];
+ public ref byte Unused6 => ref Data[6];
+ public ref byte BannerAndIconFlags => ref Data[7];
+ public Span FileName => Data[8..0x28];
+
+ // Seconds since Jan 1, 2000
+ public uint ModificationTime { get => BinaryPrimitives.ReadUInt32BigEndian(Data[0x28..]); set => BinaryPrimitives.WriteUInt32BigEndian(Data[0x28..], value); }
+ public uint ImageOffset { get => BinaryPrimitives.ReadUInt32BigEndian(Data[0x2C..]); set => BinaryPrimitives.WriteUInt32BigEndian(Data[0x2C..], value); }
+
+ // 2 bits per icon
+ public ushort IconFormat { get => BinaryPrimitives.ReadUInt16BigEndian(Data[0x30..]); set => BinaryPrimitives.WriteUInt16BigEndian(Data[0x30..], value); }
+
+ public ushort AnimationSpeed { get => BinaryPrimitives.ReadUInt16BigEndian(Data[0x32..]); set => BinaryPrimitives.WriteUInt16BigEndian(Data[0x32..], value); }
+
+ public ref byte FilePermissions => ref Data[0x34];
+ public ref byte CopyCounter => ref Data[0x35];
+
+ public ushort FirstBlock { get => BinaryPrimitives.ReadUInt16BigEndian(Data[0x36..]); set => BinaryPrimitives.WriteUInt16BigEndian(Data[0x36..], value); }
+ public ushort BlockCount { get => BinaryPrimitives.ReadUInt16BigEndian(Data[0x38..]); set => BinaryPrimitives.WriteUInt16BigEndian(Data[0x38..], value); }
+
+ public ushort Unused3A { get => BinaryPrimitives.ReadUInt16BigEndian(Data[0x3A..]); set => BinaryPrimitives.WriteUInt16BigEndian(Data[0x3A..], value); }
+ public uint CommentsAddress { get => BinaryPrimitives.ReadUInt32BigEndian(Data[0x3C..]); set => BinaryPrimitives.WriteUInt32BigEndian(Data[0x3C..], value); }
+
+ public bool IsEmpty => BinaryPrimitives.ReadUInt32BigEndian(GameCode) is 0 or uint.MaxValue; // FF is "Uninitialized", but check for 0 to be sure.
+ public DateTime ModificationDate => Epoch.AddSeconds(ModificationTime);
+
+ public void CopyTo(DEntry savDEntry) => Data[..SIZE].CopyTo(savDEntry.Data[..SIZE]);
+ private static DateTime Epoch => new(2000, 1, 1);
+ public void SetModificationTime(DateTime dt) => ModificationTime = (uint)dt.Subtract(Epoch).TotalSeconds;
+
+ public int SaveDataOffset => FirstBlock * SAV3GCMemoryCard.BLOCK_SIZE;
+ public int SaveDataLength => BlockCount * SAV3GCMemoryCard.BLOCK_SIZE;
+ public bool IsStartInvalid => FirstBlock < 5; // 5 is the first block that can be used for data
+
+ public string GetSaveName(Encoding encoding)
+ {
+ Span result = stackalloc char[4 + 1 + 2 + 1 + 0x20];
+ encoding.GetChars(GameCode, result); // 4 bytes
+ result[4] = '-';
+ encoding.GetChars(MakerCode, result[5..]); // 2 bytes
+ result[7] = '-';
+ encoding.GetChars(FileName, result[8..]); // 0x20 bytes (max)
+
+ var zero = result.IndexOf('\0');
+ if (zero >= 0)
+ result = result[..zero];
+
+ return result.ToString();
+ }
+}
diff --git a/PKHeX.Core/Saves/MemoryCard/MemoryCardChecksumStatus.cs b/PKHeX.Core/Saves/MemoryCard/MemoryCardChecksumStatus.cs
new file mode 100644
index 000000000..004c5a61b
--- /dev/null
+++ b/PKHeX.Core/Saves/MemoryCard/MemoryCardChecksumStatus.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace PKHeX.Core;
+
+[Flags]
+public enum MemoryCardChecksumStatus
+{
+ None = 0,
+ HeaderBad = 1 << 0,
+ DirectoryBad = 1 << 1,
+ DirectoryBackupBad = 1 << 2,
+ BlockAllocBad = 1 << 3,
+ BlockAllocBackupBad = 1 << 4,
+}
diff --git a/PKHeX.Core/Saves/MemoryCard/MemoryCardSaveStatus.cs b/PKHeX.Core/Saves/MemoryCard/MemoryCardSaveStatus.cs
new file mode 100644
index 000000000..1e21986a5
--- /dev/null
+++ b/PKHeX.Core/Saves/MemoryCard/MemoryCardSaveStatus.cs
@@ -0,0 +1,17 @@
+namespace PKHeX.Core;
+
+///
+/// Flags for indicating what data is present in the Memory Card
+///
+public enum MemoryCardSaveStatus : byte
+{
+ Invalid,
+ NoPkmSaveGame,
+ SaveGameCOLO,
+ SaveGameXD,
+ SaveGameRSBOX,
+ MultipleSaveGame,
+ DuplicateCOLO,
+ DuplicateXD,
+ DuplicateRSBOX,
+}
diff --git a/PKHeX.Core/Saves/MemoryCard/SAV3GCMemoryCard.cs b/PKHeX.Core/Saves/MemoryCard/SAV3GCMemoryCard.cs
new file mode 100644
index 000000000..70bc2c633
--- /dev/null
+++ b/PKHeX.Core/Saves/MemoryCard/SAV3GCMemoryCard.cs
@@ -0,0 +1,324 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+using static System.Buffers.Binary.BinaryPrimitives;
+
+namespace PKHeX.Core;
+
+///
+/// GameCube save container which may or may not contain Generation 3 objects.
+///
+///
+/// GameCube memory card format data, checksum and code to extract files based on Dolphin code, adapted from C++ to C#
+/// https://github.com/dolphin-emu/dolphin/
+///
+public sealed class SAV3GCMemoryCard(Memory Raw)
+{
+ public const int BLOCK_SIZE = 0x2000;
+ private const int MBIT_TO_BLOCKS = 0x10;
+ private const int DENTRY_SIZE = 0x40;
+ private const int NumEntries_Directory = (BLOCK_SIZE / DENTRY_SIZE) - 1; // 127
+
+ public Span Data => Raw.Span;
+
+ public static bool IsMemoryCardSize(long size)
+ {
+ if ((size & 0x7F8_0000) == 0) // 512KB - 64MB
+ return false;
+ return (size & (size - 1)) == 0; // size is a power of 2
+ }
+
+ public static bool IsMemoryCardSize(ReadOnlySpan data)
+ {
+ if (!IsMemoryCardSize(data.Length))
+ return false; // bad size
+ if (ReadUInt64BigEndian(data) == ulong.MaxValue)
+ return false; // uninitialized
+
+ // Size in megabits, not megabytes
+ int m_sizeMb = data.Length / BLOCK_SIZE / MBIT_TO_BLOCKS;
+ if (m_sizeMb != ReadUInt16BigEndian(data[0x22..])) // Memory card file size does not match the header size
+ return false;
+ return true;
+ }
+
+ // Control blocks
+ private const int Header_Block = 0;
+ private const int Directory_Block = 1;
+ private const int DirectoryBackup_Block = 2;
+ private const int BlockAlloc_Block = 3;
+ private const int BlockAllocBackup_Block = 4;
+
+ private const int Header = BLOCK_SIZE * Header_Block;
+ private const int Directory = BLOCK_SIZE * Directory_Block;
+ private const int DirectoryBAK = BLOCK_SIZE * DirectoryBackup_Block;
+ private const int BlockAlloc = BLOCK_SIZE * BlockAlloc_Block;
+ private const int BlockAllocBAK = BLOCK_SIZE * BlockAllocBackup_Block;
+
+ private Span SpanHeader => Data.Slice(Header, BLOCK_SIZE);
+ private Span SpanDirectory => Data.Slice(Directory, BLOCK_SIZE);
+ private Span SpanDirectoryBAK => Data.Slice(DirectoryBAK, BLOCK_SIZE);
+ private Span SpanDirectoryActive => Data.Slice(DirectoryBlock_Used * BLOCK_SIZE, BLOCK_SIZE);
+ private Span SpanBlockAlloc => Data.Slice(BlockAlloc, BLOCK_SIZE);
+ private Span SpanBlockAllocBAK => Data.Slice(BlockAllocBAK, BLOCK_SIZE);
+
+ // Checksums
+ private (ushort Checksum, ushort Inverse) GetChecksum(int block, int offset, [ConstantExpected(Min = 0)] int length)
+ {
+ var ofs = (block * BLOCK_SIZE) + offset;
+ var span = Data.Slice(ofs, length);
+ return GetChecksum(span);
+ }
+
+ private static (ushort Checksum, ushort Inverse) GetChecksum(ReadOnlySpan span)
+ {
+ ushort csum = 0;
+ ushort inv_csum = 0;
+
+ for (int i = 0; i < span.Length; i += 2)
+ {
+ var value = ReadUInt16BigEndian(span[i..]);
+ csum += value;
+ inv_csum += (ushort)~value;
+ }
+ if (csum == 0xffff)
+ csum = 0;
+ if (inv_csum == 0xffff)
+ inv_csum = 0;
+
+ return (csum, inv_csum);
+ }
+
+ private MemoryCardChecksumStatus VerifyChecksums()
+ {
+ MemoryCardChecksumStatus results = 0;
+
+ var (chk, inv) = GetChecksum(Header_Block, 0, 0x1FC);
+ if (Header_Checksum != chk || Header_Checksum_Inv != inv)
+ results |= MemoryCardChecksumStatus.HeaderBad;
+
+ (chk, inv) = GetChecksum(Directory_Block, 0, 0x1FFC);
+ if (Directory_Checksum != chk || Directory_Checksum_Inv != inv)
+ results |= MemoryCardChecksumStatus.DirectoryBad;
+
+ (chk, inv) = GetChecksum(DirectoryBackup_Block, 0, 0x1FFC);
+ if (DirectoryBAK_Checksum != chk || DirectoryBAK_Checksum_Inv != inv)
+ results |= MemoryCardChecksumStatus.DirectoryBackupBad;
+
+ (chk, inv) = GetChecksum(BlockAlloc_Block, 4, 0x1FFC);
+ if (BlockAlloc_Checksum != chk || BlockAlloc_Checksum_Inv != inv)
+ results |= MemoryCardChecksumStatus.BlockAllocBad;
+
+ (chk, inv) = GetChecksum(BlockAllocBackup_Block, 4, 0x1FFC);
+ if ((BlockAllocBAK_Checksum != chk) || BlockAllocBAK_Checksum_Inv != inv)
+ results |= MemoryCardChecksumStatus.BlockAllocBackupBad;
+
+ return results;
+ }
+
+ // Structure
+ private int Header_Size => ReadUInt16BigEndian(SpanHeader[0x0022..]);
+ private int Header_Encoding => ReadUInt16BigEndian(SpanHeader[0x0024..]);
+ private ushort Header_Checksum => ReadUInt16BigEndian(SpanHeader[0x01fc..]);
+ private ushort Header_Checksum_Inv => ReadUInt16BigEndian(SpanHeader[0x01fe..]);
+
+ // Encoding (Windows-1252 or Shift JIS)
+ private bool Header_Japanese => Header_Encoding == 1;
+ public Encoding EncodingType => Header_Japanese ? Encoding.GetEncoding(1252) : Encoding.GetEncoding(932);
+
+ private int Directory_UpdateCounter => ReadUInt16BigEndian(SpanDirectory[0x1ffa..]);
+ private int Directory_Checksum => ReadUInt16BigEndian(SpanDirectory[0x1ffc..]);
+ private int Directory_Checksum_Inv => ReadUInt16BigEndian(SpanDirectory[0x1ffe..]);
+
+ private int DirectoryBAK_UpdateCounter => ReadUInt16BigEndian(SpanDirectoryBAK[0x1ffa..]);
+ private int DirectoryBAK_Checksum => ReadUInt16BigEndian(SpanDirectoryBAK[0x1ffc..]);
+ private int DirectoryBAK_Checksum_Inv => ReadUInt16BigEndian(SpanDirectoryBAK[0x1ffe..]);
+
+ private int BlockAlloc_Checksum => ReadUInt16BigEndian(SpanBlockAlloc);
+ private int BlockAlloc_Checksum_Inv => ReadUInt16BigEndian(SpanBlockAlloc[0x0002..]);
+
+ private int BlockAllocBAK_Checksum => ReadUInt16BigEndian(SpanBlockAllocBAK);
+ private int BlockAllocBAK_Checksum_Inv => ReadUInt16BigEndian(SpanBlockAllocBAK[0x0002..]);
+
+ private int DirectoryBlock_Used;
+
+ private int EntryCOLO = -1;
+ private int EntryXD = -1;
+ private int EntryRSBOX = -1;
+ private int EntrySelected = -1;
+ public bool HasCOLO => EntryCOLO >= 0;
+ public bool HasXD => EntryXD >= 0;
+ public bool HasRSBOX => EntryRSBOX >= 0;
+ public int SaveGameCount { get; private set; }
+
+ private bool IsCorruptedMemoryCard()
+ {
+ var csums = VerifyChecksums();
+ if (csums.HasFlag(MemoryCardChecksumStatus.HeaderBad))
+ return true;
+
+ if (csums.HasFlag(MemoryCardChecksumStatus.DirectoryBad))
+ {
+ if (csums.HasFlag(MemoryCardChecksumStatus.DirectoryBackupBad)) // backup is also wrong
+ return true; // Directory checksum and directory backup checksum failed
+ RestoreBackup(); // backup is correct, restore
+ csums = VerifyChecksums(); // update checksums
+ }
+
+ if (!csums.HasFlag(MemoryCardChecksumStatus.BlockAllocBad))
+ return false;
+ if (csums.HasFlag(MemoryCardChecksumStatus.BlockAllocBackupBad)) // backup is also wrong
+ return true;
+ RestoreBackup(); // backup is correct, restore
+ return false;
+ }
+
+ private void RestoreBackup()
+ {
+ SpanDirectoryBAK.CopyTo(SpanDirectory);
+ SpanBlockAllocBAK.CopyTo(SpanBlockAlloc);
+ }
+
+ public MemoryCardSaveStatus GetMemoryCardState()
+ {
+ if (!IsMemoryCardSize(Data))
+ return MemoryCardSaveStatus.Invalid; // Invalid size
+
+ if (IsCorruptedMemoryCard())
+ return MemoryCardSaveStatus.Invalid;
+
+ // Use the most recent directory block
+ DirectoryBlock_Used = DirectoryBAK_UpdateCounter > Directory_UpdateCounter
+ ? DirectoryBackup_Block
+ : Directory_Block;
+
+ // Search for Pokémon saves in the directory
+ SaveGameCount = 0;
+ for (int i = 0; i < NumEntries_Directory; i++)
+ {
+ var dEntry = GetDEntry(i);
+ if (dEntry.IsEmpty) // empty entry
+ continue;
+ if (dEntry.IsStartInvalid)
+ continue;
+ if (dEntry.SaveDataOffset + dEntry.SaveDataLength > Data.Length)
+ continue; // Memory card directory contains info for deleted files with boundaries beyond memory card size, ignore
+
+ var version = SaveHandlerGCI.GetGameCode(dEntry.GameCode);
+ if (version == GameVersion.COLO)
+ {
+ if (HasCOLO) // another entry already exists
+ return MemoryCardSaveStatus.DuplicateCOLO;
+ EntryCOLO = i;
+ SaveGameCount++;
+ }
+ else if (version == GameVersion.XD)
+ {
+ if (HasXD) // another entry already exists
+ return MemoryCardSaveStatus.DuplicateXD;
+ EntryXD = i;
+ SaveGameCount++;
+ }
+ else if (version == GameVersion.RSBOX)
+ {
+ if (HasRSBOX) // another entry already exists
+ return MemoryCardSaveStatus.DuplicateRSBOX;
+ EntryRSBOX = i;
+ SaveGameCount++;
+ }
+ }
+ return AutoInferState();
+ }
+
+ private MemoryCardSaveStatus AutoInferState()
+ {
+ if (SaveGameCount == 0)
+ return MemoryCardSaveStatus.NoPkmSaveGame;
+ if (SaveGameCount > 1)
+ return MemoryCardSaveStatus.MultipleSaveGame;
+
+ if (HasCOLO)
+ {
+ EntrySelected = EntryCOLO;
+ return MemoryCardSaveStatus.SaveGameCOLO;
+ }
+ if (HasXD)
+ {
+ EntrySelected = EntryXD;
+ return MemoryCardSaveStatus.SaveGameXD;
+ }
+ EntrySelected = EntryRSBOX;
+ return MemoryCardSaveStatus.SaveGameRSBOX;
+ }
+
+ public GameVersion SelectedGameVersion
+ {
+ get
+ {
+ if (EntrySelected == EntryCOLO)
+ return GameVersion.COLO;
+ if (EntrySelected == EntryXD)
+ return GameVersion.XD;
+ if (EntrySelected == EntryRSBOX)
+ return GameVersion.RSBOX;
+ return GameVersion.Any; //Default for no game selected
+ }
+ }
+
+ public void SelectSaveGame(GameVersion Game)
+ {
+ switch (Game)
+ {
+ case GameVersion.COLO: if (HasCOLO) EntrySelected = EntryCOLO; break;
+ case GameVersion.XD: if (HasXD) EntrySelected = EntryXD; break;
+ case GameVersion.RSBOX: if (HasRSBOX) EntrySelected = EntryRSBOX; break;
+ }
+ }
+
+ public ReadOnlyMemory ReadSaveGameData()
+ {
+ var entry = EntrySelected;
+ if (entry < 0)
+ return default; // No entry selected
+ return ReadSaveGameData(entry);
+ }
+
+ private Memory GetSaveGame(DEntry entry) => Raw.Slice(entry.SaveDataOffset, entry.SaveDataLength);
+
+ public DEntry GetDEntry(int index)
+ {
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, NumEntries_Directory, nameof(index));
+ return new DEntry(SpanDirectoryActive.Slice(index * DENTRY_SIZE, DENTRY_SIZE));
+ }
+
+ public ReadOnlyMemory ReadSaveGameData(int entry)
+ {
+ var dEntry = GetDEntry(entry);
+ return GetSaveGame(dEntry);
+ }
+
+ public void WriteSaveGameData(Span data)
+ {
+ var entry = EntrySelected;
+ if (entry < 0) // Can't write anywhere
+ return;
+ WriteSaveGameData(data, entry);
+ }
+
+ public void WriteSaveGameData(Span data, int entry)
+ {
+ var dEntry = GetDEntry(entry);
+ if (dEntry.SaveDataLength != data.Length) // File size should never change.
+ throw new ArgumentException("Save data size changed.", nameof(entry));
+
+ var dest = GetSaveGame(dEntry).Span;
+ data.CopyTo(dest);
+
+ var timestamp = EncounterDate.GetDateTimeGC();
+ dEntry.SetModificationTime(timestamp);
+
+ // Revise the DEntry of the injected save data to match that of the main DEntry
+ var savDEntry = new DEntry(dest[..DENTRY_SIZE]);
+ dEntry.CopyTo(savDEntry);
+ }
+}
diff --git a/PKHeX.Core/Saves/SAV3Colosseum.cs b/PKHeX.Core/Saves/SAV3Colosseum.cs
index 015dccb2d..3584b143f 100644
--- a/PKHeX.Core/Saves/SAV3Colosseum.cs
+++ b/PKHeX.Core/Saves/SAV3Colosseum.cs
@@ -88,7 +88,7 @@ protected override byte[] GetFinalData()
return newFile;
MemoryCard.WriteSaveGameData(newFile);
- return MemoryCard.Data;
+ return MemoryCard.Data.ToArray();
}
private byte[] GetInnerData()
diff --git a/PKHeX.Core/Saves/SAV3GCMemoryCard.cs b/PKHeX.Core/Saves/SAV3GCMemoryCard.cs
deleted file mode 100644
index 4e7f73c9a..000000000
--- a/PKHeX.Core/Saves/SAV3GCMemoryCard.cs
+++ /dev/null
@@ -1,427 +0,0 @@
-using System;
-using System.Diagnostics.CodeAnalysis;
-using System.Text;
-using static System.Buffers.Binary.BinaryPrimitives;
-
-namespace PKHeX.Core;
-/* GameCube memory card format data, checksum and code to extract files based on Dolphin code, adapted from C++ to C#
- * https://github.com/dolphin-emu/dolphin/
- */
-
-///
-/// Flags for indicating what data is present in the Memory Card
-///
-public enum GCMemoryCardState
-{
- Invalid,
- NoPkmSaveGame,
- SaveGameCOLO,
- SaveGameXD,
- SaveGameRSBOX,
- MultipleSaveGame,
- DuplicateCOLO,
- DuplicateXD,
- DuplicateRSBOX,
-}
-
-///
-/// GameCube save container which may or may not contain Generation 3 objects.
-///
-public sealed class SAV3GCMemoryCard(byte[] Data)
-{
- private const int BLOCK_SIZE = 0x2000;
- private const int MBIT_TO_BLOCKS = 0x10;
- private const int DENTRY_STRLEN = 0x20;
- private const int DENTRY_SIZE = 0x40;
- private const int NumEntries_Directory = BLOCK_SIZE / DENTRY_SIZE; // 128
-
- public static bool IsMemoryCardSize(long size)
- {
- if ((size & 0x7F8_0000) == 0) // 512KB - 64MB
- return false;
- return (size & (size - 1)) == 0; // size is a power of 2
- }
-
- public static bool IsMemoryCardSize(ReadOnlySpan Data)
- {
- if (!IsMemoryCardSize(Data.Length))
- return false; // bad size
- if (ReadUInt64BigEndian(Data) == ulong.MaxValue)
- return false; // uninitialized
- return true;
- }
-
- // Control blocks
- private const int Header_Block = 0;
- private const int Directory_Block = 1;
- private const int DirectoryBackup_Block = 2;
- private const int BlockAlloc_Block = 3;
- private const int BlockAllocBackup_Block = 4;
-
- private const int Header = BLOCK_SIZE * Header_Block;
- private const int Directory = BLOCK_SIZE * Directory_Block;
- private const int DirectoryBAK = BLOCK_SIZE * DirectoryBackup_Block;
- private const int BlockAlloc = BLOCK_SIZE * BlockAlloc_Block;
- private const int BlockAllocBAK = BLOCK_SIZE * BlockAllocBackup_Block;
-
- // Checksums
- private (ushort Checksum, ushort Inverse) GetChecksum(int block, int offset, [ConstantExpected(Min = 0)] int length)
- {
- var ofs = (block * BLOCK_SIZE) + offset;
- var span = Data.AsSpan(ofs, length);
- return GetChecksum(span);
- }
-
- private static (ushort Checksum, ushort Inverse) GetChecksum(ReadOnlySpan span)
- {
- ushort csum = 0;
- ushort inv_csum = 0;
-
- for (int i = 0; i < span.Length; i += 2)
- {
- var value = ReadUInt16BigEndian(span[i..]);
- csum += value;
- inv_csum += (ushort)~value;
- }
- if (csum == 0xffff)
- csum = 0;
- if (inv_csum == 0xffff)
- inv_csum = 0;
-
- return (csum, inv_csum);
- }
-
- private MemoryCardChecksumStatus VerifyChecksums()
- {
- MemoryCardChecksumStatus results = 0;
-
- var (chk, inv) = GetChecksum(Header_Block, 0, 0x1FC);
- if (Header_Checksum != chk || Header_Checksum_Inv != inv)
- results |= MemoryCardChecksumStatus.HeaderBad;
-
- (chk, inv) = GetChecksum(Directory_Block, 0, 0x1FFC);
- if (Directory_Checksum != chk || Directory_Checksum_Inv != inv)
- results |= MemoryCardChecksumStatus.DirectoryBad;
-
- (chk, inv) = GetChecksum(DirectoryBackup_Block, 0, 0x1FFC);
- if (DirectoryBAK_Checksum != chk || DirectoryBAK_Checksum_Inv != inv)
- results |= MemoryCardChecksumStatus.DirectoryBackupBad;
-
- (chk, inv) = GetChecksum(BlockAlloc_Block, 4, 0x1FFC);
- if (BlockAlloc_Checksum != chk || BlockAlloc_Checksum_Inv != inv)
- results |= MemoryCardChecksumStatus.BlockAllocBad;
-
- (chk, inv) = GetChecksum(BlockAllocBackup_Block, 4, 0x1FFC);
- if ((BlockAllocBAK_Checksum != chk) || BlockAllocBAK_Checksum_Inv != inv)
- results |= MemoryCardChecksumStatus.BlockAllocBackupBad;
-
- return results;
- }
-
- // Structure
- private int Header_Size => ReadUInt16BigEndian(Data.AsSpan(Header + 0x0022));
- private ushort Header_Checksum => ReadUInt16BigEndian(Data.AsSpan(Header + 0x01fc));
- private ushort Header_Checksum_Inv => ReadUInt16BigEndian(Data.AsSpan(Header + 0x01fe));
-
- // Encoding (Windows-1252 or Shift JIS)
- private int Header_Encoding => ReadUInt16BigEndian(Data.AsSpan(Header + 0x0024));
- private bool Header_Japanese => Header_Encoding == 1;
- private Encoding EncodingType => Header_Japanese ? Encoding.GetEncoding(1252) : Encoding.GetEncoding(932);
-
- private int Directory_UpdateCounter => ReadUInt16BigEndian(Data.AsSpan(Directory + 0x1ffa));
- private int Directory_Checksum => ReadUInt16BigEndian(Data.AsSpan(Directory + 0x1ffc));
- private int Directory_Checksum_Inv => ReadUInt16BigEndian(Data.AsSpan(Directory + 0x1ffe));
-
- private int DirectoryBAK_UpdateCounter => ReadUInt16BigEndian(Data.AsSpan(DirectoryBAK + 0x1ffa));
- private int DirectoryBAK_Checksum => ReadUInt16BigEndian(Data.AsSpan(DirectoryBAK + 0x1ffc));
- private int DirectoryBAK_Checksum_Inv => ReadUInt16BigEndian(Data.AsSpan(DirectoryBAK + 0x1ffe));
-
- private int BlockAlloc_Checksum => ReadUInt16BigEndian(Data.AsSpan(BlockAlloc + 0x0000));
- private int BlockAlloc_Checksum_Inv => ReadUInt16BigEndian(Data.AsSpan(BlockAlloc + 0x0002));
-
- private int BlockAllocBAK_Checksum => ReadUInt16BigEndian(Data.AsSpan(BlockAllocBAK + 0x0000));
- private int BlockAllocBAK_Checksum_Inv => ReadUInt16BigEndian(Data.AsSpan(BlockAllocBAK + 0x0002));
-
- private int DirectoryBlock_Used;
-
- private int EntryCOLO = -1;
- private int EntryXD = -1;
- private int EntryRSBOX = -1;
- private int EntrySelected = -1;
- public bool HasCOLO => EntryCOLO >= 0;
- public bool HasXD => EntryXD >= 0;
- public bool HasRSBOX => EntryRSBOX >= 0;
- public int SaveGameCount { get; private set; }
-
- [Flags]
- public enum MemoryCardChecksumStatus
- {
- None = 0,
- HeaderBad = 1 << 0,
- DirectoryBad = 1 << 1,
- DirectoryBackupBad = 1 << 2,
- BlockAllocBad,
- BlockAllocBackupBad,
- }
-
- private bool IsCorruptedMemoryCard()
- {
- var csums = VerifyChecksums();
-
- if ((csums & MemoryCardChecksumStatus.HeaderBad) != 0)
- return true;
-
- if ((csums & MemoryCardChecksumStatus.DirectoryBad) != 0)
- {
- if ((csums & MemoryCardChecksumStatus.DirectoryBackupBad) != 0) // backup is also wrong
- return true; // Directory checksum and directory backup checksum failed
-
- // backup is correct, restore
- RestoreBackup();
- csums = VerifyChecksums(); // update checksums
- }
-
- if ((csums & MemoryCardChecksumStatus.BlockAllocBad) != 0)
- {
- if ((csums & MemoryCardChecksumStatus.BlockAllocBackupBad) != 0) // backup is also wrong
- return true;
-
- // backup is correct, restore
- RestoreBackup();
- }
-
- return false;
- }
-
- private void RestoreBackup()
- {
- Array.Copy(Data, DirectoryBackup_Block*BLOCK_SIZE, Data, Directory_Block*BLOCK_SIZE, BLOCK_SIZE);
- Array.Copy(Data, BlockAllocBackup_Block*BLOCK_SIZE, Data, BlockAlloc_Block*BLOCK_SIZE, BLOCK_SIZE);
- }
-
- public GCMemoryCardState GetMemoryCardState()
- {
- Span data = Data;
- if (!IsMemoryCardSize(data))
- return GCMemoryCardState.Invalid; // Invalid size
-
- // Size in megabits, not megabytes
- int m_sizeMb = data.Length / BLOCK_SIZE / MBIT_TO_BLOCKS;
- if (m_sizeMb != Header_Size) // Memory card file size does not match the header size
- return GCMemoryCardState.Invalid;
-
- if (IsCorruptedMemoryCard())
- return GCMemoryCardState.Invalid;
-
- // Use the most recent directory block
- DirectoryBlock_Used = DirectoryBAK_UpdateCounter > Directory_UpdateCounter
- ? DirectoryBackup_Block
- : Directory_Block;
-
- // Search for Pokémon saves in the directory
- SaveGameCount = 0;
- for (int i = 0; i < NumEntries_Directory; i++)
- {
- int offset = (DirectoryBlock_Used * BLOCK_SIZE) + (i * DENTRY_SIZE);
- var slice = data.Slice(offset, DENTRY_SIZE);
- var dEntry = new DEntry(slice);
-
- if (dEntry.IsEmpty) // empty entry
- continue;
-
- int firstBlock = dEntry.FirstBlock;
- if (firstBlock < 5)
- continue;
-
- int blockCount = dEntry.BlockCount;
- var dataEnd = (firstBlock + blockCount) * BLOCK_SIZE;
-
- // Memory card directory contains info for deleted files with boundaries beyond memory card size, ignore
- if (dataEnd > data.Length)
- continue;
-
- var version = SaveHandlerGCI.GetGameCode(dEntry.GameCode);
- if (version == GameVersion.COLO)
- {
- if (HasCOLO) // another entry already exists
- return GCMemoryCardState.DuplicateCOLO;
- EntryCOLO = i;
- SaveGameCount++;
- }
- else if (version == GameVersion.XD)
- {
- if (HasXD) // another entry already exists
- return GCMemoryCardState.DuplicateXD;
- EntryXD = i;
- SaveGameCount++;
- }
- else if (version == GameVersion.RSBOX)
- {
- if (HasRSBOX) // another entry already exists
- return GCMemoryCardState.DuplicateRSBOX;
- EntryRSBOX = i;
- SaveGameCount++;
- }
- }
- if (SaveGameCount == 0)
- return GCMemoryCardState.NoPkmSaveGame;
-
- if (SaveGameCount > 1)
- return GCMemoryCardState.MultipleSaveGame;
-
- if (HasCOLO)
- {
- EntrySelected = EntryCOLO;
- return GCMemoryCardState.SaveGameCOLO;
- }
- if (HasXD)
- {
- EntrySelected = EntryXD;
- return GCMemoryCardState.SaveGameXD;
- }
- EntrySelected = EntryRSBOX;
- return GCMemoryCardState.SaveGameRSBOX;
- }
-
- public GameVersion SelectedGameVersion
- {
- get
- {
- if (EntrySelected < 0)
- return GameVersion.Any;
-
- if (EntrySelected == EntryCOLO)
- return GameVersion.COLO;
- if (EntrySelected == EntryXD)
- return GameVersion.XD;
- if (EntrySelected == EntryRSBOX)
- return GameVersion.RSBOX;
- return GameVersion.Any; //Default for no game selected
- }
- }
-
- public void SelectSaveGame(GameVersion Game)
- {
- switch (Game)
- {
- case GameVersion.COLO: if (HasCOLO) EntrySelected = EntryCOLO; break;
- case GameVersion.XD: if (HasXD) EntrySelected = EntryXD; break;
- case GameVersion.RSBOX: if (HasRSBOX) EntrySelected = EntryRSBOX; break;
- }
- }
-
- public string GCISaveName => GCISaveGameName();
- public readonly byte[] Data = Data;
-
- private string GCISaveGameName()
- {
- int offset = (DirectoryBlock_Used * BLOCK_SIZE) + (EntrySelected * DENTRY_SIZE);
- var span = Data.AsSpan(offset, DENTRY_SIZE);
- var dEntry = new DEntry(span);
-
- return GetSaveName(EncodingType, dEntry);
- }
-
- private static string GetSaveName(Encoding encoding, DEntry dEntry)
- {
- Span result = stackalloc char[4 + 1 + 2 + 1 + DENTRY_STRLEN];
- encoding.GetChars(dEntry.GameCode, result); // 4 bytes
- result[4] = '-';
- encoding.GetChars(dEntry.MakerCode, result[5..]); // 2 bytes
- result[7] = '-';
- encoding.GetChars(dEntry.FileName, result[8..]); // 0x20 bytes (max)
-
- var zero = result.IndexOf('\0');
- if (zero >= 0)
- result = result[..zero];
-
- return result.ToString();
- }
-
- public ReadOnlyMemory ReadSaveGameData()
- {
- var entry = EntrySelected;
- if (entry < 0)
- return default; // No entry selected
- return ReadSaveGameData(entry);
- }
-
- private ReadOnlyMemory ReadSaveGameData(int entry)
- {
- int offset = (DirectoryBlock_Used * BLOCK_SIZE) + (entry * DENTRY_SIZE);
- var span = Data.AsSpan(offset, DENTRY_SIZE);
- var dEntry = new DEntry(span);
-
- return Data.AsMemory(dEntry.FirstBlock * BLOCK_SIZE, dEntry.BlockCount * BLOCK_SIZE);
- }
-
- public void WriteSaveGameData(Span data)
- {
- var entry = EntrySelected;
- if (entry < 0) // Can't write anywhere
- return;
- WriteSaveGameData(data, entry);
- }
-
- private static DateTime Epoch => new(2000, 1, 1);
-
- private void WriteSaveGameData(Span data, int entry)
- {
- int offset = (DirectoryBlock_Used * BLOCK_SIZE) + (entry * DENTRY_SIZE);
- var span = Data.AsSpan(offset, DENTRY_SIZE);
- var dEntry = new DEntry(span);
-
- var blockFirst = dEntry.FirstBlock;
- var blockCount = dEntry.BlockCount;
-
- if (data.Length != blockCount * BLOCK_SIZE) // Invalid File Size
- return;
-
- var timestamp = (uint)EncounterDate.GetDateTimeGC().Subtract(Epoch).TotalSeconds;
-
- var dest = Data.AsSpan(blockFirst * BLOCK_SIZE);
- data[..(blockCount * BLOCK_SIZE)].CopyTo(dest);
- dEntry.ModificationTime = timestamp;
-
- // Revise the DEntry of the injected save data to match that of the main DEntry
- _ = new DEntry(dest[..DENTRY_SIZE])
- {
- ModificationTime = timestamp,
- FirstBlock = blockFirst,
- BlockCount = blockCount,
- };
- }
-
- private readonly ref struct DEntry(Span data)
- {
- public const int SIZE = 0x40;
- private readonly Span Data = data;
-
- public Span GameCode => Data[..4];
- public Span MakerCode => Data[4..6];
- public ref byte Unused6 => ref Data[6];
- public ref byte BannerAndIconFlags => ref Data[7];
- public Span FileName => Data[8..0x28];
-
- // Seconds since Jan 1, 2000
- public uint ModificationTime { get => ReadUInt32BigEndian(Data[0x28..]); set => WriteUInt32BigEndian(Data[0x28..], value); }
- public uint ImageOffset { get => ReadUInt32BigEndian(Data[0x2C..]); set => WriteUInt32BigEndian(Data[0x2C..], value); }
-
- // 2 bits per icon
- public ushort IconFormat { get => ReadUInt16BigEndian(Data[0x30..]); set => WriteUInt16BigEndian(Data[0x30..], value); }
-
- public ushort AnimationSpeed { get => ReadUInt16BigEndian(Data[0x32..]); set => WriteUInt16BigEndian(Data[0x32..], value); }
-
- public ref byte FilePermissions => ref Data[0x34];
- public ref byte CopyCounter => ref Data[0x35];
-
- public ushort FirstBlock { get => ReadUInt16BigEndian(Data[0x36..]); set => WriteUInt16BigEndian(Data[0x36..], value); }
- public ushort BlockCount { get => ReadUInt16BigEndian(Data[0x38..]); set => WriteUInt16BigEndian(Data[0x38..], value); }
-
- public ushort Unused3A { get => ReadUInt16BigEndian(Data[0x3A..]); set => WriteUInt16BigEndian(Data[0x3A..], value); }
- public uint CommentsAddress { get => ReadUInt32BigEndian(Data[0x3C..]); set => WriteUInt32BigEndian(Data[0x3C..], value); }
-
- public bool IsEmpty => ReadUInt32BigEndian(GameCode) is 0 or uint.MaxValue; // FF is "Uninitialized", but check for 0 to be sure.
- public DateTime ModificationDate => Epoch.AddSeconds(ModificationTime);
- }
-}
diff --git a/PKHeX.Core/Saves/SAV3RSBox.cs b/PKHeX.Core/Saves/SAV3RSBox.cs
index 17074bfe6..3a3b34d93 100644
--- a/PKHeX.Core/Saves/SAV3RSBox.cs
+++ b/PKHeX.Core/Saves/SAV3RSBox.cs
@@ -82,7 +82,7 @@ protected override byte[] GetFinalData()
return newFile;
MemoryCard.WriteSaveGameData(newFile);
- return MemoryCard.Data;
+ return MemoryCard.Data.ToArray();
}
private byte[] GetInnerData()
diff --git a/PKHeX.Core/Saves/SAV3XD.cs b/PKHeX.Core/Saves/SAV3XD.cs
index 7f6a35213..efd715a91 100644
--- a/PKHeX.Core/Saves/SAV3XD.cs
+++ b/PKHeX.Core/Saves/SAV3XD.cs
@@ -148,7 +148,7 @@ protected override byte[] GetFinalData()
return newFile;
MemoryCard.WriteSaveGameData(newFile);
- return MemoryCard.Data;
+ return MemoryCard.Data.ToArray();
}
private byte[] GetInnerData()
diff --git a/PKHeX.WinForms/MainWindow/Main.cs b/PKHeX.WinForms/MainWindow/Main.cs
index 2971786b1..27bce3652 100644
--- a/PKHeX.WinForms/MainWindow/Main.cs
+++ b/PKHeX.WinForms/MainWindow/Main.cs
@@ -714,26 +714,26 @@ private static bool CheckGCMemoryCard(SAV3GCMemoryCard memCard, string path)
var state = memCard.GetMemoryCardState();
switch (state)
{
- case GCMemoryCardState.NoPkmSaveGame:
+ case MemoryCardSaveStatus.NoPkmSaveGame:
WinFormsUtil.Error(MsgFileGameCubeNoGames, path);
return false;
- case GCMemoryCardState.DuplicateCOLO:
- case GCMemoryCardState.DuplicateXD:
- case GCMemoryCardState.DuplicateRSBOX:
+ case MemoryCardSaveStatus.DuplicateCOLO:
+ case MemoryCardSaveStatus.DuplicateXD:
+ case MemoryCardSaveStatus.DuplicateRSBOX:
WinFormsUtil.Error(MsgFileGameCubeDuplicate, path);
return false;
- case GCMemoryCardState.MultipleSaveGame:
+ case MemoryCardSaveStatus.MultipleSaveGame:
var game = SelectMemoryCardSaveGame(memCard);
if (game == GameVersion.Invalid) //Cancel
return false;
memCard.SelectSaveGame(game);
break;
- case GCMemoryCardState.SaveGameCOLO: memCard.SelectSaveGame(GameVersion.COLO); break;
- case GCMemoryCardState.SaveGameXD: memCard.SelectSaveGame(GameVersion.XD); break;
- case GCMemoryCardState.SaveGameRSBOX: memCard.SelectSaveGame(GameVersion.RSBOX); break;
+ case MemoryCardSaveStatus.SaveGameCOLO: memCard.SelectSaveGame(GameVersion.COLO); break;
+ case MemoryCardSaveStatus.SaveGameXD: memCard.SelectSaveGame(GameVersion.XD); break;
+ case MemoryCardSaveStatus.SaveGameRSBOX: memCard.SelectSaveGame(GameVersion.RSBOX); break;
default:
WinFormsUtil.Error(!SaveUtil.IsSizeValid(memCard.Data.Length) ? MsgFileGameCubeBad : GetHintInvalidFile(memCard.Data, path), path);
diff --git a/PKHeX.WinForms/Subforms/SAV_Database.cs b/PKHeX.WinForms/Subforms/SAV_Database.cs
index 5395f2cf7..bd315ac34 100644
--- a/PKHeX.WinForms/Subforms/SAV_Database.cs
+++ b/PKHeX.WinForms/Subforms/SAV_Database.cs
@@ -444,7 +444,7 @@ private static void TryAddPKMsFromSaveFilePath(ConcurrentBag dbTemp,
private static void TryAddPKMsFromMemoryCard(ConcurrentBag dbTemp, SAV3GCMemoryCard mc, string file)
{
var state = mc.GetMemoryCardState();
- if (state == GCMemoryCardState.Invalid)
+ if (state == MemoryCardSaveStatus.Invalid)
return;
if (mc.HasCOLO)