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
{
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;

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)
{
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);
}

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
{
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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<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;
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 <see cref="local"/> game.
/// </summary>
/// <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.
// 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.

View File

@ -15,6 +15,9 @@ internal static class EvolutionRestrictions
/// <summary>
/// List of species that evolve from a previous species having a move while leveling up
/// </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
{
(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,
};
/// <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;
/// <summary>
@ -50,6 +60,9 @@ internal static class EvolutionRestrictions
_ => 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
{
(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.
/// </summary>
/// <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)
{
// 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<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)
{
foreach (var move in any)