PKHeX/PKHeX.Core/Saves/SAV3XD.cs
Kurt e21d108fb2 Split PokeCrypto from PKX
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
2020-01-04 14:48:39 -08:00

369 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
namespace PKHeX.Core
{
/// <summary>
/// Generation 3 <see cref="SaveFile"/> object for Pokémon XD saves.
/// </summary>
public sealed class SAV3XD : SaveFile, IGCSaveFile
{
protected override string BAKText => $"{OT} ({Version}) #{SaveCount:0000}";
public override string Filter => this.GCFilter();
public override string Extension => this.GCExtension();
public bool IsMemoryCardSave => MC != null;
private readonly SAV3GCMemoryCard? MC;
private const int SLOT_SIZE = 0x28000;
private const int SLOT_START = 0x6000;
private const int SLOT_COUNT = 2;
private int SaveCount = -1;
private int SaveIndex = -1;
private int Trainer1;
private int Memo;
private int Shadow;
private readonly StrategyMemo StrategyMemo;
private readonly ShadowInfoTableXD ShadowInfo;
public int MaxShadowID => ShadowInfo.Count;
private int OFS_PouchHeldItem, OFS_PouchKeyItem, OFS_PouchBalls, OFS_PouchTMHM, OFS_PouchBerry, OFS_PouchCologne, OFS_PouchDisc;
private readonly int[] subOffsets = new int[16];
public SAV3XD(byte[] data, SAV3GCMemoryCard MC) : this(data, MC.Data) { this.MC = MC; }
public SAV3XD(byte[] data) : this(data, (byte[])data.Clone()) { }
public SAV3XD() : base(SaveUtil.SIZE_G3XD)
{
// create fake objects
StrategyMemo = new StrategyMemo();
ShadowInfo = new ShadowInfoTableXD();
Initialize();
ClearBoxes();
}
private SAV3XD(byte[] data, byte[] bak) : base(data, bak)
{
InitializeData(out StrategyMemo, out ShadowInfo);
Initialize();
}
public override PersonalTable Personal => PersonalTable.RS;
public override IReadOnlyList<ushort> HeldItems => Legal.HeldItems_XD;
private void InitializeData(out StrategyMemo memo, out ShadowInfoTableXD info)
{
// Scan all 3 save slots for the highest counter
for (int i = 0; i < SLOT_COUNT; i++)
{
int slotOffset = SLOT_START + (i * SLOT_SIZE);
int SaveCounter = BigEndian.ToInt32(Data, slotOffset + 4);
if (SaveCounter <= SaveCount)
continue;
SaveCount = SaveCounter;
SaveIndex = i;
}
// Decrypt most recent save slot
{
byte[] slot = new byte[SLOT_SIZE];
int slotOffset = SLOT_START + (SaveIndex * SLOT_SIZE);
Array.Copy(Data, slotOffset, slot, 0, slot.Length);
ushort[] keys = new ushort[4];
for (int i = 0; i < keys.Length; i++)
keys[i] = BigEndian.ToUInt16(slot, 8 + (i * 2));
// Decrypt Slot
Data = GCSaveUtil.Decrypt(slot, 0x00010, 0x27FD8, keys);
}
// Get Offset Info
ushort[] subLength = new ushort[16];
for (int i = 0; i < 16; i++)
{
subLength[i] = BigEndian.ToUInt16(Data, 0x20 + (2 * i));
subOffsets[i] = BigEndian.ToUInt16(Data, 0x40 + (4 * i)) | BigEndian.ToUInt16(Data, 0x40 + (4 * i) + 2) << 16;
}
// Offsets are displaced by the 0xA8 savedata region
Trainer1 = subOffsets[1] + 0xA8;
Party = Trainer1 + 0x30;
Box = subOffsets[2] + 0xA8;
DaycareOffset = subOffsets[4] + 0xA8;
Memo = subOffsets[5] + 0xA8;
Shadow = subOffsets[7] + 0xA8;
// Purifier = subOffsets[14] + 0xA8;
memo = new StrategyMemo(Data, Memo, xd: true);
info = new ShadowInfoTableXD(Data.Slice(Shadow, subLength[7]));
}
private void Initialize()
{
OFS_PouchHeldItem = Trainer1 + 0x4C8;
OFS_PouchKeyItem = Trainer1 + 0x540;
OFS_PouchBalls = Trainer1 + 0x5EC;
OFS_PouchTMHM = Trainer1 + 0x62C;
OFS_PouchBerry = Trainer1 + 0x72C;
OFS_PouchCologne = Trainer1 + 0x7E4;
OFS_PouchDisc = Trainer1 + 0x7F0;
// Since PartyCount is not stored in the save file,
// Count up how many party slots are active.
for (int i = 0; i < 6; i++)
{
if (GetPartySlot(Data, GetPartyOffset(i)).Species != 0)
PartyCount++;
}
}
protected override byte[] GetFinalData()
{
var newFile = GetInnerData();
// Return the gci if Memory Card is not being exported
if (!IsMemoryCardSave)
return newFile;
MC!.SelectedSaveData = newFile;
return MC.Data;
}
private byte[] GetInnerData()
{
// Set Memo Back
StrategyMemo.Write().CopyTo(Data, Memo);
ShadowInfo.Write().CopyTo(Data, Shadow);
SetChecksums();
// Get updated save slot data
ushort[] keys = new ushort[4];
for (int i = 0; i < keys.Length; i++)
keys[i] = BigEndian.ToUInt16(Data, 8 + (i * 2));
byte[] newSAV = GCSaveUtil.Encrypt(Data, 0x10, 0x27FD8, keys);
// Put save slot back in original save data
byte[] newFile = MC != null ? MC.SelectedSaveData : (byte[])BAK.Clone();
Array.Copy(newSAV, 0, newFile, SLOT_START + (SaveIndex * SLOT_SIZE), newSAV.Length);
return newFile;
}
// Configuration
public override SaveFile Clone()
{
var data = GetInnerData();
var sav = IsMemoryCardSave ? new SAV3XD(data, MC!) : new SAV3XD(data);
sav.Header = (byte[]) Header.Clone();
return sav;
}
public override int SIZE_STORED => PokeCrypto.SIZE_3XSTORED;
protected override int SIZE_PARTY => PokeCrypto.SIZE_3XSTORED; // unused
public override PKM BlankPKM => new XK3();
public override Type PKMType => typeof(XK3);
public override int MaxMoveID => Legal.MaxMoveID_3;
public override int MaxSpeciesID => Legal.MaxSpeciesID_3;
public override int MaxAbilityID => Legal.MaxAbilityID_3;
public override int MaxBallID => Legal.MaxBallID_3;
public override int MaxItemID => Legal.MaxItemID_3_XD;
public override int MaxGameID => Legal.MaxGameID_3;
public override int MaxEV => 255;
public override int Generation => 3;
protected override int GiftCountMax => 1;
public override int OTLength => 7;
public override int NickLength => 10;
public override int MaxMoney => 999999;
public override int BoxCount => 8;
public override bool IsPKMPresent(byte[] data, int offset) => PKX.IsPKMPresentGC(Data, offset);
// Checksums
protected override void SetChecksums()
{
Data = SetChecksums(Data, subOffsets[0]);
}
public override bool ChecksumsValid => !ChecksumInfo.Contains("Invalid");
public override string ChecksumInfo
{
get
{
byte[] data = SetChecksums(Data, subOffsets[0]);
const int start = 0xA8; // 0x88 + 0x20
int oldHC = BigEndian.ToInt32(Data, start + subOffsets[0] + 0x38);
int newHC = BigEndian.ToInt32(data, start + subOffsets[0] + 0x38);
bool header = newHC == oldHC;
var oldCHK = Data.Skip(0x10).Take(0x10);
var newCHK = data.Skip(0x10).Take(0x10);
bool body = newCHK.SequenceEqual(oldCHK);
return $"Header Checksum {(header ? "V" : "Inv")}alid, Body Checksum {(body ? "V" : "Inv")}alid.";
}
}
private static byte[] SetChecksums(byte[] input, int subOffset0)
{
if (input.Length != 0x28000)
throw new ArgumentException("Input should be a slot, not the entire save binary.");
byte[] data = (byte[])input.Clone();
const int start = 0xA8; // 0x88 + 0x20
// Header Checksum
int newHC = 0;
for (int i = 0; i < 8; i++)
newHC += data[i];
BigEndian.GetBytes(newHC).CopyTo(data, start + subOffset0 + 0x38);
// Body Checksum
new byte[16].CopyTo(data, 0x10); // Clear old Checksum Data
uint[] checksum = new uint[4];
int dt = 8;
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 0x9FF4; j += 2, dt += 2)
checksum[i] += BigEndian.ToUInt16(data, dt);
}
ushort[] newchks = new ushort[8];
for (int i = 0; i < 4; i++)
{
newchks[i*2] = (ushort)(checksum[i] >> 16);
newchks[(i * 2) + 1] = (ushort)checksum[i];
}
Array.Reverse(newchks);
for (int i = 0; i < newchks.Length; i++)
BigEndian.GetBytes(newchks[i]).CopyTo(data, 0x10 + (2 * i));
return data;
}
// Trainer Info
public override GameVersion Version { get => GameVersion.XD; protected set { } }
public override string OT { get => GetString(Trainer1 + 0x00, 20); set => SetString(value, 10).CopyTo(Data, Trainer1 + 0x00); }
public override int SID { get => BigEndian.ToUInt16(Data, Trainer1 + 0x2C); set => BigEndian.GetBytes((ushort)value).CopyTo(Data, Trainer1 + 0x2C); }
public override int TID { get => BigEndian.ToUInt16(Data, Trainer1 + 0x2E); set => BigEndian.GetBytes((ushort)value).CopyTo(Data, Trainer1 + 0x2E); }
public override int Gender { get => Data[Trainer1 + 0x8E0]; set => Data[Trainer1 + 0x8E0] = (byte)value; }
public override uint Money { get => BigEndian.ToUInt32(Data, Trainer1 + 0x8E4); set => BigEndian.GetBytes(value).CopyTo(Data, Trainer1 + 0x8E4); }
public uint Coupons { get => BigEndian.ToUInt32(Data, Trainer1 + 0x8E8); set => BigEndian.GetBytes(value).CopyTo(Data, Trainer1 + 0x8E8); }
// Storage
public override int GetPartyOffset(int slot) => Party + (SIZE_STORED * slot);
private int GetBoxInfoOffset(int box) => Box + (((30 * SIZE_STORED) + 0x14) * box);
public override int GetBoxOffset(int box) => GetBoxInfoOffset(box) + 20;
public override string GetBoxName(int box) => GetString(GetBoxInfoOffset(box), 16);
public override void SetBoxName(int box, string value)
{
if (value.Length > 8)
value = value.Substring(0, 8); // Hard cap
SetString(value, 8).CopyTo(Data, GetBoxInfoOffset(box));
}
protected override PKM GetPKM(byte[] data)
{
if (data.Length != SIZE_STORED)
Array.Resize(ref data, SIZE_STORED);
return new XK3(data);
}
protected override byte[] DecryptPKM(byte[] data) => data;
public override PKM GetPartySlot(byte[] data, int offset) => GetStoredSlot(data, offset);
public override PKM GetStoredSlot(byte[] data, int offset)
{
// Get Shadow Data
var pk = (XK3)base.GetStoredSlot(data, offset);
if (pk.ShadowID > 0 && pk.ShadowID < ShadowInfo.Count)
pk.Purification = ShadowInfo[pk.ShadowID].Purification;
return pk;
}
protected override void SetPKM(PKM pkm)
{
if (!(pkm is XK3 pk))
return; // shouldn't ever hit
if (pk.CurrentRegion == 0)
pk.CurrentRegion = 2; // NTSC-U
if (pk.OriginalRegion == 0)
pk.OriginalRegion = 2; // NTSC-U
// Set Shadow Data back to save
if (pk.ShadowID <= 0 || pk.ShadowID >= ShadowInfo.Count)
return;
var entry = ShadowInfo[pk.ShadowID];
entry.Purification = pk.Purification;
entry.Species = pk.Species;
entry.PID = pk.PID;
entry.IsPurified = pk.Purification == 0;
}
protected override void SetDex(PKM pkm)
{
// Dex Related
var entry = StrategyMemo.GetEntry(pkm.Species);
if (entry.IsEmpty) // Populate
{
entry.Species = pkm.Species;
entry.PID = pkm.PID;
entry.TID = pkm.TID;
entry.SID = pkm.SID;
}
if (entry.Matches(pkm.Species, pkm.PID, pkm.TID, pkm.SID))
{
entry.Seen = true;
entry.Owned = true;
}
StrategyMemo.SetEntry(entry);
}
public override InventoryPouch[] Inventory
{
get
{
InventoryPouch[] pouch =
{
new InventoryPouch3GC(InventoryType.Items, Legal.Pouch_Items_XD, 999, OFS_PouchHeldItem, 30), // 20 COLO, 30 XD
new InventoryPouch3GC(InventoryType.KeyItems, Legal.Pouch_Key_XD, 1, OFS_PouchKeyItem, 43),
new InventoryPouch3GC(InventoryType.Balls, Legal.Pouch_Ball_RS, 999, OFS_PouchBalls, 16),
new InventoryPouch3GC(InventoryType.TMHMs, Legal.Pouch_TM_RS, 999, OFS_PouchTMHM, 64),
new InventoryPouch3GC(InventoryType.Berries, Legal.Pouch_Berries_RS, 999, OFS_PouchBerry, 46),
new InventoryPouch3GC(InventoryType.Medicine, Legal.Pouch_Cologne_XD, 999, OFS_PouchCologne, 3), // Cologne
new InventoryPouch3GC(InventoryType.BattleItems, Legal.Pouch_Disc_XD, 999, OFS_PouchDisc, 60)
};
return pouch.LoadAll(Data);
}
set => value.SaveAll(Data);
}
// Daycare Structure:
// 0x00 -- Occupied
// 0x01 -- Deposited Level
// 0x02-0x03 -- unused?
// 0x04-0x07 -- Initial EXP
public override int GetDaycareSlotOffset(int loc, int slot) { return DaycareOffset + 8; }
public override uint? GetDaycareEXP(int loc, int slot) { return null; }
public override bool? IsDaycareOccupied(int loc, int slot) { return null; }
public override void SetDaycareEXP(int loc, int slot, uint EXP) { /* todo */ }
public override void SetDaycareOccupied(int loc, int slot, bool occupied) { /* todo */ }
public override string GetString(byte[] data, int offset, int length) => StringConverter3.GetBEString3(data, offset, length);
public override byte[] SetString(string value, int maxLength, int PadToSize = 0, ushort PadWith = 0)
{
if (PadToSize == 0)
PadToSize = maxLength + 1;
return StringConverter3.SetBEString3(value, maxLength, PadToSize, PadWith);
}
}
}