Misc edge case tests

Gen9a Antishiny edge case
Evolve-move traversal tweaks; eager checks and more
This commit is contained in:
Kurt 2026-01-08 23:41:02 -06:00
parent 812f8e847e
commit 18f95269c0
10 changed files with 118 additions and 29 deletions

View File

@ -321,13 +321,15 @@ private static uint GetAdaptedPID(ref Xoroshiro128Plus rand, PKM pk, in Generate
else // Never else // Never
{ {
if (ShinyUtil.GetIsShiny6(fakeTID, pid)) // battled if (ShinyUtil.GetIsShiny6(fakeTID, pid)) // battled
pid ^= 0x1000_0000; pid = AntiShiny(pid);
if (ShinyUtil.GetIsShiny6(pk.ID32, pid)) // captured if (ShinyUtil.GetIsShiny6(pk.ID32, pid)) // captured
pid ^= 0x1000_0000; pid = AntiShiny(pid);
} }
return pid; return pid;
} }
public static uint AntiShiny(uint pid) => pid ^ 0x1000_0000;
private static bool IsMatchUnknownPreFillIVs(PKM pk, in GenerateParam9a enc, Xoroshiro128Plus rand) private static bool IsMatchUnknownPreFillIVs(PKM pk, in GenerateParam9a enc, Xoroshiro128Plus rand)
{ {
int k = enc.FlawlessIVs; int k = enc.FlawlessIVs;

View File

@ -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) private static bool TryGetSeedSkip(in GenerateParam9a param, PKM pk, uint ec, uint pid, out ulong seed)
{ {
var solver = new XoroMachineSkip(ec, pid); 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); return TryGetSeed(param, pk, solver, out seed);
} }

View File

@ -13,10 +13,10 @@ public sealed class LearnGroup7 : ILearnGroup
public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => enc.Generation switch public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => enc.Generation switch
{ {
Generation => null, 1 => LearnGroup1.Instance,
1 => history.HasVisitedGen1 ? LearnGroup1.Instance : null, 2 => LearnGroup2.Instance,
<= 2 => history.HasVisitedGen2 ? LearnGroup2.Instance : null, (3 or 4 or 5 or 6) => LearnGroup6.Instance,
_ => history.HasVisitedGen6 ? LearnGroup6.Instance : null, _ => null,
}; };
public bool HasVisited(PKM pk, EvolutionHistory history) => history.HasVisitedGen7; public bool HasVisited(PKM pk, EvolutionHistory history) => history.HasVisitedGen7;

View File

@ -54,7 +54,7 @@ public sealed class LearnGroup8 : ILearnGroup
var home = LearnGroupHOME.Instance; var home = LearnGroupHOME.Instance;
if (option != LearnOption.HOME && home.HasVisited(pk, history)) 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; return false;
} }

View File

@ -27,7 +27,7 @@ public sealed class LearnGroup8a : ILearnGroup
var home = LearnGroupHOME.Instance; var home = LearnGroupHOME.Instance;
if (option != LearnOption.HOME && home.HasVisited(pk, history)) 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; return false;
} }

View File

@ -33,7 +33,7 @@ public sealed class LearnGroup8b : ILearnGroup
var home = LearnGroupHOME.Instance; var home = LearnGroupHOME.Instance;
if (option != LearnOption.HOME && home.HasVisited(pk, history)) 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; return false;
} }

View File

@ -35,7 +35,7 @@ public sealed class LearnGroup9 : ILearnGroup
var home = LearnGroupHOME.Instance; var home = LearnGroupHOME.Instance;
if (option != LearnOption.HOME && home.HasVisited(pk, history)) 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; return false;
} }

View File

@ -30,7 +30,7 @@ public sealed class LearnGroup9a : ILearnGroup
var home = LearnGroupHOME.Instance; var home = LearnGroupHOME.Instance;
if (option != LearnOption.HOME && home.HasVisited(pk, history)) 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; return false;
} }

View File

