mirror of
https://github.com/kwsch/PKHeX.git
synced 2026-10-01 14:58:19 -05:00
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:
13
PKHeX.Core/Saves/Abstractions/IStorageCleanup.cs
Normal file
13
PKHeX.Core/Saves/Abstractions/IStorageCleanup.cs
Normal 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();
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user