PKHeX/PKHeX.Core/Saves/SAV5.cs
Kurt 4deb5080d1 Misc tweaks
rearrange getmove with respect to tutors; verifycurrentmoves was getting
tutor moves in levelup
extract not-found hint fetch
Expose gen5 block objects (so I can play with saves in linqpad)
2018-10-06 18:45:15 -07:00

612 lines
23 KiB
C#

using System;
using System.Linq;
using System.Text;
namespace PKHeX.Core
{
/// <summary>
/// Generation 5 <see cref="SaveFile"/> object.
/// </summary>
public sealed class SAV5 : SaveFile
{
// Save Data Attributes
protected override string BAKText => $"{OT} ({(GameVersion)Game}) - {PlayTimeString}";
public override string Filter => (Footer.Length > 0 ? "DeSmuME DSV|*.dsv|" : "") + "SAV File|*.sav|All Files|*.*";
public override string Extension => ".sav";
public SAV5(byte[] data = null, GameVersion versionOverride = GameVersion.Any)
{
Data = data ?? new byte[SaveUtil.SIZE_G5RAW];
BAK = (byte[])Data.Clone();
Exportable = !IsRangeEmpty(0, Data.Length);
// Get Version
if (data == null)
Version = GameVersion.B2W2;
else if (versionOverride != GameVersion.Any)
Version = versionOverride;
else Version = SaveUtil.GetIsG5SAV(Data);
if (Version == GameVersion.Invalid)
return;
// First blocks are always the same position/size
PCLayout = 0x0;
Box = 0x400;
Party = 0x18E00;
Trainer1 = 0x19400;
WondercardData = 0x1C800;
AdventureInfo = 0x1D900;
// Different Offsets for later blocks
switch (Version)
{
case GameVersion.BW:
BattleBox = 0x20A00;
Trainer2 = 0x21200;
EventConst = 0x20100;
EventFlag = EventConst + 0x27C;
Daycare = 0x20E00;
PokeDex = 0x21600;
PokeDexLanguageFlags = PokeDex + 0x320;
BattleSubway = 0x21D00;
CGearInfoOffset = 0x1C000;
CGearDataOffset = 0x52000;
EntreeForestOffset = 0x22C00;
// Inventory offsets are the same for each game.
OFS_PouchHeldItem = 0x18400; // 0x188D7
OFS_PouchKeyItem = 0x188D8; // 0x18A23
OFS_PouchTMHM = 0x18A24; // 0x18BD7
OFS_PouchMedicine = 0x18BD8; // 0x18C97
OFS_PouchBerry = 0x18C98; // 0x18DBF
LegalItems = Legal.Pouch_Items_BW;
LegalKeyItems = Legal.Pouch_Key_BW;
LegalTMHMs = Legal.Pouch_TMHM_BW;
LegalMedicine = Legal.Pouch_Medicine_BW;
LegalBerries = Legal.Pouch_Berries_BW;
Personal = PersonalTable.BW;
break;
case GameVersion.B2W2: // B2W2
BattleBox = 0x20900;
Trainer2 = 0x21100;
EventConst = 0x1FF00;
EventFlag = EventConst + 0x35E;
Daycare = 0x20D00;
PokeDex = 0x21400;
PokeDexLanguageFlags = PokeDex + 0x328; // forme flags size is + 8 from bw with new formes (therians)
BattleSubway = 0x21B00;
CGearInfoOffset = 0x1C000;
CGearDataOffset = 0x52800;
EntreeForestOffset = 0x22A00;
// Inventory offsets are the same for each game.
OFS_PouchHeldItem = 0x18400; // 0x188D7
OFS_PouchKeyItem = 0x188D8; // 0x18A23
OFS_PouchTMHM = 0x18A24; // 0x18BD7
OFS_PouchMedicine = 0x18BD8; // 0x18C97
OFS_PouchBerry = 0x18C98; // 0x18DBF
LegalItems = Legal.Pouch_Items_BW;
LegalKeyItems = Legal.Pouch_Key_B2W2;
LegalTMHMs = Legal.Pouch_TMHM_BW;
LegalMedicine = Legal.Pouch_Medicine_BW;
LegalBerries = Legal.Pouch_Berries_BW;
Personal = PersonalTable.B2W2;
break;
}
HeldItems = Legal.HeldItems_BW;
Blocks = Version == GameVersion.BW ? BlockInfoNDS.BlocksBW : BlockInfoNDS.BlocksB2W2;
if (!Exportable)
ClearBoxes();
}
// Configuration
public override SaveFile Clone() { return new SAV5((byte[])Data.Clone(), Version); }
public override int SIZE_STORED => PKX.SIZE_5STORED;
protected override int SIZE_PARTY => PKX.SIZE_5PARTY;
public override PKM BlankPKM => new PK5();
public override Type PKMType => typeof(PK5);
public override int BoxCount => 24;
public override int MaxEV => 255;
public override int Generation => 5;
public override int OTLength => 7;
public override int NickLength => 10;
protected override int EventConstMax => (Version == GameVersion.BW ? 0x27C : 0x35E) >> 1;
protected override int EventFlagMax => (Version == GameVersion.BW ? 0x16C : 0x17F) << 3;
protected override int GiftCountMax => 12;
public override int MaxMoveID => Legal.MaxMoveID_5;
public override int MaxSpeciesID => Legal.MaxSpeciesID_5;
public override int MaxItemID => Version == GameVersion.BW ? Legal.MaxItemID_5_BW : Legal.MaxItemID_5_B2W2;
public override int MaxAbilityID => Legal.MaxAbilityID_5;
public override int MaxBallID => Legal.MaxBallID_5;
public override int MaxGameID => Legal.MaxGameID_5; // B2
// Blocks & Offsets
public readonly BlockInfoNDS[] Blocks;
protected override void SetChecksums() => Blocks.SetChecksums(Data);
public override bool ChecksumsValid => Blocks.GetChecksumsValid(Data);
public override string ChecksumInfo => Blocks.GetChecksumInfo(Data);
private const int wcSeed = 0x1D290;
public readonly int CGearInfoOffset, CGearDataOffset;
private readonly int EntreeForestOffset;
private readonly int Trainer2, AdventureInfo, BattleSubway;
public readonly int PokeDexLanguageFlags;
// Daycare
public override int DaycareSeedSize => 16;
public override int GetDaycareSlotOffset(int loc, int slot)
{
return Daycare + 4 + (0xE4 * slot);
}
public override string GetDaycareRNGSeed(int loc)
{
if (Version != GameVersion.B2W2)
return null;
var data = Data.Skip(Daycare + 0x1CC).Take(DaycareSeedSize/2).Reverse().ToArray();
return BitConverter.ToString(data).Replace("-", "");
}
public override uint? GetDaycareEXP(int loc, int slot)
{
return BitConverter.ToUInt32(Data, Daycare + 4 + 0xDC + (slot * 0xE4));
}
public override bool? IsDaycareOccupied(int loc, int slot)
{
return BitConverter.ToUInt32(Data, Daycare + (0xE4 * slot)) == 1;
}
public override void SetDaycareEXP(int loc, int slot, uint EXP)
{
BitConverter.GetBytes(EXP).CopyTo(Data, Daycare + 4 + 0xDC + (slot * 0xE4));
}
public override void SetDaycareOccupied(int loc, int slot, bool occupied)
{
BitConverter.GetBytes((uint)(occupied ? 1 : 0)).CopyTo(Data, Daycare + 0x1CC);
}
public override void SetDaycareRNGSeed(int loc, string seed)
{
if (Version != GameVersion.B2W2)
return;
Enumerable.Range(0, seed.Length)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(seed.Substring(x, 2), 16))
.Reverse().ToArray().CopyTo(Data, Daycare + 0x1CC);
}
// Inventory
private readonly ushort[] LegalItems, LegalKeyItems, LegalTMHMs, LegalMedicine, LegalBerries;
public override InventoryPouch[] Inventory
{
get
{
InventoryPouch[] pouch =
{
new InventoryPouch4(InventoryType.Items, LegalItems, 999, OFS_PouchHeldItem),
new InventoryPouch4(InventoryType.KeyItems, LegalKeyItems, 1, OFS_PouchKeyItem),
new InventoryPouch4(InventoryType.TMHMs, LegalTMHMs, 1, OFS_PouchTMHM),
new InventoryPouch4(InventoryType.Medicine, LegalMedicine, 999, OFS_PouchMedicine),
new InventoryPouch4(InventoryType.Berries, LegalBerries, 999, OFS_PouchBerry),
};
foreach (var p in pouch)
p.GetPouch(Data);
return pouch;
}
set
{
foreach (var p in value)
p.SetPouch(Data);
}
}
// Storage
public override int PartyCount
{
get => Data[Party + 4];
protected set => Data[Party + 4] = (byte)value;
}
public override int GetBoxOffset(int box)
{
return Box + (SIZE_STORED * box * 30) + (box * 0x10);
}
public override int GetPartyOffset(int slot)
{
return Party + 8 + (SIZE_PARTY * slot);
}
public override string GetBoxName(int box)
{
if (box >= BoxCount)
return "";
return StringConverter.TrimFromFFFF(Encoding.Unicode.GetString(Data, PCLayout + (0x28 * box) + 4, 0x28));
}
public override void SetBoxName(int box, string value)
{
if (value.Length > 38)
return;
value += '\uFFFF';
Encoding.Unicode.GetBytes(value.PadRight(0x14, '\0')).CopyTo(Data, PCLayout + (0x28 * box) + 4);
Edited = true;
}
protected override int GetBoxWallpaperOffset(int box)
{
return PCLayout + 0x3C4 + box;
}
public override int CurrentBox
{
get => Data[PCLayout];
set => Data[PCLayout] = (byte)value;
}
public override bool BattleBoxLocked
{
get => BattleBox >= 0 && Data[BattleBox + 0x358] != 0; // wifi/live
set { }
}
public override PKM GetPKM(byte[] data)
{
return new PK5(data);
}
public override byte[] DecryptPKM(byte[] data)
{
return PKX.DecryptArray45(data);
}
protected override void SetPKM(PKM pkm)
{
var pk5 = (PK5)pkm;
// Apply to this Save File
DateTime Date = DateTime.Now;
if (pk5.Trade(OT, TID, SID, Gender, Date.Day, Date.Month, Date.Year))
pkm.RefreshChecksum();
}
// Mystery Gift
public override MysteryGiftAlbum GiftAlbum
{
get
{
uint seed = BitConverter.ToUInt32(Data, wcSeed);
MysteryGiftAlbum Info = new MysteryGiftAlbum { Seed = seed };
byte[] wcData = GetData(WondercardData, 0xA90); // Encrypted, Decrypt
for (int i = 0; i < wcData.Length; i += 2)
BitConverter.GetBytes((ushort)(BitConverter.ToUInt16(wcData, i) ^ PKX.LCRNG(ref seed) >> 16)).CopyTo(wcData, i);
Info.Flags = new bool[GiftFlagMax];
Info.Gifts = new MysteryGift[GiftCountMax];
// 0x100 Bytes for Used Flags
for (int i = 0; i < GiftFlagMax; i++)
Info.Flags[i] = (wcData[i/8] >> i%8 & 0x1) == 1;
// 12 PGFs
for (int i = 0; i < Info.Gifts.Length; i++)
Info.Gifts[i] = new PGF(wcData.Skip(0x100 + (i *PGF.Size)).Take(PGF.Size).ToArray());
return Info;
}
set
{
byte[] wcData = new byte[0xA90];
// Toss back into byte[]
for (int i = 0; i < value.Flags.Length; i++)
{
if (value.Flags[i])
wcData[i/8] |= (byte)(1 << (i & 7));
}
for (int i = 0; i < value.Gifts.Length; i++)
value.Gifts[i].Data.CopyTo(wcData, 0x100 + (i *PGF.Size));
// Decrypted, Encrypt
uint seed = value.Seed;
for (int i = 0; i < wcData.Length; i += 2)
BitConverter.GetBytes((ushort)(BitConverter.ToUInt16(wcData, i) ^ PKX.LCRNG(ref seed) >> 16)).CopyTo(wcData, i);
// Write Back
wcData.CopyTo(Data, WondercardData);
BitConverter.GetBytes(value.Seed).CopyTo(Data, wcSeed);
}
}
protected override bool[] MysteryGiftReceivedFlags { get => null; set { } }
protected override MysteryGift[] MysteryGiftCards { get => Array.Empty<MysteryGift>(); set { } }
// Trainer Info
public override string OT
{
get => GetString(Trainer1 + 0x4, 16);
set => SetString(value, OTLength).CopyTo(Data, Trainer1 + 0x4);
}
public override int TID
{
get => BitConverter.ToUInt16(Data, Trainer1 + 0x14 + 0);
set => BitConverter.GetBytes((ushort)value).CopyTo(Data, Trainer1 + 0x14 + 0);
}
public override int SID
{
get => BitConverter.ToUInt16(Data, Trainer1 + 0x14 + 2);
set => BitConverter.GetBytes((ushort)value).CopyTo(Data, Trainer1 + 0x14 + 2);
}
public override uint Money
{
get => BitConverter.ToUInt32(Data, Trainer2);
set => BitConverter.GetBytes(value).CopyTo(Data, Trainer2);
}
public override int Gender
{
get => Data[Trainer1 + 0x21];
set => Data[Trainer1 + 0x21] = (byte)value;
}
public override int Language
{
get => Data[Trainer1 + 0x1E];
set => Data[Trainer1 + 0x1E] = (byte)value;
}
public override int Game
{
get => Data[Trainer1 + 0x1F];
set => Data[Trainer1 + 0x1F] = (byte)value;
}
public int Badges
{
get => Data[Trainer2 + 0x4];
set => Data[Trainer2 + 0x4] = (byte)value;
}
public int M
{
get => BitConverter.ToInt32(Data, Trainer1 + 0x180);
set => BitConverter.GetBytes((ushort)value).CopyTo(Data, Trainer1 + 0x180);
}
public int X
{
get => BitConverter.ToUInt16(Data, Trainer1 + 0x186);
set => BitConverter.GetBytes((ushort)value).CopyTo(Data, Trainer1 + 0x186);
}
public int Z
{
get => BitConverter.ToUInt16(Data, Trainer1 + 0x18A);
set => BitConverter.GetBytes((ushort)value).CopyTo(Data, Trainer1 + 0x18A);
}
public int Y
{
get => BitConverter.ToUInt16(Data, Trainer1 + 0x18E);
set => BitConverter.GetBytes((ushort)value).CopyTo(Data, Trainer1 + 0x18E);
}
public override int PlayedHours
{
get => BitConverter.ToUInt16(Data, Trainer1 + 0x24);
set => BitConverter.GetBytes((ushort)value).CopyTo(Data, Trainer1 + 0x24);
}
public override int PlayedMinutes
{
get => Data[Trainer1 + 0x24 + 2];
set => Data[Trainer1 + 0x24 + 2] = (byte)value;
}
public override int PlayedSeconds
{
get => Data[Trainer1 + 0x24 + 3];
set => Data[Trainer1 + 0x24 + 3] = (byte)value;
}
public override int SecondsToStart { get => BitConverter.ToInt32(Data, AdventureInfo + 0x34); set => BitConverter.GetBytes(value).CopyTo(Data, AdventureInfo + 0x34); }
public override int SecondsToFame { get => BitConverter.ToInt32(Data, AdventureInfo + 0x3C); set => BitConverter.GetBytes(value).CopyTo(Data, AdventureInfo + 0x3C); }
public int BP
{
get => BitConverter.ToUInt16(Data, BattleSubway);
set => BitConverter.GetBytes((ushort)value).CopyTo(Data, BattleSubway);
}
public ushort GetPWTRecord(int id) => GetPWTRecord((PWTRecordID) id);
public ushort GetPWTRecord(PWTRecordID id)
{
if (id < PWTRecordID.Normal || id > PWTRecordID.MixMaster)
throw new ArgumentException(nameof(id));
int ofs = 0x2375C + ((int)id * 2);
return BitConverter.ToUInt16(Data, ofs);
}
public void SetPWTRecord(int id, ushort value) => SetPWTRecord((PWTRecordID) id, value);
public void SetPWTRecord(PWTRecordID id, ushort value)
{
if (id < PWTRecordID.Normal || id > PWTRecordID.MixMaster)
throw new ArgumentException(nameof(id));
int ofs = 0x2375C + ((int)id * 2);
SetData(BitConverter.GetBytes(value), ofs);
}
protected override void SetDex(PKM pkm)
{
if (pkm.Species == 0)
return;
if (pkm.Species > MaxSpeciesID)
return;
if (Version == GameVersion.Invalid)
return;
if (PokeDex < 0)
return;
const int brSize = 0x54;
int bit = pkm.Species - 1;
int gender = pkm.Gender % 2; // genderless -> male
int shiny = pkm.IsShiny ? 1 : 0;
int shift = (shiny * 2) + gender + 1;
int shiftoff = (shiny * brSize * 2) + (gender * brSize) + brSize;
int ofs = PokeDex + 0x8 + (bit >> 3);
// Set the Species Owned Flag
Data[ofs + (brSize * 0)] |= (byte)(1 << (bit % 8));
// Set the [Species/Gender/Shiny] Seen Flag
Data[PokeDex + 0x8 + shiftoff + (bit / 8)] |= (byte)(1 << (bit&7));
// Set the Display flag if none are set
bool Displayed = false;
Displayed |= (Data[ofs + (brSize * 5)] & (byte)(1 << (bit&7))) != 0;
Displayed |= (Data[ofs + (brSize * 6)] & (byte)(1 << (bit&7))) != 0;
Displayed |= (Data[ofs + (brSize * 7)] & (byte)(1 << (bit&7))) != 0;
Displayed |= (Data[ofs + (brSize * 8)] & (byte)(1 << (bit&7))) != 0;
if (!Displayed) // offset is already biased by brSize, reuse shiftoff but for the display flags.
Data[ofs + (brSize *(shift + 4))] |= (byte)(1 << (bit&7));
// Set the Language
if (bit < 493) // shifted by 1, Gen5 species do not have international language bits
{
int lang = pkm.Language - 1; if (lang > 5) lang--; // 0-6 language vals
if (lang < 0) lang = 1;
Data[PokeDexLanguageFlags + (((bit * 7) + lang)>>3)] |= (byte)(1 << (((bit * 7) + lang) & 7));
}
// Formes
int fc = Personal[pkm.Species].FormeCount;
int f = B2W2 ? SaveUtil.GetDexFormIndexB2W2(pkm.Species, fc) : SaveUtil.GetDexFormIndexBW(pkm.Species, fc);
if (f < 0) return;
int FormLen = B2W2 ? 0xB : 0x9;
int FormDex = PokeDex + 0x8 + (brSize * 9);
bit = f + pkm.AltForm;
// Set Form Seen Flag
Data[FormDex + (FormLen * shiny) + (bit>>3)] |= (byte)(1 << (bit&7));
// Set Displayed Flag if necessary, check all flags
for (int i = 0; i < fc; i++)
{
bit = f + i;
if ((Data[FormDex + (FormLen * 2) + (bit>>3)] & (byte)(1 << (bit&7))) != 0) // Nonshiny
return; // already set
if ((Data[FormDex + (FormLen * 3) + (bit>>3)] & (byte)(1 << (bit&7))) != 0) // Shiny
return; // already set
}
bit = f + pkm.AltForm;
Data[FormDex + (FormLen * (2 + shiny)) + (bit>>3)] |= (byte)(1 << (bit&7));
}
public override bool GetCaught(int species)
{
int bit = species - 1;
int bd = bit >> 3; // div8
int bm = bit & 7; // mod8
int ofs = PokeDex // Raw Offset
+ 0x08; // Magic + Flags
return (1 << bm & Data[ofs + bd]) != 0;
}
public override bool GetSeen(int species)
{
const int brSize = 0x54;
int bit = species - 1;
int bd = bit >> 3; // div8
int bm = bit & 7; // mod8
int ofs = PokeDex // Raw Offset
+ 0x08; // Magic + Flags
for (int i = 1; i <= 4; i++)
{
if ((1 << bm & Data[ofs + bd + (i * brSize)]) != 0)
return true;
}
return false;
}
public override string GetString(int Offset, int Length) => StringConverter.GetString5(Data, Offset, Length);
public override byte[] SetString(string value, int maxLength, int PadToSize = 0, ushort PadWith = 0)
{
if (PadToSize == 0)
PadToSize = maxLength + 1;
return StringConverter.SetString5(value, maxLength, PadToSize, PadWith);
}
// DLC
private int CGearSkinInfoOffset => CGearInfoOffset + (B2W2 ? 0x10 : 0) + 0x24;
private bool CGearSkinPresent
{
get => Data[CGearSkinInfoOffset + 2] == 1;
set => Data[CGearSkinInfoOffset + 2] = Data[Trainer1 + (B2W2 ? 0x6C : 0x54)] = (byte) (value ? 1 : 0);
}
public byte[] CGearSkinData
{
get
{
byte[] data = new byte[0x2600];
if (CGearSkinPresent)
Array.Copy(Data, CGearDataOffset, data, 0, data.Length);
return data;
}
set
{
if (value == null)
return; // no clearing
byte[] dlcfooter = { 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x14, 0x27, 0x00, 0x00, 0x27, 0x35, 0x05, 0x31, 0x00, 0x00 };
byte[] bgdata = value;
SetData(bgdata, CGearDataOffset);
ushort chk = SaveUtil.CRC16_CCITT(bgdata);
var chkbytes = BitConverter.GetBytes(chk);
int footer = CGearDataOffset + bgdata.Length;
BitConverter.GetBytes((ushort)1).CopyTo(Data, footer); // block updated once
chkbytes.CopyTo(Data, footer + 2); // checksum
chkbytes.CopyTo(Data, footer + 0x100); // second checksum
dlcfooter.CopyTo(Data, footer + 0x102);
ushort skinchkval = SaveUtil.CRC16_CCITT(Data, footer + 0x100, 4);
BitConverter.GetBytes(skinchkval).CopyTo(Data, footer + 0x112);
// Indicate in the save file that data is present
BitConverter.GetBytes((ushort)0xC21E).CopyTo(Data, 0x19438);
chkbytes.CopyTo(Data, CGearSkinInfoOffset);
CGearSkinPresent = true;
Edited = true;
}
}
public EntreeForest EntreeData
{
get => new EntreeForest(GetData(EntreeForestOffset, 0x850));
set => SetData(value.Write(), EntreeForestOffset);
}
}
}