using System; using System.Collections.Generic; using System.Linq; using static PKHeX.Core.Legal; using static PKHeX.Core.GameVersion; using static PKHeX.Core.Species; namespace PKHeX.Core; /// /// Miscellaneous GB Era restriction logic for legality checking /// internal static class GBRestrictions { private static readonly int[] G1Exeggcute_IncompatibleMoves = { 78, 77, 79 }; private static readonly int[] Stadium_CatchRate = { 167, // Normal Box 168, // Gorgeous Box }; private static readonly HashSet Stadium_GiftSpecies = new() { (int)Bulbasaur, (int)Charmander, (int)Squirtle, (int)Psyduck, (int)Hitmonlee, (int)Hitmonchan, (int)Eevee, (int)Omanyte, (int)Kabuto, }; /// /// Species that have a specific minimum amount of moves based on their evolution state. /// private static readonly HashSet SpecialMinMoveSlots = new() { (int)Pikachu, (int)Raichu, (int)NidoranF, (int)Nidorina, (int)Nidoqueen, (int)NidoranM, (int)Nidorino, (int)Nidoking, (int)Clefable, (int)Ninetales, (int)Wigglytuff, (int)Arcanine, (int)Cloyster, (int)Exeggutor, (int)Tangela, (int)Starmie, }; internal static bool TypeIDExists(int type) => Types_Gen1.Contains(type); /// /// Valid type IDs extracted from the Personal Table used for R/G/B/Y games. /// private static readonly HashSet Types_Gen1 = new() { 0, 1, 2, 3, 4, 5, 7, 8, 20, 21, 22, 23, 24, 25, 26, }; /// /// Species that have a catch rate value that is different from their pre-evolutions, and cannot be obtained directly. /// internal static readonly HashSet Species_NotAvailable_CatchRate = new() { (int)Butterfree, (int)Pidgeot, (int)Nidoqueen, (int)Nidoking, (int)Ninetales, (int)Vileplume, (int)Persian, (int)Arcanine, (int)Poliwrath, (int)Alakazam, (int)Machamp, (int)Victreebel, (int)Rapidash, (int)Cloyster, (int)Exeggutor, (int)Starmie, (int)Dragonite, }; internal static readonly HashSet Trade_Evolution1 = new() { (int)Kadabra, (int)Machoke, (int)Graveler, (int)Haunter, }; public static bool RateMatchesEncounter(int species, GameVersion version, int rate) { if (version.Contains(YW)) { if (rate == PersonalTable.Y[species].CatchRate) return true; if (version == YW) // no RB return false; } return rate == PersonalTable.RB[species].CatchRate; } private static int[] GetMinLevelLearnMoveG1(int species, List moves) { var result = new int[moves.Count]; for (int i = 0; i < result.Length; i++) result[i] = MoveLevelUp.GetIsLevelUp1(species, 0, moves[i], 100, 0).Level; return result; } private static int[] GetMaxLevelLearnMoveG1(int species, List moves) { var result = new int[moves.Count]; int index = PersonalTable.RB.GetFormIndex(species, 0); if (index == 0) return result; var pi_rb = ((PersonalInfoG1)PersonalTable.RB[index]).Moves; var pi_y = ((PersonalInfoG1)PersonalTable.Y[index]).Moves; for (int m = 0; m < moves.Count; m++) { bool start = pi_rb.Contains(moves[m]) && pi_y.Contains(moves[m]); result[m] = start ? 1 : Math.Max(GetHighest(LevelUpRB), GetHighest(LevelUpY)); int GetHighest(IReadOnlyList learn) => learn[index].GetLevelLearnMove(moves[m]); } return result; } private static List[] GetExclusiveMovesG1(int species1, int species2, IEnumerable tmhm, IEnumerable moves) { // Return from two species the exclusive moves that only one could learn and also the current pokemon have it in its current moveset var moves1 = MoveLevelUp.GetMovesLevelUp1(species1, 0, 1, 100); var moves2 = MoveLevelUp.GetMovesLevelUp1(species2, 0, 1, 100); // Remove common moves and remove tmhm, remove not learned moves var common = new HashSet(moves1.Intersect(moves2).Concat(tmhm)); var hashMoves = new HashSet(moves); moves1.RemoveAll(x => !hashMoves.Contains(x) || common.Contains(x)); moves2.RemoveAll(x => !hashMoves.Contains(x) || common.Contains(x)); return new[] { moves1, moves2 }; } internal static void GetIncompatibleEvolutionMoves(PKM pk, IReadOnlyList moves, IReadOnlyList tmhm, out int previousspecies, out IList incompatible_previous, out IList incompatible_current) { switch (pk.Species) { case (int)Nidoking when moves.Contains(31) && moves.Contains(37): // Nidoking learns Thrash at level 23 // Nidorino learns Fury Attack at level 36, Nidoran♂ at level 30 // Other moves are either learned by Nidoran♂ up to level 23 or by TM incompatible_current = new[] { 31 }; incompatible_previous = new[] { 37 }; previousspecies = 33; return; case (int)Exeggutor when moves.Contains(23) && moves.Any(m => G1Exeggcute_IncompatibleMoves.Contains(m)): // Exeggutor learns Stomp at level 28 // Exeggcute learns Stun Spore at 32, PoisonPowder at 37 and Sleep Powder at 48 incompatible_current = new[] { 23 }; incompatible_previous = G1Exeggcute_IncompatibleMoves; previousspecies = 103; return; case (int)Vaporeon or (int)Jolteon or (int)Flareon: incompatible_previous = new List(); incompatible_current = new List(); previousspecies = 133; var ExclusiveMoves = GetExclusiveMovesG1((int)Eevee, pk.Species, tmhm, moves); var EeveeLevels = GetMinLevelLearnMoveG1((int)Eevee, ExclusiveMoves[0]); var EvoLevels = GetMaxLevelLearnMoveG1(pk.Species, ExclusiveMoves[1]); for (int i = 0; i < ExclusiveMoves[0].Count; i++) { // There is a evolution move with a lower level that current Eevee move var el = EeveeLevels[i]; if (EvoLevels.Any(ev => ev < el)) incompatible_previous.Add(ExclusiveMoves[0][i]); } for (int i = 0; i < ExclusiveMoves[1].Count; i++) { // There is an Eevee move with a greater level that current evolution move var el = EvoLevels[i]; if (EeveeLevels.Any(ev => ev > el)) incompatible_current.Add(ExclusiveMoves[1][i]); } return; } incompatible_previous = Array.Empty(); incompatible_current = Array.Empty(); previousspecies = 0; } internal static int GetRequiredMoveCount(PK1 pk, IReadOnlyList moves, LegalInfo info, IReadOnlyList initialmoves) { var enc = info.EncounterMatch; var capsuleState = IsTimeCapsuleTransferred(pk, info.Moves, enc); if (capsuleState != TimeCapsuleEvaluation.NotTransferred) // No Move Deleter in Gen 1 return 1; // Move Deleter exits, slots from 2 onwards can always be empty if (enc is IMoveset {Moves.Count: 4 }) return 4; int required = GetRequiredMoveCount(pk, moves, info.EncounterMoves.LevelUpMoves, initialmoves, enc.Species); if (required >= 4) return 4; // tm, hm and tutor moves replace a free slots if the pokemon have less than 4 moves // Ignore tm, hm and tutor moves already in the learnset table var learn = info.EncounterMoves.LevelUpMoves; var tmhm = info.EncounterMoves.TMHMMoves; var tutor = info.EncounterMoves.TutorMoves; var union = initialmoves.Union(learn[1]); required += moves.Count(m => m != 0 && union.All(t => t != m) && (tmhm[1].Any(t => t == m) || tutor[1].Any(t => t == m))); return Math.Min(4, required); } private static int GetRequiredMoveCount(PK1 pk, IReadOnlyList moves, IReadOnlyList[] learn, IReadOnlyList initialmoves, int originalSpecies) { if (SpecialMinMoveSlots.Contains(pk.Species)) return GetRequiredMoveCountSpecial(pk, moves, learn, originalSpecies); // A pokemon is captured with initial moves and can't forget any until have all 4 slots used // If it has learn a move before having 4 it will be in one of the free slots int required = GetRequiredMoveSlotsRegular(pk, moves, learn, initialmoves, originalSpecies); return required != 0 ? required : GetRequiredMoveCountDecrement(pk, moves, learn, initialmoves); } private static int GetRequiredMoveSlotsRegular(PK1 pk, IReadOnlyList moves, IReadOnlyList[] learn, IReadOnlyList initialmoves, int originalSpecies) { if (originalSpecies == pk.Species) return 0; // Can skip over all learned moves that we can reasonably care about return originalSpecies switch { (int)Caterpie => initialmoves.Union(learn[1]).Distinct().Count(lm => lm != 0 && (lm != (int)Move.Harden || moves.Contains((int)Move.Harden))), (int)Metapod => initialmoves.Union(learn[1]).Distinct().Count(lm => lm != 0 && (lm != (int)Move.Confusion || moves.Contains((int)Move.Confusion))), (int)Mankey => initialmoves.Union(learn[1]).Distinct().Count(lm => lm != 0 && (lm != (int)Move.Rage || moves.Contains((int)Move.Rage))), _ => IsMoveCountRequired3(pk.Species, pk.CurrentLevel, moves) ? 3 : 0, }; } private static bool IsMoveCountRequired3(int species, int level, IReadOnlyList moves) => species switch { // Species that evolve and learn the 4th move as evolved species at a greater level than base species // The 4th move is included in the level up table set as a pre-evolution move, // it should be removed from the used slots count if is not the learn move (int)Pidgeotto => level < 21 && !moves.Contains(018), // Whirlwind (int)Sandslash => level < 27 && !moves.Contains(040), // Poison Sting (int)Parasect => level < 30 && !moves.Contains(147), // Spore (int)Golduck => level < 39 && !moves.Contains(093), // Confusion (int)Dewgong => level < 44 && !moves.Contains(156), // Rest (int)Weezing => level < 39 && !moves.Contains(108), // Smoke Screen (int)Haunter or (int)Gengar => level < 29 && !moves.Contains(095), // Hypnosis _ => false, }; private static int GetRequiredMoveCountDecrement(PKM pk, IReadOnlyList moves, IReadOnlyList[] learn, IReadOnlyList initialmoves) { int usedslots = initialmoves.Union(learn[1]).Where(m => m != 0).Distinct().Count(); switch (pk.Species) { case (int)Venonat: // Venonat; ignore Venomoth (by the time Venonat evolves it will always have 4 moves) if (pk.CurrentLevel >= 11 && !moves.Contains(48)) // Supersonic usedslots--; if (pk.CurrentLevel >= 19 && !moves.Contains(93)) // Confusion usedslots--; break; case (int)Kadabra or (int)Alakazam: // Abra & Kadabra int catch_rate = ((PK1)pk).Catch_Rate; if (catch_rate != 100)// Initial Yellow Kadabra Kinesis (move 134) usedslots--; if (catch_rate == 200 && pk.CurrentLevel < 20) // Kadabra Disable, not learned until 20 if captured as Abra (move 50) usedslots--; break; case (int)Cubone or (int)Marowak: // Cubone & Marowak if (!moves.Contains((int)Move.TailWhip) && !moves.Contains((int)Move.Headbutt)) // Initial Red { usedslots -=2; } else { if (!moves.Contains(39)) // Initial Yellow Tail Whip usedslots--; if (!moves.Contains(125)) // Initial Yellow Bone Club usedslots--; } if (pk.Species == 105 && pk.CurrentLevel < 33 && !moves.Contains(116)) // Marowak evolved without Focus Energy usedslots--; break; case (int)Chansey: if (!moves.Contains(39)) // Yellow Initial Tail Whip usedslots--; if (!moves.Contains(3)) // Yellow Lvl 12 and Initial Red/Blue Double Slap usedslots--; break; case (int)Mankey when pk.CurrentLevel >= 9 && !moves.Contains(67): // Mankey (Low Kick) case (int)Pinsir when pk.CurrentLevel >= 21 && !moves.Contains(20): // Pinsir (Bind) case (int)Gyarados when pk.CurrentLevel < 32: // Gyarados usedslots--; break; default: return usedslots; } return usedslots; } private static int GetRequiredMoveCountSpecial(PKM pk, IReadOnlyList moves, IReadOnlyList[] learn, int originalSpecies) { // Species with few mandatory slots, species with stone evolutions that could evolve at lower level and do not learn any more moves // and Pikachu and Nidoran family, those only have mandatory the initial moves and a few have one level up moves, // every other move could be avoided switching game or evolving var mandatory = GetRequiredMoveCountLevel(pk, originalSpecies); switch (pk.Species) { case (int)Exeggutor when pk.CurrentLevel >= 28: // Exeggutor // At level 28 learn different move if is a Exeggute or Exeggutor if (moves.Contains(73)) mandatory.Add(73); // Leech Seed level 28 Exeggute if (moves.Contains(23)) mandatory.Add(23); // Stomp level 28 Exeggutor break; case (int)Pikachu when pk.CurrentLevel >= 33: mandatory.Add(97); // Pikachu always learns Agility break; case (int)Tangela: mandatory.Add(132); // Tangela always has Constrict as Initial Move break; } // Add to used slots the non-mandatory moves from the learnset table that the pokemon have learned return mandatory.Distinct().Count(z => z != 0) + moves.Where(m => m != 0).Count(m => !mandatory.Contains(m) && learn[1].Contains(m)); } private static List GetRequiredMoveCountLevel(PKM pk, int basespecies) { int species = pk.Species; int maxlevel = 1; int minlevel = 1; if (species == (int)Tangela) // Tangela moves before level 32 are different in RB vs Y { minlevel = 32; maxlevel = pk.CurrentLevel; } else if (species is >= (int)NidoranF and <= (int)Nidoking && pk.CurrentLevel >= 8) { maxlevel = 8; // Always learns a third move at level 8 } if (minlevel > pk.CurrentLevel) return new List(); return MoveLevelUp.GetMovesLevelUp1(basespecies, 0, maxlevel, minlevel); } internal static IEnumerable GetGen2Versions(IEncounterTemplate enc, bool korean) { if (ParseSettings.AllowGen2Crystal(korean) && enc.Version is C or GSC) yield return C; yield return GS; } internal static IEnumerable GetGen1Versions(IEncounterTemplate enc) { if (enc.Species == (int)Eevee && enc.Version == Stadium) { // Stadium Eevee; check for RB and yellow initial moves yield return RB; yield return YW; yield break; } if (enc.Version == YW) { yield return YW; yield break; } // Any encounter marked with version RBY is for pokemon with the same moves and catch rate in RB and Y, // it is sufficient to check just RB's case yield return RB; } private static bool GetCatchRateMatchesPreEvolution(PK1 pk, int catch_rate) { // For species catch rate, discard any species that has no valid encounters and a different catch rate than their pre-evolutions var table = EvolutionTree.Evolves1; var chain = table.GetValidPreEvolutions(pk, maxLevel: (byte)pk.CurrentLevel); foreach (var entry in chain) { var s = entry.Species; if (Species_NotAvailable_CatchRate.Contains(s)) continue; if (catch_rate == PersonalTable.RB[s].CatchRate || catch_rate == PersonalTable.Y[s].CatchRate) return true; } // Krabby encounter trade special catch rate int species = pk.Species; if (catch_rate == 204 && (species is (int)Krabby or (int)Kingler)) return true; if (Stadium_GiftSpecies.Contains(species) && Stadium_CatchRate.Contains(catch_rate)) return true; return false; } /// /// Checks if the can inhabit /// /// Data to check /// true if can inhabit, false if not. private static bool CanInhabitGen1(this PKM pk) { // Korean Gen2 games can't trade-back because there are no Gen1 Korean games released if (pk.Korean || pk.IsEgg) return false; // Gen2 format with met data can't receive Gen1 moves, unless Stadium 2 is used (Oak's PC). // If you put a Pokemon in the N64 box, the met info is retained, even if you switch over to a Gen I game to teach it TMs // You can use rare candies from within the lab, so level-up moves from RBY context can be learned this way as well // Stadium 2 is GB Cart Era only (not 3DS Virtual Console). if (pk is ICaughtData2 {CaughtData: not 0} && !ParseSettings.AllowGBCartEra) return false; // Sanity check species, if it could have existed as a pre-evolution. int species = pk.Species; if (species <= MaxSpeciesID_1) return true; return EvolutionLegality.FutureEvolutionsGen1.Contains(species); } /// /// Gets the Tradeback status depending on various values. /// /// Pokémon to guess the tradeback status from. internal static PotentialGBOrigin GetTradebackStatusInitial(PKM pk) { if (pk is PK1 pk1) return GetTradebackStatusRBY(pk1); if (pk.Format == 2 || pk.VC2) // Check for impossible tradeback scenarios return !pk.CanInhabitGen1() ? PotentialGBOrigin.Gen2Only : PotentialGBOrigin.Either; // VC2 is released, we can assume it will be TradebackType.Any. // Is impossible to differentiate a VC1 pokemon traded to Gen7 after VC2 is available. // Met Date cannot be used definitively as the player can change their system clock. return PotentialGBOrigin.Either; } /// /// Gets the Tradeback status depending on the /// /// Pokémon to guess the tradeback status from. private static PotentialGBOrigin GetTradebackStatusRBY(PK1 pk) { if (!ParseSettings.AllowGen1Tradeback) return PotentialGBOrigin.Gen1Only; // Detect tradeback status by comparing the catch rate(Gen1)/held item(Gen2) to the species in the pk's evolution chain. var catch_rate = pk.Catch_Rate; if (catch_rate == 0) return PotentialGBOrigin.Either; bool matchAny = GetCatchRateMatchesPreEvolution(pk, catch_rate); if (!matchAny) return PotentialGBOrigin.Either; if (HeldItems_GSC.Contains((ushort)catch_rate)) return PotentialGBOrigin.Either; return PotentialGBOrigin.Gen1Only; } public static TimeCapsuleEvaluation IsTimeCapsuleTransferred(PKM pk, IReadOnlyList moves, IEncounterTemplate enc) { foreach (var z in moves) { if (z.Generation == enc.Generation || z.Generation > 2) continue; if (pk is PK1 {Catch_Rate: not 0} g1 && !IsTradebackCatchRate(g1.Catch_Rate)) return TimeCapsuleEvaluation.BadCatchRate; return enc.Generation == 2 ? TimeCapsuleEvaluation.Transferred21 : TimeCapsuleEvaluation.Transferred12; } if (pk is not GBPKM gb) { return enc.Generation switch { 2 when pk.VC2 => TimeCapsuleEvaluation.Transferred21, 1 when pk.VC1 => TimeCapsuleEvaluation.Transferred12, _ => TimeCapsuleEvaluation.NotTransferred, }; } if (gb is ICaughtData2 pk2) { if (enc.Generation == 1) return TimeCapsuleEvaluation.Transferred12; if (pk2.CaughtData != 0) return TimeCapsuleEvaluation.NotTransferred; if (enc.Version == C) return TimeCapsuleEvaluation.Transferred21; return TimeCapsuleEvaluation.Indeterminate; } if (gb is PK1 pk1) { var rate = pk1.Catch_Rate; if (rate == 0) return TimeCapsuleEvaluation.Transferred12; bool isTradebackItem = IsTradebackCatchRate(rate); if (IsCatchRateMatchEncounter(enc, pk1)) return isTradebackItem ? TimeCapsuleEvaluation.Indeterminate : TimeCapsuleEvaluation.NotTransferred; return isTradebackItem ? TimeCapsuleEvaluation.Transferred12 : TimeCapsuleEvaluation.BadCatchRate; } return TimeCapsuleEvaluation.Indeterminate; } private static bool IsCatchRateMatchEncounter(IEncounterTemplate enc, PK1 pk1) => enc switch { EncounterStatic1 s when s.GetMatchRating(pk1) != EncounterMatchRating.PartialMatch => true, EncounterTrade1 => true, _ => RateMatchesEncounter(enc.Species, enc.Version, pk1.Catch_Rate), }; public static bool IsTradebackCatchRate(int rate) { return HeldItems_GSC.Contains((ushort)rate); } } public enum PotentialGBOrigin { Either, Gen1Only, Gen2Only, } public enum TimeCapsuleEvaluation { Indeterminate, Transferred21, Transferred12, NotTransferred, BadCatchRate, } public static class TimeCapsuleEvlautationExtensions { public static bool WasTimeCapsuleTransferred(this TimeCapsuleEvaluation eval) => eval is not (TimeCapsuleEvaluation.Indeterminate or TimeCapsuleEvaluation.NotTransferred or TimeCapsuleEvaluation.BadCatchRate); }