SAV3: interact with other extdata sectors

Uninitialized sectors are now no longer updated for checksums
Checksums will be updated if not uninitialized (replacing battle video)
Not that the game really checks the checksum (lol)
This commit is contained in:
Kurt 2024-06-29 23:59:11 -05:00
parent baac00c130
commit 3824c2e9f7
3 changed files with 50 additions and 22 deletions

View file

@ -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<byte> sector) =>
sector.IndexOfAnyExcept<byte>(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; }
/// <summary>
/// Hall of Fame data is split across two sectors.
/// </summary>
/// <returns>New object containing both sectors merged together.</returns>
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<byte> savedata = Data;
var sector1 = savedata.Slice(0x1C000, SIZE_SECTOR_USED);
var sector2 = savedata.Slice(0x1D000, SIZE_SECTOR_USED);
return [..sector1, ..sector2];
}
/// <summary>
/// Unmerges the two sectors of Hall of Fame data.
/// </summary>
public void SetHallOfFameData(ReadOnlySpan<byte> value)
{
ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, SIZE_SECTOR_USED * 2);
// HoF Data is split across two sav sectors
Span<byte> 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);
}
/// <summary>
/// Only used by Japanese Emerald games.
/// </summary>
public Memory<byte> GetEReaderData() => Data.AsMemory(0x1E000, SIZE_SECTOR_USED);
/// <summary> Only used in Emerald. </summary>
public Memory<byte> GetFinalExternalData() => Data.AsMemory(0x1F000, SIZE_SECTOR_USED);
public bool IsCorruptPokedexFF() => MemoryMarshal.Read<ulong>(Small.AsSpan(0xAC)) == ulong.MaxValue;
public override void CopyChangesFrom(SaveFile sav)

View file

@ -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<byte> BattleVideoData => Data.AsMemory(OFS_BV + 4, BattleVideo3.SIZE);
public Memory<byte> 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.

View file

@ -10,7 +10,7 @@ public sealed class BattleVideo3(Memory<byte> Raw) : IBattleVideo
public BattleVideo3() : this(new byte[SIZE]) { }
public Span<byte> 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<PKM> Contents => PlayerTeams.SelectMany(z => z);