using System;
using System.Linq;
namespace PKHeX.Core
{
///
/// Generation 2 object.
///
public sealed class SAV2 : SaveFile
{
protected override string BAKText => $"{OT} ({Version}) - {PlayTimeString}";
public override string Filter => "SAV File|*.sav|All Files|*.*";
public override string Extension => ".sav";
public override string[] PKMExtensions => PKM.Extensions.Where(f =>
{
int gen = f.Last() - 0x30;
if (Korean)
return gen == 2;
return 1 <= gen && gen <= 2;
}).ToArray();
public SAV2(byte[] data = null, GameVersion versionOverride = GameVersion.Any)
{
Data = data ?? new byte[SaveUtil.SIZE_G2RAW_U];
BAK = (byte[])Data.Clone();
Exportable = !IsRangeEmpty(0, Data.Length);
if (data == null)
Version = GameVersion.C;
else if (versionOverride != GameVersion.Any)
Version = versionOverride;
else
Version = SaveUtil.GetIsG2SAV(Data);
if (Version == GameVersion.Invalid)
return;
Japanese = SaveUtil.GetIsG2SAVJ(Data) != GameVersion.Invalid;
if (!Japanese)
Korean = SaveUtil.GetIsG2SAVK(Data) != GameVersion.Invalid;
Box = Data.Length;
Array.Resize(ref Data, Data.Length + SIZE_RESERVED);
Party = GetPartyOffset(0);
Personal = Version == GameVersion.GS ? PersonalTable.GS : PersonalTable.C;
Offsets = new SAV2Offsets(this);
LegalItems = Legal.Pouch_Items_GSC;
LegalBalls = Legal.Pouch_Ball_GSC;
LegalKeyItems = Version == GameVersion.C ? Legal.Pouch_Key_C : Legal.Pouch_Key_GS;
LegalTMHMs = Legal.Pouch_TMHM_GSC;
HeldItems = Legal.HeldItems_GSC;
// Stash boxes after the save file's end.
int splitAtIndex = (Japanese ? 6 : 7);
int stored = SIZE_STOREDBOX;
int baseDest = Data.Length - SIZE_RESERVED;
var capacity = Japanese ? PokeListType.StoredJP : PokeListType.Stored;
for (int i = 0; i < BoxCount; i++)
{
int ofs = GetBoxRawDataOffset(i, splitAtIndex);
var box = GetData(ofs, stored);
var boxDest = baseDest + (i * SIZE_BOX);
var boxPL = new PokeList2(box, capacity, Japanese);
for (int j = 0; j < boxPL.Pokemon.Length; j++)
{
var dest = boxDest + (j * SIZE_STORED);
var pkDat = (j < boxPL.Count)
? new PokeList2(boxPL[j]).Write()
: new byte[PokeList2.GetDataLength(PokeListType.Single, Japanese)];
pkDat.CopyTo(Data, dest);
}
}
var current = GetData(Offsets.CurrentBox, stored);
var curBoxPL = new PokeList2(current, capacity, Japanese);
var curDest = baseDest + (CurrentBox * SIZE_BOX);
for (int i = 0; i < curBoxPL.Pokemon.Length; i++)
{
var dest = curDest + (i * SIZE_STORED);
var pkDat = i < curBoxPL.Count
? new PokeList2(curBoxPL[i]).Write()
: new byte[PokeList2.GetDataLength(PokeListType.Single, Japanese)];
pkDat.CopyTo(Data, dest);
}
var party = GetData(Offsets.Party, SIZE_STOREDPARTY);
var partyPL = new PokeList2(party, PokeListType.Party, Japanese);
for (int i = 0; i < partyPL.Pokemon.Length; i++)
{
var dest = GetPartyOffset(i);
var pkDat = i < partyPL.Count
? new PokeList2(partyPL[i]).Write()
: new byte[PokeList2.GetDataLength(PokeListType.Single, Japanese)];
pkDat.CopyTo(Data, dest);
}
// Daycare currently undocumented for all Gen II games.
if (Offsets.Daycare >= 0)
{
int offset = Offsets.Daycare;
DaycareFlags[0] = Data[offset]; offset++;
var pk1 = ReadPKMFromOffset(offset); // parent 1
var daycare1 = new PokeList2(pk1);
offset += (StringLength * 2) + 0x20; // nick/ot/pkm
DaycareFlags[1] = Data[offset]; offset++;
byte steps = Data[offset]; offset++;
byte BreedMotherOrNonDitto = Data[offset]; offset++;
var pk2 = ReadPKMFromOffset(offset); // parent 2
var daycare2 = new PokeList2(pk2);
offset += (StringLength * 2) + PKX.SIZE_2STORED; // nick/ot/pkm
var pk3 = ReadPKMFromOffset(offset); // egg!
pk3.IsEgg = true;
var daycare3 = new PokeList2(pk3);
daycare1.Write().CopyTo(Data, GetPartyOffset(7 + (0 * 2)));
daycare2.Write().CopyTo(Data, GetPartyOffset(7 + (1 * 2)));
daycare3.Write().CopyTo(Data, GetPartyOffset(7 + (2 * 2)));
Daycare = Offsets.Daycare;
}
// Enable Pokedex editing
PokeDex = 0;
EventFlag = Offsets.EventFlag;
if (!Exportable)
ClearBoxes();
}
private PK2 ReadPKMFromOffset(int offset)
{
byte[] nick = new byte[StringLength];
byte[] ot = new byte[StringLength];
byte[] pk = new byte[PKX.SIZE_2STORED];
Array.Copy(Data, offset, nick, 0, nick.Length); offset += nick.Length;
Array.Copy(Data, offset, ot, 0, ot.Length); offset += ot.Length;
Array.Copy(Data, offset, pk, 0, pk.Length);
return new PK2(pk, jp: Japanese) { otname = ot, nick = nick };
}
private const int SIZE_RESERVED = 0x8000; // unpacked box data
public bool Korean { get; }
private readonly SAV2Offsets Offsets;
private int GetBoxRawDataOffset(int i, int splitAtIndex)
{
if (i < splitAtIndex)
return 0x4000 + (i * (SIZE_STOREDBOX + 2));
return 0x6000 + ((i - splitAtIndex) * (SIZE_STOREDBOX + 2));
}
protected override byte[] Write(bool DSV)
{
int len = SIZE_STOREDBOX;
int splitAtIndex = (Japanese ? 6 : 7);
for (int i = 0; i < BoxCount; i++)
{
var boxPL = new PokeList2(Japanese ? PokeListType.StoredJP : PokeListType.Stored, Japanese);
int slot = 0;
for (int j = 0; j < boxPL.Pokemon.Length; j++)
{
PK2 boxPK = (PK2) GetPKM(GetData(GetBoxOffset(i) + (j * SIZE_STORED), SIZE_STORED));
if (boxPK.Species > 0)
boxPL[slot++] = boxPK;
}
int src = GetBoxRawDataOffset(i, splitAtIndex);
boxPL.Write().CopyTo(Data, src);
if (i == CurrentBox)
boxPL.Write().CopyTo(Data, Offsets.CurrentBox);
}
var partyPL = new PokeList2(PokeListType.Party, Japanese);
int pSlot = 0;
for (int i = 0; i < 6; i++)
{
PK2 partyPK = (PK2)GetPKM(GetData(GetPartyOffset(i), SIZE_STORED));
if (partyPK.Species > 0)
partyPL[pSlot++] = partyPK;
}
partyPL.Write().CopyTo(Data, Offsets.Party);
SetChecksums();
if (Japanese)
{
switch (Version)
{
case GameVersion.GS: Array.Copy(Data, Offsets.Trainer1, Data, 0x7209, 0xC83); break;
case GameVersion.C: Array.Copy(Data, Offsets.Trainer1, Data, 0x7209, 0xADA); break;
}
}
else if (Korean)
{
// Calculate oddball checksum
ushort sum = 0;
ushort[][] offsetpairs =
{
new ushort[] {0x106B, 0x1533},
new ushort[] {0x1534, 0x1A12},
new ushort[] {0x1A13, 0x1C38},
new ushort[] {0x3DD8, 0x3F79},
new ushort[] {0x7E39, 0x7E6A},
};
foreach (ushort[] p in offsetpairs)
{
for (int i = p[0]; i < p[1]; i++)
sum += Data[i];
}
BitConverter.GetBytes(sum).CopyTo(Data, 0x7E6B);
}
else
{
switch (Version)
{
case GameVersion.GS:
Array.Copy(Data, 0x2009, Data, 0x15C7, 0x222F - 0x2009);
Array.Copy(Data, 0x222F, Data, 0x3D69, 0x23D9 - 0x222F);
Array.Copy(Data, 0x23D9, Data, 0x0C6B, 0x2856 - 0x23D9);
Array.Copy(Data, 0x2856, Data, 0x7E39, 0x288A - 0x2856);
Array.Copy(Data, 0x288A, Data, 0x10E8, 0x2D69 - 0x288A);
break;
case GameVersion.C:
Array.Copy(Data, 0x2009, Data, 0x1209, 0xB7A);
break;
}
}
byte[] outData = new byte[Data.Length - SIZE_RESERVED];
Array.Copy(Data, outData, outData.Length);
return outData;
}
// Configuration
public override SaveFile Clone() { return new SAV2(Write(DSV: false)); }
public override int SIZE_STORED => Japanese ? PKX.SIZE_2JLIST : PKX.SIZE_2ULIST;
protected override int SIZE_PARTY => Japanese ? PKX.SIZE_2JLIST : PKX.SIZE_2ULIST;
public override PKM BlankPKM => new PK2(jp: Japanese);
public override Type PKMType => typeof(PK2);
private int SIZE_BOX => BoxSlotCount*SIZE_STORED;
private int SIZE_STOREDBOX => PokeList2.GetDataLength(Japanese ? PokeListType.StoredJP : PokeListType.Stored, Japanese);
private int SIZE_STOREDPARTY => PokeList2.GetDataLength(PokeListType.Party, Japanese);
public override int MaxMoveID => Legal.MaxMoveID_2;
public override int MaxSpeciesID => Legal.MaxSpeciesID_2;
public override int MaxAbilityID => Legal.MaxAbilityID_2;
public override int MaxItemID => Legal.MaxItemID_2;
public override int MaxBallID => 0; // unused
public override int MaxGameID => 99; // unused
public override int MaxMoney => 999999;
public override int MaxCoins => 9999;
public override bool IsPKMPresent(int Offset) => PKX.IsPKMPresentGB(Data, Offset);
// not correct, but whole contains. Data[EventFlag+0x22F]=Data[0x1A2F] means repel count.
protected override int EventFlagMax => Version == GameVersion.C ? 0x230 << 3 : base.EventFlagMax;
protected override int EventConstMax => Version == GameVersion.C ? 0 : base.EventConstMax;
public override int BoxCount => Japanese ? 9 : 14;
public override int MaxEV => 65535;
public override int MaxIV => 15;
public override int Generation => 2;
protected override int GiftCountMax => 0;
public override int OTLength => Japanese || Korean ? 5 : 7;
public override int NickLength => Japanese || Korean ? 5 : 10;
public override int BoxSlotCount => Japanese ? 30 : 20;
public override bool HasParty => true;
public override bool HasNamableBoxes => true;
private int StringLength => Japanese ? PK1.STRLEN_J : PK1.STRLEN_U;
// Checksums
private ushort GetChecksum()
{
return (ushort)Data.Skip(Offsets.Trainer1).Take(Offsets.AccumulatedChecksumEnd - Offsets.Trainer1 + 1).Sum(a => a);
}
protected override void SetChecksums()
{
ushort accum = GetChecksum();
BitConverter.GetBytes(accum).CopyTo(Data, Offsets.OverallChecksumPosition);
BitConverter.GetBytes(accum).CopyTo(Data, Offsets.OverallChecksumPosition2);
}
public override bool ChecksumsValid
{
get
{
ushort accum = GetChecksum();
ushort actual = BitConverter.ToUInt16(Data, Offsets.OverallChecksumPosition);
return accum == actual;
}
}
public override string ChecksumInfo => ChecksumsValid ? "Checksum valid." : "Checksum invalid";
// Trainer Info
public override GameVersion Version { get; protected set; }
public override string OT
{
get => GetString(Offsets.Trainer1 + 2, (Korean ? 2 : 1) * OTLength);
set => SetString(value, (Korean ? 2 : 1) * OTLength).CopyTo(Data, Offsets.Trainer1 + 2);
}
public byte[] OT_Trash { get => GetData(Offsets.Trainer1 + 2, StringLength); set { if (value?.Length == StringLength) SetData(value, Offsets.Trainer1 + 2); } }
public override int Gender
{
get => Version == GameVersion.C ? Data[Offsets.Gender] : 0;
set
{
if (Version != GameVersion.C)
return;
Data[Offsets.Gender] = (byte) value;
Data[Offsets.Palette] = (byte) value;
}
}
public override int TID
{
get => BigEndian.ToUInt16(Data, Offsets.Trainer1); set => BigEndian.GetBytes((ushort)value).CopyTo(Data, Offsets.Trainer1);
}
public override int SID { get => 0; set { } }
public override int PlayedHours
{
get => BigEndian.ToUInt16(Data, Offsets.TimePlayed);
set => BigEndian.GetBytes((ushort)value).CopyTo(Data, Offsets.TimePlayed);
}
public override int PlayedMinutes
{
get => Data[Offsets.TimePlayed + 2];
set => Data[Offsets.TimePlayed + 2] = (byte)value;
}
public override int PlayedSeconds
{
get => Data[Offsets.TimePlayed + 3];
set => Data[Offsets.TimePlayed + 3] = (byte)value;
}
public int Badges
{
get => BitConverter.ToUInt16(Data, Offsets.JohtoBadges);
set { if (value < 0) return; BitConverter.GetBytes((ushort)value).CopyTo(Data, Offsets.JohtoBadges); }
}
private byte Options
{
get => Data[Offsets.Options];
set => Data[Offsets.Options] = value;
}
public bool BattleEffects
{
get => (Options & 0x80) == 0;
set => Options = (byte)((Options & 0x7F) | (value ? 0 : 0x80));
}
public bool BattleStyleSwitch
{
get => (Options & 0x40) == 0;
set => Options = (byte)((Options & 0xBF) | (value ? 0 : 0x40));
}
public int Sound
{
get => (Options & 0x30) >> 4;
set
{
var new_sound = value;
if (new_sound > 0)
new_sound = 2; // Stereo
if (new_sound < 0)
new_sound = 0; // Mono
Options = (byte)((Options & 0xCF) | (new_sound << 4));
}
}
public int TextSpeed
{
get => Options & 0x7;
set
{
var new_speed = value;
if (new_speed > 7)
new_speed = 7;
if (new_speed < 0)
new_speed = 0;
Options = (byte)((Options & 0xF8) | new_speed);
}
}
public override uint Money
{
get => BigEndian.ToUInt32(Data, Offsets.Money - 1) & 0xFFFFFF;
set
{
byte[] data = BigEndian.GetBytes((uint) Math.Min(value, MaxMoney));
Array.Copy(data, 1, Data, Offsets.Money, 3);
}
}
public uint Coin
{
get => BigEndian.ToUInt16(Data, Offsets.Money + 7);
set
{
value = (ushort)Math.Min(value, MaxCoins);
BigEndian.GetBytes((ushort)value).CopyTo(Data, Offsets.Money + 7);
}
}
private readonly ushort[] LegalItems, LegalKeyItems, LegalBalls, LegalTMHMs;
public override InventoryPouch[] Inventory
{
get
{
InventoryPouch[] pouch =
{
new InventoryPouchGB(InventoryType.TMHMs, LegalTMHMs, 99, Offsets.PouchTMHM, 57),
new InventoryPouchGB(InventoryType.Items, LegalItems, 99, Offsets.PouchItem, 20),
new InventoryPouchGB(InventoryType.KeyItems, LegalKeyItems, 99, Offsets.PouchKey, 26),
new InventoryPouchGB(InventoryType.Balls, LegalBalls, 99, Offsets.PouchBall, 12),
new InventoryPouchGB(InventoryType.PCItems, LegalItems.Concat(LegalKeyItems).Concat(LegalBalls).Concat(LegalTMHMs).ToArray(), 99, Offsets.PouchPC, 50)
};
foreach (var p in pouch)
{
p.GetPouch(Data);
}
return pouch;
}
set
{
foreach (var p in value)
{
int ofs = 0;
for (int i = 0; i < p.Count; i++)
{
while (p.Items[ofs].Count == 0)
ofs++;
p.Items[i] = p.Items[ofs++];
}
while (ofs < p.Items.Length)
p.Items[ofs++] = new InventoryItem();
p.SetPouch(Data);
}
}
}
private readonly byte[] DaycareFlags = new byte[2];
public override int GetDaycareSlotOffset(int loc, int slot) => GetPartyOffset(7 + (slot * 2));
public override uint? GetDaycareEXP(int loc, int slot) => null;
public override bool? IsDaycareOccupied(int loc, int slot) => (DaycareFlags[slot] & 1) != 0;
public override void SetDaycareEXP(int loc, int slot, uint EXP) { }
public override void SetDaycareOccupied(int loc, int slot, bool occupied) { }
// Storage
public override int PartyCount
{
get => Data[Offsets.Party]; protected set => Data[Offsets.Party] = (byte)value;
}
public override int GetBoxOffset(int box)
{
return Data.Length - SIZE_RESERVED + (box * SIZE_BOX);
}
public override int GetPartyOffset(int slot)
{
return Data.Length - SIZE_RESERVED + (BoxCount * SIZE_BOX) + (slot * SIZE_STORED);
}
public override int CurrentBox
{
get => Data[Offsets.CurrentBoxIndex] & 0x7F; set => Data[Offsets.CurrentBoxIndex] = (byte)((Data[Offsets.OtherCurrentBox] & 0x80) | (value & 0x7F));
}
public override string GetBoxName(int box)
{
int len = Korean ? 17 : 9;
return GetString(Offsets.BoxNames + (box * len), len);
}
public override void SetBoxName(int box, string value)
{
int len = Korean ? 17 : 9;
var data = SetString(value, len, len, 0x50);
SetData(data, Offsets.BoxNames + (box * len));
}
public override PKM GetPKM(byte[] data)
{
if (data.Length == SIZE_STORED)
return new PokeList2(data, PokeListType.Single, Japanese)[0];
return new PK2(data);
}
public override byte[] DecryptPKM(byte[] data)
{
return data;
}
// Pokédex
protected override void SetDex(PKM pkm)
{
int species = pkm.Species;
if (!CanSetDex(species))
return;
SetCaught(pkm.Species, true);
SetSeen(pkm.Species, true);
}
private bool CanSetDex(int species)
{
if (species <= 0)
return false;
if (species > MaxSpeciesID)
return false;
if (Version == GameVersion.Invalid)
return false;
return true;
}
public override void SetSeen(int species, bool seen)
{
int bit = species - 1;
int ofs = bit >> 3;
SetFlag(Offsets.PokedexSeen + ofs, bit & 7, seen);
}
public override void SetCaught(int species, bool caught)
{
int bit = species - 1;
int ofs = bit >> 3;
SetFlag(Offsets.PokedexCaught + ofs, bit & 7, caught);
if (caught && species == 201)
SetUnownFormFlags();
}
private void SetUnownFormFlags()
{
// Give all Unown caught to prevent a crash on pokedex view
for (int i = 1; i <= 26; i++)
Data[Offsets.PokedexSeen + 0x1F + i] = (byte)i;
if (UnownFirstSeen == 0) // Invalid
UnownFirstSeen = 1; // A
}
///
/// Toggles the availability of Unown letter groups in the Wild
///
///
/// Max value of 0x0F, 4 bitflags
/// 1 lsh 0: A, B, C, D, E, F, G, H, I, J, K
/// 1 lsh 1: L, M, N, O, P, Q, R
/// 1 lsh 2: S, T, U, V, W
/// 1 lsh 3: X, Y, Z
///
public int UnownUnlocked
{
get => Data[Offsets.PokedexSeen + 0x1F + 27];
set => Data[Offsets.PokedexSeen + 0x1F + 27] = (byte)value;
}
///
/// Unlocks all Unown letters/forms in the wild.
///
public void UnownUnlockAll() => UnownUnlocked = 0x0F; // all 4 bitflags
///
/// Flag that determines if Unown Letters are available in the wild: A, B, C, D, E, F, G, H, I, J, K
///
public bool UnownUnlocked0
{
get => (UnownUnlocked & 1 << 0) == 1 << 0;
set => UnownUnlocked |= 1 << 0;
}
///
/// Flag that determines if Unown Letters are available in the wild: L, M, N, O, P, Q, R
///
public bool UnownUnlocked1
{
get => (UnownUnlocked & 1 << 1) == 1 << 1;
set => UnownUnlocked |= 1 << 1;
}
///
/// Flag that determines if Unown Letters are available in the wild: S, T, U, V, W
///
public bool UnownUnlocked2
{
get => (UnownUnlocked & 1 << 2) == 1 << 2;
set => UnownUnlocked |= 1 << 2;
}
///
/// Flag that determines if Unown Letters are available in the wild: X, Y, Z
///
public bool UnownUnlocked3
{
get => (UnownUnlocked & 1 << 3) == 1 << 3;
set => UnownUnlocked |= 1 << 3;
}
///
/// Chooses which Unown sprite to show in the regular Pokédex View
///
public int UnownFirstSeen
{
get => Data[Offsets.PokedexSeen + 0x1F + 28];
set => Data[Offsets.PokedexSeen + 0x1F + 28] = (byte)value;
}
public override bool GetSeen(int species)
{
int bit = species - 1;
int ofs = bit >> 3;
return GetFlag(Offsets.PokedexSeen + ofs, bit & 7);
}
public override bool GetCaught(int species)
{
int bit = species - 1;
int ofs = bit >> 3;
return GetFlag(Offsets.PokedexCaught + ofs, bit & 7);
}
// Misc
public ushort ResetKey => GetResetKey();
private ushort GetResetKey()
{
var val = (TID >> 8) + (TID & 0xFF) + ((Money >> 16) & 0xFF) + ((Money >> 8) & 0xFF) + (Money & 0xFF);
var ot = Data.Skip(Offsets.Trainer1 + 2).TakeWhile((z, i) => i < 5 && z != 0x50);
var tr = ot.Sum(z => z);
return (ushort)(val + tr);
}
public void UnlockAllDecorations()
{
for (int i = 676; i <= 721; i++)
SetEventFlag(i, true);
}
public override string GetString(int Offset, int Length)
{
if (Korean)
return StringConverter.GetString2KOR(Data, Offset, Length);
return StringConverter.GetString1(Data, Offset, Length, Japanese);
}
public override byte[] SetString(string value, int maxLength, int PadToSize = 0, ushort PadWith = 0)
{
if (Korean)
return StringConverter.SetString2KOR(value, maxLength);
return StringConverter.SetString1(value, maxLength, Japanese);
}
}
}