mirror of
https://github.com/kwsch/PKHeX
synced 2024-12-26 20:33:18 +00:00
e21d108fb2
All logic in PokeCrypto is separate from the rest of the PKHeX.Core library; makes it easy to just rip this portion out and reuse in other projects without needing the entirety of PKHeX.Core logic optimize out the CheckEncrypted to the actual path, separate methods. Only usages of this method were with hardcoded Format values, so no impact
1044 lines
38 KiB
C#
1044 lines
38 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace PKHeX.Core
|
|
{
|
|
/// <summary>
|
|
/// Generation 3 <see cref="SaveFile"/> object.
|
|
/// </summary>
|
|
public sealed class SAV3 : SaveFile
|
|
{
|
|
protected override string BAKText => $"{OT} ({Version}) - {PlayTimeString}";
|
|
public override string Filter => "SAV File|*.sav|All Files|*.*";
|
|
public override string Extension => ".sav";
|
|
public readonly bool Japanese;
|
|
public bool IndeterminateGame => Version == GameVersion.Unknown;
|
|
|
|
/* 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 const int SIZE_BLOCK = 0x1000;
|
|
private const int BLOCK_COUNT = 14;
|
|
private const int SIZE_RESERVED = 0x10000; // unpacked box data will start after the save data
|
|
public const int SIZE_BLOCK_USED = 0xF80;
|
|
|
|
private static readonly ushort[] chunkLength =
|
|
{
|
|
0xf2c, // 0 | Small Block (Trainer Info)
|
|
0xf80, // 1 | Large Block Part 1
|
|
0xf80, // 2 | Large Block Part 2
|
|
0xf80, // 3 | Large Block Part 3
|
|
0xf08, // 4 | Large Block Part 4
|
|
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 static void GetLargeBlockOffset(int contiguousOffset, out int chunk, out int chunkOffset)
|
|
{
|
|
for (chunk = 1; chunk <= 4; chunk++)
|
|
{
|
|
int chunkSize = chunkLength[chunk];
|
|
if (chunkSize > contiguousOffset)
|
|
break;
|
|
contiguousOffset -= chunkSize;
|
|
}
|
|
chunkOffset = contiguousOffset;
|
|
}
|
|
|
|
public static int GetLargeBlockOffset(int chunk, int chunkOffset)
|
|
{
|
|
if (chunk == 1)
|
|
return chunkOffset;
|
|
for (int i = 1; i <= 4; i++)
|
|
{
|
|
if (chunk == i)
|
|
break;
|
|
chunkOffset += chunkLength[i];
|
|
}
|
|
return chunkOffset;
|
|
}
|
|
|
|
private PersonalTable _personal;
|
|
public override PersonalTable Personal => _personal;
|
|
public override IReadOnlyList<ushort> HeldItems => Legal.HeldItems_RS;
|
|
|
|
public SAV3(GameVersion version = GameVersion.FRLG, bool japanese = false) : base(SaveUtil.SIZE_G3RAW)
|
|
{
|
|
if (version == GameVersion.FR || version == GameVersion.LG)
|
|
Version = GameVersion.FRLG;
|
|
else if (version == GameVersion.R || version == GameVersion.S)
|
|
Version = GameVersion.RS;
|
|
else
|
|
Version = version;
|
|
_personal = SaveUtil.GetG3Personal(Version) ?? PersonalTable.RS;
|
|
Japanese = japanese;
|
|
|
|
LoadBlocks(out BlockOrder, out BlockOfs);
|
|
// spoof block offsets
|
|
BlockOfs = Enumerable.Range(0, BLOCK_COUNT).ToArray();
|
|
|
|
LegalKeyItems = Version switch
|
|
{
|
|
GameVersion.RS => Legal.Pouch_Key_RS,
|
|
GameVersion.E => Legal.Pouch_Key_E,
|
|
_ => Legal.Pouch_Key_FRLG
|
|
};
|
|
PokeDex = BlockOfs[0] + 0x18;
|
|
SeenFlagOffsets = Array.Empty<int>();
|
|
|
|
Initialize();
|
|
ClearBoxes();
|
|
}
|
|
|
|
public SAV3(byte[] data, GameVersion versionOverride = GameVersion.Any) : base(data)
|
|
{
|
|
LoadBlocks(out BlockOrder, out BlockOfs);
|
|
Version = versionOverride != GameVersion.Any ? versionOverride : GetVersion(Data, BlockOfs[0]);
|
|
_personal = SaveUtil.GetG3Personal(Version) ?? PersonalTable.RS;
|
|
|
|
// Japanese games are limited to 5 character OT names; any unused characters are 0xFF.
|
|
// 5 for JP, 7 for INT. There's always 1 terminator, thus we can check 0x6-0x7 being 0xFFFF = INT
|
|
// OT name is stored at the top of the first block.
|
|
Japanese = BitConverter.ToInt16(Data, BlockOfs[0] + 0x6) == 0;
|
|
|
|
LegalKeyItems = Version switch
|
|
{
|
|
GameVersion.RS => Legal.Pouch_Key_RS,
|
|
GameVersion.E => Legal.Pouch_Key_E,
|
|
_ => Legal.Pouch_Key_FRLG
|
|
};
|
|
|
|
PokeDex = BlockOfs[0] + 0x18;
|
|
SeenFlagOffsets = Version switch
|
|
{
|
|
GameVersion.RS => new[] { PokeDex + 0x44, BlockOfs[1] + 0x938, BlockOfs[4] + 0xC0C },
|
|
GameVersion.E => new[] { PokeDex + 0x44, BlockOfs[1] + 0x988, BlockOfs[4] + 0xCA4 },
|
|
_ => new[] { PokeDex + 0x44, BlockOfs[1] + 0x5F8, BlockOfs[4] + 0xB98 }
|
|
};
|
|
|
|
Initialize();
|
|
}
|
|
|
|
private void Initialize()
|
|
{
|
|
// 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 < BLOCK_COUNT; i++)
|
|
{
|
|
int blockIndex = Array.IndexOf(BlockOrder, i);
|
|
if (blockIndex == -1) // block empty
|
|
continue;
|
|
Array.Copy(Data, (blockIndex * SIZE_BLOCK) + ABO, Data, Box + ((i - 5) * 0xF80), chunkLength[i]);
|
|
}
|
|
|
|
switch (Version)
|
|
{
|
|
case GameVersion.RS:
|
|
OFS_PCItem = BlockOfs[1] + 0x0498;
|
|
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;
|
|
EventFlag = BlockOfs[2] + 0x2A0;
|
|
EventConst = EventFlag + (EventFlagMax / 8);
|
|
DaycareOffset = BlockOfs[4] + 0x11C;
|
|
break;
|
|
case GameVersion.E:
|
|
OFS_PCItem = BlockOfs[1] + 0x0498;
|
|
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;
|
|
EventFlag = BlockOfs[2] + 0x2F0;
|
|
EventConst = EventFlag + (EventFlagMax / 8);
|
|
DaycareOffset = BlockOfs[4] + 0x1B0;
|
|
break;
|
|
case GameVersion.FRLG:
|
|
OFS_PCItem = BlockOfs[1] + 0x0298;
|
|
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;
|
|
EventFlag = BlockOfs[1] + 0xEE0;
|
|
EventConst = BlockOfs[2] + 0x80;
|
|
DaycareOffset = BlockOfs[4] + 0x100;
|
|
break;
|
|
default:
|
|
throw new ArgumentException(nameof(Version));
|
|
}
|
|
|
|
LoadEReaderBerryData();
|
|
|
|
// Sanity Check SeenFlagOffsets -- early saves may not have block 4 initialized yet
|
|
SeenFlagOffsets = SeenFlagOffsets.Where(z => z >= 0).ToArray();
|
|
}
|
|
|
|
private void LoadBlocks(out int[] blockOrder, out int[] blockOfs)
|
|
{
|
|
int[] o1 = GetBlockOrder(0);
|
|
if (Data.Length > SaveUtil.SIZE_G3RAWHALF)
|
|
{
|
|
int[] o2 = GetBlockOrder(0xE000);
|
|
ActiveSAV = GetActiveSaveIndex(o1, o2);
|
|
blockOrder = ActiveSAV == 0 ? o1 : o2;
|
|
}
|
|
else
|
|
{
|
|
ActiveSAV = 0;
|
|
blockOrder = o1;
|
|
}
|
|
|
|
blockOfs = new int[BLOCK_COUNT];
|
|
for (int i = 0; i < BLOCK_COUNT; i++)
|
|
{
|
|
int index = Array.IndexOf(blockOrder, i);
|
|
blockOfs[i] = index < 0 ? int.MinValue : (index * SIZE_BLOCK) + ABO;
|
|
}
|
|
}
|
|
|
|
private int[] GetBlockOrder(int ofs)
|
|
{
|
|
int[] order = new int[BLOCK_COUNT];
|
|
for (int i = 0; i < BLOCK_COUNT; i++)
|
|
order[i] = BitConverter.ToInt16(Data, ofs + (i * SIZE_BLOCK) + 0xFF4);
|
|
return order;
|
|
}
|
|
|
|
private int GetActiveSaveIndex(int[] BlockOrder1, int[] BlockOrder2)
|
|
{
|
|
int zeroBlock1 = Array.IndexOf(BlockOrder1, 0);
|
|
int zeroBlock2 = Array.IndexOf(BlockOrder2, 0);
|
|
if (zeroBlock2 < 0)
|
|
return 0;
|
|
if (zeroBlock1 < 0)
|
|
return 1;
|
|
var count1 = BitConverter.ToUInt32(Data, (zeroBlock1 * SIZE_BLOCK) + 0x0FFC);
|
|
var count2 = BitConverter.ToUInt32(Data, (zeroBlock2 * SIZE_BLOCK) + 0xEFFC);
|
|
return count1 > count2 ? 0 : 1;
|
|
}
|
|
|
|
public static GameVersion GetVersion(byte[] data, int block0Ofs)
|
|
{
|
|
uint GameCode = BitConverter.ToUInt32(data, block0Ofs + 0xAC);
|
|
switch (GameCode)
|
|
{
|
|
case 1: return GameVersion.FRLG; // fixed value
|
|
case 0: return GameVersion.RS; // no battle tower record data
|
|
case uint.MaxValue: return GameVersion.Unknown; // what a hack
|
|
default:
|
|
// Ruby doesn't set data as far down as Emerald.
|
|
// 00 FF 00 00 00 00 00 00 00 FF 00 00 00 00 00 00
|
|
// ^ byte pattern in Emerald saves, is all zero in Ruby/Sapphire as far as I can tell.
|
|
// Some saves have had data @ 0x550
|
|
if (BitConverter.ToUInt64(data, block0Ofs + 0xEE0) != 0)
|
|
return GameVersion.E;
|
|
if (BitConverter.ToUInt64(data, block0Ofs + 0xEE8) != 0)
|
|
return GameVersion.E;
|
|
return GameVersion.RS;
|
|
}
|
|
}
|
|
|
|
protected override byte[] GetFinalData()
|
|
{
|
|
// Copy Box data back
|
|
for (int i = 5; i < BLOCK_COUNT; i++)
|
|
{
|
|
int blockIndex = Array.IndexOf(BlockOrder, i);
|
|
if (blockIndex == -1) // block empty
|
|
continue;
|
|
Array.Copy(Data, Box + ((i - 5) * 0xF80), Data, (blockIndex * SIZE_BLOCK) + ABO, chunkLength[i]);
|
|
}
|
|
|
|
SetChecksums();
|
|
var result = new byte[Data.Length - SIZE_RESERVED];
|
|
Buffer.BlockCopy(Data, 0, result, 0, result.Length);
|
|
return result;
|
|
}
|
|
|
|
private int ActiveSAV;
|
|
private int ABO => ActiveSAV*SIZE_BLOCK*0xE;
|
|
private readonly int[] BlockOrder;
|
|
private readonly int[] BlockOfs;
|
|
public int GetBlockOffset(int block) => BlockOfs[block];
|
|
|
|
// Configuration
|
|
public override SaveFile Clone() => new SAV3(Write(), Version);
|
|
|
|
public override int SIZE_STORED => PokeCrypto.SIZE_3STORED;
|
|
protected override int SIZE_PARTY => PokeCrypto.SIZE_3PARTY;
|
|
public override PKM BlankPKM => new PK3();
|
|
public override Type PKMType => typeof(PK3);
|
|
|
|
public override int MaxMoveID => Legal.MaxMoveID_3;
|
|
public override int MaxSpeciesID => Legal.MaxSpeciesID_3;
|
|
public override int MaxAbilityID => Legal.MaxAbilityID_3;
|
|
public override int MaxItemID => Legal.MaxItemID_3;
|
|
public override int MaxBallID => Legal.MaxBallID_3;
|
|
public override int MaxGameID => Legal.MaxGameID_3;
|
|
|
|
public override int BoxCount => 14;
|
|
public override int MaxEV => 255;
|
|
public override int Generation => 3;
|
|
protected override int GiftCountMax => 1;
|
|
public override int OTLength => 7;
|
|
public override int NickLength => 10;
|
|
public override int MaxMoney => 999999;
|
|
protected override int EventFlagMax => 8 * (E ? 300 : 288); // 0x960 E, else 0x900
|
|
protected override int EventConstMax => 0x100;
|
|
|
|
public bool E => Version == GameVersion.E;
|
|
public bool FRLG => Version == GameVersion.FRLG;
|
|
public bool RS => Version == GameVersion.RS;
|
|
|
|
public override bool HasParty => true;
|
|
|
|
public override bool IsPKMPresent(byte[] data, int offset) => PKX.IsPKMPresentGBA(data, offset);
|
|
|
|
// Checksums
|
|
protected override void SetChecksums()
|
|
{
|
|
for (int i = 0; i < BLOCK_COUNT; i++)
|
|
{
|
|
int ofs = ABO + (i * SIZE_BLOCK);
|
|
var index = BlockOrder[i];
|
|
if (index == -1)
|
|
continue;
|
|
int len = chunkLength[index];
|
|
ushort chk = Checksums.CRC32(Data, ofs, len);
|
|
BitConverter.GetBytes(chk).CopyTo(Data, ofs + 0xFF6);
|
|
}
|
|
|
|
if (BAK.Length < SaveUtil.SIZE_G3RAW) // don't update HoF for half-sizes
|
|
return;
|
|
|
|
// Hall of Fame Checksums
|
|
{
|
|
ushort chk = Checksums.CRC32(Data, 0x1C000, SIZE_BLOCK_USED);
|
|
BitConverter.GetBytes(chk).CopyTo(Data, 0x1CFF4);
|
|
}
|
|
{
|
|
ushort chk = Checksums.CRC32(Data, 0x1D000, SIZE_BLOCK_USED);
|
|
BitConverter.GetBytes(chk).CopyTo(Data, 0x1DFF4);
|
|
}
|
|
}
|
|
|
|
public override bool ChecksumsValid
|
|
{
|
|
get
|
|
{
|
|
for (int i = 0; i < BLOCK_COUNT; i++)
|
|
{
|
|
if (!IsChunkValid(i))
|
|
return false;
|
|
}
|
|
|
|
if (BAK.Length < SaveUtil.SIZE_G3RAW) // don't check HoF for half-sizes
|
|
return true;
|
|
|
|
if (!IsChunkValidHoF(0x1C000))
|
|
return false;
|
|
if (!IsChunkValidHoF(0x1D000))
|
|
return false;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private bool IsChunkValidHoF(int ofs)
|
|
{
|
|
ushort chk = Checksums.CRC32(Data, ofs, SIZE_BLOCK_USED);
|
|
return chk == BitConverter.ToUInt16(Data, ofs + 0xFF4);
|
|
}
|
|
|
|
private bool IsChunkValid(int i)
|
|
{
|
|
int ofs = ABO + (i * SIZE_BLOCK);
|
|
int len = chunkLength[BlockOrder[i]];
|
|
ushort chk = Checksums.CRC32(Data, ofs, len);
|
|
return chk == BitConverter.ToUInt16(Data, ofs + 0xFF6);
|
|
}
|
|
|
|
public override string ChecksumInfo
|
|
{
|
|
get
|
|
{
|
|
var list = new List<string>();
|
|
for (int i = 0; i < BLOCK_COUNT; i++)
|
|
{
|
|
if (!IsChunkValid(i))
|
|
list.Add($"Block {BlockOrder[i]:00} @ {i*SIZE_BLOCK:X5} invalid.");
|
|
}
|
|
|
|
if (BAK.Length > SaveUtil.SIZE_G3RAW) // don't check HoF for half-sizes
|
|
{
|
|
if (!IsChunkValidHoF(0x1C000))
|
|
list.Add("HoF Block 1 invalid.");
|
|
if (!IsChunkValidHoF(0x1D000))
|
|
list.Add("HoF Block 2 invalid.");
|
|
}
|
|
return list.Count != 0 ? string.Join(Environment.NewLine, list) : "Checksums are valid.";
|
|
}
|
|
}
|
|
|
|
// Trainer Info
|
|
public override GameVersion Version { get; protected set; }
|
|
|
|
public uint SecurityKey
|
|
{
|
|
get
|
|
{
|
|
return Version switch
|
|
{
|
|
GameVersion.E => BitConverter.ToUInt32(Data, BlockOfs[0] + 0xAC),
|
|
GameVersion.FRLG => BitConverter.ToUInt32(Data, BlockOfs[0] + 0xF20),
|
|
_ => 0u
|
|
};
|
|
}
|
|
}
|
|
|
|
public override string OT
|
|
{
|
|
get => GetString(BlockOfs[0], 0x10);
|
|
set
|
|
{
|
|
int len = Japanese ? 5 : OTLength;
|
|
SetString(value, len, PadToSize: len, PadWith: 0xFF).CopyTo(Data, BlockOfs[0]);
|
|
}
|
|
}
|
|
|
|
public override int Gender
|
|
{
|
|
get => Data[BlockOfs[0] + 8];
|
|
set => Data[BlockOfs[0] + 8] = (byte)value;
|
|
}
|
|
|
|
public override int TID
|
|
{
|
|
get => BitConverter.ToUInt16(Data, BlockOfs[0] + 0xA + 0);
|
|
set => BitConverter.GetBytes((ushort)value).CopyTo(Data, BlockOfs[0] + 0xA + 0);
|
|
}
|
|
|
|
public override int SID
|
|
{
|
|
get => BitConverter.ToUInt16(Data, BlockOfs[0] + 0xC);
|
|
set => BitConverter.GetBytes((ushort)value).CopyTo(Data, BlockOfs[0] + 0xC);
|
|
}
|
|
|
|
public override int PlayedHours
|
|
{
|
|
get => BitConverter.ToUInt16(Data, BlockOfs[0] + 0xE);
|
|
set => BitConverter.GetBytes((ushort)value).CopyTo(Data, BlockOfs[0] + 0xE);
|
|
}
|
|
|
|
public override int PlayedMinutes
|
|
{
|
|
get => Data[BlockOfs[0] + 0x10];
|
|
set => Data[BlockOfs[0] + 0x10] = (byte)value;
|
|
}
|
|
|
|
public override int PlayedSeconds
|
|
{
|
|
get => Data[BlockOfs[0] + 0x11];
|
|
set => Data[BlockOfs[0] + 0x11] = (byte)value;
|
|
}
|
|
|
|
public int PlayedFrames
|
|
{
|
|
get => Data[BlockOfs[0] + 0x12];
|
|
set => Data[BlockOfs[0] + 0x12] = (byte)value;
|
|
}
|
|
|
|
public override bool GetEventFlag(int flagNumber)
|
|
{
|
|
if (flagNumber >= EventFlagMax)
|
|
throw new ArgumentException($"Event Flag to get ({flagNumber}) is greater than max ({EventFlagMax}).");
|
|
|
|
var start = EventFlag;
|
|
if (Version == GameVersion.FRLG && flagNumber >= 0x500)
|
|
{
|
|
flagNumber -= 0x500;
|
|
start = BlockOfs[2];
|
|
}
|
|
return GetFlag(start + (flagNumber >> 3), flagNumber & 7);
|
|
}
|
|
|
|
public override void SetEventFlag(int flagNumber, bool value)
|
|
{
|
|
if (flagNumber >= EventFlagMax)
|
|
throw new ArgumentException($"Event Flag to set ({flagNumber}) is greater than max ({EventFlagMax}).");
|
|
|
|
var start = EventFlag;
|
|
if (Version == GameVersion.FRLG && flagNumber >= 0x500)
|
|
{
|
|
flagNumber -= 0x500;
|
|
start = BlockOfs[2];
|
|
}
|
|
SetFlag(start + (flagNumber >> 3), flagNumber & 7, value);
|
|
}
|
|
|
|
public int Badges
|
|
{
|
|
get
|
|
{
|
|
int startFlag = BadgeFlagStart;
|
|
int val = 0;
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
if (GetEventFlag(startFlag + i))
|
|
val |= 1 << i;
|
|
}
|
|
|
|
return val;
|
|
}
|
|
set
|
|
{
|
|
int startFlag = BadgeFlagStart;
|
|
for (int i = 0; i < 8; i++)
|
|
SetEventFlag(startFlag + i, (value & (1 << i)) != 0);
|
|
}
|
|
}
|
|
|
|
private int BadgeFlagStart
|
|
{
|
|
get
|
|
{
|
|
if (Version == GameVersion.FRLG)
|
|
return 0x820;
|
|
if (Version == GameVersion.RS)
|
|
return 0x807;
|
|
return 0x867; // emerald
|
|
}
|
|
}
|
|
|
|
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 (ushort)(BitConverter.ToUInt16(Data, BlockOfs[1] + 0x0494) ^ SecurityKey);
|
|
case GameVersion.FRLG: return (ushort)(BitConverter.ToUInt16(Data, BlockOfs[1] + 0x0294) ^ SecurityKey);
|
|
default: return 0;
|
|
}
|
|
}
|
|
set
|
|
{
|
|
if (value > 9999)
|
|
value = 9999;
|
|
switch (Version)
|
|
{
|
|
case GameVersion.RS:
|
|
case GameVersion.E: BitConverter.GetBytes((ushort)(value ^ SecurityKey)).CopyTo(Data, BlockOfs[1] + 0x0494); break;
|
|
case GameVersion.FRLG: BitConverter.GetBytes((ushort)(value ^ SecurityKey)).CopyTo(Data, BlockOfs[1] + 0x0294); break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public uint BP
|
|
{
|
|
get => BitConverter.ToUInt16(Data, BlockOfs[0] + 0xEB8);
|
|
set
|
|
{
|
|
if (value > 9999)
|
|
value = 9999;
|
|
BitConverter.GetBytes((ushort)value).CopyTo(Data, BlockOfs[0] + 0xEB8);
|
|
}
|
|
}
|
|
|
|
public uint BPEarned
|
|
{
|
|
get => BitConverter.ToUInt16(Data, BlockOfs[0] + 0xEBA);
|
|
set
|
|
{
|
|
if (value > 65535)
|
|
value = 65535;
|
|
BitConverter.GetBytes((ushort)value).CopyTo(Data, BlockOfs[0] + 0xEBA);
|
|
}
|
|
}
|
|
|
|
public uint BerryPowder
|
|
{
|
|
get
|
|
{
|
|
if (Version != GameVersion.FRLG)
|
|
return 0;
|
|
return BitConverter.ToUInt32(Data, BlockOfs[0] + 0xAF8) ^ SecurityKey;
|
|
}
|
|
set
|
|
{
|
|
if (Version != GameVersion.FRLG)
|
|
return;
|
|
SetData(BitConverter.GetBytes(value ^ SecurityKey), BlockOfs[0] + 0xAF8);
|
|
}
|
|
}
|
|
|
|
private readonly ushort[] LegalKeyItems;
|
|
private static ushort[] LegalItems => Legal.Pouch_Items_RS;
|
|
private static ushort[] LegalBalls => Legal.Pouch_Ball_RS;
|
|
private static ushort[] LegalTMHMs => Legal.Pouch_TMHM_RS;
|
|
private static ushort[] LegalBerries => Legal.Pouch_Berries_RS;
|
|
|
|
private int OFS_PCItem, OFS_PouchHeldItem, OFS_PouchKeyItem, OFS_PouchBalls, OFS_PouchTMHM, OFS_PouchBerry;
|
|
|
|
public override InventoryPouch[] Inventory
|
|
{
|
|
get
|
|
{
|
|
int max = Version == GameVersion.FRLG ? 999 : 99;
|
|
var PCItems = new [] {LegalItems, LegalKeyItems, LegalKeyItems, LegalBalls, LegalTMHMs, LegalBerries}.SelectMany(a => a).ToArray();
|
|
InventoryPouch[] pouch =
|
|
{
|
|
new InventoryPouch3(InventoryType.Items, LegalItems, max, OFS_PouchHeldItem, (OFS_PouchKeyItem - OFS_PouchHeldItem)/4),
|
|
new InventoryPouch3(InventoryType.KeyItems, LegalKeyItems, 1, OFS_PouchKeyItem, (OFS_PouchBalls - OFS_PouchKeyItem)/4),
|
|
new InventoryPouch3(InventoryType.Balls, LegalBalls, max, OFS_PouchBalls, (OFS_PouchTMHM - OFS_PouchBalls)/4),
|
|
new InventoryPouch3(InventoryType.TMHMs, LegalTMHMs, max, OFS_PouchTMHM, (OFS_PouchBerry - OFS_PouchTMHM)/4),
|
|
new InventoryPouch3(InventoryType.Berries, LegalBerries, 999, OFS_PouchBerry, Version == GameVersion.FRLG ? 43 : 46),
|
|
new InventoryPouch3(InventoryType.PCItems, PCItems, 999, OFS_PCItem, (OFS_PouchHeldItem - OFS_PCItem)/4),
|
|
};
|
|
foreach (var p in pouch)
|
|
{
|
|
if (p.Type != InventoryType.PCItems)
|
|
((InventoryPouch3)p).SecurityKey = SecurityKey;
|
|
}
|
|
return pouch.LoadAll(Data);
|
|
}
|
|
set => value.SaveAll(Data);
|
|
}
|
|
|
|
private int DaycareSlotSize => RS ? SIZE_STORED : SIZE_STORED + 0x3C; // 0x38 mail + 4 exp
|
|
public override int DaycareSeedSize => E ? 8 : 4; // 32bit, 16bit
|
|
public override uint? GetDaycareEXP(int loc, int slot) => BitConverter.ToUInt32(Data, GetDaycareEXPOffset(slot));
|
|
public override void SetDaycareEXP(int loc, int slot, uint EXP) => BitConverter.GetBytes(EXP).CopyTo(Data, GetDaycareEXPOffset(slot));
|
|
public override bool? IsDaycareOccupied(int loc, int slot) => IsPKMPresent(Data, GetDaycareSlotOffset(loc, slot));
|
|
public override void SetDaycareOccupied(int loc, int slot, bool occupied) { /* todo */ }
|
|
public override int GetDaycareSlotOffset(int loc, int slot) => DaycareOffset + (slot * DaycareSlotSize);
|
|
|
|
private int EggEventFlag => GameVersion.FRLG.Contains(Version) ? 0x266 : 0x86;
|
|
public override bool? IsDaycareHasEgg(int loc) => GetEventFlag(EggEventFlag);
|
|
public override void SetDaycareHasEgg(int loc, bool hasEgg) => SetEventFlag(EggEventFlag, hasEgg);
|
|
|
|
private int GetDaycareEXPOffset(int slot)
|
|
{
|
|
if (Version == GameVersion.RS)
|
|
return GetDaycareSlotOffset(0, 2) + (2 * 0x38) + (4 * slot); // consecutive vals, after both consecutive slots & 2 mail
|
|
return GetDaycareSlotOffset(0, slot + 1) - 4; // @ end of each pkm slot
|
|
}
|
|
|
|
public override string GetDaycareRNGSeed(int loc)
|
|
{
|
|
if (Version == GameVersion.E)
|
|
return BitConverter.ToUInt32(Data, GetDaycareSlotOffset(0, 2)).ToString("X8"); // after the 2 slots, before the step counter
|
|
return BitConverter.ToUInt16(Data, GetDaycareEXPOffset(2)).ToString("X4"); // after the 2nd slot EXP, before the step counter
|
|
}
|
|
|
|
public override void SetDaycareRNGSeed(int loc, string seed)
|
|
{
|
|
if (Version == GameVersion.E) // egg pid
|
|
{
|
|
var val = Util.GetHexValue(seed);
|
|
BitConverter.GetBytes(val).CopyTo(Data, GetDaycareSlotOffset(0, 2));
|
|
}
|
|
// egg pid half
|
|
{
|
|
var val = (ushort)Util.GetHexValue(seed);
|
|
BitConverter.GetBytes(val).CopyTo(Data, GetDaycareEXPOffset(2));
|
|
}
|
|
}
|
|
|
|
// 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 => Data[Box];
|
|
set => Data[Box] = (byte)value;
|
|
}
|
|
|
|
protected override int GetBoxWallpaperOffset(int box)
|
|
{
|
|
int offset = GetBoxOffset(BoxCount);
|
|
offset += (BoxCount * 0x9) + box;
|
|
return offset;
|
|
}
|
|
|
|
public override string GetBoxName(int box)
|
|
{
|
|
int offset = GetBoxOffset(BoxCount);
|
|
return StringConverter3.GetString3(Data, offset + (box * 9), 9, Japanese);
|
|
}
|
|
|
|
public override void SetBoxName(int box, string value)
|
|
{
|
|
int offset = GetBoxOffset(BoxCount);
|
|
SetString(value, 8).CopyTo(Data, offset + (box * 9));
|
|
}
|
|
|
|
protected override PKM GetPKM(byte[] data)
|
|
{
|
|
return new PK3(data);
|
|
}
|
|
|
|
protected override byte[] DecryptPKM(byte[] data)
|
|
{
|
|
return PokeCrypto.DecryptArray3(data);
|
|
}
|
|
|
|
// Pokédex
|
|
private int[] SeenFlagOffsets;
|
|
|
|
protected override void SetDex(PKM pkm)
|
|
{
|
|
int species = pkm.Species;
|
|
if (!CanSetDex(species))
|
|
return;
|
|
|
|
switch (species)
|
|
{
|
|
case (int)Species.Unown when !GetSeen(species): // Unown
|
|
DexPIDUnown = pkm.PID;
|
|
break;
|
|
case (int)Species.Spinda when !GetSeen(species): // Spinda
|
|
DexPIDSpinda = pkm.PID;
|
|
break;
|
|
}
|
|
SetCaught(species, true);
|
|
SetSeen(species, true);
|
|
}
|
|
|
|
private bool CanSetDex(int species)
|
|
{
|
|
if (species <= 0)
|
|
return false;
|
|
if (species > MaxSpeciesID)
|
|
return false;
|
|
if (Version == GameVersion.Invalid)
|
|
return false;
|
|
if (BlockOfs.Any(z => z < 0))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
public uint DexPIDUnown { get => BitConverter.ToUInt32(Data, PokeDex + 0x4); set => BitConverter.GetBytes(value).CopyTo(Data, PokeDex + 0x4); }
|
|
public uint DexPIDSpinda { get => BitConverter.ToUInt32(Data, PokeDex + 0x8); set => BitConverter.GetBytes(value).CopyTo(Data, PokeDex + 0x8); }
|
|
public int DexUnownForm => PKX.GetUnownForm(DexPIDUnown);
|
|
|
|
public override bool GetCaught(int species)
|
|
{
|
|
int bit = species - 1;
|
|
int ofs = bit >> 3;
|
|
int caughtOffset = PokeDex + 0x10;
|
|
return GetFlag(caughtOffset + ofs, bit & 7);
|
|
}
|
|
|
|
public override void SetCaught(int species, bool caught)
|
|
{
|
|
int bit = species - 1;
|
|
int ofs = bit >> 3;
|
|
int caughtOffset = PokeDex + 0x10;
|
|
SetFlag(caughtOffset + ofs, bit & 7, caught);
|
|
}
|
|
|
|
public override bool GetSeen(int species)
|
|
{
|
|
int bit = species - 1;
|
|
int ofs = bit >> 3;
|
|
int seenOffset = PokeDex + 0x44;
|
|
return GetFlag(seenOffset + ofs, bit & 7);
|
|
}
|
|
|
|
public override void SetSeen(int species, bool seen)
|
|
{
|
|
int bit = species - 1;
|
|
int ofs = bit >> 3;
|
|
|
|
foreach (int o in SeenFlagOffsets)
|
|
SetFlag(o + ofs, bit & 7, seen);
|
|
}
|
|
|
|
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 Data[PokeDex + 2] == 0xDA; // enable nat dex option magic value
|
|
case GameVersion.FRLG:
|
|
return Data[PokeDex + 3] == 0xB9;
|
|
}
|
|
return false;
|
|
}
|
|
set
|
|
{
|
|
if (BlockOfs.Any(z => z < 0))
|
|
return;
|
|
switch (Version)
|
|
{
|
|
case GameVersion.RS:
|
|
Data[PokeDex + 1] = (byte)(value ? 1 : 0); // mode
|
|
Data[PokeDex + 2] = (byte)(value ? 0xDA : 0); // magic
|
|
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:
|
|
Data[PokeDex + 1] = (byte)(value ? 1 : 0); // mode
|
|
Data[PokeDex + 2] = (byte)(value ? 0xDA : 0); // magic
|
|
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[PokeDex + 2] = (byte)(value ? 0xDA : 0); // magic
|
|
Data[PokeDex + 3] = (byte)(value ? 0xB9 : 0); // magic
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
public override string GetString(byte[] data, int offset, int length) => StringConverter3.GetString3(data, offset, length, Japanese);
|
|
|
|
public override byte[] SetString(string value, int maxLength, int PadToSize = 0, ushort PadWith = 0)
|
|
{
|
|
if (PadToSize == 0)
|
|
PadToSize = maxLength + 1;
|
|
return StringConverter3.SetString3(value, maxLength, Japanese, PadToSize, PadWith);
|
|
}
|
|
|
|
#region eBerry
|
|
// Offset and checksum code based from
|
|
// https://github.com/suloku/wc-tool by Suloku
|
|
private const int SIZE_EBERRY = 0x530;
|
|
private const int OFFSET_EBERRY = 0x2E0;
|
|
|
|
private uint EBerryChecksum => BitConverter.ToUInt32(Data, BlockOfs[4] + OFFSET_EBERRY + SIZE_EBERRY - 4);
|
|
private bool IsEBerryChecksumValid { get; set; }
|
|
|
|
public string EBerryName
|
|
{
|
|
get
|
|
{
|
|
if (!GameVersion.RS.Contains(Version) || !IsEBerryChecksumValid)
|
|
return string.Empty;
|
|
return StringConverter3.GetString3(Data, BlockOfs[4] + OFFSET_EBERRY, 7, Japanese).Trim();
|
|
}
|
|
}
|
|
|
|
public bool IsEBerryIsEnigma => string.IsNullOrEmpty(EBerryName.Trim());
|
|
|
|
private void LoadEReaderBerryData()
|
|
{
|
|
if (!GameVersion.RS.Contains(Version))
|
|
return;
|
|
|
|
byte[] data = GetData(BlockOfs[4] + OFFSET_EBERRY, SIZE_EBERRY - 4);
|
|
|
|
// 8 bytes are 0x00 for chk calculation
|
|
for (int i = 0; i < 8; i++)
|
|
data[0xC + i] = 0x00;
|
|
uint chk = (uint)data.Sum(z => z);
|
|
IsEBerryChecksumValid = EBerryChecksum == chk;
|
|
}
|
|
#endregion
|
|
|
|
public RTC3 ClockInitial
|
|
{
|
|
get
|
|
{
|
|
if (FRLG)
|
|
throw new ArgumentException(nameof(ClockInitial));
|
|
int block0 = GetBlockOffset(0);
|
|
return new RTC3(GetData(block0 + 0x98, RTC3.Size));
|
|
}
|
|
set
|
|
{
|
|
if (FRLG)
|
|
return;
|
|
int block0 = GetBlockOffset(0);
|
|
SetData(value.Data, block0 + 0x98);
|
|
}
|
|
}
|
|
|
|
public RTC3 ClockElapsed
|
|
{
|
|
get
|
|
{
|
|
if (FRLG)
|
|
throw new ArgumentException(nameof(ClockElapsed));
|
|
int block0 = GetBlockOffset(0);
|
|
return new RTC3(GetData(block0 + 0xA0, RTC3.Size));
|
|
}
|
|
set
|
|
{
|
|
if (FRLG)
|
|
return;
|
|
int block0 = GetBlockOffset(0);
|
|
SetData(value.Data, block0 + 0xA0);
|
|
}
|
|
}
|
|
|
|
public PokeBlock3Case PokeBlocks
|
|
{
|
|
get
|
|
{
|
|
var ofs = PokeBlockOffset;
|
|
if (ofs < 0)
|
|
throw new ArgumentException($"Game does not support {nameof(PokeBlocks)}.");
|
|
return new PokeBlock3Case(Data, ofs);
|
|
}
|
|
set => SetData(value.Write(), PokeBlockOffset);
|
|
}
|
|
|
|
private int PokeBlockOffset
|
|
{
|
|
get
|
|
{
|
|
if (Version == GameVersion.E)
|
|
return BlockOfs[1] + 0x848;
|
|
if (Version == GameVersion.RS)
|
|
return BlockOfs[1] + 0x7F8;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
public int GetMailOffset(int index)
|
|
{
|
|
GetMailBlockOffset(Version, ref index, out int block, out int offset);
|
|
return (index * Mail3.SIZE) + GetBlockOffset(block) + offset;
|
|
}
|
|
|
|
private static void GetMailBlockOffset(GameVersion game, ref int index, out int block, out int offset)
|
|
{
|
|
block = 3;
|
|
if (game == GameVersion.E)
|
|
{
|
|
offset = 0xCE0;
|
|
}
|
|
else if (GameVersion.RS.Contains(game))
|
|
{
|
|
offset = 0xC4C;
|
|
}
|
|
else // FRLG
|
|
{
|
|
if (index >= 12)
|
|
{
|
|
block = 4;
|
|
offset = 0;
|
|
index -= 12;
|
|
}
|
|
else
|
|
{
|
|
offset = 0xDD0;
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool HasReceivedWishmkrJirachi
|
|
{
|
|
get => GameVersion.RS.Contains(Version) && GetFlag(BlockOfs[4] + 0x2B1, 0);
|
|
set
|
|
{
|
|
if (GameVersion.RS.Contains(Version))
|
|
SetFlag(BlockOfs[4] + 0x2B1, 0, value);
|
|
}
|
|
}
|
|
|
|
public bool ResetPersonal(GameVersion g)
|
|
{
|
|
var pt = SaveUtil.GetG3Personal(g);
|
|
if (pt == null)
|
|
return false;
|
|
_personal = pt;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// RTC
|
|
public sealed class RTC3
|
|
{
|
|
public readonly byte[] Data;
|
|
public const int Size = 8;
|
|
|
|
public RTC3(byte[] data) => Data = data;
|
|
|
|
public int Day { get => BitConverter.ToUInt16(Data, 0x00); set => BitConverter.GetBytes((ushort)value).CopyTo(Data, 0x00); }
|
|
public int Hour { get => Data[2]; set => Data[2] = (byte)value; }
|
|
public int Minute { get => Data[3]; set => Data[3] = (byte)value; }
|
|
public int Second { get => Data[4]; set => Data[4] = (byte)value; }
|
|
}
|
|
}
|