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 1ed2e1593..0cda3a625 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,82 @@ private bool openXOR(byte[] input, string path) openSAV(s, s.FileName); return true; } + private GameVersion SelectMemoryCardSaveGame(SAV3GCMemoryCard MC) + { + 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) @@ -2884,7 +2974,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) @@ -3319,9 +3409,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.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); } 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 0fa428d93..fd374b2a4 100644 --- a/PKHeX/Legality/Checks.cs +++ b/PKHeX/Legality/Checks.cs @@ -947,7 +947,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(); 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..bdfd68c7f 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 Raw File|*.raw|Memory Card Binary File|*.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..ca7160bdf 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 Raw File|*.raw|Memory Card Binary File|*.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..9babc5386 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 Raw File|*.raw|Memory Card Binary File|*.bin|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..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 @@ -418,6 +422,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. diff --git a/README.md b/README.md index 567fbe768..9ae88046c 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) +* 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