From eced6510248cd52a796660e308a1868d3b3a520e Mon Sep 17 00:00:00 2001 From: Kurt Date: Wed, 15 Jan 2025 01:26:49 -0600 Subject: [PATCH] Pokewalker: Handle 539 stroll, revise signatures Was missing the worst case scenario; ran a bruteforce script to find a 539 after adding the final logic. We try to do the least amount of cpu instructions so this is probably the better arrangement anyway. Revise the method signatures to exclude the scratchspace span reuse (not-obvious behavior). Just stackalloc a new span. Add seed indication to legality formatting. #4416 --- .../Gen4/EncounterStatic4Pokewalker.cs | 8 ++-- .../Legality/Formatting/LegalityFormatting.cs | 10 +++++ .../RNG/ClassicEra/Gen4/PokewalkerRNG.cs | 41 ++++++++++++------- Tests/PKHeX.Core.Tests/PKM/PIDIVTests.cs | 8 ++-- 4 files changed, 45 insertions(+), 22 deletions(-) diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4Pokewalker.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4Pokewalker.cs index 68deda2ba..712a89d15 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4Pokewalker.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4Pokewalker.cs @@ -116,7 +116,7 @@ private uint GetIV32(EncounterCriteria criteria) if (criteria.IsSpecifiedIVsAll()) // Don't trust that the requirements are valid { criteria.GetCombinedIVs(out var iv1, out var iv2); - var seed = PokewalkerRNG.GetFirstSeed(iv1, iv2); + var seed = PokewalkerRNG.GetLeastEffortSeed(iv1, iv2); if (seed.Type != PokewalkerSeedType.None) return criteria.GetCombinedIVs(); } @@ -142,11 +142,11 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo) return true; } - private bool IsMatchSeed(PKM pk) + private static bool IsMatchSeed(PKM pk) { Span ivs = stackalloc int[6]; pk.GetIVs(ivs); - var seed = PokewalkerRNG.GetFirstSeed(ivs); + var seed = PokewalkerRNG.GetLeastEffortSeed(ivs); return seed.Type != PokewalkerSeedType.None; } @@ -191,7 +191,7 @@ public EncounterMatchRating GetMatchRating(PKM pk) return EncounterMatchRating.Match; } - private bool IsMatchPartial(PKM pk) => pk.Ball != (byte)Ball.Poke || !IsMatchSeed(pk); + private static bool IsMatchPartial(PKM pk) => pk.Ball != (byte)Ball.Poke || !IsMatchSeed(pk); #endregion public bool IsCompatible(PIDType type, PKM pk) diff --git a/PKHeX.Core/Legality/Formatting/LegalityFormatting.cs b/PKHeX.Core/Legality/Formatting/LegalityFormatting.cs index e743e3744..02527e4be 100644 --- a/PKHeX.Core/Legality/Formatting/LegalityFormatting.cs +++ b/PKHeX.Core/Legality/Formatting/LegalityFormatting.cs @@ -117,7 +117,17 @@ private static void AddEncounterInfoPIDIV(List lines, LegalInfo info) } lines.Add(type); if (pidiv.NoSeed) + { + if (pidiv.Type is PIDType.Pokewalker) + { + var pk = info.Entity; + var result = PokewalkerRNG.GetLeastEffortSeed((uint)pk.IV_HP, (uint)pk.IV_ATK, (uint)pk.IV_DEF, (uint)pk.IV_SPA, (uint)pk.IV_SPD, (uint)pk.IV_SPE); + var line = string.Format(L_FOriginSeed_0, result.Seed.ToString("X8")); + line += $" [{result.Type} @ {result.PriorPoke}]"; + lines.Add(line); + } return; + } if (pidiv.IsSeed64()) { diff --git a/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/PokewalkerRNG.cs b/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/PokewalkerRNG.cs index 37200c00e..9f6d93707 100644 --- a/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/PokewalkerRNG.cs +++ b/PKHeX.Core/Legality/RNG/ClassicEra/Gen4/PokewalkerRNG.cs @@ -99,29 +99,32 @@ public static bool IsSeedFormatNoStroll(uint seed) ]; /// - /// Gets the first valid seed for the given Pokéwalker IVs. + /// Finds an initial seed for the Pokéwalker IVs with the least amount of capture-IVs advances needed. /// - public static PokewalkerSeedResult GetFirstSeed(Span ivs) + /// IVs in order (speed last). + public static PokewalkerSeedResult GetLeastEffortSeed(Span ivs) { var tmp = MemoryMarshal.Cast(ivs); - return GetFirstSeed(tmp, tmp[0], tmp[1], tmp[2], tmp[4], tmp[5], spe: tmp[3]); + return GetLeastEffortSeed(tmp[0], tmp[1], tmp[2], tmp[4], tmp[5], spe: tmp[3]); } - /// - public static PokewalkerSeedResult GetFirstSeed(Span tmpIVs, - uint hp, uint atk, uint def, uint spa, uint spd, uint spe) + /// + public static PokewalkerSeedResult GetLeastEffortSeed(uint hp, uint atk, uint def, uint spa, uint spd, uint spe) { + Span result = stackalloc uint[LCRNG.MaxCountSeedsIV]; uint first = (hp | (atk << 5) | (def << 10)) << 16; uint second = (spe | (spa << 5) | (spd << 10)) << 16; - return GetFirstSeed(tmpIVs, first, second); + return GetLeastEffortSeed(result, first, second); } - /// - public static PokewalkerSeedResult GetFirstSeed(uint first, uint second) - => GetFirstSeed(stackalloc uint[LCRNG.MaxCountSeedsIV], first, second); + /// + /// + public static PokewalkerSeedResult GetLeastEffortSeed(uint first, uint second) + => GetLeastEffortSeed(stackalloc uint[LCRNG.MaxCountSeedsIV], first, second); - /// - public static PokewalkerSeedResult GetFirstSeed(Span result, uint first, uint second) + /// + /// + public static PokewalkerSeedResult GetLeastEffortSeed(Span result, uint first, uint second) { // When generating a set of Pokéwalker Pokémon (and their IVs), the game does the following logic: // If the player begins a stroll, generate an initial seed based on seconds elapsed in the day (< 86400) and 3 slots. @@ -146,15 +149,25 @@ public static PokewalkerSeedResult GetFirstSeed(Span result, uint first, u // Check the [no-stroll] case. if (IsSeedFormatNoStroll(s)) return new(s, priorPoke, PokewalkerSeedType.NoStroll); - s = seed = LCRNG.Prev(seed); + s = LCRNG.Prev(s); // Check the [stroll] case. // Due to this backtracking algorithm, the first time we check won't be a valid (needs 3 advancements) if (priorPoke != 0 && IsSeedFormatStroll(s)) // don't check species; can be disassociated from slots. return new(s, --priorPoke, PokewalkerSeedType.Stroll); // decrement priorPoke back to 0-indexed - seed = LCRNG.Prev(seed); // prep for next loop + seed = LCRNG.Prev(s); // prep for next loop } } + + // The above logic for Stroll checks for [0,n-1) due to the backtracking nature of the algorithm. + // Check the last-empty-slot for the Stroll case. + // That's catching 540 'mons... quite unlikely! But still possible, as not all spreads are obtainable. + foreach (ref var seed in result) + { + var s = LCRNG.Prev(seed); + if (IsSeedFormatStroll(s)) // don't check species; can be disassociated from slots. + return new(s, boxCapacity - 1, PokewalkerSeedType.Stroll); // decrement priorPoke back to 0-indexed + } return default; } diff --git a/Tests/PKHeX.Core.Tests/PKM/PIDIVTests.cs b/Tests/PKHeX.Core.Tests/PKM/PIDIVTests.cs index a06ccbc43..2e0467056 100644 --- a/Tests/PKHeX.Core.Tests/PKM/PIDIVTests.cs +++ b/Tests/PKHeX.Core.Tests/PKM/PIDIVTests.cs @@ -157,11 +157,11 @@ public void PIDIVPokeSpotTest() } [Theory] - [InlineData(30, 31, 31, 14, 31, 31, 0x28070031, 24, (int)Species.Pikachu, PokewalkerCourse4.YellowForest, PokewalkerSeedType.NoStroll)] - public void PokewalkerIVTest(uint hp, uint atk, uint def, uint spA, uint spD, uint spE, uint seed, ushort expect, ushort species, PokewalkerCourse4 course, PokewalkerSeedType type) + [InlineData(30, 31, 31, 14, 31, 31, 0x28070031, 024, PokewalkerSeedType.NoStroll)] + [InlineData(00, 00, 00, 05, 03, 09, 0x00011434, 539, PokewalkerSeedType.Stroll)] + public void PokewalkerIVTest(uint hp, uint atk, uint def, uint spA, uint spD, uint spE, uint seed, ushort expect, PokewalkerSeedType type) { - Span tmp = stackalloc uint[LCRNG.MaxCountSeedsIV]; - var result = PokewalkerRNG.GetFirstSeed(tmp, hp, atk, def, spA, spD, spE); + var result = PokewalkerRNG.GetLeastEffortSeed(hp, atk, def, spA, spD, spE); result.Type.Should().Be(type); result.PriorPoke.Should().Be(expect); result.Seed.Should().Be(seed);