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.