PKHeX/PKHeX.Core/Legality/Verifiers/FormVerifier.cs
Kurt 6609dd210b Misc legality fixes for Z-A alterations
- Evolving knowing move: relearnable additions in the evolved stage was bypassing the requirement (Sylveon can relearn Charm at any level, but Eevee cannot). Prune tree to only check if pre-evolutions could have learned move.
- FormArgument requiring a minimum level to actually use the move (Primeape). Probably isn't a "complete" check, since it's implemented differently compared to Qwilfish's logic. Might be worth revising in the future to be consistent (using the same as Primeape logic? if in game, and can learn, can increase from 0).
- Flag Hangry Morpeko if cannot learn Aura Wheel yet
- Flag mega evo mismatches for Tatsugiri/Magearna/Meowstic
- Permit mega meowstic gender in party

- Remap DLC TMs (I forgot this remapping was needed; pkNX dumped it but I didn't update the table until now...)
2025-12-20 14:44:08 -06:00

216 lines
9.9 KiB
C#

using static PKHeX.Core.LegalityCheckResultCode;
using static PKHeX.Core.Species;
namespace PKHeX.Core;
/// <summary>
/// Verifies the <see cref="PKM.Form"/> value.
/// </summary>
public sealed class FormVerifier : Verifier
{
protected override CheckIdentifier Identifier => CheckIdentifier.Form;
private static readonly FormArgumentVerifier FormArg = new();
public override void Verify(LegalityAnalysis data)
{
var pk = data.Entity;
if (pk.Format < 4)
return; // no forms exist
var result = VerifyForm(data);
data.AddLine(result);
FormArg.Verify(data);
}
private CheckResult VALID => GetValid(FormValid);
private CheckResult VerifyForm(LegalityAnalysis data)
{
var pk = data.Entity;
var pi = data.PersonalInfo;
int count = pi.FormCount;
var form = pk.Form;
if (count <= 1 && form == 0)
return VALID; // no forms to check
var species = pk.Species;
var enc = data.EncounterMatch;
if (!pi.IsFormWithinRange(form) && !FormInfo.IsValidOutOfBoundsForm(species, form, enc.Generation))
return GetInvalid(FormInvalidRangeLEQ_0F, (ushort)(count - 1));
switch ((Species)species)
{
case Pikachu when enc.Generation == 6: // Cosplay
if (enc is not EncounterStatic6 s6)
{
if (form == 0)
break; // Regular Pikachu, OK.
return GetInvalid(FormPikachuCosplay);
}
if (form != s6.Form)
return GetInvalid(FormPikachuCosplayInvalid);
if (pk.Format != 6)
return GetInvalid(TransferBad); // Can't transfer.
break;
// LGP/E: Can't get the other game's Starter form.
case Pikachu when form is not 0 && ParseSettings.ActiveTrainer is SAV7b {Version:GameVersion.GE}:
case Eevee when form is not 0 && ParseSettings.ActiveTrainer is SAV7b {Version:GameVersion.GP}:
return GetInvalid(FormBattle);
case Pikachu when enc.Generation >= 7: // Cap
var expectForm = enc is EncounterInvalid or IEncounterEgg ? 0 : enc.Form;
if (form != expectForm)
{
bool gift = enc is MysteryGift g && g.Form != form;
var msg = gift ? FormPikachuEventInvalid : FormInvalidGame;
return GetInvalid(msg);
}
break;
case Unown when enc.Generation == 2 && form >= 26:
return GetInvalid(FormInvalidRangeLEQ_0F, 25);
case Unown when enc.Generation == 3:
var expectUnown = EntityPID.GetUnownForm3(pk.EncryptionConstant);
if (expectUnown != form)
return GetInvalid(FormInvalidExpect_0, expectUnown);
break;
case Dialga or Palkia or Giratina or Arceus when form > 0 && pk is PA8: // can change forms with key items
break;
case Dialga when pk.Format >= 9 && ((form == 1) != (pk.HeldItem == 1777)): // Origin Forme Dialga with Adamant Crystal
case Palkia when pk.Format >= 9 && ((form == 1) != (pk.HeldItem == 1778)): // Origin Forme Palkia with Lustrous Globe
case Giratina when pk.Format >= 9 && ((form == 1) != (pk.HeldItem == 1779)): // Origin Forme Giratina with Griseous Core
case Giratina when pk.Format <= 8 && ((form == 1) != (pk.HeldItem == 0112)): // Origin Forme Giratina with Griseous Orb
return GetInvalid(FormItemInvalid);
case Arceus:
var arceus = FormItem.GetFormArceus(pk.HeldItem, pk.Format);
return arceus != form ? GetInvalid(FormItemInvalid) : GetValid(FormItemMatches);
case Keldeo when enc.Generation != 5 || pk.Format >= 8:
// can mismatch in Gen5 via B/W tutor and transfer up
// can mismatch in Gen8+ as the form activates in battle when knowing the move; outside of battle can be either state.
// Generation 8 patched out the mismatch; always forced to match moves.
bool hasSword = pk.HasMove((int) Move.SecretSword);
bool isSword = pk.Form == 1;
if (isSword != hasSword)
return GetInvalid(MoveKeldeoMismatch);
break;
case Genesect:
var genesect = FormItem.GetFormGenesect(pk.HeldItem);
return genesect != form ? GetInvalid(FormItemInvalid) : GetValid(FormItemMatches);
case Greninja:
if (form > 1) // Ash Battle Bond active
return GetInvalid(FormBattle);
if (form != 0 && enc is not MysteryGift) // Form can not be bred for, MysteryGift already checked
return GetInvalid(FormInvalidRangeLEQ_0F, 0);
break;
case Scatterbug or Spewpa or Vivillon when enc.Context is EntityContext.Gen9:
if (form > 18 && enc.Form != form) // Pokéball
return GetInvalid(FormVivillonEventPre);
if (form != 18 && enc is IEncounterEgg) // Fancy
return GetInvalid(FormVivillonNonNative);
break;
case Scatterbug or Spewpa:
if (form > Vivillon3DS.MaxWildFormID) // Fancy & Pokéball
return GetInvalid(FormVivillonEventPre);
if (pk is not IRegionOrigin tr)
break;
if (!Vivillon3DS.IsPatternValid(form, tr.ConsoleRegion))
return GetInvalid(FormVivillonInvalid);
if (!Vivillon3DS.IsPatternNative(form, tr.Country, tr.Region))
data.AddLine(Get(Severity.Fishy, FormVivillonNonNative));
break;
case Vivillon:
if (form > Vivillon3DS.MaxWildFormID) // Fancy & Pokéball
{
if (enc is not MysteryGift)
return GetInvalid(FormVivillonInvalid);
return GetValid(FormVivillon);
}
if (pk is not IRegionOrigin trv)
break;
if (!Vivillon3DS.IsPatternValid(form, trv.ConsoleRegion))
return GetInvalid(FormVivillonInvalid);
if (!Vivillon3DS.IsPatternNative(form, trv.Country, trv.Region))
data.AddLine(Get(Severity.Fishy, FormVivillonNonNative));
break;
case Floette when form == 5: // Eternal Flower Floette - not released until Pokémon Legends: Z-A
if (enc is not EncounterGift9a)
return GetInvalid(FormEternalInvalid);
return GetValid(FormEternal);
case Meowstic when (form & 1) != pk.Gender:
return GetInvalid(GenderInvalidNone);
case Silvally:
var silvally = FormItem.GetFormSilvally(pk.HeldItem);
return silvally != form ? GetInvalid(FormItemInvalid) : GetValid(FormItemMatches);
// Form doesn't exist in SM; cannot originate from that game.
case Rockruff when enc.Generation == 7 && form == 1 && pk.SM:
case Lycanroc when enc.Generation == 7 && form == 2 && pk.SM:
return GetInvalid(FormInvalidGame);
// Toxel encounters have already been checked for the nature-specific evolution criteria.
case Toxtricity when enc.Species == (int)Toxtricity:
// The game enforces the Nature for Toxtricity encounters too!
if (pk.Form != ToxtricityUtil.GetAmpLowKeyResult(pk.Nature))
return GetInvalid(FormInvalidNature);
break;
// Ogerpon's form changes depending on its held mask
case Ogerpon when (form & 3) != FormItem.GetFormOgerpon(pk.HeldItem):
return GetInvalid(FormItemInvalid);
// Impossible Egg forms
case Rotom when pk.IsEgg && form != 0:
case Furfrou when pk.IsEgg && form != 0:
return GetInvalid(EggSpecies);
// Party Only Forms
case Shaymin:
case Furfrou:
case Hoopa:
if (form != 0 && !data.IsStoredSlot(StorageSlotType.Party) && pk.Format <= 6) // has form but stored in box
return GetInvalid(FormParty);
break;
}
var format = pk.Format;
if (!FormInfo.IsBattleOnlyForm(species, form, format))
return VALID;
if (pk.Context is EntityContext.Gen9a)
return VerifyBattleForms9a(data, species, form);
return GetInvalid(FormBattle);
}
private CheckResult VerifyBattleForms9a(LegalityAnalysis data, ushort species, byte form)
{
if (!data.IsStoredSlot(StorageSlotType.Party))
return GetInvalid(FormBattle); // Should have reverted to base form when stored.
// Battle forms can exist in Party.
if (!FormInfo.IsMegaForm(species, form) && !FormInfo.IsPrimalForm(species, form))
{
// Morpeko-1 can exist in party, but only if it's at a level where it can know the move Aura Wheel; only learns starting at Lv. 57.
// You can use move menu to forget Aura Wheel while it's in Hangry Mode, but the form doesn't revert, kinda like Mega Rayquaza with Dragon Ascent
if (species is (int)Morpeko && !MemoryPermissions.GetCanKnowMove(data.EncounterMatch, (ushort)Move.AuraWheel, data.Info.EvoChainsAllGens, data.Entity, LearnGroupUtil.GetCurrentGroup(data.Entity)))
return GetInvalid(FormInvalidExpect_0, 0);
return VALID;
}
var megaStone = ItemStorage9ZA.GetExpectedMegaStoneOrPrimalOrb(species, form);
if (megaStone == 0 || data.Entity.HeldItem == megaStone)
return VALID;
return GetInvalid(FormBattle);
}
}