From abcaaa44cd54092e4a0aec5fb7c8bb7b73e9e7ce Mon Sep 17 00:00:00 2001 From: Kurt Date: Sat, 7 Oct 2023 23:14:34 -0700 Subject: [PATCH] Extract pokewalker logic from template ctor Actually searching (instead of brute-forcing) for a spread will forever be haunting. Add to Legality Check matching with vague partial match --- .../Encounters/Generator/EncounterCriteria.cs | 19 ++ .../Information/EncounterSummary.cs | 2 +- .../Templates/Gen4/EncounterStatic4.cs | 3 +- .../Gen4/EncounterStatic4Pokewalker.cs | 40 ++- PKHeX.Core/Legality/RNG/Frame/Frame.cs | 2 +- PKHeX.Core/Legality/RNG/MethodFinder.cs | 131 +------- .../Legality/RNG/Methods/PokewalkerRNG.cs | 290 ++++++++++++++++++ .../Legality/RNG/{ => Methods}/RaidRNG.cs | 0 PKHeX.Core/Legality/RNG/PIDGenerator.cs | 49 +-- .../Legality/Restrictions/WordFilter.cs | 5 +- Tests/PKHeX.Core.Tests/PKM/PIDIVTests.cs | 12 +- 11 files changed, 358 insertions(+), 195 deletions(-) create mode 100644 PKHeX.Core/Legality/RNG/Methods/PokewalkerRNG.cs rename PKHeX.Core/Legality/RNG/{ => Methods}/RaidRNG.cs (100%) diff --git a/PKHeX.Core/Legality/Encounters/Generator/EncounterCriteria.cs b/PKHeX.Core/Legality/Encounters/Generator/EncounterCriteria.cs index 8b2c47daa..a28b2ec08 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/EncounterCriteria.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/EncounterCriteria.cs @@ -299,4 +299,23 @@ static int Get(sbyte template, int request) return Util.Rand.Next(32); } } + + public bool IsCompatibleIVs(ReadOnlySpan ivs) + { + if (ivs.Length != 6) + return false; + if (IV_HP != RandomIV && IV_HP != ivs[0]) + return false; + if (IV_ATK != RandomIV && IV_ATK != ivs[1]) + return false; + if (IV_DEF != RandomIV && IV_DEF != ivs[2]) + return false; + if (IV_SPE != RandomIV && IV_SPE != ivs[3]) + return false; + if (IV_SPA != RandomIV && IV_SPA != ivs[4]) + return false; + if (IV_SPD != RandomIV && IV_SPD != ivs[5]) + return false; + return true; + } } diff --git a/PKHeX.Core/Legality/Encounters/Information/EncounterSummary.cs b/PKHeX.Core/Legality/Encounters/Information/EncounterSummary.cs index 01c77a246..5f8b9ae82 100644 --- a/PKHeX.Core/Legality/Encounters/Information/EncounterSummary.cs +++ b/PKHeX.Core/Legality/Encounters/Information/EncounterSummary.cs @@ -6,7 +6,7 @@ namespace PKHeX.Core; /// /// Provides a summary for an object. /// -public record EncounterSummary +public sealed record EncounterSummary { private readonly GameVersion Version; private readonly string LocationName; diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4.cs index e70f455b6..5b2f8bcdc 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4.cs @@ -103,7 +103,8 @@ private void SetPINGA(PK4 pk, EncounterCriteria criteria, PersonalInfo4 pi) // Pichu is special -- use Pokewalker method if (Species == (int)Core.Species.Pichu) { - PIDGenerator.SetRandomPIDPokewalker(pk, (byte)Nature, Gender); + var pid = pk.PID = PokewalkerRNG.GetPID(pk.TID16, pk.SID16, (uint)Nature, pk.Gender = Gender, pi.Gender); + pk.RefreshAbility((int)(pid & 1)); criteria.SetRandomIVs(pk); return; } diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4Pokewalker.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4Pokewalker.cs index 485374a73..555aa0c4d 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4Pokewalker.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4Pokewalker.cs @@ -46,16 +46,17 @@ private EncounterStatic4Pokewalker(ReadOnlySpan data, PokewalkerCourse4 co public static EncounterStatic4Pokewalker[] GetAll(ReadOnlySpan data) { - const int size = 0xC; - var count = data.Length / size; - System.Diagnostics.Debug.Assert(count == 6 * (int)PokewalkerCourse4.MAX_COUNT); - var result = new EncounterStatic4Pokewalker[count]; - for (int i = 0; i < result.Length; i++) + const int SlotSize = 0xC; + const int SlotsPerCourse = PokewalkerRNG.SlotsPerCourse; + const int SlotCount = SlotsPerCourse * (int)PokewalkerCourse4.MAX_COUNT; + System.Diagnostics.Debug.Assert(data.Length == SlotCount * SlotSize); + + PokewalkerCourse4 course = 0; + var result = new EncounterStatic4Pokewalker[SlotCount]; + for (int i = 0, offset = 0; i < result.Length; course++) { - var offset = i * size; - var slice = data.Slice(offset, size); - var course = (PokewalkerCourse4)(i / 6); - result[i] = new(slice, course); + for (int s = 0; s < SlotsPerCourse; s++, offset += SlotSize) + result[i++] = new(data.Slice(offset, SlotSize), course); } return result; } @@ -102,13 +103,14 @@ public PK4 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) private void SetPINGA(PK4 pk, EncounterCriteria criteria, PersonalInfo4 pi) { int gender = criteria.GetGender(Gender, pi); - int nature = (int)criteria.GetNature(); + var nature = (uint)criteria.GetNature(); + var pid = pk.PID = PokewalkerRNG.GetPID(pk.TID16, pk.SID16, nature, pk.Gender = gender, pi.Gender); // Cannot force an ability; nature-gender-trainerID only yield fixed PIDs. - // int ability = criteria.GetAbilityFromNumber(Ability, pi); - - PIDGenerator.SetRandomPIDPokewalker(pk, nature, gender); - criteria.SetRandomIVs(pk); + pk.RefreshAbility((int)(pid & 1)); + Span ivs = stackalloc int[6]; + PokewalkerRNG.SetRandomIVs(ivs, criteria); + pk.SetIVs(ivs); } #endregion @@ -129,6 +131,14 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo) return true; } + private bool IsMatchSeed(PKM pk) + { + Span ivs = stackalloc int[6]; + pk.GetIVs(ivs); + var seed = PokewalkerRNG.GetFirstSeed(Species, Course, ivs); + return seed.Type != PokewalkerSeedType.None; + } + private bool IsMatchGender(PKM pk) { if (pk.Gender == Gender) @@ -170,7 +180,7 @@ public EncounterMatchRating GetMatchRating(PKM pk) return EncounterMatchRating.Match; } - private static bool IsMatchPartial(PKM pk) => pk.Ball != (byte)Ball.Poke; + private bool IsMatchPartial(PKM pk) => pk.Ball != (byte)Ball.Poke || !IsMatchSeed(pk); #endregion public bool IsCompatible(PIDType val, PKM pk) diff --git a/PKHeX.Core/Legality/RNG/Frame/Frame.cs b/PKHeX.Core/Legality/RNG/Frame/Frame.cs index d4d9f6bc1..ef8f783c6 100644 --- a/PKHeX.Core/Legality/RNG/Frame/Frame.cs +++ b/PKHeX.Core/Legality/RNG/Frame/Frame.cs @@ -9,7 +9,7 @@ namespace PKHeX.Core; /// /// [DebuggerDisplay($"{{{nameof(FrameType)},nq}}[{{{nameof(Lead)},nq}}]")] -public record Frame(uint Seed, FrameType FrameType, LeadRequired Lead) +public sealed record Frame(uint Seed, FrameType FrameType, LeadRequired Lead) { /// /// Starting seed for the frame (to generate the frame). diff --git a/PKHeX.Core/Legality/RNG/MethodFinder.cs b/PKHeX.Core/Legality/RNG/MethodFinder.cs index 1bcf648e3..1cfec072a 100644 --- a/PKHeX.Core/Legality/RNG/MethodFinder.cs +++ b/PKHeX.Core/Legality/RNG/MethodFinder.cs @@ -511,17 +511,17 @@ private static bool GetBACDMatch(Span seeds, PKM pk, uint pid, ReadOnlySpa return GetNonMatch(out pidiv); } - private static bool GetPokewalkerMatch(PKM pk, uint oldpid, out PIDIV pidiv) + private static bool GetPokewalkerMatch(PKM pk, uint pid, out PIDIV pidiv) { // check surface compatibility // Bits 8-24 must all be zero or all be one. const uint midMask = 0x00FFFF00; - var mid = oldpid & midMask; + var mid = pid & midMask; if (mid is not (0 or midMask)) return GetNonMatch(out pidiv); // Quirky Nature is not possible with the algorithm. - var nature = oldpid % 25; + var nature = pid % 25; if (nature == 24) return GetNonMatch(out pidiv); @@ -531,10 +531,10 @@ private static bool GetPokewalkerMatch(PKM pk, uint oldpid, out PIDIV pidiv) var gr = pk.PersonalInfo.Gender; if (pk.Species == (int)Species.Froslass) gr = 0x7F; // Snorunt - uint pid = PIDGenerator.GetPokeWalkerPID(pk.TID16, pk.SID16, nature, gender, gr); - if (pid != oldpid) + var expect = PokewalkerRNG.GetPID(pk.TID16, pk.SID16, nature, gender, gr); + if (expect != pid) { - if (!(gender == 0 && IsAzurillEdgeCaseM(pk, nature, oldpid))) + if (!(gender == 0 && IsAzurillEdgeCaseM(pk, nature, pid))) return GetNonMatch(out pidiv); } pidiv = PIDIV.Pokewalker; @@ -554,7 +554,7 @@ private static bool IsAzurillEdgeCaseM(PKM pk, uint nature, uint oldpid) if (gender != 1) return false; - var pid = PIDGenerator.GetPokeWalkerPID(pk.TID16, pk.SID16, nature, 1, AzurillGenderRatio); + var pid = PokewalkerRNG.GetPID(pk.TID16, pk.SID16, nature, 1, AzurillGenderRatio); return pid == oldpid; } @@ -807,120 +807,3 @@ public static PIDIV GetPokeSpotSeedFirst(PKM pk, byte slot) return default; } } - -public static class MethodFinderPokewalker -{ - public static (uint Seed, int Prior) GetSeedsPokewalkerIVs(ushort species, PokewalkerCourse4 course, - Span tmpIVs, uint hp, uint atk, uint def, uint spa, uint spd, uint spe) - { - uint first = (hp | (atk << 5) | (def << 10)) << 16; - uint second = (spe | (spa << 5) | (spd << 10)) << 16; - return GetSeedsPokewalkerIVs(species, course, tmpIVs, first, second); - } - - public const int NoPokwalkerMatch = -1; - - public static (uint Seed, int Prior) GetSeedsPokewalkerIVs(ushort species, PokewalkerCourse4 course, - 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 does not begin a stroll, generate an initial seed based on seconds elapsed in the day (< 86400). - // 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. - // Then, the game generates each Pokémon's IVs by calling rand() twice. - // 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). - - // 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); - if (ctr == 0) - return (0, NoPokwalkerMatch); - - result = result[..ctr]; - - const int boxCount = 18; - const int boxSize = 30; - const int boxCapacity = boxCount * boxSize; - const int maxHours = 24; - const int maxYears = 100; - const int secondsPerDay = 60 * 60 * 24; - for (int priorPoke = 0; priorPoke < boxCapacity; priorPoke++) - { - foreach (ref var seed in result) - { - var s = seed; // already unrolled once - - // Check the [no-stroll] case. - if ((byte)(s >> 16) < maxHours && (ushort)s < maxYears) - return (s, priorPoke); - s = seed = LCRNG.Prev(seed); - - // 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 - return (s, priorPoke); - seed = LCRNG.Prev(seed); // prep for next loop - } - } - return (0, NoPokwalkerMatch); - } - - private const int SlotsPerCourse = 6; - - public static bool IsValidStrollSeed(uint seed, ushort species, PokewalkerCourse4 course) - { - // initial seed - // rand() & 1 => slot A - // rand() & 1 => slot B - // rand() & 1 => slot C - // generate IVs - var span = CourseSpecies.Slice((int)course * SlotsPerCourse, SlotsPerCourse); - seed = LCRNG.Next(seed); - var slotA = (seed >> 16) & 1; - if (span[(int)slotA] == species) - return true; - seed = LCRNG.Next(seed); - var slotB = (seed >> 16) & 1; - if (span[(int)slotB + 2] == species) - return true; - seed = LCRNG.Next(seed); - var slotC = (seed >> 16) & 1; - if (span[(int)slotC + 4] == species) - return true; - return false; - } - - private static ReadOnlySpan CourseSpecies => new ushort[] - { - 115, 084, 029, 032, 016, 161, // 00 Refreshing Field - 202, 069, 048, 046, 043, 021, // 01 Noisy Forest - 240, 095, 066, 077, 163, 074, // 02 Rugged Road - 054, 120, 079, 060, 191, 194, // 03 Beautiful Beach - 239, 081, 081, 198, 163, 019, // 04 Suburban Area - 238, 092, 092, 095, 041, 066, // 05 Dim Cave - 147, 060, 098, 090, 118, 072, // 06 Blue Lake - 063, 100, 109, 088, 019, 162, // 07 Town Outskirts - 300, 264, 314, 313, 263, 265, // 08 Hoenn Field - 320, 298, 116, 318, 118, 129, // 09 Warm Beach - 218, 307, 228, 111, 077, 074, // 10 Volcano Path - 352, 351, 203, 234, 044, 070, // 11 Treehouse - 105, 128, 042, 177, 066, 092, // 12 Scary Cave - 439, 415, 403, 406, 399, 401, // 13 Sinnoh Field - 459, 361, 215, 436, 220, 179, // 14 Icy Mountain Road - 357, 438, 114, 400, 179, 102, // 15 Big Forest - 433, 200, 093, 418, 223, 170, // 16 White Lake - 456, 422, 129, 086, 054, 090, // 17 Stormy Beach - 417, 025, 039, 035, 183, 187, // 18 Resort - 442, 446, 433, 349, 164, 042, // 19 Quiet Cave - 120, 224, 116, 222, 223, 170, // 20 Beyond The Sea - 035, 039, 041, 163, 074, 095, // 21 Night Sky's Edge - 025, 025, 025, 025, 025, 025, // 22 Yellow Forest - 441, 302, 025, 453, 427, 417, // 23 Rally - 255, 133, 279, 061, 052, 025, // 24 Sightseeing - 446, 374, 116, 355, 129, 436, // 25 Winners Path - 239, 240, 238, 440, 174, 173, // 26 Amity Meadow - }; -} diff --git a/PKHeX.Core/Legality/RNG/Methods/PokewalkerRNG.cs b/PKHeX.Core/Legality/RNG/Methods/PokewalkerRNG.cs new file mode 100644 index 000000000..af5212135 --- /dev/null +++ b/PKHeX.Core/Legality/RNG/Methods/PokewalkerRNG.cs @@ -0,0 +1,290 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace PKHeX.Core; + +/// +/// Logic for Pokéwalker RNG. +/// +public static class PokewalkerRNG +{ + private const int boxCount = 18; + private const int boxSize = 30; + private const int boxCapacity = boxCount * boxSize; + private const int maxHours = 24; + private const int maxYears = 100; + private const int secondsPerDay = 60 * 60 * 24; + + /// + /// Get the 32-bit RNG seed for a stroll generation instance. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetStrollSeed(uint hour, uint minute, uint second) => (3600 * hour) + (60 * minute) + second; + + /// + /// Get the 32-bit RNG seed for a no-stroll generation instance. + /// + [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; + + /// Species slots per course. + public const int SlotsPerCourse = 6; + + /// + /// All species for all Pokéwalker courses. + /// + /// 6 species per course; each course has 3 groups (A/B/C) of 2 species (0/1). + private static ReadOnlySpan CourseSpecies => new ushort[] + { + 115, 084, 029, 032, 016, 161, // 00 Refreshing Field + 202, 069, 048, 046, 043, 021, // 01 Noisy Forest + 240, 095, 066, 077, 163, 074, // 02 Rugged Road + 054, 120, 079, 060, 191, 194, // 03 Beautiful Beach + 239, 081, 081, 198, 163, 019, // 04 Suburban Area + 238, 092, 092, 095, 041, 066, // 05 Dim Cave + 147, 060, 098, 090, 118, 072, // 06 Blue Lake + 063, 100, 109, 088, 019, 162, // 07 Town Outskirts + 300, 264, 314, 313, 263, 265, // 08 Hoenn Field + 320, 298, 116, 318, 118, 129, // 09 Warm Beach + 218, 307, 228, 111, 077, 074, // 10 Volcano Path + 352, 351, 203, 234, 044, 070, // 11 Treehouse + 105, 128, 042, 177, 066, 092, // 12 Scary Cave + 439, 415, 403, 406, 399, 401, // 13 Sinnoh Field + 459, 361, 215, 436, 220, 179, // 14 Icy Mountain Road + 357, 438, 114, 400, 179, 102, // 15 Big Forest + 433, 200, 093, 418, 223, 170, // 16 White Lake + 456, 422, 129, 086, 054, 090, // 17 Stormy Beach + 417, 025, 039, 035, 183, 187, // 18 Resort + 442, 446, 433, 349, 164, 042, // 19 Quiet Cave + 120, 224, 116, 222, 223, 170, // 20 Beyond The Sea + 035, 039, 041, 163, 074, 095, // 21 Night Sky's Edge + 025, 025, 025, 025, 025, 025, // 22 Yellow Forest + 441, 302, 025, 453, 427, 417, // 23 Rally + 255, 133, 279, 061, 052, 025, // 24 Sightseeing + 446, 374, 116, 355, 129, 436, // 25 Winners Path + 239, 240, 238, 440, 174, 173, // 26 Amity Meadow + }; + + /// + /// Gets the first valid seed for the given Pokéwalker IVs. + /// + public static PokewalkerSeedResult GetFirstSeed(ushort species, PokewalkerCourse4 course, Span ivs) + { + var tmp = MemoryMarshal.Cast(ivs); + return GetFirstSeed(species, course, tmp, tmp[0], tmp[1], tmp[2], tmp[4], tmp[5], spe: tmp[3]); + } + + /// + public static PokewalkerSeedResult GetFirstSeed(ushort species, PokewalkerCourse4 course, + Span tmpIVs, uint hp, uint atk, uint def, uint spa, uint spd, uint spe) + { + uint first = (hp | (atk << 5) | (def << 10)) << 16; + uint second = (spe | (spa << 5) | (spd << 10)) << 16; + return GetFirstSeed(species, course, tmpIVs, first, second); + } + + /// + public static PokewalkerSeedResult GetFirstSeed(ushort species, PokewalkerCourse4 course, + 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 does not begin a stroll, generate an initial seed based on seconds elapsed in the day (< 86400). + // 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. + // Then, the game generates each Pokémon's IVs by calling rand() twice. + // 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). + + // 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); + if (ctr == 0) + return default; + + result = result[..ctr]; + for (ushort priorPoke = 0; priorPoke < boxCapacity; priorPoke++) + { + foreach (ref var seed in result) + { + var s = seed; // already unrolled once + + // Check the [no-stroll] case. + if ((byte)(s >> 16) < maxHours && (ushort)s < maxYears) + return new(s, priorPoke, PokewalkerSeedType.NoStroll); + s = seed = LCRNG.Prev(seed); + + // 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 + return new(s, priorPoke, PokewalkerSeedType.Stroll); + seed = LCRNG.Prev(seed); // prep for next loop + } + } + return default; + } + + /// + /// Indicates if the given seed is a valid Stroll seed for the given species on the given course. + /// + /// Seed generated for a Pokéwalker Stroll. + /// Species expected to be encountered. + /// Course the Stroll is taking place on. + /// True if the seed is valid, false otherwise. + public static bool IsValidStrollSeed(uint seed, ushort species, PokewalkerCourse4 course) + { + // initial seed + // rand() & 1 => slot A + // rand() & 1 => slot B + // rand() & 1 => slot C + // generate IVs + var span = GetSpecies(course); + seed = LCRNG.Next(seed); + var slotA = (seed >> 16) & 1; + if (span[(int)slotA] == species) + return true; + seed = LCRNG.Next(seed); + var slotB = (seed >> 16) & 1; + if (span[(int)slotB + 2] == species) + return true; + seed = LCRNG.Next(seed); + var slotC = (seed >> 16) & 1; + if (span[(int)slotC + 4] == species) + return true; + return false; + } + + /// + /// Gets all 6 species for the given course. + /// + /// Course to get species for. + /// Span of all 6 species. + public static ReadOnlySpan GetSpecies(PokewalkerCourse4 course) => CourseSpecies.Slice((int)course * SlotsPerCourse, SlotsPerCourse); + + /// + /// Gets the species for the given course, group, and rand bit. + /// + /// Course to get species for. + /// Group to get species for (A/B/C). + /// Rand bit to get species for (0/1). + /// Species for the given course, group, and rand bit. + /// + public static ushort GetSpecies(PokewalkerCourse4 course, int group, int rare) + { + if ((uint)group > 2) + throw new ArgumentOutOfRangeException(nameof(group)); + if ((uint)rare > 1) + throw new ArgumentOutOfRangeException(nameof(rare)); + var span = GetSpecies(course); + return span[(group * 2) + rare]; + } + + /// + /// Calculates a Pokewalker PID based on the given parameters. + /// + /// 16-bit Trainer ID. + /// 16-bit Secret ID. + /// Nature to set PID to. + /// Gender to set PID to. + /// Gender ratio of the species. + /// PID for the given parameters. + public static uint GetPID(ushort TID16, ushort SID16, uint nature, int gender, byte genderRatio) + { + if (nature >= 24) + nature = 0; + uint pid = ((((uint)TID16 ^ SID16) >> 8) ^ 0xFF) << 24; // the most significant byte of the PID is chosen so the Pokémon can never be shiny. + // Ensure nature is set to required nature without affecting shininess + pid += nature - (pid % 25); + + if (genderRatio is 0 or >= 0xFE) // non-dual gender + return pid; + + // Ensure Gender is set to required gender without affecting other properties + // If Gender is modified, modify the ability if appropriate + + // either m/f + var pidGender = (pid & 0xFF) < genderRatio ? 1 : 0; + if (gender == pidGender) + return pid; + + if (gender == 0) // Male + { + pid += (((genderRatio - (pid & 0xFF)) / 25) + 1) * 25; + if ((nature & 1) != (pid & 1)) + pid += 25; + } + else + { + pid -= ((((pid & 0xFF) - genderRatio) / 25) + 1) * 25; + if ((nature & 1) != (pid & 1)) + pid -= 25; + } + return pid; + } + + /// + /// Sets the IVs to a valid Pokewalker IV spread. + /// + /// IVs to set. + /// Criteria to set IVs with. + public static bool SetRandomIVs(Span ivs, EncounterCriteria criteria) + { + // Try to find a seed that works for the given criteria. + // Don't waste too much time iterating, try around 100k. + // 256 * 24 * 2 * 10 = 122,880 + for (uint year = 0; year < 2; year++) + { + uint seed = year; + for (uint hour = 0; hour < maxHours; hour++) + { + for (uint i = 0; i <= 0xFF; i++) + { + var iterSeed = seed; + for (int p = 0; p < 10; p++) + { + if (TryApply(ref iterSeed, ivs, criteria)) + return true; + } + seed += 0x01_000000; + } + seed += 0x01_0000; + } + } + + var randByte = (uint)Util.Rand.Next(256) << 24; + TryApply(ref randByte, ivs, EncounterCriteria.Unrestricted); + return false; + } + + private static bool TryApply(ref uint seed, Span ivs, EncounterCriteria criteria) + { + // Act like a Non-Stroll encounter, generate IV rand() results immediately. + uint iv1 = LCRNG.Next(seed); + uint iv2 = seed = LCRNG.Next(iv1); + MethodFinder.GetIVsInt32(ivs, iv1 >> 16, iv2 >> 16); + return criteria.IsCompatibleIVs(ivs); + } +} + +/// +/// Wrapper for Pokewalker Seed Results +/// +/// 32-bit seed +/// Count of Pokémon generated prior to the checked Pokémon +/// Type of seed +public readonly record struct PokewalkerSeedResult(uint Seed, ushort PriorPoke, PokewalkerSeedType Type); + +/// +/// Type of Pokewalker Seed +/// +public enum PokewalkerSeedType : byte +{ + /// Invalid + None = 0, + /// Stroll Seed + Stroll = 1, + /// No Stroll Seed + NoStroll = 2, +} diff --git a/PKHeX.Core/Legality/RNG/RaidRNG.cs b/PKHeX.Core/Legality/RNG/Methods/RaidRNG.cs similarity index 100% rename from PKHeX.Core/Legality/RNG/RaidRNG.cs rename to PKHeX.Core/Legality/RNG/Methods/RaidRNG.cs diff --git a/PKHeX.Core/Legality/RNG/PIDGenerator.cs b/PKHeX.Core/Legality/RNG/PIDGenerator.cs index fbf3dffa1..074bbda35 100644 --- a/PKHeX.Core/Legality/RNG/PIDGenerator.cs +++ b/PKHeX.Core/Legality/RNG/PIDGenerator.cs @@ -178,7 +178,11 @@ public static void SetValuesFromSeed(PKM pk, PIDType type, uint seed) return SetValuesFromSeedMG5Shiny; case PIDType.Pokewalker: - return (pk, seed) => pk.PID = GetPokeWalkerPID(pk.TID16, pk.SID16, seed%24, pk.Gender, pk.PersonalInfo.Gender); + return (pk, seed) => + { + var pid = pk.PID = PokewalkerRNG.GetPID(pk.TID16, pk.SID16, seed % 24, pk.Gender, pk.PersonalInfo.Gender); + pk.RefreshAbility((int)(pid & 1)); + }; // others: unimplemented case PIDType.CuteCharm: @@ -245,40 +249,6 @@ public static uint GetMG5ShinyPID(uint gval, uint av, ushort TID16, ushort SID16 return PID; } - public static uint GetPokeWalkerPID(ushort TID16, ushort SID16, uint nature, int gender, byte gr) - { - if (nature >= 24) - nature = 0; - uint pid = ((((uint)TID16 ^ SID16) >> 8) ^ 0xFF) << 24; // the most significant byte of the PID is chosen so the Pokémon can never be shiny. - // Ensure nature is set to required nature without affecting shininess - pid += nature - (pid % 25); - - if (gr is 0 or >= 0xFE) // non-dual gender - return pid; - - // Ensure Gender is set to required gender without affecting other properties - // If Gender is modified, modify the ability if appropriate - - // either m/f - var pidGender = (pid & 0xFF) < gr ? 1 : 0; - if (gender == pidGender) - return pid; - - if (gender == 0) // Male - { - pid += (((gr - (pid & 0xFF)) / 25) + 1) * 25; - if ((nature & 1) != (pid & 1)) - pid += 25; - } - else - { - pid -= ((((pid & 0xFF) - gr) / 25) + 1) * 25; - if ((nature & 1) != (pid & 1)) - pid -= 25; - } - return pid; - } - public static void SetValuesFromSeedMG5Shiny(PKM pk, uint seed) { var gv = seed >> 24; @@ -287,15 +257,6 @@ public static void SetValuesFromSeedMG5Shiny(PKM pk, uint seed) SetRandomIVs(pk); } - public static void SetRandomPIDPokewalker(PKM pk, int nature, int gender) - { - // Pokewalker PIDs cannot yield multiple abilities from the input nature-gender-trainerID. Disregard any ability request. - var pi = pk.PersonalInfo.Gender; - pk.Gender = gender; - pk.PID = GetPokeWalkerPID(pk.TID16, pk.SID16, (uint)nature, gender, pi); - pk.RefreshAbility((int) (pk.PID & 1)); - } - public static void SetRandomWildPID4(PKM pk, int nature, int ability, int gender, PIDType type) { pk.RefreshAbility(ability); diff --git a/PKHeX.Core/Legality/Restrictions/WordFilter.cs b/PKHeX.Core/Legality/Restrictions/WordFilter.cs index 9c33b993e..2b281fadb 100644 --- a/PKHeX.Core/Legality/Restrictions/WordFilter.cs +++ b/PKHeX.Core/Legality/Restrictions/WordFilter.cs @@ -23,10 +23,9 @@ private static Regex[] LoadPatterns(ReadOnlySpan patterns) { var lineCount = 1 + patterns.Count('\n'); var result = new Regex[lineCount]; - var enumerator = patterns.EnumerateLines(); int i = 0; - while (enumerator.MoveNext()) - result[i++] = new Regex(enumerator.Current.ToString(), Options); + foreach (var line in patterns.EnumerateLines()) + result[i++] = new Regex(line.ToString(), Options); return result; } diff --git a/Tests/PKHeX.Core.Tests/PKM/PIDIVTests.cs b/Tests/PKHeX.Core.Tests/PKM/PIDIVTests.cs index 294b4e2e3..6e99a0c17 100644 --- a/Tests/PKHeX.Core.Tests/PKM/PIDIVTests.cs +++ b/Tests/PKHeX.Core.Tests/PKM/PIDIVTests.cs @@ -153,14 +153,14 @@ public void PIDIVPokeSpotTest() } [Theory] - [InlineData(30, 31, 31, 14, 31, 31, 0x28070031, 24, (int)Species.Pikachu, PokewalkerCourse4.YellowForest)] - public void PokewalkerIVTest(uint hp, uint atk, uint def, uint spA, uint spD, uint spE, uint seed, int expect, ushort species, PokewalkerCourse4 course) + [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) { Span tmp = stackalloc uint[LCRNG.MaxCountSeedsIV]; - (uint actualSeed, int prior) = MethodFinderPokewalker.GetSeedsPokewalkerIVs(species, course, tmp, hp, atk, def, spA, spD, spE); - prior.Should().NotBe(MethodFinderPokewalker.NoPokwalkerMatch); - prior.Should().Be(expect); - actualSeed.Should().Be(seed); + var result = PokewalkerRNG.GetFirstSeed(species, course, tmp, hp, atk, def, spA, spD, spE); + result.Type.Should().Be(type); + result.PriorPoke.Should().Be(expect); + result.Seed.Should().Be(seed); } [Fact]