PKHeX/PKHeX.Core/Saves/SAV5.cs
Kurt 383d4b7700 "" -> string.Empty
be explicit that the string is empty rather than possibly missing
disallow encrypted export for BK4 (they're not encrypted), removes type
check
simplify replaceall in showdownset (don't call ReplaceAll 4x, just get
valid chars and rebuild)
simplify get ribbon sprite name (precompute ToLower and appended values
2019-02-07 21:40:20 -08:00

613 lines
23 KiB
C#

using System;
using System.Collections.Generic;
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|" : string.Empty) + "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 IReadOnlyList<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("-", string.Empty);
}
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 string.Empty;
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 => Array.Empty<bool>(); 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(byte[] data, 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);
}
}
}