PKHeX/Saves/SAV3.cs
Kaphotics e362a62410 Fix gen3 save exporting
Thanks BeyondTheHorizon!
2016-07-28 08:52:04 -07:00

518 lines
20 KiB
C#

using System;
using System.Linq;
namespace PKHeX
{
public sealed class SAV3 : SaveFile
{
public override string BAKName => $"{FileName} [{OT} ({Version})" +/* - {LastSavedTime}*/ "].bak";
public override string Filter => "SAV File|*.sav";
public override string Extension => ".sav";
/* SAV3 Structure:
* 0xE000 per save file
* 14 blocks @ 0x1000 each.
* Blocks do not use all 0x1000 bytes allocated.
* Via: http://bulbapedia.bulbagarden.net/wiki/Save_data_structure_in_Generation_III
*/
private readonly int[] chunkLength =
{
0xf2c, // 0 | Trainer info
0xf80, // 1 | Team / items
0xf80, // 2 | Unknown
0xf80, // 3 | Unknown
0xf08, // 4 | Rival info
0xf80, // 5 | PC Block 0
0xf80, // 6 | PC Block 1
0xf80, // 7 | PC Block 2
0xf80, // 8 | PC Block 3
0xf80, // 9 | PC Block 4
0xf80, // A | PC Block 5
0xf80, // B | PC Block 6
0xf80, // C | PC Block 7
0x7d0 // D | PC Block 8
};
public SAV3(byte[] data = null, GameVersion versionOverride = GameVersion.Any)
{
Data = data == null ? new byte[SaveUtil.SIZE_G3RAW] : (byte[])data.Clone();
BAK = (byte[])Data.Clone();
Exportable = !Data.SequenceEqual(new byte[Data.Length]);
if (data == null)
Version = GameVersion.FRLG;
else if (versionOverride != GameVersion.Any)
Version = versionOverride;
else Version = SaveUtil.getIsG3SAV(Data);
if (Version == GameVersion.Invalid)
return;
BlockOrder = new int[14];
BlockOfs = new int[14];
ActiveSAV = SaveUtil.SIZE_G3RAWHALF == data.Length || BitConverter.ToUInt32(Data, 0xFFC) > BitConverter.ToUInt32(Data, 0xEFFC) ? 0 : 1;
for (int i = 0; i < 14; i++)
BlockOrder[i] = BitConverter.ToInt16(Data, ABO + i*0x1000 + 0xFF4);
for (int i = 0; i < 14; i++)
BlockOfs[i] = Array.IndexOf(BlockOrder, i)*0x1000 + ABO;
// Set up PC data buffer beyond end of save file.
Box = Data.Length;
Array.Resize(ref Data, Data.Length + SIZE_RESERVED); // More than enough empty space.
// Copy chunk to the allocated location
for (int i = 5; i < 14; i++)
{
int blockIndex = Array.IndexOf(BlockOrder, i);
if (blockIndex == -1) // block empty
continue;
Array.Copy(Data, blockIndex * 0x1000 + ABO, Data, Box + (i - 5)*0xF80, chunkLength[i]);
}
switch (Version)
{
case GameVersion.RS:
LegalKeyItems = Legal.Pouch_Key_RS;
OFS_PouchHeldItem = BlockOfs[1] + 0x0560;
OFS_PouchKeyItem = BlockOfs[1] + 0x05B0;
OFS_PouchBalls = BlockOfs[1] + 0x0600;
OFS_PouchTMHM = BlockOfs[1] + 0x0640;
OFS_PouchBerry = BlockOfs[1] + 0x0740;
Personal = PersonalTable.RS;
break;
case GameVersion.FRLG:
LegalKeyItems = Legal.Pouch_Key_FRLG;
OFS_PouchHeldItem = BlockOfs[1] + 0x0310;
OFS_PouchKeyItem = BlockOfs[1] + 0x03B8;
OFS_PouchBalls = BlockOfs[1] + 0x0430;
OFS_PouchTMHM = BlockOfs[1] + 0x0464;
OFS_PouchBerry = BlockOfs[1] + 0x054C;
Personal = PersonalTable.FR; // todo split FR & LG
break;
case GameVersion.E:
LegalKeyItems = Legal.Pouch_Key_E;
OFS_PouchHeldItem = BlockOfs[1] + 0x0560;
OFS_PouchKeyItem = BlockOfs[1] + 0x05D8;
OFS_PouchBalls = BlockOfs[1] + 0x0650;
OFS_PouchTMHM = BlockOfs[1] + 0x0690;
OFS_PouchBerry = BlockOfs[1] + 0x0790;
Personal = PersonalTable.E;
break;
}
LegalItems = Legal.Pouch_Items_RS;
LegalBalls = Legal.Pouch_Ball_RS;
LegalTMHMs = Legal.Pouch_TMHM_RS;
LegalBerries = Legal.Pouch_Berries_RS;
HeldItems = Legal.HeldItems_RS;
if (!Exportable)
resetBoxes();
}
private const int SIZE_RESERVED = 0x10000; // unpacked box data
public override byte[] Write(bool DSV)
{
// Copy Box data back
for (int i = 5; i < 14; i++)
{
int blockIndex = Array.IndexOf(BlockOrder, i);
if (blockIndex == -1) // block empty
continue;
Array.Copy(Data, Box + (i - 5) * 0xF80, Data, blockIndex * 0x1000 + ABO, chunkLength[i]);
}
setChecksums();
return Data.Take(Data.Length - SIZE_RESERVED).ToArray();
}
private readonly int ActiveSAV;
private int ABO => ActiveSAV*0xE000;
private readonly int[] BlockOrder;
private readonly int[] BlockOfs;
// Configuration
public override SaveFile Clone() { return new SAV3(Data.Take(Box).ToArray(), Version); }
public override int SIZE_STORED => PKX.SIZE_3STORED;
public override int SIZE_PARTY => PKX.SIZE_3PARTY;
public override PKM BlankPKM => new PK3();
protected override Type PKMType => typeof(PK3);
public override int MaxMoveID => 354;
public override int MaxSpeciesID => 386;
public override int MaxAbilityID => 77;
public override int MaxItemID => 374;
public override int MaxBallID => 0xC;
public override int MaxGameID => 5;
public override int BoxCount => 14;
public override int MaxEV => 252;
public override int Generation => 3;
protected override int GiftCountMax => 1;
public override int OTLength => 8;
public override int NickLength => 10;
public override bool HasParty => true;
// Checksums
protected override void setChecksums()
{
for (int i = 0; i < 14; i++)
{
byte[] chunk = Data.Skip(ABO + i*0x1000).Take(chunkLength[BlockOrder[i]]).ToArray();
ushort chk = SaveUtil.check32(chunk);
BitConverter.GetBytes(chk).CopyTo(Data, ABO + i + 0xFF4);
}
}
public override bool ChecksumsValid
{
get
{
for (int i = 0; i < 14; i++)
{
byte[] chunk = Data.Skip(ABO + i * 0x1000).Take(chunkLength[BlockOrder[i]]).ToArray();
ushort chk = SaveUtil.check32(chunk);
if (chk != BitConverter.ToUInt16(Data, ABO + i*0xFF4))
return false;
}
return true;
}
}
public override string ChecksumInfo
{
get
{
string r = "";
for (int i = 0; i < 14; i++)
{
byte[] chunk = Data.Skip(ABO + i * 0x1000).Take(chunkLength[BlockOrder[i]]).ToArray();
ushort chk = SaveUtil.check32(chunk);
if (chk != BitConverter.ToUInt16(Data, ABO + i * 0xFF4))
r += $"Block {BlockOrder[i]} @ {i*0x1000} (len {chunkLength[BlockOrder[i]]}) invalid." + Environment.NewLine;
}
return r.Length == 0 ? "Checksums valid." : r.TrimEnd();
}
}
// Trainer Info
public override GameVersion Version { get; protected set; }
private uint SecurityKey
{
get
{
switch (Version)
{
case GameVersion.E: return BitConverter.ToUInt32(Data, BlockOfs[0] + 0xAC);
case GameVersion.FRLG: return BitConverter.ToUInt32(Data, BlockOfs[0] + 0xAF8);
default: return 0;
}
}
}
public override string OT
{
get
{
return PKX.getG3Str(Data.Skip(BlockOfs[0]).Take(0x10).ToArray(), Japanese)
.Replace("\uE08F", "\u2640") // Nidoran ♂
.Replace("\uE08E", "\u2642") // Nidoran ♀
.Replace("\u2019", "\u0027"); // Farfetch'd
}
set
{
if (value.Length > 7)
value = value.Substring(0, 7); // Hard cap
string TempNick = value // Replace Special Characters and add Terminator
.Replace("\u2640", "\uE08F") // Nidoran ♂
.Replace("\u2642", "\uE08E") // Nidoran ♀
.Replace("\u0027", "\u2019"); // Farfetch'd
PKX.setG3Str(TempNick, Japanese).CopyTo(Data, BlockOfs[0]);
}
}
public override int Gender
{
get { return Data[BlockOfs[0] + 8]; }
set { Data[BlockOfs[0] + 8] = (byte)value; }
}
public override ushort TID
{
get { return BitConverter.ToUInt16(Data, BlockOfs[0] + 0xA + 0); }
set { BitConverter.GetBytes(value).CopyTo(Data, BlockOfs[0] + 0xA + 0); }
}
public override ushort SID
{
get { return BitConverter.ToUInt16(Data, BlockOfs[0] + 0xC); }
set { BitConverter.GetBytes(value).CopyTo(Data, BlockOfs[0] + 0xC); }
}
public override int PlayedHours
{
get { return BitConverter.ToUInt16(Data, BlockOfs[0] + 0xE); }
set { BitConverter.GetBytes((ushort)value).CopyTo(Data, BlockOfs[0] + 0xE); }
}
public override int PlayedMinutes
{
get { return Data[BlockOfs[0] + 0x10]; }
set { Data[BlockOfs[0] + 0x10] = (byte)value; }
}
public override int PlayedSeconds
{
get { return Data[BlockOfs[0] + 0x11]; }
set { Data[BlockOfs[0] + 0x11] = (byte)value; }
}
public int PlayedFrames
{
get { return Data[BlockOfs[0] + 0x12]; }
set { Data[BlockOfs[0] + 0x12] = (byte)value; }
}
public override uint Money
{
get
{
switch (Version)
{
case GameVersion.RS:
case GameVersion.E: return BitConverter.ToUInt32(Data, BlockOfs[1] + 0x0490) ^ SecurityKey;
case GameVersion.FRLG: return BitConverter.ToUInt32(Data, BlockOfs[1] + 0x0290) ^ SecurityKey;
default: return 0;
}
}
set
{
switch (Version)
{
case GameVersion.RS:
case GameVersion.E: BitConverter.GetBytes(value ^ SecurityKey).CopyTo(Data, BlockOfs[1] + 0x0490); break;
case GameVersion.FRLG: BitConverter.GetBytes(value ^ SecurityKey).CopyTo(Data, BlockOfs[1] + 0x0290); break;
}
}
}
public uint Coin
{
get
{
switch (Version)
{
case GameVersion.RS:
case GameVersion.E: return BitConverter.ToUInt32(Data, BlockOfs[1] + 0x0494) ^ SecurityKey;
case GameVersion.FRLG: return BitConverter.ToUInt32(Data, BlockOfs[1] + 0x0294) ^ SecurityKey;
default: return 0;
}
}
set
{
switch (Version)
{
case GameVersion.RS:
case GameVersion.E: BitConverter.GetBytes(value ^ SecurityKey).CopyTo(Data, BlockOfs[1] + 0x0494); break;
case GameVersion.FRLG: BitConverter.GetBytes(value ^ SecurityKey).CopyTo(Data, BlockOfs[1] + 0x0294); break;
}
}
}
public int BP
{
get { return Data[BlockOfs[0] + 0xEB8]; }
set { Data[BlockOfs[0] + 0xEB8] = (byte)value; }
}
private readonly ushort[] LegalItems, LegalKeyItems, LegalBalls, LegalTMHMs, LegalBerries;
public override InventoryPouch[] Inventory
{
get
{
InventoryPouch[] pouch =
{
new InventoryPouch(InventoryType.Items, LegalItems, 95, OFS_PouchHeldItem, (OFS_PouchKeyItem - OFS_PouchHeldItem)/4),
new InventoryPouch(InventoryType.KeyItems, LegalKeyItems, 1, OFS_PouchKeyItem, (OFS_PouchBalls - OFS_PouchKeyItem)/4),
new InventoryPouch(InventoryType.Balls, LegalBalls, 95, OFS_PouchBalls, (OFS_PouchTMHM - OFS_PouchBalls)/4),
new InventoryPouch(InventoryType.TMHMs, LegalTMHMs, 95, OFS_PouchTMHM, (OFS_PouchBerry - OFS_PouchTMHM)/4),
new InventoryPouch(InventoryType.Berries, LegalBerries, 95, OFS_PouchBerry, Version == GameVersion.FRLG ? 43 : 46),
};
foreach (var p in pouch)
{
p.SecurityKey = SecurityKey;
p.getPouch(ref Data);
}
return pouch;
}
set
{
foreach (var p in value)
p.setPouch(ref Data);
}
}
public override int getDaycareSlotOffset(int loc, int slot)
{
return Daycare + slot * SIZE_PARTY;
}
public override ulong? getDaycareRNGSeed(int loc)
{
return null;
}
public override uint? getDaycareEXP(int loc, int slot)
{
int ofs = Daycare + (slot + 1) * SIZE_PARTY - 4;
return BitConverter.ToUInt32(Data, ofs);
}
public override bool? getDaycareOccupied(int loc, int slot)
{
return null;
}
public override void setDaycareEXP(int loc, int slot, uint EXP)
{
int ofs = Daycare + (slot + 1) * SIZE_PARTY - 4;
BitConverter.GetBytes(EXP).CopyTo(Data, ofs);
}
public override void setDaycareOccupied(int loc, int slot, bool occupied)
{
}
// Storage
public override int PartyCount
{
get
{
int ofs = 0x34;
if (GameVersion.FRLG != Version)
ofs += 0x200;
return Data[BlockOfs[1] + ofs];
}
protected set
{
int ofs = 0x34;
if (GameVersion.FRLG != Version)
ofs += 0x200;
Data[BlockOfs[1] + ofs] = (byte)value;
}
}
public override int getBoxOffset(int box)
{
return Box + 4 + SIZE_STORED * box * 30;
}
public override int getPartyOffset(int slot)
{
int ofs = 0x38;
if (GameVersion.FRLG != Version)
ofs += 0x200;
return BlockOfs[1] + ofs + SIZE_PARTY * slot;
}
public override int CurrentBox
{
get { return Data[Box]; }
set { Data[Box] = (byte)value; }
}
public override int getBoxWallpaper(int box)
{
// Box Wallpaper is directly after the Box Names
int offset = getBoxOffset(BoxCount);
offset += BoxCount * 0x9 + box;
return Data[offset];
}
public override string getBoxName(int box)
{
int offset = getBoxOffset(BoxCount);
return PKX.getG3Str(Data.Skip(offset + box * 9).Take(9).ToArray(), Japanese);
}
public override void setBoxName(int box, string value)
{
if (value.Length > 8)
value = value.Substring(0, 8); // Hard cap
int offset = getBoxOffset(BoxCount);
PKX.setG3Str(value, Japanese).CopyTo(Data, offset + box * 9);
}
public override PKM getPKM(byte[] data)
{
return new PK3(data);
}
public override byte[] decryptPKM(byte[] data)
{
return PKX.decryptArray3(data);
}
protected override void setDex(PKM pkm)
{
if (pkm.Species == 0)
return;
if (pkm.Species > MaxSpeciesID)
return;
if (Version == GameVersion.Unknown)
return;
if (BlockOfs.Any(z => z < 0))
return;
int bit = pkm.Species - 1;
int ofs = bit/8;
byte bitval = (byte)(1 << (bit%8));
// Set the Captured Flag
Data[BlockOfs[0] + 0x28 + ofs] |= bitval;
// Set the Seen Flag
Data[BlockOfs[0] + 0x5C + ofs] |= bitval;
// Set the two other Seen flags (mirrored)
switch (Version)
{
case GameVersion.RS:
Data[BlockOfs[1] + 0x938 + ofs] |= bitval;
Data[BlockOfs[4] + 0xC0C + ofs] |= bitval;
break;
case GameVersion.E:
Data[BlockOfs[1] + 0x988 + ofs] |= bitval;
Data[BlockOfs[4] + 0xCA4 + ofs] |= bitval;
break;
case GameVersion.FRLG:
Data[BlockOfs[1] + 0x5F8 + ofs] |= bitval;
Data[BlockOfs[4] + 0xB98 + ofs] |= bitval;
break;
}
}
public bool NationalDex
{
get
{
if (BlockOfs.Any(z => z < 0))
return false;
switch (Version) // only check natdex status in Block0
{
case GameVersion.RS:
case GameVersion.E:
return BitConverter.ToUInt16(Data, BlockOfs[0] + 0x19) == 0xDA01;
case GameVersion.FRLG:
return Data[BlockOfs[0] + 0x1B] == 0xB9;
}
return false;
}
set
{
if (BlockOfs.Any(z => z < 0))
return;
switch (Version)
{
case GameVersion.RS:
BitConverter.GetBytes((ushort)(value ? 0xDA01 : 0)).CopyTo(Data, BlockOfs[0] + 0x19); // A
Data[BlockOfs[2] + 0x3A6] &= 0xBF;
Data[BlockOfs[2] + 0x3A6] |= (byte)(value ? 1 << 6 : 0); // B
BitConverter.GetBytes((ushort)(value ? 0x0302 : 0)).CopyTo(Data, BlockOfs[2] + 0x44C); // C
break;
case GameVersion.E:
BitConverter.GetBytes((ushort)(value ? 0xDA01 : 0)).CopyTo(Data, BlockOfs[0] + 0x19); // A
Data[BlockOfs[2] + 0x402] &= 0xBF; // Bit6
Data[BlockOfs[2] + 0x402] |= (byte)(value ? 1 << 6 : 0); // B
BitConverter.GetBytes((ushort)(value ? 0x6258 : 0)).CopyTo(Data, BlockOfs[2] + 0x4A8); // C
break;
case GameVersion.FRLG:
Data[BlockOfs[0] + 0x1B] = (byte)(value ? 0xB9 : 0); // A
Data[BlockOfs[2] + 0x68] &= 0xFE;
Data[BlockOfs[2] + 0x68] |= (byte)(value ? 1 : 0); // B
BitConverter.GetBytes((ushort)(value ? 0x6258 : 0)).CopyTo(Data, BlockOfs[2] + 0x11C); // C
break;
}
}
}
}
}