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)