mirror of
https://github.com/kwsch/PKHeX
synced 2024-12-23 02:43:13 +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
369 lines
15 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|