Pokewalker: Don't check slots for stroll seeds

Closes #4416
Majority of the changelog here is additional/revised comments/xmldoc or "unused code". Only `GetFirstSeed`'s behavior has changed on line 154 (new). Since there is no longer the need to refer to Species and Course, remove from the method signatures & usages.

Refer to the discussion in the ^ mentioned issue.

Co-Authored-By: HappyLappy1 <86489014+happylappy1@users.noreply.github.com>
Co-Authored-By: NickPlayeZ <80699972+nickplayez@users.noreply.github.com>
This commit is contained in:
Kurt 2025-01-13 23:24:18 -06:00
parent ef60ee622d
commit 9f0812cb8b
3 changed files with 83 additions and 37 deletions

View File

@ -116,7 +116,7 @@ private uint GetIV32(EncounterCriteria criteria)
if (criteria.IsSpecifiedIVsAll()) // Don't trust that the requirements are valid if (criteria.IsSpecifiedIVsAll()) // Don't trust that the requirements are valid
{ {
criteria.GetCombinedIVs(out var iv1, out var iv2); criteria.GetCombinedIVs(out var iv1, out var iv2);
var seed = PokewalkerRNG.GetFirstSeed(Species, Course, iv1, iv2); var seed = PokewalkerRNG.GetFirstSeed(iv1, iv2);
if (seed.Type != PokewalkerSeedType.None) if (seed.Type != PokewalkerSeedType.None)
return criteria.GetCombinedIVs(); return criteria.GetCombinedIVs();
} }
@ -146,7 +146,7 @@ private bool IsMatchSeed(PKM pk)
{ {
Span<int> ivs = stackalloc int[6]; Span<int> ivs = stackalloc int[6];
pk.GetIVs(ivs); pk.GetIVs(ivs);
var seed = PokewalkerRNG.GetFirstSeed(Species, Course, ivs); var seed = PokewalkerRNG.GetFirstSeed(ivs);
return seed.Type != PokewalkerSeedType.None; return seed.Type != PokewalkerSeedType.None;
} }

View File

