From 18f95269c071ec4141b0a8d5f7e0453e19385b23 Mon Sep 17 00:00:00 2001 From: Kurt Date: Thu, 8 Jan 2026 23:41:02 -0600 Subject: [PATCH] Misc edge case tests Gen9a Antishiny edge case Evolve-move traversal tweaks; eager checks and more --- .../Encounters/Templates/Gen9a/LumioseRNG.cs | 6 +- .../Templates/Gen9a/LumioseSolver.cs | 6 ++ .../Legality/LearnSource/Group/LearnGroup7.cs | 8 +- .../Legality/LearnSource/Group/LearnGroup8.cs | 2 +- .../LearnSource/Group/LearnGroup8a.cs | 2 +- .../LearnSource/Group/LearnGroup8b.cs | 2 +- .../Legality/LearnSource/Group/LearnGroup9.cs | 2 +- .../LearnSource/Group/LearnGroup9a.cs | 2 +- .../LearnSource/Group/LearnGroupHOME.cs | 40 +++++----- .../Restrictions/EvolutionRestrictions.cs | 77 +++++++++++++++++++ 10 files changed, 118 insertions(+), 29 deletions(-) diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen9a/LumioseRNG.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen9a/LumioseRNG.cs index 26e67b0cb..ad21ed595 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen9a/LumioseRNG.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen9a/LumioseRNG.cs @@ -321,13 +321,15 @@ private static uint GetAdaptedPID(ref Xoroshiro128Plus rand, PKM pk, in Generate else // Never { if (ShinyUtil.GetIsShiny6(fakeTID, pid)) // battled - pid ^= 0x1000_0000; + pid = AntiShiny(pid); if (ShinyUtil.GetIsShiny6(pk.ID32, pid)) // captured - pid ^= 0x1000_0000; + pid = AntiShiny(pid); } return pid; } + public static uint AntiShiny(uint pid) => pid ^ 0x1000_0000; + private static bool IsMatchUnknownPreFillIVs(PKM pk, in GenerateParam9a enc, Xoroshiro128Plus rand) { int k = enc.FlawlessIVs; diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen9a/LumioseSolver.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen9a/LumioseSolver.cs index b0d44553b..61439a7bd 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen9a/LumioseSolver.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen9a/LumioseSolver.cs @@ -81,6 +81,12 @@ private static bool TryGetSeedConsecutive(in GenerateParam9a param, PKM pk, uint private static bool TryGetSeedSkip(in GenerateParam9a param, PKM pk, uint ec, uint pid, out ulong seed) { var solver = new XoroMachineSkip(ec, pid); + if (TryGetSeed(param, pk, solver, out seed)) + return true; + + // Try again assuming the FakeTrainer XORed the resulting PID to be anti-shiny (terrible luck for the player). + pid = LumioseRNG.AntiShiny(pid); + solver = new XoroMachineSkip(ec, pid); return TryGetSeed(param, pk, solver, out seed); } diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup7.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup7.cs index a4df815cc..03b88ddc3 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup7.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup7.cs @@ -13,10 +13,10 @@ public sealed class LearnGroup7 : ILearnGroup public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => enc.Generation switch { - Generation => null, - 1 => history.HasVisitedGen1 ? LearnGroup1.Instance : null, - <= 2 => history.HasVisitedGen2 ? LearnGroup2.Instance : null, - _ => history.HasVisitedGen6 ? LearnGroup6.Instance : null, + 1 => LearnGroup1.Instance, + 2 => LearnGroup2.Instance, + (3 or 4 or 5 or 6) => LearnGroup6.Instance, + _ => null, }; public bool HasVisited(PKM pk, EvolutionHistory history) => history.HasVisitedGen7; diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8.cs index 4048fecaa..a36361818 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8.cs @@ -54,7 +54,7 @@ public sealed class LearnGroup8 : ILearnGroup var home = LearnGroupHOME.Instance; if (option != LearnOption.HOME && home.HasVisited(pk, history)) - return home.Check(result, current, pk, history, enc, types); + return home.Check(result, current, pk, history, enc, types, option); return false; } diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8a.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8a.cs index 80aa699d3..d1bf48f2b 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8a.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8a.cs @@ -27,7 +27,7 @@ public sealed class LearnGroup8a : ILearnGroup var home = LearnGroupHOME.Instance; if (option != LearnOption.HOME && home.HasVisited(pk, history)) - return home.Check(result, current, pk, history, enc, types); + return home.Check(result, current, pk, history, enc, types, option); return false; } diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8b.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8b.cs index 2523624da..71a84a1f8 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8b.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup8b.cs @@ -33,7 +33,7 @@ public sealed class LearnGroup8b : ILearnGroup var home = LearnGroupHOME.Instance; if (option != LearnOption.HOME && home.HasVisited(pk, history)) - return home.Check(result, current, pk, history, enc, types); + return home.Check(result, current, pk, history, enc, types, option); return false; } diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9.cs index bc2333fd6..1d482b1c9 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9.cs @@ -35,7 +35,7 @@ public sealed class LearnGroup9 : ILearnGroup var home = LearnGroupHOME.Instance; if (option != LearnOption.HOME && home.HasVisited(pk, history)) - return home.Check(result, current, pk, history, enc, types); + return home.Check(result, current, pk, history, enc, types, option); return false; } diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9a.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9a.cs index 104b9d7bc..c2d1b5f29 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9a.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroup9a.cs @@ -30,7 +30,7 @@ public sealed class LearnGroup9a : ILearnGroup var home = LearnGroupHOME.Instance; if (option != LearnOption.HOME && home.HasVisited(pk, history)) - return home.Check(result, current, pk, history, enc, types); + return home.Check(result, current, pk, history, enc, types, option); return false; } diff --git a/PKHeX.Core/Legality/LearnSource/Group/LearnGroupHOME.cs b/PKHeX.Core/Legality/LearnSource/Group/LearnGroupHOME.cs index a8fb22fd4..c614c7aee 100644 --- a/PKHeX.Core/Legality/LearnSource/Group/LearnGroupHOME.cs +++ b/PKHeX.Core/Legality/LearnSource/Group/LearnGroupHOME.cs @@ -12,13 +12,14 @@ namespace PKHeX.Core; public sealed class LearnGroupHOME : ILearnGroup { public static readonly LearnGroupHOME Instance = new(); + private const LearnOption Option = LearnOption.HOME; public ushort MaxMoveID => 0; public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => null; public bool HasVisited(PKM pk, EvolutionHistory history) => pk is IHomeTrack { HasTracker: true } || !ParseSettings.IgnoreTransferIfNoTracker; public bool Check(Span result, ReadOnlySpan current, PKM pk, EvolutionHistory history, - IEncounterTemplate enc, MoveSourceType types = MoveSourceType.All, LearnOption option = LearnOption.HOME) + IEncounterTemplate enc, MoveSourceType types = MoveSourceType.All, LearnOption option = Option) { var context = pk.Context; if (context == EntityContext.None) @@ -29,36 +30,36 @@ public sealed class LearnGroupHOME : ILearnGroup if (history.HasVisitedGen9 && pk is not PK9) { var instance = LearnGroup9.Instance; - instance.Check(result, current, pk, history, enc, types, option); - if (CleanPurge(result, current, pk, types, local, evos)) + instance.Check(result, current, pk, history, enc, types, Option); + if (CleanPurge(result, current, pk, types, local, evos, option)) return true; } if (history.HasVisitedZA && pk is not PA9) { var instance = LearnGroup9a.Instance; - instance.Check(result, current, pk, history, enc, types, option); - if (CleanPurge(result, current, pk, types, local, evos)) + instance.Check(result, current, pk, history, enc, types, Option); + if (CleanPurge(result, current, pk, types, local, evos, option)) return true; } if (history.HasVisitedSWSH && pk is not PK8) { var instance = LearnGroup8.Instance; - instance.Check(result, current, pk, history, enc, types, option); - if (CleanPurge(result, current, pk, types, local, evos)) + instance.Check(result, current, pk, history, enc, types, Option); + if (CleanPurge(result, current, pk, types, local, evos, option)) return true; } if (history.HasVisitedPLA && pk is not PA8) { var instance = LearnGroup8a.Instance; - instance.Check(result, current, pk, history, enc, types, option); - if (CleanPurge(result, current, pk, types, local, evos)) + instance.Check(result, current, pk, history, enc, types, Option); + if (CleanPurge(result, current, pk, types, local, evos, option)) return true; } if (history.HasVisitedBDSP && pk is not PB8) { var instance = LearnGroup8b.Instance; - instance.Check(result, current, pk, history, enc, types, option); - if (CleanPurge(result, current, pk, types, local, evos)) + instance.Check(result, current, pk, history, enc, types, Option); + if (CleanPurge(result, current, pk, types, local, evos, option)) return true; } @@ -67,14 +68,14 @@ public sealed class LearnGroupHOME : ILearnGroup // SW/SH is the only game that can ever harbor external moves, and is the only game that uses Battle Version. if (TryAddOriginalMoves(result, current, pk, enc)) { - if (CleanPurge(result, current, pk, types, local, evos)) + if (CleanPurge(result, current, pk, types, local, evos, option)) return true; } // HOME is silly and allows form exclusive moves to be transferred without ever knowing the move. if (TryAddExclusiveMoves(result, current, pk)) { - if (CleanPurge(result, current, pk, types, local, evos)) + if (CleanPurge(result, current, pk, types, local, evos, option)) return true; } @@ -82,8 +83,8 @@ public sealed class LearnGroupHOME : ILearnGroup { // PK8 w/ Battle Version can be ignored, as LGP/E has separate HOME data. var instance = LearnGroup7b.Instance; - instance.Check(result, current, pk, history, enc, types, option); - if (CleanPurge(result, current, pk, types, local, evos)) + instance.Check(result, current, pk, history, enc, types, Option); + if (CleanPurge(result, current, pk, types, local, evos, option)) return true; } else if (history.HasVisitedGen7) @@ -93,8 +94,8 @@ public sealed class LearnGroupHOME : ILearnGroup ILearnGroup instance = LearnGroup7.Instance; while (true) { - instance.Check(result, current, pk, history, enc, types, option); - if (CleanPurge(result, current, pk, types, local, evos)) + instance.Check(result, current, pk, history, enc, types, Option); + if (CleanPurge(result, current, pk, types, local, evos, option)) return true; var prev = instance.GetPrevious(pk, history, enc, option); if (prev is null) @@ -111,8 +112,11 @@ public sealed class LearnGroupHOME : ILearnGroup /// Scan the results and remove any that are not valid for the game game. /// /// True if all results are valid. - private static bool CleanPurge(Span result, ReadOnlySpan current, PKM pk, MoveSourceType types, IHomeSource local, ReadOnlySpan evos) + private static bool CleanPurge(Span result, ReadOnlySpan current, PKM pk, MoveSourceType types, IHomeSource local, ReadOnlySpan evos, LearnOption option) { + if (option == LearnOption.AtAnyTime) + return MoveResult.AllParsed(result); + // The logic used to update the results did not check if the move could be learned in the local game. // Double-check the results and remove any that are not valid for the local game. // SW/SH will continue to iterate downwards to previous groups after HOME is checked, so we can exactly check via Environment. diff --git a/PKHeX.Core/Legality/Restrictions/EvolutionRestrictions.cs b/PKHeX.Core/Legality/Restrictions/EvolutionRestrictions.cs index fde0fe0d0..f28763047 100644 --- a/PKHeX.Core/Legality/Restrictions/EvolutionRestrictions.cs +++ b/PKHeX.Core/Legality/Restrictions/EvolutionRestrictions.cs @@ -15,6 +15,9 @@ internal static class EvolutionRestrictions /// /// List of species that evolve from a previous species having a move while leveling up /// + /// + /// Using moves with a counter stored in form argument is explicitly checked via other logic. + /// private static ushort GetSpeciesEvolutionMove(ushort species) => species switch { (int)Sylveon => EEVEE, @@ -28,15 +31,22 @@ internal static class EvolutionRestrictions (int)Tsareena => (int)Stomp, (int)Naganadel => (int)DragonPulse, (int)Grapploct => (int)Taunt, + + // Form Argument evolutions sometimes with extra conditions; verify here because they aren't checked "completely" elsewhere (yet). (int)Wyrdeer => (int)PsyshieldBash, (int)Overqwil => (int)BarbBarrage, (int)Annihilape => (int)RageFist, + (int)Farigiraf => (int)TwinBeam, (int)Dudunsparce => (int)HyperDrill, (int)Hydrapple => (int)DragonCheer, _ => NONE, }; + /// + /// Checks if the evolution is the "rare" variant (less common). + /// + /// Random value used to pivot between evolution results. public static bool IsEvolvedSpeciesFormRare(uint encryptionConstant) => encryptionConstant % 100 is 0; /// @@ -50,6 +60,9 @@ internal static class EvolutionRestrictions _ => throw new ArgumentOutOfRangeException(nameof(species), species, "Incorrect EC%100 species."), }; + /// + /// Checks if the evolution result matches what is expected for + /// public static bool GetIsExpectedEvolveFormEC100(ushort species, byte form, bool rare) => species switch { (ushort)Maushold => form == (byte)(rare ? 0 : 1), @@ -76,6 +89,9 @@ public static bool IsFormArgEvolution(ushort species) /// Checks if the is correctly evolved, assuming it had a known move requirement evolution in its evolution chain. /// /// True if unnecessary to check or the evolution was valid. + /// + /// Performs some eager checks to skip doing a full evolution tree move check. + /// public static bool IsValidEvolutionWithMove(PKM pk, LegalInfo info) { // Known-move evolutions were introduced in Gen4. @@ -88,6 +104,15 @@ public static bool IsValidEvolutionWithMove(PKM pk, LegalInfo info) if (enc.Species == species) return true; + // All move evolutions arrive at a maximally-evolved species chain. + // Except for Mr. Rime -- just manually devolve and let the rest of the logic check. + if (species is (ushort)MrRime) + { + species = (ushort)MrMime; + if (enc.Species == species) + return true; + } + // Exclude evolution paths that did not require a move w/level-up evolution var move = GetSpeciesEvolutionMove(species); if (move is NONE) @@ -95,6 +120,10 @@ public static bool IsValidEvolutionWithMove(PKM pk, LegalInfo info) if (!IsMoveSlotAvailable(info.Moves)) return false; + // Check if the move is in relearn moves (can know at any time) + if (pk.Format >= 6 && IsMoveInRelearnSource(pk, info, move)) + return true; + // Check the entire chain to see if it could have learnt it at any point. var head = LearnGroupUtil.GetCurrentGroup(pk); var pruned = info.EvoChainsAllGens.PruneKeepPreEvolutions(species); @@ -104,6 +133,54 @@ public static bool IsValidEvolutionWithMove(PKM pk, LegalInfo info) return MemoryPermissions.GetCanKnowMove(enc, move, pruned, pk, head); } + private static bool IsMoveInRelearnSource(PKM pk, LegalInfo info, ushort move) + { + if (move is not EEVEE) + return IsMoveInRelearn(pk, info, move); + return IsMoveInRelearn(pk, info, EeveeFairyMoves); + } + + private static bool IsMoveInRelearn(PKM pk, LegalInfo info, ReadOnlySpan arr) + { + foreach (var move in arr) + { + if (IsMoveInRelearn(pk, info, move)) + return true; + } + return false; + } + + private static bool IsMoveInRelearn(PKM pk, LegalInfo info, ushort move) + { + if (pk.IsOriginalMovesetDeleted()) + return WasMoveInRelearn(pk, info, move); + + var first = pk.RelearnMove1; + if (first is 0) // eager check + return false; + if (pk.RelearnMove1 == move) + return true; + if (pk.RelearnMove2 == move) + return true; + if (pk.RelearnMove3 == move) + return true; + if (pk.RelearnMove4 == move) + return true; + return false; + } + + private static bool WasMoveInRelearn(PKM pk, LegalInfo info, ushort move) + { + for (var i = 0; i < info.Moves.Length; i++) + { + if (pk.GetMove(i) != move) + continue; + var method = info.Moves[i].Info.Method; + return method is { IsEggSource: true } or { IsRelearn: true } or LearnMethod.Encounter; + } + return false; + } + private static bool IsValidEvolutionWithMoveAny(IEncounterTemplate enc, ReadOnlySpan any, EvolutionHistory history, PKM pk, ILearnGroup head) { foreach (var move in any)