@ -12,13 +12,14 @@ namespace PKHeX.Core;
public sealed class LearnGroupHOME : ILearnGroup public sealed class LearnGroupHOME : ILearnGroup
{ {
public static readonly LearnGroupHOME Instance = new(); public static readonly LearnGroupHOME Instance = new();
private const LearnOption Option = LearnOption.HOME;
public ushort MaxMoveID => 0; public ushort MaxMoveID => 0;
public ILearnGroup? GetPrevious(PKM pk, EvolutionHistory history, IEncounterTemplate enc, LearnOption option) => null; 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 HasVisited(PKM pk, EvolutionHistory history) => pk is IHomeTrack { HasTracker: true } || !ParseSettings.IgnoreTransferIfNoTracker;
public bool Check(Span<MoveResult> result, ReadOnlySpan<ushort> current, PKM pk, EvolutionHistory history, public bool Check(Span<MoveResult> result, ReadOnlySpan<ushort> 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; var context = pk.Context;
if (context == EntityContext.None) if (context == EntityContext.None)
@ -29,36 +30,36 @@ public sealed class LearnGroupHOME : ILearnGroup
if (history.HasVisitedGen9 && pk is not PK9) if (history.HasVisitedGen9 && pk is not PK9)
{ {
var instance = LearnGroup9.Instance; var instance = LearnGroup9.Instance;
instance.Check(result, current, pk, history, enc, types, option); instance.Check(result, current, pk, history, enc, types, Option);
if (CleanPurge(result, current, pk, types, local, evos)) if (CleanPurge(result, current, pk, types, local, evos, option))
return true; return true;
} }
if (history.HasVisitedZA && pk is not PA9) if (history.HasVisitedZA && pk is not PA9)
{ {
var instance = LearnGroup9a.Instance; var instance = LearnGroup9a.Instance;
instance.Check(result, current, pk, history, enc, types, option); instance.Check(result, current, pk, history, enc, types, Option);
if (CleanPurge(result, current, pk, types, local, evos)) if (CleanPurge(result, current, pk, types, local, evos, option))
return true; return true;
} }
if (history.HasVisitedSWSH && pk is not PK8) if (history.HasVisitedSWSH && pk is not PK8)
{ {
var instance = LearnGroup8.Instance; var instance = LearnGroup8.Instance;
instance.Check(result, current, pk, history, enc, types, option); instance.Check(result, current, pk, history, enc, types, Option);
if (CleanPurge(result, current, pk, types, local, evos)) if (CleanPurge(result, current, pk, types, local, evos, option))
return true; return true;
} }
if (history.HasVisitedPLA && pk is not PA8) if (history.HasVisitedPLA && pk is not PA8)
{ {
var instance = LearnGroup8a.Instance; var instance = LearnGroup8a.Instance;
instance.Check(result, current, pk, history, enc, types, option); instance.Check(result, current, pk, history, enc, types, Option);
if (CleanPurge(result, current, pk, types, local, evos)) if (CleanPurge(result, current, pk, types, local, evos, option))
return true; return true;
} }
if (history.HasVisitedBDSP && pk is not PB8) if (history.HasVisitedBDSP && pk is not PB8)
{ {
var instance = LearnGroup8b.Instance; var instance = LearnGroup8b.Instance;
instance.Check(result, current, pk, history, enc, types, option); instance.Check(result, current, pk, history, enc, types, Option);
if (CleanPurge(result, current, pk, types, local, evos)) if (CleanPurge(result, current, pk, types, local, evos, option))
return true; 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. // 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 (TryAddOriginalMoves(result, current, pk, enc))
{ {
if (CleanPurge(result, current, pk, types, local, evos)) if (CleanPurge(result, current, pk, types, local, evos, option))
return true; return true;
} }
// HOME is silly and allows form exclusive moves to be transferred without ever knowing the move. // HOME is silly and allows form exclusive moves to be transferred without ever knowing the move.
if (TryAddExclusiveMoves(result, current, pk)) if (TryAddExclusiveMoves(result, current, pk))
{ {
if (CleanPurge(result, current, pk, types, local, evos)) if (CleanPurge(result, current, pk, types, local, evos, option))
return true; 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. // PK8 w/ Battle Version can be ignored, as LGP/E has separate HOME data.
var instance = LearnGroup7b.Instance; var instance = LearnGroup7b.Instance;
instance.Check(result, current, pk, history, enc, types, option); instance.Check(result, current, pk, history, enc, types, Option);
if (CleanPurge(result, current, pk, types, local, evos)) if (CleanPurge(result, current, pk, types, local, evos, option))
return true; return true;
} }
else if (history.HasVisitedGen7) else if (history.HasVisitedGen7)
@ -93,8 +94,8 @@ public sealed class LearnGroupHOME : ILearnGroup
ILearnGroup instance = LearnGroup7.Instance; ILearnGroup instance = LearnGroup7.Instance;
while (true) while (true)
{ {
instance.Check(result, current, pk, history, enc, types, option); instance.Check(result, current, pk, history, enc, types, Option);
if (CleanPurge(result, current, pk, types, local, evos)) if (CleanPurge(result, current, pk, types, local, evos, option))
return true; return true;
var prev = instance.GetPrevious(pk, history, enc, option); var prev = instance.GetPrevious(pk, history, enc, option);
if (prev is null) 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 <see cref="local"/> game. /// Scan the results and remove any that are not valid for the game <see cref="local"/> game.
/// </summary> /// </summary>
/// <returns>True if all results are valid.</returns> /// <returns>True if all results are valid.</returns>
private static bool CleanPurge(Span<MoveResult> result, ReadOnlySpan<ushort> current, PKM pk, MoveSourceType types, IHomeSource local, ReadOnlySpan<EvoCriteria> evos) private static bool CleanPurge(Span<MoveResult> result, ReadOnlySpan<ushort> current, PKM pk, MoveSourceType types, IHomeSource local, ReadOnlySpan<EvoCriteria> 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. // 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. // 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. // SW/SH will continue to iterate downwards to previous groups after HOME is checked, so we can exactly check via Environment.

View File

@ -15,6 +15,9 @@ internal static class EvolutionRestrictions
/// <summary> /// <summary>
/// List of species that evolve from a previous species having a move while leveling up /// List of species that evolve from a previous species having a move while leveling up
/// </summary> /// </summary>
/// <remarks>
/// Using moves with a counter stored in form argument is explicitly checked via other logic. <see cref="IsFormArgEvolution"/>
/// </remarks>
private static ushort GetSpeciesEvolutionMove(ushort species) => species switch private static ushort GetSpeciesEvolutionMove(ushort species) => species switch
{ {
(int)Sylveon => EEVEE, (int)Sylveon => EEVEE,
@ -28,15 +31,22 @@ internal static class EvolutionRestrictions
(int)Tsareena => (int)Stomp, (int)Tsareena => (int)Stomp,
(int)Naganadel => (int)DragonPulse, (int)Naganadel => (int)DragonPulse,
(int)Grapploct => (int)Taunt, (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)Wyrdeer => (int)PsyshieldBash,
(int)Overqwil => (int)BarbBarrage, (int)Overqwil => (int)BarbBarrage,
(int)Annihilape => (int)RageFist, (int)Annihilape => (int)RageFist,
(int)Farigiraf => (int)TwinBeam, (int)Farigiraf => (int)TwinBeam,
(int)Dudunsparce => (int)HyperDrill, (int)Dudunsparce => (int)HyperDrill,
(int)Hydrapple => (int)DragonCheer, (int)Hydrapple => (int)DragonCheer,
_ => NONE, _ => NONE,
}; };
/// <summary>
/// Checks if the evolution is the "rare" variant (less common).
/// </summary>
/// <param name="encryptionConstant">Random value used to pivot between evolution results.</param>
public static bool IsEvolvedSpeciesFormRare(uint encryptionConstant) => encryptionConstant % 100 is 0; public static bool IsEvolvedSpeciesFormRare(uint encryptionConstant) => encryptionConstant % 100 is 0;
/// <summary> /// <summary>
@ -50,6 +60,9 @@ internal static class EvolutionRestrictions
_ => throw new ArgumentOutOfRangeException(nameof(species), species, "Incorrect EC%100 species."), _ => throw new ArgumentOutOfRangeException(nameof(species), species, "Incorrect EC%100 species."),
}; };
/// <summary>
/// Checks if the evolution result matches what is expected for <see cref="IsEvolvedSpeciesFormRare"/>
/// </summary>
public static bool GetIsExpectedEvolveFormEC100(ushort species, byte form, bool rare) => species switch public static bool GetIsExpectedEvolveFormEC100(ushort species, byte form, bool rare) => species switch
{ {
(ushort)Maushold => form == (byte)(rare ? 0 : 1), (ushort)Maushold => form == (byte)(rare ? 0 : 1),
@ -76,6 +89,9 @@ public static bool IsFormArgEvolution(ushort species)
/// Checks if the <see cref="pk"/> is correctly evolved, assuming it had a known move requirement evolution in its evolution chain. /// Checks if the <see cref="pk"/> is correctly evolved, assuming it had a known move requirement evolution in its evolution chain.
/// </summary> /// </summary>
/// <returns>True if unnecessary to check or the evolution was valid.</returns> /// <returns>True if unnecessary to check or the evolution was valid.</returns>
/// <remarks>
/// Performs some eager checks to skip doing a full evolution tree move check.
/// </remarks>
public static bool IsValidEvolutionWithMove(PKM pk, LegalInfo info) public static bool IsValidEvolutionWithMove(PKM pk, LegalInfo info)
{ {
// Known-move evolutions were introduced in Gen4. // Known-move evolutions were introduced in Gen4.
@ -88,6 +104,15 @@ public static bool IsValidEvolutionWithMove(PKM pk, LegalInfo info)
if (enc.Species == species) if (enc.Species == species)
return true; 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 // Exclude evolution paths that did not require a move w/level-up evolution
var move = GetSpeciesEvolutionMove(species); var move = GetSpeciesEvolutionMove(species);
if (move is NONE) if (move is NONE)
@ -95,6 +120,10 @@ public static bool IsValidEvolutionWithMove(PKM pk, LegalInfo info)
if (!IsMoveSlotAvailable(info.Moves)) if (!IsMoveSlotAvailable(info.Moves))
return false; 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. // Check the entire chain to see if it could have learnt it at any point.
var head = LearnGroupUtil.GetCurrentGroup(pk); var head = LearnGroupUtil.GetCurrentGroup(pk);
var pruned = info.EvoChainsAllGens.PruneKeepPreEvolutions(species); 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); 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<ushort> 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<ushort> any, EvolutionHistory history, PKM pk, ILearnGroup head) private static bool IsValidEvolutionWithMoveAny(IEncounterTemplate enc, ReadOnlySpan<ushort> any, EvolutionHistory history, PKM pk, ILearnGroup head)
{ {
foreach (var move in any) foreach (var move in any)