mirror of
https://github.com/kwsch/PKHeX
synced 2024-12-26 20:33:18 +00:00
e21d108fb2
All logic in PokeCrypto is separate from the rest of the PKHeX.Core library; makes it easy to just rip this portion out and reuse in other projects without needing the entirety of PKHeX.Core logic optimize out the CheckEncrypted to the actual path, separate methods. Only usages of this method were with hardcoded Format values, so no impact
325 lines
11 KiB
C#
325 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
|
|
namespace PKHeX.Core
|
|
{
|
|
/// <summary>
|
|
/// Generation 4 <see cref="SaveFile"/> object for Pokémon Battle Revolution saves.
|
|
/// </summary>
|
|
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<ushort> 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<int> SaveSlots = new List<int>(SAVE_COUNT);
|
|
public readonly List<string> SaveNames = new List<string>(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);
|
|
}
|
|
}
|
|
}
|