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);