PKHeX/PKHeX.Core/Saves/SaveFile.cs
Kurt bc2549b24e
Minimize BAK file allocations (#3426)
Stop allocating an entire shadow copy of the save file whenever we create a new savefile object from file.

Prior commits added the clear SaveFileMetadata class to cleanly track the file path. Backups are copied from the original path.
2022-02-09 16:48:55 -08:00

1007 lines
41 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Linq;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core
{
/// <summary>
/// Base Class for Save Files
/// </summary>
public abstract class SaveFile : ITrainerInfo, IGameValueLimit, IBoxDetailWallpaper, IBoxDetailName, IGeneration
{
// 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)
{
Data = size == 0 ? Array.Empty<byte>() : new byte[size];
State = new SaveFileState(false);
Metadata = new SaveFileMetadata(this);
}
protected internal abstract string ShortSummary { get; }
public abstract string Extension { get; }
protected abstract SaveFile CloneInternal();
public SaveFile Clone()
{
var sav = CloneInternal();
sav.Metadata = Metadata;
return sav;
}
public virtual string PlayTimeString => $"{PlayedHours}ː{PlayedMinutes:00}ː{PlayedSeconds:00}"; // not :
public virtual IReadOnlyList<string> PKMExtensions => Array.FindAll(PKM.Extensions, f =>
{
int gen = f[^1] - 0x30;
return 3 <= gen && gen <= Generation;
});
// General SAV Properties
public byte[] Write(ExportFlags flags = ExportFlags.None)
{
byte[] data = GetFinalData();
return Metadata.Finalize(data, flags);
}
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; }
#endregion
#region Savedata Container Handling
public byte[] GetData(int offset, int length) => GetData(Data, offset, length);
protected static byte[] GetData(byte[] data, int offset, int length) => data.Slice(offset, length);
public void SetData(byte[] input, int offset) => SetData(Data, input, offset);
public void SetData(Span<byte> dest, Span<byte> input, int offset)
{
input.CopyTo(dest[offset..]);
State.Edited = true;
}
public abstract string GetString(ReadOnlySpan<byte> data);
public string GetString(int offset, int length) => GetString(Data.AsSpan(offset, length));
public abstract int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option);
#endregion
public virtual void CopyChangesFrom(SaveFile sav) => SetData(sav.Data, 0);
// Offsets
#region Stored PKM Limits
public abstract PersonalTable Personal { get; }
public abstract int OTLength { get; }
public abstract int NickLength { get; }
public abstract int MaxMoveID { get; }
public abstract int 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
#region Event Work
public virtual bool HasEvents => GetEventFlags().Length != 0;
protected virtual int EventFlagMax { get; } = int.MinValue;
protected virtual int EventConstMax { get; } = int.MinValue;
protected int EventFlag { get; set; } = int.MinValue;
protected int EventConst { get; set; } = int.MinValue;
/// <summary> All Event Flag values for the savegame </summary>
public bool[] GetEventFlags()
{
if (EventFlagMax < 0)
return Array.Empty<bool>();
bool[] result = new bool[EventFlagMax];
for (int i = 0; i < result.Length; i++)
result[i] = GetEventFlag(i);
return result;
}
/// <summary> All Event Flag values for the savegame </summary>
public void SetEventFlags(bool[] value)
{
if (EventFlagMax < 0)
return;
if (value.Length != EventFlagMax)
return;
for (int i = 0; i < value.Length; i++)
SetEventFlag(i, value[i]);
}
/// <summary> All Event Constant values for the savegame </summary>
public virtual ushort[] GetEventConsts()
{
if (EventConstMax <= 0 || Data.Length == 0)
return Array.Empty<ushort>();
ushort[] Constants = new ushort[EventConstMax];
for (int i = 0; i < Constants.Length; i++)
Constants[i] = ReadUInt16LittleEndian(Data.AsSpan(EventConst + (i * 2)));
return Constants;
}
/// <summary> All Event Constant values for the savegame </summary>
public virtual void SetEventConsts(ReadOnlySpan<ushort> value)
{
if (EventConstMax <= 0)
return;
if (value.Length != EventConstMax)
return;
var span = Data.AsSpan(EventConst);
for (int i = 0; i < value.Length; i++)
WriteUInt16LittleEndian(span[(i*2)..], value[i]);
}
/// <summary>
/// Gets the <see cref="bool"/> status of a desired Event Flag
/// </summary>
/// <param name="flagNumber">Event Flag to check</param>
/// <returns>Flag is Set (true) or not Set (false)</returns>
public virtual bool GetEventFlag(int flagNumber)
{
if ((uint)flagNumber >= EventFlagMax)
throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to get ({flagNumber}) is greater than max ({EventFlagMax}).");
return GetFlag(EventFlag + (flagNumber >> 3), flagNumber & 7);
}
/// <summary>
/// Sets the <see cref="bool"/> status of a desired Event Flag
/// </summary>
/// <param name="flagNumber">Event Flag to check</param>
/// <param name="value">Event Flag status to set</param>
/// <remarks>Flag is Set (true) or not Set (false)</remarks>
public virtual void SetEventFlag(int flagNumber, bool value)
{
if ((uint)flagNumber >= EventFlagMax)
throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to set ({flagNumber}) is greater than max ({EventFlagMax}).");
SetFlag(EventFlag + (flagNumber >> 3), flagNumber & 7, value);
}
/// <summary>
/// Gets the <see cref="bool"/> status of the Flag at the specified offset and index.
/// </summary>
/// <param name="offset">Offset to read from</param>
/// <param name="bitIndex">Bit index to read</param>
/// <returns>Flag is Set (true) or not Set (false)</returns>
public virtual bool GetFlag(int offset, int bitIndex) => FlagUtil.GetFlag(Data, offset, bitIndex);
/// <summary>
/// Sets the <see cref="bool"/> status of the Flag at the specified offset and index.
/// </summary>
/// <param name="offset">Offset to read from</param>
/// <param name="bitIndex">Bit index to read</param>
/// <param name="value">Flag status to set</param>
/// <remarks>Flag is Set (true) or not Set (false)</remarks>
public virtual void SetFlag(int offset, int bitIndex, bool value) => FlagUtil.SetFlag(Data, offset, bitIndex, value);
#endregion
public virtual IReadOnlyList<InventoryPouch> Inventory { get => Array.Empty<InventoryPouch>(); set { } }
#region Mystery Gift
protected virtual int GiftCountMax { get; } = int.MinValue;
protected virtual int GiftFlagMax { get; } = 0x800;
protected int WondercardData { get; set; } = int.MinValue;
public bool HasWondercards => WondercardData > -1;
protected virtual bool[] MysteryGiftReceivedFlags { get => Array.Empty<bool>(); set { } }
protected virtual DataMysteryGift[] MysteryGiftCards { get => Array.Empty<DataMysteryGift>(); 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 => -1; set { } }
public virtual int TID { get; set; }
public virtual int SID { 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 MultiplayerSpriteID { get => 0; set { } }
public int TrainerID7 { get => (int)((uint)(TID | (SID << 16)) % 1000000); set => SetID7(TrainerSID7, value); }
public int TrainerSID7 { get => (int)((uint)(TID | (SID << 16)) / 1000000); set => SetID7(value, TrainerID7); }
public virtual int MaxMoney => 9999999;
public virtual int MaxCoins => 9999;
public int DisplayTID
{
get => Generation >= 7 ? TrainerID7 : TID;
set { if (Generation >= 7) TrainerID7 = value; else TID = value; }
}
public int DisplaySID
{
get => Generation >= 7 ? TrainerSID7 : SID;
set { if (Generation >= 7) TrainerSID7 = value; else SID = value; }
}
#endregion
private void SetID7(int sid7, int tid7)
{
var oid = (sid7 * 1_000_000) + (tid7 % 1_000_000);
TID = (ushort)oid;
SID = oid >> 16;
}
#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<PKM> 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] = GetPartySlot(PartyBuffer, GetPartyOffset(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))
SetPartySlot(exist, PartyBuffer, GetPartyOffset(ctr++));
for (int i = ctr; i < 6; i++)
SetPartySlot(BlankPKM, PartyBuffer, GetPartyOffset(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 { get; } = 0;
public int DaycareIndex = 0;
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
public PKM GetPartySlotAtIndex(int index) => GetPartySlot(PartyBuffer, GetPartyOffset(index));
public void SetPartySlotAtIndex(PKM pkm, 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 (pkm.Species != 0)
{
if (currentCount <= index)
PartyCount = index + 1;
}
else if (currentCount > index)
{
PartyCount = index;
}
int offset = GetPartyOffset(index);
SetPartySlot(pkm, PartyBuffer, offset, trade, dex);
}
public void SetSlotFormatParty(PKM pkm, byte[] data, int offset, PKMImportSetting trade = PKMImportSetting.UseDefault, PKMImportSetting dex = PKMImportSetting.UseDefault)
{
if (pkm.GetType() != PKMType)
throw new ArgumentException($"PKM Format needs to be {PKMType} when setting to this Save File.");
UpdatePKM(pkm, isParty: true, trade, dex);
SetPartyValues(pkm, isParty: true);
WritePartySlot(pkm, data, offset);
}
public void SetPartySlot(PKM pkm, byte[] data, int offset, PKMImportSetting trade = PKMImportSetting.UseDefault, PKMImportSetting dex = PKMImportSetting.UseDefault)
{
if (pkm.GetType() != PKMType)
throw new ArgumentException($"PKM Format needs to be {PKMType} when setting to this Save File.");
UpdatePKM(pkm, isParty: true, trade, dex);
SetPartyValues(pkm, isParty: true);
WritePartySlot(pkm, data, offset);
}
public void SetSlotFormatStored(PKM pkm, Span<byte> data, int offset, PKMImportSetting trade = PKMImportSetting.UseDefault, PKMImportSetting dex = PKMImportSetting.UseDefault)
{
if (pkm.GetType() != PKMType)
throw new ArgumentException($"PKM Format needs to be {PKMType} when setting to this Save File.");
UpdatePKM(pkm, isParty: false, trade, dex);
SetPartyValues(pkm, isParty: false);
WriteSlotFormatStored(pkm, data, offset);
}
public void SetBoxSlot(PKM pkm, Span<byte> data, int offset, PKMImportSetting trade = PKMImportSetting.UseDefault, PKMImportSetting dex = PKMImportSetting.UseDefault)
{
if (pkm.GetType() != PKMType)
throw new ArgumentException($"PKM Format needs to be {PKMType} when setting to this Save File.");
UpdatePKM(pkm, isParty: false, trade, dex);
SetPartyValues(pkm, isParty: false);
WriteBoxSlot(pkm, data, offset);
}
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 IReadOnlyList<ushort> HeldItems { get; }
protected virtual byte[] BoxBuffer => Data;
protected virtual byte[] PartyBuffer => Data;
public virtual bool IsPKMPresent(ReadOnlySpan<byte> data) => PKX.IsPKMPresent(data);
public virtual PKM GetDecryptedPKM(byte[] data) => GetPKM(DecryptPKM(data));
public virtual PKM GetPartySlot(byte[] data, int offset) => GetDecryptedPKM(GetData(data, offset, SIZE_PARTY));
public virtual PKM GetStoredSlot(byte[] data, int offset) => GetDecryptedPKM(GetData(data, offset, SIZE_STORED));
public virtual PKM GetBoxSlot(int offset) => GetStoredSlot(BoxBuffer, offset);
public virtual byte[] GetDataForFormatStored(PKM pkm) => pkm.EncryptedBoxData;
public virtual byte[] GetDataForFormatParty(PKM pkm) => pkm.EncryptedPartyData;
public virtual byte[] GetDataForParty(PKM pkm) => pkm.EncryptedPartyData;
public virtual byte[] GetDataForBox(PKM pkm) => pkm.EncryptedBoxData;
public virtual void WriteSlotFormatStored(PKM pkm, Span<byte> data, int offset) => SetData(data, GetDataForFormatStored(pkm), offset);
public virtual void WriteSlotFormatParty(PKM pkm, Span<byte> data, int offset) => SetData(data, GetDataForFormatParty(pkm), offset);
public virtual void WritePartySlot(PKM pkm, Span<byte> data, int offset) => SetData(data, GetDataForParty(pkm), offset);
public virtual void WriteBoxSlot(PKM pkm, Span<byte> data, int offset) => SetData(data, GetDataForBox(pkm), offset);
protected virtual void SetPartyValues(PKM pkm, bool isParty)
{
if (!isParty)
return;
if (pkm.PartyStatsPresent) // Stats already present
return;
pkm.SetStats(pkm.GetStats(pkm.PersonalInfo));
pkm.Stat_Level = pkm.CurrentLevel;
}
/// <summary>
/// Conditions a <see cref="pkm"/> for this save file as if it was traded to it.
/// </summary>
/// <param name="pkm">Entity to adapt</param>
/// <param name="party">Entity exists in party format</param>
/// <param name="trade">Setting on whether or not to adapt</param>
public void AdaptPKM(PKM pkm, bool party = true, PKMImportSetting trade = PKMImportSetting.UseDefault)
{
if (GetTradeUpdateSetting(trade))
SetPKM(pkm, party);
}
protected void UpdatePKM(PKM pkm, bool isParty, PKMImportSetting trade, PKMImportSetting dex)
{
AdaptPKM(pkm, isParty, trade);
if (GetDexUpdateSetting(dex))
SetDex(pkm);
}
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 pkm, bool isParty = false) { }
protected virtual void SetDex(PKM pkm) { }
#endregion
#region Pokédex
public int PokeDex { get; protected set; } = int.MinValue;
public bool HasPokeDex => PokeDex > -1;
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 ? Enumerable.Range(1, MaxSpeciesID).Count(GetSeen) : 0;
public int CaughtCount => HasPokeDex ? Enumerable.Range(1, MaxSpeciesID).Count(GetCaught) : 0;
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<byte>(); set { } }
public virtual int CurrentBox { get; set; }
#region BoxData
protected int Box { get; set; } = int.MinValue;
public IList<PKM> 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<PKM> 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<PKM> 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<int>();
/// <summary>
/// Slot indexes that are protected from overwriting.
/// </summary>
protected virtual IList<int>[] SlotPointers => new[] { TeamSlots };
public virtual StorageSlotFlag GetSlotFlags(int index) => StorageSlotFlag.None;
public StorageSlotFlag GetSlotFlags(int box, int slot) => GetSlotFlags((box * BoxSlotCount) + slot);
public bool IsSlotLocked(int box, int slot) => GetSlotFlags(box, slot).HasFlagFast(StorageSlotFlag.Locked);
public bool IsSlotLocked(int index) => GetSlotFlags(index).HasFlagFast(StorageSlotFlag.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.AsSpan();
int count = BoxSlotCount * BoxCount;
for (int i = lastKnownOccupied + 1; i < count; i++)
{
int offset = GetBoxSlotOffset(i);
if (!IsPKMPresent(storage[offset..]))
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).HasFlagFast(StorageSlotFlag.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 pkm, int box, int slot, PKMImportSetting trade = PKMImportSetting.UseDefault, PKMImportSetting dex = PKMImportSetting.UseDefault)
=> SetBoxSlot(pkm, BoxBuffer, GetBoxSlotOffset(box, slot), trade, dex);
public void SetBoxSlotAtIndex(PKM pkm, int index, PKMImportSetting trade = PKMImportSetting.UseDefault, PKMImportSetting dex = PKMImportSetting.UseDefault)
=> SetBoxSlot(pkm, 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, byte[] 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); // get all boxes
string[] boxNames = Get(GetBoxName, BoxCount);
int[] boxWallpapers = Get(GetBoxWallpaper, BoxCount);
static T[] Get<T>(Func<int, T> 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++;
}
Buffer.BlockCopy(boxdata, len * i, storage, GetBoxOffset(b), len);
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<byte> boxData)
{
int b1o = GetBoxOffset(box1);
int b2o = GetBoxOffset(box2);
int len = BoxSlotCount * SIZE_BOXSLOT;
Span<byte> 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);
}
/// <summary>
/// Sorts all <see cref="PKM"/> present within the range specified by <see cref="BoxStart"/> and <see cref="BoxEnd"/> with the provied <see cref="sortMethod"/>.
/// </summary>
/// <param name="BoxStart">Starting box; if not provided, will iterate from the first box.</param>
/// <param name="BoxEnd">Ending box; if not provided, will iterate to the end.</param>
/// <param name="sortMethod">Sorting logic required to order a <see cref="PKM"/> with respect to its peers; if not provided, will use a default sorting method.</param>
/// <param name="reverse">Reverse the sorting order</param>
/// <returns>Count of repositioned <see cref="PKM"/> slots.</returns>
public int SortBoxes(int BoxStart = 0, int BoxEnd = -1, Func<IEnumerable<PKM>, int, IEnumerable<PKM>>? 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<int, bool> 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;
}
/// <summary>
/// Compresses the <see cref="BoxData"/> by pulling out the empty storage slots and putting them at the end, retaining all existing data.
/// </summary>
/// <param name="storedCount">Count of actual <see cref="PKM"/> stored.</param>
/// <param name="slotPointers">Important slot pointers that need to be repointed if a slot moves.</param>
/// <returns>True if <see cref="BoxData"/> was updated, false if no update done.</returns>
public bool CompressStorage(out int storedCount, params IList<int>[] slotPointers) => this.CompressStorage(BoxBuffer, out storedCount, slotPointers);
/// <summary>
/// Removes all <see cref="PKM"/> present within the range specified by <see cref="BoxStart"/> and <see cref="BoxEnd"/> if the provied <see cref="deleteCriteria"/> is satisfied.
/// </summary>
/// <param name="BoxStart">Starting box; if not provided, will iterate from the first box.</param>
/// <param name="BoxEnd">Ending box; if not provided, will iterate to the end.</param>
/// <param name="deleteCriteria">Criteria required to be satisfied for a <see cref="PKM"/> to be deleted; if not provided, will clear if possible.</param>
/// <returns>Count of deleted <see cref="PKM"/> slots.</returns>
public int ClearBoxes(int BoxStart = 0, int BoxEnd = -1, Func<PKM, bool>? deleteCriteria = null)
{
var storage = BoxBuffer.AsSpan();
if (BoxEnd < 0)
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, blank, ofs);
++deleted;
}
}
return deleted;
}
/// <summary>
/// Modifies all <see cref="PKM"/> present within the range specified by <see cref="BoxStart"/> and <see cref="BoxEnd"/> with the modification routine provided by <see cref="action"/>.
/// </summary>
/// <param name="action">Modification to perform on a <see cref="PKM"/></param>
/// <param name="BoxStart">Starting box; if not provided, will iterate from the first box.</param>
/// <param name="BoxEnd">Ending box (inclusive); if not provided, will iterate to the end.</param>
/// <returns>Count of modified <see cref="PKM"/> slots.</returns>
public int ModifyBoxes(Action<PKM> action, int BoxStart = 0, int BoxEnd = -1)
{
if (BoxEnd < 0)
BoxEnd = BoxCount - 1;
var storage = BoxBuffer.AsSpan();
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);
if (!IsPKMPresent(storage[ofs..]))
continue;
var pk = GetBoxSlotAtIndex(b, s);
action(pk);
++modified;
SetBoxSlot(pk, storage, ofs, 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, string 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(byte[] data)
{
if (IsRegionOverwriteProtected(0, SlotCount))
return false;
int expectLength = SlotCount * GetDataForBox(BlankPKM).Length;
return SetConcatenatedBinary(data, expectLength);
}
public bool SetBoxBinary(byte[] data, int box)
{
int start = box * BoxSlotCount;
int end = start + BoxSlotCount;
if (IsRegionOverwriteProtected(start, end))
return false;
int expectLength = BoxSlotCount * GetDataForBox(BlankPKM).Length;
return SetConcatenatedBinary(data, expectLength, start);
}
private bool SetConcatenatedBinary(byte[] data, int expectLength, int start = 0)
{
if (data.Length != expectLength)
return false;
var BD = BoxData;
var entryLength = GetDataForBox(BlankPKM).Length;
var pkdata = ArrayUtil.EnumerateSplit(data, entryLength);
pkdata.Select(GetPKM).CopyTo(BD, IsSlotOverwriteProtected, start);
BoxData = BD;
return true;
}
#endregion
}
public static class StorageUtil
{
public static bool CompressStorage(this SaveFile sav, Span<byte> storage, out int storedCount, IList<int>[] slotPointers)
{
// keep track of empty slots, and only write them at the end if slots were shifted (no need otherwise).
var empty = new List<byte[]>();
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;
}
}
}