diff --git a/PKHeX.Core/Saves/SAV3.cs b/PKHeX.Core/Saves/SAV3.cs index 932ce4bef..d232fb0ee 100644 --- a/PKHeX.Core/Saves/SAV3.cs +++ b/PKHeX.Core/Saves/SAV3.cs @@ -29,8 +29,10 @@ public abstract class SAV3 : SaveFile, ILangDeviantSave, IEventFlag37, IBoxDetai // Extra data is always at the same sector, while the main sectors rotate sectors within their region (on each successive save?). private const int SIZE_SECTOR = 0x1000; - private const int SIZE_SECTOR_USED = 0xF80; + public const int SIZE_SECTOR_USED = 0xF80; private const int COUNT_MAIN = 14; // sectors worth of data + //private const int COUNT_BACKUP = COUNT_MAIN; // sectors worth of data + private const int COUNT_EXTRA = 4; // sectors worth of data private const int SIZE_MAIN = COUNT_MAIN * SIZE_SECTOR; // There's no harm having buffers larger than their actual size (per format). @@ -227,9 +229,8 @@ public abstract class SAV3 : SaveFile, ILangDeviantSave, IEventFlag37, IBoxDetai if (Data.Length < SaveUtil.SIZE_G3RAW) // don't update HoF for half-sizes return; - // Hall of Fame Checksums - SetSectoryValidExtra(0x1C000); - SetSectoryValidExtra(0x1D000); + for (int i = 0; i < COUNT_EXTRA; i++) + SetSectorValidExtra(0x1C000 + (i * SIZE_SECTOR)); } public sealed override bool ChecksumsValid @@ -245,17 +246,21 @@ public abstract class SAV3 : SaveFile, ILangDeviantSave, IEventFlag37, IBoxDetai if (Data.Length < SaveUtil.SIZE_G3RAW) // don't check HoF for half-sizes return true; - if (!IsSectorValidExtra(0x1C000)) - return false; - if (!IsSectorValidExtra(0x1D000)) - return false; + for (int i = 0; i < COUNT_EXTRA; i++) + { + if (!IsSectorValidExtra(0x1C000 + (i * SIZE_SECTOR))) + return false; + } + return true; } } - private void SetSectoryValidExtra(int offset) + private void SetSectorValidExtra(int offset) { var sector = Data.AsSpan(offset, SIZE_SECTOR); + if (IsSectorUninitialized(sector)) + return; var expect = Checksums.CheckSum32(sector[..SIZE_SECTOR_USED]); WriteUInt16LittleEndian(sector[0xFF4..], expect); } @@ -263,11 +268,16 @@ public abstract class SAV3 : SaveFile, ILangDeviantSave, IEventFlag37, IBoxDetai private bool IsSectorValidExtra(int offset) { var sector = Data.AsSpan(offset, SIZE_SECTOR); + if (IsSectorUninitialized(sector)) + return true; var expect = Checksums.CheckSum32(sector[..SIZE_SECTOR_USED]); var actual = ReadUInt16LittleEndian(sector[0xFF4..]); return expect == actual; } + private static bool IsSectorUninitialized(ReadOnlySpan sector) => + sector.IndexOfAnyExcept(0, 0xFF) == -1; + private bool IsSectorValid(int sectorIndex) { int start = ActiveSlot * SIZE_MAIN; @@ -295,6 +305,10 @@ public abstract class SAV3 : SaveFile, ILangDeviantSave, IEventFlag37, IBoxDetai list.Add("HoF first sector invalid."); if (!IsSectorValidExtra(0x1D000)) list.Add("HoF second sector invalid."); + if (!IsSectorValidExtra(0x1E000)) + list.Add("e-Reader data invalid."); + if (!IsSectorValidExtra(0x1F000)) + list.Add("Final extra data invalid."); } return list.Count != 0 ? string.Join(Environment.NewLine, list) : "Checksums are valid."; } @@ -629,24 +643,39 @@ public abstract class SAV3 : SaveFile, ILangDeviantSave, IEventFlag37, IBoxDetai public abstract Gen3MysteryData MysteryData { get; set; } + /// + /// Hall of Fame data is split across two sectors. + /// + /// New object containing both sectors merged together. public byte[] GetHallOfFameData() { - // HoF Data is split across two sectors - byte[] data = new byte[SIZE_SECTOR_USED * 2]; - Data.AsSpan(0x1C000, SIZE_SECTOR_USED).CopyTo(data.AsSpan(0 , SIZE_SECTOR_USED)); - Data.AsSpan(0x1D000, SIZE_SECTOR_USED).CopyTo(data.AsSpan(SIZE_SECTOR_USED, SIZE_SECTOR_USED)); - return data; + Span savedata = Data; + var sector1 = savedata.Slice(0x1C000, SIZE_SECTOR_USED); + var sector2 = savedata.Slice(0x1D000, SIZE_SECTOR_USED); + return [..sector1, ..sector2]; } + /// + /// Unmerges the two sectors of Hall of Fame data. + /// public void SetHallOfFameData(ReadOnlySpan value) { ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, SIZE_SECTOR_USED * 2); - // HoF Data is split across two sav sectors Span savedata = Data; - value[..SIZE_SECTOR_USED].CopyTo(savedata[0x1C000..]); - value.Slice(SIZE_SECTOR_USED, SIZE_SECTOR_USED).CopyTo(savedata[0x1D000..]); + var sector1 = savedata.Slice(0x1C000, SIZE_SECTOR_USED); + var sector2 = savedata.Slice(0x1D000, SIZE_SECTOR_USED); + value[..SIZE_SECTOR_USED].CopyTo(sector1); + value[SIZE_SECTOR_USED..].CopyTo(sector2); } + /// + /// Only used by Japanese Emerald games. + /// + public Memory GetEReaderData() => Data.AsMemory(0x1E000, SIZE_SECTOR_USED); + + /// Only used in Emerald. + public Memory GetFinalExternalData() => Data.AsMemory(0x1F000, SIZE_SECTOR_USED); + public bool IsCorruptPokedexFF() => MemoryMarshal.Read(Small.AsSpan(0xAC)) == ulong.MaxValue; public override void CopyChangesFrom(SaveFile sav) diff --git a/PKHeX.Core/Saves/SAV3E.cs b/PKHeX.Core/Saves/SAV3E.cs index 93e444b62..3fcf6fe8d 100644 --- a/PKHeX.Core/Saves/SAV3E.cs +++ b/PKHeX.Core/Saves/SAV3E.cs @@ -285,11 +285,10 @@ public sealed class SAV3E : SAV3, IGen3Hoenn, IGen3Joyful, IGen3Wonder, IDaycare #endregion private const uint EXTRADATA_SENTINEL = 0x0000B39D; - private const int OFS_BV = 31 * 0x1000; // last sector of the save - public bool HasBattleVideo => Data.Length > SaveUtil.SIZE_G3RAWHALF && ReadUInt32LittleEndian(Data.AsSpan(OFS_BV)) == EXTRADATA_SENTINEL; - public void SetExtraDataSentinelBattleVideo() => WriteUInt32LittleEndian(Data.AsSpan(OFS_BV), EXTRADATA_SENTINEL); + public bool HasBattleVideo => Data.Length > SaveUtil.SIZE_G3RAWHALF && ReadUInt32LittleEndian(GetFinalExternalData().Span) == EXTRADATA_SENTINEL; + public void SetExtraDataSentinelBattleVideo() => WriteUInt32LittleEndian(GetFinalExternalData().Span, EXTRADATA_SENTINEL); - public Memory BattleVideoData => Data.AsMemory(OFS_BV + 4, BattleVideo3.SIZE); + public Memory BattleVideoData => GetFinalExternalData().Slice(4, BattleVideo3.SIZE); public BattleVideo3 BattleVideo { // decouple from the save file object on get, as the consumer might not be aware that mutations will touch the save. diff --git a/PKHeX.Core/Saves/Substructures/Battle Videos/BattleVideo3.cs b/PKHeX.Core/Saves/Substructures/Battle Videos/BattleVideo3.cs index 97048c5a0..2edc41815 100644 --- a/PKHeX.Core/Saves/Substructures/Battle Videos/BattleVideo3.cs +++ b/PKHeX.Core/Saves/Substructures/Battle Videos/BattleVideo3.cs @@ -10,7 +10,7 @@ public sealed class BattleVideo3(Memory Raw) : IBattleVideo public BattleVideo3() : this(new byte[SIZE]) { } public Span Data => Raw.Span; - internal const int SIZE = 0xF80; + internal const int SIZE = SAV3.SIZE_SECTOR_USED - 4; // Skip sentinel public byte Generation => 3; public IEnumerable Contents => PlayerTeams.SelectMany(z => z);