diff --git a/PKHeX.Core/Saves/Abstractions/IStorageCleanup.cs b/PKHeX.Core/Saves/Abstractions/IStorageCleanup.cs
new file mode 100644
index 000000000..a72abe3bf
--- /dev/null
+++ b/PKHeX.Core/Saves/Abstractions/IStorageCleanup.cs
@@ -0,0 +1,13 @@
+namespace PKHeX.Core;
+
+///
+/// Interface for storage cleanup before exporting.
+///
+public interface IStorageCleanup
+{
+ ///
+ /// Fixes the storage before writing to the save.
+ ///
+ /// True if the storage was modified, false otherwise.
+ bool FixStoragePreWrite();
+}
diff --git a/PKHeX.Core/Saves/SAV1Stadium.cs b/PKHeX.Core/Saves/SAV1Stadium.cs
index 7982bc913..c4a11e321 100644
--- a/PKHeX.Core/Saves/SAV1Stadium.cs
+++ b/PKHeX.Core/Saves/SAV1Stadium.cs
@@ -7,7 +7,7 @@ namespace PKHeX.Core;
///
/// Pokémon Stadium (Pokémon Stadium 2 in Japan)
///
-public sealed class SAV1Stadium : SAV_STADIUM
+public sealed class SAV1Stadium : SAV_STADIUM, IStorageCleanup
{
public override int SaveRevision => Japanese ? 0 : 1;
public override string SaveRevisionString => Japanese ? "J" : "U";
@@ -66,6 +66,7 @@ public sealed class SAV1Stadium : SAV_STADIUM
public SAV1Stadium(byte[] data, bool japanese) : base(data, japanese, GetIsSwap(data, japanese))
{
Box = BoxStart;
+ ConditionBoxes();
}
public SAV1Stadium(bool japanese = false) : base(japanese, SaveUtil.SIZE_G1STAD)
@@ -74,11 +75,93 @@ public SAV1Stadium(bool japanese = false) : base(japanese, SaveUtil.SIZE_G1STAD)
ClearBoxes();
}
+ public static bool IsHeaderValid(ReadOnlySpan header, ReadOnlySpan footer, bool japanese)
+ {
+ var h = ReadUInt32BigEndian(header);
+ var expect = GetExpectedHeader4(japanese);
+ if (h != expect)
+ return false;
+
+ var f = ReadUInt32LittleEndian(footer);
+ if (f != MAGIC_FOOTER)
+ return false;
+
+ return true;
+ }
+
+ private static uint GetExpectedHeader4(bool japanese) => japanese ? 0x00_00_50_00u : 0x00_50_00_00u;
+
+ public bool IsUsingBackupBoxSlots { get; private set; }
+
+ ///
+ /// Detect the current box buffer, initialize boxes if not already, and hide (delete) slots that aren't present (ignore ghost slots).
+ ///
+ private void ConditionBoxes()
+ {
+ var blank = BlankPKM;
+ for (int i = 0; i < BoxCount; i++)
+ {
+ // If the box is uninitialized, reset it to the right state.
+ var ofs = GetBoxOffset(i);
+ var raw = Data.AsSpan(ofs - ListHeaderSize, BoxSize);
+ var header = raw[..ListHeaderSize];
+ var footer = raw[^ListFooterSize..];
+
+ if (!IsHeaderValid(header, footer, Japanese))
+ {
+ if (i == 0 && !IsUsingBackupBoxSlots && IsBackupSegmentValid())
+ {
+ IsUsingBackupBoxSlots = true;
+ Box += 0x1_0000;
+ i--; // reset to repeat iteration on backup-Box-0.
+ continue;
+ }
+ ResetBox(raw, header, footer);
+ continue;
+ }
+
+ // Wipe empty slots after the count; don't display ghost slots.
+ var count = GetBoxSlotCount(ofs);
+ if (count >= BoxSlotCount)
+ continue; // already full
+
+ // Fill empty slots with blank PKM so that arbitrary reads are correct
+ // If you want to see the ghost slots, add your own code to `continue` instead of doing the loop.
+ for (int s = count; s < BoxSlotCount; s++)
+ {
+ var rel = ofs + (s * SIZE_STORED);
+ var slice = Data.AsSpan(rel, SIZE_STORED);
+ var species = slice[0];
+ if (species == 0) // don't bother converting from internal->national
+ continue; // don't bother wiping already-empty slots.
+ WriteBoxSlot(blank, slice);
+ }
+ }
+ }
+
+ private bool IsBackupSegmentValid()
+ {
+ var ofs = GetBoxOffset(0);
+ ofs += 0x1_0000;
+ var raw = Data.AsSpan(ofs - ListHeaderSize, BoxSize);
+ var header = raw[..ListHeaderSize];
+ var footer = raw[^ListFooterSize..];
+ return IsHeaderValid(header, footer, Japanese);
+ }
+
+ private void ResetBox(Span raw, Span header, Span footer)
+ {
+ raw.Clear();
+ header.Clear();
+ WriteUInt32BigEndian(header, GetExpectedHeader4(Japanese));
+ WriteUInt32LittleEndian(footer, MAGIC_FOOTER);
+ }
+
protected override bool GetIsBoxChecksumValid(int box)
{
var boxOfs = GetBoxOffset(box) - ListHeaderSize;
var size = BoxSize - 2;
- var chk = Checksums.CheckSum16(new ReadOnlySpan(Data, boxOfs, size));
+ var chk = Checksums.CheckSum16(Data.AsSpan(boxOfs, size));
var actual = ReadUInt16BigEndian(Data.AsSpan(boxOfs + size));
return chk == actual;
}
@@ -87,7 +170,7 @@ protected override void SetBoxChecksum(int box)
{
var boxOfs = GetBoxOffset(box) - ListHeaderSize;
var size = BoxSize - 2;
- var chk = Checksums.CheckSum16(new ReadOnlySpan(Data, boxOfs, size));
+ var chk = Checksums.CheckSum16(Data.AsSpan(boxOfs, size));
WriteUInt16BigEndian(Data.AsSpan(boxOfs + size), chk);
}
@@ -102,10 +185,46 @@ protected override void SetBoxMetadata(int box)
var rel = bdata + (SIZE_STORED * s);
if (Data[rel] != 0) // Species present
count++;
+ else
+ break; // stop at first empty slot
}
// Last byte of header
- Data[bdata - 1] = (byte)count;
+ SetBoxSlotCount(bdata, count);
+ }
+
+ // offset immediately after the header
+ private void SetBoxSlotCount(int boxDataStart, int count) => Data[boxDataStart - 1] = (byte)count;
+ private byte GetBoxSlotCount(int boxDataStart) => Data[boxDataStart - 1];
+
+ public bool FixStoragePreWrite()
+ {
+ // Compress the storage.
+ bool anyShifted = false;
+ // For each box, move present slots to the front.
+ for (int i = 0; i < BoxCount; i++)
+ {
+ int present = 0;
+ var ofs = GetBoxOffset(i);
+ for (int s = 0; s < BoxSlotCount; s++)
+ {
+ var rel = ofs + (s * SIZE_STORED);
+ var species = Data[rel];
+ if (species == 0)
+ continue;
+ if (present != s)
+ {
+ anyShifted = true;
+ var upSlot = Data.AsSpan(ofs + (present * SIZE_STORED));
+ var src = Data.AsSpan(rel, SIZE_STORED);
+ src.CopyTo(upSlot);
+ // wipe the old slot
+ src.Clear();
+ }
+ present++;
+ }
+ }
+ return anyShifted;
}
protected override PK1 GetPKM(byte[] data)
diff --git a/PKHeX.Core/Saves/SAV1StadiumJ.cs b/PKHeX.Core/Saves/SAV1StadiumJ.cs
index b000ba882..46a078266 100644
--- a/PKHeX.Core/Saves/SAV1StadiumJ.cs
+++ b/PKHeX.Core/Saves/SAV1StadiumJ.cs
@@ -62,7 +62,7 @@ protected override bool GetIsBoxChecksumValid(int box)
{
var boxOfs = GetBoxOffset(box) - ListHeaderSize;
const int size = BoxSizeJ - 2;
- var chk = Checksums.CheckSum16(new ReadOnlySpan(Data, boxOfs, size));
+ var chk = Checksums.CheckSum16(Data.AsSpan(boxOfs, size));
var actual = ReadUInt16BigEndian(Data.AsSpan(boxOfs + size));
return chk == actual;
}
@@ -71,7 +71,7 @@ protected override void SetBoxChecksum(int box)
{
var boxOfs = GetBoxOffset(box) - ListHeaderSize;
const int size = BoxSizeJ - 2;
- var chk = Checksums.CheckSum16(new ReadOnlySpan(Data, boxOfs, size));
+ var chk = Checksums.CheckSum16(Data.AsSpan(boxOfs, size));
WriteUInt16BigEndian(Data.AsSpan(boxOfs + size), chk);
}
diff --git a/PKHeX.Core/Saves/SAV2Stadium.cs b/PKHeX.Core/Saves/SAV2Stadium.cs
index a0c66d5fe..e93c11202 100644
--- a/PKHeX.Core/Saves/SAV2Stadium.cs
+++ b/PKHeX.Core/Saves/SAV2Stadium.cs
@@ -80,7 +80,7 @@ protected override bool GetIsBoxChecksumValid(int box)
{
var boxOfs = GetBoxOffset(box) - ListHeaderSizeBox;
var size = BoxSize - 2;
- var chk = Checksums.CheckSum16(new ReadOnlySpan(Data, boxOfs, size));
+ var chk = Checksums.CheckSum16(Data.AsSpan(boxOfs, size));
var actual = ReadUInt16BigEndian(Data.AsSpan(boxOfs + size));
return chk == actual;
}
@@ -118,7 +118,7 @@ protected override void SetBoxChecksum(int box)
{
var boxOfs = GetBoxOffset(box) - ListHeaderSizeBox;
var size = BoxSize - 2;
- var chk = Checksums.CheckSum16(new ReadOnlySpan(Data, boxOfs, size));
+ var chk = Checksums.CheckSum16(Data.AsSpan(boxOfs, size));
WriteUInt16BigEndian(Data.AsSpan(boxOfs + size), chk);
}
@@ -217,12 +217,12 @@ private void SetMailChecksums()
{
var ofs = MailboxBlockOffset(Language);
var size = MailboxBlockSize - 2;
- var chk = Checksums.CheckSum16(new ReadOnlySpan(Data, ofs, size));
+ var chk = Checksums.CheckSum16(Data.AsSpan(ofs, size));
WriteUInt16BigEndian(Data.AsSpan(ofs + size), chk);
var ofsHeld = MailboxHeldBlockOffset(Language);
var sizeHeld = MailboxHeldBlockSize - 2;
- var chkHeld = Checksums.CheckSum16(new ReadOnlySpan(Data, ofsHeld, sizeHeld));
+ var chkHeld = Checksums.CheckSum16(Data.AsSpan(ofsHeld, sizeHeld));
WriteUInt16BigEndian(Data.AsSpan(ofsHeld + sizeHeld), chkHeld);
}
#endregion
diff --git a/PKHeX.Core/Saves/SAV7b.cs b/PKHeX.Core/Saves/SAV7b.cs
index 50fbefef3..27353f6ba 100644
--- a/PKHeX.Core/Saves/SAV7b.cs
+++ b/PKHeX.Core/Saves/SAV7b.cs
@@ -6,7 +6,7 @@ namespace PKHeX.Core;
///
/// Generation 7 object for games.
///
-public sealed class SAV7b : SAV_BEEF, ISaveBlock7b, IGameSync, IMysteryGiftStorageProvider
+public sealed class SAV7b : SAV_BEEF, ISaveBlock7b, IGameSync, IMysteryGiftStorageProvider, IStorageCleanup
{
protected internal override string ShortSummary => $"{OT} ({Version}) - {Blocks.Played.LastSavedTime}";
public override string Extension => ".bin";
@@ -89,7 +89,7 @@ private void Initialize()
public override int BoxSlotCount => 25;
public override int BoxCount => 40; // 1000/25
- public bool FixPreWrite() => Blocks.Storage.CompressStorage();
+ public bool FixStoragePreWrite() => Blocks.Storage.CompressStorage();
protected override void SetPKM(PKM pk, bool isParty = false)
{
diff --git a/PKHeX.Core/Saves/SAV_STADIUM.cs b/PKHeX.Core/Saves/SAV_STADIUM.cs
index a93b1bd8e..b36e1ddab 100644
--- a/PKHeX.Core/Saves/SAV_STADIUM.cs
+++ b/PKHeX.Core/Saves/SAV_STADIUM.cs
@@ -107,8 +107,7 @@ public sealed override int SetString(Span destBuffer, ReadOnlySpan v
/// Full savedata
private static void ReverseEndianness(Span data)
{
- var uintArr = MemoryMarshal.Cast(data);
- for (int i = 0; i < uintArr.Length; i++)
- uintArr[i] = BinaryPrimitives.ReverseEndianness(uintArr[i]);
+ var as32 = MemoryMarshal.Cast(data);
+ BinaryPrimitives.ReverseEndianness(as32, as32);
}
}
diff --git a/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs b/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs
index 212d4aa4e..7be860c17 100644
--- a/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs
+++ b/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs
@@ -941,7 +941,7 @@ private static bool IsFolderPath(out string path)
public bool ExportSaveFile()
{
ValidateChildren();
- bool reload = SAV is SAV7b b && b.FixPreWrite();
+ bool reload = SAV is IStorageCleanup b && b.FixStoragePreWrite();
if (reload)
ReloadSlots();
return WinFormsUtil.ExportSAVDialog(SAV, SAV.CurrentBox);