mirror of
https://github.com/kwsch/PKHeX
synced 2025-01-12 12:38:49 +00:00
fa9a809751
* Extract/encapsulate inventory legal arrays to interface+class Hiding them behind methods allows these to be left as ReadOnlySpan<ushort> and thus never allocate on the heap. Also refactors out some logic for checking if an item is legal. End result feels more maintainable and is less bloaty (no more passing in a nullable func) Batch editing * Add HasType filter ``` =HasType=11 .HeldItem=Meadow Plate ``` slaps a meadow plate on any pkm with grass type Use `=PersonalType1=11` for only primary grass types; only-secondary-type grass will not match it.
451 lines
17 KiB
C#
451 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using static System.Buffers.Binary.BinaryPrimitives;
|
|
|
|
namespace PKHeX.Core;
|
|
|
|
/// <summary>
|
|
/// Generation 3 <see cref="SaveFile"/> object for Pokémon XD saves.
|
|
/// </summary>
|
|
public sealed class SAV3XD : SaveFile, IGCSaveFile
|
|
{
|
|
protected internal override string ShortSummary => $"{OT} ({Version}) {PlayTimeString}";
|
|
public override string Extension => this.GCExtension();
|
|
public SAV3GCMemoryCard? MemoryCard { get; init; }
|
|
|
|
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 Config;
|
|
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];
|
|
private readonly byte[] BAK;
|
|
|
|
public SAV3XD() : base(SaveUtil.SIZE_G3XD)
|
|
{
|
|
BAK = Array.Empty<byte>();
|
|
// create fake objects
|
|
StrategyMemo = new StrategyMemo();
|
|
ShadowInfo = new ShadowInfoTableXD(false);
|
|
Config = 0xA8;
|
|
Trainer1 = 0xCCD8;
|
|
Party = 0xCD08;
|
|
Box = 0x10E08;
|
|
DaycareOffset = 0x1CA68;
|
|
Memo = 0xF678;
|
|
Shadow = 0x1CB48;
|
|
Initialize();
|
|
ClearBoxes();
|
|
}
|
|
|
|
public SAV3XD(byte[] data) : base(data)
|
|
{
|
|
BAK = data;
|
|
Japanese = InitializeData(out StrategyMemo, out ShadowInfo);
|
|
Initialize();
|
|
}
|
|
|
|
public override PersonalTable3 Personal => PersonalTable.RS;
|
|
public override ReadOnlySpan<ushort> HeldItems => Legal.HeldItems_RS;
|
|
|
|
private readonly bool Japanese;
|
|
|
|
private bool 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 = ReadInt32BigEndian(Data.AsSpan(slotOffset + 4));
|
|
if (SaveCounter <= SaveCount)
|
|
continue;
|
|
|
|
SaveCount = SaveCounter;
|
|
SaveIndex = i;
|
|
}
|
|
|
|
// Decrypt most recent save slot
|
|
Data = ReadSlot(Data, SaveIndex);
|
|
|
|
// Get Offset Info
|
|
Span<ushort> subLength = stackalloc ushort[16];
|
|
for (int i = 0; i < 16; i++)
|
|
{
|
|
subLength[i] = ReadUInt16BigEndian(Data.AsSpan(0x20 + (2 * i)));
|
|
subOffsets[i] = ReadUInt16BigEndian(Data.AsSpan(0x40 + (4 * i))) | (ReadUInt16BigEndian(Data.AsSpan(0x40 + (4 * i) + 2)) << 16);
|
|
}
|
|
|
|
// Offsets are displaced by the 0xA8 savedata region
|
|
Config = subOffsets[0] + 0xA8;
|
|
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;
|
|
|
|
bool jp = subLength[7] == 0x1E00;
|
|
memo = new StrategyMemo(Data.AsSpan(Memo, subLength[5]), xd: true);
|
|
info = new ShadowInfoTableXD(Data.AsSpan(Shadow, subLength[7]), jp);
|
|
return jp;
|
|
}
|
|
|
|
private static byte[] ReadSlot(Span<byte> data, int index)
|
|
{
|
|
int slotOffset = SLOT_START + (index * SLOT_SIZE);
|
|
var slot = data.Slice(slotOffset, SLOT_SIZE);
|
|
var result = new byte[SLOT_SIZE];
|
|
var destSpan = result.AsSpan();
|
|
|
|
// Decrypt Slot
|
|
Span<ushort> keys = stackalloc ushort[4];
|
|
GeniusCrypto.ReadKeys(slot.Slice(8, keys.Length * 2), keys);
|
|
Range r = new(0x10, 0x27FD8);
|
|
GeniusCrypto.Decrypt(slot[r], destSpan[r], keys); // body
|
|
slot[..0x10].CopyTo(destSpan[..0x10]); // checksums
|
|
slot[^0x18..].CopyTo(destSpan[^0x18..]); // tail end
|
|
return result;
|
|
}
|
|
|
|
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++)
|
|
{
|
|
var ofs = GetPartyOffset(i);
|
|
var span = Data.AsSpan(ofs);
|
|
if (ReadUInt16BigEndian(span) != 0) // species is at offset 0x00
|
|
PartyCount++;
|
|
}
|
|
}
|
|
|
|
protected override byte[] GetFinalData()
|
|
{
|
|
var newFile = GetInnerData();
|
|
|
|
// Return the gci if Memory Card is not being exported
|
|
if (MemoryCard is null)
|
|
return newFile;
|
|
|
|
MemoryCard.WriteSaveGameData(newFile);
|
|
return MemoryCard.Data;
|
|
}
|
|
|
|
private byte[] GetInnerData()
|
|
{
|
|
// Set Memo Back
|
|
StrategyMemo.Write(); // .CopyTo(Data, Memo);
|
|
ShadowInfo.Write().CopyTo(Data, Shadow);
|
|
SetChecksums();
|
|
|
|
// Put save slot back in original save data
|
|
var destOffset = SLOT_START + (SaveIndex * SLOT_SIZE);
|
|
byte[] dest = MemoryCard != null ? MemoryCard.ReadSaveGameData().ToArray() : (byte[])BAK.Clone();
|
|
var destSpan = dest.AsSpan(destOffset, Data.Length);
|
|
|
|
// Get updated save slot data
|
|
Span<byte> slot = Data;
|
|
Span<ushort> keys = stackalloc ushort[4];
|
|
GeniusCrypto.ReadKeys(slot.Slice(8, keys.Length * 2), keys);
|
|
Range r = new(0x10, 0x27FD8);
|
|
GeniusCrypto.Encrypt(slot[r], destSpan[r], keys);
|
|
slot[..0x10].CopyTo(destSpan[..0x10]); // checksum/keys
|
|
slot[^0x18..].CopyTo(destSpan[^0x18..]); // tail end
|
|
return dest;
|
|
}
|
|
|
|
// Configuration
|
|
protected override SAV3XD CloneInternal() => new(GetInnerData()) { MemoryCard = MemoryCard };
|
|
|
|
protected override int SIZE_STORED => PokeCrypto.SIZE_3XSTORED;
|
|
protected override int SIZE_PARTY => PokeCrypto.SIZE_3XSTORED; // unused
|
|
public override XK3 BlankPKM => new();
|
|
public override Type PKMType => typeof(XK3);
|
|
|
|
public override ushort MaxMoveID => Legal.MaxMoveID_3;
|
|
public override ushort 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;
|
|
public override EntityContext Context => EntityContext.Gen3;
|
|
protected override int GiftCountMax => 1;
|
|
public override int MaxStringLengthOT => 7;
|
|
public override int MaxStringLengthNickname => 10;
|
|
public override int MaxMoney => 9999999;
|
|
|
|
public override int BoxCount => 8;
|
|
|
|
public override bool IsPKMPresent(ReadOnlySpan<byte> data) => EntityDetection.IsPresentGC(data);
|
|
|
|
// 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 = ReadInt32BigEndian(Data.AsSpan(start + subOffsets[0] + 0x38));
|
|
int newHC = ReadInt32BigEndian(data.AsSpan(start + subOffsets[0] + 0x38));
|
|
bool header = newHC == oldHC;
|
|
|
|
var oldCHK = Data.AsSpan(0x10, 0x10);
|
|
var newCHK = data.AsSpan(0x10, 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 != SLOT_SIZE)
|
|
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];
|
|
|
|
WriteInt32BigEndian(data.AsSpan(start + subOffset0 + 0x38), newHC);
|
|
|
|
// Body Checksum
|
|
data.AsSpan(0x10, 0x10).Clear(); // Clear old Checksum Data
|
|
Span<uint> checksum = stackalloc uint[4];
|
|
int dt = 8;
|
|
for (int i = 0; i < checksum.Length; i++)
|
|
{
|
|
uint val = 0;
|
|
var end = dt + 0x9FF4;
|
|
for (int j = dt; j < end; j += 2)
|
|
val += ReadUInt16BigEndian(data.AsSpan(j));
|
|
dt = end;
|
|
checksum[i] = val;
|
|
}
|
|
|
|
Span<ushort> newchks = stackalloc ushort[8];
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
newchks[i*2] = (ushort)(checksum[i] >> 16);
|
|
newchks[(i * 2) + 1] = (ushort)checksum[i];
|
|
}
|
|
|
|
for (int i = 0; i < newchks.Length; i++)
|
|
{
|
|
var dest = data.AsSpan(0x10 + (2 * i));
|
|
var chk = newchks[newchks.Length - 1 - i];
|
|
WriteUInt16BigEndian(dest, chk);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
// Config
|
|
public GCVersion GCGameIndex { get => (GCVersion)Data[Config + 0x00]; set => Data[Config + 0x00] = (byte)value; }
|
|
public GCRegion CurrentRegion { get => (GCRegion)Data[Config + 0x01]; set => Data[Config + 0x01] = (byte)value; }
|
|
public GCRegion OriginalRegion { get => (GCRegion)Data[Config + 0x02]; set => Data[Config + 0x02] = (byte)value; }
|
|
public LanguageGC GCLanguage { get => (LanguageGC)Data[Config + 0x03]; set => Data[Config + 0x03] = (byte)value; }
|
|
public override int Language { get => (int)GCLanguage.ToLanguageID(); set => GCLanguage = ((LanguageID)value).ToLanguageGC(); }
|
|
|
|
private TimeSpan PlayedSpan
|
|
{
|
|
get => TimeSpan.FromSeconds(TotalSeconds);
|
|
set => TotalSeconds = value.TotalSeconds;
|
|
}
|
|
|
|
private double TotalSeconds
|
|
{
|
|
get
|
|
{
|
|
if (Japanese)
|
|
return ReadSingleBigEndian(Data.AsSpan(Config + 0x20));
|
|
return ReadDoubleBigEndian(Data.AsSpan(Config + 0x30));
|
|
}
|
|
set
|
|
{
|
|
if (Japanese)
|
|
WriteSingleBigEndian(Data.AsSpan(Config + 0x20), (float)value);
|
|
else
|
|
WriteDoubleBigEndian(Data.AsSpan(Config + 0x30), value);
|
|
}
|
|
}
|
|
|
|
public override int PlayedHours
|
|
{
|
|
get => (ushort)PlayedSpan.TotalHours;
|
|
set { var time = PlayedSpan; PlayedSpan = time - TimeSpan.FromHours(time.TotalHours) + TimeSpan.FromHours(value); }
|
|
}
|
|
|
|
public override int PlayedMinutes
|
|
{
|
|
get => (byte)PlayedSpan.Minutes;
|
|
set { var time = PlayedSpan; PlayedSpan = time - TimeSpan.FromMinutes(time.Minutes) + TimeSpan.FromMinutes(value); }
|
|
}
|
|
|
|
public override int PlayedSeconds
|
|
{
|
|
get => (byte)PlayedSpan.Seconds;
|
|
set { var time = PlayedSpan; PlayedSpan = time - TimeSpan.FromSeconds(time.Seconds) + TimeSpan.FromSeconds(value); }
|
|
}
|
|
|
|
// Trainer Info
|
|
public override GameVersion Version { get => GameVersion.XD; protected set { } }
|
|
public override string OT { get => GetString(Data.AsSpan(Trainer1 + 0x00, 20)); set => SetString(Data.AsSpan(Trainer1 + 0x00, 20), value, 10, StringConverterOption.ClearZero); }
|
|
public override uint ID32 { get => ReadUInt32BigEndian(Data.AsSpan(Trainer1 + 0x2C)); set => WriteUInt32BigEndian(Data.AsSpan(Trainer1 + 0x2C), value); }
|
|
public override ushort SID16 { get => ReadUInt16BigEndian(Data.AsSpan(Trainer1 + 0x2C)); set => WriteUInt16BigEndian(Data.AsSpan(Trainer1 + 0x2C), value); }
|
|
public override ushort TID16 { get => ReadUInt16BigEndian(Data.AsSpan(Trainer1 + 0x2E)); set => WriteUInt16BigEndian(Data.AsSpan(Trainer1 + 0x2E), value); }
|
|
|
|
public override int Gender { get => Data[Trainer1 + 0x8E0]; set => Data[Trainer1 + 0x8E0] = (byte)value; }
|
|
public override uint Money { get => ReadUInt32BigEndian(Data.AsSpan(Trainer1 + 0x8E4)); set => WriteUInt32BigEndian(Data.AsSpan(Trainer1 + 0x8E4), value); }
|
|
public uint Coupons { get => ReadUInt32BigEndian(Data.AsSpan(Trainer1 + 0x8E8)); set => WriteUInt32BigEndian(Data.AsSpan(Trainer1 + 0x8E8), value); }
|
|
|
|
// 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(Data.AsSpan(GetBoxInfoOffset(box), 16));
|
|
|
|
public override void SetBoxName(int box, ReadOnlySpan<char> value)
|
|
{
|
|
SetString(Data.AsSpan(GetBoxInfoOffset(box), 20), value, 8, StringConverterOption.ClearZero);
|
|
}
|
|
|
|
protected override XK3 GetPKM(byte[] data)
|
|
{
|
|
if (data.Length != SIZE_STORED)
|
|
Array.Resize(ref data, SIZE_STORED);
|
|
return new(data);
|
|
}
|
|
|
|
protected override byte[] DecryptPKM(byte[] data) => data;
|
|
public override XK3 GetPartySlot(ReadOnlySpan<byte> data) => GetStoredSlot(data);
|
|
|
|
public override XK3 GetStoredSlot(ReadOnlySpan<byte> data)
|
|
{
|
|
// Get Shadow Data
|
|
var pk = (XK3)base.GetStoredSlot(data);
|
|
if (pk.ShadowID > 0 && pk.ShadowID < ShadowInfo.Count)
|
|
pk.Purification = ShadowInfo[pk.ShadowID].Purification;
|
|
return pk;
|
|
}
|
|
|
|
protected override void SetPKM(PKM pk, bool isParty = false)
|
|
{
|
|
if (pk is not XK3 xk3)
|
|
return; // shouldn't ever hit
|
|
|
|
xk3.CurrentRegion = (byte)CurrentRegion;
|
|
xk3.OriginalRegion = (byte)OriginalRegion;
|
|
|
|
// Set Shadow Data back to save
|
|
if (xk3.ShadowID <= 0 || xk3.ShadowID >= ShadowInfo.Count)
|
|
return;
|
|
|
|
var entry = ShadowInfo[xk3.ShadowID];
|
|
entry.Purification = xk3.Purification;
|
|
entry.Species = xk3.Species;
|
|
entry.PID = xk3.PID;
|
|
entry.IV_HP = xk3.IV_HP ;
|
|
entry.IV_ATK = xk3.IV_ATK;
|
|
entry.IV_DEF = xk3.IV_DEF;
|
|
entry.IV_SPA = xk3.IV_SPA;
|
|
entry.IV_SPD = xk3.IV_SPD;
|
|
entry.IV_SPE = xk3.IV_SPE;
|
|
}
|
|
|
|
protected override void SetDex(PKM pk)
|
|
{
|
|
/*
|
|
if (pk.Species is 0 or > Legal.MaxSpeciesID_3)
|
|
return;
|
|
if (pk.IsEgg)
|
|
return;
|
|
|
|
// Dex Related
|
|
var entry = StrategyMemo.GetEntry(pk.Species);
|
|
if (entry.IsEmpty) // Populate
|
|
{
|
|
entry.Species = pk.Species;
|
|
entry.PID = pk.PID;
|
|
entry.TID16 = pk.TID16;
|
|
entry.SID16 = pk.SID16;
|
|
}
|
|
if (entry.Matches(pk.Species, pk.PID, pk.TID16, pk.SID16))
|
|
{
|
|
entry.Seen = true;
|
|
entry.Owned = true;
|
|
}
|
|
StrategyMemo.SetEntry(entry);
|
|
*/
|
|
}
|
|
|
|
public override IReadOnlyList<InventoryPouch> Inventory
|
|
{
|
|
get
|
|
{
|
|
var info = ItemStorage3XD.Instance;
|
|
InventoryPouch[] pouch =
|
|
{
|
|
new InventoryPouch3GC(InventoryType.Items, info, 999, OFS_PouchHeldItem, 30), // 20 COLO, 30 XD
|
|
new InventoryPouch3GC(InventoryType.KeyItems, info, 1, OFS_PouchKeyItem, 43),
|
|
new InventoryPouch3GC(InventoryType.Balls, info, 999, OFS_PouchBalls, 16),
|
|
new InventoryPouch3GC(InventoryType.TMHMs, info, 999, OFS_PouchTMHM, 64),
|
|
new InventoryPouch3GC(InventoryType.Berries, info, 999, OFS_PouchBerry, 46),
|
|
new InventoryPouch3GC(InventoryType.Medicine, info, 999, OFS_PouchCologne, 3), // Cologne
|
|
new InventoryPouch3GC(InventoryType.BattleItems, info, 1, 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(ReadOnlySpan<byte> data) => StringConverter3GC.GetString(data);
|
|
|
|
public override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option)
|
|
{
|
|
return StringConverter3GC.SetString(destBuffer, value, maxLength, option);
|
|
}
|
|
}
|