2022-06-18 18:04:24 +00:00
|
|
|
using System;
|
2022-04-09 08:39:34 +00:00
|
|
|
using static PKHeX.Core.PokeCrypto;
|
|
|
|
using static PKHeX.Core.EntityFormatDetected;
|
2022-05-07 18:47:01 +00:00
|
|
|
using static System.Buffers.Binary.BinaryPrimitives;
|
2022-04-09 08:39:34 +00:00
|
|
|
|
|
|
|
namespace PKHeX.Core;
|
|
|
|
|
|
|
|
public static class EntityFormat
|
|
|
|
{
|
|
|
|
/// <summary>
|
2023-12-04 04:13:20 +00:00
|
|
|
/// Gets the generation of the Pokémon data.
|
2022-04-09 08:39:34 +00:00
|
|
|
/// </summary>
|
2023-12-04 04:13:20 +00:00
|
|
|
/// <param name="data">Raw data representing a Pokémon.</param>
|
2022-06-07 10:33:45 +00:00
|
|
|
/// <returns>Enum indicating the generation of the PKM file, or <see cref="None"/> if the data is invalid.</returns>
|
2022-04-09 08:39:34 +00:00
|
|
|
public static EntityFormatDetected GetFormat(ReadOnlySpan<byte> data)
|
|
|
|
{
|
2022-05-07 03:38:55 +00:00
|
|
|
if (!EntityDetection.IsSizePlausible(data.Length))
|
2022-04-09 08:39:34 +00:00
|
|
|
return None;
|
|
|
|
|
|
|
|
return GetFormatInternal(data);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static EntityFormatDetected GetFormatInternal(ReadOnlySpan<byte> data) => data.Length switch
|
|
|
|
{
|
|
|
|
SIZE_1JLIST or SIZE_1ULIST => FormatPK1,
|
|
|
|
SIZE_2JLIST or SIZE_2ULIST => FormatPK2,
|
|
|
|
SIZE_2STADIUM => FormatSK2,
|
|
|
|
SIZE_3PARTY or SIZE_3STORED => FormatPK3,
|
|
|
|
SIZE_3CSTORED => FormatCK3,
|
|
|
|
SIZE_3XSTORED => FormatXK3,
|
|
|
|
SIZE_4PARTY or SIZE_4STORED => GetFormat45(data),
|
2022-10-02 20:14:42 +00:00
|
|
|
SIZE_4RSTORED => FormatRK4,
|
2022-04-09 08:39:34 +00:00
|
|
|
SIZE_5PARTY => FormatPK5,
|
|
|
|
SIZE_6STORED => GetFormat67(data),
|
|
|
|
SIZE_6PARTY => GetFormat67_PGT(data),
|
2022-11-25 01:42:17 +00:00
|
|
|
SIZE_8PARTY or SIZE_8STORED => GetFormat89(data),
|
2022-04-09 08:39:34 +00:00
|
|
|
SIZE_8APARTY or SIZE_8ASTORED => FormatPA8,
|
|
|
|
_ => None,
|
|
|
|
};
|
|
|
|
|
|
|
|
private static EntityFormatDetected GetFormat67_PGT(ReadOnlySpan<byte> data)
|
|
|
|
{
|
|
|
|
if (!IsFormat67(data))
|
|
|
|
return None; // PGT collision, same size.
|
|
|
|
return GetFormat67(data);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static bool IsFormat67(ReadOnlySpan<byte> data)
|
|
|
|
{
|
|
|
|
if (ReadUInt16LittleEndian(data[0x04..]) != 0) // Bad Sanity?
|
2022-06-15 00:29:47 +00:00
|
|
|
return false; // PGT with non-zero ItemID
|
2022-04-09 08:39:34 +00:00
|
|
|
|
2023-12-04 04:13:20 +00:00
|
|
|
// PGT files have the last 0x10 bytes 00; PK6/etc. will have data here.
|
|
|
|
if (data[..^0x10].ContainsAnyExcept<byte>(0))
|
2023-05-07 22:29:40 +00:00
|
|
|
return true;
|
2022-06-15 00:29:47 +00:00
|
|
|
|
2023-07-15 18:22:48 +00:00
|
|
|
if (ReadUInt16LittleEndian(data[0x06..]) == Checksums.Add16(data[8..SIZE_6STORED]))
|
2022-06-15 00:29:47 +00:00
|
|
|
return true; // decrypted
|
2022-04-09 08:39:34 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// assumes decrypted state
|
|
|
|
private static EntityFormatDetected GetFormat45(ReadOnlySpan<byte> data)
|
|
|
|
{
|
2022-11-25 01:42:17 +00:00
|
|
|
if (data.Length == SIZE_4RSTORED)
|
2022-10-02 20:14:42 +00:00
|
|
|
return FormatRK4;
|
2022-04-09 08:39:34 +00:00
|
|
|
if (ReadUInt16LittleEndian(data[0x4..]) != 0)
|
|
|
|
return FormatBK4; // BK4 non-zero sanity
|
|
|
|
if (data[0x5F] < 0x10 && ReadUInt16LittleEndian(data[0x80..]) < 0x3333)
|
2023-12-04 04:13:20 +00:00
|
|
|
return FormatPK4; // Gen3/4 version origin, not Transporter
|
2022-04-09 08:39:34 +00:00
|
|
|
if (ReadUInt16LittleEndian(data[0x46..]) != 0)
|
2024-02-23 03:20:54 +00:00
|
|
|
return FormatPK4; // PK4.MetLocationExtended (unused in PK5)
|
2022-04-09 08:39:34 +00:00
|
|
|
return FormatPK5;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
2022-06-07 10:33:45 +00:00
|
|
|
/// Checks if the input <see cref="PK6"/> file is really a <see cref="PK7"/>.
|
2022-04-09 08:39:34 +00:00
|
|
|
/// </summary>
|
|
|
|
private static EntityFormatDetected GetFormat67(ReadOnlySpan<byte> data)
|
|
|
|
{
|
|
|
|
var pk = new PK6(data.ToArray());
|
|
|
|
return IsFormatReally7(pk);
|
|
|
|
}
|
|
|
|
|
2022-06-07 10:33:45 +00:00
|
|
|
/// <summary>
|
|
|
|
/// Checks if the input <see cref="PK8"/> file is really a <see cref="PB8"/>.
|
|
|
|
/// </summary>
|
2022-11-25 01:42:17 +00:00
|
|
|
private static EntityFormatDetected GetFormat89(ReadOnlySpan<byte> data)
|
2022-04-09 08:39:34 +00:00
|
|
|
{
|
2022-06-07 10:33:45 +00:00
|
|
|
var pk = new PK8(data.ToArray());
|
2022-11-25 01:42:17 +00:00
|
|
|
if (IsFormatReally9(pk))
|
|
|
|
return FormatPK9;
|
2022-06-07 10:33:45 +00:00
|
|
|
return IsFormatReally8b(pk);
|
2022-04-09 08:39:34 +00:00
|
|
|
}
|
|
|
|
|
2022-11-25 01:42:17 +00:00
|
|
|
private static bool IsFormatReally9(PK8 pk)
|
|
|
|
{
|
|
|
|
// PK8: Unused Alignment, PK9: Obedience Level
|
|
|
|
if (pk.Data[0x11F] != 0)
|
|
|
|
return true;
|
|
|
|
// PK8: Version, PK9: Unused -- Version relocated to 0xCE
|
|
|
|
return pk.Data[0xDE] == 0;
|
|
|
|
// No need to check for other usages being different.
|
|
|
|
}
|
|
|
|
|
2022-04-09 08:39:34 +00:00
|
|
|
/// <summary>
|
|
|
|
/// Creates an instance of <see cref="PKM"/> from the given data.
|
|
|
|
/// </summary>
|
2023-12-04 04:13:20 +00:00
|
|
|
/// <param name="data">Raw data of the Pokémon file.</param>
|
2022-04-09 08:39:34 +00:00
|
|
|
/// <param name="prefer">Optional identifier for the preferred generation. Usually the generation of the destination save file.</param>
|
|
|
|
/// <returns>An instance of <see cref="PKM"/> created from the given <paramref name="data"/>, or null if <paramref name="data"/> is invalid.</returns>
|
2022-06-08 06:32:57 +00:00
|
|
|
public static PKM? GetFromBytes(byte[] data, EntityContext prefer = EntityContext.None)
|
2022-04-09 08:39:34 +00:00
|
|
|
{
|
|
|
|
var format = GetFormat(data);
|
|
|
|
return GetFromBytes(data, format, prefer);
|
|
|
|
}
|
|
|
|
|
2022-06-07 10:33:45 +00:00
|
|
|
private static PKM? GetFromBytes(byte[] data, EntityFormatDetected format, EntityContext prefer) => format switch
|
2022-04-09 08:39:34 +00:00
|
|
|
{
|
|
|
|
FormatPK1 => new PokeList1(data)[0],
|
|
|
|
FormatPK2 => new PokeList2(data)[0],
|
|
|
|
FormatSK2 => new SK2(data),
|
|
|
|
FormatPK3 => new PK3(data),
|
|
|
|
FormatCK3 => new CK3(data),
|
|
|
|
FormatXK3 => new XK3(data),
|
|
|
|
FormatPK4 => new PK4(data),
|
|
|
|
FormatBK4 => new BK4(data),
|
2022-10-02 20:14:42 +00:00
|
|
|
FormatRK4 => new RK4(data),
|
2022-04-09 08:39:34 +00:00
|
|
|
FormatPK5 => new PK5(data),
|
|
|
|
FormatPK6 => new PK6(data),
|
|
|
|
FormatPK7 => new PK7(data),
|
|
|
|
FormatPB7 => new PB7(data),
|
|
|
|
FormatPK8 => new PK8(data),
|
|
|
|
FormatPA8 => new PA8(data),
|
|
|
|
FormatPB8 => new PB8(data),
|
2022-06-07 10:33:45 +00:00
|
|
|
Format6or7 => prefer == EntityContext.Gen6 ? new PK6(data) : new PK7(data),
|
|
|
|
Format8or8b => prefer == EntityContext.Gen8b ? new PB8(data) : new PK8(data),
|
2022-11-25 01:42:17 +00:00
|
|
|
FormatPK9 => new PK9(data),
|
2022-04-09 08:39:34 +00:00
|
|
|
_ => null,
|
|
|
|
};
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Checks if the input PK6 file is really a PK7.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="pk">PK6 to check</param>
|
|
|
|
/// <returns>Boolean is a PK7</returns>
|
|
|
|
private static EntityFormatDetected IsFormatReally7(PK6 pk)
|
|
|
|
{
|
|
|
|
if (pk.Version > Legal.MaxGameID_6)
|
|
|
|
{
|
2024-02-23 03:20:54 +00:00
|
|
|
if (pk.Version is (GameVersion.GP or GameVersion.GE or GameVersion.GO))
|
2022-04-09 08:39:34 +00:00
|
|
|
return FormatPB7;
|
|
|
|
return FormatPK7;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check Ranges
|
|
|
|
if (pk.Species > Legal.MaxSpeciesID_6)
|
|
|
|
return FormatPK7;
|
2022-06-26 06:08:28 +00:00
|
|
|
|
|
|
|
const int maxMove = Legal.MaxMoveID_6_AO;
|
|
|
|
if (pk.Move1 > maxMove || pk.Move2 > maxMove || pk.Move3 > maxMove || pk.Move4 > maxMove)
|
2022-04-09 08:39:34 +00:00
|
|
|
return FormatPK7;
|
2022-06-26 06:08:28 +00:00
|
|
|
if (pk.RelearnMove1 > maxMove || pk.RelearnMove2 > maxMove || pk.RelearnMove3 > maxMove || pk.RelearnMove4 > maxMove)
|
2022-04-09 08:39:34 +00:00
|
|
|
return FormatPK7;
|
|
|
|
if (pk.Ability > Legal.MaxAbilityID_6_AO)
|
|
|
|
return FormatPK7;
|
|
|
|
if (pk.HeldItem > Legal.MaxItemID_6_AO)
|
|
|
|
return FormatPK7;
|
|
|
|
|
|
|
|
// Ground Tile property is replaced with Hyper Training PK6->PK7
|
|
|
|
var et = pk.GroundTile;
|
|
|
|
if (et != 0)
|
|
|
|
{
|
|
|
|
if (pk.CurrentLevel < 100) // can't be hyper trained
|
|
|
|
return FormatPK6;
|
|
|
|
|
|
|
|
if (!pk.Gen4) // can't have GroundTile
|
|
|
|
return FormatPK7;
|
2023-12-04 04:13:20 +00:00
|
|
|
if (et > GroundTileType.Max_Pt) // invalid Gen4 GroundTile
|
2022-04-09 08:39:34 +00:00
|
|
|
return FormatPK7;
|
|
|
|
}
|
|
|
|
|
|
|
|
int mb = ReadUInt16LittleEndian(pk.Data.AsSpan(0x16));
|
|
|
|
if (mb > 0xAAA)
|
|
|
|
return FormatPK6;
|
|
|
|
for (int i = 0; i < 6; i++)
|
|
|
|
{
|
2022-06-18 18:04:24 +00:00
|
|
|
if (((mb >> (i << 1)) & 3) == 3) // markings are 10 or 01 (or 00), never 11
|
2022-04-09 08:39:34 +00:00
|
|
|
return FormatPK6;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (pk.Data[0x2A] > 20) // ResortEventStatus is always < 20
|
|
|
|
return FormatPK6;
|
|
|
|
|
|
|
|
return Format6or7;
|
|
|
|
}
|
2022-06-07 10:33:45 +00:00
|
|
|
|
|
|
|
private static EntityFormatDetected IsFormatReally8b(PK8 pk)
|
|
|
|
{
|
2022-11-25 01:42:17 +00:00
|
|
|
if (pk.Species > Legal.MaxSpeciesID_4)
|
|
|
|
return FormatPK8;
|
2022-06-07 10:33:45 +00:00
|
|
|
if (pk.DynamaxLevel != 0)
|
|
|
|
return FormatPK8;
|
|
|
|
if (pk.CanGigantamax)
|
|
|
|
return FormatPK8;
|
|
|
|
|
|
|
|
return Format8or8b;
|
|
|
|
}
|
2022-04-09 08:39:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public enum EntityFormatDetected
|
|
|
|
{
|
|
|
|
None = -1,
|
|
|
|
|
|
|
|
FormatPK1,
|
|
|
|
FormatPK2, FormatSK2,
|
|
|
|
FormatPK3, FormatCK3, FormatXK3,
|
2022-10-02 20:14:42 +00:00
|
|
|
FormatPK4, FormatBK4, FormatRK4, FormatPK5,
|
2022-04-09 08:39:34 +00:00
|
|
|
FormatPK6, FormatPK7, FormatPB7,
|
|
|
|
FormatPK8, FormatPA8, FormatPB8,
|
2022-11-25 01:42:17 +00:00
|
|
|
FormatPK9,
|
2022-04-09 08:39:34 +00:00
|
|
|
|
|
|
|
Format6or7,
|
2022-06-07 10:33:45 +00:00
|
|
|
Format8or8b,
|
2022-04-09 08:39:34 +00:00
|
|
|
}
|