PKHeX/PKHeX.Core/Legality/Verifiers/FormVerifier.cs

401 lines
16 KiB
C#
Raw Normal View History

using System;
using static PKHeX.Core.LegalityCheckStrings;
2021-01-11 02:15:33 +00:00
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;
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);
if (pk is IFormArgument f)
data.AddLine(VerifyFormArgument(data, f));
}
private CheckResult VALID => GetValid(LFormValid);
private CheckResult VerifyForm(LegalityAnalysis data)
{
var pk = data.Entity;
var PersonalInfo = data.PersonalInfo;
int count = PersonalInfo.FormCount;
var form = pk.Form;
if (count <= 1 && form == 0)
return VALID; // no forms to check
var species = pk.Species;
var enc = data.EncounterMatch;
var Info = data.Info;
if (!PersonalInfo.IsFormWithinRange(form) && !FormInfo.IsValidOutOfBoundsForm(species, form, Info.Generation))
return GetInvalid(string.Format(LFormInvalidRange, count - 1, form));
switch ((Species)species)
{
case Pikachu when Info.Generation == 6: // Cosplay
bool isStatic = enc is EncounterStatic6;
bool validCosplay = form == (isStatic ? enc.Form : 0);
if (!validCosplay)
return GetInvalid(isStatic ? LFormPikachuCosplayInvalid : LFormPikachuCosplay);
break;
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(LFormBattle);
case Pikachu when Info.Generation >= 7: // Cap
bool validCap = form == (enc is EncounterInvalid or EncounterEgg ? 0 : enc.Form);
if (!validCap)
{
bool gift = enc is MysteryGift g && g.Form != form;
var msg = gift ? LFormPikachuEventInvalid : LFormInvalidGame;
return GetInvalid(msg);
}
break;
case Unown when Info.Generation == 2 && form >= 26:
return GetInvalid(string.Format(LFormInvalidRange, "Z", form == 26 ? "!" : "?"));
2022-09-26 14:27:51 +00:00
case Unown when Info.Generation == 3 && form != EntityPID.GetUnownForm3(pk.EncryptionConstant):
return GetInvalid(string.Format(LFormInvalidExpect_0, EntityPID.GetUnownForm3(pk.EncryptionConstant)));
case Dialga or Palkia or Giratina or Arceus when form > 0 && pk is PA8: // can change forms with key items
break;
case Giratina when form == 1 ^ pk.HeldItem == 112: // Giratina, Origin form only with Griseous Orb
return GetInvalid(LFormItemInvalid);
case Arceus:
{
int arceus = GetArceusFormFromHeldItem(pk.HeldItem, pk.Format);
return arceus != form ? GetInvalid(LFormItemInvalid) : GetValid(LFormItem);
}
case Keldeo when enc.Generation != 5 || pk.Format >= 8:
// can mismatch in gen5 via BW 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(LMoveKeldeoMismatch);
break;
case Genesect:
{
int genesect = GetGenesectFormFromHeldItem(pk.HeldItem);
return genesect != form ? GetInvalid(LFormItemInvalid) : GetValid(LFormItem);
}
case Greninja:
if (form > 1) // Ash Battle Bond active
return GetInvalid(LFormBattle);
Refactoring: Move Source (Legality) (#3560) Rewrites a good amount of legality APIs pertaining to: * Legal moves that can be learned * Evolution chains & cross-generation paths * Memory validation with forgotten moves In generation 8, there are 3 separate contexts an entity can exist in: SW/SH, BD/SP, and LA. Not every entity can cross between them, and not every entity from generation 7 can exist in generation 8 (Gogoat, etc). By creating class models representing the restrictions to cross each boundary, we are able to better track and validate data. The old implementation of validating moves was greedy: it would iterate for all generations and evolutions, and build a full list of every move that can be learned, storing it on the heap. Now, we check one game group at a time to see if the entity can learn a move that hasn't yet been validated. End result is an algorithm that requires 0 allocation, and a smaller/quicker search space. The old implementation of storing move parses was inefficient; for each move that was parsed, a new object is created and adjusted depending on the parse. Now, move parse results are `struct` and store the move parse contiguously in memory. End result is faster parsing and 0 memory allocation. * `PersonalTable` objects have been improved with new API methods to check if a species+form can exist in the game. * `IEncounterTemplate` objects have been improved to indicate the `EntityContext` they originate in (similar to `Generation`). * Some APIs have been extended to accept `Span<T>` instead of Array/IEnumerable
2022-08-03 23:15:27 +00:00
if (form != 0 && enc is not MysteryGift) // Forms are not breedable, MysteryGift already checked
return GetInvalid(string.Format(LFormInvalidRange, 0, form));
break;
case Scatterbug or Spewpa:
if (form > Vivillon3DS.MaxWildFormID) // Fancy & Pokéball
return GetInvalid(LFormVivillonEventPre);
if (pk is not IRegionOrigin tr)
break;
if (!Vivillon3DS.IsPatternValid(form, tr.ConsoleRegion))
return GetInvalid(LFormVivillonInvalid);
if (!Vivillon3DS.IsPatternNative(form, tr.Country, tr.Region))
data.AddLine(Get(LFormVivillonNonNative, Severity.Fishy));
break;
case Vivillon:
if (form > Vivillon3DS.MaxWildFormID) // Fancy & Pokéball
{
if (enc is not MysteryGift)
return GetInvalid(LFormVivillonInvalid);
return GetValid(LFormVivillon);
}
if (pk is not IRegionOrigin trv)
break;
if (!Vivillon3DS.IsPatternValid(form, trv.ConsoleRegion))
return GetInvalid(LFormVivillonInvalid);
if (!Vivillon3DS.IsPatternNative(form, trv.Country, trv.Region))
data.AddLine(Get(LFormVivillonNonNative, Severity.Fishy));
break;
case Floette when form == 5: // Floette Eternal Flower -- Never Released
if (enc is not MysteryGift)
return GetInvalid(LFormEternalInvalid);
return GetValid(LFormEternal);
case Meowstic when form != pk.Gender:
return GetInvalid(LGenderInvalidNone);
case Silvally:
{
int silvally = GetSilvallyFormFromHeldItem(pk.HeldItem);
return silvally != form ? GetInvalid(LFormItemInvalid) : GetValid(LFormItem);
}
// 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(LFormInvalidGame);
// 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 != EvolutionMethod.GetAmpLowKeyResult(pk.Nature))
return GetInvalid(LFormInvalidNature);
break;
}
// Impossible Egg forms
case Rotom when pk.IsEgg && form != 0:
case Furfrou when pk.IsEgg && form != 0:
return GetInvalid(LEggSpecies);
// Party Only Forms
case Shaymin:
case Furfrou:
case Hoopa:
if (form != 0 && data.SlotOrigin is not SlotOrigin.Party && pk.Format <= 6) // has form but stored in box
return GetInvalid(LFormParty);
break;
}
var format = pk.Format;
if (FormInfo.IsBattleOnlyForm(species, form, format))
return GetInvalid(LFormBattle);
if (form == 0)
return VALID;
// everything below here is not Form 0, so it has a form.
if (format >= 7 && Info.Generation < 7)
{
if (species == 25 || Legal.AlolanOriginForms.Contains(species) || Legal.AlolanVariantEvolutions12.Contains(enc.Species))
return GetInvalid(LFormInvalidGame);
}
if (format >= 8 && Info.Generation < 8)
{
var orig = enc.Species;
if (Legal.GalarOriginForms.Contains(species) || Legal.GalarVariantFormEvolutions.Contains(orig))
{
if (species == (int)Meowth && enc.Form != 2)
{
// We're okay here. There's also Alolan Meowth...
}
else if (((Species)orig is MrMime or MimeJr) && pk.CurrentLevel > enc.LevelMin && Info.Generation >= 4)
{
// We're okay with a Mime Jr. that has evolved via level up.
}
else if (enc.Version != GameVersion.GO)
{
return GetInvalid(LFormInvalidGame);
}
}
}
return VALID;
}
private static readonly ushort[] Arceus_PlateIDs = { 303, 306, 304, 305, 309, 308, 310, 313, 298, 299, 301, 300, 307, 302, 311, 312, 644 };
private static readonly ushort[] Arceus_ZCrystal = { 782, 785, 783, 784, 788, 787, 789, 792, 777, 778, 780, 779, 786, 781, 790, 791, 793 };
2022-05-07 18:47:01 +00:00
public static byte GetArceusFormFromHeldItem(int item, int format) => item switch
{
(>= 777 and <= 793) => GetArceusFormFromZCrystal(item),
(>= 298 and <= 313) or 644 => GetArceusFormFromPlate(item, format),
_ => 0,
};
2022-05-07 18:47:01 +00:00
private static byte GetArceusFormFromZCrystal(int item)
{
return (byte)(Array.IndexOf(Arceus_ZCrystal, (ushort)item) + 1);
}
private static byte GetArceusFormFromPlate(int item, int format)
{
byte form = (byte)(Array.IndexOf(Arceus_PlateIDs, (ushort)item) + 1);
if (format != 4) // No need to consider Curse type
return form;
if (form < 9)
return form;
return ++form; // ??? type Form shifts everything by 1
}
2022-08-31 05:49:54 +00:00
public static byte GetSilvallyFormFromHeldItem(int item)
{
if (item is >= 904 and <= 920)
2022-08-31 05:49:54 +00:00
return (byte)(item - 903);
return 0;
}
2022-08-31 05:49:54 +00:00
public static byte GetGenesectFormFromHeldItem(int item)
{
if (item is >= 116 and <= 119)
2022-08-31 05:49:54 +00:00
return (byte)(item - 115);
return 0;
}
private CheckResult VerifyFormArgument(LegalityAnalysis data, IFormArgument f)
{
var pk = data.Entity;
var enc = data.EncounterMatch;
var arg = f.FormArgument;
2021-02-06 00:52:46 +00:00
var unusedMask = pk.Format == 6 ? 0xFFFF_FF00 : 0xFF00_0000;
if ((arg & unusedMask) != 0)
return GetInvalid(LFormArgumentHigh);
2021-02-06 01:25:13 +00:00
return (Species)pk.Species switch
2021-02-06 01:25:13 +00:00
{
// Transfer Edge Cases -- Bank wipes the form but keeps old FormArgument value.
Furfrou when pk.Format == 7 && pk.Form == 0 &&
((enc.Generation == 6 && f.FormArgument <= byte.MaxValue) || IsFormArgumentDayCounterValid(f, 5, true))
=> GetValid(LFormArgumentValid),
Furfrou when pk.Form != 0 => !IsFormArgumentDayCounterValid(f, 5, true) ? GetInvalid(LFormArgumentInvalid) :GetValid(LFormArgumentValid),
Hoopa when pk.Form == 1 => !IsFormArgumentDayCounterValid(f, 3) ? GetInvalid(LFormArgumentInvalid) : GetValid(LFormArgumentValid),
Yamask when pk.Form == 1 => arg switch
2021-02-09 07:18:45 +00:00
{
not 0 when pk.IsEgg => GetInvalid(LFormArgumentNotAllowed),
> 9_999 => GetInvalid(LFormArgumentHigh),
_ => GetValid(LFormArgumentValid),
},
Runerigus when enc.Species == (int)Runerigus => arg switch
{
not 0 => GetInvalid(LFormArgumentNotAllowed),
_ => GetValid(LFormArgumentValid),
},
Runerigus => arg switch // From Yamask-1
{
< 49 => GetInvalid(LFormArgumentLow),
> 9_999 => GetInvalid(LFormArgumentHigh),
_ => GetValid(LFormArgumentValid),
},
Alcremie when enc.Species == (int)Alcremie => arg switch
{
not 0 => GetInvalid(LFormArgumentNotAllowed),
_ => GetValid(LFormArgumentValid),
},
Alcremie => arg switch // From Milcery
{
> (uint) AlcremieDecoration.Ribbon => GetInvalid(LFormArgumentHigh),
_ => GetValid(LFormArgumentValid),
},
Overqwil when enc.Species == (int)Overqwil => arg switch
{
not 0 => GetInvalid(LFormArgumentNotAllowed),
_ => GetValid(LFormArgumentValid),
},
Wyrdeer when enc.Species == (int)Wyrdeer => arg switch
{
not 0 => GetInvalid(LFormArgumentNotAllowed),
_ => GetValid(LFormArgumentValid),
},
Basculegion when enc.Species == (int)Basculegion => arg switch
{
not 0 => GetInvalid(LFormArgumentNotAllowed),
_ => GetValid(LFormArgumentValid),
},
Basculin when pk.Form is 2 => arg switch
{
not 0 when pk.IsEgg => GetInvalid(LFormArgumentNotAllowed),
> 9_999 => GetInvalid(LFormArgumentHigh),
_ => GetValid(LFormArgumentValid),
},
Qwilfish when pk.Form is 1 => arg switch
{
not 0 when pk.IsEgg => GetInvalid(LFormArgumentNotAllowed),
not 0 when pk.CurrentLevel < 25 => GetInvalid(LFormArgumentHigh),
> 9_999 => GetInvalid(LFormArgumentHigh),
_ => GetValid(LFormArgumentValid),
},
Stantler when pk is PA8 || data.Info.EvoChainsAllGens.HasVisitedPLA => arg switch
{
not 0 when pk.IsEgg => GetInvalid(LFormArgumentNotAllowed),
not 0 when pk.CurrentLevel < 31 => GetInvalid(LFormArgumentHigh),
> 9_999 => GetInvalid(LFormArgumentHigh),
_ => GetValid(LFormArgumentValid),
},
Wyrdeer => arg switch // From Stantler
{
< 20 => GetInvalid(LFormArgumentLow),
> 9_999 => GetInvalid(LFormArgumentHigh),
_ => GetValid(LFormArgumentValid),
},
Overqwil => arg switch // From Qwilfish-1
{
< 20 => GetInvalid(LFormArgumentLow),
> 9_999 => GetInvalid(LFormArgumentHigh),
_ => GetValid(LFormArgumentValid),
},
Basculegion => arg switch // From Basculin-2
{
< 294 => GetInvalid(LFormArgumentLow),
> 9_999 => GetInvalid(LFormArgumentHigh),
_ => GetValid(LFormArgumentValid),
},
_ => VerifyFormArgumentNone(pk, f),
};
}
2021-02-09 07:18:45 +00:00
private CheckResult VerifyFormArgumentNone(PKM pk, IFormArgument f)
{
if (pk is not PK6 pk6)
{
2021-02-09 07:18:45 +00:00
if (f.FormArgument != 0)
{
if (pk.Species == (int)Furfrou && pk.Form == 0 && (f.FormArgument & ~0xFF_00_00u) == 0)
2021-02-09 07:18:45 +00:00
return GetValid(LFormArgumentValid);
return GetInvalid(LFormArgumentNotAllowed);
}
return GetValid(LFormArgumentValid);
}
if (f.FormArgument != 0)
{
if (pk.Species == (int)Furfrou && pk.Form == 0 && (f.FormArgument & ~0xFFu) == 0)
return GetValid(LFormArgumentValid);
return GetInvalid(LFormArgumentNotAllowed);
}
// Stored separately from main form argument value
if (pk6.FormArgumentRemain != 0)
return GetInvalid(LFormArgumentNotAllowed);
if (pk6.FormArgumentElapsed != 0)
return GetInvalid(LFormArgumentNotAllowed);
return GetValid(LFormArgumentValid);
}
private static bool IsFormArgumentDayCounterValid(IFormArgument f, uint maxSeed, bool canRefresh = false)
{
var remain = f.FormArgumentRemain;
var elapsed = f.FormArgumentElapsed;
var maxElapsed = f.FormArgumentMaximum;
if (canRefresh)
{
if (maxElapsed < elapsed)
return false;
if (remain + elapsed < maxSeed)
return false;
}
else
{
if (maxElapsed != 0)
return false;
if (remain + elapsed != maxSeed)
2021-02-06 01:25:13 +00:00
return false;
}
if (remain > maxSeed)
return false;
return remain != 0;
}
}