mirror of
https://github.com/kwsch/PKHeX
synced 2024-11-16 00:58:01 +00:00
02420d3e93
* Handle some nullable cases Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data) Make some classes have explicit constructors instead of { } initialization * Handle bits more obviously without null * Make SaveFile.BAK explicitly readonly again * merge constructor methods to have readonly fields * Inline some properties * More nullable handling * Rearrange box actions define straightforward classes to not have any null properties * Make extrabyte reference array immutable * Move tooltip creation to designer * Rearrange some logic to reduce nesting * Cache generated fonts * Split mystery gift album purpose * Handle more tooltips * Disallow null setters * Don't capture RNG object, only type enum * Unify learnset objects Now have readonly properties which are never null don't new() empty learnsets (>800 Learnset objects no longer created, total of 2400 objects since we also new() a move & level array) optimize g1/2 reader for early abort case * Access rewrite Initialize blocks in a separate object, and get via that object removes a couple hundred "might be null" warnings since blocks are now readonly getters some block references have been relocated, but interfaces should expose all that's needed put HoF6 controls in a groupbox, and disable * Readonly personal data * IVs non nullable for mystery gift * Explicitly initialize forced encounter moves * Make shadow objects readonly & non-null Put murkrow fix in binary data resource, instead of on startup * Assign dex form fetch on constructor Fixes legality parsing edge cases also handle cxd parse for valid; exit before exception is thrown in FrameGenerator * Remove unnecessary null checks * Keep empty value until init SetPouch sets the value to an actual one during load, but whatever * Readonly team lock data * Readonly locks Put locked encounters at bottom (favor unlocked) * Mail readonly data / offset Rearrange some call flow and pass defaults Add fake classes for SaveDataEditor mocking Always party size, no need to check twice in stat editor use a fake save file as initial data for savedata editor, and for gamedata (wow i found a usage) constrain eventwork editor to struct variable types (uint, int, etc), thus preventing null assignment errors
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 => PKX.SIZE_4STORED;
|
|
protected override int SIZE_PARTY => PKX.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);
|
|
}
|
|
}
|
|
}
|