PKHeX/PKHeX.Core/Legality/Verifiers/Ball/BallVerifier.cs

473 lines
21 KiB
C#
Raw Normal View History

using System.Collections.Generic;
using static PKHeX.Core.LegalityCheckStrings;
using static PKHeX.Core.Ball;
namespace PKHeX.Core
{
2018-07-02 02:17:37 +00:00
/// <summary>
/// Verifies the <see cref="PKM.Ball"/> value.
/// </summary>
public sealed class BallVerifier : Verifier
{
protected override CheckIdentifier Identifier => CheckIdentifier.Ball;
private CheckResult NONE => GetInvalid(LBallNone);
public override void Verify(LegalityAnalysis data)
{
if (data.pkm.Format <= 2)
return; // no ball info saved
var result = VerifyBall(data);
data.AddLine(result);
}
private static int IsReplacedBall(IVersion enc, PKM pk) => pk switch
{
PK8 when enc.Version == GameVersion.PLA => (int)Poke,
_ => (int)None,
};
private CheckResult VerifyBall(LegalityAnalysis data)
{
var Info = data.Info;
2021-01-11 02:15:33 +00:00
var enc = Info.EncounterMatch;
var ball = IsReplacedBall(enc, data.pkm);
if (ball != 0)
return VerifyBallEquals(data, ball);
// Fixed ball cases -- can be only one ball ever
2021-01-11 02:15:33 +00:00
switch (enc)
{
case MysteryGift g:
return VerifyBallMysteryGift(data, g);
case EncounterTrade t:
return VerifyBallEquals(data, t.Ball);
case EncounterStatic {Gift: true} s:
return VerifyBallEquals(data, s.Ball);
case EncounterSlot8GO: // Already a strict match
return GetResult(true);
case EncounterSlot8b {IsMarsh: true}:
return VerifyBallEquals(data, (int)Safari);
}
// Capture / Inherit cases -- can be one of many balls
var pkm = data.pkm;
2021-07-30 23:22:10 +00:00
if (pkm.Species == (int)Species.Shedinja && enc.Species != (int)Species.Shedinja) // Shedinja. For gen3, copy the ball from Nincada
{
// Only Gen3 origin Shedinja can copy the wild ball.
// Evolution chains will indicate if it could have existed as Shedinja in Gen3.
// The special move verifier has a similar check!
if (pkm.HGSS && pkm.Ball == (int)Sport) // Can evolve in DP to retain the HG/SS ball -- not able to be captured in any other ball
return VerifyBallEquals(data, (int)Sport);
if (Info.Generation != 3 || Info.EvoChainsAllGens.Gen3.Length != 2)
return VerifyBallEquals(data, (int)Poke); // Pokeball Only
}
2021-01-11 02:15:33 +00:00
if (pkm.Ball == (int)Heavy && BallBreedLegality.AlolanCaptureNoHeavyBall.Contains(enc.Species) && !enc.EggEncounter && pkm.SM)
return GetInvalid(LBallHeavy); // Heavy Ball, can inherit if from egg (USUM fixed catch rate calc)
2021-01-11 02:15:33 +00:00
return enc switch
{
EncounterStatic e => VerifyBallStatic(data, e),
EncounterSlot w => VerifyBallWild(data, w),
EncounterEgg => VerifyBallEgg(data),
EncounterInvalid => VerifyBallEquals(data, pkm.Ball), // ignore ball, pass whatever
2021-08-20 20:49:20 +00:00
_ => VerifyBallEquals(data, (int)Poke),
};
}
private CheckResult VerifyBallMysteryGift(LegalityAnalysis data, MysteryGift g)
{
if (g.Generation == 4 && g.Species == (int)Species.Manaphy && g.Ball == 0) // there is no ball data in Manaphy Mystery Gift from Gen4
2018-08-26 18:29:47 +00:00
return VerifyBallEquals(data, (int)Poke); // Pokeball
return VerifyBallEquals(data, g.Ball);
}
private CheckResult VerifyBallStatic(LegalityAnalysis data, EncounterStatic s)
{
if (s.Location == 75 && s.Generation == 5) // Entree Forest (Dream World)
return VerifyBallEquals(data, BallUseLegality.DreamWorldBalls);
return VerifyBallEquals(data, BallUseLegality.GetWildBalls(data.Info.Generation, s.Version));
}
Offload EncounterSlot loading logic to reduce complexity (#2980) * Rework gen1 slot loading Slot templates are precomputed from ROM data and just loaded straight in, with tight coupling to the encounter area (grouped by slot types). * Revise fuzzy met check for underleveled wild evos Example: Level 23 poliwhirl in RBY as a level 50 poliwhirl, will assume the chain is 25-50 for poliwhirl (as poliwag evolves at 25). Instead of revising the origin chain, just ignore the evo min level in the comparison. Previous commit fixed it for gen1. * Rework gen2-4 slot loading Gen4 not finished, Type Encounter data and some edge encounters not recognizing yet... * Add feebas slots for old/good encounters * Begin moving properties Great news! Gen5-7 need to be de-dumbed like Gen1-4. Then I can remove the bang (!) on the Area accessor and ensure that it's never null! * Split off XD pokespot slot encounter table type * Set area in constructor * Deduplicate g3 roaming encounters * Deduplicate xd encounter locations (rebattle) Only difference is met location; no need to create 500 extra encounter objects. A simple contains check is ok (rarely in gen3 format). * Make all slots have a readonly reference to their parent area * Minor clean * Remove "Safari" slot type flag Can be determined via other means (generation-location), allows us to reduce the size of SlotType member to a byte Output of slot binaries didn't preserve the Safari flag anyway. * Update SlotType.cs * Handle type encounters correctly * Merge safari area into regular xy area * Merge dexnav accessor logic * fix some logic so that tests pass again rearrange g5 dw init to be done outside of static constructor (initializer instead) PIDGenerator: friend safari slots now generate with required flawless IV count * Add cianwood tentacool gift encounter * Remove unnecessary abstractions Fake area just returned a slot; since Slots have a non-null reference to the area, we can just return the slot and use the API to grab a list of possible slots for the chain. Increase restrictiveness of location/type get-set operations * Minor tweaks, pass parameters DexNav observed state isn't necessary to use, only need to see if it's possible to dexnav. Now that we have metadata for slots, we can. * Remove unused legality tables
2020-08-30 17:23:22 +00:00
private CheckResult VerifyBallWild(LegalityAnalysis data, EncounterSlot w)
{
var req = w.FixedBall;
Offload EncounterSlot loading logic to reduce complexity (#2980) * Rework gen1 slot loading Slot templates are precomputed from ROM data and just loaded straight in, with tight coupling to the encounter area (grouped by slot types). * Revise fuzzy met check for underleveled wild evos Example: Level 23 poliwhirl in RBY as a level 50 poliwhirl, will assume the chain is 25-50 for poliwhirl (as poliwag evolves at 25). Instead of revising the origin chain, just ignore the evo min level in the comparison. Previous commit fixed it for gen1. * Rework gen2-4 slot loading Gen4 not finished, Type Encounter data and some edge encounters not recognizing yet... * Add feebas slots for old/good encounters * Begin moving properties Great news! Gen5-7 need to be de-dumbed like Gen1-4. Then I can remove the bang (!) on the Area accessor and ensure that it's never null! * Split off XD pokespot slot encounter table type * Set area in constructor * Deduplicate g3 roaming encounters * Deduplicate xd encounter locations (rebattle) Only difference is met location; no need to create 500 extra encounter objects. A simple contains check is ok (rarely in gen3 format). * Make all slots have a readonly reference to their parent area * Minor clean * Remove "Safari" slot type flag Can be determined via other means (generation-location), allows us to reduce the size of SlotType member to a byte Output of slot binaries didn't preserve the Safari flag anyway. * Update SlotType.cs * Handle type encounters correctly * Merge safari area into regular xy area * Merge dexnav accessor logic * fix some logic so that tests pass again rearrange g5 dw init to be done outside of static constructor (initializer instead) PIDGenerator: friend safari slots now generate with required flawless IV count * Add cianwood tentacool gift encounter * Remove unnecessary abstractions Fake area just returned a slot; since Slots have a non-null reference to the area, we can just return the slot and use the API to grab a list of possible slots for the chain. Increase restrictiveness of location/type get-set operations * Minor tweaks, pass parameters DexNav observed state isn't necessary to use, only need to see if it's possible to dexnav. Now that we have metadata for slots, we can. * Remove unused legality tables
2020-08-30 17:23:22 +00:00
if (req != None)
return VerifyBallEquals(data, (int) req);
return VerifyBallEquals(data, BallUseLegality.GetWildBalls(data.Info.Generation, w.Version));
}
private CheckResult VerifyBallEgg(LegalityAnalysis data)
{
var pkm = data.pkm;
if (data.Info.Generation < 6) // No inheriting Balls
2018-08-26 18:29:47 +00:00
return VerifyBallEquals(data, (int)Poke); // Must be Pokéball -- no ball inheritance.
return pkm.Ball switch
{
(int)Master => GetInvalid(LBallEggMaster), // Master Ball
(int)Cherish => GetInvalid(LBallEggCherish), // Cherish Ball
2021-08-20 20:49:20 +00:00
_ => VerifyBallInherited(data),
};
}
private CheckResult VerifyBallInherited(LegalityAnalysis data) => data.Info.Generation switch
{
6 => VerifyBallEggGen6(data), // Gen6 Inheritance Rules
7 => VerifyBallEggGen7(data), // Gen7 Inheritance Rules
8 => data.pkm.BDSP ? VerifyBallEggGen8BDSP(data) : VerifyBallEggGen8(data),
2021-08-20 20:49:20 +00:00
_ => NONE,
};
private CheckResult VerifyBallEggGen6(LegalityAnalysis data)
{
var pkm = data.pkm;
if (pkm.Ball == (int)Poke)
return GetValid(LBallEnc); // Poké Ball
var enc = data.EncounterMatch;
int species = enc.Species;
if (pkm.Gender == 2 || BallBreedLegality.BreedMaleOnly6.Contains(species)) // Genderless
2018-08-26 18:29:47 +00:00
return VerifyBallEquals(data, (int)Poke); // Must be Pokéball as ball can only pass via mother (not Ditto!)
Ball ball = (Ball)pkm.Ball;
if (ball == Safari) // Safari Ball
{
if (!BallBreedLegality.Inherit_Safari.Contains(species))
return GetInvalid(LBallSpecies);
if (IsHiddenAndNotPossible(pkm))
return GetInvalid(LBallAbility);
return GetValid(LBallSpeciesPass);
}
if (ball.IsApricornBall()) // Apricorn Ball
{
if (!BallBreedLegality.Inherit_Apricorn6.Contains(species))
return GetInvalid(LBallSpecies);
if (IsHiddenAndNotPossible(pkm))
return GetInvalid(LBallAbility);
return GetValid(LBallSpeciesPass);
}
if (ball == Sport) // Sport Ball
{
if (!BallBreedLegality.Inherit_Sport.Contains(species))
return GetInvalid(LBallSpecies);
if (IsHiddenAndNotPossible(pkm))
return GetInvalid(LBallAbility);
return GetValid(LBallSpeciesPass);
}
if (ball == Dream) // Dream Ball
{
if (BallBreedLegality.Ban_DreamHidden.Contains(species) && IsHiddenAndNotPossible(pkm))
return GetInvalid(LBallAbility);
if (BallBreedLegality.Inherit_Dream.Contains(species))
return GetValid(LBallSpeciesPass);
return GetInvalid(LBallSpecies);
}
if (ball is >= Dusk and <= Quick) // Dusk Heal Quick
{
if (!BallBreedLegality.Ban_Gen4Ball_6.Contains(species))
return GetValid(LBallSpeciesPass);
return GetInvalid(LBallSpecies);
}
if (ball is >= Ultra and <= Premier) // Don't worry, Safari was already checked.
{
if (BallBreedLegality.Ban_Gen3Ball.Contains(species))
return GetInvalid(LBallSpecies);
if (BallBreedLegality.Ban_Gen3BallHidden.Contains(species | (enc.Form << 11)) && IsHiddenAndNotPossible(pkm))
return GetInvalid(LBallAbility);
return GetValid(LBallSpeciesPass);
}
if (species > 650 && species != 700) // Sylveon
{
if (BallUseLegality.WildPokeballs6.Contains(pkm.Ball))
return GetValid(LBallSpeciesPass);
return GetInvalid(LBallSpecies);
}
if (ball >= Dream)
return GetInvalid(LBallUnavailable);
return NONE;
}
private CheckResult VerifyBallEggGen7(LegalityAnalysis data)
{
var pkm = data.pkm;
if (pkm.Ball == (int)Poke)
return GetValid(LBallEnc); // Poké Ball
int species = data.EncounterMatch.Species;
if (species is >= 722 and <= 730) // G7 Starters
2018-08-26 18:29:47 +00:00
return VerifyBallEquals(data, (int)Poke);
Ball ball = (Ball)pkm.Ball;
if (ball == Safari)
{
if (!(BallBreedLegality.Inherit_Safari.Contains(species) || BallBreedLegality.Inherit_SafariMale.Contains(species)))
return GetInvalid(LBallSpecies);
if (BallBreedLegality.Ban_SafariBallHidden_7.Contains(species) && IsHiddenAndNotPossible(pkm))
return GetInvalid(LBallAbility);
return GetValid(LBallSpeciesPass);
}
if (ball.IsApricornBall()) // Apricorn Ball
{
if (!BallBreedLegality.Inherit_Apricorn7.Contains(species))
return GetInvalid(LBallSpecies);
if (BallBreedLegality.Ban_NoHidden7Apricorn.Contains(species | pkm.Form << 11) && IsHiddenAndNotPossible(pkm))
return GetInvalid(LBallAbility);
return GetValid(LBallSpeciesPass);
}
if (ball == Sport) // Sport Ball
{
if (!BallBreedLegality.Inherit_Sport.Contains(species))
return GetInvalid(LBallSpecies);
2020-12-25 20:30:26 +00:00
if ((species is (int)Species.Volbeat or (int)Species.Illumise) && IsHiddenAndNotPossible(pkm)) // Volbeat/Illumise
return GetInvalid(LBallAbility);
return GetValid(LBallSpeciesPass);
}
if (ball == Dream) // Dream Ball
{
if (BallBreedLegality.Inherit_Dream.Contains(species) || BallBreedLegality.Inherit_DreamMale.Contains(species))
return GetValid(LBallSpeciesPass);
return GetInvalid(LBallSpecies);
}
if (ball is >= Dusk and <= Quick) // Dusk Heal Quick
{
if (!BallBreedLegality.Ban_Gen4Ball_7.Contains(species))
return GetValid(LBallSpeciesPass);
return GetInvalid(LBallSpecies);
}
if (ball is >= Ultra and <= Premier) // Don't worry, Safari was already checked.
{
if (!BallBreedLegality.Ban_Gen3Ball_7.Contains(species))
return GetValid(LBallSpeciesPass);
return GetInvalid(LBallSpecies);
}
if (ball == Beast)
{
if (species == (int)Species.Flabébé && pkm.Form == 3 && IsHiddenAndNotPossible(pkm))
return GetInvalid(LBallAbility); // Can't obtain Flabébé-Blue with Hidden Ability in wild
if (species == (int)Species.Voltorb && IsHiddenAndNotPossible(pkm))
return GetInvalid(LBallAbility); // Can't obtain with Hidden Ability in wild (can only breed with Ditto)
if ((species is >= (int)Species.Pikipek and <= (int)Species.Kommoo) || (BallBreedLegality.AlolanCaptureOffspring.Contains(species) && !BallBreedLegality.PastGenAlolanNativesUncapturable.Contains(species)))
return GetValid(LBallSpeciesPass);
if (BallBreedLegality.PastGenAlolanScans.Contains(species))
return GetValid(LBallSpeciesPass);
// next statement catches all new alolans
}
if (species > (int)Species.Volcanion)
return VerifyBallEquals(data, BallUseLegality.WildPokeballs7);
if (ball > Beast)
return GetInvalid(LBallUnavailable);
return NONE;
}
private CheckResult VerifyBallEggGen8BDSP(LegalityAnalysis data)
{
int species = data.EncounterMatch.Species;
if (species == (int)Species.Phione)
return VerifyBallEquals(data, (int)Poke);
if (species is (int)Species.Cranidos or (int)Species.Shieldon)
return VerifyBallEquals(data, BallUseLegality.DreamWorldBalls);
var pkm = data.pkm;
Ball ball = (Ball)pkm.Ball;
var balls = BallUseLegality.GetWildBalls(8, GameVersion.BDSP);
if (balls.Contains((int)ball))
return GetValid(LBallSpeciesPass);
if (species is (int)Species.Spinda)
return GetInvalid(LBallSpecies); // Can't enter or exit, needs to adhere to wild balls.
// Cross-game inheritance
if (IsGalarCatchAndBreed(species))
{
if (BallUseLegality.WildPokeballs8.Contains(pkm.Ball))
return GetValid(LBallSpeciesPass);
}
if (ball == Safari)
{
if (!(BallBreedLegality.Inherit_Safari.Contains(species) || BallBreedLegality.Inherit_SafariMale.Contains(species)))
return GetInvalid(LBallSpecies);
if (BallBreedLegality.Ban_SafariBallHidden_7.Contains(species) && IsHiddenAndNotPossible(pkm))
return GetInvalid(LBallAbility);
return GetValid(LBallSpeciesPass);
}
if (ball.IsApricornBall()) // Apricorn Ball
{
if (!BallBreedLegality.Inherit_Apricorn7.Contains(species))
return GetInvalid(LBallSpecies);
if (BallBreedLegality.Ban_NoHidden8Apricorn.Contains(species | pkm.Form << 11) && IsHiddenAndNotPossible(pkm)) // lineage is 3->2->origin
return GetInvalid(LBallAbility);
return GetValid(LBallSpeciesPass);
}
if (ball == Sport) // Sport Ball
{
if (!BallBreedLegality.Inherit_Sport.Contains(species))
return GetInvalid(LBallSpecies);
if ((species is (int)Species.Volbeat or (int)Species.Illumise) && IsHiddenAndNotPossible(pkm)) // Volbeat/Illumise
return GetInvalid(LBallAbility);
return GetValid(LBallSpeciesPass);
}
if (ball == Dream) // Dream Ball
{
if (BallBreedLegality.Inherit_Dream.Contains(species) || BallBreedLegality.Inherit_DreamMale.Contains(species))
return GetValid(LBallSpeciesPass);
return GetInvalid(LBallSpecies);
}
if (ball is >= Dusk and <= Quick) // Dusk Heal Quick
{
if (!BallBreedLegality.Ban_Gen4Ball_7.Contains(species))
return GetValid(LBallSpeciesPass);
return GetInvalid(LBallSpecies);
}
if (ball is >= Ultra and <= Premier) // Don't worry, Safari was already checked.
{
if (!BallBreedLegality.Ban_Gen3Ball_7.Contains(species))
return GetValid(LBallSpeciesPass);
return GetInvalid(LBallSpecies);
}
if (ball == Beast)
{
if (BallBreedLegality.PastGenAlolanScans.Contains(species))
return GetValid(LBallSpeciesPass);
}
if (ball > Beast)
return GetInvalid(LBallUnavailable);
return GetInvalid(LBallEncMismatch);
}
private CheckResult VerifyBallEggGen8(LegalityAnalysis data)
{
var pkm = data.pkm;
if (pkm.Ball == (int)Poke)
return GetValid(LBallEnc); // Poké Ball
int species = data.EncounterMatch.Species;
if (species is >= (int)Species.Grookey and <= (int)Species.Inteleon) // G8 Starters
return VerifyBallEquals(data, (int)Poke);
if (IsGalarCatchAndBreed(species))
{
if (BallUseLegality.WildPokeballs8.Contains(pkm.Ball))
return GetValid(LBallSpeciesPass);
if (species >= (int)Species.Grookey)
return GetInvalid(LBallSpecies);
}
Ball ball = (Ball)pkm.Ball;
if (ball == Safari)
{
if (!(BallBreedLegality.Inherit_Safari.Contains(species) || BallBreedLegality.Inherit_SafariMale.Contains(species)))
return GetInvalid(LBallSpecies);
if (BallBreedLegality.Ban_SafariBallHidden_7.Contains(species) && IsHiddenAndNotPossible(pkm))
return GetInvalid(LBallAbility);
return GetValid(LBallSpeciesPass);
}
if (ball.IsApricornBall()) // Apricorn Ball
{
if (!BallBreedLegality.Inherit_Apricorn7.Contains(species))
return GetInvalid(LBallSpecies);
if (BallBreedLegality.Ban_NoHidden8Apricorn.Contains(species | pkm.Form << 11) && IsHiddenAndNotPossible(pkm)) // lineage is 3->2->origin
return GetInvalid(LBallAbility);
return GetValid(LBallSpeciesPass);
}
if (ball == Sport) // Sport Ball
{
if (!BallBreedLegality.Inherit_Sport.Contains(species))
return GetInvalid(LBallSpecies);
2020-12-25 20:30:26 +00:00
if ((species is (int)Species.Volbeat or (int)Species.Illumise) && IsHiddenAndNotPossible(pkm)) // Volbeat/Illumise
return GetInvalid(LBallAbility);
return GetValid(LBallSpeciesPass);
}
if (ball == Dream) // Dream Ball
{
if (BallBreedLegality.Inherit_Dream.Contains(species) || BallBreedLegality.Inherit_DreamMale.Contains(species))
return GetValid(LBallSpeciesPass);
return GetInvalid(LBallSpecies);
}
if (ball is >= Dusk and <= Quick) // Dusk Heal Quick
{
if (!BallBreedLegality.Ban_Gen4Ball_7.Contains(species))
return GetValid(LBallSpeciesPass);
return GetInvalid(LBallSpecies);
}
if (ball is >= Ultra and <= Premier) // Don't worry, Safari was already checked.
{
if (!BallBreedLegality.Ban_Gen3Ball_7.Contains(species))
return GetValid(LBallSpeciesPass);
return GetInvalid(LBallSpecies);
}
if (ball == Beast)
{
if (species == (int)Species.Flabébé && pkm.Form == 3 && IsHiddenAndNotPossible(pkm))
return GetInvalid(LBallAbility); // Can't obtain Flabébé-Blue with Hidden Ability in wild
if ((species is >= (int)Species.Pikipek and <= (int)Species.Kommoo) || (BallBreedLegality.AlolanCaptureOffspring.Contains(species) && !BallBreedLegality.PastGenAlolanNativesUncapturable.Contains(species)))
return GetValid(LBallSpeciesPass);
if (BallBreedLegality.PastGenAlolanScans.Contains(species))
return GetValid(LBallSpeciesPass);
// next statement catches all new alolans
}
if (species > Legal.MaxSpeciesID_7_USUM)
return VerifyBallEquals(data, BallUseLegality.WildPokeballs8);
if (species > (int)Species.Volcanion)
return VerifyBallEquals(data, BallUseLegality.WildPokeballs7);
if (ball > Beast)
return GetInvalid(LBallUnavailable);
return NONE;
}
private static bool IsHiddenAndNotPossible(PKM pkm)
{
if (pkm.AbilityNumber != 4)
return false;
return !AbilityVerifier.CanAbilityPatch(pkm.Format, pkm.PersonalInfo.Abilities, pkm.Species);
}
2020-10-25 17:02:52 +00:00
private static bool IsGalarCatchAndBreed(int species)
{
if (species is >= (int)Species.Grookey and <= (int)Species.Inteleon) // starter
return false;
2020-10-25 17:02:52 +00:00
// Everything breed-able that is in the Galar Dex can be captured in-game.
var pt = PersonalTable.SWSH;
var pi = (PersonalInfoSWSH) pt.GetFormEntry(species, 0);
if (pi.IsInDex)
return true;
// Foreign Captures
if (species is >= (int)Species.Treecko and <= (int)Species.Swampert) // Dynamax Adventures
return true;
2021-10-01 00:12:54 +00:00
if (species is >= (int)Species.Rowlet and <= (int)Species.Primarina) // Distribution Raids
return true;
return false;
}
private CheckResult VerifyBallEquals(LegalityAnalysis data, int ball) => GetResult(ball == data.pkm.Ball);
private CheckResult VerifyBallEquals(LegalityAnalysis data, HashSet<int> balls) => GetResult(balls.Contains(data.pkm.Ball));
private CheckResult VerifyBallEquals(LegalityAnalysis data, ICollection<int> balls) => GetResult(balls.Contains(data.pkm.Ball));
private CheckResult GetResult(bool valid) => valid ? GetValid(LBallEnc) : GetInvalid(LBallEncMismatch);
}
}