using System.Collections.Generic; using static PKHeX.Core.LegalityCheckStrings; namespace PKHeX.Core { /// /// Verifies the values. /// 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 abilval = pi.GetAbilityIndex(ability); if (abilval < 0) return GetInvalid(LAbilityUnexpected); var enc = data.EncounterMatch; var abilities = pi.Abilities; if (enc is MysteryGift g && g.Format >= 4) return VerifyAbilityMG(data, g, abilities); if (pkm.Format < 6) return VerifyAbility345(data, enc, abilities, abilval); // Check AbilityNumber is a single set bit var num = pkm.AbilityNumber; if (num == 0 || num == 0b101 || num == 0b110 || num == 0b011) return GetInvalid(LAbilityMismatchFlag); // Check AbilityNumber points to ability int an = num >> 1; if (an >= abilities.Count || abilities[an] != ability) return GetInvalid(LAbilityMismatchFlag); return VerifyAbility(data, abilities, abilval); } private CheckResult VerifyAbility(LegalityAnalysis data, IReadOnlyList abilities, int abilnum) { var enc = data.EncounterMatch; var eabil = GetEncounterFixedAbilityNumber(enc); if (eabil >= 0) { if ((data.pkm.AbilityNumber == 4) != (eabil == 4)) return GetInvalid(LAbilityHiddenFail); if (eabil > 0) return VerifyFixedAbility(data, abilities, AbilityState.CanMismatch, eabil, abilnum); } var gen = data.Info.Generation; return gen switch { 5 => VerifyAbility5(data, enc, abilities), 6 => VerifyAbility6(data, enc), 7 => VerifyAbility7(data, enc), 8 => VerifyAbility8(data, enc), _ => CheckMatch(data.pkm, abilities, gen, AbilityState.CanMismatch) }; } private CheckResult VerifyAbility345(LegalityAnalysis data, IEncounterable enc, IReadOnlyList abilities, int abilnum) { var pkm = data.pkm; var state = AbilityState.MustMatch; if (3 <= pkm.Format && pkm.Format <= 5 && abilities[0] != abilities[1]) // 3-4/5 and have 2 distinct abilities now state = VerifyAbilityPreCapsule(data, abilities); var EncounterMatch = data.EncounterMatch; int eabil = GetEncounterFixedAbilityNumber(EncounterMatch); if (eabil >= 0) { if ((data.pkm.AbilityNumber == 4) != (eabil == 4)) return GetInvalid(LAbilityHiddenFail); if (eabil > 0) return VerifyFixedAbility(data, abilities, state, eabil, abilnum); } int gen = data.Info.Generation; if (gen == 5) return VerifyAbility5(data, enc, abilities); return CheckMatch(pkm, abilities, gen, state); } private CheckResult VerifyFixedAbility(LegalityAnalysis data, IReadOnlyList abilities, AbilityState state, int EncounterAbility, int abilval) { var pkm = data.pkm; if (data.Info.EncounterMatch.Generation >= 6) { if (IsAbilityCapsuleModified(pkm, abilities, EncounterAbility)) return GetValid(LAbilityCapsuleUsed); if (pkm.AbilityNumber != EncounterAbility) return INVALID; return VALID; } if ((pkm.AbilityNumber == 4) != (EncounterAbility == 4)) return GetInvalid(LAbilityHiddenFail); if (data.EncounterMatch.Species != pkm.Species && state != AbilityState.CanMismatch) // evolved return CheckMatch(pkm, abilities, data.Info.Generation, AbilityState.MustMatch); if (EncounterAbility == 1 << abilval) return GetValid(LAbilityFlag); if (pkm.AbilityNumber == EncounterAbility) return VALID; if (state == AbilityState.CanMismatch || EncounterAbility == 0) return CheckMatch(pkm, abilities, data.Info.Generation, AbilityState.MustMatch); if (IsAbilityCapsuleModified(pkm, abilities, EncounterAbility)) return GetValid(LAbilityCapsuleUsed); return INVALID; } private AbilityState VerifyAbilityPreCapsule(LegalityAnalysis data, IReadOnlyList abilities) { var pkm = data.pkm; // CXD pokemon can have any ability without matching PID if (pkm.Version == (int)GameVersion.CXD && pkm.Format == 3) return AbilityState.CanMismatch; // Gen3 native or Gen4/5 origin if (pkm.Format == 3 || !pkm.InhabitedGeneration(3)) 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. if (data.Info.EvoChainsAllGens[3].Count == 0) return AbilityState.MustMatch; // Fall through when gen3 pkm transferred to gen4/5 return VerifyAbilityGen3Transfer(data, abilities, data.Info.EvoChainsAllGens[3][0].Species); } private AbilityState VerifyAbilityGen3Transfer(LegalityAnalysis data, IReadOnlyList 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].Count > 1 || (pkm.Format == 5 && chain[5].Count > 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 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 && abilities[0] != abilities[1]) return GetValid(LAbilityCapsuleUsed); return GetInvalid(pkm.Format < 6 ? LAbilityMismatchPID : LAbilityMismatchFlag); } private CheckResult VerifyAbilityPCD(LegalityAnalysis data, IReadOnlyList abilities, PCD pcd) { var pkm = data.pkm; if (pkm.Format >= 6) { if (abilities[0] == abilities[1]) { // Gen3-5 transfer with same ability -> 1st ability that matches if (pkm.AbilityNumber == 1) return GetValid(LAbilityFlag); return CheckMatch(pkm, abilities, 4, AbilityState.MustMatch); // 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); // 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, IEncounterable enc, IReadOnlyList abilities) { var pkm = data.pkm; switch (enc) { case EncounterSlot w: // Hidden Abilities for Wild Encounters are only available at a Hidden Grotto bool grotto = w.Area.Type == SlotType.HiddenGrotto; if (pkm.AbilityNumber == 4 ^ grotto) return GetInvalid(grotto ? LAbilityMismatchGrotto : LAbilityHiddenFail); break; case EncounterEgg e when pkm.AbilityNumber == 4: // Hidden Abilities for some are unbreedable or unreleased if (Legal.Ban_BreedHidden5.Contains(e.Species)) return GetInvalid(LAbilityHiddenUnavailable); break; } var state = pkm.Format == 5 ? AbilityState.MustMatch : AbilityState.CanMismatch; return CheckMatch(data.pkm, abilities, 5, state); } private CheckResult VerifyAbility6(LegalityAnalysis data, IEncounterable enc) { var pkm = data.pkm; if (pkm.AbilityNumber != 4) return VALID; // hidden abilities if (enc is EncounterSlot slot) { bool valid = (slot is EncounterSlot6AO ao && ao.CanDexNav) || slot.Area.Type == SlotType.FriendSafari || slot.Area.Type == SlotType.Horde; if (!valid) return GetInvalid(LAbilityMismatchHordeSafari); } if (Legal.Ban_NoHidden6.Contains(pkm.SpecForm)) return GetInvalid(LAbilityHiddenUnavailable); return VALID; } private CheckResult VerifyAbility7(LegalityAnalysis data, IEncounterable enc) { var pkm = data.pkm; if (enc is EncounterSlot slot && pkm.AbilityNumber == 4) { bool valid = slot.Area.Type == SlotType.SOS; if (!valid) return GetInvalid(LAbilityMismatchSOS); } if (Legal.Ban_NoHidden7.Contains(pkm.SpecForm) && pkm.AbilityNumber == 4) return GetInvalid(LAbilityHiddenUnavailable); return VALID; } private CheckResult VerifyAbility8(LegalityAnalysis data, IEncounterable enc) { var pkm = data.pkm; if (enc is EncounterSlot && pkm.AbilityNumber == 4) return GetInvalid(LAbilityHiddenUnavailable); if (Legal.Ban_NoHidden8.Contains(pkm.SpecForm) && pkm.AbilityNumber == 4) return GetInvalid(LAbilityHiddenUnavailable); return VALID; } /// /// Final checks assuming nothing else has flagged the ability. /// /// Pokémon /// Current abilities /// Generation /// Permissive to allow ability to deviate under special circumstances private CheckResult CheckMatch(PKM pkm, IReadOnlyList abilities, int gen, AbilityState state) { if (3 <= gen && gen <= 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 && abilities[pkm.AbilityNumber >> 1] != pkm.Ability) return GetInvalid(pkm.Format < 6 ? LAbilityMismatchPID : LAbilityMismatchFlag); return VALID; } // Ability Capsule can change between 1/2 private static bool IsAbilityCapsuleModified(PKM pkm, IReadOnlyList abilities, int EncounterAbility) { if (pkm.Format < 6) return false; // Ability Capsule does not exist if (abilities[0] == abilities[1]) return false; // Cannot alter ability index if it is the same as the other ability. if (pkm.AbilityNumber == 4) return false; // Cannot alter to hidden ability. if (EncounterAbility == 4) return false; // Cannot alter from hidden ability. return true; } private static int GetEncounterFixedAbilityNumber(IEncounterable enc) { return enc switch { EncounterStatic s => s.Ability, EncounterTrade t => t.Ability, _ => -1 }; } } }