@ -16,17 +16,43 @@ public static class PokewalkerRNG
private const int maxYears = 100; private const int maxYears = 100;
private const int secondsPerDay = 60 * 60 * 24; private const int secondsPerDay = 60 * 60 * 24;
/// <summary> // seeding for [stroll]: 3600 * hour + 60 * minute + second
/// Get the 32-bit RNG seed for a stroll generation instance. // seeding for [no-stroll]: (((month*day + minute + second) & 0xff) << 24) | (hour << 16) | (year)
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint GetStrollSeed(uint hour, uint minute, uint second) => (3600 * hour) + (60 * minute) + second;
/// <summary> /// <summary>
/// Get the 32-bit RNG seed for a no-stroll generation instance. /// Get the 32-bit RNG seed for a Stroll seeding.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint GetNoStrollSeed(uint year, uint month, uint day, uint hour, uint minute, uint second) => ((((month * day) + minute + second) & 0xff) << 24) | (hour << 16) | year; public static uint GetSeedStroll(uint hour, uint minute, uint second) => (3600 * hour) + (60 * minute) + second;
/// <summary>
/// Get the 32-bit RNG seed for a No-Stroll seeding.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint GetSeedNoStroll(uint year, uint month, uint day, uint hour, uint minute, uint second) => ((((month * day) + minute + second) & 0xff) << 24) | (hour << 16) | year;
/// <summary>
/// Check if the seed is from a Stroll seeding.
/// </summary>
public static bool IsSeedFormatStroll(uint seed)
{
// XXXS_SSSS
return seed < secondsPerDay;
}
/// <summary>
/// Check if the seed is from a No-Stroll seeding.
/// </summary>
public static bool IsSeedFormatNoStroll(uint seed)
{
// the top byte of no-stroll can be any value, so we can skip checking that byte.
// XX_HH_YYYY
if ((ushort)seed >= maxYears)
return false;
if ((byte)(seed >> 16) >= maxHours)
return false;
return true;
}
/// <summary> Species slots per course. </summary> /// <summary> Species slots per course. </summary>
public const int SlotsPerCourse = 6; public const int SlotsPerCourse = 6;
@ -36,7 +62,11 @@ public static class PokewalkerRNG
/// <summary> /// <summary>
/// All species for all Pokéwalker courses. /// All species for all Pokéwalker courses.
/// </summary> /// </summary>
/// <remarks>6 species per course; each course has 3 groups (A/B/C) of 2 species (0/1).</remarks> /// <remarks>
/// 6 species per course; each course has 3 groups (A/B/C) of 2 species (0/1).
/// Data is ripped from Overlay 112's route data, distilled down to just a list of species.
/// When selecting a slot, the game uses the result of the rand() & 1 == 0, so invert the index.
/// </remarks>
private static ReadOnlySpan<ushort> CourseSpecies => private static ReadOnlySpan<ushort> CourseSpecies =>
[ [
115, 084, 029, 032, 016, 161, // 00 Refreshing Field 115, 084, 029, 032, 016, 161, // 00 Refreshing Field
@ -71,31 +101,30 @@ public static class PokewalkerRNG
/// <summary> /// <summary>
/// Gets the first valid seed for the given Pokéwalker IVs. /// Gets the first valid seed for the given Pokéwalker IVs.
/// </summary> /// </summary>
public static PokewalkerSeedResult GetFirstSeed(ushort species, PokewalkerCourse4 course, Span<int> ivs) public static PokewalkerSeedResult GetFirstSeed(Span<int> ivs)
{ {
var tmp = MemoryMarshal.Cast<int, uint>(ivs); var tmp = MemoryMarshal.Cast<int, uint>(ivs);
return GetFirstSeed(species, course, tmp, tmp[0], tmp[1], tmp[2], tmp[4], tmp[5], spe: tmp[3]); return GetFirstSeed(tmp, tmp[0], tmp[1], tmp[2], tmp[4], tmp[5], spe: tmp[3]);
} }
/// <inheritdoc cref="GetFirstSeed(ushort, PokewalkerCourse4, Span{int})"/> /// <inheritdoc cref="GetFirstSeed(Span{int})"/>
public static PokewalkerSeedResult GetFirstSeed(ushort species, PokewalkerCourse4 course, public static PokewalkerSeedResult GetFirstSeed(Span<uint> tmpIVs,
Span<uint> tmpIVs, uint hp, uint atk, uint def, uint spa, uint spd, uint spe) uint hp, uint atk, uint def, uint spa, uint spd, uint spe)
{ {
uint first = (hp | (atk << 5) | (def << 10)) << 16; uint first = (hp | (atk << 5) | (def << 10)) << 16;
uint second = (spe | (spa << 5) | (spd << 10)) << 16; uint second = (spe | (spa << 5) | (spd << 10)) << 16;
return GetFirstSeed(species, course, tmpIVs, first, second); return GetFirstSeed(tmpIVs, first, second);
} }
/// <inheritdoc cref="GetFirstSeed(ushort, PokewalkerCourse4, Span{int})"/> /// <inheritdoc cref="GetFirstSeed(Span{int})"/>
public static PokewalkerSeedResult GetFirstSeed(ushort species, PokewalkerCourse4 course, public static PokewalkerSeedResult GetFirstSeed(uint first, uint second)
uint first, uint second) => GetFirstSeed(species, course, stackalloc uint[LCRNG.MaxCountSeedsIV], first, second); => GetFirstSeed(stackalloc uint[LCRNG.MaxCountSeedsIV], first, second);
/// <inheritdoc cref="GetFirstSeed(ushort, PokewalkerCourse4, Span{int})"/> /// <inheritdoc cref="GetFirstSeed(Span{int})"/>
public static PokewalkerSeedResult GetFirstSeed(ushort species, PokewalkerCourse4 course, public static PokewalkerSeedResult GetFirstSeed(Span<uint> result, uint first, uint second)
Span<uint> result, uint first, uint second)
{ {
// When generating a set of Pokéwalker Pokémon (and their IVs), the game does the following logic: // When generating a set of Pokéwalker Pokémon (and their IVs), the game does the following logic:
// If the player does not begin a stroll, generate an initial seed based on seconds elapsed in the day (< 86400). // If the player begins a stroll, generate an initial seed based on seconds elapsed in the day (< 86400) and 3 slots.
// Otherwise, generate an initial seed based on the elapsed time and date (similar to Gen4 initial seeding). // Otherwise, generate an initial seed based on the elapsed time and date (similar to Gen4 initial seeding).
// If the player begins a stroll, the game generates a set of 3 Pokémon to see, with results untraceable to the correlation. // If the player begins a stroll, the game generates a set of 3 Pokémon to see, with results untraceable to the correlation.
@ -103,10 +132,6 @@ public static PokewalkerSeedResult GetFirstSeed(ushort species, PokewalkerCourse
// Since stroll causes 3 RNG advancements, an initial seed [stroll] can be advanced 3+(2*n) times, or [no-stroll] advanced 0+(2*n) times. // Since stroll causes 3 RNG advancements, an initial seed [stroll] can be advanced 3+(2*n) times, or [no-stroll] advanced 0+(2*n) times.
// To determine the first valid initial seed, take advantage of the even-odd nature of the RNG frames (different initial seeding algorithm). // To determine the first valid initial seed, take advantage of the even-odd nature of the RNG frames (different initial seeding algorithm).
// seeding for [stroll]: 3600 * hour + 60 * minute + second
// seeding for [no-stroll]: (((month*day + minute + second) & 0xff) << 24) | (hour << 16) | (year)
// the top byte of no-stroll can be any value, so we can skip checking that byte.
int ctr = LCRNGReversal.GetSeedsIVs(result, first, second); int ctr = LCRNGReversal.GetSeedsIVs(result, first, second);
if (ctr == 0) if (ctr == 0)
return default; return default;
@ -116,16 +141,17 @@ public static PokewalkerSeedResult GetFirstSeed(ushort species, PokewalkerCourse
{ {
foreach (ref var seed in result) foreach (ref var seed in result)
{ {
var s = seed; // already unrolled once var s = seed; // first loop is already unrolled once (immediately generates IVs)
// Check the [no-stroll] case. // Check the [no-stroll] case.
if ((byte)(s >> 16) < maxHours && (ushort)s < maxYears) if (IsSeedFormatNoStroll(s))
return new(s, priorPoke, PokewalkerSeedType.NoStroll); return new(s, priorPoke, PokewalkerSeedType.NoStroll);
s = seed = LCRNG.Prev(seed); s = seed = LCRNG.Prev(seed);
// Check the [stroll] case. // Check the [stroll] case.
if (priorPoke != 0 && s < secondsPerDay && IsValidStrollSeed(s, species, course)) // seed can't be hit due to the 3 advances from stroll // Due to this backtracking algorithm, the first time we check won't be a valid (needs 3 advancements)
return new(s, priorPoke, PokewalkerSeedType.Stroll); 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(seed); // prep for next loop
} }
} }
@ -139,26 +165,46 @@ public static PokewalkerSeedResult GetFirstSeed(ushort species, PokewalkerCourse
/// <param name="species">Species expected to be encountered.</param> /// <param name="species">Species expected to be encountered.</param>
/// <param name="course">Course the Stroll is taking place on.</param> /// <param name="course">Course the Stroll is taking place on.</param>
/// <returns>True if the seed is valid, false otherwise.</returns> /// <returns>True if the seed is valid, false otherwise.</returns>
public static bool IsValidStrollSeed(uint seed, ushort species, PokewalkerCourse4 course) /// <remarks>
/// By immediately cancelling a Stroll, the next frames are not used to generate IVs, which makes these results irrelevant for checking IVs->Slot.
/// </remarks>
public static bool IsValidSeedStrollSlots(uint seed, ushort species, PokewalkerCourse4 course)
{ {
// initial seed // initial seed
// rand() & 1 => slot A // rand() & 1 => slot A
// rand() & 1 => slot B // rand() & 1 => slot B
// rand() & 1 => slot C // rand() & 1 => slot C
// generate IVs // To pick the actual index, it is the result of the rand() & 1 == 0, so invert the index.
var span = GetSpecies(course); var span = GetSpecies(course);
var slotA = (int)(LCRNG.Next16(ref seed) & 1); var slotA = (int)(LCRNG.Next16(ref seed) & 1) ^ 1;
if (span[slotA] == species) if (span[slotA] == species)
return true; return true;
var slotB = (int)(LCRNG.Next16(ref seed) & 1); var slotB = (int)(LCRNG.Next16(ref seed) & 1) ^ 1;
if (span[slotB + 2] == species) if (span[slotB + 2] == species)
return true; return true;
var slotC = (int)(LCRNG.Next16(ref seed) & 1); var slotC = (int)(LCRNG.Next16(ref seed) & 1) ^ 1;
if (span[slotC + 4] == species) if (span[slotC + 4] == species)
return true; return true;
return false; return false;
} }
/// <summary>
/// Gets the slot indexes for referencing the overlay data.
/// </summary>
public static (int A, int B, int C) GetSlotsStroll(ref uint seed)
{
// initial seed
// rand() & 1 => slot A
// rand() & 1 => slot B
// rand() & 1 => slot C
// To pick the actual index, it is the result of the rand() & 1 == 0, so invert the index.
var slotA = (int)(LCRNG.Next16(ref seed) & 1) ^ 1;
var slotB = (int)(LCRNG.Next16(ref seed) & 1) ^ 1;
var slotC = (int)(LCRNG.Next16(ref seed) & 1) ^ 1;
return (slotA, slotB, slotC);
}
/// <summary> /// <summary>
/// Gets all 6 species for the given course. /// Gets all 6 species for the given course.
/// </summary> /// </summary>

View File

@ -161,7 +161,7 @@ public void PIDIVPokeSpotTest()
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) 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)
{ {
Span<uint> tmp = stackalloc uint[LCRNG.MaxCountSeedsIV]; Span<uint> tmp = stackalloc uint[LCRNG.MaxCountSeedsIV];
var result = PokewalkerRNG.GetFirstSeed(species, course, tmp, hp, atk, def, spA, spD, spE); var result = PokewalkerRNG.GetFirstSeed(tmp, hp, atk, def, spA, spD, spE);
result.Type.Should().Be(type); result.Type.Should().Be(type);
result.PriorPoke.Should().Be(expect); result.PriorPoke.Should().Be(expect);
result.Seed.Should().Be(seed); result.Seed.Should().Be(seed);