using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace PKHeX.Core { /// /// Generation 4 object for Pokémon Battle Revolution saves. /// public sealed class SAV4BR : SaveFile { protected override string BAKText => $"{Version} #{SaveCount:0000}"; public override string Filter => "PbrSaveData|*"; public override string Extension => string.Empty; public override PersonalTable Personal => PersonalTable.DP; public override IReadOnlyList HeldItems => Legal.HeldItems_DP; private const int SAVE_COUNT = 4; public SAV4BR() : base(SaveUtil.SIZE_G4BR) { ClearBoxes(); } public SAV4BR(byte[] data) : base(data) { InitializeData(data); } private void InitializeData(byte[] data) { Data = DecryptPBRSaveData(data); // Detect active save SaveCount = Math.Max(BigEndian.ToUInt32(Data, 0x1C004C), BigEndian.ToUInt32(Data, 0x4C)); if (BigEndian.ToUInt32(Data, 0x1C004C) > BigEndian.ToUInt32(Data, 0x4C)) { byte[] tempData = new byte[0x1C0000]; Array.Copy(Data, 0, tempData, 0, 0x1C0000); Array.Copy(Data, 0x1C0000, Data, 0, 0x1C0000); tempData.CopyTo(Data, 0x1C0000); } for (int i = 0; i < SAVE_COUNT; i++) { if (!IsOTNamePresent(i)) continue; SaveSlots.Add(i); SaveNames.Add(GetOTName(i)); } CurrentSlot = SaveSlots[0]; } private bool IsOTNamePresent(int i) { return BitConverter.ToUInt16(Data, 0x390 + (0x6FF00 * i)) != 0; } private uint SaveCount; protected override byte[] GetFinalData() { SetChecksums(); return EncryptPBRSaveData(Data); } // Configuration public override SaveFile Clone() => new SAV4BR(Write()); public readonly List SaveSlots = new List(SAVE_COUNT); public readonly List SaveNames = new List(SAVE_COUNT); private int _currentSlot; public int CurrentSlot { get => SaveSlots.IndexOf(_currentSlot); // 4 save slots, data reading depends on current slot set { _currentSlot = SaveSlots[value]; var ofs = 0x6FF00 * _currentSlot; Box = ofs + 0x978; Party = ofs + 0x13A54; // first team slot after boxes BoxName = ofs + 0x58674; } } public override int SIZE_STORED => PokeCrypto.SIZE_4STORED; protected override int SIZE_PARTY => PokeCrypto.SIZE_4STORED + 4; public override PKM BlankPKM => new BK4(); public override Type PKMType => typeof(BK4); public override int MaxMoveID => 467; public override int MaxSpeciesID => Legal.MaxSpeciesID_4; public override int MaxAbilityID => Legal.MaxAbilityID_4; public override int MaxItemID => Legal.MaxItemID_4_HGSS; public override int MaxBallID => Legal.MaxBallID_4; public override int MaxGameID => Legal.MaxGameID_4; public override int MaxEV => 255; public override int Generation => 4; protected override int GiftCountMax => 1; public override int OTLength => 7; public override int NickLength => 10; public override int MaxMoney => 999999; public override int Language => (int)LanguageID.English; // prevent KOR from inhabiting public override int BoxCount => 18; public override int PartyCount { get { int ctr = 0; for (int i = 0; i < 6; i++) { if (Data[GetPartyOffset(i) + 4] != 0) // sanity ctr++; } return ctr; } protected set { // Ignore, value is calculated } } // Checksums protected override void SetChecksums() { SetChecksum(Data, 0, 0x100, 8); SetChecksum(Data, 0, 0x1C0000, 0x1BFF80); SetChecksum(Data, 0x1C0000, 0x100, 0x1C0008); SetChecksum(Data, 0x1C0000, 0x1C0000, 0x1BFF80 + 0x1C0000); } public override bool ChecksumsValid => IsChecksumsValid(Data); public override string ChecksumInfo => $"Checksums valid: {ChecksumsValid}."; public static bool IsChecksumsValid(byte[] sav) { return VerifyChecksum(sav, 0x000000, 0x1C0000, 0x1BFF80) && VerifyChecksum(sav, 0x000000, 0x000100, 0x000008) && VerifyChecksum(sav, 0x1C0000, 0x1C0000, 0x1BFF80 + 0x1C0000) && VerifyChecksum(sav, 0x1C0000, 0x000100, 0x1C0008); } // Trainer Info public override GameVersion Version { get => GameVersion.BATREV; protected set { } } private string GetOTName(int slot) { var ofs = 0x390 + (0x6FF00 * slot); var str = Encoding.BigEndianUnicode.GetString(Data, ofs, 0x10); return Util.TrimFromZero(str); } private void SetOTName(int slot, string name) { if (name.Length > 7) name = name.Substring(0, 7); var bytes = Encoding.BigEndianUnicode.GetBytes(name.PadRight(8, '\0')); var ofs = 0x390 + (0x6FF00 * slot); SetData(bytes, ofs); } public string CurrentOT { get => GetOTName(_currentSlot); set => SetOTName(_currentSlot, value); } // Storage public override int GetPartyOffset(int slot) => Party + (SIZE_PARTY * slot); public override int GetBoxOffset(int box) => Box + (SIZE_STORED * box * 30); // Save file does not have Box Name / Wallpaper info private int BoxName = -1; private const int BoxNameLength = 0x28; public override string GetBoxName(int box) { if (BoxName < 0) return $"BOX {box + 1}"; var str = Encoding.BigEndianUnicode.GetString(Data, BoxName + (box * BoxNameLength), BoxNameLength); str = Util.TrimFromZero(str); if (string.IsNullOrWhiteSpace(str)) return $"BOX {box + 1}"; return str; } public override void SetBoxName(int box, string value) { if (BoxName < 0) return; int ofs = BoxName + (box * BoxNameLength); var str = Encoding.BigEndianUnicode.GetString(Data, ofs, BoxNameLength); str = Util.TrimFromZero(str); if (string.IsNullOrWhiteSpace(str)) return; var data = Encoding.BigEndianUnicode.GetBytes(value.PadLeft(BoxNameLength / 2, '\0')); SetData(data, ofs); } protected override PKM GetPKM(byte[] data) { if (data.Length != SIZE_STORED) Array.Resize(ref data, SIZE_STORED); return new BK4(data); } protected override byte[] DecryptPKM(byte[] data) => data; protected override void SetDex(PKM pkm) { /* There's no PokéDex */ } protected override void SetPKM(PKM pkm) { var pk4 = (BK4)pkm; // Apply to this Save File DateTime Date = DateTime.Now; if (pk4.Trade(OT, TID, SID, Gender, Date.Day, Date.Month, Date.Year)) pkm.RefreshChecksum(); } protected override void SetPartyValues(PKM pkm, bool isParty) { pkm.Sanity = (ushort)(isParty ? 0xC000 : 0x4000); } public static byte[] DecryptPBRSaveData(byte[] input) { byte[] output = new byte[input.Length]; for (int i = 0; i < SaveUtil.SIZE_G4BR; i += 0x1C0000) { var keys = GetKeys(input, i); Array.Copy(input, i, output, i, 8); GCSaveUtil.Decrypt(input, i + 8, i + 0x1C0000, keys, output); } return output; } private static byte[] EncryptPBRSaveData(byte[] input) { byte[] output = new byte[input.Length]; for (int i = 0; i < SaveUtil.SIZE_G4BR; i += 0x1C0000) { var keys = GetKeys(input, i); Array.Copy(input, i, output, i, 8); GCSaveUtil.Encrypt(input, i + 8, i + 0x1C0000, keys, output); } return output; } private static ushort[] GetKeys(byte[] input, int ofs) { ushort[] keys = new ushort[4]; for (int i = 0; i < keys.Length; i++) keys[i] = BigEndian.ToUInt16(input, ofs + (i * 2)); return keys; } public static bool VerifyChecksum(byte[] input, int offset, int len, int checksum_offset) { uint[] storedChecksums = new uint[16]; for (int i = 0; i < storedChecksums.Length; i++) { storedChecksums[i] = BigEndian.ToUInt32(input, checksum_offset + (i * 4)); BitConverter.GetBytes(0u).CopyTo(input, checksum_offset + (i * 4)); } uint[] checksums = new uint[16]; for (int i = 0; i < len; i += 2) { uint val = BigEndian.ToUInt16(input, offset + i); for (int j = 0; j < 16; j++) { checksums[j] += ((val >> j) & 1); } } for (int i = 0; i < storedChecksums.Length; i++) { BigEndian.GetBytes(storedChecksums[i]).CopyTo(input, checksum_offset + (i * 4)); } return checksums.SequenceEqual(storedChecksums); } private static void SetChecksum(byte[] input, int offset, int len, int checksum_offset) { uint[] storedChecksums = new uint[16]; for (int i = 0; i < storedChecksums.Length; i++) { storedChecksums[i] = BigEndian.ToUInt32(input, checksum_offset + (i * 4)); BitConverter.GetBytes(0u).CopyTo(input, checksum_offset + (i * 4)); } uint[] checksums = new uint[16]; for (int i = 0; i < len; i += 2) { uint val = BigEndian.ToUInt16(input, offset + i); for (int j = 0; j < 16; j++) checksums[j] += ((val >> j) & 1); } for (int i = 0; i < checksums.Length; i++) { BigEndian.GetBytes(checksums[i]).CopyTo(input, checksum_offset + (i * 4)); } } public override string GetString(byte[] data, int offset, int length) => StringConverter4.GetBEString4(data, offset, length); public override byte[] SetString(string value, int maxLength, int PadToSize = 0, ushort PadWith = 0) { if (PadToSize == 0) PadToSize = maxLength + 1; return StringConverter4.SetBEString4(value, maxLength, PadToSize, PadWith); } } }