using System; using System.Linq; namespace PKHeX.Core { // Base Class for Save Files public abstract class SaveFile { public static bool SetUpdateDex = true; public static bool SetUpdatePKM = true; // General Object Properties public byte[] Data; public bool Edited; public string FileName, FilePath; public abstract string BAKName { get; } public byte[] BAK { get; protected set; } public bool Exportable { get; protected set; } 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 { get; set; } public string PlayTimeString => $"{PlayedHours}ː{PlayedMinutes:00}ː{PlayedSeconds:00}"; // not : public virtual bool IndeterminateGame => false; public virtual bool IndeterminateSubVersion => false; public abstract string Extension { get; } public virtual string[] PKMExtensions => PKM.Extensions.Where(f => { 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); public abstract PKM BlankPKM { get; } public abstract byte[] decryptPKM(byte[] data); public abstract int SIZE_STORED { get; } public abstract int SIZE_PARTY { get; } public abstract int MaxEV { get; } public virtual int MaxIV => 31; public ushort[] HeldItems { get; protected set; } // General SAV Properties public virtual byte[] Write(bool DSV, bool GCI) { return Write(DSV); } 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 ""; } public virtual string MiscSaveInfo() { return ""; } public virtual GameVersion Version { get; protected set; } public abstract bool ChecksumsValid { get; } public abstract string ChecksumInfo { get; } public abstract int Generation { get; } public PersonalTable Personal { get; set; } public bool SM => Data.Length == SaveUtil.SIZE_G7SM; public bool ORASDEMO => Data.Length == SaveUtil.SIZE_G6ORASDEMO; public bool ORAS => Data.Length == SaveUtil.SIZE_G6ORAS; public bool XY => Data.Length == SaveUtil.SIZE_G6XY; public bool B2W2 => Version == GameVersion.B2W2; public bool BW => Version == GameVersion.BW; public bool HGSS => Version == GameVersion.HGSS; public bool Pt => Version == GameVersion.Pt; public bool DP => Version == GameVersion.DP; public bool E => Version == GameVersion.E; public bool FRLG => Version == GameVersion.FRLG; public bool RS => Version == GameVersion.RS; public bool GSC => Version == GameVersion.GS || Version == GameVersion.C; public bool RBY => Version == GameVersion.RBY; public bool GameCube => new[] { GameVersion.COLO, GameVersion.XD, GameVersion.RSBOX }.Contains(Version); public virtual int MaxMoveID => int.MaxValue; public virtual int MaxSpeciesID => int.MaxValue; public virtual int MaxAbilityID => int.MaxValue; public virtual int MaxItemID => int.MaxValue; public virtual int MaxBallID => int.MaxValue; public virtual int MaxGameID => int.MaxValue; // Flags public bool HasWondercards => WondercardData > -1; public bool HasSuperTrain => SuperTrain > -1; public bool HasBerryField => BerryField > -1; public bool HasHoF => HoF > -1; public bool HasSecretBase => SecretBase > -1; public bool HasPuff => Puff > -1; public bool HasPSS => PSS > -1; public bool HasOPower => OPower > -1; public bool HasJPEG => JPEGData != null; public bool HasBox => Box > -1; public virtual bool HasParty => Party > -1; public bool HasBattleBox => BattleBox > -1; public bool HasFused => Fused > -1; public bool HasGTS => GTS > -1; public bool HasDaycare => Daycare > -1; public virtual bool HasPokeDex => PokeDex > -1; public virtual bool HasBoxWallpapers => getBoxWallpaperOffset(0) > -1; public virtual bool HasSUBE => SUBE > -1 && !ORAS; public virtual bool HasGeolocation => false; public bool HasPokeBlock => ORAS && !ORASDEMO; public bool HasEvents => EventFlags != null; public bool HasLink => ORAS && !ORASDEMO || XY; // Counts protected virtual int GiftCountMax { get; } = int.MinValue; protected virtual int GiftFlagMax { get; } = 0x800; protected virtual int EventFlagMax { get; } = int.MinValue; protected virtual int EventConstMax { get; } = int.MinValue; public virtual int DaycareSeedSize { get; } = 0; public abstract int OTLength { get; } public abstract int NickLength { get; } public virtual int MaxMoney => 9999999; public virtual int MaxCoins => 9999; public virtual int MaxShadowID => 0; // Offsets protected virtual int Box { get; set; } = int.MinValue; protected int Party { get; set; } = int.MinValue; protected int Trainer1 { get; set; } = int.MinValue; protected int Daycare { get; set; } = int.MinValue; protected int WondercardData { get; set; } = int.MinValue; protected int PCLayout { get; set; } = int.MinValue; protected int EventFlag { get; set; } = int.MinValue; protected int EventConst { get; set; } = int.MinValue; public int GTS { get; protected set; } = int.MinValue; public int BattleBox { get; protected set; } = int.MinValue; public int Fused { get; protected set; } = int.MinValue; public int SUBE { get; protected set; } = int.MinValue; public int PokeDex { get; protected set; } = int.MinValue; public int SuperTrain { get; protected set; } = int.MinValue; public int SecretBase { get; protected set; } = int.MinValue; public int Puff { get; protected set; } = int.MinValue; public int PSS { get; protected set; } = int.MinValue; public int BerryField { get; protected set; } = int.MinValue; public int OPower { get; protected set; } = int.MinValue; public int HoF { get; protected set; } = int.MinValue; // SAV Properties public PKM[] BoxData { get { PKM[] data = new PKM[BoxCount*BoxSlotCount]; for (int i = 0; i < data.Length; i++) { data[i] = getStoredSlot(getBoxOffset(i/BoxSlotCount) + SIZE_STORED*(i%BoxSlotCount)); data[i].Identifier = $"{getBoxName(i/BoxSlotCount)}:{i%BoxSlotCount + 1:00}"; data[i].Box = i/BoxSlotCount + 1; data[i].Slot = i%BoxSlotCount + 1; data[i].Locked = getIsSlotLocked(data[i].Box, data[i].Slot); } return data; } set { if (value == null) throw new ArgumentNullException(); if (value.Length != BoxCount*BoxSlotCount) throw new ArgumentException($"Expected {BoxCount*BoxSlotCount}, got {value.Length}"); if (value.Any(pk => PKMType != pk.GetType())) throw new ArgumentException($"Not {PKMType} array."); for (int i = 0; i < value.Length; i++) setStoredSlot(value[i], getBoxOffset(i/BoxSlotCount) + SIZE_STORED*(i%BoxSlotCount)); } } public PKM[] PartyData { get { PKM[] data = new PKM[PartyCount]; for (int i = 0; i < data.Length; i++) data[i] = getPartySlot(getPartyOffset(i)); return data; } set { if (value == null) throw new ArgumentNullException(); if (value.Length == 0 || value.Length > 6) throw new ArgumentException("Expected 1-6, got " + value.Length); if (value.Any(pk => PKMType != pk.GetType())) throw new ArgumentException($"Not {PKMType} array."); if (value[0].Species == 0) Console.WriteLine($"Empty first slot, received {value.Length}."); PKM[] newParty = value.Where(pk => pk.Species != 0).ToArray(); PartyCount = newParty.Length; Array.Resize(ref newParty, 6); for (int i = PartyCount; i < newParty.Length; i++) newParty[i] = BlankPKM; for (int i = 0; i < newParty.Length; i++) setPartySlot(newParty[i], getPartyOffset(i)); } } public PKM[] BattleBoxData { get { if (!HasBattleBox) return new PKM[0]; PKM[] data = new PKM[6]; for (int i = 0; i < data.Length; i++) { data[i] = getStoredSlot(BattleBox + SIZE_STORED * i); data[i].Locked = BattleBoxLocked; if (data[i].Species == 0) return data.Take(i).ToArray(); } return data; } } public bool[] EventFlags { get { if (EventFlagMax < 0) return null; bool[] Flags = new bool[EventFlagMax]; for (int i = 0; i < Flags.Length; i++) Flags[i] = (Data[EventFlag + (i >> 3)] >> (i & 7) & 0x1) == 1; return Flags; } set { if (EventFlagMax < 0) return; if (value.Length != EventFlagMax) return; byte[] data = new byte[value.Length>>3]; for (int i = 0; i < value.Length; i++) if (value[i]) data[i >> 3] |= (byte)(1 << (i & 7)); data.CopyTo(Data, EventFlag); } } public ushort[] EventConsts { get { if (EventConstMax < 0) return null; ushort[] Constants = new ushort[EventConstMax]; for (int i = 0; i < Constants.Length; i++) Constants[i] = BitConverter.ToUInt16(Data, EventConst + i * 2); return Constants; } set { if (EventConstMax < 0) return; if (value.Length != EventConstMax) return; for (int i = 0; i < value.Length; i++) BitConverter.GetBytes(value[i]).CopyTo(Data, EventConst + i * 2); } } // Inventory 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; protected int OFS_PouchTMHM { get; set; } = int.MinValue; protected int OFS_PouchBerry { get; set; } = int.MinValue; protected int OFS_PouchBalls { get; set; } = int.MinValue; protected int OFS_BattleItems { get; set; } = int.MinValue; protected int OFS_MailItems { get; set; } = int.MinValue; protected int OFS_PCItem { get; set; } = int.MinValue; protected int OFS_PouchZCrystals { get; set; } = int.MinValue; // Mystery Gift protected virtual bool[] MysteryGiftReceivedFlags { get => null; set { } } protected virtual MysteryGift[] MysteryGiftCards { get => null; set { } } public virtual MysteryGiftAlbum GiftAlbum { get => new MysteryGiftAlbum { Flags = MysteryGiftReceivedFlags, Gifts = MysteryGiftCards }; set { MysteryGiftReceivedFlags = value.Flags; MysteryGiftCards = value.Gifts; } } public virtual bool BattleBoxLocked { get => false; set { } } public virtual string JPEGTitle => null; public virtual byte[] JPEGData => null; public virtual int Country { get => -1; set { } } public virtual int ConsoleRegion { get => -1; set { } } public virtual int SubRegion { get => -1; set { } } // Trainer Info public virtual int Gender { get; set; } public virtual int Language { get => -1; set { } } public virtual int Game { get => -1; set { } } public virtual ushort TID { get; set; } public virtual ushort SID { get; set; } public int TrainerID7 => (int)((uint)(TID | (SID << 16)) % 1000000); public virtual string OT { get; set; } = "PKHeX"; 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 virtual uint Money { get; set; } public abstract int BoxCount { get; } public int SlotCount => BoxCount * BoxSlotCount; public virtual int PartyCount { get; protected set; } public virtual int MultiplayerSpriteID { get => 0; set { } } // Varied Methods protected abstract void setChecksums(); public abstract int getBoxOffset(int box); public abstract int getPartyOffset(int slot); public abstract string getBoxName(int box); public abstract void setBoxName(int box, string val); public virtual int GameSyncIDSize { get; } = 8; public virtual string GameSyncID { get => null; set { } } public virtual ulong? Secure1 { get => null; set { } } public virtual ulong? Secure2 { get => null; set { } } // Daycare public int DaycareIndex = 0; public virtual bool HasTwoDaycares => false; public virtual int getDaycareSlotOffset(int loc, int slot) { return -1; } public virtual uint? getDaycareEXP(int loc, int slot) { return null; } public virtual string getDaycareRNGSeed(int loc) { return null; } public virtual bool? getDaycareHasEgg(int loc) { return null; } 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, string seed) { } public virtual void setDaycareHasEgg(int loc, bool hasEgg) { } public virtual void setDaycareOccupied(int loc, int slot, bool occupied) { } // Storage public virtual int BoxSlotCount => 30; public virtual int BoxesUnlocked { get => -1; set { } } public virtual byte[] BoxFlags { get => null; set { } } public virtual int CurrentBox { get => 0; set { } } protected int[] LockedSlots = new int[0]; protected int[] TeamSlots = new int[0]; public bool MoveBox(int box, int insertBeforeBox) { if (box == insertBeforeBox) // no movement required return true; if (box >= BoxCount || insertBeforeBox >= BoxCount) // invalid box positions return false; int pos1 = BoxSlotCount*box; int pos2 = BoxSlotCount*insertBeforeBox; int min = Math.Min(pos1, pos2); int max = Math.Max(pos1, pos2); if (LockedSlots.Any(slot => min <= slot && slot < max)) // slots locked within operation range return false; int len = BoxSlotCount*SIZE_STORED; byte[] boxdata = getData(getBoxOffset(0), len*BoxCount); // get all boxes string[] boxNames = new int[BoxCount].Select((x, i) => getBoxName(i)).ToArray(); int[] boxWallpapers = new int[BoxCount].Select((x, i) => getBoxWallpaper(i)).ToArray(); min /= BoxSlotCount; max /= BoxSlotCount; // move all boxes within range to final spot for (int i = min, ctr = min; i < max; i++) { int b = insertBeforeBox; // if box is the moved box, move to insertion point, else move to unused box. if (i != box) { if (insertBeforeBox == ctr) ++ctr; b = ctr++; } Array.Copy(boxdata, len*i, Data, getBoxOffset(b), len); setBoxName(b, boxNames[i]); setBoxWallpaper(b, boxWallpapers[i]); } return true; } public bool SwapBox(int box1, int box2) { if (box1 == box2) // no movement required return true; if (box1 >= BoxCount || box2 >= BoxCount) // invalid box positions return false; int min = BoxSlotCount * box1; int max = BoxSlotCount * box1 + BoxSlotCount; if (LockedSlots.Any(slot => min <= slot && slot < max)) // locked slot within box return false; if (TeamSlots.Any(slot => min <= slot && slot < max)) // team slot within box return false; min = BoxSlotCount * box2; max = BoxSlotCount * box2 + BoxSlotCount; if (LockedSlots.Any(slot => min <= slot && slot < max)) // locked slot within box return false; if (TeamSlots.Any(slot => min <= slot && slot < max)) // team slot within box return false; // Data int b1o = getBoxOffset(box1); int b2o = getBoxOffset(box2); int len = BoxSlotCount*SIZE_STORED; byte[] b1 = new byte[len]; Array.Copy(Data, b1o, b1, 0, len); Array.Copy(Data, b2o, Data, b1o, len); Array.Copy(b1, 0, Data, b2o, len); // Name string b1n = getBoxName(box1); setBoxName(box1, getBoxName(box2)); setBoxName(box2, b1n); // Wallpaper int b1w = getBoxWallpaper(box1); setBoxWallpaper(box1, getBoxWallpaper(box2)); setBoxWallpaper(box2, b1w); return true; } protected virtual int getBoxWallpaperOffset(int box) { return -1; } public virtual int getBoxWallpaper(int box) { int offset = getBoxWallpaperOffset(box); if (offset < 0 || box > BoxCount) return box; return Data[offset]; } public virtual void setBoxWallpaper(int box, int val) { int offset = getBoxWallpaperOffset(box); if (offset < 0 || box > BoxCount) return; Data[offset] = (byte)val; } public virtual PKM getPartySlot(int offset) { return getPKM(decryptPKM(getData(offset, SIZE_PARTY))); } public virtual PKM getStoredSlot(int offset) { return getPKM(decryptPKM(getData(offset, SIZE_STORED))); } public void setPartySlot(PKM pkm, int offset, bool? trade = null, bool? dex = null) { if (pkm == null) return; if (pkm.GetType() != PKMType) throw new ArgumentException($"PKM Format needs to be {PKMType} when setting to this Save File."); if (trade ?? SetUpdatePKM) setPKM(pkm); if (dex ?? SetUpdateDex) setDex(pkm); setPartyValues(pkm, isParty: true); for (int i = 0; i < 6; i++) if (getPartyOffset(i) == offset) if (PartyCount <= i) PartyCount = i + 1; setData(pkm.EncryptedPartyData, offset); Edited = true; } public virtual void setStoredSlot(PKM pkm, int offset, bool? trade = null, bool? dex = null) { if (pkm == null) return; if (pkm.GetType() != PKMType) throw new ArgumentException($"PKM Format needs to be {PKMType} when setting to this Save File."); if (trade ?? SetUpdatePKM) setPKM(pkm); if (dex ?? SetUpdateDex) setDex(pkm); setPartyValues(pkm, isParty: false); setData(pkm.EncryptedBoxData, offset); Edited = true; } public void deletePartySlot(int slot) { if (PartyCount <= slot) // beyond party range (or empty data already present) return; // Move all party slots down one for (int i = slot + 1; i < 6; i++) // Slide slots down { int slotTo = getPartyOffset(i - 1); int slotFrom = getPartyOffset(i); setData(getData(slotFrom, SIZE_PARTY), slotTo); } setStoredSlot(BlankPKM, getPartyOffset(5), false, false); PartyCount -= 1; } public virtual bool getIsSlotLocked(int box, int slot) { return false; } public bool getBoxHasLockedSlot(int BoxStart, int BoxEnd) { return LockedSlots.Any(slot => BoxStart*BoxSlotCount <= slot && slot < (BoxEnd + 1)*BoxSlotCount); } public virtual bool getIsTeamSet(int box, int slot) { return false; } public void sortBoxes(int BoxStart = 0, int BoxEnd = -1) { PKM[] BD = BoxData; var Section = BD.Skip(BoxStart*BoxSlotCount); if (BoxEnd > BoxStart) Section = Section.Take(BoxSlotCount*(BoxEnd - BoxStart)); var Sorted = Section .OrderBy(p => p.Species == 0) // empty slots at end .ThenBy(p => p.IsEgg) // eggs to the end .ThenBy(p => p.Species) // species sorted .ThenBy(p => p.AltForm) // altforms sorted .ThenBy(p => p.Gender) // gender sorted .ThenBy(p => p.IsNicknamed).ToArray(); Array.Copy(Sorted, 0, BD, BoxStart*BoxSlotCount, Sorted.Length); BoxData = BD; } public void resetBoxes(int BoxStart = 0, int BoxEnd = -1) { if (BoxEnd < 0) BoxEnd = BoxCount; var blank = BlankPKM.EncryptedBoxData; if (this is SAV3RSBox) Array.Resize(ref blank, blank.Length + 4); // 00000 TID/SID at end for (int i = BoxStart; i < BoxEnd; i++) { int offset = getBoxOffset(i); for (int p = 0; p < BoxSlotCount; p++) setData(blank, offset + SIZE_STORED * p); } } public byte[] getPCBin() { return BoxData.SelectMany(pk => pk.EncryptedBoxData).ToArray(); } public byte[] getBoxBin(int box) { return BoxData.Skip(box*BoxSlotCount).Take(BoxSlotCount).SelectMany(pk => pk.EncryptedBoxData).ToArray(); } public bool setPCBin(byte[] data) { if (LockedSlots.Any()) return false; if (data.Length != getPCBin().Length) return false; 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/len] = new byte[len]; Array.Copy(data, i, pkdata[i/len], 0, len); } PKM[] pkms = BoxData; for (int i = 0; i < pkms.Length; i++) pkms[i] = getPKM(decryptPKM(pkdata[i])); BoxData = pkms; return true; } public bool setBoxBin(byte[] data, int box) { if (LockedSlots.Any(slot => box * BoxSlotCount <= slot && slot < (box + 1) * BoxSlotCount)) return false; if (data.Length != getBoxBin(box).Length) return false; 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/len] = new byte[len]; Array.Copy(data, i, pkdata[i/len], 0, len); } PKM[] pkms = BoxData; for (int i = 0; i < BoxSlotCount; i++) pkms[box*BoxSlotCount + i] = getPKM(decryptPKM(pkdata[i])); BoxData = pkms; return true; } protected virtual void setPartyValues(PKM pkm, bool isParty) { } protected virtual void setPKM(PKM pkm) { } protected virtual void setDex(PKM pkm) { } public virtual bool getSeen(int species) => false; public virtual void setSeen(int species, bool seen) { } public virtual bool getCaught(int species) => false; public virtual void setCaught(int species, bool caught) { } public int SeenCount => HasPokeDex ? new bool[MaxSpeciesID].Where((b, i) => getSeen(i+1)).Count() : 0; public int CaughtCount => HasPokeDex ? new bool[MaxSpeciesID].Where((b, i) => getCaught(i+1)).Count() : 0; public byte[] getData(int Offset, int Length) { if (Offset + Length > Data.Length) return null; byte[] data = new byte[Length]; Array.Copy(Data, Offset, data, 0, Length); return data; } public void setData(byte[] input, int Offset) { input.CopyTo(Data, Offset); Edited = true; } public abstract string getString(int Offset, int Length); public abstract byte[] setString(string value, int maxLength, int PadToSize = 0, ushort PadWith = 0); public virtual bool RequiresMemeCrypto => false; public virtual string eBerryName => string.Empty; public virtual bool eBerryIsEnigma => true; } }