using System; using System.Collections.Generic; using static PKHeX.Core.LegalityCheckStrings; namespace PKHeX.Core { public static class EncounterVerifier { /// /// Gets the method to verify the data. /// /// Source generation to verify /// Returns the verification method appropriate for the input PKM public static Func GetEncounterVerifierMethod(int generation) => generation switch { 1 or 2 => VerifyEncounterG12, _ => VerifyEncounter, }; private static CheckResult VerifyEncounter(PKM pkm, IEncounterTemplate enc) => enc switch { EncounterEgg e => VerifyEncounterEgg(pkm, e.Generation), EncounterTrade t => VerifyEncounterTrade(pkm, t), EncounterSlot w => VerifyEncounterWild(w), EncounterStatic s => VerifyEncounterStatic(pkm, s), MysteryGift g => VerifyEncounterEvent(pkm, g), _ => new CheckResult(Severity.Invalid, LEncInvalid, CheckIdentifier.Encounter), }; private static CheckResult VerifyEncounterG12(PKM pkm, IEncounterTemplate enc) { if (enc.EggEncounter) return VerifyEncounterEgg(pkm, enc.Generation); return enc switch { EncounterSlot1 => new CheckResult(Severity.Valid, LEncCondition, CheckIdentifier.Encounter), EncounterSlot2 s2 => VerifyWildEncounterGen2(pkm, s2), EncounterStatic s => VerifyEncounterStatic(pkm, s), EncounterTrade t => VerifyEncounterTrade(pkm, t), _ => new CheckResult(Severity.Invalid, LEncInvalid, CheckIdentifier.Encounter), }; } // Gen2 Wild Encounters private static CheckResult VerifyWildEncounterGen2(PKM pkm, EncounterSlot2 encounter) { switch (encounter.GetSlotType()) { case SlotType.Headbutt: return VerifyWildEncounterCrystalHeadbutt(pkm, encounter); case SlotType.Old_Rod or SlotType.Good_Rod or SlotType.Super_Rod: switch (encounter.Location) { case 19: // National Park return new CheckResult(Severity.Invalid, LG2InvalidTilePark, CheckIdentifier.Encounter); case 76: // Route 14 return new CheckResult(Severity.Invalid, LG2InvalidTileR14, CheckIdentifier.Encounter); } break; } return new CheckResult(Severity.Valid, LEncCondition, CheckIdentifier.Encounter); } private static CheckResult VerifyWildEncounterCrystalHeadbutt(ITrainerID tr, EncounterSlot2 s2) { return s2.IsTreeAvailable(tr.TID) ? new CheckResult(Severity.Valid, LG2TreeID, CheckIdentifier.Encounter) : new CheckResult(Severity.Invalid, LG2InvalidTileTreeNotFound, CheckIdentifier.Encounter); } // Eggs private static CheckResult VerifyEncounterEgg(PKM pkm, int gen) => gen switch { 2 => new CheckResult(CheckIdentifier.Encounter), // valid -- no met location info 3 => pkm.IsEgg ? VerifyUnhatchedEgg3(pkm) : VerifyEncounterEgg3(pkm), 4 => pkm.IsEgg ? VerifyUnhatchedEgg(pkm, Locations.LinkTrade4) : VerifyEncounterEgg4(pkm), 5 => pkm.IsEgg ? VerifyUnhatchedEgg(pkm, Locations.LinkTrade5) : VerifyEncounterEgg5(pkm), 6 => pkm.IsEgg ? VerifyUnhatchedEgg(pkm, Locations.LinkTrade6) : VerifyEncounterEgg6(pkm), 7 => pkm.IsEgg ? VerifyUnhatchedEgg(pkm, Locations.LinkTrade6) : VerifyEncounterEgg7(pkm), 8 when GameVersion.BDSP.Contains((GameVersion)pkm.Version) => pkm.IsEgg ? VerifyUnhatchedEgg(pkm, Locations.LinkTrade6NPC, Locations.Default8bNone) : VerifyEncounterEgg8BDSP(pkm), 8 => pkm.IsEgg ? VerifyUnhatchedEgg(pkm, Locations.LinkTrade6) : VerifyEncounterEgg8(pkm), _ => new CheckResult(Severity.Invalid, LEggLocationInvalid, CheckIdentifier.Encounter), }; private static CheckResult VerifyUnhatchedEgg3(PKM pkm) { if (pkm.Met_Level != 0) return new CheckResult(Severity.Invalid, string.Format(LEggFMetLevel_0, 0), CheckIdentifier.Encounter); // Only EncounterEgg should reach here. var loc = pkm.FRLG ? Locations.HatchLocationFRLG : Locations.HatchLocationRSE; if (pkm.Met_Location != loc) return new CheckResult(Severity.Invalid, LEggMetLocationFail, CheckIdentifier.Encounter); return new CheckResult(Severity.Valid, LEggLocation, CheckIdentifier.Encounter); } private static CheckResult VerifyEncounterEgg3(PKM pkm) { if (pkm.Format != 3) return VerifyEncounterEgg3Transfer(pkm); if (pkm.Met_Level != 0) return new CheckResult(Severity.Invalid, string.Format(LEggFMetLevel_0, 0), CheckIdentifier.Encounter); // Check the origin game list. var met = pkm.Met_Location; var locs = pkm.FRLG ? Legal.ValidMet_FRLG : pkm.E ? Legal.ValidMet_E : Legal.ValidMet_RS; if (locs.Contains(met)) return new CheckResult(Severity.Valid, LEggLocation, CheckIdentifier.Encounter); // Version isn't updated when hatching on a different game. Check any game. if (Legal.ValidMet_FRLG.Contains(met) || Legal.ValidMet_E.Contains(met) || Legal.ValidMet_RS.Contains(met)) return new CheckResult(Severity.Valid, LEggLocationTrade, CheckIdentifier.Encounter); return new CheckResult(Severity.Invalid, LEggLocationInvalid, CheckIdentifier.Encounter); } private static CheckResult VerifyEncounterEgg3Transfer(PKM pkm) { if (pkm.IsEgg) return new CheckResult(Severity.Invalid, LTransferEgg, CheckIdentifier.Encounter); if (pkm.Met_Level < 5) return new CheckResult(Severity.Invalid, LTransferEggMetLevel, CheckIdentifier.Encounter); if (pkm.Egg_Location != 0) return new CheckResult(Severity.Invalid, LEggLocationNone, CheckIdentifier.Encounter); if (pkm.Format != 4) { if (pkm.Met_Location != Locations.Transfer4) return new CheckResult(Severity.Invalid, LTransferEggLocationTransporter, CheckIdentifier.Encounter); } else { if (pkm.Met_Location != Locations.Transfer3) return new CheckResult(Severity.Invalid, LEggLocationPalPark, CheckIdentifier.Encounter); } return new CheckResult(Severity.Valid, LEggLocation, CheckIdentifier.Encounter); } private static CheckResult VerifyEncounterEgg4(PKM pkm) { if (pkm.Format == 4) { // Traded eggs don't update Version, like in future games. var locations = pkm.WasTradedEgg ? Legal.ValidMet_4 : pkm.HGSS ? Legal.ValidMet_HGSS : pkm.Pt ? Legal.ValidMet_Pt : Legal.ValidMet_DP; return VerifyEncounterEggLevelLoc(pkm, 0, locations); } if (pkm.IsEgg) return new CheckResult(Severity.Invalid, LTransferEgg, CheckIdentifier.Encounter); // transferred if (pkm.Met_Level < 1) return new CheckResult(Severity.Invalid, LTransferEggMetLevel, CheckIdentifier.Encounter); if (pkm.Met_Location != Locations.Transfer4) return new CheckResult(Severity.Invalid, LTransferEggLocationTransporter, CheckIdentifier.Encounter); return new CheckResult(Severity.Valid, LEggLocation, CheckIdentifier.Encounter); } private static CheckResult VerifyEncounterEgg5(PKM pkm) { return VerifyEncounterEggLevelLoc(pkm, 1, pkm.B2W2 ? Legal.ValidMet_B2W2 : Legal.ValidMet_BW); } private static CheckResult VerifyEncounterEgg6(PKM pkm) { if (pkm.AO) return VerifyEncounterEggLevelLoc(pkm, 1, Legal.ValidMet_AO); if (pkm.Egg_Location == 318) return new CheckResult(Severity.Invalid, LEggMetLocationFail, CheckIdentifier.Encounter); return VerifyEncounterEggLevelLoc(pkm, 1, Legal.ValidMet_XY); } private static CheckResult VerifyEncounterEgg7(PKM pkm) { if (pkm.SM) return VerifyEncounterEggLevelLoc(pkm, 1, Legal.ValidMet_SM); if (pkm.USUM) return VerifyEncounterEggLevelLoc(pkm, 1, Legal.ValidMet_USUM); // no other games return new CheckResult(Severity.Invalid, LEggLocationInvalid, CheckIdentifier.Encounter); } private static CheckResult VerifyEncounterEgg8(PKM pkm) { if (pkm.SWSH) return VerifyEncounterEggLevelLoc(pkm, 1, Legal.ValidMet_SWSH); // no other games return new CheckResult(Severity.Invalid, LEggLocationInvalid, CheckIdentifier.Encounter); } private static CheckResult VerifyEncounterEgg8BDSP(PKM pkm) { if (pkm.BDSP) return VerifyEncounterEggLevelLoc(pkm, 1, Legal.IsValidEggHatchLocation8b); // no other games return new CheckResult(Severity.Invalid, LEggLocationInvalid, CheckIdentifier.Encounter); } private static CheckResult VerifyEncounterEggLevelLoc(PKM pkm, int eggLevel, ICollection MetLocations) { return VerifyEncounterEggLevelLoc(pkm, eggLevel, (location, _) => MetLocations.Contains(location)); } // (hatch location, hatch version, bool result) private static CheckResult VerifyEncounterEggLevelLoc(PKM pkm, int eggLevel, Func isValid) { if (pkm.Met_Level != eggLevel) return new CheckResult(Severity.Invalid, string.Format(LEggFMetLevel_0, eggLevel), CheckIdentifier.Encounter); return isValid(pkm.Met_Location, (GameVersion)pkm.Version) ? new CheckResult(Severity.Valid, LEggLocation, CheckIdentifier.Encounter) : new CheckResult(Severity.Invalid, LEggLocationInvalid, CheckIdentifier.Encounter); } private static CheckResult VerifyUnhatchedEgg(PKM pkm, int tradeLoc, short noneLoc = 0) { var eggLevel = pkm.Format < 5 ? 0 : 1; if (pkm.Met_Level != eggLevel) return new CheckResult(Severity.Invalid, string.Format(LEggFMetLevel_0, eggLevel), CheckIdentifier.Encounter); if (pkm.Egg_Location == tradeLoc) return new CheckResult(Severity.Invalid, LEggLocationTradeFail, CheckIdentifier.Encounter); var met = pkm.Met_Location; if (met == tradeLoc) return new CheckResult(Severity.Valid, LEggLocationTrade, CheckIdentifier.Encounter); return (short)met == noneLoc ? new CheckResult(Severity.Valid, LEggUnhatched, CheckIdentifier.Encounter) : new CheckResult(Severity.Invalid, LEggLocationNone, CheckIdentifier.Encounter); } // Other private static CheckResult VerifyEncounterWild(EncounterSlot slot) { var summary = slot.GetConditionString(out bool valid); return new CheckResult(valid ? Severity.Valid : Severity.Invalid, summary, CheckIdentifier.Encounter); } private static CheckResult VerifyEncounterStatic(PKM pkm, EncounterStatic s) { // Check for Unreleased Encounters / Collisions switch (s.Generation) { case 3: if (s is EncounterStaticShadow {EReader: true} && pkm.Language != (int)LanguageID.Japanese) // Non-JP E-reader Pokemon return new CheckResult(Severity.Invalid, LG3EReader, CheckIdentifier.Encounter); switch (s.Species) { case (int)Species.Mew when s.Location == 201 && pkm.Language != (int)LanguageID.Japanese: // Non-JP Mew (Old Sea Map) return new CheckResult(Severity.Invalid, LEncUnreleasedEMewJP, CheckIdentifier.Encounter); case (int)Species.Deoxys when s.Location == 200 && pkm.Language == (int)LanguageID.Japanese: // JP Deoxys (Birth Island) return new CheckResult(Severity.Invalid, LEncUnreleased, CheckIdentifier.Encounter); } break; case 4: if (s is EncounterStatic4 {Roaming: true} && pkm.Met_Location == 193 && pkm is IGroundTile {GroundTile:GroundTileType.Water}) // Roaming pokemon surfing in Johto Route 45 return new CheckResult(Severity.Invalid, LG4InvalidTileR45Surf, CheckIdentifier.Encounter); break; case 7: if (s.EggLocation == Locations.Daycare5 && pkm.RelearnMove1 != 0) // Eevee gift egg return new CheckResult(Severity.Invalid, LEncStaticRelearn, CheckIdentifier.RelearnMove); // not gift egg break; } if (s.EggEncounter && !pkm.IsEgg) // hatched { var hatchCheck = VerifyEncounterEgg(pkm, s.Generation); if (!hatchCheck.Valid) return hatchCheck; } return new CheckResult(Severity.Valid, LEncStaticMatch, CheckIdentifier.Encounter); } private static CheckResult VerifyEncounterTrade(ISpeciesForm pkm, EncounterTrade trade) { var species = pkm.Species; if (trade.EvolveOnTrade && trade.Species == species) { // Pokemon that evolve on trade can not be in the phase evolution after the trade // If the trade holds an everstone EvolveOnTrade will be false for the encounter var names = ParseSettings.SpeciesStrings; var evolved = names[species + 1]; var unevolved = names[species]; return new CheckResult(Severity.Invalid, string.Format(LEvoTradeReq, unevolved, evolved), CheckIdentifier.Encounter); } return new CheckResult(Severity.Valid, LEncTradeMatch, CheckIdentifier.Encounter); } private static CheckResult VerifyEncounterEvent(PKM pkm, MysteryGift gift) { switch (gift) { case PCD pcd: if (!pcd.CanBeReceivedBy(pkm.Version) && pcd.Gift.PK.Version == 0) return new CheckResult(Severity.Invalid, string.Format(L_XMatches0_1, gift.CardHeader, $"-- {LEncGiftVersionNotDistributed}"), CheckIdentifier.Encounter); break; } if (!pkm.IsEgg && gift.IsEgg) // hatched { var hatchCheck = VerifyEncounterEgg(pkm, gift.Generation); if (!hatchCheck.Valid) return hatchCheck; } // Strict matching already performed by EncounterGenerator. May be worth moving some checks here to better flag invalid gifts. return new CheckResult(Severity.Valid, string.Format(L_XMatches0_1, gift.CardHeader, string.Empty), CheckIdentifier.Encounter); } } }