mirror of
https://github.com/kwsch/PKHeX
synced 2024-12-22 18:33:14 +00:00
0b32cbf132
Co-Authored-By: Matt <17801814+sora10pls@users.noreply.github.com> Co-Authored-By: SciresM <8676005+SciresM@users.noreply.github.com> Co-Authored-By: Lusamine <30205550+Lusamine@users.noreply.github.com>
472 lines
20 KiB
C#
472 lines
20 KiB
C#
using System;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
|
|
using static System.Buffers.Binary.BinaryPrimitives;
|
|
using static PKHeX.Core.MessageStrings;
|
|
|
|
namespace PKHeX.Core
|
|
{
|
|
/// <summary>
|
|
/// Logic for converting a <see cref="PKM"/> from one generation specific format to another.
|
|
/// </summary>
|
|
public static class PKMConverter
|
|
{
|
|
public static void SetPrimaryTrainer(ITrainerInfo t)
|
|
{
|
|
Trainer = t;
|
|
if (t is IRegionOrigin o)
|
|
Trainer67 = o;
|
|
}
|
|
|
|
private static ITrainerInfo Trainer { get; set; } = new SimpleTrainerInfo();
|
|
private static IRegionOrigin Trainer67 { get; set; } = new SimpleTrainerInfo(GameVersion.SN);
|
|
public static string OT_Name => Trainer.OT;
|
|
public static int OT_Gender => Trainer.Gender;
|
|
public static int Language => Trainer.Language;
|
|
public static int Format => Trainer.Generation;
|
|
public static int Game => Trainer.Game;
|
|
public static bool AllowIncompatibleConversion { private get; set; }
|
|
|
|
public static void SetConsoleRegionData3DS(IRegionOrigin pkm)
|
|
{
|
|
var trainer = Trainer is IRegionOrigin r ? r : Trainer67;
|
|
pkm.ConsoleRegion = trainer.ConsoleRegion;
|
|
pkm.Country = trainer.Country;
|
|
pkm.Region = trainer.Region;
|
|
}
|
|
|
|
public static void SetFirstCountryRegion(IGeoTrack pkm)
|
|
{
|
|
var trainer = Trainer is IRegionOrigin r ? r : Trainer67;
|
|
pkm.Geo1_Country = trainer.Country;
|
|
pkm.Geo1_Region = trainer.Region;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the generation of the Pokemon data.
|
|
/// </summary>
|
|
/// <param name="data">Raw data representing a Pokemon.</param>
|
|
/// <returns>An integer indicating the generation of the PKM file, or -1 if the data is invalid.</returns>
|
|
public static int GetPKMDataFormat(ReadOnlySpan<byte> data)
|
|
{
|
|
if (!PKX.IsPKM(data.Length))
|
|
return -1;
|
|
|
|
switch (data.Length)
|
|
{
|
|
case PokeCrypto.SIZE_1JLIST or PokeCrypto.SIZE_1ULIST:
|
|
return 1;
|
|
case PokeCrypto.SIZE_2JLIST or PokeCrypto.SIZE_2ULIST:
|
|
case PokeCrypto.SIZE_2STADIUM:
|
|
return 2;
|
|
case PokeCrypto.SIZE_3PARTY or PokeCrypto.SIZE_3STORED:
|
|
case PokeCrypto.SIZE_3CSTORED:
|
|
case PokeCrypto.SIZE_3XSTORED:
|
|
return 3;
|
|
case PokeCrypto.SIZE_4PARTY or PokeCrypto.SIZE_4STORED:
|
|
case PokeCrypto.SIZE_5PARTY:
|
|
if ((ReadUInt16LittleEndian(data[0x4..]) == 0) && (ReadUInt16LittleEndian(data[0x80..]) >= 0x3333 || data[0x5F] >= 0x10) && ReadUInt16LittleEndian(data[0x46..]) == 0) // PK5
|
|
return 5;
|
|
return 4;
|
|
case PokeCrypto.SIZE_6STORED:
|
|
return 6;
|
|
case PokeCrypto.SIZE_6PARTY: // collision with PGT, same size.
|
|
if (ReadUInt16LittleEndian(data[0x4..]) != 0) // Bad Sanity?
|
|
return -1;
|
|
if (ReadUInt32LittleEndian(data[0x06..]) == PokeCrypto.GetCHK(data, PokeCrypto.SIZE_6STORED))
|
|
return 6;
|
|
if (ReadUInt16LittleEndian(data[0x58..]) != 0) // Encrypted?
|
|
{
|
|
for (int i = data.Length - 0x10; i < data.Length; i++) // 0x10 of 00's at the end != PK6
|
|
{
|
|
if (data[i] != 0)
|
|
return 6;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
return 6;
|
|
case PokeCrypto.SIZE_8PARTY or PokeCrypto.SIZE_8STORED:
|
|
return 8;
|
|
case PokeCrypto.SIZE_8APARTY or PokeCrypto.SIZE_8ASTORED:
|
|
return 8;
|
|
|
|
default:
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an instance of <see cref="PKM"/> from the given data.
|
|
/// </summary>
|
|
/// <param name="data">Raw data of the Pokemon file.</param>
|
|
/// <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>
|
|
public static PKM? GetPKMfromBytes(byte[] data, int prefer = 7)
|
|
{
|
|
int format = GetPKMDataFormat(data);
|
|
return format switch
|
|
{
|
|
1 => new PokeList1(data)[0],
|
|
2 => data.Length != PokeCrypto.SIZE_2STADIUM ? new PokeList2(data)[0] : new SK2(data),
|
|
3 => data.Length switch
|
|
{
|
|
PokeCrypto.SIZE_3CSTORED => new CK3(data),
|
|
PokeCrypto.SIZE_3XSTORED => new XK3(data),
|
|
_ => new PK3(data),
|
|
},
|
|
4 => ReadUInt16LittleEndian(data.AsSpan(0x04)) == 0 ? new PK4(data) : new BK4(data),
|
|
5 => new PK5(data),
|
|
6 => CheckPKMFormat7(new PK6(data), prefer),
|
|
8 => CheckPKMFormat8(data),
|
|
_ => null,
|
|
};
|
|
}
|
|
|
|
private static PKM CheckPKMFormat8(byte[] data)
|
|
{
|
|
if (data.Length is PokeCrypto.SIZE_8ASTORED or PokeCrypto.SIZE_8APARTY)
|
|
return new PA8(data);
|
|
|
|
var pk8 = new PB8(data);
|
|
var ver = pk8.Version;
|
|
if (GameVersion.BDSP.Contains(ver))
|
|
return pk8;
|
|
|
|
return new PK8(data);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the input PK6 file is really a PK7, if so, updates the object.
|
|
/// </summary>
|
|
/// <param name="pk">PKM to check</param>
|
|
/// <param name="prefer">Prefer a certain generation over another</param>
|
|
/// <returns>Updated PKM if actually PK7</returns>
|
|
private static G6PKM CheckPKMFormat7(PK6 pk, int prefer)
|
|
{
|
|
if (GameVersion.Gen7b.Contains(pk.Version))
|
|
return new PB7(pk.Data);
|
|
if (IsPK6FormatReallyPK7(pk, prefer))
|
|
return new PK7(pk.Data);
|
|
return pk;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the input PK6 file is really a PK7.
|
|
/// </summary>
|
|
/// <param name="pk">PK6 to check</param>
|
|
/// <param name="preferredFormat">Prefer a certain generation over another</param>
|
|
/// <returns>Boolean is a PK7</returns>
|
|
private static bool IsPK6FormatReallyPK7(PK6 pk, int preferredFormat)
|
|
{
|
|
if (pk.Version > Legal.MaxGameID_6)
|
|
return true;
|
|
|
|
// Check Ranges
|
|
if (pk.Species > Legal.MaxSpeciesID_6)
|
|
return true;
|
|
if (pk.Moves.Any(move => move > Legal.MaxMoveID_6_AO))
|
|
return true;
|
|
if (pk.RelearnMoves.Any(move => move > Legal.MaxMoveID_6_AO))
|
|
return true;
|
|
if (pk.Ability > Legal.MaxAbilityID_6_AO)
|
|
return true;
|
|
if (pk.HeldItem > Legal.MaxItemID_6_AO)
|
|
return true;
|
|
|
|
// 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 false;
|
|
|
|
if (!pk.Gen4) // can't have GroundTile
|
|
return true;
|
|
if (et > GroundTileType.Max_Pt) // invalid gen4 GroundTile
|
|
return true;
|
|
}
|
|
|
|
int mb = ReadUInt16LittleEndian(pk.Data.AsSpan(0x16));
|
|
if (mb > 0xAAA)
|
|
return false;
|
|
for (int i = 0; i < 6; i++)
|
|
{
|
|
if ((mb >> (i << 1) & 3) == 3) // markings are 10 or 01 (or 00), never 11
|
|
return false;
|
|
}
|
|
|
|
if (pk.Data[0x2A] > 20) // ResortEventStatus is always < 20
|
|
return false;
|
|
|
|
return preferredFormat > 6;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the input <see cref="PKM"/> file is capable of being converted to the desired format.
|
|
/// </summary>
|
|
/// <param name="pk"></param>
|
|
/// <param name="format"></param>
|
|
/// <returns></returns>
|
|
public static bool IsConvertibleToFormat(PKM pk, int format)
|
|
{
|
|
if (pk.Format >= 3 && pk.Format > format)
|
|
return false; // pk3->upward can't go backwards
|
|
if (pk.Format <= 2 && format is > 2 and < 7)
|
|
return false; // pk1/2->upward has to be 7 or greater
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts a PKM from one Generation format to another. If it matches the destination format, the conversion will automatically return.
|
|
/// </summary>
|
|
/// <param name="pk">PKM to convert</param>
|
|
/// <param name="destType">Format/Type to convert to</param>
|
|
/// <param name="comment">Comments regarding the transfer's success/failure</param>
|
|
/// <returns>Converted PKM</returns>
|
|
public static PKM? ConvertToType(PKM pk, Type destType, out string comment)
|
|
{
|
|
Type fromType = pk.GetType();
|
|
if (fromType == destType)
|
|
{
|
|
comment = "No need to convert, current format matches requested format.";
|
|
return pk;
|
|
}
|
|
|
|
var pkm = ConvertPKM(pk, destType, fromType, out comment);
|
|
if (!AllowIncompatibleConversion || pkm != null)
|
|
return pkm;
|
|
|
|
if (pk is PK8 && destType == typeof(PB8))
|
|
return new PB8(pk.Data);
|
|
if (pk is PB8 && destType == typeof(PK8))
|
|
return new PK8(pk.Data);
|
|
|
|
// Try Incompatible Conversion
|
|
pkm = GetBlank(destType);
|
|
pk.TransferPropertiesWithReflection(pkm);
|
|
if (!IsPKMCompatibleWithModifications(pkm))
|
|
return null;
|
|
comment = "Converted via reflection.";
|
|
return pkm;
|
|
}
|
|
|
|
private static PKM? ConvertPKM(PKM pk, Type destType, Type srcType, out string comment)
|
|
{
|
|
if (IsNotTransferable(pk, out comment))
|
|
return null;
|
|
|
|
string destName = destType.Name;
|
|
string srcName = srcType.Name;
|
|
Debug.WriteLine($"Trying to convert {srcName} to {destName}.");
|
|
|
|
// All types that inherit PKM have the generation specifier as the last char in their class name.
|
|
int destGeneration = destName[^1] - '0';
|
|
var pkm = ConvertPKM(pk, destType, destGeneration, ref comment);
|
|
var msg = pkm == null ? MsgPKMConvertFailFormat : MsgPKMConvertSuccess;
|
|
var formatted = string.Format(msg, srcName, destName);
|
|
comment = comment.Length != 0 ? formatted : string.Concat(formatted, Environment.NewLine, comment);
|
|
return pkm;
|
|
}
|
|
|
|
private static PKM? ConvertPKM(PKM pk, Type destType, int destGeneration, ref string comment)
|
|
{
|
|
PKM? pkm = pk.Clone();
|
|
if (pkm.IsEgg)
|
|
pkm.ForceHatchPKM();
|
|
while (true)
|
|
{
|
|
pkm = IntermediaryConvert(pkm, destType, destGeneration, ref comment);
|
|
if (pkm == null) // fail convert
|
|
return null;
|
|
if (pkm.GetType() == destType) // finish convert
|
|
return pkm;
|
|
}
|
|
}
|
|
|
|
private static PKM? IntermediaryConvert(PKM pk, Type destType, int destGeneration, ref string comment)
|
|
{
|
|
switch (pk)
|
|
{
|
|
// Non-sequential
|
|
case PK1 pk1 when destGeneration > 2: return pk1.ConvertToPK7();
|
|
case PK2 pk2 when destGeneration > 2: return pk2.ConvertToPK7();
|
|
case PK2 pk2 when destType == typeof(SK2): return pk2.ConvertToSK2();
|
|
case PK3 pk3 when destType == typeof(CK3): return pk3.ConvertToCK3();
|
|
case PK3 pk3 when destType == typeof(XK3): return pk3.ConvertToXK3();
|
|
case PK4 pk4 when destType == typeof(BK4): return pk4.ConvertToBK4();
|
|
|
|
// Invalid
|
|
case PK2 pk2 when pk.Species > Legal.MaxSpeciesID_1:
|
|
var lang = pk2.Japanese ? (int)LanguageID.Japanese : (int)LanguageID.English;
|
|
var name = SpeciesName.GetSpeciesName(pk2.Species, lang);
|
|
comment = string.Format(MsgPKMConvertFailFormat, name, destType.Name);
|
|
return null;
|
|
|
|
// Sequential
|
|
case PK1 pk1: return pk1.ConvertToPK2();
|
|
case PK2 pk2: return pk2.ConvertToPK1();
|
|
case PK3 pk3: return pk3.ConvertToPK4();
|
|
case PK4 pk4: return pk4.ConvertToPK5();
|
|
case PK5 pk5: return pk5.ConvertToPK6();
|
|
case PK6 pk6: return pk6.ConvertToPK7();
|
|
case PK7 pk7: return pk7.ConvertToPK8();
|
|
case PB7 pb7: return pb7.ConvertToPK8();
|
|
|
|
// Side-Formats back to Mainline
|
|
case SK2 sk2: return sk2.ConvertToPK2();
|
|
case CK3 ck3: return ck3.ConvertToPK3();
|
|
case XK3 xk3: return xk3.ConvertToPK3();
|
|
case BK4 bk4: return bk4.ConvertToPK4();
|
|
|
|
// None
|
|
default:
|
|
comment = MsgPKMConvertFailNoMethod;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks to see if a PKM is transferable relative to in-game restrictions and <see cref="PKM.Form"/>.
|
|
/// </summary>
|
|
/// <param name="pk">PKM to convert</param>
|
|
/// <param name="comment">Comment indicating why the <see cref="PKM"/> is not transferable.</param>
|
|
/// <returns>Indication if Not Transferable</returns>
|
|
private static bool IsNotTransferable(PKM pk, out string comment)
|
|
{
|
|
switch (pk)
|
|
{
|
|
case PK4 { Species: (int)Species.Pichu } pk4 when pk4.Form != 0:
|
|
case PK6 { Species: (int)Species.Pikachu } pk6 when pk6.Form != 0:
|
|
case PB7 { Species: (int)Species.Pikachu } pika when pika.Form != 0:
|
|
case PB7 { Species: (int)Species.Eevee } eevee when eevee.Form != 0:
|
|
comment = MsgPKMConvertFailForm;
|
|
return true;
|
|
default:
|
|
comment = string.Empty;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the <see cref="PKM"/> is compatible with the input <see cref="PKM"/>, and makes any necessary modifications to force compatibility.
|
|
/// </summary>
|
|
/// <remarks>Should only be used when forcing a backwards conversion to sanitize the PKM fields to the target format.
|
|
/// If the PKM is compatible, some properties may be forced to sanitized values.</remarks>
|
|
/// <param name="pk">PKM input that is to be sanity checked.</param>
|
|
/// <returns>Indication whether or not the PKM is compatible.</returns>
|
|
public static bool IsPKMCompatibleWithModifications(PKM pk) => IsPKMCompatibleWithModifications(pk, pk);
|
|
|
|
public static bool IsPKMCompatibleWithModifications(PKM pk, IGameValueLimit limit)
|
|
{
|
|
if (pk.Species > limit.MaxSpeciesID)
|
|
return false;
|
|
|
|
if (pk.HeldItem > limit.MaxItemID)
|
|
pk.HeldItem = 0;
|
|
|
|
if (pk.Nickname.Length > limit.NickLength)
|
|
pk.Nickname = pk.Nickname[..pk.NickLength];
|
|
|
|
if (pk.OT_Name.Length > limit.OTLength)
|
|
pk.OT_Name = pk.OT_Name[..pk.OTLength];
|
|
|
|
if (pk.Moves.Any(move => move > limit.MaxMoveID))
|
|
pk.ClearInvalidMoves();
|
|
|
|
int maxEV = pk.MaxEV;
|
|
for (int i = 0; i < 6; i++)
|
|
{
|
|
if (pk.GetEV(i) > maxEV)
|
|
pk.SetEV(i, maxEV);
|
|
}
|
|
|
|
int maxIV = pk.MaxIV;
|
|
for (int i = 0; i < 6; i++)
|
|
{
|
|
if (pk.GetIV(i) > maxIV)
|
|
pk.SetIV(i, maxIV);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the input <see cref="PKM"/> is compatible with the target <see cref="PKM"/>.
|
|
/// </summary>
|
|
/// <param name="pk">Input to check -> update/sanitize</param>
|
|
/// <param name="target">Target type PKM with misc properties accessible for checking.</param>
|
|
/// <param name="c">Comment output</param>
|
|
/// <param name="pkm">Output compatible PKM</param>
|
|
/// <returns>Indication if the input is (now) compatible with the target.</returns>
|
|
public static bool TryMakePKMCompatible(PKM pk, PKM target, out string c, out PKM pkm)
|
|
{
|
|
if (!IsConvertibleToFormat(pk, target.Format))
|
|
{
|
|
pkm = target;
|
|
c = string.Format(MsgPKMConvertFailBackwards, pk.GetType().Name, target.Format);
|
|
if (!AllowIncompatibleConversion)
|
|
return false;
|
|
}
|
|
if (IsIncompatibleGB(target, target.Japanese, pk.Japanese))
|
|
{
|
|
pkm = target;
|
|
c = GetIncompatibleGBMessage(pk, target.Japanese);
|
|
return false;
|
|
}
|
|
var convert = ConvertToType(pk, target.GetType(), out c);
|
|
if (convert == null)
|
|
{
|
|
pkm = target;
|
|
return false;
|
|
}
|
|
|
|
pkm = convert;
|
|
Debug.WriteLine(c);
|
|
return true;
|
|
}
|
|
|
|
public static string GetIncompatibleGBMessage(PKM pk, bool destJapanese)
|
|
{
|
|
var src = destJapanese ? MsgPKMConvertInternational : MsgPKMConvertJapanese;
|
|
var dest = !destJapanese ? MsgPKMConvertInternational : MsgPKMConvertJapanese;
|
|
return string.Format(MsgPKMConvertIncompatible, src, pk.GetType().Name, dest);
|
|
}
|
|
|
|
public static bool IsIncompatibleGB(PKM pk, bool destJapanese, bool srcJapanese) => pk.Format <= 2 && destJapanese != srcJapanese && !(pk is SK2 sk2 && sk2.IsPossible(srcJapanese));
|
|
|
|
/// <summary>
|
|
/// Gets a Blank <see cref="PKM"/> object of the specified type.
|
|
/// </summary>
|
|
/// <param name="type">Type of <see cref="PKM"/> instance desired.</param>
|
|
/// <returns>New instance of a blank <see cref="PKM"/> object.</returns>
|
|
public static PKM GetBlank(Type type)
|
|
{
|
|
var constructors = type.GetTypeInfo().DeclaredConstructors.Where(z => !z.IsStatic);
|
|
var argCount = constructors.Min(z => z.GetParameters().Length);
|
|
return (PKM)Activator.CreateInstance(type, new object[argCount]);
|
|
}
|
|
|
|
public static PKM GetBlank(int gen, GameVersion ver) => gen switch
|
|
{
|
|
1 when ver == GameVersion.BU => new PK1(true),
|
|
7 when GameVersion.Gen7b.Contains(ver) => new PB7(),
|
|
8 when GameVersion.BDSP.Contains(ver) => new PB8(),
|
|
8 when GameVersion.PLA == ver => new PA8(),
|
|
_ => GetBlank(gen),
|
|
};
|
|
|
|
public static PKM GetBlank(int gen, int ver) => GetBlank(gen, (GameVersion) ver);
|
|
|
|
public static PKM GetBlank(int gen)
|
|
{
|
|
var type = Type.GetType($"PKHeX.Core.PK{gen}");
|
|
if (type is null)
|
|
throw new InvalidCastException($"Unable to get the type for PK{gen}.");
|
|
|
|
return GetBlank(type);
|
|
}
|
|
}
|
|
}
|