diff --git a/PKHeX.Core/Legality/Analysis.cs b/PKHeX.Core/Legality/Analysis.cs index 9f063f5f7..334659a07 100644 --- a/PKHeX.Core/Legality/Analysis.cs +++ b/PKHeX.Core/Legality/Analysis.cs @@ -12,19 +12,19 @@ namespace PKHeX.Core /// public partial class LegalityAnalysis { - private readonly PKM pkm; + internal readonly PKM pkm; private readonly bool Error; private readonly List Parse = new List(); private IEncounterable EncounterOriginalGB; - private IEncounterable EncounterMatch => Info.EncounterMatch; + public IEncounterable EncounterMatch => Info.EncounterMatch; public IEncounterable EncounterOriginal => EncounterOriginalGB ?? EncounterMatch; - private CheckResult Encounter, History; + public CheckResult Encounter; public readonly bool Parsed; public readonly bool Valid; - private readonly PersonalInfo PersonalInfo; + public readonly PersonalInfo PersonalInfo; public LegalInfo Info { get; private set; } public string Report(bool verbose = false) => verbose ? GetVerboseLegalityReport() : GetLegalityReport(); private IEnumerable AllSuggestedMoves @@ -134,11 +134,10 @@ private void ParsePK1() UpdateInfo(); UpdateTypeInfo(); VerifyNickname(); - VerifyDVs(); VerifyEVs(); VerifyLevelG1(); - VerifyOTG1(); - VerifyMiscG1(); + Trainer.VerifyOTG1(this); + Misc.VerifyMiscG1(this); if (pkm.Format == 2) VerifyItem(); } @@ -147,7 +146,7 @@ private void ParsePK3() UpdateInfo(); UpdateChecks(); if (pkm.Format > 3) - VerifyTransferLegalityG3(); + Transfer.VerifyTransferLegalityG3(this); if (pkm.Version == (int)GameVersion.CXD) VerifyCXD(); @@ -160,7 +159,7 @@ private void ParsePK4() UpdateInfo(); UpdateChecks(); if (pkm.Format > 4) - VerifyTransferLegalityG4(); + Transfer.VerifyTransferLegalityG4(this); } private void ParsePK5() { @@ -180,8 +179,8 @@ private void ParsePK7() UpdateChecks(); } - private void AddLine(Severity s, string c, CheckIdentifier i) => AddLine(new CheckResult(s, c, i)); - private void AddLine(CheckResult chk) => Parse.Add(chk); + public void AddLine(Severity s, string c, CheckIdentifier i) => AddLine(new CheckResult(s, c, i)); + public void AddLine(CheckResult chk) => Parse.Add(chk); private void UpdateVCTransferInfo() { @@ -192,7 +191,7 @@ private void UpdateVCTransferInfo() if (!(Info.EncounterMatch is EncounterStatic s) || !EncounterStaticGenerator.IsVCStaticTransferEncounterValid(pkm, s)) { AddLine(Severity.Invalid, V80, CheckIdentifier.Encounter); return; } - foreach (var z in VerifyVCEncounter(pkm, EncounterOriginalGB, s, Info.Moves)) + foreach (var z in TransferVerifier.VerifyVCEncounter(pkm, EncounterOriginalGB, s, Info.Moves)) AddLine(z); } private void UpdateInfo() @@ -249,14 +248,13 @@ private void UpdateChecks() VerifyEncounterType(); // Gen 6->7 transfer deletes encounter type data if (pkm.Format >= 6) { - History = VerifyHistory(); - AddLine(History); - VerifyOTMemory(); - VerifyHTMemory(); + VerifyHistory(); + Memory.VerifyOTMemory(this); + Memory.VerifyHTMemory(this); VerifyHyperTraining(); VerifyMedals(); VerifyConsoleRegion(); - VerifyVersionEvolution(); + Misc.VerifyVersionEvolution(this); } } private string GetLegalityReport() diff --git a/PKHeX.Core/Legality/Checks.cs b/PKHeX.Core/Legality/Checks.cs index 7a1daecfa..94b5bb8b9 100644 --- a/PKHeX.Core/Legality/Checks.cs +++ b/PKHeX.Core/Legality/Checks.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using static PKHeX.Core.LegalityCheckStrings; @@ -7,2488 +6,65 @@ namespace PKHeX.Core { public partial class LegalityAnalysis { - private void VerifyGender() - { - var pi = pkm.PersonalInfo; - if (pi.Genderless != (pkm.Gender == 2)) - { - // DP/HGSS shedinja glitch -- only generation 4 spawns - bool ignore = pkm.Format == 4 && pkm.Species == 292 && pkm.Met_Level != pkm.CurrentLevel; - if (!ignore) - AddLine(Severity.Invalid, V203, CheckIdentifier.Gender); - return; - } + private static readonly Verifier Language = new LanguageVerifier(); + private static readonly Verifier Nickname = new NicknameVerifier(); + private static readonly Verifier EffortValues = new EffortValueVerifier(); + private static readonly Verifier IndividualValues = new IndividualValueVerifier(); + private static readonly Verifier Ball = new BallVerifier(); + private static readonly Verifier Form = new FormVerifier(); + private static readonly Verifier ConsoleRegion = new ConsoleRegionVerifier(); + private static readonly Verifier Ability = new AbilityVerifier(); + private static readonly Verifier Medal = new MedalVerifier(); + private static readonly Verifier Ribbon = new RibbonVerifier(); + private static readonly Verifier Item = new ItemVerifier(); + private static readonly Verifier EncounterType = new EncounterTypeVerifier(); + private static readonly Verifier HyperTraining = new HyperTrainingVerifier(); + private static readonly Verifier Gender = new GenderVerifier(); + private static readonly Verifier PIDEC = new PIDVerifier(); + private static readonly Verifier NHarmonia = new NHarmoniaVerifier(); + private static readonly Verifier CXD = new CXDVerifier(); - // Check for PID relationship to Gender & Nature if applicable - int gen = Info.Generation; + private static readonly TrainerNameVerifier Trainer = new TrainerNameVerifier(); + private static readonly MemoryVerifier Memory = new MemoryVerifier(); + private static readonly LevelVerifier Level = new LevelVerifier(); + private static readonly MiscVerifier Misc = new MiscVerifier(); + private static readonly TransferVerifier Transfer = new TransferVerifier(); - if (3 <= gen && gen <= 5) - { - // Gender-PID & Nature-PID relationship check - if (IsValidGenderPID()) - AddLine(Severity.Valid, V250, CheckIdentifier.Gender); - else - AddLine(Severity.Invalid, V251, CheckIdentifier.Gender); - - if (gen != 5) - VerifyNaturePID(); - return; - } - - // Check fixed gender cases - if ((pi.OnlyFemale && pkm.Gender != 1) || (pi.OnlyMale && pkm.Gender != 0)) - AddLine(Severity.Invalid, V203, CheckIdentifier.Gender); - } - private bool IsValidGenderPID() - { - bool genderValid = pkm.IsGenderValid(); - if (!genderValid) - { - if (pkm.Format == 4 && pkm.Species == 292) // Shedinja glitch - { - // should match original gender - var gender = PKX.GetGenderFromPIDAndRatio(pkm.PID, 0x7F); // 50M-50F - if (gender == pkm.Gender) - return true; - } - else if (pkm.Format > 5 && (pkm.Species == 183 || pkm.Species == 184)) // Azurill/Marill Gender Ratio Change - { - var gv = pkm.PID & 0xFF; - if (gv > 63 && pkm.Gender == 1) // evolved from azurill after transferring to keep gender - return true; - } - return false; - } - - // check for mixed->fixed gender incompatibility by checking the gender of the original species - if (Legal.FixedGenderFromBiGender.Contains(EncounterMatch.Species) && pkm.Gender != 2) // shedinja - { - var gender = PKX.GetGenderFromPID(EncounterMatch.Species, pkm.EncryptionConstant); - if (gender != pkm.Gender) // gender must not be different from original - return false; - } - return true; - } - private void VerifyNaturePID() - { - if (pkm.EncryptionConstant % 25 == pkm.Nature) - AddLine(Severity.Valid, V252, CheckIdentifier.Nature); - else - AddLine(Severity.Invalid, V253, CheckIdentifier.Nature); - } - private void VerifyItem() - { - if (!Legal.IsHeldItemAllowed(pkm)) - AddLine(Severity.Invalid, V204, CheckIdentifier.Form); - if (pkm.Format == 3 && pkm.HeldItem == 175) // Enigma Berry - VerifyEReaderBerry(); - if (pkm.IsEgg && pkm.HeldItem != 0) - AddLine(Severity.Invalid, V419, CheckIdentifier.Egg); - } - private void VerifyEReaderBerry() - { - if (Legal.EReaderBerryIsEnigma) // no E-Reader berry data provided, can't hold berry. - { - AddLine(Severity.Invalid, V204, CheckIdentifier.Form); - return; - } - - var matchUSA = Legal.EReaderBerriesNames_USA.Contains(Legal.EReaderBerryName); - var matchJP = Legal.EReaderBerriesNames_JP.Contains(Legal.EReaderBerryName); - if (!matchJP && !matchUSA) // Does not match any released E-Reader berry - AddLine(Severity.Invalid, V369, CheckIdentifier.Form); - else if (matchJP && !Legal.SavegameJapanese && Legal.SavegameLanguage >= 0) // E-Reader is region locked - AddLine(Severity.Invalid, V370, CheckIdentifier.Form); - else if (matchUSA && Legal.SavegameJapanese && Legal.SavegameLanguage >= 0) // E-Reader is region locked - AddLine(Severity.Invalid, V371, CheckIdentifier.Form); - } - private void VerifyECPID() - { - if (pkm.Format >= 6) - VerifyEC(); - if (EncounterMatch.Species == 265) - VerifyECPIDWurmple(); - - if (pkm.PID == 0) - AddLine(Severity.Fishy, V207, CheckIdentifier.PID); - - if ((Info.Generation >= 6 || Info.Generation < 3 && pkm.Format >= 7) && pkm.PID == pkm.EncryptionConstant) - AddLine(Severity.Invalid, V208, CheckIdentifier.PID); // better to flag than 1:2^32 odds since RNG is not feasible to yield match - - switch (EncounterMatch) - { - case EncounterStatic s: - if (!s.Shiny.IsValid(pkm)) - AddLine(Severity.Invalid, V209, CheckIdentifier.Shiny); - - // gen5 correlation - if (Info.Generation != 5) - break; - if (s.Location == 75) // Entree Forest - break; - if (s.Gift || s.Roaming || s.Ability != 4) - break; - if (s is EncounterStaticPID p && p.NSparkle) - break; - VerifyG5PID_IDCorrelation(); - break; - case EncounterSlot w: - if (pkm.IsShiny && w.Type == SlotType.HiddenGrotto) - AddLine(Severity.Invalid, V221, CheckIdentifier.Shiny); - if (Info.Generation == 5 && w.Type != SlotType.HiddenGrotto) - VerifyG5PID_IDCorrelation(); - break; - case PCD d: // fixed PID - if (d.Gift.PK.PID != 1 && pkm.EncryptionConstant != d.Gift.PK.PID) - AddLine(Severity.Invalid, V410, CheckIdentifier.Shiny); - break; - } - } - private void VerifyG5PID_IDCorrelation() - { - var pid = pkm.EncryptionConstant; - var result = (pid & 1) ^ (pid >> 31) ^ (pkm.TID & 1) ^ (pkm.SID & 1); - if (result != 0) - AddLine(Severity.Invalid, V411, CheckIdentifier.PID); - } - private void VerifyECPIDWurmple() - { - uint evoVal = PKX.GetWurmpleEvoVal(pkm.EncryptionConstant); - - if (pkm.Species == 265) - AddLine(Severity.Valid, string.Format(V212, evoVal == 0 ? SpeciesStrings[267] : SpeciesStrings[269]), CheckIdentifier.EC); - else if (evoVal != Array.IndexOf(Legal.WurmpleEvolutions, pkm.Species) / 2) - AddLine(Severity.Invalid, V210, CheckIdentifier.EC); - } - private void VerifyEC() - { - if (pkm.EncryptionConstant == 0) - AddLine(Severity.Fishy, V201, CheckIdentifier.EC); - if (3 <= Info.Generation && Info.Generation <= 5) - VerifyTransferEC(); - else - { - int xor = pkm.TSV ^ pkm.PSV; - if (xor < 16 && xor >= 8 && (pkm.PID ^ 0x80000000) == pkm.EncryptionConstant) - AddLine(Severity.Fishy, V211, CheckIdentifier.EC); - } - } - private void VerifyTransferEC() - { - // When transferred to Generation 6, the Encryption Constant is copied from the PID. - // The PID is then checked to see if it becomes shiny with the new Shiny rules (>>4 instead of >>3) - // If the PID is nonshiny->shiny, the top bit is flipped. - - // Check to see if the PID and EC are properly configured. - bool xorPID = ((pkm.TID ^ pkm.SID ^ (int)(pkm.PID & 0xFFFF) ^ (int)(pkm.PID >> 16)) & ~0x7) == 8; - bool valid = xorPID - ? pkm.EncryptionConstant == (pkm.PID ^ 0x80000000) - : pkm.EncryptionConstant == pkm.PID; - - if (!valid) - AddLine(Severity.Invalid, xorPID ? V215 : V216, CheckIdentifier.EC); - } - #region verifyLanguage - private bool VerifyLanguage() - { - int originalGeneration = Info.Generation; - int currentLanguage = pkm.Language; - int maxLanguageID = Legal.GetMaxLanguageID(originalGeneration); - - // Language ID 6 is unused; flag if an impossible language is used - if (currentLanguage == (int)LanguageID.UNUSED_6 || currentLanguage > maxLanguageID || currentLanguage <= (int)LanguageID.Hacked && !Legal.IsValidMissingLanguage(pkm)) - { - AddLine(Severity.Invalid, string.Format(V5, $"<={maxLanguageID}", currentLanguage), CheckIdentifier.Language); - return false; - } - - // Korean Gen4 games can not trade with other Gen4 languages, but can use Pal Park with any Gen3 game/language. - if (pkm.Format == 4 && pkm.Gen4 - && (currentLanguage == (int)LanguageID.Korean) ^ (Legal.SavegameLanguage == (int)LanguageID.Korean) && Legal.SavegameLanguage >= 0) - { - bool kor = currentLanguage == (int) LanguageID.Korean; - var currentpkm = kor ? V611 : V612; - var currentsav = kor ? V612 : V611; - AddLine(Severity.Invalid, string.Format(V610, currentpkm, currentsav), CheckIdentifier.Language); - return false; - } - - // Korean Crystal does not exist, neither do VC1 - if (originalGeneration <= 2 && pkm.Korean && !GameVersion.GS.Contains((GameVersion)pkm.Version)) - { - AddLine(Severity.Invalid, string.Format(V5, $"!={(LanguageID)currentLanguage}", currentLanguage), CheckIdentifier.Language); - return false; - } - - return true; - } - #endregion - #region verifyNickname - private void VerifyNickname() - { - // If the Pokémon is not nicknamed, it should match one of the language strings. - if (pkm.Nickname.Length == 0) - { - AddLine(Severity.Invalid, V2, CheckIdentifier.Nickname); - return; - } - if (pkm.Species > PKX.SpeciesLang[0].Length) - { - AddLine(Severity.Indeterminate, V3, CheckIdentifier.Nickname); - return; - } - - if (pkm.VC && pkm.IsNicknamed) - { - VerifyG1NicknameWithinBounds(pkm.Nickname); - } - else if (EncounterMatch is MysteryGift m) - { - if (pkm.IsNicknamed && !m.IsEgg) - AddLine(Severity.Fishy, V0, CheckIdentifier.Nickname); - } - - if (!Encounter.Valid) - return; - - if (pkm.Format >= 3 && !VerifyLanguage()) - return; - - if (EncounterMatch is EncounterTrade) - { - VerifyNicknameTrade(); - return; - } - - if (pkm.IsEgg) - { - VerifyNicknameEgg(); - return; - } - - string nickname = pkm.Nickname.Replace("'", "’"); - if (pkm.IsNicknamed) - { - for (int i = 0; i < PKX.SpeciesLang.Length; i++) - { - if (!PKX.SpeciesDict[i].TryGetValue(nickname, out int index)) - continue; - var msg = index == pkm.Species && i != pkm.Language ? V15 : V16; - AddLine(Severity.Fishy, msg, CheckIdentifier.Nickname); - return; - } - if (nickname.Any(c => 0x4E00 <= c && c <= 0x9FFF)) // East Asian Scripts - { - AddLine(Severity.Invalid, V222, CheckIdentifier.Nickname); - return; - } - AddLine(Severity.Valid, V17, CheckIdentifier.Nickname); - } - else if (pkm.Format < 3) - { - // pk1/pk2 IsNicknamed getter checks for match, logic should only reach here if matches. - AddLine(Severity.Valid, V18, CheckIdentifier.Nickname); - } - else - { - // Can't have another language name if it hasn't evolved or wasn't a language-traded egg. - bool evolved = EncounterMatch.Species != pkm.Species; - bool match = PKX.GetSpeciesNameGeneration(pkm.Species, pkm.Language, pkm.Format) == nickname; - if (pkm.WasTradedEgg || evolved) - match |= !PKX.IsNicknamedAnyLanguage(pkm.Species, nickname, pkm.Format); - if (!match && pkm.Format == 5 && !pkm.IsNative) // transfer - { - if (evolved) - match |= !PKX.IsNicknamedAnyLanguage(pkm.Species, nickname, 4); - else - match |= PKX.GetSpeciesNameGeneration(pkm.Species, pkm.Language, 4) == nickname; - } - - if (!match) - { - if (EncounterMatch is WC7 wc7 && wc7.CardID == 2046 && (pkm.SID << 16 | pkm.TID) == 0x79F57B49) // ash greninja - AddLine(Severity.Valid, V19, CheckIdentifier.Nickname); - else - AddLine(Severity.Invalid, V20, CheckIdentifier.Nickname); - } - else - AddLine(Severity.Valid, V18, CheckIdentifier.Nickname); - } - - // Non-nicknamed strings have already been checked. - if (Legal.CheckWordFilter && pkm.IsNicknamed && WordFilter.IsFiltered(nickname, out string bad)) - AddLine(Severity.Invalid, $"Wordfilter: {bad}", CheckIdentifier.Nickname); - } - private void VerifyNicknameEgg() - { - switch (pkm.Format) - { - case 4: - if (pkm.IsNicknamed) // gen4 doesn't use the nickname flag for eggs - AddLine(Severity.Invalid, V224, CheckIdentifier.Egg); - break; - case 7: - if (EncounterMatch is EncounterStatic ^ !pkm.IsNicknamed) // gen7 doesn't use for ingame gifts - AddLine(Severity.Invalid, pkm.IsNicknamed ? V224 : V12, CheckIdentifier.Egg); - break; - default: - if (!pkm.IsNicknamed) - AddLine(Severity.Invalid, V12, CheckIdentifier.Egg); - break; - } - - if (pkm.Format == 2 && pkm.IsEgg && !PKX.IsNicknamedAnyLanguage(0, pkm.Nickname, 2)) - AddLine(Severity.Valid, V14, CheckIdentifier.Egg); - else if (PKX.GetSpeciesNameGeneration(0, pkm.Language, Info.Generation) != pkm.Nickname) - AddLine(Severity.Invalid, V13, CheckIdentifier.Egg); - else - AddLine(Severity.Valid, V14, CheckIdentifier.Egg); - } - private void VerifyNicknameTrade() - { - switch (Info.Generation) - { - case 1: - case 2: VerifyTrade12(); return; - case 3: VerifyTrade3(); return; - case 4: VerifyTrade4(); return; - case 5: VerifyTrade5(); return; - case 6: VerifyTrade6(); return; - case 7: VerifyTrade7(); return; - } - } - private void VerifyTrade12() - { - var et = (EncounterTrade)(EncounterOriginalGB ?? EncounterMatch); - if (et.TID != 0) // Gen2 Trade - return; // already checked all relevant properties when fetching with getValidEncounterTradeVC2 - - if (!EncounterGenerator.IsEncounterTrade1Valid(pkm)) - AddLine(Severity.Invalid, V10, CheckIdentifier.Trainer); - } - private void VerifyTrade3() - { - if (pkm.FRLG) - { - int lang = pkm.Language; - if (EncounterMatch.Species == 124) // Jynx - lang = DetectTradeLanguageG3DANTAEJynx(pkm, lang); - VerifyTradeTable(Encounters3.TradeFRLG, Encounters3.TradeGift_FRLG, lang); - } - else - VerifyTradeTable(Encounters3.TradeRSE, Encounters3.TradeGift_RSE); - } - private void VerifyTrade4() - { - if (pkm.TID == 1000) - { - VerifyTrade4Ranch(); - return; - } - if (pkm.HGSS) - { - int lang = pkm.Language; - if (EncounterMatch.Species == 25) // Pikachu - { - lang = DetectTradeLanguageG4SurgePikachu(pkm, lang); - // flag korean magikarp on gen4 saves since the pkm.Language is German - if (pkm.Format == 4 && lang == (int)LanguageID.Korean && Legal.SavegameLanguage != (int)LanguageID.Korean && Legal.SavegameLanguage >= 0) - AddLine(Severity.Invalid, string.Format(V610, V611, V612), CheckIdentifier.Language); - } - VerifyTradeTable(Encounters4.TradeHGSS, Encounters4.TradeGift_HGSS, lang); - } - else // DPPt - { - int lang = pkm.Language; - if (EncounterMatch.Species == 129) // Magikarp - { - lang = DetectTradeLanguageG4MeisterMagikarp(pkm, lang); - // flag korean magikarp on gen4 saves since the pkm.Language is German - if (pkm.Format == 4 && lang == (int)LanguageID.Korean && Legal.SavegameLanguage != (int)LanguageID.Korean && Legal.SavegameLanguage >= 0) - AddLine(Severity.Invalid, string.Format(V610, V611, V612), CheckIdentifier.Language); - } - else if (!pkm.Pt && lang == 1) // DP English origin are Japanese lang - { - int index = Array.IndexOf(Encounters4.TradeGift_DPPt, EncounterMatch); - if (Encounters4.TradeDPPt[1][index] != pkm.Nickname) // not japanese - lang = 2; // English - } - VerifyTradeTable(Encounters4.TradeDPPt, Encounters4.TradeGift_DPPt, lang); - } - } - private static int DetectTradeLanguageG3DANTAEJynx(PKM pk, int lang) - { - if (lang != (int)LanguageID.Italian) - return lang; - - if (pk.Version == (int) GameVersion.LG) - lang = (int)LanguageID.English; // translation error; OT was not localized => same as English - return lang; - } - private static int DetectTradeLanguageG4MeisterMagikarp(PKM pkm, int lang) - { - if (lang == (int)LanguageID.English) - return (int)LanguageID.German; - - // All have German, regardless of origin version. - // Detect which language they originated from... roughly. - var table = Encounters4.TradeDPPt; - for (int i = 0; i < table.Length; i++) - { - if (table[i].Length == 0) - continue; - // Nick @ 3, OT @ 7 - if (table[i][7] != pkm.OT_Name) - continue; - lang = i; - break; - } - if (lang == 2) // possible collision with FR/ES/DE. Check nickname - return pkm.Nickname == table[3][3] ? (int)LanguageID.French : (int)LanguageID.Spanish; // Spanish is same as English - - return lang; - } - private static int DetectTradeLanguageG4SurgePikachu(PKM pkm, int lang) - { - if (lang == (int)LanguageID.French) - return (int)LanguageID.English; - - // All have English, regardless of origin version. - // Detect which language they originated from... roughly. - var table = Encounters4.TradeHGSS; - for (int i = 0; i < table.Length; i++) - { - if (table[i].Length == 0) - continue; - // Nick @ 6, OT @ 18 - if (table[i][18] != pkm.OT_Name) - continue; - lang = i; - break; - } - if (lang == 2) // possible collision with ES/IT. Check nickname - return pkm.Nickname == table[4][6] ? (int)LanguageID.Italian : (int)LanguageID.Spanish; - - return lang; - } - private void VerifyTrade5() - { - // Trades for JPN games have language ID of 0, not 1. - if (pkm.BW) - { - int lang = pkm.Language; - if (pkm.Format == 5 && lang == (int)LanguageID.Japanese) - AddLine(Severity.Invalid, string.Format(V5, 0, (int)LanguageID.Japanese), CheckIdentifier.Language); - - lang = Math.Max(lang, 1); - VerifyTradeTable(Encounters5.TradeBW, Encounters5.TradeGift_BW, lang); - } - else // B2W2 - { - if (Encounters5.TradeGift_B2W2_YancyCurtis.Contains(EncounterMatch)) - VerifyTradeOTOnly(pkm.OT_Gender == 0 ? Encounters5.TradeOT_B2W2_M : Encounters5.TradeOT_B2W2_F); - else - VerifyTradeTable(Encounters5.TradeB2W2, Encounters5.TradeGift_B2W2); - } - } - private void VerifyTrade6() - { - if (pkm.XY) - VerifyTradeTable(Encounters6.TradeXY, Encounters6.TradeGift_XY, pkm.Language); - else if (pkm.AO) - VerifyTradeTable(Encounters6.TradeAO, Encounters6.TradeGift_AO, pkm.Language); - } - private void VerifyTrade7() - { - if (pkm.SM) - VerifyTradeTable(Encounters7.TradeSM, Encounters7.TradeGift_SM, pkm.Language); - else if (pkm.USUM) - VerifyTradeTable(Encounters7.TradeUSUM, Encounters7.TradeGift_USUM, pkm.Language); - } - private void VerifyTrade4Ranch() => VerifyTradeOTOnly(Encounters4.RanchOTNames); - - private void VerifyTradeTable(string[][] ots, EncounterTrade[] table) => VerifyTradeTable(ots, table, pkm.Language); - private void VerifyTradeTable(string[][] ots, EncounterTrade[] table, int language) - { - var validOT = language >= ots.Length ? ots[0] : ots[language]; - var index = Array.IndexOf(table, EncounterMatch); - VerifyTradeOTNick(validOT, index); - } - private void VerifyTradeOTOnly(string[] validOT) - { - if (pkm.IsNicknamed) - AddLine(Severity.Invalid, V9, CheckIdentifier.Nickname); - int lang = pkm.Language; - if (validOT.Length <= lang) - AddLine(Severity.Invalid, V8, CheckIdentifier.Trainer); - else if (validOT[lang] != pkm.OT_Name) - AddLine(Severity.Invalid, V10, CheckIdentifier.Trainer); - else - AddLine(Severity.Valid, V11, CheckIdentifier.Nickname); - } - private void VerifyTradeOTNick(string[] validOT, int index) - { - if (validOT.Length == 0) - { - AddLine(Severity.Indeterminate, V7, CheckIdentifier.Trainer); - return; - } - if (index == -1 || validOT.Length < index * 2) - { - AddLine(Severity.Indeterminate, V8, CheckIdentifier.Trainer); - return; - } - - string nick = validOT[index]; - string OT = validOT[validOT.Length / 2 + index]; - - if (nick != pkm.Nickname - && !(nick == "Quacklin’" && pkm.Nickname == "Quacklin'") // apostrophe farfetch'd edge case - && ((EncounterTrade)EncounterMatch).IsNicknamed) // trades that are not nicknamed (but are present in a table with others being named) - AddLine(Severity.Invalid, V9, CheckIdentifier.Nickname); - else - AddLine(Severity.Valid, V11, CheckIdentifier.Nickname); - - if (OT != pkm.OT_Name) - AddLine(Severity.Invalid, V10, CheckIdentifier.Trainer); - } - #endregion - private void VerifyEVs() - { - var evs = pkm.EVs; - int sum = pkm.EVTotal; - if (sum > 0 && pkm.IsEgg) - AddLine(Severity.Invalid, V22, CheckIdentifier.EVs); - if (pkm.Format >= 3 && sum > 510) - AddLine(Severity.Invalid, V25, CheckIdentifier.EVs); - if (pkm.Format >= 6 && evs.Any(ev => ev > 252)) - AddLine(Severity.Invalid, V26, CheckIdentifier.EVs); - if (pkm.Format == 4 && pkm.Gen4 && EncounterMatch.LevelMin == 100) - { - // Cannot EV train at level 100 -- Certain events are distributed at level 100. - if (evs.Any(ev => ev > 100)) // EVs can only be increased by vitamins to a max of 100. - AddLine(Severity.Invalid, V367, CheckIdentifier.EVs); - } - else if (pkm.Format < 5) - { - // In Generations I and II, when a Pokémon is taken out of the Day Care, its experience will lower to the minimum value for its current level. - if (pkm.Format < 3) // can abuse daycare for EV training without EXP gain - return; - - const int maxEV = 100; // Vitamin Max - if (PKX.GetEXP(EncounterMatch.LevelMin, pkm.Species) == pkm.EXP && evs.Any(ev => ev > maxEV)) - AddLine(Severity.Invalid, string.Format(V418, maxEV), CheckIdentifier.EVs); - } - - // Only one of the following can be true: 0, 508, and x%6!=0 - if (sum == 0 && !EncounterMatch.IsWithinRange(pkm)) - AddLine(Severity.Fishy, V23, CheckIdentifier.EVs); - else if (sum == 508) - AddLine(Severity.Fishy, V24, CheckIdentifier.EVs); - else if (evs[0] != 0 && evs.All(ev => evs[0] == ev)) - AddLine(Severity.Fishy, V27, CheckIdentifier.EVs); - } - private void VerifyIVs() - { - if (EncounterMatch is EncounterStatic s) - { - if (s.FlawlessIVCount != 0 && pkm.IVs.Count(iv => iv == 31) < s.FlawlessIVCount) - { - AddLine(Severity.Invalid, string.Format(V28, s.FlawlessIVCount), CheckIdentifier.IVs); - return; - } - } - if (EncounterMatch is EncounterSlot w) - { - bool force2 = w.Type == SlotType.FriendSafari || w.Generation == 7 && pkm.AbilityNumber == 4; - if (force2 && pkm.IVs.Count(iv => iv == 31) < 2) - { - AddLine(Severity.Invalid, w.Type == SlotType.FriendSafari ? V29 : string.Format(V28, 2), CheckIdentifier.IVs); - return; - } - } - if (EncounterMatch is MysteryGift g) - { - int[] IVs; - switch (g.Format) - { - case 7: IVs = ((WC7)EncounterMatch).IVs; break; - case 6: IVs = ((WC6)EncounterMatch).IVs; break; - case 5: IVs = ((PGF)EncounterMatch).IVs; break; - default: IVs = null; break; - } - - if (IVs != null) - { - var pkIVs = pkm.IVs; - var ivflag = Array.Find(IVs, iv => (byte)(iv - 0xFC) < 3); - if (ivflag == 0) // Random IVs - { - bool valid = true; - for (int i = 0; i < 6; i++) - if (IVs[i] <= 31 && IVs[i] != pkIVs[i]) - valid = false; - if (!valid) - AddLine(Severity.Invalid, V30, CheckIdentifier.IVs); - } - else - { - int IVCount = ivflag - 0xFB; // IV2/IV3 - if (pkIVs.Count(iv => iv == 31) < IVCount) - AddLine(Severity.Invalid, string.Format(V28, IVCount), CheckIdentifier.IVs); - } - } - } - if (pkm.IVTotal == 0) - AddLine(Severity.Fishy, V31, CheckIdentifier.IVs); - else if (pkm.IVs[0] < 30 && pkm.IVs.All(iv => pkm.IVs[0] == iv)) - AddLine(Severity.Fishy, V32, CheckIdentifier.IVs); - } - private void VerifyDVs() - { - // todo - } - #region VerifyOT - private void VerifyOT() - { - if (EncounterMatch is EncounterTrade) - return; // Already matches Encounter information - - if (EncounterMatch is MysteryGift g && !g.IsEgg) - return; // Already matches Encounter information - - if (EncounterMatch is EncounterStaticPID s && s.NSparkle) - return; // Already checked by VerifyMisc - - var ot = pkm.OT_Name; - if (ot.Length == 0) - AddLine(Severity.Invalid, V106, CheckIdentifier.Trainer); - - if (pkm.TID == 0 && pkm.SID == 0) - AddLine(Severity.Fishy, V33, CheckIdentifier.Trainer); - else if (pkm.VC) - { - if (pkm.SID != 0) - AddLine(Severity.Invalid, V34, CheckIdentifier.Trainer); - } - else if (pkm.TID == pkm.SID) - AddLine(Severity.Fishy, V35, CheckIdentifier.Trainer); - else if (pkm.TID == 0) - AddLine(Severity.Fishy, V36, CheckIdentifier.Trainer); - else if (pkm.SID == 0) - AddLine(Severity.Fishy, V37, CheckIdentifier.Trainer); - else if (pkm.TID == 12345 && pkm.SID == 54321 || ot.StartsWith("PKHeX")) - AddLine(Severity.Fishy, V417, CheckIdentifier.Trainer); - - if (pkm.VC) - VerifyOTG1(); - - if (Legal.CheckWordFilter) - { - if (WordFilter.IsFiltered(ot, out string bad)) - AddLine(Severity.Invalid, $"Wordfilter: {bad}", CheckIdentifier.Trainer); - if (WordFilter.IsFiltered(pkm.HT_Name, out bad)) - AddLine(Severity.Invalid, $"Wordfilter: {bad}", CheckIdentifier.Trainer); - } - } - private void VerifyOTG1() - { - string tr = pkm.OT_Name; - - VerifyG1OTWithinBounds(tr); - if (EncounterMatch is EncounterStatic s && (s.Version == GameVersion.Stadium || s.Version == GameVersion.Stadium2)) - VerifyG1OTStadium(tr); - - if (pkm.Species == 151) - { - var OTMatch = (tr == Legal.GetG1OT_GFMew((int)LanguageID.Japanese)) - || (tr == Legal.GetG1OT_GFMew((int)LanguageID.English)); - if (!OTMatch || pkm.TID != 22796) - AddLine(Severity.Invalid, V39, CheckIdentifier.Trainer); - } - - if (pkm.OT_Gender == 1 && (pkm.Format == 2 && pkm.Met_Location == 0 || pkm.Format > 2 && pkm.VC1)) - AddLine(Severity.Invalid, V408, CheckIdentifier.Trainer); - } - private void VerifyG1OTWithinBounds(string str) - { - if (StringConverter.GetIsG1English(str)) - { - if (str.Length > 7 && !((EncounterOriginalGB ?? EncounterMatch) is EncounterTrade)) // OT already verified; GER shuckle has 8 chars - AddLine(Severity.Invalid, V38, CheckIdentifier.Trainer); - } - else if (StringConverter.GetIsG1Japanese(str)) - { - if (str.Length > 5) - AddLine(Severity.Invalid, V38, CheckIdentifier.Trainer); - } - else if (pkm.Korean && StringConverter.GetIsG2Korean(str)) - { - if (str.Length > 5) - AddLine(Severity.Invalid, V38, CheckIdentifier.Trainer); - } - else - { - AddLine(Severity.Invalid, V421, CheckIdentifier.Trainer); - } - } - private void VerifyG1NicknameWithinBounds(string str) - { - if (StringConverter.GetIsG1English(str)) - { - if (str.Length > 10) - AddLine(Severity.Invalid, V1, CheckIdentifier.Trainer); - } - else if (StringConverter.GetIsG1Japanese(str)) - { - if (str.Length > 5) - AddLine(Severity.Invalid, V1, CheckIdentifier.Trainer); - } - else if (pkm.Korean && StringConverter.GetIsG2Korean(str)) - { - if (str.Length > 5) - AddLine(Severity.Invalid, V38, CheckIdentifier.Trainer); - } - else - { - AddLine(Severity.Invalid, V422, CheckIdentifier.Trainer); - } - } - private void VerifyG1OTStadium(string tr) - { - bool jp = pkm.Japanese; - bool valid = GetIsStadiumOTIDValid(jp, tr); - if (!valid) - AddLine(Severity.Invalid, V402, CheckIdentifier.Trainer); - else - AddLine(Severity.Valid, jp ? V404 : V403, CheckIdentifier.Trainer); - } - private bool GetIsStadiumOTIDValid(bool jp, string tr) - { - if (jp) - return tr == "スタジアム" && pkm.TID == 1999; - return tr == (Info.Generation == 1 ? "STADIUM" : "Stadium") && pkm.TID == 2000; - } - #endregion - private void VerifyHyperTraining() - { - if (!(pkm is IHyperTrain t)) - return; // No Hyper Training before Gen7 - if (!t.IsHyperTrained()) - return; - - if (pkm.CurrentLevel != 100) - { - AddLine(Severity.Invalid, V40, CheckIdentifier.IVs); - return; - } - - int max = pkm.MaxIV; - if (pkm.IVTotal == max * 6) - { - AddLine(Severity.Invalid, V41, CheckIdentifier.IVs); - } - else - { - for (int i = 0; i < 6; i++) // Check individual IVs - { - if (pkm.GetIV(i) != max || !t.IsHyperTrained(i)) - continue; - AddLine(Severity.Invalid, V42, CheckIdentifier.IVs); - break; - } - } - } - #region VerifyEncounter - private void VerifyFormFriendSafari() - { - switch (pkm.Species) - { - case 670: // Floette - case 671: // Florges - if (!new[] { 0, 1, 3 }.Contains(pkm.AltForm)) // 0/1/3 - RBY - AddLine(Severity.Invalid, V64, CheckIdentifier.Form); - break; - case 710 when pkm.AltForm != 0: // Pumpkaboo - case 711 when pkm.AltForm != 0: // Goregeist Average - AddLine(Severity.Invalid, V6, CheckIdentifier.Form); - break; - case 423 when pkm.AltForm != 0: // Gastrodon West - AddLine(Severity.Invalid, V64, CheckIdentifier.Form); - break; - case 586 when pkm.AltForm != 0: // Sawsbuck - AddLine(Severity.Invalid, V65, CheckIdentifier.Form); - break; - } - } - private void VerifyEncounterType() - { - if (!Encounter.Valid) - return; - - EncounterType type = EncounterType.None; - // Encounter type data is only stored for gen 4 encounters - // All eggs have encounter type none, even if they are from static encounters - if (pkm.Gen4 && !pkm.WasEgg) - { - if (EncounterMatch is EncounterSlot w) - type = w.TypeEncounter; - if (EncounterMatch is EncounterStaticTyped s) - type = s.TypeEncounter; - } - - if (!type.Contains(pkm.EncounterType)) - AddLine(Severity.Invalid, V381, CheckIdentifier.Encounter); - else - AddLine(Severity.Valid, V380, CheckIdentifier.Encounter); - } - - private void VerifyTransferLegalityG3() - { - if (pkm.Format == 4 && pkm.Met_Location != Legal.Transfer3) // Pal Park - AddLine(Severity.Invalid, V60, CheckIdentifier.Encounter); - if (pkm.Format != 4 && pkm.Met_Location != Legal.Transfer4) - AddLine(Severity.Invalid, V61, CheckIdentifier.Encounter); - } - private void VerifyTransferLegalityG4() - { - // Transfer Legality - int loc = pkm.Met_Location; - if (loc != 30001) // PokéTransfer - { - // Crown - switch (pkm.Species) - { - case 251: // Celebi - if (loc != Legal.Transfer4_CelebiUnused && loc != Legal.Transfer4_CelebiUsed) - AddLine(Severity.Invalid, V351, CheckIdentifier.Encounter); - break; - case 243: // Raikou - case 244: // Entei - case 245: // Suicune - if (loc != Legal.Transfer4_CrownUnused && loc != Legal.Transfer4_CrownUsed) - AddLine(Severity.Invalid, V351, CheckIdentifier.Encounter); - break; - default: - AddLine(Severity.Invalid, V61, CheckIdentifier.Encounter); - break; - } - } - } - private static IEnumerable VerifyVCEncounter(PKM pkm, IEncounterable encounter, ILocation transfer, IList Moves) - { - // Check existing EncounterMatch - if (encounter is EncounterInvalid || transfer == null) - yield break; // Avoid duplicate invaild message - - if (encounter is EncounterStatic v && (GameVersion.GBCartEraOnly.Contains(v.Version) || v.Version == GameVersion.VCEvents)) - { - bool exceptions = false; - exceptions |= v.Version == GameVersion.VCEvents && encounter.Species == 151 && pkm.TID == 22796; - if (!exceptions) - yield return new CheckResult(Severity.Invalid, V79, CheckIdentifier.Encounter); - } - - if (pkm.Met_Location != transfer.Location) - yield return new CheckResult(Severity.Invalid, V81, CheckIdentifier.Encounter); - if (pkm.Egg_Location != transfer.EggLocation) - yield return new CheckResult(Severity.Invalid, V59, CheckIdentifier.Encounter); - - // Flag Moves that cannot be transferred - if (encounter is EncounterStatic s && s.Version == GameVersion.C && s.EggLocation == 256) // Dizzy Punch Gifts - { - // can't have Dizzy Punch at all - int index = Array.IndexOf(pkm.Moves, 146); // Dizzy Punch - if (index >= 0) - { - var chk = Moves[index]; - if (chk.Generation == 2) // not obtained from a future gen - Moves[index] = new CheckMoveResult(chk.Source, chk.Generation, Severity.Invalid, V82, CheckIdentifier.Move); - } - } - - bool checkShiny = pkm.VC2 || pkm.TradebackStatus == TradebackType.WasTradeback && pkm.VC1; - if (!checkShiny) - yield break; - if (pkm.Gender == 1) // female - { - if (pkm.PersonalInfo.Gender == 31 && pkm.IsShiny) // impossible gender-shiny - yield return new CheckResult(Severity.Invalid, V209, CheckIdentifier.PID); - } - else if (pkm.Species == 201) // unown - { - if (pkm.AltForm != 8 && pkm.AltForm != 21 && pkm.IsShiny) // impossibly form-shiny (not I or V) - yield return new CheckResult(Severity.Invalid, V209, CheckIdentifier.PID); - } - } - #endregion - private void VerifyLevel() - { - if (EncounterMatch is MysteryGift gift) - { - if (gift.Level != pkm.Met_Level && pkm.HasOriginalMetLocation) - { - switch (gift) - { - case WC3 wc3 when wc3.Met_Level == pkm.Met_Level || wc3.IsEgg: - break; - case WC7 wc7 when wc7.MetLevel == pkm.Met_Level: - break; - default: - AddLine(new CheckResult(Severity.Invalid, V83, CheckIdentifier.Level)); - return; - } - } - if (gift.Level > pkm.CurrentLevel) - { - AddLine(new CheckResult(Severity.Invalid, V84, CheckIdentifier.Level)); - return; - } - } - - if (pkm.IsEgg) - { - int elvl = Legal.GetEggHatchLevel(pkm); - if (elvl != pkm.CurrentLevel) - AddLine(Severity.Invalid, string.Format(V52, elvl), CheckIdentifier.Level); - return; - } - - int lvl = pkm.CurrentLevel; - if (lvl < pkm.Met_Level) - AddLine(Severity.Invalid, V85, CheckIdentifier.Level); - else if (!EncounterMatch.IsWithinRange(pkm) && lvl != 100 && pkm.EXP == PKX.GetEXP(lvl, pkm.Species)) - AddLine(Severity.Fishy, V87, CheckIdentifier.Level); - else - AddLine(Severity.Valid, V88, CheckIdentifier.Level); - } - private void VerifyLevelG1() - { - if (pkm.IsEgg) - { - int elvl = Legal.GetEggHatchLevel(pkm); - if (elvl != pkm.CurrentLevel) - AddLine(Severity.Invalid, string.Format(V52, elvl), CheckIdentifier.Level); - return; - } - if (pkm.Met_Location != 0) // crystal - { - int lvl = pkm.CurrentLevel; - if (lvl < pkm.Met_Level) - AddLine(Severity.Invalid, V85, CheckIdentifier.Level); - } - - // There is no way to prevent a gen1 trade evolution as held items (everstone) did not exist. - // Machoke, Graveler, Haunter and Kadabra captured in the second phase evolution, excluding in-game trades, are already checked - if (pkm.Format <= 2 && !(EncounterMatch is EncounterTrade) && EncounterMatch.Species == pkm.Species && Legal.Trade_Evolution1.Contains(EncounterMatch.Species)) - VerifyG1TradeEvo(); - } - private void VerifyG1TradeEvo() - { - var mustevolve = pkm.TradebackStatus == TradebackType.WasTradeback || (pkm.Format == 1 && Legal.IsOutsider(pkm)) || Legal.IsTradedKadabraG1(pkm); - if (!mustevolve) - return; - // Pokemon have been traded but it is not evolved, trade evos are sequential dex numbers - var unevolved = SpeciesStrings[pkm.Species]; - var evolved = SpeciesStrings[pkm.Species + 1]; - AddLine(Severity.Invalid, string.Format(V405, unevolved, evolved), CheckIdentifier.Level); - } - #region VerifyMedals + private void VerifyBall() => Ball.Verify(this); + private void VerifyForm() => Form.Verify(this); + private void VerifyEVs() => EffortValues.Verify(this); + private void VerifyIVs() => IndividualValues.Verify(this); + private void VerifyHistory() => Memory.Verify(this); + private void VerifyConsoleRegion() => ConsoleRegion.Verify(this); + private void VerifyAbility() => Ability.Verify(this); + private void VerifyRibbons() => Ribbon.Verify(this); + private void VerifyItem() => Item.Verify(this); + private void VerifyLevel() => Level.Verify(this); + private void VerifyLevelG1() => Level.VerifyG1(this); + private void VerifyEncounterType() => EncounterType.Verify(this); + private void VerifyOT() => Trainer.Verify(this); + private void VerifyHyperTraining() => HyperTraining.Verify(this); + private void VerifyGender() => Gender.Verify(this); + private void VerifyECPID() => PIDEC.Verify(this); + private void VerifyCXD() => CXD.Verify(this); private void VerifyMedals() { - if (pkm.Format < 6) - return; - - VerifyMedalsRegular(); - VerifyMedalsEvent(); + if (pkm.Format >= 6) + Medal.Verify(this); } - private void VerifyMedalsRegular() + private void VerifyNickname() { - uint data = BitConverter.ToUInt32(pkm.Data, 0x2C); - if ((data & 3) != 0) // 2 unused flags - AddLine(Severity.Invalid, V98, CheckIdentifier.Training); - int TrainCount = pkm.SuperTrainingMedalCount(); - - if (pkm.IsEgg && TrainCount > 0) - AddLine(Severity.Invalid, V89, CheckIdentifier.Training); - else if (TrainCount > 0 && Info.Generation > 6) - AddLine(Severity.Invalid, V90, CheckIdentifier.Training); - else - { - if (pkm.Format >= 7) - { - if (pkm.SecretSuperTrainingUnlocked) - AddLine(Severity.Invalid, V91, CheckIdentifier.Training); - if (pkm.SecretSuperTrainingComplete) - AddLine(Severity.Invalid, V92, CheckIdentifier.Training); - } - else - { - if (TrainCount == 30 ^ pkm.SecretSuperTrainingComplete) - AddLine(Severity.Invalid, V93, CheckIdentifier.Training); - } - } - } - - private void VerifyMedalsEvent() - { - byte data = pkm.Data[0x3A]; - if ((data & 0xC0) != 0) // 2 unused flags highest bits - AddLine(Severity.Invalid, V98, CheckIdentifier.Training); - - int TrainCount = 0; - for (int i = 0; i < 6; i++) - { - if ((data & 1) != 0) - TrainCount++; - data >>= 1; - } - if (pkm.IsEgg && TrainCount > 0) - { AddLine(Severity.Invalid, V89, CheckIdentifier.Training); } - else if (TrainCount > 0 && Info.Generation > 6) - { AddLine(Severity.Invalid, V90, CheckIdentifier.Training); } - else if (TrainCount > 0) - { AddLine(Severity.Fishy, V94, CheckIdentifier.Training); } - } - #endregion - private void VerifyRibbons() - { - if (!Encounter.Valid) - return; - - // Check Unobtainable Ribbons - var encounterContent = (EncounterMatch as MysteryGift)?.Content ?? EncounterMatch; - if (pkm.IsEgg) - { - if (RibbonVerifier.GetIncorrectRibbonsEgg(pkm, encounterContent)) - AddLine(Severity.Invalid, V603, CheckIdentifier.Ribbon); - return; - } - - int gen = Info.Generation < 3 ? 7 : Info.Generation; // Flag VC (Gen1/2) ribbons using Gen7 origin rules. - var result = RibbonVerifier.GetIncorrectRibbons(pkm, encounterContent, gen); - if (result.Count != 0) - AddLine(Severity.Invalid, string.Join(Environment.NewLine, result.Where(s => !string.IsNullOrEmpty(s))), CheckIdentifier.Ribbon); - else - AddLine(Severity.Valid, V602, CheckIdentifier.Ribbon); - } - - private void VerifyCXD() - { - if (EncounterMatch is EncounterStatic) - VerifyCXDStarterCorrelation(Info.PIDIV); - else if (pkm.WasEgg) // can't obtain eggs in CXD - AddLine(Severity.Invalid, V80, CheckIdentifier.Encounter); // invalid encounter - - if (pkm.OT_Gender == 1) - AddLine(Severity.Invalid, V407, CheckIdentifier.Trainer); - } - private void VerifyCXDStarterCorrelation(PIDIV pidiv) - { - if (pidiv.Type != PIDType.CXD) - return; - - bool valid; - switch (EncounterMatch.Species) - { - case 133: - valid = LockFinder.IsXDStarterValid(pidiv.OriginSeed, pkm.TID, pkm.SID); break; - case 196: case 197: - valid = pidiv.Type == PIDType.CXD_ColoStarter; break; - default: - return; - } - if (!valid) - AddLine(Severity.Invalid, V400, CheckIdentifier.PID); - } - - private void VerifyAbility() - { - int[] abilities = pkm.PersonalInfo.Abilities; - int abilval = Array.IndexOf(abilities, pkm.Ability); - if (abilval < 0) - { - AddLine(Severity.Invalid, V107, CheckIdentifier.Ability); - return; - } - - bool? AbilityUnchanged = true; - // 3 states flag: true for unchanged, false for changed, null for uncertain/allowing PID mismatch - // if true, check encounter ability - // if true or false, check PID/AbilityNumber - if (3 <= pkm.Format && pkm.Format <= 5 && abilities[0] != abilities[1]) // 3-5 and have 2 distinct ability now - AbilityUnchanged = VerifyAbilityPreCapsule(abilities, abilval); - - if (Encounter.Valid) - { - if (EncounterMatch is PCD d) - { - if (VerifyAbilityPCD(d, AbilityUnchanged, abilities)) - return; - } - else // Check Ability Mismatches - { - int? EncounterAbility = (EncounterMatch as EncounterStatic)?.Ability ?? - (EncounterMatch as EncounterTrade)?.Ability ?? - (EncounterMatch as EncounterLink)?.Ability; - - if (EncounterAbility != null && VerifySetAbility(EncounterAbility, AbilityUnchanged, abilities, abilval)) - return; // result added via VerifySetAbility - } - switch (Info.Generation) - { - case 5: VerifyAbility5(abilities); break; - case 6: VerifyAbility6(abilities); break; - case 7: VerifyAbility7(abilities); break; - } - } - - if (3 <= Info.Generation && Info.Generation <= 4 && pkm.AbilityNumber == 4) - AddLine(Severity.Invalid, V112, CheckIdentifier.Ability); - else if (AbilityUnchanged != null && abilities[pkm.AbilityNumber >> 1] != pkm.Ability) - AddLine(Severity.Invalid, pkm.Format < 6 ? V113 : V114, CheckIdentifier.Ability); - else - AddLine(Severity.Valid, V115, CheckIdentifier.Ability); - } - private bool VerifyAbilityPCD(PCD pcd, bool? abilityUnchanged, int[] abilities) - { - if (pcd.Species == pkm.Species && pkm.Ability == pcd.Gift.PK.Ability) // Edge case (PID ability gift mismatch) - AddLine(Severity.Valid, V115, CheckIdentifier.Ability); - else if (pkm.Format >= 6 && abilities[0] == abilities[1] && pkm.AbilityNumber == 1) - AddLine(Severity.Valid, V115, CheckIdentifier.Ability); // gen3-5 transfer with same ability -> 1st ability that matches - else if (pkm.Format >= 6 && abilities[0] != abilities[1] && pkm.AbilityNumber < 4) // Ability Capsule can change between 1/2 - AddLine(Severity.Valid, V109, CheckIdentifier.Ability); - else - { - if (!(abilityUnchanged ?? false)) - return false; - AddLine(Severity.Invalid, V223, CheckIdentifier.Ability); - } - return true; - } - private bool VerifySetAbility(int? EncounterAbility, bool? AbilityUnchanged, int[] abilities, int abilval) - { - if (pkm.AbilityNumber == 4 && EncounterAbility != 4) - { - AddLine(Severity.Invalid, V108, CheckIdentifier.Ability); - return true; - } - - if (EncounterMatch is EncounterTradePID z) - { - if (z.Species != pkm.Species) - return false; // Must match PID ability, handle via default check path - if (EncounterAbility == 1 << abilval) - { - AddLine(Severity.Valid, V115, CheckIdentifier.Ability); - return true; - } - } - - if (!(AbilityUnchanged ?? false) || EncounterAbility == 0 || pkm.AbilityNumber == EncounterAbility) - return false; - if (IsAbilityCapsuleModified(pkm, abilities, EncounterAbility)) - AddLine(Severity.Valid, V109, CheckIdentifier.Ability); - else - AddLine(Severity.Invalid, V223, CheckIdentifier.Ability); - return true; - } - - // 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 bool? VerifyAbilityPreCapsule(int[] abilities, int abilval) - { - // CXD pokemon could have any ability without maching PID - if (pkm.Version == (int)GameVersion.CXD && pkm.Format == 3) - return null; - - // gen3 native or gen4/5 origin - if (pkm.Format == 3 || !pkm.InhabitedGeneration(3)) - return true; - - // Evovled in gen4/5 - if (pkm.Species > Legal.MaxSpeciesID_3) - return false; - - // gen3Species will be zero for pokemon with illegal gen 3 encounters, like Infernape with gen 3 "origin" - var gen3Species = Info.EvoChainsAllGens[3].FirstOrDefault()?.Species ?? 0; - if (gen3Species == 0) - return true; - - // Fall through when gen3 pkm transferred to gen4/5 - return VerifyAbilityGen3Transfer(abilities, abilval, gen3Species); - } - private bool? VerifyAbilityGen3Transfer(int[] abilities, int abilval, int Species_g3) - { - var pers = (PersonalInfoG3)PersonalTable.E[Species_g3]; - 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; - - int Species_g4 = Info.EvoChainsAllGens[4].FirstOrDefault()?.Species ?? 0; - int Species_g5 = pkm.Format == 5 ? Info.EvoChainsAllGens[5].FirstOrDefault()?.Species ?? 0 : 0; - if (Math.Max(Species_g5, Species_g4) > Species_g3) // it has evolved in either gen 4 or gen 5; the ability must match PID - return false; - - var Evolutions_g45 = Math.Max(Info.EvoChainsAllGens[4].Count, pkm.Format == 5 ? Info.EvoChainsAllGens[5].Count : 0); - if (Evolutions_g45 > 1) - { - // Evolutions_g45 > 1 and Species_g45 = Species_g3 with means both options, evolve in gen 4-5 or not evolve, are possible - if (pkm.Ability == pers.Ability1) - // It could evolve in gen 4-5 an have generation 3 only ability - // that means it have not actually evolved in gen 4-5, ability do not need to match PID - return null; - if (pkm.Ability == abilities[1]) - // It could evolve in gen4-5 an have generation 4 second ability - // that means it have actually evolved in gen 4-5, ability must match PID - return false; - } - // Evolutions_g45 == 1 means it have not evolved in gen 4-5 games, - // ability do not need to match PID, but only generation 3 ability is allowed - if (pkm.Ability != pers.Ability1) - // Not evolved in gen4-5 but do not have generation 3 only ability - AddLine(Severity.Invalid, V373, CheckIdentifier.Ability); - return null; - } - private void VerifyAbility5(int[] abilities) - { - switch (EncounterMatch) - { - case PGF g: - VerifyAbilityMG456(abilities, g.AbilityType); - break; - - case EncounterSlot w: - // Hidden Abilities for Wild Encounters are only available at a Hidden Grotto - bool grotto = w.Type == SlotType.HiddenGrotto; - if (pkm.AbilityNumber == 4 ^ grotto) - AddLine(Severity.Invalid, grotto ? V217 : V108, CheckIdentifier.Ability); - break; - - case EncounterEgg e when pkm.AbilityNumber == 4: - // Hidden Abilities for some are unbreedable (male only distribution) - if (Legal.MixedGenderBreeding.Contains(e.Species) || Legal.FixedGenderFromBiGender.Contains(e.Species)) - break; // from female - if ((pkm.PersonalInfo.Gender & 0xFF) == 0 || Legal.Ban_BreedHidden.Contains(e.Species)) - AddLine(Severity.Invalid, V112, CheckIdentifier.Ability); - break; - } - } - private void VerifyAbility6(int[] abilities) - { - if (EncounterMatch is EncounterSlot slot && pkm.AbilityNumber == 4) - { - bool valid = slot.Permissions.DexNav || slot.Type == SlotType.FriendSafari || slot.Type == SlotType.Horde; - if (!valid) - AddLine(Severity.Invalid, V300, CheckIdentifier.Ability); - } - else if (EncounterMatch is WC6 g) - VerifyAbilityMG456(abilities, g.AbilityType); - else if (Legal.Ban_NoHidden6.Contains(pkm.SpecForm) && pkm.AbilityNumber == 4) - AddLine(Severity.Invalid, V112, CheckIdentifier.Ability); - } - private void VerifyAbility7(int[] abilities) - { - if (EncounterMatch is EncounterSlot slot && pkm.AbilityNumber == 4) - { - bool valid = slot.Type == SlotType.SOS; - if (!valid) - AddLine(Severity.Invalid, V111, CheckIdentifier.Ability); - } - else if (EncounterMatch is WC7 g) - VerifyAbilityMG456(abilities, g.AbilityType); - else if (Legal.Ban_NoHidden7.Contains(pkm.SpecForm) && pkm.AbilityNumber == 4) - AddLine(Severity.Invalid, V112, CheckIdentifier.Ability); - } - private void VerifyAbilityMG456(int[] abilities, int cardtype) - { - int abilNumber = pkm.AbilityNumber; - if (cardtype < 3 && abilNumber != 1 << cardtype) // set number - { - // Ability can be flipped 0/1 if Ability Capsule is available, is not Hidden Ability, and Abilities are different. - if (pkm.Format >= 6 && cardtype < 2 && abilNumber < 3 && abilities[0] != abilities[1]) - AddLine(Severity.Valid, V109, CheckIdentifier.Ability); - else - AddLine(Severity.Invalid, V110, CheckIdentifier.Ability); - } - else if (cardtype == 3 && abilNumber == 4) // 1/2 only - AddLine(Severity.Invalid, V110, CheckIdentifier.Ability); - } - #region VerifyBall - private void VerifyBallEquals(int ball) => AddBallLine(ball == pkm.Ball); - private void VerifyBallEquals(ICollection balls) => AddBallLine(balls.Contains(pkm.Ball)); - private void AddBallLine(bool valid) - { - if (valid) - AddLine(Severity.Valid, V119, CheckIdentifier.Ball); - else - AddLine(Severity.Invalid, V118, CheckIdentifier.Ball); - } - private void VerifyBall() - { - if (pkm.Format < 3) - return; // no ball info saved - - if (!Encounter.Valid) - return; - - if (EncounterMatch is MysteryGift g) - { - if (pkm.Species == 490 && g.Ball == 0) - // there is no ball data in Manaphy Mystery Gift - VerifyBallEquals(4); // Pokeball - else - VerifyBallEquals(g.Ball); - return; - } - if (EncounterMatch is EncounterLink l) - { - VerifyBallEquals(l.Ball); - return; - } - if (EncounterMatch is EncounterTrade t) - { - VerifyBallEquals(t.Ball); // Pokeball - return; - } - - if (pkm.Species == 292 && Info.Generation > 3) // Shedinja. For gen3, copy the ball from Nincada - { - VerifyBallEquals(4); // Pokeball Only - return; - } - - if (pkm.Ball == 0x14 && !Info.EncounterMatch.EggEncounter && pkm.SM) // Heavy Ball - { - var lineage = EvolutionChain.GetLineage(pkm); - if (lineage.Any(e => Legal.AlolanCaptureNoHeavyBall.Contains(e))) - { - AddLine(Severity.Invalid, V116, CheckIdentifier.Ball); - return; - } - } - - if (EncounterMatch is EncounterStatic s) - { - if (s.Gift) - VerifyBallEquals(s.Ball); - else if (s.Location == 75 && s.Generation == 5) // Entree Forest (Dream World) - VerifyBallEquals(Legal.DreamWorldBalls); - else - VerifyBallEquals(Legal.GetWildBalls(pkm)); - return; - } - if (EncounterMatch is EncounterSlot w) - { - if (w.Location == 30016 && w.Generation == 7) // Poké Pelago - VerifyBallEquals(4); // Pokeball - // For gen3/4 Safari Zones and BCC getValidWildEncounters already filter to not return - // mixed possible encounters between safari, BCC and other encounters - // That means is the first encounter is not safari then there is no safari encounter in the array - else if (3 <= Info.Generation && Info.Generation <= 4 && w.Type.IsSafariType()) - VerifyBallEquals(5); // Safari Ball - else if (Info.Generation == 4 && w.Type == SlotType.BugContest) - VerifyBallEquals(0x18); // Sport Ball - else - VerifyBallEquals(Legal.GetWildBalls(pkm)); - return; - } - - if (pkm.WasEgg) - { - VerifyBallEgg(); - return; - } - - VerifyBallEquals(4); // Pokeball - } - private void VerifyBallEgg() - { - if (Info.Generation < 6) // No inheriting Balls - { - VerifyBallEquals(4); // Must be Pokéball -- no ball inheritance. - return; - } - - if (pkm.Ball == 0x01) // Master Ball - { AddLine(Severity.Invalid, V117, CheckIdentifier.Ball); return; } - if (pkm.Ball == 0x10) // Cherish Ball - { AddLine(Severity.Invalid, V120, CheckIdentifier.Ball); return; } - if (pkm.Ball == 0x04) // Poké Ball - { AddLine(Severity.Valid, V119, CheckIdentifier.Ball); return; } - - switch (Info.Generation) - { - case 6: // Gen6 Inheritance Rules - VerifyBallEggGen6(); - return; - case 7: // Gen7 Inheritance Rules - VerifyBallEggGen7(); - return; - } - } - private void VerifyBallEggGen6() - { - if (pkm.Gender == 2) // Genderless - { - VerifyBallEquals(4); // Must be Pokéball as ball can only pass via mother (not Ditto!) - return; - } - if (Legal.BreedMaleOnly.Contains(pkm.Species)) - { - VerifyBallEquals(4); // Must be Pokéball as ball can only pass via mother (not Ditto!) - return; - } - - int ball = pkm.Ball; - - if (ball >= 26) - { - AddLine(Severity.Invalid, V126, CheckIdentifier.Ball); - return; - } - if (ball == 0x05) // Safari Ball - { - if (EvolutionChain.GetLineage(pkm).All(e => !Legal.Inherit_Safari.Contains(e))) - AddLine(Severity.Invalid, V121, CheckIdentifier.Ball); - else if (pkm.AbilityNumber == 4) - AddLine(Severity.Invalid, V122, CheckIdentifier.Ball); - else - AddLine(Severity.Valid, V123, CheckIdentifier.Ball); - - return; - } - if (0x10 < ball && ball < 0x18) // Apricorn Ball - { - if (EvolutionChain.GetLineage(pkm).All(e => !Legal.Inherit_Apricorn6.Contains(e))) - AddLine(Severity.Invalid, V121, CheckIdentifier.Ball); - if (pkm.AbilityNumber == 4) - AddLine(Severity.Invalid, V122, CheckIdentifier.Ball); - else - AddLine(Severity.Valid, V123, CheckIdentifier.Ball); - - return; - } - if (ball == 0x18) // Sport Ball - { - if (EvolutionChain.GetLineage(pkm).All(e => !Legal.Inherit_Sport.Contains(e))) - AddLine(Severity.Invalid, V121, CheckIdentifier.Ball); - else if (pkm.AbilityNumber == 4) - AddLine(Severity.Invalid, V122, CheckIdentifier.Ball); - else - AddLine(Severity.Valid, V123, CheckIdentifier.Ball); - - return; - } - if (ball == 0x19) // Dream Ball - { - if (EvolutionChain.GetLineage(pkm).Any(e => Legal.Inherit_Dream.Contains(e))) - AddLine(Severity.Valid, V123, CheckIdentifier.Ball); - else - AddLine(Severity.Invalid, V121, CheckIdentifier.Ball); - - if (pkm.AbilityNumber == 4 && Legal.Ban_DreamHidden.Contains(pkm.Species)) - AddLine(Severity.Invalid, V122, CheckIdentifier.Ball); - - return; - } - if (0x0D <= ball && ball <= 0x0F) - { - if (!Legal.Ban_Gen4Ball_6.Contains(pkm.Species)) - AddLine(Severity.Valid, V123, CheckIdentifier.Ball); - else - AddLine(Severity.Invalid, V121, CheckIdentifier.Ball); - - return; - } - if (0x02 <= ball && ball <= 0x0C) // Don't worry, Ball # 0x05 was already checked. - { - if (Legal.Ban_Gen3Ball.Contains(pkm.Species)) - AddLine(Severity.Invalid, V121, CheckIdentifier.Ball); - else if (pkm.AbilityNumber == 4 && Legal.Ban_Gen3BallHidden.Contains(pkm.SpecForm)) - AddLine(Severity.Invalid, V122, CheckIdentifier.Ball); - else - AddLine(Severity.Valid, V123, CheckIdentifier.Ball); - - return; - } - - if (pkm.Species > 650 && pkm.Species != 700) // Sylveon - { - if (Legal.GetWildBalls(pkm).Contains(pkm.Ball)) - AddLine(Severity.Valid, V123, CheckIdentifier.Ball); - else - AddLine(Severity.Invalid, V121, CheckIdentifier.Ball); - return; - } - - AddLine(Severity.Invalid, V125, CheckIdentifier.Ball); - } - private void VerifyBallEggGen7() - { - var Lineage = EvolutionChain.GetLineage(pkm).ToArray(); - if (722 <= pkm.Species && pkm.Species <= 730) // G7 Starters - { - VerifyBallEquals(4); - return; - } - - int ball = pkm.Ball; - if (ball == 0x05) // Safari Ball - { - if (!Lineage.Any(e => Legal.Inherit_Safari.Contains(e) || Legal.Inherit_SafariMale.Contains(e))) - AddLine(Severity.Invalid, V121, CheckIdentifier.Ball); - else if (pkm.AbilityNumber == 4 && Lineage.Any(e => Legal.Ban_SafariBallHidden_7.Contains(e))) - AddLine(Severity.Invalid, V122, CheckIdentifier.Ball); - else - AddLine(Severity.Valid, V123, CheckIdentifier.Ball); - - return; - } - if (0x10 < ball && ball < 0x18) // Apricorn Ball - { - if (!Lineage.Any(e => Legal.Inherit_Apricorn7.Contains(e))) - AddLine(Severity.Invalid, V121, CheckIdentifier.Ball); - else if (pkm.AbilityNumber == 4 && Legal.Ban_NoHidden7Apricorn.Contains(Lineage.Last() | pkm.AltForm << 11)) // lineage is 3->2->origin - AddLine(Severity.Invalid, V122, CheckIdentifier.Ball); - else - AddLine(Severity.Valid, V123, CheckIdentifier.Ball); - - return; - } - if (ball == 0x18) // Sport Ball - { - if (!Lineage.Any(e => Legal.Inherit_Sport.Contains(e))) - AddLine(Severity.Invalid, V121, CheckIdentifier.Ball); - else if (pkm.AbilityNumber == 4 && (Lineage.Contains(313) || Lineage.Contains(314))) // Volbeat/Illumise - AddLine(Severity.Invalid, V122, CheckIdentifier.Ball); - else - AddLine(Severity.Valid, V123, CheckIdentifier.Ball); - - return; - } - if (ball == 0x19) // Dream Ball - { - if (Lineage.Any(e => Legal.Inherit_Dream.Contains(e) || Legal.Inherit_DreamMale.Contains(e))) - AddLine(Severity.Valid, V123, CheckIdentifier.Ball); - else - AddLine(Severity.Invalid, V121, CheckIdentifier.Ball); - - return; - } - if (0x0D <= ball && ball <= 0x0F) // Dusk Heal Quick - { - if (!Legal.Ban_Gen4Ball_7.Contains(pkm.Species)) - AddLine(Severity.Valid, V123, CheckIdentifier.Ball); - else - AddLine(Severity.Invalid, V121, CheckIdentifier.Ball); - - return; - } - if (0x02 <= ball && ball <= 0x0C) // Don't worry, Ball # 0x05 was already checked. - { - if (!Legal.Ban_Gen3Ball_7.Contains(pkm.Species)) - AddLine(Severity.Valid, V123, CheckIdentifier.Ball); - else - AddLine(Severity.Invalid, V121, CheckIdentifier.Ball); - - return; - } - - if (ball == 26) - { - if (Lineage[0] == 669 && pkm.AltForm == 3 && pkm.AbilityNumber == 4) - { - // Can't obtain Flabébé-Blue with Hidden Ability in wild - AddLine(Severity.Invalid, V122, CheckIdentifier.Ball); - return; - } - if ((pkm.Species > 731 && pkm.Species <= 785) || Lineage.Any(e => Legal.PastGenAlolanNatives.Contains(e) && !Legal.PastGenAlolanNativesUncapturable.Contains(e))) - { - AddLine(Severity.Valid, V123, CheckIdentifier.Ball); - return; - } - if (Lineage.Any(e => Legal.PastGenAlolanScans.Contains(e))) - { - AddLine(Severity.Valid, V123, CheckIdentifier.Ball); - return; - } - // next statement catches all new alolans - } - - if (pkm.Species > 721) - { - VerifyBallEquals(Legal.GetWildBalls(pkm)); - return; - } - - if (ball >= 27) - { - AddLine(Severity.Invalid, V126, CheckIdentifier.Ball); - return; - } - AddLine(Severity.Invalid, V125, CheckIdentifier.Ball); - } - #endregion - private CheckResult VerifyHistory() - { - if (!Encounter.Valid) - return new CheckResult(Severity.Valid, V127, CheckIdentifier.History); - - // ORAS contests mistakenly apply 20 affection to the OT instead of the current handler's value - bool IsInvalidContestAffection() => pkm.OT_Affection != 255 && pkm.OT_Affection % 20 != 0; - - if (Info.Generation < 6) - { - if (pkm.Format < 6) - return new CheckResult(Severity.Valid, V128, CheckIdentifier.History); - - if (pkm.OT_Affection != 0 && Info.Generation <= 2 || IsInvalidContestAffection()) - return new CheckResult(Severity.Invalid, V129, CheckIdentifier.History); - if (pkm.OT_Memory > 0 || pkm.OT_Feeling > 0 || pkm.OT_Intensity > 0 || pkm.OT_TextVar > 0) - return new CheckResult(Severity.Invalid, V130, CheckIdentifier.History); - } - - if (pkm.Format >= 6 && Info.Generation != pkm.Format && pkm.CurrentHandler != 1) - return new CheckResult(Severity.Invalid, V124, CheckIdentifier.History); - - if (pkm.HT_Gender > 1) - return new CheckResult(Severity.Invalid, string.Format(V131, pkm.HT_Gender), CheckIdentifier.History); - - if (EncounterMatch is WC6 wc6 && wc6.OT_Name.Length > 0) - { - if (pkm.OT_Friendship != PersonalTable.AO[EncounterMatch.Species].BaseFriendship) - return new CheckResult(Severity.Invalid, V132, CheckIdentifier.History); - if (pkm.OT_Affection != 0 && (pkm.AO || !pkm.IsUntraded) && IsInvalidContestAffection()) - return new CheckResult(Severity.Invalid, V133, CheckIdentifier.History); - if (pkm.CurrentHandler != 1) - return new CheckResult(Severity.Invalid, V134, CheckIdentifier.History); - } - else if (EncounterMatch is WC7 wc7 && wc7.OT_Name.Length > 0 && wc7.TID != 18075) // Ash Pikachu QR Gift doesn't set Current Handler - { - if (pkm.OT_Friendship != PersonalTable.USUM[EncounterMatch.Species].BaseFriendship) - return new CheckResult(Severity.Invalid, V132, CheckIdentifier.History); - if (pkm.OT_Affection != 0) - return new CheckResult(Severity.Invalid, V133, CheckIdentifier.History); - if (pkm.CurrentHandler != 1) - return new CheckResult(Severity.Invalid, V134, CheckIdentifier.History); - } - else if (EncounterMatch is MysteryGift mg && mg.Format < 6 && pkm.Format >= 6) - { - if (pkm.OT_Affection != 0 && IsInvalidContestAffection()) - return new CheckResult(Severity.Invalid, V133, CheckIdentifier.History); - if (pkm.CurrentHandler != 1) - return new CheckResult(Severity.Invalid, V134, CheckIdentifier.History); - } - - // Geolocations - var geo = new[] - { - pkm.Geo1_Country, pkm.Geo2_Country, pkm.Geo3_Country, pkm.Geo4_Country, pkm.Geo5_Country, - pkm.Geo1_Region, pkm.Geo2_Region, pkm.Geo3_Region, pkm.Geo4_Region, pkm.Geo5_Region, - }; - - // Check sequential order (no zero gaps) - bool geoEnd = false; - for (int i = 0; i < 5; i++) - { - if (geoEnd && geo[i] != 0) - return new CheckResult(Severity.Invalid, V135, CheckIdentifier.History); - - if (geo[i] != 0) - continue; - if (geo[i + 5] != 0) - return new CheckResult(Severity.Invalid, V136, CheckIdentifier.History); - geoEnd = true; - } - if (pkm.Format >= 7) - return VerifyHistory7(geo); - - // Determine if we should check for Handling Trainer Memories - // A Pokémon is untraded if... - bool untraded = pkm.HT_Name.Length == 0 || pkm.Geo1_Country == 0; - if (EncounterMatch is MysteryGift gift) - { - untraded |= !pkm.WasEventEgg; - untraded &= gift.IsEgg; - } - - if (EncounterMatch is EncounterLink link && !link.OT) - untraded = false; - else if (Info.Generation < 6) - untraded = false; - - if (untraded) // Is not Traded - { - if (pkm.HT_Name.Length != 0) - return new CheckResult(Severity.Invalid, V146, CheckIdentifier.History); - if (pkm.Geo1_Country != 0) - return new CheckResult(Severity.Invalid, V147, CheckIdentifier.History); - if (pkm.HT_Memory != 0) - return new CheckResult(Severity.Invalid, V148, CheckIdentifier.History); - if (pkm.CurrentHandler != 0) // Badly edited; PKHeX doesn't trip this. - return new CheckResult(Severity.Invalid, V139, CheckIdentifier.History); - if (pkm.HT_Friendship != 0) - return new CheckResult(Severity.Invalid, V140, CheckIdentifier.History); - if (pkm.HT_Affection != 0) - return new CheckResult(Severity.Invalid, V141, CheckIdentifier.History); - if (pkm.XY && pkm is IContestStats s && s.HasContestStats()) - return new CheckResult(Severity.Invalid, V138, CheckIdentifier.History); - - if (VerifyHistoryUntradedHandler(pkm, out CheckResult chk1)) - return chk1; - if (EncounterMatch.Species != pkm.Species && VerifyHistoryUntradedEvolution(pkm, Info.EvoChainsAllGens, out CheckResult chk2)) - return chk2; - } - else // Is Traded - { - if (pkm.Format == 6 && pkm.HT_Memory == 0 && !pkm.IsEgg) - return new CheckResult(Severity.Invalid, V150, CheckIdentifier.History); - } - - // Memory ChecksResult - if (pkm.IsEgg) - { - if (pkm.HT_Memory != 0) - return new CheckResult(Severity.Invalid, V149, CheckIdentifier.History); - if (pkm.OT_Memory != 0) - return new CheckResult(Severity.Invalid, V151, CheckIdentifier.History); - } - else if (!(EncounterMatch is WC6)) - { - if (pkm.OT_Memory == 0 ^ !pkm.Gen6) - return new CheckResult(Severity.Invalid, V152, CheckIdentifier.History); - if (Info.Generation < 6 && pkm.OT_Affection != 0) - return new CheckResult(Severity.Invalid, V129, CheckIdentifier.History); - } - // Unimplemented: Ingame Trade Memories - - return new CheckResult(Severity.Valid, V145, CheckIdentifier.History); - } - private CheckResult VerifyHistory7(int[] geo) - { - if (pkm.VC1) - { - var hasGeo = geo.Any(d => d != 0); - - if (!hasGeo) - return new CheckResult(Severity.Invalid, V137, CheckIdentifier.History); - } - - if ((2 >= Info.Generation || Info.Generation >= 7) && pkm is IContestStats s && s.HasContestStats()) - return new CheckResult(Severity.Invalid, V138, CheckIdentifier.History); - - if (!pkm.WasEvent && pkm.HT_Name.Length == 0) // Is not Traded - { - if (VerifyHistoryUntradedHandler(pkm, out CheckResult chk1)) - return chk1; - if (EncounterMatch.Species != pkm.Species && VerifyHistoryUntradedEvolution(pkm, Info.EvoChainsAllGens, out CheckResult chk2)) - return chk2; - } - - return new CheckResult(Severity.Valid, V145, CheckIdentifier.History); - } - private static bool VerifyHistoryUntradedHandler(PKM pkm, out CheckResult result) - { - result = null; - if (pkm.CurrentHandler != 0) // Badly edited; PKHeX doesn't trip this. - result = new CheckResult(Severity.Invalid, V139, CheckIdentifier.History); - else if (pkm.HT_Friendship != 0) - result = new CheckResult(Severity.Invalid, V140, CheckIdentifier.History); - else if (pkm.HT_Affection != 0) - result = new CheckResult(Severity.Invalid, V141, CheckIdentifier.History); - else - return false; - - return true; - } - private static bool VerifyHistoryUntradedEvolution(PKM pkm, IReadOnlyList[] chain, out CheckResult result) - { - result = null; - // Handling Trainer string is empty implying it has not been traded. - // If it must be trade evolved, flag it. - - if (pkm.Species == 350) // Milotic - { - if (Legal.IsTradeEvolved(chain, pkm.Format)) - return false; - if (pkm is IContestStats s && s.CNT_Beauty < 170) // Beauty Contest Stat Requirement - result = new CheckResult(Severity.Invalid, V143, CheckIdentifier.History); - else if (pkm.CurrentLevel == 1) - result = new CheckResult(Severity.Invalid, V144, CheckIdentifier.History); - else - return false; - return true; - } - if (!Legal.IsTradeEvolved(chain, pkm.Format)) - return false; - result = new CheckResult(Severity.Invalid, V142, CheckIdentifier.History); - return true; - } - private CheckResult VerifyCommonMemory(int handler) - { - Memories.GetMemoryVariables(pkm, out int m, out int t, out int i, out int f, out string tr, handler); - int matchingMoveMemory = Array.IndexOf(Memories.MoveSpecificMemories[0], m); - if (matchingMoveMemory != -1 && pkm.Species != 235 && !Legal.GetCanLearnMachineMove(pkm, Memories.MoveSpecificMemories[1][matchingMoveMemory], 6)) - return new CheckResult(Severity.Invalid, string.Format(V153, tr), CheckIdentifier.Memory); - - if (m == 6 && !Memories.LocationsWithPKCenter[0].Contains(t)) - return new CheckResult(Severity.Invalid, string.Format(V154, tr), CheckIdentifier.Memory); - - if (m == 21) // {0} saw {2} carrying {1} on its back. {4} that {3}. - if (!Legal.GetCanLearnMachineMove(new PK6 { Species = t, EXP = PKX.GetEXP(100, t) }, 19, 6)) - return new CheckResult(Severity.Invalid, string.Format(V153, tr), CheckIdentifier.Memory); - - if ((m == 16 || m == 48) && (t == 0 || !Legal.GetCanKnowMove(pkm, t, 6))) - return new CheckResult(Severity.Invalid, string.Format(V153, tr), CheckIdentifier.Memory); - - if (m == 49 && (t == 0 || !Legal.GetCanRelearnMove(pkm, t, 6))) // {0} was able to remember {2} at {1}'s instruction. {4} that {3}. - return new CheckResult(Severity.Invalid, string.Format(V153, tr), CheckIdentifier.Memory); - - if (i < Memories.MemoryMinIntensity[m]) - return new CheckResult(Severity.Invalid, string.Format(V254, tr, Memories.MemoryMinIntensity[m]), CheckIdentifier.Memory); - - if (m != 4 && (Memories.MemoryFeelings[m] & (1 << f)) == 0) - return new CheckResult(Severity.Invalid, string.Format(V255, tr), CheckIdentifier.Memory); - - return new CheckResult(Severity.Valid, string.Format(V155, tr), CheckIdentifier.Memory); - } - - private void VerifyOTMemoryIs(int[] values) - { - if (pkm.OT_Memory != values[0]) - AddLine(Severity.Invalid, string.Format(V197, V205, values[0]), CheckIdentifier.Memory); - if (pkm.OT_Intensity != values[1]) - AddLine(Severity.Invalid, string.Format(V198, V205, values[1]), CheckIdentifier.Memory); - if (pkm.OT_TextVar != values[2]) - AddLine(Severity.Invalid, string.Format(V199, V205, values[2]), CheckIdentifier.Memory); - if (pkm.OT_Feeling != values[3]) - AddLine(Severity.Invalid, string.Format(V200, V205, values[3]), CheckIdentifier.Memory); - } - private void VerifyOTMemory() - { - if (pkm.Format < 6) - return; - - if (!History.Valid) - return; - - if (Info.Generation < 6 || pkm.IsEgg) - { - VerifyOTMemoryIs(new [] {0, 0, 0, 0}); // empty - return; - } - - switch (EncounterMatch) - { - case EncounterTrade _: - switch (Info.Generation) - { - case 6: - break; // Undocumented, uncommon, and insignificant -- don't bother. - case 7: - VerifyOTMemoryIs(new[] { 1, 3, 40, 5 }); - break; - } - return; - case WC6 g when !g.IsEgg: - VerifyOTMemoryIs(new[] { g.OT_Memory, g.OT_Intensity, g.OT_TextVar, g.OT_Feeling }); - return; - case WC7 g when !g.IsEgg: - VerifyOTMemoryIs(new[] { g.OT_Memory, g.OT_Intensity, g.OT_TextVar, g.OT_Feeling }); - return; - } - - if (Info.Generation >= 7) - { - VerifyOTMemoryIs(new[] {0, 0, 0, 0}); // empty - return; - } - - switch (pkm.OT_Memory) - { - case 2: // {0} hatched from an Egg and saw {1} for the first time at... {2}. {4} that {3}. - if (pkm.Egg_Location == 0) - AddLine(Severity.Invalid, string.Format(V160, V205), CheckIdentifier.Memory); - break; - - case 4: // {0} became {1}’s friend when it arrived via Link Trade at... {2}. {4} that {3}. - AddLine(Severity.Invalid, string.Format(V161, V205), CheckIdentifier.Memory); - return; - - case 6: // {0} went to the Pokémon Center in {2} with {1} and had its tired body healed there. {4} that {3}. - int matchingOriginGame = Array.IndexOf(Memories.LocationsWithPKCenter[0], pkm.OT_TextVar); - if (matchingOriginGame != -1) - { - int gameID = Memories.LocationsWithPKCenter[1][matchingOriginGame]; - if (pkm.XY && gameID != 0 || pkm.AO && gameID != 1) - AddLine(Severity.Invalid, string.Format(V162, V205), CheckIdentifier.Memory); - } - AddLine(VerifyCommonMemory(0)); - return; - - case 14: - if (!Legal.GetCanBeCaptured(pkm.OT_TextVar, Info.Generation, (GameVersion)pkm.Version)) - AddLine(Severity.Invalid, string.Format(V165, V205), CheckIdentifier.Memory); - else - AddLine(Severity.Valid, string.Format(V164, V205), CheckIdentifier.Memory); - return; - } - if (pkm.XY && Memories.Memory_NotXY.Contains(pkm.OT_Memory)) - AddLine(Severity.Invalid, string.Format(V163, V205), CheckIdentifier.Memory); - if (pkm.AO && Memories.Memory_NotAO.Contains(pkm.OT_Memory)) - AddLine(Severity.Invalid, string.Format(V163, V205), CheckIdentifier.Memory); - - AddLine(VerifyCommonMemory(0)); - } - private void VerifyHTMemory() - { - if (pkm.Format < 6) - return; - - if (!History.Valid) - return; - - if (pkm.Format >= 7) - { - /* - * Bank Transfer adds in the Link Trade Memory. - * Trading 7<->7 between games (not Bank) clears this data. - */ - if (pkm.HT_Memory == 0) - { - if (pkm.HT_TextVar != 0 || pkm.HT_Intensity != 0 || pkm.HT_Feeling != 0) - AddLine(Severity.Invalid, V329, CheckIdentifier.Memory); - return; - } - - // Transfer 6->7 & withdraw to same HT => keeps past gen memory - // Don't require link trade memory for these past gen cases - int gen = Info.Generation; - if (3 <= gen && gen < 7 && pkm.CurrentHandler == 1) - return; - - if (pkm.HT_Memory != 4) - AddLine(Severity.Invalid, V156, CheckIdentifier.Memory); - if (pkm.HT_TextVar != 0) - AddLine(Severity.Invalid, V157, CheckIdentifier.Memory); - if (pkm.HT_Intensity != 1) - AddLine(Severity.Invalid, V158, CheckIdentifier.Memory); - if (pkm.HT_Feeling > 10) - AddLine(Severity.Invalid, V159, CheckIdentifier.Memory); - return; - } - - switch (pkm.HT_Memory) - { - case 0: - if (string.IsNullOrEmpty(pkm.HT_Name)) - return; - AddLine(Severity.Invalid, V150, CheckIdentifier.Memory); return; - case 1: // {0} met {1} at... {2}. {1} threw a Poké Ball at it, and they started to travel together. {4} that {3}. - AddLine(Severity.Invalid, string.Format(V202, V206), CheckIdentifier.Memory); return; - - case 2: // {0} hatched from an Egg and saw {1} for the first time at... {2}. {4} that {3}. - AddLine(Severity.Invalid, string.Format(V160, V206), CheckIdentifier.Memory); return; - - case 14: - if (Legal.GetCanBeCaptured(pkm.HT_TextVar, 6)) - AddLine(Severity.Valid, string.Format(V164, V206), CheckIdentifier.Memory); - else - AddLine(Severity.Invalid, string.Format(V165, V206), CheckIdentifier.Memory); - return; - } - AddLine(VerifyCommonMemory(1)); - } - private void VerifyConsoleRegion() - { - AddLine(VerifyConsoleRegion(pkm)); - } - private static CheckResult VerifyConsoleRegion(PKM pkm) - { - int consoleRegion = pkm.ConsoleRegion; - if (consoleRegion >= 7) - return new CheckResult(Severity.Invalid, V301, CheckIdentifier.Geography); - return Legal.IsConsoleRegionCountryValid(consoleRegion, pkm.Country) - ? new CheckResult(Severity.Valid, V303, CheckIdentifier.Geography) - : new CheckResult(Severity.Invalid, V302, CheckIdentifier.Geography); - } - private void VerifyForm() - { - if (!Encounter.Valid) - return; - - if (pkm.Format < 4) - return; // no forms exist - - int count = PersonalInfo.FormeCount; - if (count <= 1 && pkm.AltForm == 0) - return; // no forms to check - - if (!PersonalInfo.IsFormeWithinRange(pkm.AltForm) && !FormConverter.IsValidOutOfBoundsForme(pkm.Species, pkm.AltForm, Info.Generation)) - { - AddLine(Severity.Invalid, string.Format(V304, count-1, pkm.AltForm), CheckIdentifier.Form); - return; - } - - if (EncounterMatch is EncounterSlot w && w.Type == SlotType.FriendSafari) - VerifyFormFriendSafari(); - else if (EncounterMatch is EncounterEgg) - { - if (FormConverter.IsTotemForm(pkm.Species, pkm.AltForm)) - { - AddLine(Severity.Invalid, V317, CheckIdentifier.Form); - return; - } - } - - switch (pkm.Species) - { - case 25 when Info.Generation == 6: // Pikachu Cosplay - bool isStatic = EncounterMatch is EncounterStatic; - if (isStatic != (pkm.AltForm != 0)) - { - string msg = isStatic ? V305 : V306; - AddLine(Severity.Invalid, msg, CheckIdentifier.Form); - return; - } - break; - case 25 when Info.Generation == 7: // Pikachu Cap - bool IsValidPikachuCap() - { - switch (EncounterMatch) - { - default: return pkm.AltForm == 0; - case WC7 wc7: return wc7.Form == pkm.AltForm; - case EncounterStatic s: return s.Form == pkm.AltForm; - } - } - - if (!IsValidPikachuCap()) - { - bool gift = EncounterMatch is WC7 g && g.Form != pkm.AltForm; - var msg = gift ? V307 : V317; - AddLine(Severity.Invalid, msg, CheckIdentifier.Form); - return; - } - break; - case 201 when Info.Generation == 2 && pkm.AltForm >= 26: - AddLine(Severity.Invalid, string.Format(V304, "Z", pkm.AltForm == 26 ? "!" : "?"), CheckIdentifier.Form); - break; - case 487: // Giratina - if (pkm.AltForm == 1 ^ pkm.HeldItem == 112) // Origin form only with Griseous Orb - { - AddLine(Severity.Invalid, V308, CheckIdentifier.Form); - return; - } - break; - case 493: // Arceus - { - int item = pkm.HeldItem; - int form = 0; - if (298 <= item && item <= 313 || item == 644) - form = Array.IndexOf(Legal.Arceus_Plate, item) + 1; - else if (777 <= item && item <= 793) - form = Array.IndexOf(Legal.Arceus_ZCrystal, item) + 1; - if (pkm.Format == 4 && form >= 9) - form++; // ??? type Form shifts everything by 1 - - if (form != pkm.AltForm) - AddLine(Severity.Invalid, V308, CheckIdentifier.Form); - else if (form != 0) - AddLine(Severity.Valid, V309, CheckIdentifier.Form); - } - break; - case 647: // Keldeo - { - if (pkm.Gen5) // can mismatch in gen5 via BW tutor and transfer up - break; - int index = Array.IndexOf(pkm.Moves, 548); // Secret Sword - bool noSword = index < 0; - if (pkm.AltForm == 0 ^ noSword) // mismatch - Info.Moves[noSword ? 0 : index] = new CheckMoveResult(Info.Moves[noSword ? 0 : index], Severity.Invalid, V169, CheckIdentifier.Move); - break; - } - case 649: // Genesect - { - int item = pkm.HeldItem; - int form = 0; - if (116 <= item && item <= 119) - form = item - 115; - - if (form != pkm.AltForm) - AddLine(Severity.Invalid, V308, CheckIdentifier.Form); - else - AddLine(Severity.Valid, V309, CheckIdentifier.Form); - } - break; - case 658: // Greninja - if (pkm.AltForm > 1) // Ash Battle Bond active - { - AddLine(Severity.Invalid, V310, CheckIdentifier.Form); - return; - } - if (pkm.AltForm != 0 && !(EncounterMatch is MysteryGift)) // Formes are not breedable, MysteryGift already checked - { - AddLine(Severity.Invalid, string.Format(V304, 0, pkm.AltForm), CheckIdentifier.Form); - return; - } - break; - case 664: // Scatterbug - case 665: // Spewpa - if (pkm.AltForm > 17) // Fancy & Pokéball - { - AddLine(Severity.Invalid, V311, CheckIdentifier.Form); - return; - } - if (!Legal.CheckVivillonPattern(pkm.AltForm, pkm.Country, pkm.Region)) - AddLine(Severity.Fishy, V312, CheckIdentifier.Form); - break; - case 666: // Vivillon - if (pkm.AltForm > 17) // Fancy & Pokéball - { - if (!(EncounterMatch is MysteryGift)) - AddLine(Severity.Invalid, V312, CheckIdentifier.Form); - else - AddLine(Severity.Valid, V313, CheckIdentifier.Form); - - return; - } - if (!Legal.CheckVivillonPattern(pkm.AltForm, pkm.Country, pkm.Region)) - AddLine(Severity.Fishy, V312, CheckIdentifier.Form); - break; - case 670: // Floette - if (pkm.AltForm == 5) // Eternal Flower -- Never Released - { - if (!(EncounterMatch is MysteryGift)) - AddLine(Severity.Invalid, V314, CheckIdentifier.Form); - else - AddLine(Severity.Valid, V315, CheckIdentifier.Form); - - return; - } - break; - case 678: // Meowstic - if (pkm.AltForm != pkm.Gender) - AddLine(Severity.Invalid, V203, CheckIdentifier.Form); - break; - case 773: // Silvally - { - int item = pkm.HeldItem; - int form = 0; - if ((904 <= item && item <= 920) || item == 644) - form = item - 903; - if (form != pkm.AltForm) - AddLine(Severity.Invalid, V308, CheckIdentifier.Form); - else if (form != 0) - AddLine(Severity.Valid, V309, CheckIdentifier.Form); - break; - } - - case 744 when Info.EncounterMatch.EggEncounter && pkm.AltForm == 1 && pkm.SM: - case 745 when Info.EncounterMatch.EggEncounter && pkm.AltForm == 2 && pkm.SM: - AddLine(Severity.Invalid, V317, CheckIdentifier.Form); - return; - - // Impossible Egg forms - case 479 when pkm.IsEgg: // Rotom - case 676 when pkm.IsEgg: // Furfrou - if (pkm.AltForm != 0) // has form - { - AddLine(Severity.Invalid, V50, CheckIdentifier.Form); - return; - } - break; - - // Party Only Forms - case 492: // Shaymin - case 676: // Furfrou - case 720: // Hoopa - if (pkm.AltForm != 0 && pkm.Box > -1 && pkm.Format <= 6) // has form but stored in box - { - AddLine(Severity.Invalid, V316, CheckIdentifier.Form); - return; - } - break; - - // Battle only Forms with other legal forms allowed - case 718 when pkm.AltForm >= 4: // Zygarde Complete - case 774 when pkm.AltForm < 7: // Minior Shield - case 800 when pkm.AltForm == 3: // Ultra Necrozma - AddLine(Severity.Invalid, V310, CheckIdentifier.Form); - return; - case 800 when pkm.AltForm < 3: // Necrozma Fused forms & default - case 778 when pkm.AltForm == 2: // Totem disguise Mimikyu - AddLine(Severity.Valid, V318, CheckIdentifier.Form); - return; - } - - if (pkm.Format >= 7 && Info.Generation < 7 && pkm.AltForm != 0) - { - if (pkm.Species == 25 || Legal.AlolanOriginForms.Contains(pkm.Species) - || Legal.AlolanVariantEvolutions12.Contains((EncounterOriginalGB ?? EncounterMatch).Species)) - { AddLine(Severity.Invalid, V317, CheckIdentifier.Form); return; } - } - if (pkm.AltForm > 0 && new[] {Legal.BattleForms, Legal.BattleMegas, Legal.BattlePrimals}.Any(arr => arr.Contains(pkm.Species))) - { AddLine(Severity.Invalid, V310, CheckIdentifier.Form); return; } - - AddLine(Severity.Valid, V318, CheckIdentifier.Form); - } - private void VerifyMiscG1() - { - if (pkm.IsEgg) - { - VerifyMiscEggCommon(); - if (pkm.PKRS_Cured || pkm.PKRS_Infected) - AddLine(Severity.Invalid, V368, CheckIdentifier.Egg); - } - - if (!(pkm is PK1 pk1)) - return; - - VerifyMiscG1Types(pk1); - VerifyMiscG1CatchRate(pk1); - } - private void VerifyMiscG1Types(PK1 pk1) - { - var Type_A = pk1.Type_A; - var Type_B = pk1.Type_B; - if (pkm.Species == 137) - { - // Porygon can have any type combination of any generation 1 species because of the move Conversion, - // that change Porygon type to match the oponent types - var Type_A_Match = Legal.Types_Gen1.Any(t => t == Type_A); - var Type_B_Match = Legal.Types_Gen1.Any(t => t == Type_B); - if (!Type_A_Match) - AddLine(Severity.Invalid, V386, CheckIdentifier.Misc); - if (!Type_B_Match) - AddLine(Severity.Invalid, V387, CheckIdentifier.Misc); - if (Type_A_Match && Type_B_Match) - { - var TypesAB_Match = PersonalTable.RB.IsValidTypeCombination(Type_A, Type_B); - if (TypesAB_Match) - AddLine(Severity.Valid, V391, CheckIdentifier.Misc); - else - AddLine(Severity.Invalid, V388, CheckIdentifier.Misc); - } - } - else // Types must match species types - { - var Type_A_Match = Type_A == PersonalTable.RB[pkm.Species].Type1; - var Type_B_Match = Type_B == PersonalTable.RB[pkm.Species].Type2; - - AddLine(Type_A_Match ? Severity.Valid : Severity.Invalid, Type_A_Match ? V392 : V389, CheckIdentifier.Misc); - AddLine(Type_B_Match ? Severity.Valid : Severity.Invalid, Type_B_Match ? V393 : V390, CheckIdentifier.Misc); - } - } - private void VerifyMiscG1CatchRate(PK1 pk1) - { - var catch_rate = pk1.Catch_Rate; - switch (pkm.TradebackStatus) - { - case TradebackType.Any: - case TradebackType.WasTradeback: - if (catch_rate == 0 || Legal.HeldItems_GSC.Any(h => h == catch_rate)) - AddLine(Severity.Valid, V394, CheckIdentifier.Misc); - else if (pkm.TradebackStatus == TradebackType.WasTradeback) - AddLine(Severity.Invalid, V395, CheckIdentifier.Misc); - else - goto case TradebackType.Gen1_NotTradeback; - break; - case TradebackType.Gen1_NotTradeback: - if ((EncounterMatch as EncounterStatic)?.Version == GameVersion.Stadium || EncounterMatch is EncounterTradeCatchRate) - // Encounters detected by the catch rate, cant be invalid if match this encounters - AddLine(Severity.Valid, V398, CheckIdentifier.Misc); - else if (pkm.Species == 149 && catch_rate == PersonalTable.Y[149].CatchRate || Legal.Species_NotAvailable_CatchRate.Contains(pkm.Species) && catch_rate == PersonalTable.RB[pkm.Species].CatchRate) - AddLine(Severity.Invalid, V396, CheckIdentifier.Misc); - else if (!Info.EvoChainsAllGens[1].Any(e => catch_rate == PersonalTable.RB[e.Species].CatchRate || catch_rate == PersonalTable.Y[e.Species].CatchRate)) - AddLine(Severity.Invalid, pkm.Gen1_NotTradeback ? V397 : V399, CheckIdentifier.Misc); - else - AddLine(Severity.Valid, V398, CheckIdentifier.Misc); - break; - } + Nickname.Verify(this); + if (pkm.Format >= 3) + Language.Verify(this); } private void VerifyMisc() { - if (pkm is PK7 pk7 && pk7.PelagoEventStatus != 0) - { - // TODO: Figure out what PelagoEventStati are legal. - } - - if (pkm.IsEgg) - { - VerifyMiscEggCommon(); - - if (pkm is IContestStats s && s.HasContestStats()) - AddLine(Severity.Invalid, V320, CheckIdentifier.Egg); - if (pkm is PK4 pk4) - { - if (pk4.ShinyLeaf != 0) - AddLine(Severity.Invalid, V414, CheckIdentifier.Egg); - if (pk4.PokéathlonStat != 0) - AddLine(Severity.Invalid, V415, CheckIdentifier.Egg); - } - if (pkm is PK3) - { - if (pkm.Language != 1) // All Eggs are Japanese and flagged specially for localized string - AddLine(Severity.Invalid, string.Format(V5, LanguageID.Japanese, (LanguageID)pkm.Language), CheckIdentifier.Egg); - } - } - - if (!Encounter.Valid) - return; - if (Info.Generation == 5) - VerifyNsPKM(); + NHarmonia.Verify(this); + Misc.Verify(this); + } - VerifyMiscFatefulEncounter(); - } - private void VerifyMiscFatefulEncounter() - { - switch (EncounterMatch) - { - case WC3 w when w.Fateful: - if (w.IsEgg) - { - // Eggs hatched in RS clear the obedience flag! - if (pkm.Format != 3) - return; // possible hatched in either game, don't bother checking - if (pkm.Met_Location <= 087) // hatched in RS - break; // ensure fateful is not active - // else, ensure fateful is active (via below) - } - VerifyFatefulIngameActive(); - VerifyWC3Shiny(w); - return; - case WC3 w: - if (w.Version == GameVersion.XD) - return; // Can have either state - VerifyWC3Shiny(w); - break; - case MysteryGift g when g.Format != 3: // WC3 - VerifyFatefulMysteryGift(g); - return; - case EncounterStatic s when s.Fateful: // ingame fateful - case EncounterSlot _ when pkm.Version == 15: // ingame pokespot - case EncounterTrade t when t.Fateful: - VerifyFatefulIngameActive(); - return; - } - if (pkm.FatefulEncounter) - AddLine(Severity.Invalid, V325, CheckIdentifier.Fateful); - } - private void VerifyMiscEggCommon() - { - if (new[] {pkm.Move1_PPUps, pkm.Move2_PPUps, pkm.Move3_PPUps, pkm.Move4_PPUps}.Any(ppup => ppup > 0)) - AddLine(Severity.Invalid, V319, CheckIdentifier.Egg); - if (pkm.Move1_PP != pkm.GetMovePP(pkm.Move1, 0) || pkm.Move2_PP != pkm.GetMovePP(pkm.Move2, 0) - || pkm.Move3_PP != pkm.GetMovePP(pkm.Move3, 0) || pkm.Move4_PP != pkm.GetMovePP(pkm.Move4, 0)) - AddLine(Severity.Invalid, V420, CheckIdentifier.Egg); - - var HatchCycles = (EncounterMatch as EncounterStatic)?.EggCycles; - if (HatchCycles == 0 || HatchCycles == null) - HatchCycles = pkm.PersonalInfo.HatchCycles; - if (pkm.CurrentFriendship > HatchCycles) - AddLine(Severity.Invalid, V374, CheckIdentifier.Egg); - - if (pkm.Format >= 6 && EncounterMatch is EncounterEgg && !pkm.Moves.SequenceEqual(pkm.RelearnMoves)) - AddLine(Severity.Invalid, string.Format(V343, string.Join(", ", GetMoveNames(pkm.Moves))), CheckIdentifier.Egg); - } - private void VerifyFatefulMysteryGift(MysteryGift g) - { - if (g is PGF p && p.IsShiny) - { - Info.PIDIV = MethodFinder.Analyze(pkm); - if (Info.PIDIV.Type != PIDType.G5MGShiny && pkm.Egg_Location != 30003) - AddLine(Severity.Invalid, V411, CheckIdentifier.PID); - } - - if (pkm.FatefulEncounter) - AddLine(Severity.Valid, V321, CheckIdentifier.Fateful); - else - AddLine(Severity.Invalid, V322, CheckIdentifier.Fateful); - } - private void VerifyWC3Shiny(WC3 g3) - { - // check for shiny locked gifts - if (!g3.Shiny.IsValid(pkm)) - AddLine(Severity.Invalid, V409, CheckIdentifier.Fateful); - } - private void VerifyFatefulIngameActive() - { - if (pkm.Version == 15 && pkm is XK3 xk3 && Info.WasXD) - { - // can't have fateful until traded away, which clears ShadowID - if (xk3.FatefulEncounter && xk3.ShadowID != 0 && EncounterMatch is EncounterStaticShadow) - AddLine(Severity.Invalid, V325, CheckIdentifier.Fateful); - - return; // fateful is set when transferred away - } - - if (pkm.FatefulEncounter) - AddLine(Severity.Valid, V323, CheckIdentifier.Fateful); - else - AddLine(Severity.Invalid, V324, CheckIdentifier.Fateful); - } - private void VerifyNsPKM() - { - bool req = EncounterMatch is EncounterStaticPID s && s.NSparkle; - if (pkm is PK5 pk5) - { - bool has = pk5.NPokémon; - if (req && !has) - AddLine(Severity.Invalid, V326, CheckIdentifier.Fateful); - if (!req && has) - AddLine(Severity.Invalid, V327, CheckIdentifier.Fateful); - } - if (req) - { - if (pkm.IVs.Any(iv => iv != 30)) - AddLine(Severity.Invalid, V218, CheckIdentifier.IVs); - if (!VerifyNsPKMOTValid()) - AddLine(Severity.Invalid, V219, CheckIdentifier.Trainer); - if (pkm.IsShiny) - AddLine(Severity.Invalid, V220, CheckIdentifier.Shiny); - } - } - private bool VerifyNsPKMOTValid() - { - if (pkm.TID != 00002 || pkm.SID != 00000) - return false; - var ot = pkm.OT_Name; - if (ot.Length != 1) - return false; - var c = Legal.GetG5OT_NSparkle(pkm.Language); - return c == ot; - } - private void VerifyVersionEvolution() - { - if (pkm.Format < 7 || EncounterMatch.Species == pkm.Species) - return; - - // No point using the evolution tree. Just handle certain species. - switch (pkm.Species) - { - case 745 when (pkm.AltForm == 0 && Moon()) || (pkm.AltForm == 1 && Sun()): // Lycanroc - case 791 when Moon(): // Solgaleo - case 792 when Sun(): // Lunala - bool Sun() => pkm.Version == (int)GameVersion.SN || pkm.Version == (int)GameVersion.US; - bool Moon() => pkm.Version == (int)GameVersion.MN || pkm.Version == (int)GameVersion.UM; - if (pkm.IsUntraded) - AddLine(Severity.Invalid, V328, CheckIdentifier.Evolution); - break; - } - } public static string[] MoveStrings { internal get; set; } = Util.GetMovesList("en"); public static string[] SpeciesStrings { internal get; set; } = Util.GetSpeciesList("en"); internal static IEnumerable GetMoveNames(IEnumerable moves) => moves.Select(m => m >= MoveStrings.Length ? V190 : MoveStrings[m]); diff --git a/PKHeX.Core/Legality/Enums/CheckIdentifier.cs b/PKHeX.Core/Legality/Enums/CheckIdentifier.cs index ecb5531d2..d0324e3ec 100644 --- a/PKHeX.Core/Legality/Enums/CheckIdentifier.cs +++ b/PKHeX.Core/Legality/Enums/CheckIdentifier.cs @@ -1,7 +1,7 @@ namespace PKHeX.Core { /// Identification flair for what properties a pertains to - internal enum CheckIdentifier + public enum CheckIdentifier { /// /// The pertains to the . @@ -35,11 +35,11 @@ internal enum CheckIdentifier /// The pertains to the . /// Gender, - /// + /// /// The pertains to the . /// EVs, - /// + /// /// The pertains to the . /// Language, @@ -91,7 +91,7 @@ internal enum CheckIdentifier /// The pertains to the ribbon data. /// Ribbon, - /// + /// /// The pertains to the super training data. /// Training, @@ -112,5 +112,9 @@ internal enum CheckIdentifier /// This is used for parsing checks to ensure the didn't debut on a future /// GameOrigin, + /// + /// The CheckResult pertains to the . + /// + HeldItem } } diff --git a/PKHeX.Core/Legality/Ribbons/RibbonVerifier.cs b/PKHeX.Core/Legality/Ribbons/RibbonVerifier.cs index 55f46af86..4bad638e6 100644 --- a/PKHeX.Core/Legality/Ribbons/RibbonVerifier.cs +++ b/PKHeX.Core/Legality/Ribbons/RibbonVerifier.cs @@ -1,12 +1,39 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using static PKHeX.Core.LegalityCheckStrings; namespace PKHeX.Core { - internal static class RibbonVerifier + internal class RibbonVerifier : Verifier { - internal static List GetIncorrectRibbons(PKM pkm, object encounterContent, int gen) + protected override CheckIdentifier Identifier => CheckIdentifier.Ribbon; + public override void Verify(LegalityAnalysis data) + { + var EncounterMatch = data.EncounterMatch; + var pkm = data.pkm; + var Info = data.Info; + // Check Unobtainable Ribbons + var encounterContent = (EncounterMatch as MysteryGift)?.Content ?? EncounterMatch; + if (pkm.IsEgg) + { + if (GetIncorrectRibbonsEgg(pkm, encounterContent)) + data.AddLine(GetInvalid(V603)); + return; + } + + int gen = Info.Generation < 3 ? 7 : Info.Generation; // Flag VC (Gen1/2) ribbons using Gen7 origin rules. + var result = GetIncorrectRibbons(pkm, encounterContent, gen); + if (result.Count != 0) + { + var msg = string.Join(Environment.NewLine, result.Where(s => !string.IsNullOrEmpty(s))); + data.AddLine(GetInvalid(msg)); + } + else + data.AddLine(GetValid(V602)); + } + + private static List GetIncorrectRibbons(PKM pkm, object encounterContent, int gen) { List missingRibbons = new List(); List invalidRibbons = new List(); @@ -21,7 +48,7 @@ internal static List GetIncorrectRibbons(PKM pkm, object encounterConten result.Add(string.Format(V601, string.Join(", ", invalidRibbons.Select(z => z.Replace("Ribbon", ""))))); return result; } - internal static bool GetIncorrectRibbonsEgg(PKM pkm, object encounterContent) + private static bool GetIncorrectRibbonsEgg(PKM pkm, object encounterContent) { var RibbonNames = ReflectUtil.GetPropertiesStartWithPrefix(pkm.GetType(), "Ribbon"); if (encounterContent is IRibbonSetEvent3 event3) diff --git a/PKHeX.Core/Legality/Verifiers/AbilityVerifier.cs b/PKHeX.Core/Legality/Verifiers/AbilityVerifier.cs new file mode 100644 index 000000000..8495cf880 --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/AbilityVerifier.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public class AbilityVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.Ability; + public override void Verify(LegalityAnalysis data) + { + var pkm = data.pkm; + var Info = data.Info; + var EncounterMatch = data.EncounterMatch; + + int[] abilities = pkm.PersonalInfo.Abilities; + int abilval = Array.IndexOf(abilities, pkm.Ability); + if (abilval < 0) + { + data.AddLine(GetInvalid(V107)); + return; + } + + bool? AbilityUnchanged = true; + // 3 states flag: true for unchanged, false for changed, null for uncertain/allowing PID mismatch + // if true, check encounter ability + // if true or false, check PID/AbilityNumber + if (3 <= pkm.Format && pkm.Format <= 5 && abilities[0] != abilities[1]) // 3-5 and have 2 distinct ability now + AbilityUnchanged = VerifyAbilityPreCapsule(data, abilities, abilval); + + if (EncounterMatch is PCD d) + { + if (VerifyAbilityPCD(data, d, AbilityUnchanged, abilities)) + return; + } + else // Check Ability Mismatches + { + int? EncounterAbility = (EncounterMatch as EncounterStatic)?.Ability ?? + (EncounterMatch as EncounterTrade)?.Ability ?? + (EncounterMatch as EncounterLink)?.Ability; + + if (EncounterAbility != null && VerifySetAbility(data, EncounterAbility, AbilityUnchanged, abilities, abilval)) + return; // result added via VerifySetAbility + } + switch (Info.Generation) + { + case 5: VerifyAbility5(data, abilities); break; + case 6: VerifyAbility6(data, abilities); break; + case 7: VerifyAbility7(data, abilities); break; + } + + if (3 <= Info.Generation && Info.Generation <= 4 && pkm.AbilityNumber == 4) + data.AddLine(GetInvalid(V112)); + else if (AbilityUnchanged != null && abilities[pkm.AbilityNumber >> 1] != pkm.Ability) + data.AddLine(GetInvalid(pkm.Format < 6 ? V113 : V114)); + else + data.AddLine(GetValid(V115)); + } + private bool VerifyAbilityPCD(LegalityAnalysis data, PCD pcd, bool? abilityUnchanged, int[] abilities) + { + var pkm = data.pkm; + if (pcd.Species == pkm.Species && pkm.Ability == pcd.Gift.PK.Ability) // Edge case (PID ability gift mismatch) + data.AddLine(GetValid(V115)); + else if (pkm.Format >= 6 && abilities[0] == abilities[1] && pkm.AbilityNumber == 1) + data.AddLine(GetValid(V115)); // gen3-5 transfer with same ability -> 1st ability that matches + else if (pkm.Format >= 6 && abilities[0] != abilities[1] && pkm.AbilityNumber < 4) // Ability Capsule can change between 1/2 + data.AddLine(GetValid(V109)); + else + { + if (!(abilityUnchanged ?? false)) + return false; + data.AddLine(GetInvalid(V223)); + } + return true; + } + private bool VerifySetAbility(LegalityAnalysis data, int? EncounterAbility, bool? AbilityUnchanged, int[] abilities, int abilval) + { + var pkm = data.pkm; + if (pkm.AbilityNumber == 4 && EncounterAbility != 4) + { + data.AddLine(GetInvalid(V108)); + return true; + } + + if (data.EncounterMatch is EncounterTradePID z) + { + if (z.Species != pkm.Species) + return false; // Must match PID ability, handle via default check path + if (EncounterAbility == 1 << abilval) + { + data.AddLine(GetValid(V115)); + return true; + } + } + + if (!(AbilityUnchanged ?? false) || EncounterAbility == 0 || pkm.AbilityNumber == EncounterAbility) + return false; + if (IsAbilityCapsuleModified(pkm, abilities, EncounterAbility)) + data.AddLine(GetValid(V109)); + else + data.AddLine(GetInvalid(V223)); + return true; + } + + // 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 bool? VerifyAbilityPreCapsule(LegalityAnalysis data, int[] abilities, int abilval) + { + var pkm = data.pkm; + // CXD pokemon could have any ability without maching PID + if (pkm.Version == (int)GameVersion.CXD && pkm.Format == 3) + return null; + + // gen3 native or gen4/5 origin + if (pkm.Format == 3 || !pkm.InhabitedGeneration(3)) + return true; + + // Evovled in gen4/5 + if (pkm.Species > Legal.MaxSpeciesID_3) + return false; + + // gen3Species will be zero for pokemon with illegal gen 3 encounters, like Infernape with gen 3 "origin" + var gen3Species = data.Info.EvoChainsAllGens[3].FirstOrDefault()?.Species ?? 0; + if (gen3Species == 0) + return true; + + // Fall through when gen3 pkm transferred to gen4/5 + return VerifyAbilityGen3Transfer(data, abilities, abilval, gen3Species); + } + private bool? VerifyAbilityGen3Transfer(LegalityAnalysis data, int[] abilities, int abilval, int Species_g3) + { + var pkm = data.pkm; + var Info = data.Info; + var pers = (PersonalInfoG3)PersonalTable.E[Species_g3]; + 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; + + int Species_g4 = Info.EvoChainsAllGens[4].FirstOrDefault()?.Species ?? 0; + int Species_g5 = pkm.Format == 5 ? Info.EvoChainsAllGens[5].FirstOrDefault()?.Species ?? 0 : 0; + if (Math.Max(Species_g5, Species_g4) > Species_g3) // it has evolved in either gen 4 or gen 5; the ability must match PID + return false; + + var Evolutions_g45 = Math.Max(Info.EvoChainsAllGens[4].Count, pkm.Format == 5 ? Info.EvoChainsAllGens[5].Count : 0); + if (Evolutions_g45 > 1) + { + // Evolutions_g45 > 1 and Species_g45 = Species_g3 with means both options, evolve in gen 4-5 or not evolve, are possible + if (pkm.Ability == pers.Ability1) + // It could evolve in gen 4-5 an have generation 3 only ability + // that means it have not actually evolved in gen 4-5, ability do not need to match PID + return null; + if (pkm.Ability == abilities[1]) + // It could evolve in gen4-5 an have generation 4 second ability + // that means it have actually evolved in gen 4-5, ability must match PID + return false; + } + // Evolutions_g45 == 1 means it have not evolved in gen 4-5 games, + // ability do not need to match PID, but only generation 3 ability is allowed + if (pkm.Ability != pers.Ability1) + // Not evolved in gen4-5 but do not have generation 3 only ability + data.AddLine(GetInvalid(V373)); + return null; + } + private void VerifyAbility5(LegalityAnalysis data, int[] abilities) + { + var pkm = data.pkm; + switch (data.EncounterMatch) + { + case PGF g: + VerifyAbilityMG456(data, abilities, g.AbilityType); + break; + + case EncounterSlot w: + // Hidden Abilities for Wild Encounters are only available at a Hidden Grotto + bool grotto = w.Type == SlotType.HiddenGrotto; + if (pkm.AbilityNumber == 4 ^ grotto) + data.AddLine(GetInvalid(grotto ? V217 : V108)); + break; + + case EncounterEgg e when pkm.AbilityNumber == 4: + // Hidden Abilities for some are unbreedable (male only distribution) + if (Legal.MixedGenderBreeding.Contains(e.Species) || Legal.FixedGenderFromBiGender.Contains(e.Species)) + break; // from female + if ((pkm.PersonalInfo.Gender & 0xFF) == 0 || Legal.Ban_BreedHidden.Contains(e.Species)) + data.AddLine(GetInvalid(V112)); + break; + } + } + private void VerifyAbility6(LegalityAnalysis data, int[] abilities) + { + var pkm = data.pkm; + var EncounterMatch = data.EncounterMatch; + if (EncounterMatch is EncounterSlot slot && pkm.AbilityNumber == 4) + { + bool valid = slot.Permissions.DexNav || slot.Type == SlotType.FriendSafari || slot.Type == SlotType.Horde; + if (!valid) + data.AddLine(GetInvalid(V300)); + } + else if (EncounterMatch is WC6 g) + VerifyAbilityMG456(data, abilities, g.AbilityType); + else if (Legal.Ban_NoHidden6.Contains(pkm.SpecForm) && pkm.AbilityNumber == 4) + data.AddLine(GetInvalid(V112)); + } + private void VerifyAbility7(LegalityAnalysis data, int[] abilities) + { + var pkm = data.pkm; + var EncounterMatch = data.EncounterMatch; + if (EncounterMatch is EncounterSlot slot && pkm.AbilityNumber == 4) + { + bool valid = slot.Type == SlotType.SOS; + if (!valid) + data.AddLine(GetInvalid(V111)); + } + else if (EncounterMatch is WC7 g) + VerifyAbilityMG456(data, abilities, g.AbilityType); + else if (Legal.Ban_NoHidden7.Contains(pkm.SpecForm) && pkm.AbilityNumber == 4) + data.AddLine(GetInvalid(V112)); + } + private void VerifyAbilityMG456(LegalityAnalysis data, int[] abilities, int cardtype) + { + var pkm = data.pkm; + int abilNumber = pkm.AbilityNumber; + if (cardtype < 3 && abilNumber != 1 << cardtype) // set number + { + // Ability can be flipped 0/1 if Ability Capsule is available, is not Hidden Ability, and Abilities are different. + if (pkm.Format >= 6 && cardtype < 2 && abilNumber < 3 && abilities[0] != abilities[1]) + data.AddLine(GetValid(V109)); + else + data.AddLine(GetInvalid(V110)); + } + else if (cardtype == 3 && abilNumber == 4) // 1/2 only + data.AddLine(GetInvalid(V110)); + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/BallVerifier.cs b/PKHeX.Core/Legality/Verifiers/BallVerifier.cs new file mode 100644 index 000000000..7af701af9 --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/BallVerifier.cs @@ -0,0 +1,330 @@ +using System.Collections.Generic; +using System.Linq; +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public class BallVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.Ball; + public override void Verify(LegalityAnalysis data) + { + var pkm = data.pkm; + if (pkm.Format < 3) + return; // no ball info saved + + var EncounterMatch = data.EncounterMatch; + var Info = data.Info; + + if (EncounterMatch is MysteryGift g) + { + if (pkm.Species == 490 && g.Ball == 0) + // there is no ball data in Manaphy Mystery Gift + VerifyBallEquals(data, 4); // Pokeball + else + VerifyBallEquals(data, g.Ball); + return; + } + if (EncounterMatch is EncounterLink l) + { + VerifyBallEquals(data, l.Ball); + return; + } + if (EncounterMatch is EncounterTrade t) + { + VerifyBallEquals(data, t.Ball); // Pokeball + return; + } + + if (pkm.Species == 292 && Info.Generation > 3) // Shedinja. For gen3, copy the ball from Nincada + { + VerifyBallEquals(data, 4); // Pokeball Only + return; + } + + if (pkm.Ball == 0x14 && !Info.EncounterMatch.EggEncounter && pkm.SM) // Heavy Ball + { + var lineage = EvolutionChain.GetLineage(pkm); + if (lineage.Any(e => Legal.AlolanCaptureNoHeavyBall.Contains(e))) + { + data.AddLine(GetInvalid(V116)); + return; + } + } + + if (EncounterMatch is EncounterStatic s) + { + if (s.Gift) + VerifyBallEquals(data, s.Ball); + else if (s.Location == 75 && s.Generation == 5) // Entree Forest (Dream World) + VerifyBallEquals(data, Legal.DreamWorldBalls); + else + VerifyBallEquals(data, Legal.GetWildBalls(pkm)); + return; + } + if (EncounterMatch is EncounterSlot w) + { + if (w.Location == 30016 && w.Generation == 7) // Poké Pelago + VerifyBallEquals(data, 4); // Pokeball + // For gen3/4 Safari Zones and BCC getValidWildEncounters already filter to not return + // mixed possible encounters between safari, BCC and other encounters + // That means is the first encounter is not safari then there is no safari encounter in the array + else if (3 <= Info.Generation && Info.Generation <= 4 && w.Type.IsSafariType()) + VerifyBallEquals(data, 5); // Safari Ball + else if (Info.Generation == 4 && w.Type == SlotType.BugContest) + VerifyBallEquals(data, 0x18); // Sport Ball + else + VerifyBallEquals(data, Legal.GetWildBalls(pkm)); + return; + } + + if (pkm.WasEgg) + { + VerifyBallEgg(data); + return; + } + + VerifyBallEquals(data, 4); // Pokeball + } + private void VerifyBallEgg(LegalityAnalysis data) + { + var pkm = data.pkm; + if (data.Info.Generation < 6) // No inheriting Balls + { + VerifyBallEquals(data, 4); // Must be Pokéball -- no ball inheritance. + return; + } + + if (pkm.Ball == 0x01) // Master Ball + { data.AddLine(GetInvalid(V117)); return; } + if (pkm.Ball == 0x10) // Cherish Ball + { data.AddLine(GetInvalid(V120)); return; } + if (pkm.Ball == 0x04) // Poké Ball + { data.AddLine(GetValid(V119)); return; } + + switch (data.Info.Generation) + { + case 6: // Gen6 Inheritance Rules + VerifyBallEggGen6(data); + return; + case 7: // Gen7 Inheritance Rules + VerifyBallEggGen7(data); + return; + } + } + private void VerifyBallEggGen6(LegalityAnalysis data) + { + var pkm = data.pkm; + if (pkm.Gender == 2) // Genderless + { + VerifyBallEquals(data, 4); // Must be Pokéball as ball can only pass via mother (not Ditto!) + return; + } + if (Legal.BreedMaleOnly.Contains(pkm.Species)) + { + VerifyBallEquals(data, 4); // Must be Pokéball as ball can only pass via mother (not Ditto!) + return; + } + + int ball = pkm.Ball; + + if (ball >= 26) + { + data.AddLine(GetInvalid(V126)); + return; + } + if (ball == 0x05) // Safari Ball + { + if (EvolutionChain.GetLineage(pkm).All(e => !Legal.Inherit_Safari.Contains(e))) + data.AddLine(GetInvalid(V121)); + else if (pkm.AbilityNumber == 4) + data.AddLine(GetInvalid(V122)); + else + data.AddLine(GetValid(V123)); + + return; + } + if (0x10 < ball && ball < 0x18) // Apricorn Ball + { + if (EvolutionChain.GetLineage(pkm).All(e => !Legal.Inherit_Apricorn6.Contains(e))) + data.AddLine(GetInvalid(V121)); + if (pkm.AbilityNumber == 4) + data.AddLine(GetInvalid(V122)); + else + data.AddLine(GetValid(V123)); + + return; + } + if (ball == 0x18) // Sport Ball + { + if (EvolutionChain.GetLineage(pkm).All(e => !Legal.Inherit_Sport.Contains(e))) + data.AddLine(GetInvalid(V121)); + else if (pkm.AbilityNumber == 4) + data.AddLine(GetInvalid(V122)); + else + data.AddLine(GetValid(V123)); + + return; + } + if (ball == 0x19) // Dream Ball + { + if (EvolutionChain.GetLineage(pkm).Any(e => Legal.Inherit_Dream.Contains(e))) + data.AddLine(GetValid(V123)); + else + data.AddLine(GetInvalid(V121)); + + if (pkm.AbilityNumber == 4 && Legal.Ban_DreamHidden.Contains(pkm.Species)) + data.AddLine(GetInvalid(V122)); + + return; + } + if (0x0D <= ball && ball <= 0x0F) + { + if (!Legal.Ban_Gen4Ball_6.Contains(pkm.Species)) + data.AddLine(GetValid(V123)); + else + data.AddLine(GetInvalid(V121)); + + return; + } + if (0x02 <= ball && ball <= 0x0C) // Don't worry, Ball # 0x05 was already checked. + { + if (Legal.Ban_Gen3Ball.Contains(pkm.Species)) + data.AddLine(GetInvalid(V121)); + else if (pkm.AbilityNumber == 4 && Legal.Ban_Gen3BallHidden.Contains(pkm.SpecForm)) + data.AddLine(GetInvalid(V122)); + else + data.AddLine(GetValid(V123)); + + return; + } + + if (pkm.Species > 650 && pkm.Species != 700) // Sylveon + { + if (Legal.GetWildBalls(pkm).Contains(pkm.Ball)) + data.AddLine(GetValid(V123)); + else + data.AddLine(GetInvalid(V121)); + return; + } + + data.AddLine(GetInvalid(V125)); + } + private void VerifyBallEggGen7(LegalityAnalysis data) + { + var pkm = data.pkm; + var Lineage = EvolutionChain.GetLineage(pkm).ToArray(); + if (722 <= pkm.Species && pkm.Species <= 730) // G7 Starters + { + VerifyBallEquals(data, 4); + return; + } + + int ball = pkm.Ball; + if (ball == 0x05) // Safari Ball + { + if (!Lineage.Any(e => Legal.Inherit_Safari.Contains(e) || Legal.Inherit_SafariMale.Contains(e))) + data.AddLine(GetInvalid(V121)); + else if (pkm.AbilityNumber == 4 && Lineage.Any(e => Legal.Ban_SafariBallHidden_7.Contains(e))) + data.AddLine(GetInvalid(V122)); + else + data.AddLine(GetValid(V123)); + + return; + } + if (0x10 < ball && ball < 0x18) // Apricorn Ball + { + if (!Lineage.Any(e => Legal.Inherit_Apricorn7.Contains(e))) + data.AddLine(GetInvalid(V121)); + else if (pkm.AbilityNumber == 4 && Legal.Ban_NoHidden7Apricorn.Contains(Lineage.Last() | pkm.AltForm << 11)) // lineage is 3->2->origin + data.AddLine(GetInvalid(V122)); + else + data.AddLine(GetValid(V123)); + + return; + } + if (ball == 0x18) // Sport Ball + { + if (!Lineage.Any(e => Legal.Inherit_Sport.Contains(e))) + data.AddLine(GetInvalid(V121)); + else if (pkm.AbilityNumber == 4 && (Lineage.Contains(313) || Lineage.Contains(314))) // Volbeat/Illumise + data.AddLine(GetInvalid(V122)); + else + data.AddLine(GetValid(V123)); + + return; + } + if (ball == 0x19) // Dream Ball + { + if (Lineage.Any(e => Legal.Inherit_Dream.Contains(e) || Legal.Inherit_DreamMale.Contains(e))) + data.AddLine(GetValid(V123)); + else + data.AddLine(GetInvalid(V121)); + + return; + } + if (0x0D <= ball && ball <= 0x0F) // Dusk Heal Quick + { + if (!Legal.Ban_Gen4Ball_7.Contains(pkm.Species)) + data.AddLine(GetValid(V123)); + else + data.AddLine(GetInvalid(V121)); + + return; + } + if (0x02 <= ball && ball <= 0x0C) // Don't worry, Ball # 0x05 was already checked. + { + if (!Legal.Ban_Gen3Ball_7.Contains(pkm.Species)) + data.AddLine(GetValid(V123)); + else + data.AddLine(GetInvalid(V121)); + + return; + } + + if (ball == 26) + { + if (Lineage[0] == 669 && pkm.AltForm == 3 && pkm.AbilityNumber == 4) + { + // Can't obtain Flabébé-Blue with Hidden Ability in wild + data.AddLine(GetInvalid(V122)); + return; + } + if ((pkm.Species > 731 && pkm.Species <= 785) || Lineage.Any(e => Legal.PastGenAlolanNatives.Contains(e) && !Legal.PastGenAlolanNativesUncapturable.Contains(e))) + { + data.AddLine(GetValid(V123)); + return; + } + if (Lineage.Any(e => Legal.PastGenAlolanScans.Contains(e))) + { + data.AddLine(GetValid(V123)); + return; + } + // next statement catches all new alolans + } + + if (pkm.Species > 721) + { + VerifyBallEquals(data, Legal.GetWildBalls(pkm)); + return; + } + + if (ball >= 27) + { + data.AddLine(GetInvalid(V126)); + return; + } + data.AddLine(GetInvalid(V125)); + } + + private void VerifyBallEquals(LegalityAnalysis data, int ball) => AddBallLine(data, ball == data.pkm.Ball); + private void VerifyBallEquals(LegalityAnalysis data, ICollection balls) => AddBallLine(data, balls.Contains(data.pkm.Ball)); + private void AddBallLine(LegalityAnalysis data, bool valid) + { + if (valid) + data.AddLine(GetValid(V119)); + else + data.AddLine(GetInvalid(V118)); + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/CXDVerifier.cs b/PKHeX.Core/Legality/Verifiers/CXDVerifier.cs new file mode 100644 index 000000000..f09a8b6e8 --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/CXDVerifier.cs @@ -0,0 +1,42 @@ +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public class CXDVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.Misc; + public override void Verify(LegalityAnalysis data) + { + var pkm = data.pkm; + if (data.EncounterMatch is EncounterStatic) + VerifyCXDStarterCorrelation(data); + else if (pkm.WasEgg) // can't obtain eggs in CXD + data.AddLine(GetInvalid(V80, CheckIdentifier.Encounter)); // invalid encounter + + if (pkm.OT_Gender == 1) + data.AddLine(GetInvalid(V407, CheckIdentifier.Trainer)); + } + private static void VerifyCXDStarterCorrelation(LegalityAnalysis data) + { + var pidiv = data.Info.PIDIV; + if (pidiv.Type != PIDType.CXD) + return; + + bool valid; + var EncounterMatch = data.EncounterMatch; + var pkm = data.pkm; + switch (EncounterMatch.Species) + { + case 133: + valid = LockFinder.IsXDStarterValid(pidiv.OriginSeed, pkm.TID, pkm.SID); break; + case 196: + case 197: + valid = pidiv.Type == PIDType.CXD_ColoStarter; break; + default: + return; + } + if (!valid) + data.AddLine(GetInvalid(V400, CheckIdentifier.PID)); + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/ConsoleRegionVerifier.cs b/PKHeX.Core/Legality/Verifiers/ConsoleRegionVerifier.cs new file mode 100644 index 000000000..f350ff092 --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/ConsoleRegionVerifier.cs @@ -0,0 +1,23 @@ +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public class ConsoleRegionVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.Geography; + public override void Verify(LegalityAnalysis data) + { + var result = VerifyConsoleRegion(data.pkm); + data.AddLine(result); + } + private static CheckResult VerifyConsoleRegion(PKM pkm) + { + int consoleRegion = pkm.ConsoleRegion; + if (consoleRegion >= 7) + return new CheckResult(Severity.Invalid, V301, CheckIdentifier.Geography); + return Legal.IsConsoleRegionCountryValid(consoleRegion, pkm.Country) + ? new CheckResult(Severity.Valid, V303, CheckIdentifier.Geography) + : new CheckResult(Severity.Invalid, V302, CheckIdentifier.Geography); + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/EffortValueVerifier.cs b/PKHeX.Core/Legality/Verifiers/EffortValueVerifier.cs new file mode 100644 index 000000000..59bb86907 --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/EffortValueVerifier.cs @@ -0,0 +1,47 @@ +using System.Linq; +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public sealed class EffortValueVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.EVs; + public override void Verify(LegalityAnalysis data) + { + var pkm = data.pkm; + var EncounterMatch = data.EncounterMatch; + var evs = pkm.EVs; + int sum = pkm.EVTotal; + if (sum > 0 && pkm.IsEgg) + data.AddLine(GetInvalid(V22, CheckIdentifier.EVs)); + if (pkm.Format >= 3 && sum > 510) + data.AddLine(GetInvalid(V25, CheckIdentifier.EVs)); + if (pkm.Format >= 6 && evs.Any(ev => ev > 252)) + data.AddLine(GetInvalid(V26, CheckIdentifier.EVs)); + if (pkm.Format == 4 && pkm.Gen4 && EncounterMatch.LevelMin == 100) + { + // Cannot EV train at level 100 -- Certain events are distributed at level 100. + if (evs.Any(ev => ev > 100)) // EVs can only be increased by vitamins to a max of 100. + data.AddLine(GetInvalid(V367, CheckIdentifier.EVs)); + } + else if (pkm.Format < 5) + { + // In Generations I and II, when a Pokémon is taken out of the Day Care, its experience will lower to the minimum value for its current level. + if (pkm.Format < 3) // can abuse daycare for EV training without EXP gain + return; + + const int maxEV = 100; // Vitamin Max + if (PKX.GetEXP(EncounterMatch.LevelMin, pkm.Species) == pkm.EXP && evs.Any(ev => ev > maxEV)) + data.AddLine(GetInvalid(string.Format(V418, maxEV), CheckIdentifier.EVs)); + } + + // Only one of the following can be true: 0, 508, and x%6!=0 + if (sum == 0 && !EncounterMatch.IsWithinRange(pkm)) + data.AddLine(Get(V23, Severity.Fishy)); + else if (sum == 508) + data.AddLine(Get(V24, Severity.Fishy)); + else if (evs[0] != 0 && evs.All(ev => evs[0] == ev)) + data.AddLine(Get(V27, Severity.Fishy)); + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/EncounterTypeVerifier.cs b/PKHeX.Core/Legality/Verifiers/EncounterTypeVerifier.cs new file mode 100644 index 000000000..8caca688b --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/EncounterTypeVerifier.cs @@ -0,0 +1,31 @@ +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public class EncounterTypeVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.Encounter; + + public override void Verify(LegalityAnalysis data) + { + var type = GetEncounterTypeValue(data.pkm, data.EncounterMatch); + var result = !type.Contains(data.pkm.EncounterType) ? GetInvalid(V381) : GetValid(V380); + data.AddLine(result); + } + + private static EncounterType GetEncounterTypeValue(PKM pkm, IEncounterable enc) + { + // Encounter type data is only stored for gen 4 encounters + // All eggs have encounter type none, even if they are from static encounters + if (!pkm.Gen4 || pkm.WasEgg) + return EncounterType.None; + + if (enc is EncounterSlot w) + return w.TypeEncounter; + if (enc is EncounterStaticTyped s) + return s.TypeEncounter; + + return EncounterType.None; + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/FormVerifier.cs b/PKHeX.Core/Legality/Verifiers/FormVerifier.cs new file mode 100644 index 000000000..3636e5324 --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/FormVerifier.cs @@ -0,0 +1,259 @@ +using System; +using System.Linq; +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public class FormVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.Form; + + public override void Verify(LegalityAnalysis data) + { + var pkm = data.pkm; + if (pkm.Format < 4) + return; // no forms exist + + var PersonalInfo = data.PersonalInfo; + var EncounterMatch = data.EncounterMatch; + var Info = data.Info; + + int count = PersonalInfo.FormeCount; + if (count <= 1 && pkm.AltForm == 0) + return; // no forms to check + + if (!PersonalInfo.IsFormeWithinRange(pkm.AltForm) && !FormConverter.IsValidOutOfBoundsForme(pkm.Species, pkm.AltForm, Info.Generation)) + { + data.AddLine(GetInvalid(string.Format(V304, count - 1, pkm.AltForm))); + return; + } + + if (EncounterMatch is EncounterSlot w && w.Type == SlotType.FriendSafari) + VerifyFormFriendSafari(data); + else if (EncounterMatch is EncounterEgg) + { + if (FormConverter.IsTotemForm(pkm.Species, pkm.AltForm)) + { + data.AddLine(GetInvalid(V317)); + return; + } + } + + switch (pkm.Species) + { + case 25 when Info.Generation == 6: // Pikachu Cosplay + bool isStatic = EncounterMatch is EncounterStatic; + if (isStatic != (pkm.AltForm != 0)) + { + string msg = isStatic ? V305 : V306; + data.AddLine(GetInvalid(msg)); + return; + } + break; + case 25 when Info.Generation == 7: // Pikachu Cap + bool IsValidPikachuCap() + { + switch (EncounterMatch) + { + default: return pkm.AltForm == 0; + case WC7 wc7: return wc7.Form == pkm.AltForm; + case EncounterStatic s: return s.Form == pkm.AltForm; + } + } + + if (!IsValidPikachuCap()) + { + bool gift = EncounterMatch is WC7 g && g.Form != pkm.AltForm; + var msg = gift ? V307 : V317; + data.AddLine(GetInvalid(msg)); + return; + } + break; + case 201 when Info.Generation == 2 && pkm.AltForm >= 26: + data.AddLine(GetInvalid(string.Format(V304, "Z", pkm.AltForm == 26 ? "!" : "?"))); + break; + case 487: // Giratina + if (pkm.AltForm == 1 ^ pkm.HeldItem == 112) // Origin form only with Griseous Orb + { + data.AddLine(GetInvalid(V308)); + return; + } + break; + case 493: // Arceus + { + int item = pkm.HeldItem; + int form = 0; + if (298 <= item && item <= 313 || item == 644) + form = Array.IndexOf(Legal.Arceus_Plate, item) + 1; + else if (777 <= item && item <= 793) + form = Array.IndexOf(Legal.Arceus_ZCrystal, item) + 1; + if (pkm.Format == 4 && form >= 9) + form++; // ??? type Form shifts everything by 1 + + if (form != pkm.AltForm) + data.AddLine(GetInvalid(V308)); + else if (form != 0) + data.AddLine(GetValid(V309)); + } + break; + case 647: // Keldeo + { + if (pkm.Gen5) // can mismatch in gen5 via BW tutor and transfer up + break; + int index = Array.IndexOf(pkm.Moves, 548); // Secret Sword + bool noSword = index < 0; + if (pkm.AltForm == 0 ^ noSword) // mismatch + Info.Moves[noSword ? 0 : index] = new CheckMoveResult(Info.Moves[noSword ? 0 : index], Severity.Invalid, V169, CheckIdentifier.Move); + break; + } + case 649: // Genesect + { + int item = pkm.HeldItem; + int form = 0; + if (116 <= item && item <= 119) + form = item - 115; + + if (form != pkm.AltForm) + data.AddLine(GetInvalid(V308)); + else + data.AddLine(GetValid(V309)); + } + break; + case 658: // Greninja + if (pkm.AltForm > 1) // Ash Battle Bond active + { + data.AddLine(GetInvalid(V310)); + return; + } + if (pkm.AltForm != 0 && !(EncounterMatch is MysteryGift)) // Formes are not breedable, MysteryGift already checked + { + data.AddLine(GetInvalid(string.Format(V304, 0, pkm.AltForm))); + return; + } + break; + case 664: // Scatterbug + case 665: // Spewpa + if (pkm.AltForm > 17) // Fancy & Pokéball + { + data.AddLine(GetInvalid(V311)); + return; + } + if (!Legal.CheckVivillonPattern(pkm.AltForm, pkm.Country, pkm.Region)) + data.AddLine(Get(V312, Severity.Fishy)); + break; + case 666: // Vivillon + if (pkm.AltForm > 17) // Fancy & Pokéball + { + if (!(EncounterMatch is MysteryGift)) + data.AddLine(GetInvalid(V312)); + else + data.AddLine(GetValid(V313)); + + return; + } + if (!Legal.CheckVivillonPattern(pkm.AltForm, pkm.Country, pkm.Region)) + data.AddLine(Get(V312, Severity.Fishy)); + break; + case 670: // Floette + if (pkm.AltForm == 5) // Eternal Flower -- Never Released + { + if (!(EncounterMatch is MysteryGift)) + data.AddLine(GetInvalid(V314)); + else + data.AddLine(GetValid(V315)); + + return; + } + break; + case 678: // Meowstic + if (pkm.AltForm != pkm.Gender) + data.AddLine(GetInvalid(V203)); + break; + case 773: // Silvally + { + int item = pkm.HeldItem; + int form = 0; + if ((904 <= item && item <= 920) || item == 644) + form = item - 903; + if (form != pkm.AltForm) + data.AddLine(GetInvalid(V308)); + else if (form != 0) + data.AddLine(GetValid(V309)); + break; + } + + case 744 when Info.EncounterMatch.EggEncounter && pkm.AltForm == 1 && pkm.SM: + case 745 when Info.EncounterMatch.EggEncounter && pkm.AltForm == 2 && pkm.SM: + data.AddLine(GetInvalid(V317)); + return; + + // Impossible Egg forms + case 479 when pkm.IsEgg: // Rotom + case 676 when pkm.IsEgg: // Furfrou + if (pkm.AltForm != 0) // has form + { + data.AddLine(GetInvalid(V50)); + return; + } + break; + + // Party Only Forms + case 492: // Shaymin + case 676: // Furfrou + case 720: // Hoopa + if (pkm.AltForm != 0 && pkm.Box > -1 && pkm.Format <= 6) // has form but stored in box + { + data.AddLine(GetInvalid(V316)); + return; + } + break; + + // Battle only Forms with other legal forms allowed + case 718 when pkm.AltForm >= 4: // Zygarde Complete + case 774 when pkm.AltForm < 7: // Minior Shield + case 800 when pkm.AltForm == 3: // Ultra Necrozma + data.AddLine(GetInvalid(V310)); + return; + case 800 when pkm.AltForm < 3: // Necrozma Fused forms & default + case 778 when pkm.AltForm == 2: // Totem disguise Mimikyu + data.AddLine(GetValid(V318)); + return; + } + + if (pkm.Format >= 7 && Info.Generation < 7 && pkm.AltForm != 0) + { + if (pkm.Species == 25 || Legal.AlolanOriginForms.Contains(pkm.Species) + || Legal.AlolanVariantEvolutions12.Contains(data.EncounterOriginal.Species)) + { data.AddLine(GetInvalid(V317)); return; } + } + if (pkm.AltForm > 0 && new[] { Legal.BattleForms, Legal.BattleMegas, Legal.BattlePrimals }.Any(arr => arr.Contains(pkm.Species))) + { data.AddLine(GetInvalid(V310)); return; } + + data.AddLine(GetValid(V318)); + } + + + private void VerifyFormFriendSafari(LegalityAnalysis data) + { + var pkm = data.pkm; + switch (pkm.Species) + { + case 670: // Floette + case 671: // Florges + if (!new[] { 0, 1, 3 }.Contains(pkm.AltForm)) // 0/1/3 - RBY + data.AddLine(GetInvalid(V64)); + break; + case 710 when pkm.AltForm != 0: // Pumpkaboo + case 711 when pkm.AltForm != 0: // Goregeist Average + data.AddLine(GetInvalid(V6)); + break; + case 423 when pkm.AltForm != 0: // Gastrodon West + data.AddLine(GetInvalid(V64)); + break; + case 586 when pkm.AltForm != 0: // Sawsbuck + data.AddLine(GetInvalid(V65)); + break; + } + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/GenderVerifier.cs b/PKHeX.Core/Legality/Verifiers/GenderVerifier.cs new file mode 100644 index 000000000..ee46f079b --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/GenderVerifier.cs @@ -0,0 +1,85 @@ +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public class GenderVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.Gender; + + public override void Verify(LegalityAnalysis data) + { + var pkm = data.pkm; + var pi = pkm.PersonalInfo; + if (pi.Genderless != (pkm.Gender == 2)) + { + // DP/HGSS shedinja glitch -- only generation 4 spawns + bool ignore = pkm.Format == 4 && pkm.Species == 292 && pkm.Met_Level != pkm.CurrentLevel; + if (!ignore) + data.AddLine(GetInvalid(V203)); + return; + } + + // Check for PID relationship to Gender & Nature if applicable + int gen = data.Info.Generation; + + if (3 <= gen && gen <= 5) + { + // Gender-PID & Nature-PID relationship check + if (IsValidGenderPID(data)) + data.AddLine(GetValid(V250)); + else + data.AddLine(GetInvalid(V251)); + + if (gen != 5) + VerifyNaturePID(data); + return; + } + + // Check fixed gender cases + if ((pi.OnlyFemale && pkm.Gender != 1) || (pi.OnlyMale && pkm.Gender != 0)) + data.AddLine(GetInvalid(V203)); + } + + private void VerifyNaturePID(LegalityAnalysis data) + { + var pkm = data.pkm; + var result = pkm.EncryptionConstant % 25 == pkm.Nature + ? GetValid(V252, CheckIdentifier.Nature) + : GetInvalid(V253, CheckIdentifier.Nature); + data.AddLine(result); + } + + private bool IsValidGenderPID(LegalityAnalysis data) + { + var pkm = data.pkm; + bool genderValid = pkm.IsGenderValid(); + if (!genderValid) + { + if (pkm.Format == 4 && pkm.Species == 292) // Shedinja glitch + { + // should match original gender + var gender = PKX.GetGenderFromPIDAndRatio(pkm.PID, 0x7F); // 50M-50F + if (gender == pkm.Gender) + return true; + } + else if (pkm.Format > 5 && (pkm.Species == 183 || pkm.Species == 184)) // Azurill/Marill Gender Ratio Change + { + var gv = pkm.PID & 0xFF; + if (gv > 63 && pkm.Gender == 1) // evolved from azurill after transferring to keep gender + return true; + } + return false; + } + + // check for mixed->fixed gender incompatibility by checking the gender of the original species + var EncounterMatch = data.EncounterMatch; + if (Legal.FixedGenderFromBiGender.Contains(EncounterMatch.Species) && pkm.Gender != 2) // shedinja + { + var gender = PKX.GetGenderFromPID(EncounterMatch.Species, pkm.EncryptionConstant); + if (gender != pkm.Gender) // gender must not be different from original + return false; + } + return true; + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/HyperTrainingVerifier.cs b/PKHeX.Core/Legality/Verifiers/HyperTrainingVerifier.cs new file mode 100644 index 000000000..7393b0046 --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/HyperTrainingVerifier.cs @@ -0,0 +1,40 @@ +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public class HyperTrainingVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.Training; + + public override void Verify(LegalityAnalysis data) + { + var pkm = data.pkm; + if (!(pkm is IHyperTrain t)) + return; // No Hyper Training before Gen7 + + if (!t.IsHyperTrained()) + return; + + if (pkm.CurrentLevel != 100) + { + data.AddLine(GetInvalid(V40)); + return; + } + + int max = pkm.MaxIV; + if (pkm.IVTotal == max * 6) + { + data.AddLine(GetInvalid(V41)); + return; + } + + for (int i = 0; i < 6; i++) // Check individual IVs + { + if (pkm.GetIV(i) != max || !t.IsHyperTrained(i)) + continue; + data.AddLine(GetInvalid(V42)); + break; + } + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/IndividualValueVerifier.cs b/PKHeX.Core/Legality/Verifiers/IndividualValueVerifier.cs new file mode 100644 index 000000000..b946b7167 --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/IndividualValueVerifier.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public sealed class IndividualValueVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.IVs; + public override void Verify(LegalityAnalysis data) + { + switch (data.EncounterMatch) + { + case EncounterStatic s: + VerifyIVsStatic(data, s); + break; + case EncounterSlot w: + VerifyIVsSlot(data, w); + break; + case MysteryGift g: + VerifyIVsMystery(data, g); + break; + } + + var pkm = data.pkm; + if (pkm.IVTotal == 0) + data.AddLine(Get(V321, Severity.Fishy)); + else if (pkm.IVs[0] < 30 && pkm.IVs.All(iv => pkm.IVs[0] == iv)) + data.AddLine(Get(V32, Severity.Fishy)); + } + + private void VerifyIVsMystery(LegalityAnalysis data, MysteryGift g) + { + var pkm = data.pkm; + int[] IVs = GetMysteryGiftIVs(g); + if (IVs == null) + return; + + var pkIVs = pkm.IVs; + var ivflag = Array.Find(IVs, iv => (byte)(iv - 0xFC) < 3); + if (ivflag == 0) // Random IVs + { + bool valid = GetIsFixedIVSequenceValid(IVs, pkIVs); + if (!valid) + data.AddLine(GetInvalid(V30)); + } + else + { + int IVCount = ivflag - 0xFB; // IV2/IV3 + if (pkIVs.Count(iv => iv == 31) < IVCount) + data.AddLine(GetInvalid(string.Format(V28, IVCount))); + } + } + + private static bool GetIsFixedIVSequenceValid(IReadOnlyList IVs, IReadOnlyList pkIVs) + { + for (int i = 0; i < 6; i++) + if (IVs[i] <= 31 && IVs[i] != pkIVs[i]) + return false; + return true; + } + + private static int[] GetMysteryGiftIVs(MysteryGift g) + { + switch (g) + { + case WC7 wc7: return wc7.IVs; + case WC6 wc6: return wc6.IVs; + case PGF pgf: return pgf.IVs; + default: return null; + } + } + + private void VerifyIVsSlot(LegalityAnalysis data, EncounterSlot w) + { + var pkm = data.pkm; + bool force2 = w.Type == SlotType.FriendSafari || w.Generation == 7 && pkm.AbilityNumber == 4; + if (force2 && pkm.IVs.Count(iv => iv == 31) < 2) + data.AddLine(GetInvalid(w.Type == SlotType.FriendSafari ? V29 : string.Format(V28, 2))); + } + + private void VerifyIVsStatic(LegalityAnalysis data, EncounterStatic s) + { + var pkm = data.pkm; + if (s.FlawlessIVCount != 0 && pkm.IVs.Count(iv => iv == 31) < s.FlawlessIVCount) + data.AddLine(GetInvalid(string.Format(V28, s.FlawlessIVCount))); + } + } +} \ No newline at end of file diff --git a/PKHeX.Core/Legality/Verifiers/ItemVerifier.cs b/PKHeX.Core/Legality/Verifiers/ItemVerifier.cs new file mode 100644 index 000000000..18f12e930 --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/ItemVerifier.cs @@ -0,0 +1,38 @@ +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public class ItemVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.HeldItem; + public override void Verify(LegalityAnalysis data) + { + var pkm = data.pkm; + if (!Legal.IsHeldItemAllowed(pkm)) + data.AddLine(GetInvalid(V204)); + + if (pkm.Format == 3 && pkm.HeldItem == 175) // Enigma Berry + VerifyEReaderBerry(data); + + if (pkm.IsEgg && pkm.HeldItem != 0) + data.AddLine(GetInvalid(V419)); + } + private void VerifyEReaderBerry(LegalityAnalysis data) + { + if (Legal.EReaderBerryIsEnigma) // no E-Reader berry data provided, can't hold berry. + { + data.AddLine(GetInvalid(V204)); + return; + } + + var matchUSA = Legal.EReaderBerriesNames_USA.Contains(Legal.EReaderBerryName); + var matchJP = Legal.EReaderBerriesNames_JP.Contains(Legal.EReaderBerryName); + if (!matchJP && !matchUSA) // Does not match any released E-Reader berry + data.AddLine(GetInvalid(V369)); + else if (matchJP && !Legal.SavegameJapanese && Legal.SavegameLanguage >= 0) // E-Reader is region locked + data.AddLine(GetInvalid(V370)); + else if (matchUSA && Legal.SavegameJapanese && Legal.SavegameLanguage >= 0) // E-Reader is region locked + data.AddLine(GetInvalid(V371)); + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/LanguageVerifier.cs b/PKHeX.Core/Legality/Verifiers/LanguageVerifier.cs new file mode 100644 index 000000000..081613e86 --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/LanguageVerifier.cs @@ -0,0 +1,68 @@ +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public sealed class LanguageVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.Language; + + public override void Verify(LegalityAnalysis data) + { + var pkm = data.pkm; + int originalGeneration = data.Info.Generation; + int currentLanguage = pkm.Language; + int maxLanguageID = Legal.GetMaxLanguageID(originalGeneration); + + if (!IsValidLanguageID(currentLanguage, maxLanguageID, pkm)) + { + data.AddLine(GetInvalid(string.Format(V5, $"<={maxLanguageID}", currentLanguage))); + return; + } + + // Korean Gen4 games can not trade with other Gen4 languages, but can use Pal Park with any Gen3 game/language. + if (pkm.Format == 4 && pkm.Gen4 && !IsValidG4Korean(currentLanguage)) + { + bool kor = currentLanguage == (int)LanguageID.Korean; + var msgpkm = kor ? V611 : V612; + var msgsav = kor ? V612 : V611; + data.AddLine(GetInvalid(string.Format(V610, msgpkm, msgsav))); + return; + } + + // Korean Crystal does not exist, neither do VC1 + if (originalGeneration <= 2 && pkm.Korean && !GameVersion.GS.Contains((GameVersion)pkm.Version)) + { + data.AddLine(GetInvalid(string.Format(V5, $"!={(LanguageID)currentLanguage}", currentLanguage))); + } + } + + public static bool IsValidLanguageID(int currentLanguage, int maxLanguageID, PKM pkm) + { + if (currentLanguage == (int)LanguageID.UNUSED_6) + return false; // Language ID 6 is unused. + + if (currentLanguage > maxLanguageID) + return false; // Language not available (yet) + + if (currentLanguage <= (int) LanguageID.Hacked && !Legal.IsValidMissingLanguage(pkm)) + return false; // Missing Language value is not obtainable + + return true; // Language is possible + } + + /// + /// Check if the can exist in the Generation 4 savefile. + /// + /// + /// + public static bool IsValidG4Korean(int currentLanguage) + { + bool savKOR = Legal.SavegameLanguage == (int) LanguageID.Korean; + bool pkmKOR = currentLanguage == (int) LanguageID.Korean; + if (savKOR == pkmKOR) + return true; + + return Legal.SavegameLanguage < 0; // check not overriden by Legality settings + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/LevelVerifier.cs b/PKHeX.Core/Legality/Verifiers/LevelVerifier.cs new file mode 100644 index 000000000..53d75ee35 --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/LevelVerifier.cs @@ -0,0 +1,85 @@ +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public class LevelVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.Level; + public override void Verify(LegalityAnalysis data) + { + var pkm = data.pkm; + var EncounterMatch = data.EncounterMatch; + if (EncounterMatch is MysteryGift gift) + { + if (gift.Level != pkm.Met_Level && pkm.HasOriginalMetLocation) + { + switch (gift) + { + case WC3 wc3 when wc3.Met_Level == pkm.Met_Level || wc3.IsEgg: + break; + case WC7 wc7 when wc7.MetLevel == pkm.Met_Level: + break; + default: + data.AddLine(GetInvalid(V83)); + return; + } + } + if (gift.Level > pkm.CurrentLevel) + { + data.AddLine(GetInvalid(V84)); + return; + } + } + + if (pkm.IsEgg) + { + int elvl = Legal.GetEggHatchLevel(pkm); + if (elvl != pkm.CurrentLevel) + data.AddLine(GetInvalid(string.Format(V52, elvl))); + return; + } + + int lvl = pkm.CurrentLevel; + if (lvl < pkm.Met_Level) + data.AddLine(GetInvalid(V85)); + else if (!EncounterMatch.IsWithinRange(pkm) && lvl != 100 && pkm.EXP == PKX.GetEXP(lvl, pkm.Species)) + data.AddLine(Get(V87, Severity.Fishy)); + else + data.AddLine(GetValid(V88)); + } + public void VerifyG1(LegalityAnalysis data) + { + var pkm = data.pkm; + var EncounterMatch = data.EncounterMatch; + if (pkm.IsEgg) + { + int elvl = Legal.GetEggHatchLevel(pkm); + if (elvl != pkm.CurrentLevel) + data.AddLine(GetInvalid(string.Format(V52, elvl))); + return; + } + if (pkm.Met_Location != 0) // crystal + { + int lvl = pkm.CurrentLevel; + if (lvl < pkm.Met_Level) + data.AddLine(GetInvalid(V85)); + } + + // There is no way to prevent a gen1 trade evolution as held items (everstone) did not exist. + // Machoke, Graveler, Haunter and Kadabra captured in the second phase evolution, excluding in-game trades, are already checked + if (pkm.Format <= 2 && !(EncounterMatch is EncounterTrade) && EncounterMatch.Species == pkm.Species && Legal.Trade_Evolution1.Contains(EncounterMatch.Species)) + VerifyG1TradeEvo(data); + } + private void VerifyG1TradeEvo(LegalityAnalysis data) + { + var pkm = data.pkm; + var mustevolve = pkm.TradebackStatus == TradebackType.WasTradeback || (pkm.Format == 1 && Legal.IsOutsider(pkm)) || Legal.IsTradedKadabraG1(pkm); + if (!mustevolve) + return; + // Pokemon have been traded but it is not evolved, trade evos are sequential dex numbers + var unevolved = LegalityAnalysis.SpeciesStrings[pkm.Species]; + var evolved = LegalityAnalysis.SpeciesStrings[pkm.Species + 1]; + data.AddLine(GetInvalid(string.Format(V405, unevolved, evolved))); + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/MedalVerifier.cs b/PKHeX.Core/Legality/Verifiers/MedalVerifier.cs new file mode 100644 index 000000000..6962956aa --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/MedalVerifier.cs @@ -0,0 +1,68 @@ +using System; +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public class MedalVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.Training; + + public override void Verify(LegalityAnalysis data) + { + VerifyMedalsRegular(data); + VerifyMedalsEvent(data); + } + private void VerifyMedalsRegular(LegalityAnalysis data) + { + var pkm = data.pkm; + var Info = data.Info; + uint value = BitConverter.ToUInt32(pkm.Data, 0x2C); + if ((value & 3) != 0) // 2 unused flags + data.AddLine(GetInvalid(V98)); + int TrainCount = pkm.SuperTrainingMedalCount(); + + if (pkm.IsEgg && TrainCount > 0) + data.AddLine(GetInvalid(V89)); + else if (TrainCount > 0 && Info.Generation > 6) + data.AddLine(GetInvalid(V90)); + else + { + if (pkm.Format >= 7) + { + if (pkm.SecretSuperTrainingUnlocked) + data.AddLine(GetInvalid(V91)); + if (pkm.SecretSuperTrainingComplete) + data.AddLine(GetInvalid(V92)); + } + else + { + if (TrainCount == 30 ^ pkm.SecretSuperTrainingComplete) + data.AddLine(GetInvalid(V93)); + } + } + } + + private void VerifyMedalsEvent(LegalityAnalysis data) + { + var pkm = data.pkm; + var Info = data.Info; + byte value = pkm.Data[0x3A]; + if ((value & 0xC0) != 0) // 2 unused flags highest bits + data.AddLine(GetInvalid(V98)); + + int TrainCount = 0; + for (int i = 0; i < 6; i++) + { + if ((value & 1) != 0) + TrainCount++; + value >>= 1; + } + if (pkm.IsEgg && TrainCount > 0) + data.AddLine(GetInvalid(V89)); + else if (TrainCount > 0 && Info.Generation > 6) + data.AddLine(GetInvalid(V90)); + else if (TrainCount > 0) + data.AddLine(Get(V94, Severity.Fishy)); + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/MemoryVerifier.cs b/PKHeX.Core/Legality/Verifiers/MemoryVerifier.cs new file mode 100644 index 000000000..62129a6d9 --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/MemoryVerifier.cs @@ -0,0 +1,392 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public class MemoryVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.Memory; + + public override void Verify(LegalityAnalysis data) + { + var hist = VerifyHistory(data); + data.AddLine(hist); + } + private CheckResult VerifyHistory(LegalityAnalysis data) + { + var pkm = data.pkm; + var Info = data.Info; + var EncounterMatch = data.EncounterMatch; + + if (Info.Generation < 6) + { + if (pkm.Format < 6) + return new CheckResult(Severity.Valid, V128, CheckIdentifier.History); + + if (pkm.OT_Affection != 0 && Info.Generation <= 2 || IsInvalidContestAffection(pkm)) + return new CheckResult(Severity.Invalid, V129, CheckIdentifier.History); + if (pkm.OT_Memory > 0 || pkm.OT_Feeling > 0 || pkm.OT_Intensity > 0 || pkm.OT_TextVar > 0) + return new CheckResult(Severity.Invalid, V130, CheckIdentifier.History); + } + + if (pkm.Format >= 6 && Info.Generation != pkm.Format && pkm.CurrentHandler != 1) + return new CheckResult(Severity.Invalid, V124, CheckIdentifier.History); + + if (pkm.HT_Gender > 1) + return new CheckResult(Severity.Invalid, string.Format(V131, pkm.HT_Gender), CheckIdentifier.History); + + if (EncounterMatch is WC6 wc6 && wc6.OT_Name.Length > 0) + { + if (pkm.OT_Friendship != PersonalTable.AO[EncounterMatch.Species].BaseFriendship) + return new CheckResult(Severity.Invalid, V132, CheckIdentifier.History); + if (pkm.OT_Affection != 0 && (pkm.AO || !pkm.IsUntraded) && IsInvalidContestAffection(pkm)) + return new CheckResult(Severity.Invalid, V133, CheckIdentifier.History); + if (pkm.CurrentHandler != 1) + return new CheckResult(Severity.Invalid, V134, CheckIdentifier.History); + } + else if (EncounterMatch is WC7 wc7 && wc7.OT_Name.Length > 0 && wc7.TID != 18075) // Ash Pikachu QR Gift doesn't set Current Handler + { + if (pkm.OT_Friendship != PersonalTable.USUM[EncounterMatch.Species].BaseFriendship) + return new CheckResult(Severity.Invalid, V132, CheckIdentifier.History); + if (pkm.OT_Affection != 0) + return new CheckResult(Severity.Invalid, V133, CheckIdentifier.History); + if (pkm.CurrentHandler != 1) + return new CheckResult(Severity.Invalid, V134, CheckIdentifier.History); + } + else if (EncounterMatch is MysteryGift mg && mg.Format < 6 && pkm.Format >= 6) + { + if (pkm.OT_Affection != 0 && IsInvalidContestAffection(pkm)) + return new CheckResult(Severity.Invalid, V133, CheckIdentifier.History); + if (pkm.CurrentHandler != 1) + return new CheckResult(Severity.Invalid, V134, CheckIdentifier.History); + } + + // Geolocations + var geo = new[] + { + pkm.Geo1_Country, pkm.Geo2_Country, pkm.Geo3_Country, pkm.Geo4_Country, pkm.Geo5_Country, + pkm.Geo1_Region, pkm.Geo2_Region, pkm.Geo3_Region, pkm.Geo4_Region, pkm.Geo5_Region, + }; + + // Check sequential order (no zero gaps) + bool geoEnd = false; + for (int i = 0; i < 5; i++) + { + if (geoEnd && geo[i] != 0) + return new CheckResult(Severity.Invalid, V135, CheckIdentifier.History); + + if (geo[i] != 0) + continue; + if (geo[i + 5] != 0) + return new CheckResult(Severity.Invalid, V136, CheckIdentifier.History); + geoEnd = true; + } + if (pkm.Format >= 7) + return VerifyHistory7(data, geo); + + // Determine if we should check for Handling Trainer Memories + // A Pokémon is untraded if... + bool untraded = pkm.HT_Name.Length == 0 || pkm.Geo1_Country == 0; + if (EncounterMatch is MysteryGift gift) + { + untraded |= !pkm.WasEventEgg; + untraded &= gift.IsEgg; + } + + if (EncounterMatch is EncounterLink link && !link.OT) + untraded = false; + else if (Info.Generation < 6) + untraded = false; + + if (untraded) // Is not Traded + { + if (pkm.HT_Name.Length != 0) + return new CheckResult(Severity.Invalid, V146, CheckIdentifier.History); + if (pkm.Geo1_Country != 0) + return new CheckResult(Severity.Invalid, V147, CheckIdentifier.History); + if (pkm.HT_Memory != 0) + return new CheckResult(Severity.Invalid, V148, CheckIdentifier.History); + if (pkm.CurrentHandler != 0) // Badly edited; PKHeX doesn't trip this. + return new CheckResult(Severity.Invalid, V139, CheckIdentifier.History); + if (pkm.HT_Friendship != 0) + return new CheckResult(Severity.Invalid, V140, CheckIdentifier.History); + if (pkm.HT_Affection != 0) + return new CheckResult(Severity.Invalid, V141, CheckIdentifier.History); + if (pkm.XY && pkm is IContestStats s && s.HasContestStats()) + return new CheckResult(Severity.Invalid, V138, CheckIdentifier.History); + + if (VerifyHistoryUntradedHandler(pkm, out CheckResult chk1)) + return chk1; + if (EncounterMatch.Species != pkm.Species && VerifyHistoryUntradedEvolution(pkm, Info.EvoChainsAllGens, out CheckResult chk2)) + return chk2; + } + else // Is Traded + { + if (pkm.Format == 6 && pkm.HT_Memory == 0 && !pkm.IsEgg) + return new CheckResult(Severity.Invalid, V150, CheckIdentifier.History); + } + + // Memory ChecksResult + if (pkm.IsEgg) + { + if (pkm.HT_Memory != 0) + return new CheckResult(Severity.Invalid, V149, CheckIdentifier.History); + if (pkm.OT_Memory != 0) + return new CheckResult(Severity.Invalid, V151, CheckIdentifier.History); + } + else if (!(EncounterMatch is WC6)) + { + if (pkm.OT_Memory == 0 ^ !pkm.Gen6) + return new CheckResult(Severity.Invalid, V152, CheckIdentifier.History); + if (Info.Generation < 6 && pkm.OT_Affection != 0) + return new CheckResult(Severity.Invalid, V129, CheckIdentifier.History); + } + // Unimplemented: Ingame Trade Memories + + return new CheckResult(Severity.Valid, V145, CheckIdentifier.History); + } + private CheckResult VerifyHistory7(LegalityAnalysis data, int[] geo) + { + var pkm = data.pkm; + var EncounterMatch = data.EncounterMatch; + var Info = data.Info; + + if (pkm.VC1) + { + var hasGeo = geo.Any(d => d != 0); + + if (!hasGeo) + return new CheckResult(Severity.Invalid, V137, CheckIdentifier.History); + } + + if ((2 >= Info.Generation || Info.Generation >= 7) && pkm is IContestStats s && s.HasContestStats()) + return new CheckResult(Severity.Invalid, V138, CheckIdentifier.History); + + if (!pkm.WasEvent && pkm.HT_Name.Length == 0) // Is not Traded + { + if (VerifyHistoryUntradedHandler(pkm, out CheckResult chk1)) + return chk1; + if (EncounterMatch.Species != pkm.Species && VerifyHistoryUntradedEvolution(pkm, Info.EvoChainsAllGens, out CheckResult chk2)) + return chk2; + } + + return new CheckResult(Severity.Valid, V145, CheckIdentifier.History); + } + private static bool VerifyHistoryUntradedHandler(PKM pkm, out CheckResult result) + { + result = null; + if (pkm.CurrentHandler != 0) // Badly edited; PKHeX doesn't trip this. + result = new CheckResult(Severity.Invalid, V139, CheckIdentifier.History); + else if (pkm.HT_Friendship != 0) + result = new CheckResult(Severity.Invalid, V140, CheckIdentifier.History); + else if (pkm.HT_Affection != 0) + result = new CheckResult(Severity.Invalid, V141, CheckIdentifier.History); + else + return false; + + return true; + } + private static bool VerifyHistoryUntradedEvolution(PKM pkm, IReadOnlyList[] chain, out CheckResult result) + { + result = null; + // Handling Trainer string is empty implying it has not been traded. + // If it must be trade evolved, flag it. + + if (pkm.Species == 350) // Milotic + { + if (Legal.IsTradeEvolved(chain, pkm.Format)) + return false; + if (pkm is IContestStats s && s.CNT_Beauty < 170) // Beauty Contest Stat Requirement + result = new CheckResult(Severity.Invalid, V143, CheckIdentifier.History); + else if (pkm.CurrentLevel == 1) + result = new CheckResult(Severity.Invalid, V144, CheckIdentifier.History); + else + return false; + return true; + } + if (!Legal.IsTradeEvolved(chain, pkm.Format)) + return false; + result = new CheckResult(Severity.Invalid, V142, CheckIdentifier.History); + return true; + } + private CheckResult VerifyCommonMemory(PKM pkm, int handler) + { + Memories.GetMemoryVariables(pkm, out int m, out int t, out int i, out int f, out string tr, handler); + int matchingMoveMemory = Array.IndexOf(Memories.MoveSpecificMemories[0], m); + if (matchingMoveMemory != -1 && pkm.Species != 235 && !Legal.GetCanLearnMachineMove(pkm, Memories.MoveSpecificMemories[1][matchingMoveMemory], 6)) + return new CheckResult(Severity.Invalid, string.Format(V153, tr), CheckIdentifier.Memory); + + if (m == 6 && !Memories.LocationsWithPKCenter[0].Contains(t)) + return new CheckResult(Severity.Invalid, string.Format(V154, tr), CheckIdentifier.Memory); + + if (m == 21) // {0} saw {2} carrying {1} on its back. {4} that {3}. + if (!Legal.GetCanLearnMachineMove(new PK6 { Species = t, EXP = PKX.GetEXP(100, t) }, 19, 6)) + return new CheckResult(Severity.Invalid, string.Format(V153, tr), CheckIdentifier.Memory); + + if ((m == 16 || m == 48) && (t == 0 || !Legal.GetCanKnowMove(pkm, t, 6))) + return new CheckResult(Severity.Invalid, string.Format(V153, tr), CheckIdentifier.Memory); + + if (m == 49 && (t == 0 || !Legal.GetCanRelearnMove(pkm, t, 6))) // {0} was able to remember {2} at {1}'s instruction. {4} that {3}. + return new CheckResult(Severity.Invalid, string.Format(V153, tr), CheckIdentifier.Memory); + + if (i < Memories.MemoryMinIntensity[m]) + return new CheckResult(Severity.Invalid, string.Format(V254, tr, Memories.MemoryMinIntensity[m]), CheckIdentifier.Memory); + + if (m != 4 && (Memories.MemoryFeelings[m] & (1 << f)) == 0) + return new CheckResult(Severity.Invalid, string.Format(V255, tr), CheckIdentifier.Memory); + + return new CheckResult(Severity.Valid, string.Format(V155, tr), CheckIdentifier.Memory); + } + + private void VerifyOTMemoryIs(LegalityAnalysis data, int m, int i, int t, int f) + { + var pkm = data.pkm; + if (pkm.OT_Memory != m) + data.AddLine(Severity.Invalid, string.Format(V197, V205, m), CheckIdentifier.Memory); + if (pkm.OT_Intensity != i) + data.AddLine(Severity.Invalid, string.Format(V198, V205, i), CheckIdentifier.Memory); + if (pkm.OT_TextVar != t) + data.AddLine(Severity.Invalid, string.Format(V199, V205, t), CheckIdentifier.Memory); + if (pkm.OT_Feeling != f) + data.AddLine(Severity.Invalid, string.Format(V200, V205, f), CheckIdentifier.Memory); + } + public void VerifyOTMemory(LegalityAnalysis data) + { + var pkm = data.pkm; + if (pkm.Format < 6) + return; + + var Info = data.Info; + if (Info.Generation < 6 || pkm.IsEgg) + { + VerifyOTMemoryIs(data, 0, 0, 0, 0); // empty + return; + } + + switch (data.EncounterMatch) + { + case EncounterTrade _: + switch (Info.Generation) + { + case 6: + break; // Undocumented, uncommon, and insignificant -- don't bother. + case 7: + VerifyOTMemoryIs(data, 1, 3, 40, 5); + break; + } + return; + case WC6 g when !g.IsEgg: + VerifyOTMemoryIs(data, g.OT_Memory, g.OT_Intensity, g.OT_TextVar, g.OT_Feeling); + return; + case WC7 g when !g.IsEgg: + VerifyOTMemoryIs(data,g.OT_Memory, g.OT_Intensity, g.OT_TextVar, g.OT_Feeling); + return; + } + + if (Info.Generation >= 7) + { + VerifyOTMemoryIs(data, 0, 0, 0, 0); // empty + return; + } + + switch (pkm.OT_Memory) + { + case 2: // {0} hatched from an Egg and saw {1} for the first time at... {2}. {4} that {3}. + if (pkm.Egg_Location == 0) + data.AddLine(Severity.Invalid, string.Format(V160, V205), CheckIdentifier.Memory); + break; + + case 4: // {0} became {1}’s friend when it arrived via Link Trade at... {2}. {4} that {3}. + data.AddLine(Severity.Invalid, string.Format(V161, V205), CheckIdentifier.Memory); + return; + + case 6: // {0} went to the Pokémon Center in {2} with {1} and had its tired body healed there. {4} that {3}. + int matchingOriginGame = Array.IndexOf(Memories.LocationsWithPKCenter[0], pkm.OT_TextVar); + if (matchingOriginGame != -1) + { + int gameID = Memories.LocationsWithPKCenter[1][matchingOriginGame]; + if (pkm.XY && gameID != 0 || pkm.AO && gameID != 1) + data.AddLine(Severity.Invalid, string.Format(V162, V205), CheckIdentifier.Memory); + } + data.AddLine(VerifyCommonMemory(pkm, 0)); + return; + + case 14: + if (!Legal.GetCanBeCaptured(pkm.OT_TextVar, Info.Generation, (GameVersion)pkm.Version)) + data.AddLine(Severity.Invalid, string.Format(V165, V205), CheckIdentifier.Memory); + else + data.AddLine(Severity.Valid, string.Format(V164, V205), CheckIdentifier.Memory); + return; + } + if (pkm.XY && Memories.Memory_NotXY.Contains(pkm.OT_Memory)) + data.AddLine(Severity.Invalid, string.Format(V163, V205), CheckIdentifier.Memory); + if (pkm.AO && Memories.Memory_NotAO.Contains(pkm.OT_Memory)) + data.AddLine(Severity.Invalid, string.Format(V163, V205), CheckIdentifier.Memory); + + data.AddLine(VerifyCommonMemory(pkm, 0)); + } + public void VerifyHTMemory(LegalityAnalysis data) + { + var pkm = data.pkm; + if (pkm.Format < 6) + return; + + var Info = data.Info; + if (pkm.Format >= 7) + { + /* + * Bank Transfer adds in the Link Trade Memory. + * Trading 7<->7 between games (not Bank) clears this data. + */ + if (pkm.HT_Memory == 0) + { + if (pkm.HT_TextVar != 0 || pkm.HT_Intensity != 0 || pkm.HT_Feeling != 0) + data.AddLine(Severity.Invalid, V329, CheckIdentifier.Memory); + return; + } + + // Transfer 6->7 & withdraw to same HT => keeps past gen memory + // Don't require link trade memory for these past gen cases + int gen = Info.Generation; + if (3 <= gen && gen < 7 && pkm.CurrentHandler == 1) + return; + + if (pkm.HT_Memory != 4) + data.AddLine(Severity.Invalid, V156, CheckIdentifier.Memory); + if (pkm.HT_TextVar != 0) + data.AddLine(Severity.Invalid, V157, CheckIdentifier.Memory); + if (pkm.HT_Intensity != 1) + data.AddLine(Severity.Invalid, V158, CheckIdentifier.Memory); + if (pkm.HT_Feeling > 10) + data.AddLine(Severity.Invalid, V159, CheckIdentifier.Memory); + return; + } + + switch (pkm.HT_Memory) + { + case 0: + if (string.IsNullOrEmpty(pkm.HT_Name)) + return; + data.AddLine(Severity.Invalid, V150, CheckIdentifier.Memory); return; + case 1: // {0} met {1} at... {2}. {1} threw a Poké Ball at it, and they started to travel together. {4} that {3}. + data.AddLine(Severity.Invalid, string.Format(V202, V206), CheckIdentifier.Memory); return; + + case 2: // {0} hatched from an Egg and saw {1} for the first time at... {2}. {4} that {3}. + data.AddLine(Severity.Invalid, string.Format(V160, V206), CheckIdentifier.Memory); return; + + case 14: + if (Legal.GetCanBeCaptured(pkm.HT_TextVar, 6)) + data.AddLine(Severity.Valid, string.Format(V164, V206), CheckIdentifier.Memory); + else + data.AddLine(Severity.Invalid, string.Format(V165, V206), CheckIdentifier.Memory); + return; + } + data.AddLine(VerifyCommonMemory(pkm, 1)); + } + + // ORAS contests mistakenly apply 20 affection to the OT instead of the current handler's value + private static bool IsInvalidContestAffection(PKM pkm) => pkm.OT_Affection != 255 && pkm.OT_Affection % 20 != 0; + } +} diff --git a/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs b/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs new file mode 100644 index 000000000..1d7e4acfc --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs @@ -0,0 +1,233 @@ +using System.Linq; +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public class MiscVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.Misc; + public override void Verify(LegalityAnalysis data) + { + var pkm = data.pkm; + if (pkm.IsEgg) + { + VerifyMiscEggCommon(data); + + if (pkm is IContestStats s && s.HasContestStats()) + data.AddLine(GetInvalid(V320, CheckIdentifier.Egg)); + if (pkm is PK4 pk4) + { + if (pk4.ShinyLeaf != 0) + data.AddLine(GetInvalid(V414, CheckIdentifier.Egg)); + if (pk4.PokéathlonStat != 0) + data.AddLine(GetInvalid(V415, CheckIdentifier.Egg)); + } + if (pkm is PK3) + { + if (pkm.Language != 1) // All Eggs are Japanese and flagged specially for localized string + data.AddLine(GetInvalid(string.Format(V5, LanguageID.Japanese, (LanguageID)pkm.Language), CheckIdentifier.Egg)); + } + } + + + VerifyMiscFatefulEncounter(data); + } + + public void VerifyMiscG1(LegalityAnalysis data) + { + var pkm = data.pkm; + if (pkm.IsEgg) + { + VerifyMiscEggCommon(data); + if (pkm.PKRS_Cured || pkm.PKRS_Infected) + data.AddLine(GetInvalid(V368, CheckIdentifier.Egg)); + } + + if (!(pkm is PK1 pk1)) + return; + + VerifyMiscG1Types(data, pk1); + VerifyMiscG1CatchRate(data, pk1); + } + private void VerifyMiscG1Types(LegalityAnalysis data, PK1 pk1) + { + var Type_A = pk1.Type_A; + var Type_B = pk1.Type_B; + if (pk1.Species == 137) + { + // Porygon can have any type combination of any generation 1 species because of the move Conversion, + // that change Porygon type to match the oponent types + var Type_A_Match = Legal.Types_Gen1.Any(t => t == Type_A); + var Type_B_Match = Legal.Types_Gen1.Any(t => t == Type_B); + if (!Type_A_Match) + data.AddLine(GetInvalid(V386)); + if (!Type_B_Match) + data.AddLine(GetInvalid(V387)); + if (Type_A_Match && Type_B_Match) + { + var TypesAB_Match = PersonalTable.RB.IsValidTypeCombination(Type_A, Type_B); + if (TypesAB_Match) + data.AddLine(GetValid(V391)); + else + data.AddLine(GetInvalid(V388)); + } + } + else // Types must match species types + { + var Type_A_Match = Type_A == PersonalTable.RB[pk1.Species].Type1; + var Type_B_Match = Type_B == PersonalTable.RB[pk1.Species].Type2; + + var first = Type_A_Match ? GetValid(V392) : GetInvalid(V389); + var second = Type_B_Match ? GetValid(V393) : GetInvalid(V390); + data.AddLine(first); + data.AddLine(second); + } + } + private void VerifyMiscG1CatchRate(LegalityAnalysis data, PK1 pk1) + { + var EncounterMatch = data.EncounterMatch; + var catch_rate = pk1.Catch_Rate; + switch (pk1.TradebackStatus) + { + case TradebackType.Any: + case TradebackType.WasTradeback: + if (catch_rate == 0 || Legal.HeldItems_GSC.Any(h => h == catch_rate)) + data.AddLine(GetValid(V394)); + else if (pk1.TradebackStatus == TradebackType.WasTradeback) + data.AddLine(GetInvalid(V395)); + else + goto case TradebackType.Gen1_NotTradeback; + break; + case TradebackType.Gen1_NotTradeback: + if ((EncounterMatch as EncounterStatic)?.Version == GameVersion.Stadium || EncounterMatch is EncounterTradeCatchRate) + // Encounters detected by the catch rate, cant be invalid if match this encounters + data.AddLine(GetValid(V398)); + else if (pk1.Species == 149 && catch_rate == PersonalTable.Y[149].CatchRate || Legal.Species_NotAvailable_CatchRate.Contains(pk1.Species) && catch_rate == PersonalTable.RB[pk1.Species].CatchRate) + data.AddLine(GetInvalid(V396)); + else if (!data.Info.EvoChainsAllGens[1].Any(e => catch_rate == PersonalTable.RB[e.Species].CatchRate || catch_rate == PersonalTable.Y[e.Species].CatchRate)) + data.AddLine(GetInvalid(pk1.Gen1_NotTradeback ? V397 : V399)); + else + data.AddLine(GetValid(V398)); + break; + } + } + private void VerifyMiscFatefulEncounter(LegalityAnalysis data) + { + var pkm = data.pkm; + var EncounterMatch = data.EncounterMatch; + switch (EncounterMatch) + { + case WC3 w when w.Fateful: + if (w.IsEgg) + { + // Eggs hatched in RS clear the obedience flag! + if (pkm.Format != 3) + return; // possible hatched in either game, don't bother checking + if (pkm.Met_Location <= 087) // hatched in RS + break; // ensure fateful is not active + // else, ensure fateful is active (via below) + } + VerifyFatefulIngameActive(data); + VerifyWC3Shiny(data, w); + return; + case WC3 w: + if (w.Version == GameVersion.XD) + return; // Can have either state + VerifyWC3Shiny(data, w); + break; + case MysteryGift g when g.Format != 3: // WC3 + VerifyFatefulMysteryGift(data, g); + return; + case EncounterStatic s when s.Fateful: // ingame fateful + case EncounterSlot _ when pkm.Version == 15: // ingame pokespot + case EncounterTrade t when t.Fateful: + VerifyFatefulIngameActive(data); + return; + } + if (pkm.FatefulEncounter) + data.AddLine(GetInvalid(V325, CheckIdentifier.Fateful)); + } + private void VerifyMiscEggCommon(LegalityAnalysis data) + { + var pkm = data.pkm; + if (pkm.Move1_PPUps > 0 || pkm.Move2_PPUps > 0 || pkm.Move3_PPUps > 0 || pkm.Move4_PPUps > 0) + data.AddLine(GetInvalid(V319, CheckIdentifier.Egg)); + if (pkm.Move1_PP != pkm.GetMovePP(pkm.Move1, 0) || pkm.Move2_PP != pkm.GetMovePP(pkm.Move2, 0) + || pkm.Move3_PP != pkm.GetMovePP(pkm.Move3, 0) || pkm.Move4_PP != pkm.GetMovePP(pkm.Move4, 0)) + data.AddLine(GetInvalid(V420, CheckIdentifier.Egg)); + + var EncounterMatch = data.EncounterMatch; + var HatchCycles = (EncounterMatch as EncounterStatic)?.EggCycles; + if (HatchCycles == 0 || HatchCycles == null) + HatchCycles = pkm.PersonalInfo.HatchCycles; + if (pkm.CurrentFriendship > HatchCycles) + data.AddLine(GetInvalid(V374, CheckIdentifier.Egg)); + + if (pkm.Format >= 6 && EncounterMatch is EncounterEgg && !pkm.Moves.SequenceEqual(pkm.RelearnMoves)) + { + var moves = string.Join(", ", LegalityAnalysis.GetMoveNames(pkm.Moves)); + var msg = string.Format(V343, moves); + data.AddLine(GetInvalid(msg, CheckIdentifier.Egg)); + } + } + private void VerifyFatefulMysteryGift(LegalityAnalysis data, MysteryGift g) + { + var pkm = data.pkm; + if (g is PGF p && p.IsShiny) + { + var Info = data.Info; + Info.PIDIV = MethodFinder.Analyze(pkm); + if (Info.PIDIV.Type != PIDType.G5MGShiny && pkm.Egg_Location != 30003) + data.AddLine(GetInvalid(V411, CheckIdentifier.PID)); + } + + if (pkm.FatefulEncounter) + data.AddLine(GetValid(V321, CheckIdentifier.Fateful)); + else + data.AddLine(GetInvalid(V322, CheckIdentifier.Fateful)); + } + private void VerifyWC3Shiny(LegalityAnalysis data, WC3 g3) + { + // check for shiny locked gifts + if (!g3.Shiny.IsValid(data.pkm)) + data.AddLine(GetInvalid(V409, CheckIdentifier.Fateful)); + } + private void VerifyFatefulIngameActive(LegalityAnalysis data) + { + var pkm = data.pkm; + if (pkm.Version == 15 && pkm is XK3 xk3 && data.Info.WasXD) + { + // can't have fateful until traded away, which clears ShadowID + if (xk3.FatefulEncounter && xk3.ShadowID != 0 && data.EncounterMatch is EncounterStaticShadow) + data.AddLine(GetInvalid(V325, CheckIdentifier.Fateful)); + + return; // fateful is set when transferred away + } + + if (pkm.FatefulEncounter) + data.AddLine(GetValid(V323, CheckIdentifier.Fateful)); + else + data.AddLine(GetInvalid(V324, CheckIdentifier.Fateful)); + } + + public void VerifyVersionEvolution(LegalityAnalysis data) + { + var pkm = data.pkm; + if (pkm.Format < 7 || data.EncounterMatch.Species == pkm.Species) + return; + + // No point using the evolution tree. Just handle certain species. + switch (pkm.Species) + { + case 745 when (pkm.AltForm == 0 && Moon()) || (pkm.AltForm == 1 && Sun()): // Lycanroc + case 791 when Moon(): // Solgaleo + case 792 when Sun(): // Lunala + bool Sun() => pkm.Version == (int)GameVersion.SN || pkm.Version == (int)GameVersion.US; + bool Moon() => pkm.Version == (int)GameVersion.MN || pkm.Version == (int)GameVersion.UM; + if (pkm.IsUntraded) + data.AddLine(GetInvalid(V328, CheckIdentifier.Evolution)); + break; + } + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/NHarmoniaVerifier.cs b/PKHeX.Core/Legality/Verifiers/NHarmoniaVerifier.cs new file mode 100644 index 000000000..33fb3f40b --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/NHarmoniaVerifier.cs @@ -0,0 +1,46 @@ +using System.Linq; +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public class NHarmoniaVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.Trainer; + + public override void Verify(LegalityAnalysis data) + { + var pkm = data.pkm; + var EncounterMatch = data.EncounterMatch; + + bool checksRequired = EncounterMatch is EncounterStaticPID s && s.NSparkle; + if (pkm is PK5 pk5) + { + bool has = pk5.NPokémon; + if (checksRequired && !has) + data.AddLine(GetInvalid(V326, CheckIdentifier.Fateful)); + if (!checksRequired && has) + data.AddLine(GetInvalid(V327, CheckIdentifier.Fateful)); + } + + if (!checksRequired) + return; + + if (pkm.IVs.Any(iv => iv != 30)) + data.AddLine(GetInvalid(V218, CheckIdentifier.IVs)); + if (!VerifyNsPKMOTValid(pkm)) + data.AddLine(GetInvalid(V219, CheckIdentifier.Trainer)); + if (pkm.IsShiny) + data.AddLine(GetInvalid(V220, CheckIdentifier.Shiny)); + } + private static bool VerifyNsPKMOTValid(PKM pkm) + { + if (pkm.TID != 00002 || pkm.SID != 00000) + return false; + var ot = pkm.OT_Name; + if (ot.Length != 1) + return false; + var c = Legal.GetG5OT_NSparkle(pkm.Language); + return c == ot; + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/NicknameVerifier.cs b/PKHeX.Core/Legality/Verifiers/NicknameVerifier.cs new file mode 100644 index 000000000..8658689d4 --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/NicknameVerifier.cs @@ -0,0 +1,386 @@ +using System; +using System.Linq; +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public sealed class NicknameVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.Nickname; + + public override void Verify(LegalityAnalysis data) + { + var pkm = data.pkm; + var EncounterMatch = data.EncounterMatch; + + // If the Pokémon is not nicknamed, it should match one of the language strings. + if (pkm.Nickname.Length == 0) + { + data.AddLine(GetInvalid(V2)); + return; + } + if (pkm.Species > PKX.SpeciesLang[0].Length) + { + data.AddLine(Get(V2, Severity.Indeterminate)); + return; + } + + if (pkm.VC && pkm.IsNicknamed) + { + VerifyG1NicknameWithinBounds(data, pkm.Nickname); + } + else if (EncounterMatch is MysteryGift m) + { + if (pkm.IsNicknamed && !m.IsEgg) + data.AddLine(Get(V0, Severity.Fishy)); + } + + if (EncounterMatch is EncounterTrade) + { + VerifyNicknameTrade(data); + return; + } + + if (pkm.IsEgg) + { + VerifyNicknameEgg(data); + return; + } + + string nickname = pkm.Nickname.Replace("'", "’"); + if (VerifyUnNicknamedEncounter(data, pkm, nickname)) + return; + + // Non-nicknamed strings have already been checked. + if (Legal.CheckWordFilter && pkm.IsNicknamed && WordFilter.IsFiltered(nickname, out string bad)) + data.AddLine(GetInvalid($"Wordfilter: {bad}")); + } + + private bool VerifyUnNicknamedEncounter(LegalityAnalysis data, PKM pkm, string nickname) + { + if (pkm.IsNicknamed) + { + for (int i = 0; i < PKX.SpeciesLang.Length; i++) + { + if (!PKX.SpeciesDict[i].TryGetValue(nickname, out int index)) + continue; + var msg = index == pkm.Species && i != pkm.Language ? V15 : V16; + data.AddLine(Get(msg, Severity.Fishy)); + return true; + } + if (StringConverter.HasEastAsianScriptCharacters(nickname)) // East Asian Scripts + { + data.AddLine(GetInvalid(V222)); + return true; + } + data.AddLine(GetValid(V17)); + } + else if (pkm.Format < 3) + { + // pk1/pk2 IsNicknamed getter checks for match, logic should only reach here if matches. + data.AddLine(GetValid(V18)); + } + else + { + var EncounterMatch = data.EncounterMatch; + // Can't have another language name if it hasn't evolved or wasn't a language-traded egg. + bool evolved = EncounterMatch.Species != pkm.Species; + bool match = PKX.GetSpeciesNameGeneration(pkm.Species, pkm.Language, pkm.Format) == nickname; + if (pkm.WasTradedEgg || evolved) + match |= !PKX.IsNicknamedAnyLanguage(pkm.Species, nickname, pkm.Format); + if (!match && pkm.Format == 5 && !pkm.IsNative) // transfer + { + if (evolved) + match |= !PKX.IsNicknamedAnyLanguage(pkm.Species, nickname, 4); + else + match |= PKX.GetSpeciesNameGeneration(pkm.Species, pkm.Language, 4) == nickname; + } + + if (!match) + { + if (EncounterMatch is WC7 wc7 && wc7.IsAshGreninjaWC7(pkm)) + data.AddLine(GetValid(V19)); + else + data.AddLine(GetInvalid(V20)); + } + else + data.AddLine(GetValid(V18)); + } + return false; + } + + private void VerifyNicknameEgg(LegalityAnalysis data) + { + var Info = data.Info; + var pkm = data.pkm; + var EncounterMatch = Info.EncounterMatch; + switch (pkm.Format) + { + case 4: + if (pkm.IsNicknamed) // gen4 doesn't use the nickname flag for eggs + data.AddLine(GetInvalid(V224, CheckIdentifier.Egg)); + break; + case 7: + if (EncounterMatch is EncounterStatic ^ !pkm.IsNicknamed) // gen7 doesn't use for ingame gifts + data.AddLine(GetInvalid(pkm.IsNicknamed ? V224 : V12, CheckIdentifier.Egg)); + break; + default: + if (!pkm.IsNicknamed) + data.AddLine(GetInvalid(V12, CheckIdentifier.Egg)); + break; + } + + if (pkm.Format == 2 && pkm.IsEgg && !PKX.IsNicknamedAnyLanguage(0, pkm.Nickname, 2)) + data.AddLine(GetValid(V14, CheckIdentifier.Egg)); + else if (PKX.GetSpeciesNameGeneration(0, pkm.Language, Info.Generation) != pkm.Nickname) + data.AddLine(GetInvalid(V13, CheckIdentifier.Egg)); + else + data.AddLine(GetValid(V14, CheckIdentifier.Egg)); + } + private void VerifyNicknameTrade(LegalityAnalysis data) + { + switch (data.Info.Generation) + { + case 1: + case 2: VerifyTrade12(data); return; + case 3: VerifyTrade3(data); return; + case 4: VerifyTrade4(data); return; + case 5: VerifyTrade5(data); return; + case 6: VerifyTrade6(data); return; + case 7: VerifyTrade7(data); return; + } + } + + private void VerifyG1NicknameWithinBounds(LegalityAnalysis data, string str) + { + var pkm = data.pkm; + if (StringConverter.GetIsG1English(str)) + { + if (str.Length > 10) + data.AddLine(GetInvalid(V1)); + } + else if (StringConverter.GetIsG1Japanese(str)) + { + if (str.Length > 5) + data.AddLine(GetInvalid(V1)); + } + else if (pkm.Korean && StringConverter.GetIsG2Korean(str)) + { + if (str.Length > 5) + data.AddLine(GetInvalid(V38)); + } + else + { + data.AddLine(GetInvalid(V422)); + } + } + + + private void VerifyTrade12(LegalityAnalysis data) + { + var et = (EncounterTrade)data.EncounterOriginal; + if (et.TID != 0) // Gen2 Trade + return; // already checked all relevant properties when fetching with getValidEncounterTradeVC2 + + if (!EncounterGenerator.IsEncounterTrade1Valid(data.pkm)) + data.AddLine(GetInvalid(V10, CheckIdentifier.Trainer)); + } + private void VerifyTrade3(LegalityAnalysis data) + { + var pkm = data.pkm; + var EncounterMatch = data.EncounterMatch; + if (pkm.FRLG) + { + int lang = pkm.Language; + if (EncounterMatch.Species == 124) // Jynx + lang = DetectTradeLanguageG3DANTAEJynx(pkm, lang); + VerifyTradeTable(data, Encounters3.TradeFRLG, Encounters3.TradeGift_FRLG, lang); + } + else + VerifyTradeTable(data, Encounters3.TradeRSE, Encounters3.TradeGift_RSE); + } + private void VerifyTrade4(LegalityAnalysis data) + { + var pkm = data.pkm; + var EncounterMatch = data.EncounterMatch; + if (pkm.TID == 1000) + { + VerifyTrade4Ranch(data); + return; + } + if (pkm.HGSS) + { + int lang = pkm.Language; + if (EncounterMatch.Species == 25) // Pikachu + { + lang = DetectTradeLanguageG4SurgePikachu(pkm, lang); + // flag korean magikarp on gen4 saves since the pkm.Language is German + if (pkm.Format == 4 && lang == (int)LanguageID.Korean && Legal.SavegameLanguage != (int)LanguageID.Korean && Legal.SavegameLanguage >= 0) + data.AddLine(GetInvalid(string.Format(V610, V611, V612), CheckIdentifier.Language)); + } + VerifyTradeTable(data, Encounters4.TradeHGSS, Encounters4.TradeGift_HGSS, lang); + } + else // DPPt + { + int lang = pkm.Language; + if (EncounterMatch.Species == 129) // Magikarp + { + lang = DetectTradeLanguageG4MeisterMagikarp(pkm, lang); + // flag korean magikarp on gen4 saves since the pkm.Language is German + if (pkm.Format == 4 && lang == (int)LanguageID.Korean && Legal.SavegameLanguage != (int)LanguageID.Korean && Legal.SavegameLanguage >= 0) + data.AddLine(GetInvalid(string.Format(V610, V611, V612), CheckIdentifier.Language)); + } + else if (!pkm.Pt && lang == 1) // DP English origin are Japanese lang + { + int index = Array.IndexOf(Encounters4.TradeGift_DPPt, data.EncounterMatch); + if (Encounters4.TradeDPPt[1][index] != pkm.Nickname) // not japanese + lang = 2; // English + } + VerifyTradeTable(data, Encounters4.TradeDPPt, Encounters4.TradeGift_DPPt, lang); + } + } + private static int DetectTradeLanguageG3DANTAEJynx(PKM pk, int lang) + { + if (lang != (int)LanguageID.Italian) + return lang; + + if (pk.Version == (int)GameVersion.LG) + lang = (int)LanguageID.English; // translation error; OT was not localized => same as English + return lang; + } + private static int DetectTradeLanguageG4MeisterMagikarp(PKM pkm, int lang) + { + if (lang == (int)LanguageID.English) + return (int)LanguageID.German; + + // All have German, regardless of origin version. + // Detect which language they originated from... roughly. + var table = Encounters4.TradeDPPt; + for (int i = 0; i < table.Length; i++) + { + if (table[i].Length == 0) + continue; + // Nick @ 3, OT @ 7 + if (table[i][7] != pkm.OT_Name) + continue; + lang = i; + break; + } + if (lang == 2) // possible collision with FR/ES/DE. Check nickname + return pkm.Nickname == table[3][3] ? (int)LanguageID.French : (int)LanguageID.Spanish; // Spanish is same as English + + return lang; + } + private static int DetectTradeLanguageG4SurgePikachu(PKM pkm, int lang) + { + if (lang == (int)LanguageID.French) + return (int)LanguageID.English; + + // All have English, regardless of origin version. + // Detect which language they originated from... roughly. + var table = Encounters4.TradeHGSS; + for (int i = 0; i < table.Length; i++) + { + if (table[i].Length == 0) + continue; + // Nick @ 6, OT @ 18 + if (table[i][18] != pkm.OT_Name) + continue; + lang = i; + break; + } + if (lang == 2) // possible collision with ES/IT. Check nickname + return pkm.Nickname == table[4][6] ? (int)LanguageID.Italian : (int)LanguageID.Spanish; + + return lang; + } + private void VerifyTrade5(LegalityAnalysis data) + { + var pkm = data.pkm; + var EncounterMatch = data.EncounterMatch; + // Trades for JPN games have language ID of 0, not 1. + if (pkm.BW) + { + int lang = pkm.Language; + if (pkm.Format == 5 && lang == (int)LanguageID.Japanese) + data.AddLine(GetInvalid(string.Format(V5, 0, (int)LanguageID.Japanese), CheckIdentifier.Language)); + + lang = Math.Max(lang, 1); + VerifyTradeTable(data, Encounters5.TradeBW, Encounters5.TradeGift_BW, lang); + } + else // B2W2 + { + if (Encounters5.TradeGift_B2W2_YancyCurtis.Contains(EncounterMatch)) + VerifyTradeOTOnly(data, pkm.OT_Gender == 0 ? Encounters5.TradeOT_B2W2_M : Encounters5.TradeOT_B2W2_F); + else + VerifyTradeTable(data, Encounters5.TradeB2W2, Encounters5.TradeGift_B2W2); + } + } + private void VerifyTrade6(LegalityAnalysis data) + { + var pkm = data.pkm; + if (pkm.XY) + VerifyTradeTable(data, Encounters6.TradeXY, Encounters6.TradeGift_XY, pkm.Language); + else if (pkm.AO) + VerifyTradeTable(data, Encounters6.TradeAO, Encounters6.TradeGift_AO, pkm.Language); + } + private void VerifyTrade7(LegalityAnalysis data) + { + var pkm = data.pkm; + if (pkm.SM) + VerifyTradeTable(data, Encounters7.TradeSM, Encounters7.TradeGift_SM, pkm.Language); + else if (pkm.USUM) + VerifyTradeTable(data, Encounters7.TradeUSUM, Encounters7.TradeGift_USUM, pkm.Language); + } + private void VerifyTrade4Ranch(LegalityAnalysis data) => VerifyTradeOTOnly(data, Encounters4.RanchOTNames); + + private void VerifyTradeTable(LegalityAnalysis data, string[][] ots, EncounterTrade[] table) => VerifyTradeTable(data, ots, table, data.pkm.Language); + private void VerifyTradeTable(LegalityAnalysis data, string[][] ots, EncounterTrade[] table, int language) + { + var validOT = language >= ots.Length ? ots[0] : ots[language]; + var index = Array.IndexOf(table, data.EncounterMatch); + VerifyTradeOTNick(data, validOT, index); + } + private void VerifyTradeOTOnly(LegalityAnalysis data, string[] validOT) + { + var pkm = data.pkm; + if (pkm.IsNicknamed) + data.AddLine(GetInvalid(V9, CheckIdentifier.Nickname)); + int lang = pkm.Language; + if (validOT.Length <= lang) + data.AddLine(GetInvalid(V8, CheckIdentifier.Trainer)); + else if (validOT[lang] != pkm.OT_Name) + data.AddLine(GetInvalid(V10, CheckIdentifier.Trainer)); + else + data.AddLine(GetValid(V11, CheckIdentifier.Nickname)); + } + private void VerifyTradeOTNick(LegalityAnalysis data, string[] validOT, int index) + { + if (validOT.Length == 0) + { + data.AddLine(Get(V7, Severity.Indeterminate, CheckIdentifier.Trainer)); + return; + } + if (index == -1 || validOT.Length < index * 2) + { + data.AddLine(Get(V8, Severity.Indeterminate, CheckIdentifier.Trainer)); + return; + } + + string nick = validOT[index]; + string OT = validOT[validOT.Length / 2 + index]; + + var pkm = data.pkm; + var EncounterMatch = data.EncounterMatch; + if (nick != pkm.Nickname + && !(nick == "Quacklin’" && pkm.Nickname == "Quacklin'") // apostrophe farfetch'd edge case + && ((EncounterTrade)EncounterMatch).IsNicknamed) // trades that are not nicknamed (but are present in a table with others being named) + data.AddLine(GetInvalid(V9, CheckIdentifier.Nickname)); + else + data.AddLine(GetValid(V11, CheckIdentifier.Nickname)); + + if (OT != pkm.OT_Name) + data.AddLine(GetInvalid(V10, CheckIdentifier.Trainer)); + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/PIDVerifier.cs b/PKHeX.Core/Legality/Verifiers/PIDVerifier.cs new file mode 100644 index 000000000..5fd886eea --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/PIDVerifier.cs @@ -0,0 +1,127 @@ +using System; +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public class PIDVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.PID; + + public override void Verify(LegalityAnalysis data) + { + var pkm = data.pkm; + if (pkm.Format >= 6) + VerifyEC(data); + + var EncounterMatch = data.EncounterMatch; + if (EncounterMatch.Species == 265) + VerifyECPIDWurmple(data); + + if (pkm.PID == 0) + data.AddLine(Get(V207, Severity.Fishy)); + + var Info = data.Info; + if ((Info.Generation >= 6 || Info.Generation < 3 && pkm.Format >= 7) && pkm.PID == pkm.EncryptionConstant) + data.AddLine(GetInvalid(V208)); // better to flag than 1:2^32 odds since RNG is not feasible to yield match + + VerifyShiny(data); + } + + private void VerifyShiny(LegalityAnalysis data) + { + var pkm = data.pkm; + var Info = data.Info; + + switch (data.EncounterMatch) + { + case EncounterStatic s: + if (!s.Shiny.IsValid(pkm)) + data.AddLine(GetInvalid(V209, CheckIdentifier.Shiny)); + + // gen5 correlation + if (Info.Generation != 5) + break; + if (s.Location == 75) // Entree Forest + break; + if (s.Gift || s.Roaming || s.Ability != 4) + break; + if (s is EncounterStaticPID p && p.NSparkle) + break; + VerifyG5PID_IDCorrelation(data); + break; + + case EncounterSlot w: + if (pkm.IsShiny && w.Type == SlotType.HiddenGrotto) + data.AddLine(GetInvalid(V221, CheckIdentifier.Shiny)); + if (Info.Generation == 5 && w.Type != SlotType.HiddenGrotto) + VerifyG5PID_IDCorrelation(data); + break; + + case PCD d: // fixed PID + if (d.Gift.PK.PID != 1 && pkm.EncryptionConstant != d.Gift.PK.PID) + data.AddLine(GetInvalid(V410, CheckIdentifier.Shiny)); + break; + } + } + + private void VerifyG5PID_IDCorrelation(LegalityAnalysis data) + { + var pkm = data.pkm; + var pid = pkm.EncryptionConstant; + var result = (pid & 1) ^ (pid >> 31) ^ (pkm.TID & 1) ^ (pkm.SID & 1); + if (result != 0) + data.AddLine(GetInvalid(V411)); + } + private void VerifyECPIDWurmple(LegalityAnalysis data) + { + var pkm = data.pkm; + uint evoVal = PKX.GetWurmpleEvoVal(pkm.EncryptionConstant); + + if (pkm.Species == 265) + { + var spec = evoVal == 0 ? LegalityAnalysis.SpeciesStrings[267] : LegalityAnalysis.SpeciesStrings[269]; + var msg = string.Format(V212, spec); + data.AddLine(GetValid(msg, CheckIdentifier.EC)); + } + else if (evoVal != Array.IndexOf(Legal.WurmpleEvolutions, pkm.Species) / 2) + { + data.AddLine(GetInvalid(V210, CheckIdentifier.EC)); + } + } + private void VerifyEC(LegalityAnalysis data) + { + var pkm = data.pkm; + var Info = data.Info; + + if (pkm.EncryptionConstant == 0) + data.AddLine(Get(V201, Severity.Fishy, CheckIdentifier.EC)); + if (3 <= Info.Generation && Info.Generation <= 5) + VerifyTransferEC(data); + else + { + int xor = pkm.TSV ^ pkm.PSV; + if (xor < 16 && xor >= 8 && (pkm.PID ^ 0x80000000) == pkm.EncryptionConstant) + data.AddLine(Get(V211, Severity.Fishy, CheckIdentifier.EC)); + } + } + private void VerifyTransferEC(LegalityAnalysis data) + { + var pkm = data.pkm; + // When transferred to Generation 6, the Encryption Constant is copied from the PID. + // The PID is then checked to see if it becomes shiny with the new Shiny rules (>>4 instead of >>3) + // If the PID is nonshiny->shiny, the top bit is flipped. + + // Check to see if the PID and EC are properly configured. + bool xorPID = ((pkm.TID ^ pkm.SID ^ (int)(pkm.PID & 0xFFFF) ^ (int)(pkm.PID >> 16)) & ~0x7) == 8; + bool valid = xorPID + ? pkm.EncryptionConstant == (pkm.PID ^ 0x80000000) + : pkm.EncryptionConstant == pkm.PID; + + if (valid) + return; + + var msg = xorPID ? V215 : V216; + data.AddLine(GetInvalid(msg, CheckIdentifier.EC)); + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/TrainerNameVerifier.cs b/PKHeX.Core/Legality/Verifiers/TrainerNameVerifier.cs new file mode 100644 index 000000000..00e9f3847 --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/TrainerNameVerifier.cs @@ -0,0 +1,116 @@ +using System.Linq; +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public class TrainerNameVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.Trainer; + + private readonly string[] SuspiciousOTNames = + { + "PKHeX", + }; + public override void Verify(LegalityAnalysis data) + { + var pkm = data.pkm; + switch (data.EncounterMatch) + { + case EncounterTrade _: + case MysteryGift g when !g.IsEgg: + case EncounterStaticPID s when s.NSparkle: + return; // already verified + } + + var ot = pkm.OT_Name; + if (ot.Length == 0) + data.AddLine(GetInvalid(V106)); + + if (pkm.TID == 0 && pkm.SID == 0) + data.AddLine(Get(V33, Severity.Fishy)); + else if (pkm.VC) + { + if (pkm.SID != 0) + data.AddLine(GetInvalid(V34)); + } + else if (pkm.TID == pkm.SID) + data.AddLine(Get(V35, Severity.Fishy)); + else if (pkm.TID == 0) + data.AddLine(Get(V36, Severity.Fishy)); + else if (pkm.SID == 0) + data.AddLine(Get(V37, Severity.Fishy)); + else if (pkm.TID == 12345 && pkm.SID == 54321 || SuspiciousOTNames.Any(z => ot.StartsWith(z))) + data.AddLine(Get(V417, Severity.Fishy)); + + if (pkm.VC) + VerifyOTG1(data); + + if (Legal.CheckWordFilter) + { + if (WordFilter.IsFiltered(ot, out string bad)) + data.AddLine(GetInvalid($"Wordfilter: {bad}")); + if (WordFilter.IsFiltered(pkm.HT_Name, out bad)) + data.AddLine(GetInvalid($"Wordfilter: {bad}")); + } + } + public void VerifyOTG1(LegalityAnalysis data) + { + var pkm = data.pkm; + string tr = pkm.OT_Name; + + VerifyG1OTWithinBounds(data, tr); + if (data.EncounterOriginal is EncounterStatic s && (s.Version == GameVersion.Stadium || s.Version == GameVersion.Stadium2)) + VerifyG1OTStadium(data, tr); + + if (pkm.Species == 151) + { + var OTMatch = (tr == Legal.GetG1OT_GFMew((int)LanguageID.Japanese)) + || (tr == Legal.GetG1OT_GFMew((int)LanguageID.English)); + if (!OTMatch || pkm.TID != 22796) + data.AddLine(GetInvalid(V39)); + } + + if (pkm.OT_Gender == 1 && (pkm.Format == 2 && pkm.Met_Location == 0 || pkm.Format > 2 && pkm.VC1)) + data.AddLine(GetInvalid(V408)); + } + private void VerifyG1OTWithinBounds(LegalityAnalysis data, string str) + { + if (StringConverter.GetIsG1English(str)) + { + if (str.Length > 7 && !(data.EncounterOriginal is EncounterTrade)) // OT already verified; GER shuckle has 8 chars + data.AddLine(GetInvalid(V38)); + } + else if (StringConverter.GetIsG1Japanese(str)) + { + if (str.Length > 5) + data.AddLine(GetInvalid(V38)); + } + else if (data.pkm.Korean && StringConverter.GetIsG2Korean(str)) + { + if (str.Length > 5) + data.AddLine(GetInvalid(V38)); + } + else + { + data.AddLine(GetInvalid(V421)); + } + } + private void VerifyG1OTStadium(LegalityAnalysis data, string tr) + { + var pkm = data.pkm; + bool jp = pkm.Japanese; + bool valid = GetIsStadiumOTIDValid(data, jp, tr); + if (!valid) + data.AddLine(GetInvalid(V402)); + else + data.AddLine(GetValid(jp ? V404 : V403)); + } + private bool GetIsStadiumOTIDValid(LegalityAnalysis data, bool jp, string tr) + { + var pkm = data.pkm; + if (jp) + return tr == "スタジアム" && pkm.TID == 1999; + return tr == (data.Info.Generation == 1 ? "STADIUM" : "Stadium") && pkm.TID == 2000; + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/TransferVerifier.cs b/PKHeX.Core/Legality/Verifiers/TransferVerifier.cs new file mode 100644 index 000000000..f521b01f8 --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/TransferVerifier.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using static PKHeX.Core.LegalityCheckStrings; + +namespace PKHeX.Core +{ + public class TransferVerifier : Verifier + { + protected override CheckIdentifier Identifier => CheckIdentifier.Encounter; + public override void Verify(LegalityAnalysis data) + { + throw new NotImplementedException(); + } + + public void VerifyTransferLegalityG3(LegalityAnalysis data) + { + var pkm = data.pkm; + if (pkm.Format == 4 && pkm.Met_Location != Legal.Transfer3) // Pal Park + data.AddLine(GetInvalid(V60)); + if (pkm.Format != 4 && pkm.Met_Location != Legal.Transfer4) + data.AddLine(GetInvalid(V61)); + } + + public void VerifyTransferLegalityG4(LegalityAnalysis data) + { + var pkm = data.pkm; + // Transfer Legality + int loc = pkm.Met_Location; + if (loc != 30001) // PokéTransfer + { + // Crown + switch (pkm.Species) + { + case 251: // Celebi + if (loc != Legal.Transfer4_CelebiUnused && loc != Legal.Transfer4_CelebiUsed) + data.AddLine(GetInvalid(V351)); + break; + case 243: // Raikou + case 244: // Entei + case 245: // Suicune + if (loc != Legal.Transfer4_CrownUnused && loc != Legal.Transfer4_CrownUsed) + data.AddLine(GetInvalid(V351)); + break; + default: + data.AddLine(GetInvalid(V61)); + break; + } + } + } + + public static IEnumerable VerifyVCEncounter(PKM pkm, IEncounterable encounter, ILocation transfer, IList Moves) + { + // Check existing EncounterMatch + if (encounter is EncounterInvalid || transfer == null) + yield break; // Avoid duplicate invaild message + + if (encounter is EncounterStatic v && (GameVersion.GBCartEraOnly.Contains(v.Version) || v.Version == GameVersion.VCEvents)) + { + bool exceptions = false; + exceptions |= v.Version == GameVersion.VCEvents && encounter.Species == 151 && pkm.TID == 22796; + if (!exceptions) + yield return new CheckResult(Severity.Invalid, V79, CheckIdentifier.Encounter); + } + + if (pkm.Met_Location != transfer.Location) + yield return new CheckResult(Severity.Invalid, V81, CheckIdentifier.Encounter); + if (pkm.Egg_Location != transfer.EggLocation) + yield return new CheckResult(Severity.Invalid, V59, CheckIdentifier.Encounter); + + // Flag Moves that cannot be transferred + if (encounter is EncounterStatic s && s.Version == GameVersion.C && s.EggLocation == 256) // Dizzy Punch Gifts + { + // can't have Dizzy Punch at all + int index = Array.IndexOf(pkm.Moves, 146); // Dizzy Punch + if (index >= 0) + { + var chk = Moves[index]; + if (chk.Generation == 2) // not obtained from a future gen + Moves[index] = new CheckMoveResult(chk.Source, chk.Generation, Severity.Invalid, V82, CheckIdentifier.Move); + } + } + + bool checkShiny = pkm.VC2 || pkm.TradebackStatus == TradebackType.WasTradeback && pkm.VC1; + if (!checkShiny) + yield break; + if (pkm.Gender == 1) // female + { + if (pkm.PersonalInfo.Gender == 31 && pkm.IsShiny) // impossible gender-shiny + yield return new CheckResult(Severity.Invalid, V209, CheckIdentifier.PID); + } + else if (pkm.Species == 201) // unown + { + if (pkm.AltForm != 8 && pkm.AltForm != 21 && pkm.IsShiny) // impossibly form-shiny (not I or V) + yield return new CheckResult(Severity.Invalid, V209, CheckIdentifier.PID); + } + } + } +} diff --git a/PKHeX.Core/Legality/Verifiers/Verifier.cs b/PKHeX.Core/Legality/Verifiers/Verifier.cs new file mode 100644 index 000000000..dfd0ebd7e --- /dev/null +++ b/PKHeX.Core/Legality/Verifiers/Verifier.cs @@ -0,0 +1,27 @@ +namespace PKHeX.Core +{ + /// + /// Verification that provides new values for a . + /// + public abstract class Verifier + { + /// + /// category. + /// + protected abstract CheckIdentifier Identifier { get; } + + /// + /// Processes the and adds any relevant values to the . + /// + /// Analysis data to process + public abstract void Verify(LegalityAnalysis data); + + protected CheckResult GetInvalid(string msg) => Get(msg, Severity.Invalid); + protected CheckResult GetValid(string msg) => Get(msg, Severity.Valid); + protected CheckResult Get(string msg, Severity s) => new CheckResult(s, msg, Identifier); + + protected static CheckResult GetInvalid(string msg, CheckIdentifier c) => Get(msg, Severity.Invalid, c); + protected static CheckResult GetValid(string msg, CheckIdentifier c) => Get(msg, Severity.Valid, c); + protected static CheckResult Get(string msg, Severity s, CheckIdentifier c) => new CheckResult(s, msg, c); + } +} diff --git a/PKHeX.Core/MysteryGifts/WC7.cs b/PKHeX.Core/MysteryGifts/WC7.cs index 1fbfe6d47..a327e83d3 100644 --- a/PKHeX.Core/MysteryGifts/WC7.cs +++ b/PKHeX.Core/MysteryGifts/WC7.cs @@ -455,5 +455,10 @@ public override PKM ConvertToPKM(ITrainerInfo SAV) pk.RefreshChecksum(); return pk; } + + public bool IsAshGreninjaWC7(PKM pkm) + { + return CardID == 2046 && (pkm.SID << 16 | pkm.TID) == 0x79F57B49; + } } } diff --git a/PKHeX.Core/PKM/Util/StringConverter.cs b/PKHeX.Core/PKM/Util/StringConverter.cs index 88a47a361..d7512bd5b 100644 --- a/PKHeX.Core/PKM/Util/StringConverter.cs +++ b/PKHeX.Core/PKM/Util/StringConverter.cs @@ -2051,5 +2051,10 @@ public static string LocalizeKOR2(string nick) { "덩쿠리", "덩구리" }, // Tangela { "슈륙챙이", "수륙챙이" }, // Poliwhirl }; + + public static bool HasEastAsianScriptCharacters(IEnumerable str) + { + return str.Any(c => 0x4E00 <= c && c <= 0x9FFF); + } } }