PKHeX/PKHeX.Core/PKM/PKMConverter.cs
Kurt 118a213b6c Fix backwards conversion copy order
reflection (now only used for backwards conversion) will use destination
order instead of source order so that the destination can order itself
for quirks

redo method call (probably breaks someone if they update, maybe pk2pk)
2018-02-03 15:32:45 -08:00

459 lines
19 KiB
C#

using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
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 int Country { get; private set; } = 49;
public static int Region { get; private set; } = 7;
public static int ConsoleRegion { get; private set; } = 1;
public static string OT_Name { get; private set; } = "PKHeX";
public static int OT_Gender { get; private set; } // Male
public static int Language { get; private set; } = 1; // en
public static bool AllowIncompatibleConversion { private get; set; }
public static void UpdateConfig(int SUBREGION, int COUNTRY, int _3DSREGION, string TRAINERNAME, int TRAINERGENDER, int LANGUAGE)
{
Region = SUBREGION;
Country = COUNTRY;
ConsoleRegion = _3DSREGION;
OT_Name = TRAINERNAME;
OT_Gender = TRAINERGENDER;
Language = LANGUAGE;
}
/// <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(byte[] data)
{
if (!PKX.IsPKM(data.Length))
return -1;
switch (data.Length)
{
case PKX.SIZE_1JLIST:
case PKX.SIZE_1ULIST:
return 1;
case PKX.SIZE_2ULIST:
case PKX.SIZE_2JLIST:
return 2;
case PKX.SIZE_3PARTY:
case PKX.SIZE_3STORED:
case PKX.SIZE_3CSTORED:
case PKX.SIZE_3XSTORED:
return 3;
case PKX.SIZE_4PARTY:
case PKX.SIZE_4STORED:
case PKX.SIZE_5PARTY:
if ((BitConverter.ToUInt16(data, 0x4) == 0) && (BitConverter.ToUInt16(data, 0x80) >= 0x3333 || data[0x5F] >= 0x10) && BitConverter.ToUInt16(data, 0x46) == 0) // PK5
return 5;
return 4;
case PKX.SIZE_6STORED:
return 6;
case PKX.SIZE_6PARTY: // collision with PGT, same size.
if (BitConverter.ToUInt16(data, 0x4) != 0) // Bad Sanity?
return -1;
if (BitConverter.ToUInt32(data, 0x06) == PKX.GetCHK(data))
return 6;
if (BitConverter.ToUInt16(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;
}
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="ident">Optional identifier for the Pokemon. Usually the full path of the source 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, string ident = null, int prefer = 7)
{
CheckEncrypted(ref data);
switch (GetPKMDataFormat(data))
{
case 1:
var PL1 = new PokemonList1(data, PokemonList1.CapacityType.Single, data.Length == PKX.SIZE_1JLIST);
if (ident != null)
PL1[0].Identifier = ident;
return PL1[0];
case 2:
var PL2 = new PokemonList2(data, PokemonList2.CapacityType.Single, data.Length == PKX.SIZE_2JLIST);
if (ident != null)
PL2[0].Identifier = ident;
return PL2[0];
case 3:
switch (data.Length) {
case PKX.SIZE_3CSTORED: return new CK3(data, ident);
case PKX.SIZE_3XSTORED: return new XK3(data, ident);
default: return new PK3(data, ident);
}
case 4:
var pk = new PK4(data, ident);
if (!pk.Valid || pk.Sanity != 0)
{
var bk = new BK4(data, ident);
if (bk.Valid)
return bk;
}
return pk;
case 5:
return new PK5(data, ident);
case 6:
var pkx = new PK6(data, ident);
return CheckPKMFormat7(pkx, prefer);
default:
return null;
}
}
/// <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 PKM CheckPKMFormat7(PK6 pk, int prefer) => IsPK6FormatReallyPK7(pk, prefer) ? new PK7(pk.Data, pk.Identifier) : (PKM)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;
if (pk.Enjoyment != 0 || pk.Fullness != 0)
return false;
// 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;
int et = pk.EncounterType;
if (et != 0)
{
if (pk.CurrentLevel < 100) // can't be hyper trained
return false;
if (pk.GenNumber != 4) // can't have encounter type
return true;
if (et > 24) // invalid encountertype
return true;
}
int mb = BitConverter.ToUInt16(pk.Data, 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;
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 > 2 && format < 7)
return false; // pk1/2->upward has to be 7 or greater
return true;
}
/// <summary>
/// Converts a PKM from one Generation 3 format to another. If it matches the destination format, the conversion will automatically return.
/// </summary>
/// <param name="pk">PKM to convert</param>
/// <param name="PKMType">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 PKMType, out string comment)
{
if (pk == null)
{
comment = $"Bad {nameof(pk)} input. Aborting.";
return null;
}
Type fromType = pk.GetType();
if (fromType == PKMType)
{
comment = "No need to convert, current format matches requested format.";
return pk;
}
var pkm = ConvertPKM(pk, PKMType, fromType, out comment);
if (!AllowIncompatibleConversion || pkm != null)
return pkm;
// Try Incompatible Conversion
pkm = GetBlank(PKMType);
pk.TransferPropertiesWithReflection(pkm);
if (!SaveUtil.IsPKMCompatibleWithModifications(pkm))
return null;
comment = "Converted via reflection.";
return pkm;
}
private static PKM ConvertPKM(PKM pk, Type PKMType, Type fromType, out string comment)
{
if (IsNotTransferrable(pk, out comment))
return null;
Debug.WriteLine($"Trying to convert {fromType.Name} to {PKMType.Name}.");
int fromFormat = int.Parse(fromType.Name.Last().ToString());
int toFormat = int.Parse(PKMType.Name.Last().ToString());
if (fromFormat > toFormat && fromFormat != 2)
{
comment = $"Cannot convert a {fromType.Name} to a {PKMType.Name}.";
return null;
}
var pkm = ConvertPKM(pk, PKMType, fromType, toFormat, ref comment);
comment = pkm == null
? $"Cannot convert a {fromType.Name} to a {PKMType.Name}."
: $"Converted from {fromType.Name} to {PKMType.Name}.";
return pkm;
}
private static PKM ConvertPKM(PKM pk, Type PKMType, Type fromType, int toFormat, ref string comment)
{
PKM pkm = pk.Clone();
if (pkm.IsEgg)
ForceHatchPKM(pkm);
switch (fromType.Name)
{
case nameof(PK1):
if (toFormat == 7) // VC->Bank
pkm = ((PK1)pk).ConvertToPK7();
else if (toFormat == 2) // GB<->GB
pkm = ((PK1)pk).ConvertToPK2();
break;
case nameof(PK2):
if (toFormat == 7) // VC->Bank
pkm = ((PK2)pk).ConvertToPK7();
else if (toFormat == 1) // GB<->GB
{
if (pk.Species > 151)
{
comment = $"Cannot convert a {PKX.GetSpeciesName(pkm.Species, pkm.Japanese ? 1 : 2)} to {PKMType.Name}";
return pkm;
}
pkm = ((PK2)pk).ConvertToPK1();
pkm.ClearInvalidMoves();
}
break;
case nameof(CK3):
pkm = ((CK3)pkm).ConvertToPK3();
goto case nameof(PK3); // fall through
case nameof(XK3):
pkm = ((XK3)pkm).ConvertToPK3();
goto case nameof(PK3); // fall through
case nameof(PK3):
if (toFormat == 3)
{
if (PKMType == typeof(CK3))
pkm = ((PK3)pkm).ConvertToCK3();
else if (PKMType == typeof(XK3))
pkm = ((PK3)pkm).ConvertToXK3();
break;
}
pkm = ((PK3) pkm).ConvertToPK4();
if (toFormat == 4)
break;
goto case nameof(PK4);
case nameof(BK4):
pkm = ((BK4) pkm).ConvertToPK4();
if (toFormat == 4)
break;
goto case nameof(PK4);
case nameof(PK4):
if (PKMType == typeof(BK4))
{
pkm = ((PK4) pkm).ConvertToBK4();
break;
}
pkm = ((PK4) pkm).ConvertToPK5();
if (toFormat == 5)
break;
goto case nameof(PK5);
case nameof(PK5):
pkm = ((PK5) pkm).ConvertToPK6();
if (toFormat == 6)
break;
goto case nameof(PK6);
case nameof(PK6):
if (pkm.Species == 25 && pkm.AltForm != 0) // cosplay pikachu
{
comment = "Cannot transfer Cosplay Pikachu forward.";
return pkm;
}
pkm = ((PK6) pkm).ConvertToPK7();
if (toFormat == 7)
break;
goto case nameof(PK7);
case nameof(PK7):
break;
}
return pkm;
}
/// <summary>
/// Checks to see if a PKM is transferrable relative to in-game restrictions and <see cref="PKM.AltForm"/>.
/// </summary>
/// <param name="pk">PKM to convert</param>
/// <param name="comment">Comment indicating why the <see cref="PKM"/> is not transferrable.</param>
/// <returns>Indication if Not Transferrable</returns>
private static bool IsNotTransferrable(PKM pk, out string comment)
{
switch (pk.Species)
{
default:
comment = null;
return false;
case 025 when pk.AltForm != 0 && pk.Gen6: // Cosplay Pikachu
comment = "Cannot transfer Cosplay Pikachu forward.";
return true;
case 172 when pk.AltForm != 0 && pk.Gen4: // Spiky Eared Pichu
comment = "Cannot transfer Spiky-Eared Pichu forward.";
return true;
}
}
/// <summary>
/// Force hatches a PKM by applying the current species name and a valid Met Location from the origin game.
/// </summary>
/// <param name="pkm">PKM to apply hatch details to</param>
/// <remarks>
/// <see cref="PKM.IsEgg"/> is not checked; can be abused to re-hatch already hatched <see cref="PKM"/> inputs.
/// <see cref="PKM.MetDate"/> is not modified; must be updated manually if desired.
/// </remarks>
private static void ForceHatchPKM(PKM pkm)
{
pkm.IsEgg = false;
pkm.Nickname = PKX.GetSpeciesNameGeneration(pkm.Species, pkm.Language, pkm.Format);
var loc = EncounterSuggestion.GetSuggestedEggMetLocation(pkm);
if (loc >= 0)
pkm.Met_Location = loc;
}
/// <summary>
/// Checks if a PKM is encrypted; if encrypted, decrypts the PKM.
/// </summary>
/// <remarks>The input PKM object is decrypted; no new object is returned.</remarks>
/// <param name="pkm">PKM to check encryption for (and decrypt if appropriate).</param>
public static void CheckEncrypted(ref byte[] pkm)
{
int format = GetPKMDataFormat(pkm);
switch (format)
{
case 1:
case 2: // no encryption
return;
case 3:
if (pkm.Length == PKX.SIZE_3CSTORED || pkm.Length == PKX.SIZE_3XSTORED)
return; // no encryption for C/XD
ushort chk = 0;
for (int i = 0x20; i < PKX.SIZE_3STORED; i += 2)
chk += BitConverter.ToUInt16(pkm, i);
if (chk != BitConverter.ToUInt16(pkm, 0x1C))
pkm = PKX.DecryptArray3(pkm);
return;
case 4:
case 5:
if (BitConverter.ToUInt16(pkm, 4) != 0) // BK4
return;
if (BitConverter.ToUInt32(pkm, 0x64) != 0)
pkm = PKX.DecryptArray45(pkm);
return;
case 6:
case 7:
if (BitConverter.ToUInt16(pkm, 0xC8) != 0 && BitConverter.ToUInt16(pkm, 0x58) != 0)
pkm = PKX.DecryptArray(pkm);
return;
default:
return; // bad!
}
}
/// <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 = null;
c = $"Can't load {pk.GetType().Name}s to Gen{target.Format} saves.";
if (!AllowIncompatibleConversion)
return false;
}
if (target.Format < 3 && pk.Japanese != target.Japanese)
{
pkm = null;
var strs = new[] { "International", "Japanese" };
var val = target.Japanese ? 0 : 1;
c = $"Cannot load {strs[val]} {pk.GetType().Name}s to {strs[val ^ 1]} saves.";
if (!AllowIncompatibleConversion)
return false;
}
pkm = ConvertToType(pk, target.GetType(), out c);
Debug.WriteLine(c);
return pkm != null;
}
/// <summary>
/// Gets a Blank <see cref="PKM"/> object of the specified type.
/// </summary>
/// <param name="t">Type of <see cref="PKM"/> instance desired.</param>
/// <returns>New instance of a blank <see cref="PKM"/> object.</returns>
public static PKM GetBlank(Type t)
{
var constructors = t.GetTypeInfo().DeclaredConstructors.Where(z => !z.IsStatic);
var argCount = constructors.First().GetParameters().Length;
return (PKM)Activator.CreateInstance(t, new object[argCount]);
}
}
}