diff --git a/PKHeX.Core/PKM/PK5.cs b/PKHeX.Core/PKM/PK5.cs index 96689cefa..d592f0595 100644 --- a/PKHeX.Core/PKM/PK5.cs +++ b/PKHeX.Core/PKM/PK5.cs @@ -38,7 +38,7 @@ private static byte[] DecryptParty(byte[] data) public override void RefreshChecksum() => Checksum = CalculateChecksum(); public override bool ChecksumValid => CalculateChecksum() == Checksum; public override bool Valid { get => Sanity == 0 && ChecksumValid; set { if (!value) return; Sanity = 0; RefreshChecksum(); } } - private ushort CalculateChecksum() => Checksums.Add16(Data.AsSpan()[8..PokeCrypto.SIZE_4STORED]); + private ushort CalculateChecksum() => Checksums.Add16(Data.AsSpan()[8..PokeCrypto.SIZE_5STORED]); // Trash Bytes public override Span NicknameTrash => Data.AsSpan(0x48, 22); diff --git a/PKHeX.Core/Saves/SAV4.cs b/PKHeX.Core/Saves/SAV4.cs index 431efbefe..01a582603 100644 --- a/PKHeX.Core/Saves/SAV4.cs +++ b/PKHeX.Core/Saves/SAV4.cs @@ -205,7 +205,7 @@ private int GetActiveExtraBlock(BlockInfo4 block) var block = ExtraBlocks[index]; var active = GetActiveExtraBlock(block); - return active == -1 ? null : new BattleVideo4(Data.AsMemory((active == 0 ? 0 : PartitionSize) + block.Offset, BattleVideo4.SIZE_BLOCK)); + return active == -1 ? null : new BattleVideo4(Data.AsMemory((active == 0 ? 0 : PartitionSize) + block.Offset, BattleVideo4.SIZE_USED)); } public Hall4? GetHall() @@ -215,7 +215,7 @@ private int GetActiveExtraBlock(BlockInfo4 block) var block = ExtraBlocks[1]; var active = GetActiveExtraBlock(block); - return active == -1 ? null : new Hall4(Data.AsMemory((active == 0 ? 0 : PartitionSize) + block.Offset, Hall4.SIZE_BLOCK)); + return active == -1 ? null : new Hall4(Data.AsMemory((active == 0 ? 0 : PartitionSize) + block.Offset, Hall4.SIZE_USED)); } protected int WondercardFlags = int.MinValue; diff --git a/PKHeX.Core/Saves/SAV5.cs b/PKHeX.Core/Saves/SAV5.cs index f05d5628a..86c2686b6 100644 --- a/PKHeX.Core/Saves/SAV5.cs +++ b/PKHeX.Core/Saves/SAV5.cs @@ -193,6 +193,11 @@ public byte[] CGearSkinData public abstract AdventureInfo5 AdventureInfo { get; } IEventFlag37 IEventFlagProvider37.EventWork => EventWork; + public abstract Span BattleVideoNative { get; } + public abstract Span BattleVideoDownload1 { get; } + public abstract Span BattleVideoDownload2 { get; } + public abstract Span BattleVideoDownload3 { get; } + protected override byte[] GetFinalData() { EntreeForest.EndAccess(); diff --git a/PKHeX.Core/Saves/SAV5B2W2.cs b/PKHeX.Core/Saves/SAV5B2W2.cs index 4483f8996..dc0c611c9 100644 --- a/PKHeX.Core/Saves/SAV5B2W2.cs +++ b/PKHeX.Core/Saves/SAV5B2W2.cs @@ -72,4 +72,9 @@ public Span Rival_Trash get => Data.AsSpan(0x23BA4, MaxStringLengthOT * 2); set { if (value.Length == MaxStringLengthOT * 2) value.CopyTo(Data.AsSpan(0x23BA4)); } } + + public override Span BattleVideoNative => Data.AsSpan(0x4C000, BattleVideo5.SIZE_USED); + public override Span BattleVideoDownload1 => Data.AsSpan(0x4DA00, BattleVideo5.SIZE_USED); + public override Span BattleVideoDownload2 => Data.AsSpan(0x4F400, BattleVideo5.SIZE_USED); + public override Span BattleVideoDownload3 => Data.AsSpan(0x50E00, BattleVideo5.SIZE_USED); } diff --git a/PKHeX.Core/Saves/SAV5BW.cs b/PKHeX.Core/Saves/SAV5BW.cs index 30a71f0ed..ebfc57a11 100644 --- a/PKHeX.Core/Saves/SAV5BW.cs +++ b/PKHeX.Core/Saves/SAV5BW.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace PKHeX.Core; @@ -55,4 +56,9 @@ private void Initialize() public override GTS5 GTS => Blocks.GTS; public override WhiteBlack5BW Forest => Blocks.Forest; public override AdventureInfo5 AdventureInfo => Blocks.AdventureInfo; + + public override Span BattleVideoNative => Data.AsSpan(0x4A000, BattleVideo5.SIZE_USED); + public override Span BattleVideoDownload1 => Data.AsSpan(0x4C000, BattleVideo5.SIZE_USED); + public override Span BattleVideoDownload2 => Data.AsSpan(0x4E000, BattleVideo5.SIZE_USED); + public override Span BattleVideoDownload3 => Data.AsSpan(0x50000, BattleVideo5.SIZE_USED); } diff --git a/PKHeX.Core/Saves/Substructures/Battle Videos/BattleVideo.cs b/PKHeX.Core/Saves/Substructures/Battle Videos/BattleVideo.cs index 392680756..3c9ce3f1f 100644 --- a/PKHeX.Core/Saves/Substructures/Battle Videos/BattleVideo.cs +++ b/PKHeX.Core/Saves/Substructures/Battle Videos/BattleVideo.cs @@ -6,10 +6,12 @@ public static class BattleVideo { public static IBattleVideo? GetVariantBattleVideo(byte[] data) { - if (BattleVideo6.IsValid(data)) - return new BattleVideo6(data); if (BattleVideo7.IsValid(data)) return new BattleVideo7(data); + if (BattleVideo6.IsValid(data)) + return new BattleVideo6(data); + if (BattleVideo5.IsValid(data)) + return new BattleVideo5(data); if (BattleVideo4.IsValid(data)) return new BattleVideo4(data); if (BattleVideo3.IsValid(data)) @@ -19,9 +21,11 @@ public static class BattleVideo public static bool IsValid(ReadOnlySpan data) { + if (BattleVideo7.IsValid(data)) + return true; if (BattleVideo6.IsValid(data)) return true; - if (BattleVideo7.IsValid(data)) + if (BattleVideo5.IsValid(data)) return true; if (BattleVideo4.IsValid(data)) return true; diff --git a/PKHeX.Core/Saves/Substructures/Battle Videos/BattleVideo4.cs b/PKHeX.Core/Saves/Substructures/Battle Videos/BattleVideo4.cs index 7d8c280db..23138525e 100644 --- a/PKHeX.Core/Saves/Substructures/Battle Videos/BattleVideo4.cs +++ b/PKHeX.Core/Saves/Substructures/Battle Videos/BattleVideo4.cs @@ -6,15 +6,15 @@ namespace PKHeX.Core; /// -/// Extra-data block for Hall of Fame records. +/// Extra-data block for Battle Video records. /// /// Chunk of memory storing the structure. public sealed class BattleVideo4(Memory Raw) : IBattleVideo { private const int SIZE = 0x1D50; private const int SIZE_FOOTER = 0x10; - private const int SIZE_USED = SIZE + SIZE_FOOTER; - public const int SIZE_BLOCK = 0x2000; + public const int SIZE_USED = SIZE + SIZE_FOOTER; + //public const int SIZE_BLOCK = 0x2000; private Span Data => Raw.Span[..SIZE_USED]; diff --git a/PKHeX.Core/Saves/Substructures/Battle Videos/BattleVideo5.cs b/PKHeX.Core/Saves/Substructures/Battle Videos/BattleVideo5.cs new file mode 100644 index 000000000..f95a21ea8 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Battle Videos/BattleVideo5.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +/// +/// Extra-data block for Battle Video records. +/// +/// Chunk of memory storing the structure. +public sealed class BattleVideo5(Memory Raw) : IBattleVideo +{ + private const int SIZE = 0x1900; + private const int SIZE_FOOTER = 0x14; + public const int SIZE_USED = SIZE + SIZE_FOOTER; + //public const int SIZE_BLOCK = 0x2000; + + private Span Data => Raw.Span[..SIZE_USED]; + + public bool IsDecrypted; + private const int CryptoStart = 0xC4; + private const int CryptoEnd = 0x18A0; + private Span CryptoData => Data[CryptoStart..CryptoEnd]; + + public byte Generation => 4; + public IEnumerable Contents => GetTeam(0).Concat(GetTeam(1)); // don't bother with multi-battles + public static bool IsValid(ReadOnlySpan data) => data.Length == SIZE_USED && ReadUInt32LittleEndian(data[0x1908..]) == SIZE_USED; + + // Structure: + // 0xC4 - 0x18A0: encrypted region + // 0x18A0: u16 cryptoSeed ~~ inflate to 32 via seed |= (seed ^ 0xFFFF) << 16; + // footer + + // Encrypted Region: + // 0xC4... ??? + // 0x0CFC: u16 count Max, u16 count Present, (6 * sizeof(0x70)) pokemon -- 0x2A4 + // 0x0FA0: u16 count Max, u16 count Present, (6 * sizeof(0x70)) pokemon -- 0x2A4 + // 0x1244: u16 count Max, u16 count Present, (6 * sizeof(0x70)) pokemon -- 0x2A4 + // 0x14E8: u16 count Max, u16 count Present, (6 * sizeof(0x70)) pokemon -- 0x2A4 + // 0x178C: 0x44 trainer data (u32 flag, char[8] OT, remainder idc) + // 0x17D0: 0x44 trainer data (u32 flag, char[8] OT, remainder idc) + // 0x1814: 0x44 trainer data (u32 flag, char[8] OT, remainder idc) + // 0x1858: 0x44 trainer data (u32 flag, char[8] OT, remainder idc) + // 0x189C: 0xC bytes ??? + + public const int SizeVideoPoke = 0x70; + + public uint Key { get => ReadUInt32LittleEndian(Data); set => WriteUInt32LittleEndian(Data, value); } + public ushort Seed { get => ReadUInt16LittleEndian(Data[0x18A0..]); set => WriteUInt16LittleEndian(Data[0x18A0..], value); } + + public void Decrypt() => SetDecryptedState(true); + public void Encrypt() => SetDecryptedState(false); + public void SetDecryptedState(bool state) + { + if (state == IsDecrypted) + return; + PokeCrypto.CryptArray(CryptoData, GetEncryptionSeed()); + IsDecrypted = state; + } + + /// + /// Same as Gen4! + /// + /// + public uint GetEncryptionSeed() + { + uint seed = Seed; + return seed | (~seed << 16); + } + + #region Footer + public bool SizeValid => BlockSize == SIZE; + public bool ChecksumValid => Checksum == GetChecksum(); + + public int BlockSize { get => ReadInt32LittleEndian(Footer[0x8..]); set => WriteInt32LittleEndian(Footer[0x8..], value); } + public ushort Checksum { get => ReadUInt16LittleEndian(Footer[0x12..]); set => WriteUInt16LittleEndian(Footer[0x12..], value); } + + private ReadOnlySpan GetRegion() => Data[..SIZE_USED]; + private Span Footer => Data.Slice(SIZE, SIZE_FOOTER); + private ushort GetChecksum() => Checksums.CRC16_CCITT(GetRegion()[..^2]); + public void RefreshChecksum() => Checksum = GetChecksum(); + #endregion + + #region Conversion + + private const int TrainerCount = 4; + private const int TrainerLength = 0x2A4; + public Span Trainer1 => Data.Slice(0x0CFC, TrainerLength); + public Span Trainer2 => Data.Slice(0x0FA0, TrainerLength); + public Span Trainer3 => Data.Slice(0x1244, TrainerLength); + public Span Trainer4 => Data.Slice(0x14E8, TrainerLength); + + public PK5[] GetTeam(int trainer) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)trainer, TrainerCount); + var span = trainer switch + { + 0 => Trainer1, + 1 => Trainer2, + 2 => Trainer3, + 3 => Trainer4, + _ => throw new ArgumentOutOfRangeException(nameof(trainer)), + }; + int count = span[2]; + if (count > 6) + count = 6; + var result = new PK5[count]; + for (int i = 0; i < count; i++) + { + var ofs = 4 + (i * SizeVideoPoke); + var segment = span.Slice(ofs, SizeVideoPoke); + var entity = new PK5(); + InflateToPK5(segment, entity.Data); + result[i] = entity; + } + return result; + } + + /// + public static void InflateToPK5(ReadOnlySpan video, Span entity) + { + VerifySpanSizes(entity, video); + + video[..6].CopyTo(entity[..6]); // PID & Sanity -- skip checksum. + video[6..0xA].CopyTo(entity[8..0xC]); // Species & Held Item + // 10,11 unused alignment + video[0xC..0x16].CopyTo(entity[0xC..0x16]); // OTID, Experience, Friendship, Ability + // Skip PK4 Marking and Language + video[0x16..0x1C].CopyTo(entity[0x18..0x1E]); // EVs + // Skip PK4 contest stats and first u32 ribbon. + video[0x1C..0x30].CopyTo(entity[0x28..0x3C]); // Moves & IVs + // Skip second u32 ribbon. + entity[0x40] = video[0x30]; // Fateful & OT Gender & Form + // 0x31 alignment, skip shiny leaf & extended met location fields + video[0x32..0x48].CopyTo(entity[0x48..0x5E]); // Nickname (0x16) + // Skip Version and Ribbons + video[0x48..0x58].CopyTo(entity[0x68..0x78]); // OT Name (0x10) + + // Ball + entity[0x83] = video[0x58]; + // Language + entity[0x17] = video[0x59]; + // u16 alignment + + // Battle Stats + video[0x5C..0x70].CopyTo(entity[0x88..0x9C]); + + // We're still missing things like Version and Met Location, but this is all we can recover. + // Recalculate checksum. + ushort checksum = Checksums.Add16(entity[8..PokeCrypto.SIZE_5STORED]); + WriteUInt16LittleEndian(entity[6..], checksum); + } + + /// + public static void DeflateFromPK5(ReadOnlySpan entity, Span video) + { + VerifySpanSizes(entity, video); + + entity[..6].CopyTo(video[..6]); // PID & Sanity -- skip checksum. + // 10,11 unused alignment + entity[0xC..0x16].CopyTo(video[0xC..0x16]); // OTID, Experience, Friendship, Ability + // Skip PK4 Marking and Language + entity[0x18..0x1E].CopyTo(video[0x16..0x1C]); // EVs + // Skip PK4 contest stats and first u32 ribbon. + entity[0x28..0x3C].CopyTo(video[0x1C..0x30]); // Moves & IVs + // Skip second u32 ribbon. + video[0x30] = entity[0x40]; // Fateful & OT Gender & Form + // 0x31 alignment, skip shiny leaf & extended met location fields + entity[0x48..0x5E].CopyTo(video[0x32..0x48]); // Nickname (0x16) + // Skip Version and Ribbons + entity[0x68..0x78].CopyTo(video[0x48..0x58]); // OT Name (0x10) + + // Ball + video[0x58] = entity[0x83]; + // Language + video[0x59] = entity[0x17]; + // u16 alignment + + // Battle Stats + entity[0x88..0x9C].CopyTo(video[0x5C..0x70]); + } + + private static void VerifySpanSizes(ReadOnlySpan entity, ReadOnlySpan video) + { + if (video.Length < SizeVideoPoke) + throw new ArgumentOutOfRangeException(nameof(video), "Video size is too small."); + if (entity.Length < PokeCrypto.SIZE_5STORED) + throw new ArgumentOutOfRangeException(nameof(entity), "Entity size is too small."); + } + #endregion +} diff --git a/PKHeX.Core/Saves/Substructures/Gen4/Hall4.cs b/PKHeX.Core/Saves/Substructures/Gen4/Hall4.cs index 854fd23ac..0c975bed1 100644 --- a/PKHeX.Core/Saves/Substructures/Gen4/Hall4.cs +++ b/PKHeX.Core/Saves/Substructures/Gen4/Hall4.cs @@ -11,8 +11,8 @@ public sealed class Hall4(Memory Raw) { private const int SIZE = 0xBA0; private const int SIZE_FOOTER = 0x10; - private const int SIZE_USED = SIZE + SIZE_FOOTER; - public const int SIZE_BLOCK = 0x1000; + public const int SIZE_USED = SIZE + SIZE_FOOTER; + //public const int SIZE_BLOCK = 0x1000; private Span Data => Raw.Span[..SIZE_USED];