From a38e923e8394cbaca35aa20cbebdef167fcbd074 Mon Sep 17 00:00:00 2001 From: javierhimura Date: Sun, 2 Apr 2017 16:53:46 +0200 Subject: [PATCH 1/7] Load pokemon savegames directly from gamecube memory card files. This files can be saved again in the same memory card file or exported as a gci file with only the savegame. Do not allow to resize memory cards nor create a new memory card from a gci files If the MC contains multiple save files the program ask the user what game he want to edit --- PKHeX.WinForms/MainWindow/Main.cs | 96 +++++++- PKHeX/PKHeX.Core.csproj | 1 + PKHeX/Saves/SAV3Colosseum.cs | 27 ++- PKHeX/Saves/SAV3GCMemoryCard.cs | 370 ++++++++++++++++++++++++++++++ PKHeX/Saves/SAV3RSBox.cs | 27 ++- PKHeX/Saves/SAV3XD.cs | 26 ++- PKHeX/Saves/SaveFile.cs | 6 +- PKHeX/Saves/SaveUtil.cs | 22 ++ 8 files changed, 565 insertions(+), 10 deletions(-) create mode 100644 PKHeX/Saves/SAV3GCMemoryCard.cs 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 @@ private void openQuick(string path, bool force = false) 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 @@ private void openFile(byte[] input, string path, string ext) { 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 @@ private bool openXOR(byte[] input, string path) 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 @@ private void clickExportSAV(object sender, EventArgs e) 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 @@ public sealed class SAV3Colosseum : SaveFile, IDisposable 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 @@ public SAV3Colosseum(byte[] data = null) 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 @@ public override byte[] Write(bool DSV) // 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 @@ public SAV3RSBox(byte[] data = null) 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 @@ public override byte[] Write(bool DSV) 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 @@ public sealed class SAV3XD : SaveFile 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 @@ public SAV3XD(byte[] data = null) 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 @@ public override byte[] Write(bool DSV) // 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 @@ public abstract class SaveFile 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 @@ public abstract class SaveFile 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 @@ public static SaveFile getVariantSAV(byte[] data) 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. From b53a101f20f7a07bd5ab0a3c4619e00e55a1da14 Mon Sep 17 00:00:00 2001 From: javierhimura Date: Sun, 2 Apr 2017 16:54:53 +0200 Subject: [PATCH 2/7] Fix for XD. XF files testes from inside a memory card only return true in getIsG3XDSAV if ignore the second pass in the for, only the first is equal to the sequence --- PKHeX/Saves/SaveUtil.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/PKHeX/Saves/SaveUtil.cs b/PKHeX/Saves/SaveUtil.cs index 0239f3935..dc274a4d5 100644 --- a/PKHeX/Saves/SaveUtil.cs +++ b/PKHeX/Saves/SaveUtil.cs @@ -274,12 +274,16 @@ public static GameVersion getIsG3XDSAV(byte[] data) // Check the intro bytes for each save slot byte[] slotintroXD = { 0x01, 0x01, 0x01, 0x00 }; int offset = data.Length - SIZE_G3XD; + // For XD savegames inside a memory card only the firs sequence is equal to slotintroXD + bool valid = false; for (int i = 0; i < 2; i++) { var ident = data.Skip(0x6000 + offset + 0x28000 * i).Take(4); - if (!ident.SequenceEqual(slotintroXD)) - return GameVersion.Invalid; + if (ident.SequenceEqual(slotintroXD)) + valid = true; } + if(!valid) + return GameVersion.Invalid; return GameVersion.XD; } /// Determines the type of 4th gen save From 699dadcccbcb4a045bf92eaba26388339459d03a Mon Sep 17 00:00:00 2001 From: javierhimura Date: Sun, 2 Apr 2017 17:10:40 +0200 Subject: [PATCH 3/7] The UI was checking wrong if a pokemon is shadow or purified The internal data of GC games for purification for shadow pokemon does not have values from 0-non shadow to 100-total shadow, it have values sarting from the initial shadow hearth gauge to 0-non shadow. --- PKHeX.WinForms/MainWindow/Main.Designer.cs | 12 ++---------- PKHeX.WinForms/MainWindow/Main.cs | 2 +- PKHeX.WinForms/MainWindow/MainCK3.cs | 6 ++---- PKHeX.WinForms/MainWindow/MainXK3.cs | 6 ++---- 4 files changed, 7 insertions(+), 19 deletions(-) diff --git a/PKHeX.WinForms/MainWindow/Main.Designer.cs b/PKHeX.WinForms/MainWindow/Main.Designer.cs index d305dddb2..036d79024 100644 --- a/PKHeX.WinForms/MainWindow/Main.Designer.cs +++ b/PKHeX.WinForms/MainWindow/Main.Designer.cs @@ -1597,16 +1597,8 @@ public void InitializeComponent() // this.NUD_Purification.Location = new System.Drawing.Point(110, 1); this.NUD_Purification.Margin = new System.Windows.Forms.Padding(0, 1, 0, 0); - this.NUD_Purification.Maximum = new decimal(new int[] { - 0, - 0, - 0, - 0}); - this.NUD_Purification.Minimum = new decimal(new int[] { - 100, - 0, - 0, - -2147483648}); + this.NUD_Purification.Maximum = new decimal(int.MaxValue); + this.NUD_Purification.Minimum = new decimal(0); this.NUD_Purification.Name = "NUD_Purification"; this.NUD_Purification.Size = new System.Drawing.Size(51, 20); this.NUD_Purification.TabIndex = 103; diff --git a/PKHeX.WinForms/MainWindow/Main.cs b/PKHeX.WinForms/MainWindow/Main.cs index c13d6d77a..2d03f5da6 100644 --- a/PKHeX.WinForms/MainWindow/Main.cs +++ b/PKHeX.WinForms/MainWindow/Main.cs @@ -2975,7 +2975,7 @@ private void updatePurification(object sender, EventArgs e) if (!fieldsLoaded) return; fieldsLoaded = false; - CHK_Shadow.Checked = NUD_Purification.Value == 0; + CHK_Shadow.Checked = NUD_Purification.Value > 0; fieldsLoaded = true; } private void updateShadowCHK(object sender, EventArgs e) diff --git a/PKHeX.WinForms/MainWindow/MainCK3.cs b/PKHeX.WinForms/MainWindow/MainCK3.cs index 236427d92..c0bd39963 100644 --- a/PKHeX.WinForms/MainWindow/MainCK3.cs +++ b/PKHeX.WinForms/MainWindow/MainCK3.cs @@ -102,13 +102,11 @@ private void populateFieldsCK3() if (ck3.ShadowID > 0) { int puri = ck3.Purification; - if (puri > NUD_Purification.Maximum) - puri = 0; - else if (puri < NUD_Purification.Minimum) + if (puri < NUD_Purification.Minimum) puri = (int)NUD_Purification.Minimum; NUD_Purification.Value = puri; - CHK_Shadow.Checked = puri < 0; + CHK_Shadow.Checked = puri > 0; NUD_ShadowID.Value = Math.Max(ck3.ShadowID, 0); } diff --git a/PKHeX.WinForms/MainWindow/MainXK3.cs b/PKHeX.WinForms/MainWindow/MainXK3.cs index 3731171f7..56b2c093e 100644 --- a/PKHeX.WinForms/MainWindow/MainXK3.cs +++ b/PKHeX.WinForms/MainWindow/MainXK3.cs @@ -102,13 +102,11 @@ private void populateFieldsXK3() if (xk3.ShadowID > 0) { int puri = xk3.Purification; - if (puri > NUD_Purification.Maximum) - puri = 0; - else if (puri < NUD_Purification.Minimum) + if (puri < NUD_Purification.Minimum) puri = (int)NUD_Purification.Minimum; NUD_Purification.Value = puri; - CHK_Shadow.Checked = puri < 0; + CHK_Shadow.Checked = puri > 0; NUD_ShadowID.Value = Math.Max(xk3.ShadowID, 0); } From 9c003f1b511f6fc6ff31fc42e0267fe02b07b843 Mon Sep 17 00:00:00 2001 From: javierhimura Date: Sun, 2 Apr 2017 17:16:53 +0200 Subject: [PATCH 4/7] Fix for legallity of Pokemon Box encounters to be included with gen 3 games static tables for every games. Also split gbcartera game versions into gen 1 events, gen 2 events, stadium and stadium2, leaving GBCartEraOnly as a group of games. Added Stadium 2 gift pokemon --- PKHeX/Game/GameVersion.cs | 22 ++++++++++++++++++---- PKHeX/Legality/Checks.cs | 2 +- PKHeX/Legality/Core.cs | 2 +- PKHeX/Legality/Tables1.cs | 4 ++-- PKHeX/Legality/Tables2.cs | 5 ++++- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/PKHeX/Game/GameVersion.cs b/PKHeX/Game/GameVersion.cs index 70eb47000..237367e44 100644 --- a/PKHeX/Game/GameVersion.cs +++ b/PKHeX/Game/GameVersion.cs @@ -40,7 +40,11 @@ public enum GameVersion // Extra Game Groupings (Generation) Gen1, Gen2, Gen3, Gen4, Gen5, Gen6, Gen7, - GBCartEraOnly, // Stadium + GBCartEraOnly, + Stadium, + Stadium2, + EventsGen1, + EventsGen2, } public static class Extension @@ -54,24 +58,34 @@ public static bool Contains(this GameVersion g1, GameVersion g2) case GameVersion.RBY: return (g2 == GameVersion.RD || g2 == GameVersion.BU || g2 == GameVersion.YW || g2 == GameVersion.GN); case GameVersion.Gen1: - return ( GameVersion.RBY.Contains(g2) || g2 == GameVersion.GBCartEraOnly); + return ( GameVersion.RBY.Contains(g2) || g2 == GameVersion.Stadium|| g2 == GameVersion.EventsGen1); + case GameVersion.Stadium: + case GameVersion.EventsGen1: + return GameVersion.RBY.Contains(g2); case GameVersion.GS: return (g2 == GameVersion.GD || g2 == GameVersion.SV); case GameVersion.GSC: return (GameVersion.GS.Contains(g2) || g2 == GameVersion.C); case GameVersion.Gen2: - return (GameVersion.GSC.Contains(g2) || g2 == GameVersion.GBCartEraOnly); + return (GameVersion.GSC.Contains(g2) || g2 == GameVersion.Stadium2 || g2 == GameVersion.EventsGen2); + case GameVersion.Stadium2: + case GameVersion.EventsGen2: + return GameVersion.GSC.Contains(g2); + case GameVersion.GBCartEraOnly: + return g2 == GameVersion.Stadium || g2 == GameVersion.Stadium2 || g2 == GameVersion.EventsGen1 || g2 == GameVersion.EventsGen2; case GameVersion.RS: return (g2 == GameVersion.R || g2 == GameVersion.S); case GameVersion.FRLG: return (g2 == GameVersion.FR || g2 == GameVersion.LG); case GameVersion.CXD: return (g2 == GameVersion.COLO || g2 == GameVersion.XD); + case GameVersion.RSBOX: return (GameVersion.RS.Contains(g2) || g2 == GameVersion.E || GameVersion.FRLG.Contains(g2)); case GameVersion.Gen3: return (GameVersion.RS.Contains(g2) || g2 == GameVersion.E || GameVersion.FRLG.Contains(g2) || GameVersion.CXD.Contains(g2) || g2 == GameVersion.RSBOX); case GameVersion.DP: return (g2 == GameVersion.D || g2 == GameVersion.P); case GameVersion.HGSS: return (g2 == GameVersion.HG || g2 == GameVersion.SS); + case GameVersion.BATREV: return (GameVersion.DP.Contains(g2) || g2 == GameVersion.Pt || GameVersion.HGSS.Contains(g2)); case GameVersion.Gen4: - return (GameVersion.DP.Contains(g2) || g2 == GameVersion.Pt || GameVersion.HGSS.Contains(g2)); + return (GameVersion.DP.Contains(g2) || g2 == GameVersion.Pt || GameVersion.HGSS.Contains(g2) || g2 == GameVersion.BATREV); case GameVersion.BW: return (g2 == GameVersion.B || g2 == GameVersion.W); case GameVersion.B2W2: return (g2 == GameVersion.B2 || g2 == GameVersion.W2); diff --git a/PKHeX/Legality/Checks.cs b/PKHeX/Legality/Checks.cs index b45b4a910..78a11fc26 100644 --- a/PKHeX/Legality/Checks.cs +++ b/PKHeX/Legality/Checks.cs @@ -939,7 +939,7 @@ private CheckResult verifyVCEncounter(int baseSpecies) return new CheckResult(Severity.Invalid, V80, CheckIdentifier.Encounter); var s = EncounterMatch as EncounterStatic; - if (s != null && s.Version == GameVersion.GBCartEraOnly) + if (s != null && GameVersion.GBCartEraOnly.Contains(s.Version)) { bool exceptions = false; exceptions |= baseSpecies == 151 && pkm.TID == 22796; diff --git a/PKHeX/Legality/Core.cs b/PKHeX/Legality/Core.cs index 5ff60b880..8ae34c19b 100644 --- a/PKHeX/Legality/Core.cs +++ b/PKHeX/Legality/Core.cs @@ -942,7 +942,7 @@ internal static EncounterStatic getValidStaticEncounter(PKM pkm, GameVersion gam // if (e.Gift && pkm.Ball != 4) // PokéBall // continue; - if (!AllowGBCartEra && e.Version == GameVersion.GBCartEraOnly) + if (!AllowGBCartEra && GameVersion.GBCartEraOnly.Contains(e.Version)) continue; // disallow gb cart era encounters (as they aren't obtainable by Main/VC series) return e; diff --git a/PKHeX/Legality/Tables1.cs b/PKHeX/Legality/Tables1.cs index f7e3a9a09..7e8168194 100644 --- a/PKHeX/Legality/Tables1.cs +++ b/PKHeX/Legality/Tables1.cs @@ -102,8 +102,8 @@ public static partial class Legal // new EncounterStatic { Species = 004, Level = 10, Version = GameVersion.YW }, // Charmander (Route 24) // new EncounterStatic { Species = 007, Level = 10, Version = GameVersion.YW }, // Squirtle (Vermillion City) - new EncounterStatic { Species = 054, Level = 15, Moves = new [] { 133, 10 }, Version = GameVersion.GBCartEraOnly }, // Stadium Psyduck (Amnesia) - new EncounterStatic { Species = 151, Level = 5, IVs = new [] {15,15,15,15,15,15}, Version = GameVersion.GBCartEraOnly }, // Event Mew + new EncounterStatic { Species = 054, Level = 15, Moves = new [] { 133, 10 }, Version = GameVersion.Stadium }, // Stadium Psyduck (Amnesia) + new EncounterStatic { Species = 151, Level = 5, IVs = new [] {15,15,15,15,15,15}, Version = GameVersion.EventsGen1 }, // Event Mew }; internal static readonly EncounterTrade[] TradeGift_RBY = { diff --git a/PKHeX/Legality/Tables2.cs b/PKHeX/Legality/Tables2.cs index 85d12259f..eec2abf5a 100644 --- a/PKHeX/Legality/Tables2.cs +++ b/PKHeX/Legality/Tables2.cs @@ -73,6 +73,9 @@ public static partial class Legal new EncounterStatic { Species = 100, Level = 23, Location = 036, Version = GameVersion.GSC }, // Voltorb @ Rocket Hideout (Mahogany Town) new EncounterStatic { Species = 101, Level = 23, Location = 036, Version = GameVersion.GSC }, // Electrode @ Rocket Hideout (Mahogany Town) new EncounterStatic { Species = 143, Level = 50, Location = 061, Version = GameVersion.GSC }, // Snorlax @ Vermillion City + + new EncounterStatic { Species = 083, Level = 05, Moves = new [] { 226, 14, 97, 37 }, Version = GameVersion.Stadium2 }, // Stadium 2 Baton Pass Farfetch'd + new EncounterStatic { Species = 207, Level = 05, Moves = new [] { 89, 68, 17 }, Version = GameVersion.Stadium2 }, // Stadium 2 Earthquake Gligar }; internal static readonly EncounterStatic[] Encounter_GS_Exclusive = @@ -102,7 +105,7 @@ public static partial class Legal new EncounterStatic { Species = 249, Level = 60, Location = 031, Version = GameVersion.C }, // Lugia @ Whirl Islands new EncounterStatic { Species = 250, Level = 60, Location = 023, Version = GameVersion.C }, // Ho-Oh @ Tin Tower - new EncounterStatic { Species = 251, Level = 30, Location = 014, Version = GameVersion.GBCartEraOnly }, // Celebi @ Ilex Forest + new EncounterStatic { Species = 251, Level = 30, Location = 014, Version = GameVersion.EventsGen2 }, // Celebi @ Ilex Forest }; internal static readonly EncounterStatic[] Encounter_GS = Encounter_GSC_Common.Concat(Encounter_GS_Exclusive).ToArray(); From f22dc2cf9fc1bbaf41c0c731f51e7bcfe4beeb24 Mon Sep 17 00:00:00 2001 From: javierhimura Date: Sun, 2 Apr 2017 17:18:44 +0200 Subject: [PATCH 5/7] Update readme file with memory card extension --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 567fbe768..9aca796bd 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Pokémon core series save editor, programmed in [C#](https://en.wikipedia.org/wi Supports the following files: * Save files ("main", \*.sav, \*.dsv, \*.dat, \*.gci) +* GC Memory Card files (.raw) * Individual Pokémon entity files (.pk\*) * Mystery Gift files (.pgt, .pcd, .pgf, .wc\*) including conversion to .pk\* * Importing teams from Decrypted 3DS Battle Videos From ab67bf0d6043466fd241f939c054b5ace0e35a02 Mon Sep 17 00:00:00 2001 From: javierhimura Date: Sun, 2 Apr 2017 17:36:50 +0200 Subject: [PATCH 6/7] Split memory card files extension for bin and raw files. Raw is the extension used by Dolphin and Bin is used by Wii Devolution, both have the exact internal format and size --- PKHeX.WinForms/MainWindow/Main.cs | 1 - PKHeX/Saves/SAV3Colosseum.cs | 2 +- PKHeX/Saves/SAV3RSBox.cs | 2 +- PKHeX/Saves/SAV3XD.cs | 2 +- README.md | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/PKHeX.WinForms/MainWindow/Main.cs b/PKHeX.WinForms/MainWindow/Main.cs index 2d03f5da6..0cda3a625 100644 --- a/PKHeX.WinForms/MainWindow/Main.cs +++ b/PKHeX.WinForms/MainWindow/Main.cs @@ -918,7 +918,6 @@ private bool openXOR(byte[] input, string path) } private GameVersion SelectMemoryCardSaveGame(SAV3GCMemoryCard MC) { - //SaveGameCount if (MC.SaveGameCount == 1) return MC.SelectedGameVersion; diff --git a/PKHeX/Saves/SAV3Colosseum.cs b/PKHeX/Saves/SAV3Colosseum.cs index 714b954c8..bdfd68c7f 100644 --- a/PKHeX/Saves/SAV3Colosseum.cs +++ b/PKHeX/Saves/SAV3Colosseum.cs @@ -12,7 +12,7 @@ public override string Filter get { if (IsMemoryCardSave) - return "Memory Card File|*.raw,*.bin|GameCube Save File|*.gci|All Files|*.*"; + return "Memory Card Raw File|*.raw|Memory Card Binary File|*.bin|GameCube Save File|*.gci|All Files|*.*"; return "GameCube Save File|*.gci|All Files|*.*"; } } diff --git a/PKHeX/Saves/SAV3RSBox.cs b/PKHeX/Saves/SAV3RSBox.cs index 01ec388a2..ca7160bdf 100644 --- a/PKHeX/Saves/SAV3RSBox.cs +++ b/PKHeX/Saves/SAV3RSBox.cs @@ -11,7 +11,7 @@ public override string Filter get { if (IsMemoryCardSave) - return "Memory Card File|*.raw,*.bin|GameCube Save File|*.gci|All Files|*.*"; + return "Memory Card Raw File|*.raw|Memory Card Binary File|*.bin|GameCube Save File|*.gci|All Files|*.*"; return "GameCube Save File|*.gci|All Files|*.*"; } } diff --git a/PKHeX/Saves/SAV3XD.cs b/PKHeX/Saves/SAV3XD.cs index 0ad8df4da..9babc5386 100644 --- a/PKHeX/Saves/SAV3XD.cs +++ b/PKHeX/Saves/SAV3XD.cs @@ -11,7 +11,7 @@ public override string Filter get { if (IsMemoryCardSave) - return "Memory Card File|*.raw|GameCube Save File|*.gci|All Files|*.*"; + return "Memory Card Raw File|*.raw|Memory Card Binary File|*.bin|GameCube Save File|*.gci|All Files|*.*"; return "GameCube Save File|*.gci|All Files|*.*"; } } diff --git a/README.md b/README.md index 9aca796bd..3b0b99e35 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Pokémon core series save editor, programmed in [C#](https://en.wikipedia.org/wi Supports the following files: * Save files ("main", \*.sav, \*.dsv, \*.dat, \*.gci) -* GC Memory Card files (.raw) +* GameCube Memory Card files (.raw, .bin) * Individual Pokémon entity files (.pk\*) * Mystery Gift files (.pgt, .pcd, .pgf, .wc\*) including conversion to .pk\* * Importing teams from Decrypted 3DS Battle Videos From b95bfdaba00bef217f098a66ea5f3992760fe765 Mon Sep 17 00:00:00 2001 From: javierhimura Date: Sun, 2 Apr 2017 18:47:18 +0200 Subject: [PATCH 7/7] =?UTF-8?q?Specify=20in=20the=20readme=20that=20only?= =?UTF-8?q?=20MC=20containing=20Pok=C3=A9mon=20savefiles=20are=20valid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3b0b99e35..9ae88046c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Pokémon core series save editor, programmed in [C#](https://en.wikipedia.org/wi Supports the following files: * Save files ("main", \*.sav, \*.dsv, \*.dat, \*.gci) -* GameCube Memory Card files (.raw, .bin) +* GameCube Memory Card files (.raw, .bin) containing GC Pokémon savegames. * Individual Pokémon entity files (.pk\*) * Mystery Gift files (.pgt, .pcd, .pgf, .wc\*) including conversion to .pk\* * Importing teams from Decrypted 3DS Battle Videos