diff --git a/PKHeX.WinForms/MainWindow/Main.cs b/PKHeX.WinForms/MainWindow/Main.cs index 1ed2e1593..c13d6d77a 100644 --- a/PKHeX.WinForms/MainWindow/Main.cs +++ b/PKHeX.WinForms/MainWindow/Main.cs @@ -725,7 +725,7 @@ namespace PKHeX.WinForms string ext = Path.GetExtension(path); FileInfo fi = new FileInfo(path); - if (fi.Length > 0x10009C && fi.Length != 0x380000) + if (fi.Length > 0x10009C && fi.Length != 0x380000 && ! SAV3GCMemoryCard.IsMemoryCardSize(fi.Length)) WinFormsUtil.Error("Input file is too large." + Environment.NewLine + $"Size: {fi.Length} bytes", path); else if (fi.Length < 32) WinFormsUtil.Error("Input file is too small." + Environment.NewLine + $"Size: {fi.Length} bytes", path); @@ -782,6 +782,20 @@ namespace PKHeX.WinForms { openSAV(sav, path); } + else if ((SAV3GCMemoryCard.IsMemoryCardSize(input))) + { + SAV3GCMemoryCard MC = CheckGCMemoryCard(input, path); + if (MC == null) + return; + if ((sav = SaveUtil.getVariantSAV(MC)) != null) + { + openSAV(sav, path); + } + else + WinFormsUtil.Error("Attempted to load an unsupported file type/size.", + $"File Loaded:{Environment.NewLine}{path}", + $"File Size:{Environment.NewLine}{input.Length} bytes (0x{input.Length:X4})"); + } else if ((temp = PKMConverter.getPKMfromBytes(input, prefer: ext.Length > 0 ? (ext.Last() - 0x30)&7 : SAV.Generation)) != null) { PKM pk = PKMConverter.convertToFormat(temp, SAV.PKMType, out c); @@ -902,6 +916,83 @@ namespace PKHeX.WinForms openSAV(s, s.FileName); return true; } + private GameVersion SelectMemoryCardSaveGame(SAV3GCMemoryCard MC) + { + //SaveGameCount + if (MC.SaveGameCount == 1) + return MC.SelectedGameVersion; + + GameVersion[] Options = new GameVersion[2]; + string[] Names = new string[2]; + if (MC.SaveGameCount == 3) + { + var drGC3Select = WinFormsUtil.Prompt(MessageBoxButtons.YesNoCancel, $"Pokémon Colloseum, Pokémon XD and Pokémon RS Box Save Files detected. Select game to edit.", + "Yes: Pokémon Colloseum/XD" + Environment.NewLine + "No: Pokémon RS Box"); + if (drGC3Select == DialogResult.Cancel) + return GameVersion.CXD; + if(drGC3Select == DialogResult.No) + return GameVersion.RSBOX; + Options = new[] { GameVersion.COLO, GameVersion.XD }; + Names = new[] { "Pokémon Colloseum", "Pokémon XD" }; + } + + // 2 games only + if (MC.SaveGameCount == 2 && MC.HaveRSBoxSaveGame) + { + if( MC.HaveColloseumSaveGame) + { + Options[0] = GameVersion.COLO; + Names[0] = "Pokémon Colloseum"; + } + else //XD + { + Options[0] = GameVersion.XD; + Names[0] = "Pokémon XD"; + } + Options[1] = GameVersion.RSBOX; + Names[1] = "Pokémon RS Box"; + } + else // RXBox discarted + { + Options = new[] { GameVersion.COLO, GameVersion.XD }; + Names = new[] { "Pokémon Colloseum", "Pokémon XD" }; + } + + var drGCSelect = WinFormsUtil.Prompt(MessageBoxButtons.YesNoCancel, $"{Names[0]} and {Names[1]} Save Files detected. Select game to edit.", + $"Yes: {Names[0]}" + Environment.NewLine + $"No: {Names[1]}"); + if (drGCSelect == DialogResult.Cancel) + return GameVersion.CXD; + if (drGCSelect == DialogResult.Yes) + return Options[0]; + return Options[1]; + } + + private SAV3GCMemoryCard CheckGCMemoryCard(byte[] Data, string path) + { + SAV3GCMemoryCard MC = new SAV3GCMemoryCard(); + GCMemoryCardState MCState = MC.LoadMemoryCardFile(Data); + switch(MCState) + { + case GCMemoryCardState.Invalid: { WinFormsUtil.Error("Invalid or corrupted GC Memory Card. Aborting.", path); return null; } + case GCMemoryCardState.NoPkmSaveGame: { WinFormsUtil.Error("GC Memory Card without any Pokémon save file. Aborting.", path); return null; } + case GCMemoryCardState.ColloseumSaveGameDuplicated: { WinFormsUtil.Error("GC Memory Card with multiple Pokémon Colloseum save files. Aborting.", path); return null; } + case GCMemoryCardState.XDSaveGameDuplicated: { WinFormsUtil.Error("GC Memory Card with multiple Pokémon XD save files. Aborting.", path); return null; } + case GCMemoryCardState.RSBoxSaveGameDuplicated: { WinFormsUtil.Error("GC Memory Card with multiple Pokémon RS Box save files. Aborting.", path); return null; } + case GCMemoryCardState.MultipleSaveGame: + { + GameVersion Game = SelectMemoryCardSaveGame(MC); + if (Game == GameVersion.CXD) //Cancel + return null; + MC.SelectSaveGame(Game); + break; + } + case GCMemoryCardState.ColloseumSaveGame: { MC.SelectSaveGame(GameVersion.COLO); break; } + case GCMemoryCardState.XDSaveGame: { MC.SelectSaveGame(GameVersion.XD); break; } + case GCMemoryCardState.RSBoxSaveGame: { MC.SelectSaveGame(GameVersion.RSBOX); break; } + } + return MC; + } + private void openSAV(SaveFile sav, string path) { if (sav == null || sav.Version == GameVersion.Invalid) @@ -3319,9 +3410,10 @@ namespace PKHeX.WinForms SAV.CurrentBox = CB_BoxSelect.SelectedIndex; bool dsv = Path.GetExtension(main.FileName)?.ToLower() == ".dsv"; + bool gci = Path.GetExtension(main.FileName)?.ToLower() == ".gci"; try { - File.WriteAllBytes(main.FileName, SAV.Write(dsv)); + File.WriteAllBytes(main.FileName, SAV.Write(dsv, gci)); SAV.Edited = false; WinFormsUtil.Alert("SAV exported to:", main.FileName); } diff --git a/PKHeX/PKHeX.Core.csproj b/PKHeX/PKHeX.Core.csproj index f39d07cb1..efbd88e1e 100644 --- a/PKHeX/PKHeX.Core.csproj +++ b/PKHeX/PKHeX.Core.csproj @@ -211,6 +211,7 @@ True Resources.resx + diff --git a/PKHeX/Saves/SAV3Colosseum.cs b/PKHeX/Saves/SAV3Colosseum.cs index bcdc8356f..714b954c8 100644 --- a/PKHeX/Saves/SAV3Colosseum.cs +++ b/PKHeX/Saves/SAV3Colosseum.cs @@ -7,8 +7,17 @@ namespace PKHeX.Core public sealed class SAV3Colosseum : SaveFile, IDisposable { public override string BAKName => $"{FileName} [{OT} ({Version}) - {PlayTimeString}].bak"; - public override string Filter => "GameCube Save File|*.gci|All Files|*.*"; - public override string Extension => ".gci"; + public override string Filter + { + get + { + if (IsMemoryCardSave) + return "Memory Card File|*.raw,*.bin|GameCube Save File|*.gci|All Files|*.*"; + return "GameCube Save File|*.gci|All Files|*.*"; + } + } + + public override string Extension => IsMemoryCardSave ? ".raw" : ".gci"; // 3 Save files are stored // 0x0000-0x6000 contains memory card data @@ -29,6 +38,13 @@ namespace PKHeX.Core private readonly int Memo; private readonly ushort[] LegalItems, LegalKeyItems, LegalBalls, LegalTMHMs, LegalBerries, LegalCologne; private readonly int OFS_PouchCologne; + private SAV3GCMemoryCard MC; + public override bool IsMemoryCardSave => MC != null; + public SAV3Colosseum(byte[] data,SAV3GCMemoryCard MC) + : this(data) + { + this.MC = MC; + } public SAV3Colosseum(byte[] data = null) { Data = data == null ? new byte[SaveUtil.SIZE_G3COLO] : (byte[])data.Clone(); @@ -100,6 +116,10 @@ namespace PKHeX.Core private readonly byte[] OriginalData; public override byte[] Write(bool DSV) + { + return Write(DSV,false); + } + public override byte[] Write(bool DSV, bool GCI = false) { StrategyMemo.FinalData.CopyTo(Data, Memo); setChecksums(); @@ -111,6 +131,9 @@ namespace PKHeX.Core // Put save slot back in original save data byte[] newFile = (byte[])OriginalData.Clone(); Array.Copy(newSAV, 0, newFile, SLOT_START + SaveIndex*SLOT_SIZE, newSAV.Length); + //Return the complete memory card only if the save was loaded from a memory card and gci output was not selecte + if (IsMemoryCardSave && !GCI) + return MC.WriteSaveGameData(newFile.ToArray()); return Header.Concat(newFile).ToArray(); } diff --git a/PKHeX/Saves/SAV3GCMemoryCard.cs b/PKHeX/Saves/SAV3GCMemoryCard.cs new file mode 100644 index 000000000..af68ba79b --- /dev/null +++ b/PKHeX/Saves/SAV3GCMemoryCard.cs @@ -0,0 +1,370 @@ +using System; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +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/ + */ + + public enum GCMemoryCardState + { + Invalid, + NoPkmSaveGame, + ColloseumSaveGame, + XDSaveGame, + RSBoxSaveGame, + MultipleSaveGame, + ColloseumSaveGameDuplicated, + XDSaveGameDuplicated, + RSBoxSaveGameDuplicated, + } + + public sealed class SAV3GCMemoryCard + { + const int BLOCK_SIZE = 0x2000; + const int MBIT_TO_BLOCKS = 0x10; + const int DENTRY_STRLEN = 0x20; + const int DENTRY_SIZE = 0x40; + int NumEntries_Directory { get { return BLOCK_SIZE / DENTRY_SIZE; } } + + internal readonly string[] Colloseum_GameCode = new[] + { + "GC6J","GC6E","GC6P" // NTSC-J, NTSC-U, PAL + }; + internal readonly string[] XD_GameCode = new[] + { + "GXXJ","GXXE","GXXP" // NTSC-J, NTSC-U, PAL + }; + internal readonly string[] Box_GameCode = new[] + { + "GPXJ","GPXE","GPXP" // NTSC-J, NTSC-U, PAL + }; + internal static readonly int[] validMCSizes = new[] + { + 524288, // 512KB 59 Blocks Memory Card + 1048576, // 1MB + 2097152, // 2MB + 4194304, // 4MB 251 Blocks Memory Card + 8388608, // 8MB + 16777216, // 16MB 1019 Blocks Default Dolphin Memory Card + 33554432, // 64MB + 67108864 // 128 MB + }; + internal readonly byte[] RawEmpty_DEntry = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }; + + // 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; + + // BigEndian treatment + private ushort SwapEndian(ushort x) + { + return (ushort)((ushort)((x & 0xff) << 8) | ((x >> 8) & 0xff)); + } + private ushort BigEndianToUint16(byte[] value, int startIndex) + { + ushort x = BitConverter.ToUInt16(value, startIndex); + if (!BitConverter.IsLittleEndian) + return x; + return SwapEndian(x); + } + + private void calc_checksumsBE(int blockoffset, int offset, int length, ref ushort csum, ref ushort inv_csum) + { + csum = inv_csum = 0; + var ofs = blockoffset * BLOCK_SIZE + offset; + + for (int i = 0; i < length; ++i) + { + csum += BigEndianToUint16(RawData, ofs + i * 2); + inv_csum += SwapEndian((ushort)(BitConverter.ToUInt16(RawData, ofs + i * 2) ^ 0xffff)); + } + if (csum == 0xffff) + { + csum = 0; + } + if (inv_csum == 0xffff) + { + inv_csum = 0; + } + } + + private uint TestChecksums() + { + ushort csum = 0, csum_inv = 0; + + uint results = 0; + + calc_checksumsBE(Header_Block, 0, 0xFE, ref csum, ref csum_inv); + if ((Header_Checksum != csum) || (Header_Checksum_Inv != csum_inv)) + results |= 1; + + calc_checksumsBE(Directory_Block, 0, 0xFFE, ref csum, ref csum_inv); + if ((Directory_Checksum != csum) || (Directory_Checksum_Inv != csum_inv)) + results |= 2; + + calc_checksumsBE(DirectoryBackup_Block, 0, 0xFFE, ref csum, ref csum_inv); + if ((DirectoryBck_Checksum != csum) || (DirectoryBck_Checksum_Inv != csum_inv)) + results |= 4; + + calc_checksumsBE(BlockAlloc_Block, 4, 0xFFE, ref csum, ref csum_inv); + if ((BlockAlloc_Checksum != csum) || (BlockAlloc_Checksum_Inv != csum_inv)) + results |= 8; + + calc_checksumsBE(BlockAllocBackup_Block, 4, 0xFFE, ref csum, ref csum_inv); + if ((BlockAllocBck_Checksum != csum) || (BlockAllocBck_Checksum_Inv != csum_inv)) + results |= 16; + + return results; + } + + int Header_Size { get { return BigEndianToUint16(RawData, Header_Block * BLOCK_SIZE + 0x0022); } } + ushort Header_Checksum { get { return BigEndianToUint16(RawData, Header_Block * BLOCK_SIZE + 0x01fc); } } + ushort Header_Checksum_Inv { get { return BigEndianToUint16(RawData, Header_Block * BLOCK_SIZE + 0x01fe); } } + + //Encoding (Windows-1252 or Shift JIS) + int Header_Encoding { get { return BigEndianToUint16(RawData, Header_Block * BLOCK_SIZE + 0x0024); } } + bool Header_Japanese { get { return Header_Encoding == 1; } } + Encoding Header_EncodingType { get { return Header_Japanese ? Encoding.GetEncoding(1252) : Encoding.GetEncoding(932); } } + + int Directory_UpdateCounter { get { return BigEndianToUint16(RawData, Directory_Block * BLOCK_SIZE + 0x1ffa); } } + int Directory_Checksum { get { return BigEndianToUint16(RawData, Directory_Block * BLOCK_SIZE + 0x1ffc); } } + int Directory_Checksum_Inv { get { return BigEndianToUint16(RawData, Directory_Block * BLOCK_SIZE + 0x1ffe); } } + + int DirectoryBck_UpdateCounter { get { return BigEndianToUint16(RawData, DirectoryBackup_Block * BLOCK_SIZE + 0x1ffa); } } + int DirectoryBck_Checksum { get { return BigEndianToUint16(RawData, DirectoryBackup_Block * BLOCK_SIZE + 0x1ffc); } } + int DirectoryBck_Checksum_Inv { get { return BigEndianToUint16(RawData, DirectoryBackup_Block * BLOCK_SIZE + 0x1ffe); } } + + int BlockAlloc_Checksum { get { return BigEndianToUint16(RawData, BlockAlloc_Block * BLOCK_SIZE + 0x0000); } } + int BlockAlloc_Checksum_Inv { get { return BigEndianToUint16(RawData, BlockAlloc_Block * BLOCK_SIZE + 0x0002); } } + + int BlockAllocBck_Checksum { get { return BigEndianToUint16(RawData, BlockAllocBackup_Block * BLOCK_SIZE + 0x0000); } } + int BlockAllocBck_Checksum_Inv { get { return BigEndianToUint16(RawData, BlockAllocBackup_Block * BLOCK_SIZE + 0x0002); } } + + byte[] RawData; + int DirectoryBlock_Used; + int NumBlocks => RawData.Length / BLOCK_SIZE - 5; + + int Colloseum_Entry = -1; + int XD_Entry = -1; + int RSBox_Entry = -1; + int Selected_Entry = -1; + public int SaveGameCount = 0; + public bool HaveColloseumSaveGame => Colloseum_Entry > -1; + public bool HaveXDSaveGame => XD_Entry > -1; + public bool HaveRSBoxSaveGame => RSBox_Entry > -1; + + private bool IsCorruptedMemoryCard() + { + uint csums = TestChecksums(); + + if ((csums & 0x1) == 1) + { + // Header checksum failed + // invalid files do not always get here + return true; + } + + if ((csums & 0x2) == 1) // directory checksum error! + { + if ((csums & 0x4) == 1) // backup is also wrong! + { + // Directory checksum and directory backup checksum failed + return true; + } + else + { + // backup is correct, restore + Array.Copy(RawData, DirectoryBackup_Block * BLOCK_SIZE, RawData, Directory_Block * BLOCK_SIZE, BLOCK_SIZE); + Array.Copy(RawData, BlockAlloc_Block * BLOCK_SIZE, RawData, BlockAllocBackup_Block * BLOCK_SIZE, BLOCK_SIZE); + + // update checksums + csums = TestChecksums(); + } + } + + if ((csums & 0x8) == 1) // BAT checksum error! + { + if ((csums & 0x10) == 1) // backup is also wrong! + { + // Block Allocation Table checksum failed + return true; + } + else + { + // backup is correct, restore + Array.Copy(RawData, DirectoryBackup_Block * BLOCK_SIZE, RawData, Directory_Block * BLOCK_SIZE, BLOCK_SIZE); + Array.Copy(RawData, BlockAlloc_Block * BLOCK_SIZE, RawData, BlockAllocBackup_Block * BLOCK_SIZE, BLOCK_SIZE); + } + } + return false; + } + + public static bool IsMemoryCardSize(long Size) + { + if (Size > int.MaxValue) + return false; + return validMCSizes.Contains(((int)Size)); + } + + public static bool IsMemoryCardSize(byte[] Data) + { + return validMCSizes.Contains(Data.Length); + } + + public GCMemoryCardState LoadMemoryCardFile(byte[] Data) + { + RawData = Data; + if (!IsMemoryCardSize(RawData)) + // Invalid size + return GCMemoryCardState.Invalid; + + // Size in megabits, not megabytes + int m_sizeMb = ((RawData.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 + if (DirectoryBck_UpdateCounter > Directory_UpdateCounter) + DirectoryBlock_Used = DirectoryBackup_Block; + else + DirectoryBlock_Used = Directory_Block; + + string Empty_DEntry = Header_EncodingType.GetString(RawEmpty_DEntry, 0, 4); + // Search for pokemon savegames in the directory + for (int i = 0; i < NumEntries_Directory; i++) + { + string GameCode = Header_EncodingType.GetString(RawData, DirectoryBlock_Used * BLOCK_SIZE + i * DENTRY_SIZE, 4); + if (GameCode == Empty_DEntry) + continue; + int FirstBlock = BigEndianToUint16(RawData, DirectoryBlock_Used * BLOCK_SIZE + i * DENTRY_SIZE + 0x36); + int BlockCount = BigEndianToUint16(RawData, DirectoryBlock_Used * BLOCK_SIZE + i * DENTRY_SIZE + 0x38); + // Memory card directory contains info for deleted files with boundaries beyond memory card size, ignore + if (FirstBlock + BlockCount > NumBlocks) + continue; + if (Colloseum_GameCode.Contains(GameCode)) + { + if (Colloseum_Entry > -1) + // Memory Card contains more than 1 Pokémon Colloseum save data. + // It is not possible with a real GC nor with Dolphin to have multiple savegames in the same MC + // If two are found assume corrupted memory card, it wont work with the games after all + return GCMemoryCardState.ColloseumSaveGameDuplicated; + + Colloseum_Entry = i; + SaveGameCount++; + } + if (XD_GameCode.Contains(GameCode)) + { + if (XD_Entry > -1) + // Memory Card contains more than 1 Pokémon XD save data. + return GCMemoryCardState.XDSaveGameDuplicated; + XD_Entry = i; + SaveGameCount++; + } + if (Box_GameCode.Contains(GameCode)) + { + if (RSBox_Entry > -1) + // Memory Card contains more than 1 Pokémon RS Box save data. + return GCMemoryCardState.RSBoxSaveGameDuplicated; + RSBox_Entry = i; + SaveGameCount++; + } + } + if (SaveGameCount == 0) + // There is no savedata from a Pokémon GameCube game. + return GCMemoryCardState.NoPkmSaveGame; + + if (SaveGameCount > 1) + return GCMemoryCardState.MultipleSaveGame; + + if (Colloseum_Entry > -1) + { + Selected_Entry = Colloseum_Entry; + return GCMemoryCardState.ColloseumSaveGame; + } + if(XD_Entry > -1) + { + Selected_Entry = XD_Entry; + return GCMemoryCardState.XDSaveGame; + } + Selected_Entry = RSBox_Entry; + return GCMemoryCardState.RSBoxSaveGame; + } + + public GameVersion SelectedGameVersion + { + get + { + if(Colloseum_Entry > -1 && Selected_Entry == Colloseum_Entry) + return GameVersion.COLO; + if (XD_Entry > -1 && Selected_Entry == XD_Entry) + return GameVersion.XD; + if (RSBox_Entry > -1 && Selected_Entry == RSBox_Entry) + return GameVersion.RSBOX; + return GameVersion.CXD; //Default for no game selected + } + } + + public void SelectSaveGame(GameVersion Game) + { + switch(Game) + { + case GameVersion.COLO: if (Colloseum_Entry > -1) Selected_Entry = Colloseum_Entry; break; + case GameVersion.XD: if (XD_Entry > -1) Selected_Entry = XD_Entry; break; + case GameVersion.RSBOX: if (RSBox_Entry > -1) Selected_Entry = RSBox_Entry; break; + } + } + + public string getGCISaveGameName() + { + string GameCode = Header_EncodingType.GetString(RawData, DirectoryBlock_Used * BLOCK_SIZE + Selected_Entry * DENTRY_SIZE, 4); + string Makercode = Header_EncodingType.GetString(RawData, DirectoryBlock_Used * BLOCK_SIZE + Selected_Entry * DENTRY_SIZE + 0x04, 2); + string FileName = Header_EncodingType.GetString(RawData, DirectoryBlock_Used * BLOCK_SIZE + Selected_Entry * DENTRY_SIZE + 0x08, DENTRY_STRLEN); + + return Makercode + "-" + GameCode + "-" + FileName.Replace("\0", "") + ".gci"; + } + + public byte[] ReadSaveGameData() + { + if (Selected_Entry == -1) + // Not selected any entry + return null; + + int FirstBlock = BigEndianToUint16(RawData, DirectoryBlock_Used * BLOCK_SIZE + Selected_Entry * DENTRY_SIZE + 0x36); + int BlockCount = BigEndianToUint16(RawData, DirectoryBlock_Used * BLOCK_SIZE + Selected_Entry * DENTRY_SIZE + 0x38); + + byte[] SaveData = new byte[BlockCount * BLOCK_SIZE]; + Array.Copy(RawData, FirstBlock * BLOCK_SIZE, SaveData, 0, BlockCount * BLOCK_SIZE); + + return SaveData; + } + + public byte[] WriteSaveGameData(byte[] SaveData) + { + if (Selected_Entry == -1) + // Not selected any entry + return RawData; + + int FirstBlock = BigEndianToUint16(RawData, DirectoryBlock_Used * BLOCK_SIZE + Selected_Entry * DENTRY_SIZE + 0x36); + int BlockCount = BigEndianToUint16(RawData, DirectoryBlock_Used * BLOCK_SIZE + Selected_Entry * DENTRY_SIZE + 0x38); + + if (SaveData.Length != BlockCount * BLOCK_SIZE) + // Invalid File Size + return null; + + Array.Copy(SaveData, 0, RawData, FirstBlock * BLOCK_SIZE, BlockCount * BLOCK_SIZE); + return RawData; + } + } +} diff --git a/PKHeX/Saves/SAV3RSBox.cs b/PKHeX/Saves/SAV3RSBox.cs index 728b87d59..01ec388a2 100644 --- a/PKHeX/Saves/SAV3RSBox.cs +++ b/PKHeX/Saves/SAV3RSBox.cs @@ -6,9 +6,23 @@ namespace PKHeX.Core public sealed class SAV3RSBox : SaveFile { public override string BAKName => $"{FileName} [{Version} #{SaveCount:0000}].bak"; - public override string Filter => "GameCube Save File|*.gci|All Files|*.*"; - public override string Extension => ".gci"; - + public override string Filter + { + get + { + if (IsMemoryCardSave) + return "Memory Card File|*.raw,*.bin|GameCube Save File|*.gci|All Files|*.*"; + return "GameCube Save File|*.gci|All Files|*.*"; + } + } + public override string Extension => IsMemoryCardSave ? ".raw" : ".gci"; + private SAV3GCMemoryCard MC; + public override bool IsMemoryCardSave => MC != null; + public SAV3RSBox(byte[] data, SAV3GCMemoryCard MC) + : this(data) + { + this.MC = MC; + } public SAV3RSBox(byte[] data = null) { Data = data == null ? new byte[SaveUtil.SIZE_G3BOX] : (byte[])data.Clone(); @@ -52,6 +66,10 @@ namespace PKHeX.Core private const int BLOCK_SIZE = 0x2000; private const int SIZE_RESERVED = BLOCK_COUNT * BLOCK_SIZE; // unpacked box data public override byte[] Write(bool DSV) + { + return Write(DSV, false); + } + public override byte[] Write(bool DSV, bool GCI = false) { // Copy Box data back to block foreach (RSBOX_Block b in Blocks) @@ -63,6 +81,9 @@ namespace PKHeX.Core foreach (RSBOX_Block b in Blocks) b.Data.CopyTo(Data, b.Offset); byte[] newFile = getData(0, Data.Length - SIZE_RESERVED); + //Return the complete memory card only if the save was loaded from a memory card and gci output was not selecte + if (IsMemoryCardSave && !GCI) + return MC.WriteSaveGameData(newFile.ToArray()); return Header.Concat(newFile).ToArray(); } diff --git a/PKHeX/Saves/SAV3XD.cs b/PKHeX/Saves/SAV3XD.cs index da5a811f2..0ad8df4da 100644 --- a/PKHeX/Saves/SAV3XD.cs +++ b/PKHeX/Saves/SAV3XD.cs @@ -6,8 +6,16 @@ namespace PKHeX.Core public sealed class SAV3XD : SaveFile { public override string BAKName => $"{FileName} [{OT} ({Version}) #{SaveCount:0000}].bak"; - public override string Filter => "GameCube Save File|*.gci|All Files|*.*"; - public override string Extension => ".gci"; + public override string Filter + { + get + { + if (IsMemoryCardSave) + return "Memory Card File|*.raw|GameCube Save File|*.gci|All Files|*.*"; + return "GameCube Save File|*.gci|All Files|*.*"; + } + } + public override string Extension => IsMemoryCardSave ? ".raw" : ".gci"; private const int SLOT_SIZE = 0x28000; private const int SLOT_START = 0x6000; @@ -22,6 +30,13 @@ namespace PKHeX.Core private readonly ushort[] LegalItems, LegalKeyItems, LegalBalls, LegalTMHMs, LegalBerries, LegalCologne, LegalDisc; private readonly int OFS_PouchCologne, OFS_PouchDisc; private readonly int[] subOffsets = new int[16]; + private SAV3GCMemoryCard MC; + public override bool IsMemoryCardSave => MC != null; + public SAV3XD(byte[] data, SAV3GCMemoryCard MC) + : this(data) + { + this.MC = MC; + } public SAV3XD(byte[] data = null) { Data = data == null ? new byte[SaveUtil.SIZE_G3XD] : (byte[])data.Clone(); @@ -109,6 +124,10 @@ namespace PKHeX.Core private readonly byte[] OriginalData; public override byte[] Write(bool DSV) + { + return Write(DSV, false); + } + public override byte[] Write(bool DSV, bool GCI = false) { // Set Memo Back StrategyMemo.FinalData.CopyTo(Data, Memo); @@ -124,6 +143,9 @@ namespace PKHeX.Core // Put save slot back in original save data byte[] newFile = (byte[])OriginalData.Clone(); Array.Copy(newSAV, 0, newFile, SLOT_START + SaveIndex * SLOT_SIZE, newSAV.Length); + //Return the complete memory card only if the save was loaded from a memory card and gci output was not selected + if (IsMemoryCardSave && !GCI) + return MC.WriteSaveGameData(newFile.ToArray()); return Header.Concat(newFile).ToArray(); } diff --git a/PKHeX/Saves/SaveFile.cs b/PKHeX/Saves/SaveFile.cs index b5a6d3619..679957580 100644 --- a/PKHeX/Saves/SaveFile.cs +++ b/PKHeX/Saves/SaveFile.cs @@ -31,7 +31,7 @@ namespace PKHeX.Core int gen = f.Last() - 0x30; return 3 <= gen && gen <= Generation; }).ToArray(); - + public virtual bool IsMemoryCardSave => false; // General PKM Properties public abstract Type PKMType { get; } public abstract PKM getPKM(byte[] data); @@ -44,6 +44,10 @@ namespace PKHeX.Core public ushort[] HeldItems { get; protected set; } // General SAV Properties + public virtual byte[] Write(bool DSV, bool GCI = false) + { + return Write(DSV); + } public virtual byte[] Write(bool DSV) { setChecksums(); diff --git a/PKHeX/Saves/SaveUtil.cs b/PKHeX/Saves/SaveUtil.cs index 504bf6031..0239f3935 100644 --- a/PKHeX/Saves/SaveUtil.cs +++ b/PKHeX/Saves/SaveUtil.cs @@ -418,6 +418,28 @@ namespace PKHeX.Core sav.Footer = footer; return sav; } + public static SaveFile getVariantSAV(SAV3GCMemoryCard MC) + { + // Pre-check for header/footer signatures + SaveFile sav; + byte[] header = new byte[0], footer = new byte[0]; + byte[] data = MC.ReadSaveGameData(); + CheckHeaderFooter(ref data, ref header, ref footer); + + switch (MC.SelectedGameVersion) + { + // Side Games + case GameVersion.COLO: sav = new SAV3Colosseum(data,MC); break; + case GameVersion.XD: sav = new SAV3XD(data, MC); break; + case GameVersion.RSBOX: sav = new SAV3RSBox(data, MC); break; + + // No pattern matched + default: return null; + } + sav.Header = header; + sav.Footer = footer; + return sav; + } /// /// Creates an instance of a SaveFile with a blank base.