Enhance Stadium1 box read/initialization

Detect the current box buffer, initialize boxes if not already, and hide (delete) slots that aren't present (ignore ghost slots).
Compress storage before export.
Closes #4454
This commit is contained in:
Kurt
2025-03-01 20:07:26 -06:00
parent 78ccc92dea
commit c47e595cd3
7 changed files with 147 additions and 16 deletions

View File

@@ -0,0 +1,13 @@
namespace PKHeX.Core;
/// <summary>
/// Interface for storage cleanup before exporting.
/// </summary>
public interface IStorageCleanup
{
/// <summary>
/// Fixes the storage before writing to the save.
/// </summary>
/// <returns>True if the storage was modified, false otherwise.</returns>
bool FixStoragePreWrite();
}

View File

@@ -7,7 +7,7 @@ namespace PKHeX.Core;
/// <summary>
/// Pokémon Stadium (Pokémon Stadium 2 in Japan)
/// </summary>
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<byte> header, ReadOnlySpan<byte> 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; }
/// <summary>
/// Detect the current box buffer, initialize boxes if not already, and hide (delete) slots that aren't present (ignore ghost slots).
/// </summary>
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<byte> raw, Span<byte> header, Span<byte> 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<byte>(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<byte>(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)

View File

@@ -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<byte>(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<byte>(Data, boxOfs, size));
var chk = Checksums.CheckSum16(Data.AsSpan(boxOfs, size));
WriteUInt16BigEndian(Data.AsSpan(boxOfs + size), chk);
}

View File

@@ -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<byte>(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<byte>(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<byte>(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<byte>(Data, ofsHeld, sizeHeld));
var chkHeld = Checksums.CheckSum16(Data.AsSpan(ofsHeld, sizeHeld));
WriteUInt16BigEndian(Data.AsSpan(ofsHeld + sizeHeld), chkHeld);
}
#endregion

View File

@@ -6,7 +6,7 @@ namespace PKHeX.Core;
/// <summary>
/// Generation 7 <see cref="SaveFile"/> object for <see cref="GameVersion.GG"/> games.
/// </summary>
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)
{

View File

@@ -107,8 +107,7 @@ public sealed override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> v
/// <param name="data">Full savedata</param>
private static void ReverseEndianness(Span<byte> data)
{
var uintArr = MemoryMarshal.Cast<byte, uint>(data);
for (int i = 0; i < uintArr.Length; i++)
uintArr[i] = BinaryPrimitives.ReverseEndianness(uintArr[i]);
var as32 = MemoryMarshal.Cast<byte, int>(data);
BinaryPrimitives.ReverseEndianness(as32, as32);
}
}

View File

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