PKHeX/PKHeX.Core/Legality/Verifiers/Ability/AbilityVerifier.cs
Kurt ccf87242c1 Eliminate boxing on encounter search (criteria)
struct implementing interface is boxed when passed to method that accepts interface (not generic method).
Removes IDexLevel (no other inheritors but EvoCriteria) and uses the primitive the data is stored (array, not IReadOnlyList) for slightly better perf.
2022-05-07 18:29:36 -07:00

491 lines
21 KiB
C#

using System.Collections.Generic;
using static PKHeX.Core.LegalityCheckStrings;
namespace PKHeX.Core
{
/// <summary>
/// Verifies the <see cref="PKM.Ability"/> values.
/// </summary>
public sealed class AbilityVerifier : Verifier
{
protected override CheckIdentifier Identifier => CheckIdentifier.Ability;
public override void Verify(LegalityAnalysis data)
{
var result = VerifyAbility(data);
data.AddLine(result);
}
private CheckResult VALID => GetValid(LAbilityFlag);
private CheckResult INVALID => GetInvalid(LAbilityMismatch);
private enum AbilityState : byte
{
CanMismatch,
MustMatch,
}
private CheckResult VerifyAbility(LegalityAnalysis data)
{
var pkm = data.pkm;
var pi = data.PersonalInfo;
// Check ability is possible (within bounds)
int ability = pkm.Ability;
int abilIndex = pi.GetAbilityIndex(ability);
if (abilIndex < 0)
return GetInvalid(LAbilityUnexpected);
var abilities = pi.Abilities;
int format = pkm.Format;
if (format >= 6)
{
var num = pkm.AbilityNumber;
if (!IsValidAbilityBits(num))
return INVALID;
// Check AbilityNumber points to ability
int an = num >> 1;
if (an >= abilities.Count || abilities[an] != ability)
return INVALID;
// Check AbilityNumber for transfers without unique abilities
int gen = data.Info.Generation;
if (gen is 3 or 4 or 5 && num != 4)
{
// To determine AbilityNumber [PK5->PK6], check if the first ability in Personal matches the ability.
// It is not possible to flip it to the other index as capsule requires unique abilities.
if (abilities[0] == abilities[1] && num != 1)
{
// Check if any pre-evolution could have it flipped.
var evos = data.Info.EvoChainsAllGens[6];
var pt = GameData.GetPersonal(GameUtil.GetVersion(format));
if (!GetWasDual(evos, pt, pkm))
return INVALID;
}
}
}
if (format >= 8) // Ability Patch
{
if (pkm.AbilityNumber == 4 && !pkm.LA)
{
if (CanAbilityPatch(format, abilities, pkm.Species))
return GetValid(LAbilityPatchUsed);
var e = data.EncounterOriginal;
if (e.Species != pkm.Species && CanAbilityPatch(format, PKX.Personal.GetFormEntry(e.Species, e.Form).Abilities, e.Species))
return GetValid(LAbilityPatchUsed);
// Verify later, it may be encountered with its hidden ability without using an ability patch.
}
}
var enc = data.EncounterMatch;
if (enc is MysteryGift {Generation: >= 4} g)
return VerifyAbilityMG(data, g, abilities);
if (format < 6)
return VerifyAbility345(data, enc, abilities, abilIndex);
return VerifyAbility(data, abilities, abilIndex);
}
public static bool IsValidAbilityBits(int num) => num is 1 or 2 or 4;
private static bool GetWasDual(EvoCriteria[] evos, PersonalTable pt, ISpeciesForm pk)
{
foreach (var evo in evos)
{
if (evo.Species == pk.Species)
continue;
var pe = pt.GetFormEntry(evo.Species, evo.Form);
var abils = pe.Abilities;
if (CanAbilityCapsule(6, abils))
return true;
}
return false;
}
private CheckResult VerifyAbility(LegalityAnalysis data, IReadOnlyList<int> abilities, int abilIndex)
{
var enc = data.EncounterMatch;
var eabil = enc.Ability;
if (eabil >= 0)
{
if ((data.pkm.AbilityNumber == 4) != (eabil == AbilityPermission.OnlyHidden))
return GetInvalid(LAbilityHiddenFail);
if (eabil > 0)
return VerifyFixedAbility(data, abilities, AbilityState.CanMismatch, eabil, abilIndex);
}
var gen = enc.Generation;
return gen switch
{
5 => VerifyAbility5(data, enc, abilities),
6 => VerifyAbility6(data, enc),
7 => VerifyAbility7(data, enc),
8 when data.pkm.BDSP => VerifyAbility8BDSP(data, enc),
>=8 => VALID,
_ => CheckMatch(data.pkm, abilities, gen, AbilityState.CanMismatch, enc),
};
}
private CheckResult VerifyAbility345(LegalityAnalysis data, IEncounterable enc, IReadOnlyList<int> abilities, int abilIndex)
{
var pkm = data.pkm;
int format = pkm.Format;
var state = AbilityState.MustMatch;
if (format is (3 or 4 or 5) && abilities[0] != abilities[1]) // 3-4/5 and have 2 distinct abilities now
state = VerifyAbilityPreCapsule(data, abilities);
var encounterAbility = enc.Ability;
if (encounterAbility >= 0)
{
if ((pkm.AbilityNumber == 4) != (encounterAbility == AbilityPermission.OnlyHidden))
return GetInvalid(LAbilityHiddenFail);
if (encounterAbility > 0)
return VerifyFixedAbility(data, abilities, state, encounterAbility, abilIndex);
}
int gen = enc.Generation;
if (gen == 5)
return VerifyAbility5(data, enc, abilities);
return CheckMatch(pkm, abilities, gen, state, enc);
}
private CheckResult VerifyFixedAbility(LegalityAnalysis data, IReadOnlyList<int> abilities, AbilityState state, AbilityPermission encounterAbility, int abilIndex)
{
var pkm = data.pkm;
var enc = data.Info.EncounterMatch;
if (enc.Generation >= 6)
{
if (IsAbilityCapsuleModified(pkm, abilities, encounterAbility))
return GetValid(LAbilityCapsuleUsed);
if (pkm.AbilityNumber != 1 << encounterAbility.GetSingleValue())
return INVALID;
return VALID;
}
if ((pkm.AbilityNumber == 4) != (encounterAbility == AbilityPermission.OnlyHidden))
return GetInvalid(LAbilityHiddenFail);
bool hasEvolved = enc.Species != pkm.Species;
if (hasEvolved && state != AbilityState.CanMismatch)
{
// Evolving in Gen3 does not mutate the ability bit, so any mismatched abilities will stay mismatched.
if (enc.Generation == 3)
{
if (encounterAbility.GetSingleValue() == abilIndex)
return VALID;
// If it is in a future game and does not match the fixed ability, then it must match the PID.
if (pkm.Format != 3)
return GetPIDAbilityMatch(pkm, abilities);
// No way to un-mismatch it while existing solely on Gen3 games.
return INVALID;
}
return CheckMatch(pkm, abilities, enc.Generation, AbilityState.MustMatch, enc);
}
if (encounterAbility.GetSingleValue() == abilIndex)
return VALID;
if (pkm.AbilityNumber == 1 << encounterAbility.GetSingleValue())
return VALID;
if (state == AbilityState.CanMismatch || encounterAbility == 0)
return CheckMatch(pkm, abilities, enc.Generation, AbilityState.MustMatch, enc);
if (IsAbilityCapsuleModified(pkm, abilities, encounterAbility))
return GetValid(LAbilityCapsuleUsed);
return INVALID;
}
private AbilityState VerifyAbilityPreCapsule(LegalityAnalysis data, IReadOnlyList<int> abilities)
{
var info = data.Info;
// Gen4/5 origin
if (info.Generation != 3)
return AbilityState.MustMatch;
// Gen3 origin... a lot of edge cases to check.
var pkm = data.pkm;
var format = pkm.Format;
// CXD pokemon can have any ability without matching PID
if (format == 3)
{
if (pkm.Version == (int)GameVersion.CXD)
return AbilityState.CanMismatch;
return AbilityState.MustMatch;
}
// Evovled in Gen4/5
if (pkm.Species > Legal.MaxSpeciesID_3)
return AbilityState.MustMatch;
// If the species could not exist in Gen3, must match.
var g3 = info.EvoChainsAllGens[3];
if (g3.Length == 0)
return AbilityState.MustMatch;
// Fall through when gen3 pkm transferred to gen4/5
var maxGen3Species = g3[0].Species;
return VerifyAbilityGen3Transfer(data, abilities, maxGen3Species);
}
private AbilityState VerifyAbilityGen3Transfer(LegalityAnalysis data, IReadOnlyList<int> abilities, int maxGen3Species)
{
var pkm = data.pkm;
var pers = (PersonalInfoG3)PersonalTable.E[maxGen3Species];
if (pers.Ability1 != pers.Ability2) // Excluding Colosseum/XD, a Gen3 pkm must match PID if it has 2 unique abilities
return pkm.Version == (int) GameVersion.CXD ? AbilityState.CanMismatch : AbilityState.MustMatch;
if (pkm.Species != maxGen3Species) // it has evolved in either gen 4 or gen 5; the ability must match PID
return AbilityState.MustMatch;
var chain = data.Info.EvoChainsAllGens;
bool evolved45 = chain[4].Length > 1 || (pkm.Format == 5 && chain[5].Length > 1);
if (evolved45)
{
if (pkm.Ability == pers.Ability1) // Could evolve in Gen4/5 and have a Gen3 only ability
return AbilityState.CanMismatch; // Not evolved in Gen4/5, doesn't need to match PIDAbility
if (pkm.Ability == abilities[1]) // It could evolve in Gen4/5 and have Gen4 second ability
return AbilityState.MustMatch; // Evolved in Gen4/5, must match PIDAbility
}
// If we reach here, it has not evolved in Gen4/5 games or has an invalid ability.
// The ability does not need to match the PIDAbility, but only Gen3 ability is allowed.
if (pkm.Ability != pers.Ability1) // Not evolved in Gen4/5, but doesn't have Gen3 only ability
data.AddLine(GetInvalid(LAbilityMismatch3)); // probably bad to do this here
return AbilityState.CanMismatch;
}
private CheckResult VerifyAbilityMG(LegalityAnalysis data, MysteryGift g, IReadOnlyList<int> abilities)
{
if (g is PCD d)
return VerifyAbilityPCD(data, abilities, d);
var pkm = data.pkm;
if (g is PGT) // Ranger Manaphy
return (pkm.Format >= 6 ? (pkm.AbilityNumber == 1) : (pkm.AbilityNumber < 4)) ? VALID : GetInvalid(LAbilityMismatchGift);
var cardType = g.AbilityType;
if (cardType == 4) // 1/2/H
return VALID;
int abilNumber = pkm.AbilityNumber;
if (cardType == 3) // 1/2
return abilNumber == 4 ? GetInvalid(LAbilityMismatchGift) : VALID;
// Only remaining matches are fixed index abilities
int cardAbilIndex = 1 << cardType;
if (abilNumber == cardAbilIndex)
return VALID;
// Can still match if the ability was changed via ability capsule...
// However, it can't change to/from Hidden Abilities.
if (abilNumber == 4 || cardType == 2)
return GetInvalid(LAbilityHiddenFail);
// Ability can be flipped 0/1 if Ability Capsule is available, is not Hidden Ability, and Abilities are different.
if (pkm.Format >= 6)
{
if (CanAbilityCapsule(6, abilities))
return GetValid(LAbilityCapsuleUsed);
// Maybe was evolved after using ability capsule.
var evos = data.Info.EvoChainsAllGens[pkm.Format];
if (GetWasDual(evos, PKX.Personal, pkm))
return GetValid(LAbilityCapsuleUsed);
}
return pkm.Format < 6 ? GetInvalid(LAbilityMismatchPID) : INVALID;
}
private CheckResult VerifyAbilityPCD(LegalityAnalysis data, IReadOnlyList<int> abilities, PCD pcd)
{
var pkm = data.pkm;
var format = pkm.Format;
if (format >= 6)
{
if (!CanAbilityCapsule(format, abilities))
{
// Gen3-5 transfer with same ability -> 1st ability that matches
if (pkm.AbilityNumber == 1)
return VALID;
return CheckMatch(pkm, abilities, 4, AbilityState.MustMatch, pcd); // evolved, must match
}
if (pkm.AbilityNumber < 4) // Ability Capsule can change between 1/2
return GetValid(LAbilityCapsuleUsed);
}
if (pcd.Species != pkm.Species)
return CheckMatch(pkm, abilities, 4, AbilityState.MustMatch, pcd); // evolved, must match
// Edge case (PID ability gift mismatch) -- must match gift ability.
return pkm.Ability == pcd.Gift.PK.Ability ? VALID : INVALID;
}
private CheckResult VerifyAbility5(LegalityAnalysis data, IEncounterTemplate enc, IReadOnlyList<int> abilities)
{
var pkm = data.pkm;
// Eggs and Encounter Slots are not yet checked for Hidden Ability potential.
return enc switch
{
EncounterEgg e when pkm.AbilityNumber == 4 && AbilityBreedLegality.BanHidden5.Contains(e.Species) => GetInvalid(LAbilityHiddenUnavailable),
_ => CheckMatch(data.pkm, abilities, 5, pkm.Format == 5 ? AbilityState.MustMatch : AbilityState.CanMismatch, enc),
};
}
private CheckResult VerifyAbility6(LegalityAnalysis data, IEncounterTemplate enc)
{
var pkm = data.pkm;
if (pkm.AbilityNumber != 4)
return VALID;
// Eggs and Encounter Slots are not yet checked for Hidden Ability potential.
return enc switch
{
EncounterEgg egg when AbilityBreedLegality.BanHidden6.Contains(egg.Species | (egg.Form << 11)) => GetInvalid(LAbilityHiddenUnavailable),
_ => VALID,
};
}
private CheckResult VerifyAbility7(LegalityAnalysis data, IEncounterTemplate enc)
{
var pkm = data.pkm;
if (pkm.AbilityNumber != 4)
return VALID;
return enc switch
{
EncounterEgg egg when AbilityBreedLegality.BanHidden7.Contains(egg.Species | (egg.Form << 11)) => GetInvalid(LAbilityHiddenUnavailable),
_ => VALID,
};
}
private CheckResult VerifyAbility8BDSP(LegalityAnalysis data, IEncounterable enc)
{
var pkm = data.pkm;
if (pkm.AbilityNumber != 4)
return VALID;
return enc switch
{
EncounterEgg egg when AbilityBreedLegality.BanHidden8b.Contains(egg.Species | (egg.Form << 11)) => GetInvalid(LAbilityHiddenUnavailable),
_ => VALID,
};
}
/// <summary>
/// Final checks assuming nothing else has flagged the ability.
/// </summary>
/// <param name="pkm">Pokémon</param>
/// <param name="abilities">Current abilities</param>
/// <param name="gen">Generation</param>
/// <param name="state">Permissive to allow ability to deviate under special circumstances</param>
/// <param name="enc">Encounter template the <see cref="pkm"/> was matched to.</param>
private CheckResult CheckMatch(PKM pkm, IReadOnlyList<int> abilities, int gen, AbilityState state, IEncounterTemplate enc)
{
if (gen is (3 or 4) && pkm.AbilityNumber == 4)
return GetInvalid(LAbilityHiddenUnavailable);
// other cases of hidden ability already flagged, all that is left is 1/2 mismatching
if (state != AbilityState.MustMatch)
return VALID;
// Check that the ability bit is correct.
if (pkm is G3PKM g3)
{
var abit = g3.AbilityBit;
// We've sanitized our personal data to replace "None" abilities with the first ability.
// Granbull, Vibrava, and Flygon have dual abilities being the same.
if (abilities[0] == abilities[1] && g3.Species is not ((int)Species.Granbull or (int)Species.Vibrava or (int)Species.Flygon)) // Not a dual ability
{
// Must not have the Ability bit flag set.
// Shadow encounters set a random ability index; don't bother checking if it's a re-battle for ability bit flipping.
if (abit && enc is not EncounterStaticShadow)
return GetInvalid(LAbilityMismatchFlag, CheckIdentifier.PID);
}
else
{
// Gen3 mainline origin sets the Ability index based on the PID, but only if it has two abilities.
// Version value check isn't factually correct, but there are no C/XD gifts with (Version!=15) that have two abilities.
// Pikachu, Celebi, Ho-Oh
if (pkm.Version != (int)GameVersion.CXD && abit != ((pkm.PID & 1) == 1))
return GetInvalid(LAbilityMismatchPID, CheckIdentifier.PID);
}
}
else if (pkm.Format >= 6)
{
// 6+ already checked at the top of the verifier call stack
return VALID;
}
// 3-5
return GetPIDAbilityMatch(pkm, abilities);
}
private CheckResult GetPIDAbilityMatch(PKM pkm, IReadOnlyList<int> abilities)
{
// Ability Number bits are already verified as clean.
var abil = abilities[pkm.AbilityNumber >> 1];
if (abil != pkm.Ability)
return GetInvalid(LAbilityMismatchPID);
return VALID;
}
// Ability Capsule can change between 1/2
private static bool IsAbilityCapsuleModified(PKM pkm, IReadOnlyList<int> abilities, AbilityPermission encounterAbility)
{
if (!CanAbilityCapsule(pkm.Format, abilities))
return false;
if (pkm.AbilityNumber == 4)
return false; // Cannot alter to hidden ability.
if (pkm.LA)
return false; // Not available.
if (encounterAbility == AbilityPermission.OnlyHidden)
return false; // Cannot alter from hidden ability.
return true;
}
public static bool CanAbilityCapsule(int format, IReadOnlyList<int> abilities)
{
if (format < 6) // Ability Capsule does not exist
return false;
return abilities[0] != abilities[1]; // Cannot alter ability index if it is the same as the other ability.
}
public static bool CanAbilityPatch(int format, IReadOnlyList<int> abilities, int species)
{
if (format < 8) // Ability Patch does not exist
return false;
// Can alter ability index if it is different from the other abilities.
var h = abilities[2];
if (h != abilities[0] || h != abilities[1])
return true;
// Some species have a distinct hidden ability only on another form, and can change between that form and its current form.
return species switch
{
(int)Species.Giratina => true, // Form-0 is a/a/h
(int)Species.Tornadus => true, // Form-0 is a/a/h
(int)Species.Thundurus => true, // Form-0 is a/a/h
(int)Species.Landorus => true, // Form-0 is a/a/h
(int)Species.Enamorus => true, // Form-0 is a/a/h
_ => false,
};
}
}
}