diff --git a/PKHeX/MainWindow/Main.cs b/PKHeX/MainWindow/Main.cs index 77ff7abb2..c657feb02 100644 --- a/PKHeX/MainWindow/Main.cs +++ b/PKHeX/MainWindow/Main.cs @@ -601,14 +601,24 @@ private void openFile(byte[] input, string path, string ext) { MysteryGift tg; PKM temp; string c; byte[] footer = new byte[0]; - #region DeSmuME .dsv detect - if (input.Length > SaveUtil.SIZE_G4RAW) + byte[] header = new byte[0]; + #region Header/Footer detect + if (input.Length > SaveUtil.SIZE_G4RAW) // DeSmuME { bool dsv = SaveUtil.FOOTER_DSV.SequenceEqual(input.Skip(input.Length - SaveUtil.FOOTER_DSV.Length)); if (dsv) { footer = input.Skip(SaveUtil.SIZE_G4RAW).ToArray(); - input = input.Take(SaveUtil.SIZE_G4RAW).ToArray(); + input = input.Take(footer.Length).ToArray(); + } + } + if (input.Length == SaveUtil.SIZE_G3BOXGCI) + { + bool gci = SaveUtil.HEADER_GCI.SequenceEqual(input.Take(SaveUtil.HEADER_GCI.Length)); + if (gci) + { + header = input.Take(SaveUtil.SIZE_G3BOXGCI - SaveUtil.SIZE_G3BOX).ToArray(); + input = input.Skip(header.Length).ToArray(); } } #endregion @@ -640,8 +650,8 @@ private void openFile(byte[] input, string path, string ext) } #endregion #region SAV/PKM - else if (SaveUtil.getSAVGeneration(input) > -1) // Supports Gen4/5/6 - { openSAV(input, path); SAV.Footer = footer; } + else if (SaveUtil.getSAVGeneration(input) != -1) + { openSAV(input, path); SAV.Footer = footer; SAV.Header = header; } else if ((temp = PKMConverter.getPKMfromBytes(input)) != null) { PKM pk = PKMConverter.convertToFormat(temp, SAV.Generation, out c); @@ -864,11 +874,30 @@ private void loadSAV(SaveFile sav, string path) } Menu_LoadBoxes.Enabled = Menu_DumpBoxes.Enabled = Menu_Report.Enabled = Menu_Modify.Enabled = B_SaveBoxBin.Enabled = SAV.HasBox; + int BoxTab = tabBoxMulti.TabPages.IndexOf(Tab_Box); + int PartyTab = tabBoxMulti.TabPages.IndexOf(Tab_PartyBattle); + + if (!SAV.HasParty && tabBoxMulti.TabPages.Contains(Tab_PartyBattle)) + tabBoxMulti.TabPages.Remove(Tab_PartyBattle); + else if (SAV.HasParty && !tabBoxMulti.TabPages.Contains(Tab_PartyBattle)) + { + int index = BoxTab; + if (index < 0) + index = -1; + tabBoxMulti.TabPages.Insert(index + 1, Tab_PartyBattle); + WindowTranslationRequired = true; + } + if (!SAV.HasDaycare && tabBoxMulti.TabPages.Contains(Tab_Other)) tabBoxMulti.TabPages.Remove(Tab_Other); else if (SAV.HasDaycare && !tabBoxMulti.TabPages.Contains(Tab_Other)) { - tabBoxMulti.TabPages.Insert(tabBoxMulti.TabPages.IndexOf(Tab_PartyBattle) + 1, Tab_Other); + int index = PartyTab; + if (index < 0) + index = BoxTab; + if (index < 0) + index = -1; + tabBoxMulti.TabPages.Insert(index + 1, Tab_Other); WindowTranslationRequired = true; } @@ -893,8 +922,12 @@ private void loadSAV(SaveFile sav, string path) B_OpenEventFlags.Visible = SAV.HasEvents; B_OpenLinkInfo.Visible = SAV.HasLink; B_CGearSkin.Visible = SAV.Generation == 5; + + B_OpenTrainerInfo.Visible = B_OpenItemPouch.Visible = SAV.HasParty; // Box RS } - + GB_SAVtools.Visible = FLP_SAVtools.Controls.Cast().Any(c => c.Visible); + + // Generational Interface byte[] extraBytes = new byte[1]; Tip1.RemoveAll(); Tip2.RemoveAll(); Tip3.RemoveAll(); // TSV/PSV diff --git a/PKHeX/PKHeX.csproj b/PKHeX/PKHeX.csproj index d22cf0809..d32512b52 100644 --- a/PKHeX/PKHeX.csproj +++ b/PKHeX/PKHeX.csproj @@ -164,6 +164,7 @@ + @@ -494,7 +495,7 @@ - + diff --git a/PKHeX/Saves/BlockInfo.cs b/PKHeX/Saves/BlockInfo.cs index 64edb09e8..ee83ef3ab 100644 --- a/PKHeX/Saves/BlockInfo.cs +++ b/PKHeX/Saves/BlockInfo.cs @@ -22,4 +22,60 @@ public BlockInfo(int offset, int length, int chkOffset, int chkMirror) ChecksumMirror = chkMirror; } } + + public class RSBOX_Block + { + private ushort CHK_0; + private ushort CHK_1; + + public readonly uint BlockNumber; + public readonly uint SaveCount; + public readonly byte[] Data; + + public readonly int Offset; + + public RSBOX_Block(byte[] data, int offset) + { + Data = (byte[])data.Clone(); + Offset = offset; + // Values stored in Big Endian format + CHK_0 = (ushort)((Data[0x0] << 8) | (Data[0x1] << 0)); + CHK_1 = (ushort)((Data[0x2] << 8) | (Data[0x3] << 0)); + BlockNumber = (uint)((Data[0x4] << 8) | (Data[0x5] << 8) | (Data[0x6] << 8) | (Data[0x7] << 0)); + SaveCount = (uint)((Data[0x8] << 8) | (Data[0x9] << 8) | (Data[0xA] << 8) | (Data[0xB] << 0)); + } + + public bool ChecksumsValid + { + get + { + ushort[] chks = getCHK(Data); + return chks[0] == CHK_0 && chks[1] == CHK_1; + } + } + public void SetChecksums() + { + ushort[] chks = getCHK(Data); + CHK_0 = chks[0]; + CHK_1 = chks[1]; + Data[0] = (byte)(CHK_0 >> 8); + Data[1] = (byte)(CHK_0 & 0xFF); + Data[2] = (byte)(CHK_1 >> 8); + Data[3] = (byte)(CHK_1 & 0xFF); + } + + private static ushort[] getCHK(byte[] data) + { + int chk = 0; // initial value + for (int j = 0x4; j < 0x1FFC; j += 2) + { + chk += data[j] << 8; + chk += data[j + 1]; + } + ushort chk0 = (ushort)chk; + ushort chk1 = (ushort)(0xF004 - chk0); + + return new[] { chk0, chk1 }; + } + } } diff --git a/PKHeX/Saves/SAV3RSBox.cs b/PKHeX/Saves/SAV3RSBox.cs new file mode 100644 index 000000000..ad13b658f --- /dev/null +++ b/PKHeX/Saves/SAV3RSBox.cs @@ -0,0 +1,186 @@ +using System; +using System.Linq; + +namespace PKHeX +{ + public sealed class SAV3RSBox : SaveFile + { + public override string BAKName => $"{FileName} [{Version} #{SaveCount.ToString("0000")}].bak"; + public override string Filter => "GameCube Save File|*.gci"; + public override string Extension => ".gci"; + + public SAV3RSBox(byte[] data = null) + { + Data = data == null ? new byte[SaveUtil.SIZE_G3BOX] : (byte[])data.Clone(); + BAK = (byte[])Data.Clone(); + Exportable = !Data.SequenceEqual(new byte[Data.Length]); + + if (SaveUtil.getIsG3BOXSAV(Data) != GameVersion.RSBOX) + return; + + Blocks = new RSBOX_Block[2*BLOCK_COUNT]; + for (int i = 0; i < Blocks.Length; i++) + { + int offset = BLOCK_SIZE + i* BLOCK_SIZE; + Blocks[i] = new RSBOX_Block(Data.Skip(offset).Take(BLOCK_SIZE).ToArray(), offset); + } + + // Detect active save + int[] SaveCounts = Blocks.Select(block => (int)block.SaveCount).ToArray(); + SaveCount = SaveCounts.Max(); + int ActiveSAV = Array.IndexOf(SaveCounts, SaveCount) / BLOCK_COUNT; + Blocks = Blocks.Skip(ActiveSAV*BLOCK_COUNT).Take(BLOCK_COUNT).OrderBy(b => b.BlockNumber).ToArray(); + + // Set up PC data buffer beyond end of save file. + Box = Data.Length; + Array.Resize(ref Data, Data.Length + SIZE_RESERVED); // More than enough empty space. + + // Copy block to the allocated location + foreach (RSBOX_Block b in Blocks) + Array.Copy(b.Data, 0xC, Data, Box + b.BlockNumber*(BLOCK_SIZE - 0x10), b.Data.Length - 0x10); + + Personal = PersonalTable.RS; + HeldItems = Legal.HeldItems_RS; + + if (!Exportable) + resetBoxes(); + } + + private readonly RSBOX_Block[] Blocks; + private readonly int SaveCount; + private const int BLOCK_COUNT = 23; + private const int BLOCK_SIZE = 0x2000; + private const int SIZE_RESERVED = BLOCK_COUNT * BLOCK_SIZE; // unpacked box data + public override byte[] Write(bool DSV) + { + // Copy Box data back to block + foreach (RSBOX_Block b in Blocks) + Array.Copy(Data, Box + b.BlockNumber * (BLOCK_SIZE - 0x10), b.Data, 0xC, b.Data.Length - 0x10); + + setChecksums(); + + // Set Data Back + foreach (RSBOX_Block b in Blocks) + b.Data.CopyTo(Data, b.Offset); + return Data.Take(Data.Length - SIZE_RESERVED).ToArray(); + } + + // Configuration + public override SaveFile Clone() { return new SAV3(Write(DSV: false), Version); } + + public override int SIZE_STORED => PKX.SIZE_3STORED + 4; + public override int SIZE_PARTY => PKX.SIZE_3PARTY; // unused + public override PKM BlankPKM => new PK3(); + protected override Type PKMType => typeof(PK3); + + public override int MaxMoveID => 354; + public override int MaxSpeciesID => 386; + public override int MaxAbilityID => 77; + public override int MaxItemID => 374; + public override int MaxBallID => 0xC; + public override int MaxGameID => 5; + + public override int MaxEV => 252; + public override int Generation => 3; + protected override int GiftCountMax => 1; + public override int OTLength => 8; + public override int NickLength => 10; + public override int MaxMoney => 999999; + + public override int BoxCount => 50; + public override bool HasParty => false; + + // Checksums + protected override void setChecksums() + { + foreach (RSBOX_Block b in Blocks) + b.SetChecksums(); + } + public override bool ChecksumsValid + { + get { return Blocks.All(t => t.ChecksumsValid); } + } + public override string ChecksumInfo + { + get + { + return string.Join(Environment.NewLine, + Blocks.Where(b => !b.ChecksumsValid).Select(b => $"Block {b.BlockNumber.ToString("00")} invalid")); + } + } + + // Trainer Info + public override GameVersion Version { get { return GameVersion.RSBOX; } protected set { } } + + // Storage + public override int getPartyOffset(int slot) + { + return -1; + } + public override int getBoxOffset(int box) + { + return Box + 8 + SIZE_STORED * box * 30; + } + public override int CurrentBox + { + get { return Data[Box + 4]*2; } + set { Data[Box + 4] = (byte)(value/2); } + } + public override int getBoxWallpaper(int box) + { + // Box Wallpaper is directly after the Box Names + int offset = Box + 0x1ED19 + box/2; + return Data[offset]; + } + public override string getBoxName(int box) + { + // Tweaked for the 1-30/31-60 box showing + string lo = (30*(box%2) + 1).ToString("00"); + string hi = (30*(box%2 + 1)).ToString("00"); + string boxName = $"[{lo}-{hi}] "; + box = box / 2; + + int offset = Box + 0x1EC38 + 9 * box; + if (Data[offset] == 0 || Data[offset] == 0xFF) + boxName += $"BOX {box + 1}"; + boxName += PKX.getG3Str(Data.Skip(offset).Take(9).ToArray(), Japanese); + + return boxName; + } + public override void setBoxName(int box, string value) + { + int offset = Box + 0x1EC38 + 9 * box; + if (value.Length > 8) + value = value.Substring(0, 8); // Hard cap + if (value == "BOX " + (box + 1)) + new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }.CopyTo(Data, offset); + } + public override PKM getPKM(byte[] data) + { + return new PK3(data.Take(PKX.SIZE_3STORED).ToArray()); + } + public override byte[] decryptPKM(byte[] data) + { + return PKX.decryptArray3(data.Take(PKX.SIZE_3STORED).ToArray()); + } + + protected override void setDex(PKM pkm) { } + + public override void setStoredSlot(PKM pkm, int offset, bool? trade = null, bool? dex = null) + { + if (pkm == null) return; + if (pkm.GetType() != PKMType) + throw new InvalidCastException($"PKM Format needs to be {PKMType} when setting to a Gen{Generation} Save File."); + if (trade ?? SetUpdatePKM) + setPKM(pkm); + if (dex ?? SetUpdateDex) + setDex(pkm); + byte[] data = pkm.EncryptedBoxData; + setData(data, offset); + + BitConverter.GetBytes((ushort)pkm.TID).CopyTo(Data, offset + data.Length + 0); + BitConverter.GetBytes((ushort)pkm.SID).CopyTo(Data, offset + data.Length + 2); + Edited = true; + } + } +} diff --git a/PKHeX/Saves/SaveFile.cs b/PKHeX/Saves/SaveFile.cs index 63305c23c..e45d190e8 100644 --- a/PKHeX/Saves/SaveFile.cs +++ b/PKHeX/Saves/SaveFile.cs @@ -19,6 +19,7 @@ public abstract class SaveFile public abstract SaveFile Clone(); public abstract string Filter { get; } public byte[] Footer { protected get; set; } = new byte[0]; // .dsv + public byte[] Header { protected get; set; } = new byte[0]; // .gci public bool Japanese { protected get; set; } public string PlayTimeString => $"{PlayedHours}ː{PlayedMinutes.ToString("00")}ː{PlayedSeconds.ToString("00")}"; // not : @@ -41,6 +42,8 @@ public virtual byte[] Write(bool DSV) setChecksums(); if (Footer.Length > 0 && DSV) return Data.Concat(Footer).ToArray(); + if (Header.Length > 0) + return Header.Concat(Data).ToArray(); return Data; } public virtual string MiscSaveChecks() { return ""; } @@ -259,7 +262,7 @@ public ushort[] EventConsts } // Inventory - public abstract InventoryPouch[] Inventory { get; set; } + public virtual InventoryPouch[] Inventory { get; set; } protected int OFS_PouchHeldItem { get; set; } = int.MinValue; protected int OFS_PouchKeyItem { get; set; } = int.MinValue; protected int OFS_PouchMedicine { get; set; } = int.MinValue; @@ -298,20 +301,20 @@ public virtual MysteryGiftAlbum GiftAlbum public virtual int SubRegion { get { return -1; } set { } } // Trainer Info - public abstract int Gender { get; set; } + public virtual int Gender { get; set; } public virtual int Language { get { return -1; } set { } } public virtual int Game { get { return -1; } set { } } - public abstract ushort TID { get; set; } - public abstract ushort SID { get; set; } - public abstract string OT { get; set; } - public abstract int PlayedHours { get; set; } - public abstract int PlayedMinutes { get; set; } - public abstract int PlayedSeconds { get; set; } + public virtual ushort TID { get; set; } + public virtual ushort SID { get; set; } + public virtual string OT { get; set; } + public virtual int PlayedHours { get; set; } + public virtual int PlayedMinutes { get; set; } + public virtual int PlayedSeconds { get; set; } public virtual int SecondsToStart { get; set; } public virtual int SecondsToFame { get; set; } - public abstract uint Money { get; set; } + public virtual uint Money { get; set; } public abstract int BoxCount { get; } - public abstract int PartyCount { get; protected set; } + public virtual int PartyCount { get; protected set; } public abstract int CurrentBox { get; set; } public abstract string Extension { get; } @@ -325,19 +328,20 @@ public virtual MysteryGiftAlbum GiftAlbum // Daycare public int DaycareIndex = 0; - public abstract int getDaycareSlotOffset(int loc, int slot); - public abstract uint? getDaycareEXP(int loc, int slot); + public virtual int getDaycareSlotOffset(int loc, int slot) { return -1; } + public virtual uint? getDaycareEXP(int loc, int slot) { return null; } public virtual ulong? getDaycareRNGSeed(int loc) { return null; } public virtual bool? getDaycareHasEgg(int loc) { return null; } - public abstract bool? getDaycareOccupied(int loc, int slot); - - public abstract void setDaycareEXP(int loc, int slot, uint EXP); + public virtual bool? getDaycareOccupied(int loc, int slot) { return null; } + + public virtual void setDaycareEXP(int loc, int slot, uint EXP) { } public virtual void setDaycareRNGSeed(int loc, ulong seed) { } public virtual void setDaycareHasEgg(int loc, bool hasEgg) { } - public abstract void setDaycareOccupied(int loc, int slot, bool occupied); + public virtual void setDaycareOccupied(int loc, int slot, bool occupied) { } // Storage public virtual int BoxSlotCount => 30; + public PKM getPartySlot(int offset) { return getPKM(decryptPKM(getData(offset, SIZE_PARTY))); @@ -457,12 +461,14 @@ public bool setPCBin(byte[] data) if (data.Length != getPCBin().Length) return false; + int len = BlankPKM.EncryptedBoxData.Length; + // split up data to individual pkm - byte[][] pkdata = new byte[data.Length/SIZE_STORED][]; - for (int i = 0; i < data.Length; i += SIZE_STORED) + byte[][] pkdata = new byte[data.Length/len][]; + for (int i = 0; i < data.Length; i += len) { - pkdata[i/SIZE_STORED] = new byte[SIZE_STORED]; - Array.Copy(data, i, pkdata[i/SIZE_STORED], 0, SIZE_STORED); + pkdata[i/len] = new byte[len]; + Array.Copy(data, i, pkdata[i/len], 0, len); } PKM[] pkms = BoxData; @@ -476,11 +482,14 @@ public bool setBoxBin(byte[] data, int box) if (data.Length != getBoxBin(box).Length) return false; - byte[][] pkdata = new byte[data.Length / SIZE_STORED][]; - for (int i = 0; i < data.Length; i += SIZE_STORED) + int len = BlankPKM.EncryptedBoxData.Length; + + // split up data to individual pkm + byte[][] pkdata = new byte[data.Length/len][]; + for (int i = 0; i < data.Length; i += len) { - pkdata[i/SIZE_STORED] = new byte[SIZE_STORED]; - Array.Copy(data, i, pkdata[i/SIZE_STORED], 0, SIZE_STORED); + pkdata[i/len] = new byte[len]; + Array.Copy(data, i, pkdata[i/len], 0, len); } PKM[] pkms = BoxData; diff --git a/PKHeX/Saves/SaveUtil.cs b/PKHeX/Saves/SaveUtil.cs index 564ce0f83..b9106095d 100644 --- a/PKHeX/Saves/SaveUtil.cs +++ b/PKHeX/Saves/SaveUtil.cs @@ -9,6 +9,7 @@ namespace PKHeX public enum GameVersion { /* I don't want to assign Gen I/II... */ + RSBOX = -5, GS = -4, C = -3, Invalid = -2, @@ -46,6 +47,8 @@ public static class SaveUtil internal const int SIZE_G5BW = 0x24000; internal const int SIZE_G5B2W2 = 0x26000; internal const int SIZE_G4RAW = 0x80000; + internal const int SIZE_G3BOX = 0x76000; + internal const int SIZE_G3BOXGCI = 0x76040; // +64 if has GCI data internal const int SIZE_G3RAW = 0x20000; internal const int SIZE_G3RAWHALF = 0x10000; internal const int SIZE_G2RAW_U = 0x8000; @@ -57,6 +60,7 @@ public static class SaveUtil internal const int SIZE_G1BAT = 0x802C; internal static readonly byte[] FOOTER_DSV = Encoding.ASCII.GetBytes("|-DESMUME SAVE-|"); + internal static readonly byte[] HEADER_GCI = {0x47, 0x50, 0x58}; // GPX* /// Determines the generation of the given save data. /// Save data of which to determine the generation @@ -69,6 +73,8 @@ public static int getSAVGeneration(byte[] data) return 2; if (getIsG3SAV(data) != GameVersion.Invalid) return 3; + if (getIsG3BOXSAV(data) != GameVersion.Invalid) + return (int)GameVersion.RSBOX; if (getIsG4SAV(data) != GameVersion.Invalid) return 4; if (getIsG5SAV(data) != GameVersion.Invalid) @@ -183,7 +189,7 @@ public static GameVersion getIsG2SAVJ(byte[] data) return GameVersion.C; return GameVersion.Invalid; } - /// Determines the type of 3th gen save + /// Determines the type of 3rd gen save /// Save data of which to determine the type /// Version Identifier or Invalid if type cannot be determined. public static GameVersion getIsG3SAV(byte[] data) @@ -213,6 +219,31 @@ public static GameVersion getIsG3SAV(byte[] data) default: return GameVersion.E; } } + /// Determines the type of 3rd gen Box RS + /// Save data of which to determine the type + /// Version Identifier or Invalid if type cannot be determined. + public static GameVersion getIsG3BOXSAV(byte[] data) + { + if (!new[] { SIZE_G3BOX, SIZE_G3BOXGCI }.Contains(data.Length)) + return GameVersion.Invalid; + + byte[] sav = data.Skip(data.Length - SIZE_G3BOX).Take(SIZE_G3BOX).ToArray(); + + // Verify first checksum + uint chk = 0; // initial value + for (int j = 0x4; j < 0x1FFC; j += 2) + { + chk += (ushort)(sav[0x2000 + j] << 8); + chk += sav[0x2000 + j + 1]; + } + ushort chkA = (ushort)chk; + ushort chkB = (ushort)(0xF004 - chkA); + + ushort CHK_A = (ushort)((sav[0x2000] << 8) | sav[0x2001]); + ushort CHK_B = (ushort)((sav[0x2002] << 8) | sav[0x2003]); + + return CHK_A == chkA && CHK_B == chkB ? GameVersion.RSBOX : GameVersion.Invalid; + } /// Determines the type of 4th gen save /// Save data of which to determine the type /// Version Identifier or Invalid if type cannot be determined. @@ -361,6 +392,8 @@ public static SaveFile getVariantSAV(byte[] data) return new SAV2(data); case 3: return new SAV3(data); + case (int)GameVersion.RSBOX: + return new SAV3RSBox(data); case 4: return new SAV4(data); case 5: