using System; using System.Collections.Generic; using System.Linq; namespace PKHeX.Core; /// /// Base Class for Save Files /// public abstract class SaveFile : ITrainerInfo, IGameValueLimit, IBoxDetailWallpaper, IBoxDetailName, IGeneration, IVersion { // General Object Properties public byte[] Data; public SaveFileState State { get; } public SaveFileMetadata Metadata { get; private set; } protected SaveFile(byte[] data, bool exportable = true) { Data = data; State = new SaveFileState(exportable); Metadata = new SaveFileMetadata(this); } protected SaveFile(int size = 0) : this(size == 0 ? Array.Empty() : new byte[size], false) { } protected internal abstract string ShortSummary { get; } public abstract string Extension { get; } protected abstract SaveFile CloneInternal(); public SaveFile Clone() { var sav = CloneInternal(); sav.Metadata = Metadata with {SAV = sav}; return sav; } public virtual string PlayTimeString => $"{PlayedHours}ː{PlayedMinutes:00}ː{PlayedSeconds:00}"; // not : public virtual IReadOnlyList PKMExtensions => Array.FindAll(PKM.Extensions, f => { int gen = f[^1] - 0x30; return 3 <= gen && gen <= Generation; }); // General SAV Properties public byte[] Write(BinaryExportSetting setting = BinaryExportSetting.None) { byte[] data = GetFinalData(); return Metadata.Finalize(data, setting); } protected virtual byte[] GetFinalData() { SetChecksums(); return Data; } #region Metadata & Limits public virtual string MiscSaveInfo() => string.Empty; public virtual GameVersion Version { get; protected set; } public abstract bool ChecksumsValid { get; } public abstract string ChecksumInfo { get; } public abstract int Generation { get; } public abstract EntityContext Context { get; } #endregion #region Savedata Container Handling public void SetData(ReadOnlySpan input, int offset) => SetData(Data.AsSpan(offset), input); public void SetData(Span dest, ReadOnlySpan input) { input.CopyTo(dest); State.Edited = true; } public abstract string GetString(ReadOnlySpan data); public abstract int SetString(Span destBuffer, ReadOnlySpan value, int maxLength, StringConverterOption option); #endregion public virtual void CopyChangesFrom(SaveFile sav) => SetData(sav.Data, 0); // Offsets #region Stored PKM Limits public abstract IPersonalTable Personal { get; } public abstract int MaxStringLengthOT { get; } public abstract int MaxStringLengthNickname { get; } public abstract ushort MaxMoveID { get; } public abstract ushort MaxSpeciesID { get; } public abstract int MaxAbilityID { get; } public abstract int MaxItemID { get; } public abstract int MaxBallID { get; } public abstract int MaxGameID { get; } public virtual int MinGameID => 0; #endregion /// /// Gets the status of the Flag at the specified offset and index. /// /// Offset to read from /// Bit index to read /// Flag is Set (true) or not Set (false) public virtual bool GetFlag(int offset, int bitIndex) => FlagUtil.GetFlag(Data, offset, bitIndex); /// /// Sets the status of the Flag at the specified offset and index. /// /// Offset to read from /// Bit index to read /// Flag status to set /// Flag is Set (true) or not Set (false) public virtual void SetFlag(int offset, int bitIndex, bool value) => FlagUtil.SetFlag(Data, offset, bitIndex, value); public virtual IReadOnlyList Inventory { get => Array.Empty(); set { } } #region Mystery Gift protected virtual int GiftCountMax => int.MinValue; protected virtual int GiftFlagMax => 0x800; protected int WondercardData { get; set; } = int.MinValue; public bool HasWondercards => WondercardData > -1; protected virtual bool[] MysteryGiftReceivedFlags { get => Array.Empty(); set { } } protected virtual DataMysteryGift[] MysteryGiftCards { get => Array.Empty(); set { } } public virtual MysteryGiftAlbum GiftAlbum { get => new(MysteryGiftCards, MysteryGiftReceivedFlags); set { MysteryGiftReceivedFlags = value.Flags; MysteryGiftCards = value.Gifts; } } #endregion #region Player Info public virtual int Gender { get; set; } public virtual int Language { get => -1; set { } } public virtual int Game { get => (int)GameVersion.Any; set { } } public virtual uint ID32 { get; set; } public virtual ushort TID16 { get; set; } public virtual ushort SID16 { get; set; } 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 uint SecondsToStart { get; set; } public virtual uint SecondsToFame { get; set; } public virtual uint Money { get; set; } public abstract int BoxCount { get; } public virtual int SlotCount => BoxCount * BoxSlotCount; public virtual int MaxMoney => 9999999; public virtual int MaxCoins => 9999; public TrainerIDFormat TrainerIDDisplayFormat => this.GetTrainerIDFormat(); public uint TrainerTID7 { get => this.GetTrainerTID7(); set => this.SetTrainerTID7(value); } public uint TrainerSID7 { get => this.GetTrainerSID7(); set => this.SetTrainerSID7(value); } public uint DisplayTID { get => this.GetDisplayTID(); set => this.SetDisplayTID(value); } public uint DisplaySID { get => this.GetDisplaySID(); set => this.SetDisplaySID(value); } #endregion #region Party public virtual int PartyCount { get; protected set; } protected int Party { get; set; } = int.MinValue; public virtual bool HasParty => Party > -1; public abstract int GetPartyOffset(int slot); public bool IsPartyAllEggs(int except = -1) { if (!HasParty) return false; for (int i = 0; i < MaxPartyCount; i++) { if (i == except) continue; if (IsPartySlotNotEggOrEmpty(i)) return false; } return true; } private bool IsPartySlotNotEggOrEmpty(int index) { var slot = GetPartySlotAtIndex(index); return !slot.IsEgg && slot.Species != 0; } private const int MaxPartyCount = 6; public IList PartyData { get { var count = PartyCount; if ((uint)count > MaxPartyCount) count = MaxPartyCount; PKM[] data = new PKM[count]; for (int i = 0; i < data.Length; i++) data[i] = GetPartySlotAtIndex(i); return data; } set { if (value.Count is 0 or > MaxPartyCount) throw new ArgumentOutOfRangeException(nameof(value), $"Expected 1-6, got {value.Count}"); #if DEBUG if (value[0].Species == 0) System.Diagnostics.Debug.WriteLine($"Empty first slot, received {value.Count}."); #endif int ctr = 0; foreach (var exist in value.Where(pk => pk.Species != 0)) SetPartySlotAtIndex(exist, ctr++); PartyCount = ctr; for (int i = ctr; i < 6; i++) SetPartySlotAtIndex(BlankPKM, i); } } #endregion // Varied Methods protected abstract void SetChecksums(); #region Daycare public bool HasDaycare => DaycareOffset > -1; protected int DaycareOffset { get; set; } = int.MinValue; public virtual int DaycareSeedSize => 0; public int DaycareIndex; public virtual bool HasTwoDaycares => false; public virtual int GetDaycareSlotOffset(int loc, int slot) => -1; public virtual uint? GetDaycareEXP(int loc, int slot) => null; public virtual string GetDaycareRNGSeed(int loc) => string.Empty; public virtual bool? IsDaycareHasEgg(int loc) => null; public virtual bool? IsDaycareOccupied(int loc, int slot) => 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) { } #endregion private Span GetPartySpan(int index) => PartyBuffer[GetPartyOffset(index)..]; public PKM GetPartySlotAtIndex(int index) => GetPartySlot(GetPartySpan(index)); public void SetPartySlotAtIndex(PKM pk, int index, PKMImportSetting trade = PKMImportSetting.UseDefault, PKMImportSetting dex = PKMImportSetting.UseDefault) { // update party count if ((uint)index > 5) throw new ArgumentOutOfRangeException(nameof(index)); int currentCount = PartyCount; if (pk.Species != 0) { if (currentCount <= index) PartyCount = index + 1; } else if (currentCount > index) { PartyCount = index; } SetPartySlot(pk, GetPartySpan(index), trade, dex); } public void SetSlotFormatParty(PKM pk, Span data, PKMImportSetting trade = PKMImportSetting.UseDefault, PKMImportSetting dex = PKMImportSetting.UseDefault) { if (pk.GetType() != PKMType) throw new ArgumentException($"PKM Format needs to be {PKMType} when setting to this Save File."); UpdatePKM(pk, isParty: true, trade, dex); SetPartyValues(pk, isParty: true); WritePartySlot(pk, data); } public void SetSlotFormatStored(PKM pk, Span data, PKMImportSetting trade = PKMImportSetting.UseDefault, PKMImportSetting dex = PKMImportSetting.UseDefault) { if (pk.GetType() != PKMType) throw new ArgumentException($"PKM Format needs to be {PKMType} when setting to this Save File."); UpdatePKM(pk, isParty: false, trade, dex); SetPartyValues(pk, isParty: false); WriteSlotFormatStored(pk, data); } public void SetPartySlot(PKM pk, Span data, PKMImportSetting trade = PKMImportSetting.UseDefault, PKMImportSetting dex = PKMImportSetting.UseDefault) => SetSlotFormatParty(pk, data, trade, dex); public void SetBoxSlot(PKM pk, Span data, PKMImportSetting trade = PKMImportSetting.UseDefault, PKMImportSetting dex = PKMImportSetting.UseDefault) { if (pk.GetType() != PKMType) throw new ArgumentException($"PKM Format needs to be {PKMType} when setting to this Save File."); UpdatePKM(pk, isParty: false, trade, dex); SetPartyValues(pk, isParty: false); WriteBoxSlot(pk, data); } public void DeletePartySlot(int slot) { int newEmpty = PartyCount - 1; if ((uint)slot > newEmpty) // beyond party range (or empty data already present) return; // Move all party slots down one for (int i = slot + 1; i <= newEmpty; i++) // Slide slots down { var current = GetPartySlotAtIndex(i); SetPartySlotAtIndex(current, i - 1, PKMImportSetting.Skip, PKMImportSetting.Skip); } SetPartySlotAtIndex(BlankPKM, newEmpty, PKMImportSetting.Skip, PKMImportSetting.Skip); // PartyCount will automatically update via above call. Do not adjust. } #region Slot Storing public static PKMImportSetting SetUpdateDex { protected get; set; } = PKMImportSetting.Update; public static PKMImportSetting SetUpdatePKM { protected get; set; } = PKMImportSetting.Update; public abstract Type PKMType { get; } protected abstract PKM GetPKM(byte[] data); protected abstract byte[] DecryptPKM(byte[] data); public abstract PKM BlankPKM { get; } protected abstract int SIZE_STORED { get; } protected abstract int SIZE_PARTY { get; } public virtual int SIZE_BOXSLOT => SIZE_STORED; public abstract int MaxEV { get; } public virtual int MaxIV => 31; public abstract ReadOnlySpan HeldItems { get; } protected virtual Span BoxBuffer => Data; protected virtual Span PartyBuffer => Data; public virtual bool IsPKMPresent(ReadOnlySpan data) => EntityDetection.IsPresent(data); public virtual PKM GetDecryptedPKM(byte[] data) => GetPKM(DecryptPKM(data)); public virtual PKM GetPartySlot(ReadOnlySpan data) => GetDecryptedPKM(data[..SIZE_PARTY].ToArray()); public virtual PKM GetStoredSlot(ReadOnlySpan data) => GetDecryptedPKM(data[..SIZE_STORED].ToArray()); public virtual PKM GetBoxSlot(int offset) => GetStoredSlot(BoxBuffer[offset..]); public virtual byte[] GetDataForFormatStored(PKM pk) => pk.EncryptedBoxData; public virtual byte[] GetDataForFormatParty(PKM pk) => pk.EncryptedPartyData; public virtual byte[] GetDataForParty(PKM pk) => pk.EncryptedPartyData; public virtual byte[] GetDataForBox(PKM pk) => pk.EncryptedBoxData; public virtual void WriteSlotFormatStored(PKM pk, Span data) => SetData(data, GetDataForFormatStored(pk)); public virtual void WriteSlotFormatParty(PKM pk, Span data) => SetData(data, GetDataForFormatParty(pk)); public virtual void WritePartySlot(PKM pk, Span data) => SetData(data, GetDataForParty(pk)); public virtual void WriteBoxSlot(PKM pk, Span data) => SetData(data, GetDataForBox(pk)); protected virtual void SetPartyValues(PKM pk, bool isParty) { if (!isParty) return; if (pk.PartyStatsPresent) // Stats already present return; pk.ResetPartyStats(); } /// /// Conditions a for this save file as if it was traded to it. /// /// Entity to adapt /// Entity exists in party format /// Setting on whether or not to adapt public void AdaptPKM(PKM pk, bool party = true, PKMImportSetting trade = PKMImportSetting.UseDefault) { if (GetTradeUpdateSetting(trade)) SetPKM(pk, party); } protected void UpdatePKM(PKM pk, bool isParty, PKMImportSetting trade, PKMImportSetting dex) { AdaptPKM(pk, isParty, trade); if (GetDexUpdateSetting(dex)) SetDex(pk); } private static bool GetTradeUpdateSetting(PKMImportSetting trade = PKMImportSetting.UseDefault) { if (trade == PKMImportSetting.UseDefault) trade = SetUpdatePKM; return trade == PKMImportSetting.Update; } private static bool GetDexUpdateSetting(PKMImportSetting trade = PKMImportSetting.UseDefault) { if (trade == PKMImportSetting.UseDefault) trade = SetUpdateDex; return trade == PKMImportSetting.Update; } protected virtual void SetPKM(PKM pk, bool isParty = false) { } protected virtual void SetDex(PKM pk) { } #endregion #region Pokédex public int PokeDex { get; protected set; } = int.MinValue; public bool HasPokeDex => PokeDex > -1; public virtual bool GetSeen(ushort species) => false; public virtual void SetSeen(ushort species, bool seen) { } public virtual bool GetCaught(ushort species) => false; public virtual void SetCaught(ushort species, bool caught) { } public int SeenCount { get { int ctr = 0; for (ushort i = 1; i <= MaxSpeciesID; i++) { if (GetSeen(i)) ctr++; } return ctr; } } /// Count of unique Species Caught (Owned) public int CaughtCount { get { int ctr = 0; for (ushort i = 1; i <= MaxSpeciesID; i++) { if (GetCaught(i)) ctr++; } return ctr; } } public decimal PercentSeen => (decimal) SeenCount / MaxSpeciesID; public decimal PercentCaught => (decimal)CaughtCount / MaxSpeciesID; #endregion public bool HasBox => Box > -1; public virtual int BoxSlotCount => 30; public virtual int BoxesUnlocked { get => -1; set { } } public virtual byte[] BoxFlags { get => Array.Empty(); set { } } public virtual int CurrentBox { get; set; } #region BoxData protected int Box { get; set; } = int.MinValue; public IList BoxData { get { PKM[] data = new PKM[BoxCount * BoxSlotCount]; for (int box = 0; box < BoxCount; box++) AddBoxData(data, box, box * BoxSlotCount); return data; } set { if (value.Count != BoxCount * BoxSlotCount) throw new ArgumentException($"Expected {BoxCount * BoxSlotCount}, got {value.Count}"); for (int b = 0; b < BoxCount; b++) SetBoxData(value, b, b * BoxSlotCount); } } public int SetBoxData(IList value, int box, int index = 0) { int skipped = 0; for (int slot = 0; slot < BoxSlotCount; slot++) { var flags = GetSlotFlags(box, slot); if (!flags.IsOverwriteProtected()) SetBoxSlotAtIndex(value[index + slot], box, slot); else ++skipped; } return skipped; } public PKM[] GetBoxData(int box) { var data = new PKM[BoxSlotCount]; AddBoxData(data, box, 0); return data; } public void AddBoxData(IList data, int box, int index) { for (int slot = 0; slot < BoxSlotCount; slot++) { int i = slot + index; data[i] = GetBoxSlotAtIndex(box, slot); } } #endregion #region Storage Health & Metadata protected int[] TeamSlots = Array.Empty(); /// /// Slot indexes that are protected from overwriting. /// protected virtual IList[] SlotPointers => new[] { TeamSlots }; public virtual StorageSlotSource GetSlotFlags(int index) => StorageSlotSource.None; public StorageSlotSource GetSlotFlags(int box, int slot) => GetSlotFlags((box * BoxSlotCount) + slot); public bool IsSlotLocked(int box, int slot) => GetSlotFlags(box, slot).HasFlag(StorageSlotSource.Locked); public bool IsSlotLocked(int index) => GetSlotFlags(index).HasFlag(StorageSlotSource.Locked); public bool IsSlotOverwriteProtected(int box, int slot) => GetSlotFlags(box, slot).IsOverwriteProtected(); public bool IsSlotOverwriteProtected(int index) => GetSlotFlags(index).IsOverwriteProtected(); private const int StorageFullValue = -1; public bool IsStorageFull => NextOpenBoxSlot() == StorageFullValue; public int NextOpenBoxSlot(int lastKnownOccupied = -1) { var storage = BoxBuffer; int count = SlotCount; for (int i = lastKnownOccupied + 1; i < count; i++) { int offset = GetBoxSlotOffset(i); // overwrite protect is only true if there is already data in slot if (IsPKMPresent(storage[offset..])) continue; return i; } return StorageFullValue; } protected virtual bool IsSlotSwapProtected(int box, int slot) => false; private bool IsRegionOverwriteProtected(int min, int max) { foreach (var arrays in SlotPointers) { foreach (int slotIndex in arrays) { if (!GetSlotFlags(slotIndex).IsOverwriteProtected()) continue; if (ArrayUtil.WithinRange(slotIndex, min, max)) return true; } } return false; } public bool IsAnySlotLockedInBox(int BoxStart, int BoxEnd) { foreach (var arrays in SlotPointers) { foreach (int slotIndex in arrays) { if (!GetSlotFlags(slotIndex).HasFlag(StorageSlotSource.Locked)) continue; if (ArrayUtil.WithinRange(slotIndex, BoxStart * BoxSlotCount, (BoxEnd + 1) * BoxSlotCount)) return true; } } return false; } #endregion #region Storage Offsets and Indexing public abstract int GetBoxOffset(int box); public int GetBoxSlotOffset(int box, int slot) => GetBoxOffset(box) + (slot * SIZE_BOXSLOT); public PKM GetBoxSlotAtIndex(int box, int slot) => GetBoxSlot(GetBoxSlotOffset(box, slot)); public void GetBoxSlotFromIndex(int index, out int box, out int slot) { box = index / BoxSlotCount; if ((uint)box >= BoxCount) throw new ArgumentOutOfRangeException(nameof(index)); slot = index % BoxSlotCount; } public PKM GetBoxSlotAtIndex(int index) { GetBoxSlotFromIndex(index, out int box, out int slot); return GetBoxSlotAtIndex(box, slot); } public int GetBoxSlotOffset(int index) { GetBoxSlotFromIndex(index, out int box, out int slot); return GetBoxSlotOffset(box, slot); } public void SetBoxSlotAtIndex(PKM pk, int box, int slot, PKMImportSetting trade = PKMImportSetting.UseDefault, PKMImportSetting dex = PKMImportSetting.UseDefault) => SetBoxSlot(pk, BoxBuffer[GetBoxSlotOffset(box, slot)..], trade, dex); public void SetBoxSlotAtIndex(PKM pk, int index, PKMImportSetting trade = PKMImportSetting.UseDefault, PKMImportSetting dex = PKMImportSetting.UseDefault) => SetBoxSlot(pk, BoxBuffer[GetBoxSlotOffset(index)..], trade, dex); #endregion #region Storage Manipulations public bool MoveBox(int box, int insertBeforeBox) { if (box == insertBeforeBox) // no movement required return true; if ((uint)box >= BoxCount || (uint)insertBeforeBox >= BoxCount) // invalid box positions return false; MoveBox(box, insertBeforeBox, BoxBuffer); return true; } private void MoveBox(int box, int insertBeforeBox, Span storage) { int pos1 = BoxSlotCount * box; int pos2 = BoxSlotCount * insertBeforeBox; int min = Math.Min(pos1, pos2); int max = Math.Max(pos1, pos2); int len = BoxSlotCount * SIZE_BOXSLOT; byte[] boxdata = storage.Slice(GetBoxOffset(0), len * BoxCount).ToArray(); // get all boxes string[] boxNames = Get(GetBoxName, BoxCount); int[] boxWallpapers = Get(GetBoxWallpaper, BoxCount); static T[] Get(Func act, int count) { T[] result = new T[count]; for (int i = 0; i < result.Length; i++) result[i] = act(i); return result; } 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++; } boxdata.AsSpan(len * i, len).CopyTo(storage[GetBoxOffset(b)..]); SetBoxName(b, boxNames[i]); SetBoxWallpaper(b, boxWallpapers[i]); } SlotPointerUtil.UpdateMove(box, insertBeforeBox, BoxSlotCount, SlotPointers); } public bool SwapBox(int box1, int box2) { if (box1 == box2) // no movement required return true; if ((uint)box1 >= BoxCount || (uint)box2 >= BoxCount) // invalid box positions return false; if (!IsBoxAbleToMove(box1) || !IsBoxAbleToMove(box2)) return false; SwapBox(box1, box2, BoxBuffer); return true; } private void SwapBox(int box1, int box2, Span boxData) { int b1o = GetBoxOffset(box1); int b2o = GetBoxOffset(box2); int len = BoxSlotCount * SIZE_BOXSLOT; Span b1 = stackalloc byte[len]; boxData.Slice(b1o, len).CopyTo(b1); boxData.Slice(b2o, len).CopyTo(boxData[b1o..]); b1.CopyTo(boxData[b2o..]); // Name string b1n = GetBoxName(box1); SetBoxName(box1, GetBoxName(box2)); SetBoxName(box2, b1n); // Wallpaper int b1w = GetBoxWallpaper(box1); SetBoxWallpaper(box1, GetBoxWallpaper(box2)); SetBoxWallpaper(box2, b1w); // Pointers SlotPointerUtil.UpdateSwap(box1, box2, BoxSlotCount, SlotPointers); } private bool IsBoxAbleToMove(int box) { int min = BoxSlotCount * box; int max = min + BoxSlotCount; return !IsRegionOverwriteProtected(min, max); } /// /// Sorts all present within the range specified by and with the provied . /// /// Starting box; if not provided, will iterate from the first box. /// Ending box; if not provided, will iterate to the end. /// Sorting logic required to order a with respect to its peers; if not provided, will use a default sorting method. /// Reverse the sorting order /// Count of repositioned slots. public int SortBoxes(int BoxStart = 0, int BoxEnd = -1, Func, int, IEnumerable>? sortMethod = null, bool reverse = false) { var BD = BoxData; int start = BoxSlotCount * BoxStart; var Section = BD.Skip(start); if (BoxEnd >= BoxStart) Section = Section.Take(BoxSlotCount * (BoxEnd - BoxStart + 1)); Func skip = IsSlotOverwriteProtected; Section = Section.Where((_, i) => !skip(start + i)); var method = sortMethod ?? ((z, _) => z.OrderBySpecies()); var Sorted = method(Section, start); if (reverse) Sorted = Sorted.ReverseSort(); var result = Sorted.ToArray(); var boxclone = new PKM[BD.Count]; BD.CopyTo(boxclone, 0); int count = result.CopyTo(boxclone, skip, start); SlotPointerUtil.UpdateRepointFrom(boxclone, BD, 0, SlotPointers); for (int i = 0; i < boxclone.Length; i++) { var pk = boxclone[i]; SetBoxSlotAtIndex(pk, i, PKMImportSetting.Skip, PKMImportSetting.Skip); } return count; } /// /// Compresses the by pulling out the empty storage slots and putting them at the end, retaining all existing data. /// /// Count of actual stored. /// Important slot pointers that need to be re-pointed if a slot moves. /// True if was updated, false if no update done. public bool CompressStorage(out int storedCount, Span slotPointers) => this.CompressStorage(BoxBuffer, out storedCount, slotPointers); /// /// Removes all present within the range specified by and if the provided is satisfied. /// /// Starting box; if not provided, will iterate from the first box. /// Ending box; if not provided, will iterate to the end. /// Criteria required to be satisfied for a to be deleted; if not provided, will clear if possible. /// Count of deleted slots. public int ClearBoxes(int BoxStart = 0, int BoxEnd = -1, Func? deleteCriteria = null) { var storage = BoxBuffer; if ((uint)BoxEnd >= BoxCount) BoxEnd = BoxCount - 1; var blank = GetDataForBox(BlankPKM); int deleted = 0; for (int i = BoxStart; i <= BoxEnd; i++) { for (int p = 0; p < BoxSlotCount; p++) { if (IsSlotOverwriteProtected(i, p)) continue; var ofs = GetBoxSlotOffset(i, p); if (!IsPKMPresent(storage[ofs..])) continue; if (deleteCriteria != null) { var pk = GetBoxSlotAtIndex(i, p); if (!deleteCriteria(pk)) continue; } SetData(storage[ofs..], blank); ++deleted; } } return deleted; } /// /// Modifies all present within the range specified by and with the modification routine provided by . /// /// Modification to perform on a /// Starting box; if not provided, will iterate from the first box. /// Ending box (inclusive); if not provided, will iterate to the end. /// Count of modified slots. public int ModifyBoxes(Action action, int BoxStart = 0, int BoxEnd = -1) { if ((uint)BoxEnd >= BoxCount) BoxEnd = BoxCount - 1; var storage = BoxBuffer; int modified = 0; for (int b = BoxStart; b <= BoxEnd; b++) { for (int s = 0; s < BoxSlotCount; s++) { if (IsSlotOverwriteProtected(b, s)) continue; var ofs = GetBoxSlotOffset(b, s); var dest = storage[ofs..]; if (!IsPKMPresent(dest)) continue; var pk = GetBoxSlotAtIndex(b, s); action(pk); ++modified; SetBoxSlot(pk, dest, PKMImportSetting.Skip, PKMImportSetting.Skip); } } return modified; } #endregion #region Storage Name & Decoration public virtual bool HasBoxWallpapers => GetBoxWallpaperOffset(0) > -1; public virtual bool HasNamableBoxes => HasBoxWallpapers; public abstract string GetBoxName(int box); public abstract void SetBoxName(int box, ReadOnlySpan value); protected virtual int GetBoxWallpaperOffset(int box) => -1; public virtual int GetBoxWallpaper(int box) { int offset = GetBoxWallpaperOffset(box); if (offset < 0 || (uint)box > BoxCount) return box; return Data[offset]; } public virtual void SetBoxWallpaper(int box, int value) { int offset = GetBoxWallpaperOffset(box); if (offset < 0 || (uint)box > BoxCount) return; Data[offset] = (byte)value; } #endregion #region Box Binaries public byte[] GetPCBinary() => BoxData.SelectMany(GetDataForBox).ToArray(); public byte[] GetBoxBinary(int box) => GetBoxData(box).SelectMany(GetDataForBox).ToArray(); public bool SetPCBinary(ReadOnlySpan data) { if (IsRegionOverwriteProtected(0, SlotCount)) return false; int expectLength = SlotCount * SIZE_BOXSLOT; return SetConcatenatedBinary(data, expectLength); } public bool SetBoxBinary(ReadOnlySpan data, int box) { int start = box * BoxSlotCount; int end = start + BoxSlotCount; if (IsRegionOverwriteProtected(start, end)) return false; int expectLength = BoxSlotCount * SIZE_BOXSLOT; return SetConcatenatedBinary(data, expectLength, start); } private bool SetConcatenatedBinary(ReadOnlySpan data, int expectLength, int start = 0) { if (data.Length != expectLength) return false; var entryLength = SIZE_BOXSLOT; for (int i = 0, ctr = start; i < data.Length; i += entryLength) { if (IsSlotOverwriteProtected(ctr)) continue; var src = data.Slice(i, entryLength); var arr = src.ToArray(); var pk = GetPKM(arr); SetBoxSlotAtIndex(pk, ctr++); } return true; } #endregion } public static class StorageUtil { public static bool CompressStorage(this SaveFile sav, Span storage, out int storedCount, Span slotPointers) { // keep track of empty slots, and only write them at the end if slots were shifted (no need otherwise). var empty = new List(); bool shiftedSlots = false; ushort ctr = 0; int size = sav.SIZE_BOXSLOT; int count = sav.BoxSlotCount * sav.BoxCount; for (int i = 0; i < count; i++) { int offset = sav.GetBoxSlotOffset(i); if (sav.IsPKMPresent(storage[offset..])) { if (ctr != i) // copy required { shiftedSlots = true; // appending empty slots afterwards is now required since a rewrite was done int destOfs = sav.GetBoxSlotOffset(ctr); storage[offset..(offset + size)].CopyTo(storage[destOfs..(destOfs + size)]); SlotPointerUtil.UpdateRepointFrom(ctr, i, slotPointers); } ctr++; continue; } // pop out an empty slot; save all unused data & preserve order var data = storage.Slice(offset, size).ToArray(); empty.Add(data); } storedCount = ctr; if (!shiftedSlots) return false; for (int i = ctr; i < count; i++) { var data = empty[i - ctr]; int offset = sav.GetBoxSlotOffset(i); data.CopyTo(storage[offset..]); } return true; } public static int FindSlotIndex(this SaveFile sav, Func method, int maxCount) { for (int i = 0; i < maxCount; i++) { var pk = sav.GetBoxSlotAtIndex(i); if (method(pk)) return i; } return -1; } }