PKHeX/PKHeX.Core/Legality/Verifiers/AbilityVerifier.cs
Kurt 944c47326e Exclude unobtainable SOS encounters
Rather than do backtracking logic to see if the slot can be yielded, don't acknowledge the slot exists if it can't be yielded.
See pk3DS commit where we check EscapeRate for the base wild slot prior to adding it to the exported slot list.
2020-10-10 00:29:53 -07:00

358 lines
15 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 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<int> 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<int> 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<int> 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<int> 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<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].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<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 && abilities[0] != abilities[1])
return GetValid(LAbilityCapsuleUsed);
return GetInvalid(pkm.Format < 6 ? LAbilityMismatchPID : LAbilityMismatchFlag);
}
private CheckResult VerifyAbilityPCD(LegalityAnalysis data, IReadOnlyList<int> 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<int> 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;
}
/// <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>
private CheckResult CheckMatch(PKM pkm, IReadOnlyList<int> 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<int> 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
};
}
}
}