PKHeX/PKHeX.Core/MysteryGifts/PGT.cs
Kurt f632aedd15
Encounter Templates: Searching and Creating (#3955)
We implement simple state machine iterators to iterate through every split type encounter array, and more finely control the path we iterate through. And, by using generics, we can have the compiler generate optimized code to avoid virtual calls.

In addition to this, we shift away from the big-5 encounter types and not inherit from an abstract class. This allows for creating a PK* of a specific type and directly writing properties (no virtual calls). Plus we can now fine-tune each encounter type to call specific code, and not have to worry about future game encounter types bothering the generation routines.
2023-08-12 16:01:16 -07:00

342 lines
13 KiB
C#

using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
/// <summary>
/// Generation 4 Mystery Gift Template File (Inner Gift Data, no card data)
/// </summary>
public sealed class PGT : DataMysteryGift, IRibbonSetEvent3, IRibbonSetEvent4, IRandomCorrelation
{
public const int Size = 0x104; // 260
public override int Generation => 4;
public override EntityContext Context => EntityContext.Gen4;
public override bool FatefulEncounter => IsManaphyEgg || PK.FatefulEncounter;
public override GameVersion Version { get => IsManaphyEgg ? GameVersion.Gen4 : (GameVersion)PK.Version; set => PK.Version = (int)value; }
public override byte Level
{
get => IsManaphyEgg ? (byte)1 : IsEntity ? (byte)PK.Met_Level : (byte)0;
set { if (IsEntity) PK.Met_Level = value; }
}
public override int Ball
{
get => IsManaphyEgg ? 4 : IsEntity ? PK.Ball : 0;
set { if (IsEntity) PK.Ball = value; }
}
public override AbilityPermission Ability => IsManaphyEgg ? AbilityPermission.Any12 : (int)(PK.PID & 1) == 1 ? AbilityPermission.OnlySecond : AbilityPermission.OnlyFirst;
private enum GiftType
{
Pokémon = 1,
PokémonEgg = 2,
Item = 3,
Rule = 4,
Seal = 5,
Accessory = 6,
ManaphyEgg = 7,
MemberCard = 8,
OaksLetter = 9,
AzureFlute = 10,
PokétchApp = 11,
Ribbon = 12,
PokéWalkerArea = 14,
}
public override string CardTitle { get => "Raw Gift (PGT)"; set { } }
public override int CardID { get => -1; set { } }
public override bool GiftUsed { get => false; set { } }
public override Shiny Shiny => IsEgg ? Shiny.Random : PK.PID == 1 ? Shiny.Never : IsShiny ? Shiny.Always : Shiny.Never;
public PGT() : this(new byte[Size]) { }
public PGT(byte[] data) : base(data) { }
public byte CardType { get => Data[0]; set => Data[0] = value; }
// Unused 0x01
public byte Slot { get => Data[2]; set => Data[2] = value; }
public byte Detail { get => Data[3]; set => Data[3] = value; }
public override int ItemID { get => ReadUInt16LittleEndian(Data.AsSpan(0x4)); set => WriteUInt16LittleEndian(Data.AsSpan(0x4), (ushort)value); }
public PK4 PK
{
get => _pk ??= new PK4(Data.Slice(8, PokeCrypto.SIZE_4PARTY));
set
{
_pk = value;
var data = value.Data;
bool zero = Array.TrueForAll(data, static z => z == 0); // all zero
if (!zero)
data = PokeCrypto.EncryptArray45(data);
data.CopyTo(Data, 8);
}
}
public override byte[] Write()
{
// Ensure PGT content is encrypted
var clone = new PGT((byte[])Data.Clone());
clone.VerifyPKEncryption();
return clone.Data;
}
private PK4? _pk;
/// <summary>
/// Double checks the encryption of the gift data for Pokemon data.
/// </summary>
/// <returns>True if data was encrypted, false if the data was not modified.</returns>
public bool VerifyPKEncryption()
{
if (PGTGiftType is not (GiftType.Pokémon or GiftType.PokémonEgg))
return false; // not encrypted
if (ReadUInt32LittleEndian(Data.AsSpan(0x64 + 8)) != 0)
return false; // already encrypted (unused PK4 field, zero)
EncryptPK();
return true;
}
private void EncryptPK()
{
var span = Data.AsSpan(8, PokeCrypto.SIZE_4PARTY);
var ekdata = PokeCrypto.EncryptArray45(span);
ekdata.CopyTo(span);
}
private GiftType PGTGiftType { get => (GiftType)Data[0]; set => Data[0] = (byte)value; }
public bool IsHatched => PGTGiftType == GiftType.Pokémon;
public override bool IsEgg { get => PGTGiftType == GiftType.PokémonEgg || IsManaphyEgg; set { if (value) { PGTGiftType = GiftType.PokémonEgg; PK.IsEgg = true; } } }
public bool IsManaphyEgg { get => PGTGiftType == GiftType.ManaphyEgg; set { if (value) PGTGiftType = GiftType.ManaphyEgg; } }
public override bool EggEncounter => IsEgg;
public override bool IsItem { get => PGTGiftType == GiftType.Item; set { if (value) PGTGiftType = GiftType.Item; } }
public override bool IsEntity { get => PGTGiftType is GiftType.Pokémon or GiftType.PokémonEgg or GiftType.ManaphyEgg; set { } }
public override ushort Species { get => IsManaphyEgg ? (ushort)490 : PK.Species; set => PK.Species = value; }
public override Moveset Moves { get => new(PK.Move1, PK.Move2, PK.Move3, PK.Move4); set => PK.SetMoves(value); }
public override int HeldItem { get => PK.HeldItem; set => PK.HeldItem = value; }
public override bool IsShiny => PK.IsShiny;
public override int Gender { get => PK.Gender; set => PK.Gender = value; }
public override byte Form { get => PK.Form; set => PK.Form = value; }
public override uint ID32 { get => PK.ID32; set => PK.ID32= value; }
public override ushort TID16 { get => PK.TID16; set => PK.TID16 = value; }
public override ushort SID16 { get => PK.SID16; set => PK.SID16 = value; }
public override string OT_Name { get => PK.OT_Name; set => PK.OT_Name = value; }
public override int Location { get => PK.Met_Location; set => PK.Met_Location = value; }
public override int EggLocation { get => PK.Egg_Location; set => PK.Egg_Location = value; }
public override bool HasFixedIVs => (PK.IV32 & 0x3FFF_FFFFu) != 0;
public override void GetIVs(Span<int> value)
{
if (HasFixedIVs)
PK.GetIVs(value);
}
public override PK4 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
{
if (!IsEntity)
throw new ArgumentException(nameof(IsEntity));
// template is already filled out, only minor mutations required
PK4 pk4 = new((byte[])PK.Data.Clone()) { Sanity = 0 };
if (!IsHatched && Detail == 0)
{
pk4.OT_Name = tr.OT;
pk4.TID16 = tr.TID16;
pk4.SID16 = tr.SID16;
pk4.OT_Gender = tr.Gender;
pk4.Language = tr.Language;
}
if (IsManaphyEgg)
SetDefaultManaphyEggDetails(pk4, tr);
SetPINGA(pk4, criteria);
SetMetData(pk4, tr);
var pi = pk4.PersonalInfo;
pk4.CurrentFriendship = pk4.IsEgg ? pi.HatchCycles : pi.BaseFriendship;
pk4.RefreshChecksum();
return pk4;
}
private void SetMetData(PK4 pk4, ITrainerInfo trainer)
{
if (!EggEncounter)
{
pk4.Met_Location = pk4.Egg_Location + 3000;
pk4.Egg_Location = 0;
pk4.MetDate = EncounterDate.GetDateNDS();
pk4.IsEgg = false;
}
else
{
pk4.Egg_Location += 3000;
if (trainer.Generation == 4)
SetUnhatchedEggDetails(pk4);
else
SetHatchedEggDetails(pk4);
}
}
private static void SetDefaultManaphyEggDetails(PK4 pk4, ITrainerInfo trainer)
{
// Since none of this data is populated, fill in default info.
pk4.Species = (int)Core.Species.Manaphy;
pk4.Gender = 2;
// Level 1 Moves
pk4.Move1 = 294; pk4.Move1_PP = 20;
pk4.Move2 = 145; pk4.Move2_PP = 30;
pk4.Move3 = 346; pk4.Move3_PP = 15;
pk4.Ability = (int)Core.Ability.Hydration;
pk4.FatefulEncounter = true;
pk4.Ball = (int)Core.Ball.Poke;
pk4.Version = GameVersion.Gen4.Contains(trainer.Game) ? trainer.Game : (int)GameVersion.D;
var lang = trainer.Language < (int)LanguageID.Korean ? trainer.Language : (int)LanguageID.English;
pk4.Language = lang;
pk4.Egg_Location = 1; // Ranger (will be +3000 later)
pk4.Nickname = SpeciesName.GetSpeciesNameGeneration((int)Core.Species.Manaphy, lang, 4);
}
private void SetPINGA(PK4 pk4, EncounterCriteria criteria)
{
// Ability is forced already, can't force anything
// Generate PID
var seed = SetPID(pk4, criteria);
if (!IsManaphyEgg)
seed = Util.Rand32(); // reseed, do not have method 1 correlation
// Generate IVs
if ((pk4.IV32 & 0x3FFF_FFFFu) == 0) // Ignore Nickname/Egg flag bits
{
uint iv1 = ((seed = LCRNG.Next(seed)) >> 16) & 0x7FFF;
uint iv2 = ((LCRNG.Next(seed)) >> 16) & 0x7FFF;
pk4.IV32 |= iv1 | (iv2 << 15);
}
}
private uint SetPID(PK4 pk4, EncounterCriteria criteria)
{
uint seed = Util.Rand32();
if (pk4.PID != 1 && !IsManaphyEgg)
return seed; // PID is already set.
// The games don't decide the Nature/Gender up-front, but we can try to honor requests.
// Pre-determine the result values, and generate something.
var n = (int)criteria.GetNature(Nature.Random);
// Gender is already pre-determined in the template.
while (true)
{
seed = GeneratePID(seed, pk4);
if (pk4.Nature != n)
continue;
return seed;
}
}
private static void SetHatchedEggDetails(PK4 pk4)
{
pk4.IsEgg = false;
// Met Location & Date is modified when transferred to pk5; don't worry about it.
pk4.EggMetDate = EncounterDate.GetDateNDS();
}
private void SetUnhatchedEggDetails(PK4 pk4)
{
pk4.IsEgg = true;
pk4.IsNicknamed = false;
pk4.Nickname = SpeciesName.GetEggName(pk4.Language, Generation);
pk4.EggMetDate = EncounterDate.GetDateNDS();
}
private static uint GeneratePID(uint seed, PK4 pk4)
{
do
{
uint pid1 = (seed = LCRNG.Next(seed)) >> 16; // low
uint pid2 = (seed = LCRNG.Next(seed)) & 0xFFFF0000; // hi
pk4.PID = pid2 | pid1;
// sanity check gender for non-genderless PID cases
} while (!pk4.IsGenderValid());
while (pk4.IsShiny) // Call the ARNG to change the PID
pk4.PID = ARNG.Next(pk4.PID);
return seed;
}
public static bool IsRangerManaphy(PKM pk)
{
var egg = pk.Egg_Location;
if (!pk.IsEgg) // Link Trade Egg or Ranger
return egg is Locations.LinkTrade4 or Locations.Ranger4;
if (egg != Locations.Ranger4)
return false;
if (pk.Language == (int)LanguageID.Korean) // never korean
return false;
var met = pk.Met_Location;
return met is Locations.LinkTrade4 or 0;
}
// Nothing is stored as a PGT besides Ranger Manaphy. Nothing should trigger these.
public override bool IsMatchExact(PKM pk, EvoCriteria evo) => false;
protected override bool IsMatchDeferred(PKM pk) => false;
protected override bool IsMatchPartial(PKM pk) => false;
public bool RibbonEarth { get => PK.RibbonEarth; set => PK.RibbonEarth = value; }
public bool RibbonNational { get => PK.RibbonNational; set => PK.RibbonNational = value; }
public bool RibbonCountry { get => PK.RibbonCountry; set => PK.RibbonCountry = value; }
public bool RibbonChampionBattle { get => PK.RibbonChampionBattle; set => PK.RibbonChampionBattle = value; }
public bool RibbonChampionRegional { get => PK.RibbonChampionRegional; set => PK.RibbonChampionRegional = value; }
public bool RibbonChampionNational { get => PK.RibbonChampionNational; set => PK.RibbonChampionNational = value; }
public bool RibbonClassic { get => PK.RibbonClassic; set => PK.RibbonClassic = value; }
public bool RibbonWishing { get => PK.RibbonWishing; set => PK.RibbonWishing = value; }
public bool RibbonPremier { get => PK.RibbonPremier; set => PK.RibbonPremier = value; }
public bool RibbonEvent { get => PK.RibbonEvent; set => PK.RibbonEvent = value; }
public bool RibbonBirthday { get => PK.RibbonBirthday; set => PK.RibbonBirthday = value; }
public bool RibbonSpecial { get => PK.RibbonSpecial; set => PK.RibbonSpecial = value; }
public bool RibbonWorld { get => PK.RibbonWorld; set => PK.RibbonWorld = value; }
public bool RibbonChampionWorld { get => PK.RibbonChampionWorld; set => PK.RibbonChampionWorld = value; }
public bool RibbonSouvenir { get => PK.RibbonSouvenir; set => PK.RibbonSouvenir = value; }
public bool IsCompatible(PIDType val, PKM pk)
{
if (IsManaphyEgg)
return IsG4ManaphyPIDValid(val, pk);
return val == PIDType.None;
}
public PIDType GetSuggestedCorrelation()
{
if (IsManaphyEgg)
return PIDType.Method_1;
return PIDType.None;
}
private static bool IsG4ManaphyPIDValid(PIDType val, PKM pk)
{
if (pk.IsEgg)
{
if (pk.IsShiny)
return false;
if (val == PIDType.Method_1)
return true;
return val == PIDType.G4MGAntiShiny && IsAntiShinyARNG(pk);
}
if (val == PIDType.Method_1)
return pk.WasTradedEgg || !pk.IsShiny; // can't be shiny on received game
return val == PIDType.G4MGAntiShiny && (pk.WasTradedEgg || IsAntiShinyARNG(pk));
static bool IsAntiShinyARNG(PKM pk)
{
var shinyPID = ARNG.Prev(pk.PID);
var tmp = pk.ID32 ^ shinyPID;
var xor = (ushort)(tmp ^ (tmp >> 16));
return xor < 8; // shiny proc
}
}
}