From 2ff2815dbe75a12e5770708cbd1c0eeb94a387a6 Mon Sep 17 00:00:00 2001 From: Kurt Date: Fri, 22 Mar 2024 16:12:56 -0500 Subject: [PATCH] Add Channel Jirachi menu pattern verification Sitting on my todo-list for far too long see pokefinder for similar implementation PKHeX has a forward & reverse implementation available for 100% documentation purposes :) The seed indicated in hover previews now matches that of the true Origin seed for the encounter (pre menu & accept dialog), similar to Method H/J/K encounters. Co-Authored-By: Admiral-Fish <24730718+Admiral-Fish@users.noreply.github.com> --- .../ByGeneration/EncounterGenerator3.cs | 10 + PKHeX.Core/Legality/RNG/Algorithms/XDRNG.cs | 11 + .../RNG/ClassicEra/Gen3/ChannelJirachi.cs | 224 ++++++++++++++++++ PKHeX.Core/MysteryGifts/WC3.cs | 1 + 4 files changed, 246 insertions(+) create mode 100644 PKHeX.Core/Legality/RNG/ClassicEra/Gen3/ChannelJirachi.cs diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3.cs index b565f4a7c..8d987f5a0 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3.cs @@ -48,6 +48,16 @@ public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, Le } if (e is not EncounterSlot3 slot) { + if (e is WC3 { TID16: 40122 } channel) + { + var chk = ChannelJirachi.GetPossible(info.PIDIV.OriginSeed); + if (chk.Pattern is not ChannelJirachiRandomResult.None) + info.PIDIV = info.PIDIV.AsEncounteredVia(new(chk.Seed, LeadRequired.None)); + else + info.ManualFlag = EncounterYieldFlag.InvalidPIDIV; + yield return channel; + yield break; + } yield return e; continue; } diff --git a/PKHeX.Core/Legality/RNG/Algorithms/XDRNG.cs b/PKHeX.Core/Legality/RNG/Algorithms/XDRNG.cs index 568085752..e15bb7380 100644 --- a/PKHeX.Core/Legality/RNG/Algorithms/XDRNG.cs +++ b/PKHeX.Core/Legality/RNG/Algorithms/XDRNG.cs @@ -116,6 +116,17 @@ public static uint Next15(ref uint seed) return (seed >> 16) & 0x7FFF; } + /// + /// Gets the previous 16 bits of the previous RNG seed. + /// + /// Seed to advance one step. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Prev16(ref uint seed) + { + seed = Prev(seed); + return seed >> 16; + } + /// /// Advances the RNG seed to the next state value a specified amount of times. /// diff --git a/PKHeX.Core/Legality/RNG/ClassicEra/Gen3/ChannelJirachi.cs b/PKHeX.Core/Legality/RNG/ClassicEra/Gen3/ChannelJirachi.cs new file mode 100644 index 000000000..692dd754d --- /dev/null +++ b/PKHeX.Core/Legality/RNG/ClassicEra/Gen3/ChannelJirachi.cs @@ -0,0 +1,224 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace PKHeX.Core; + +/// +/// Logic for Channel Jirachi RNG seeds prior to PID/IV generation. +/// +/// An arbitrary PID/IV seed is valid about 18% of the time. +public static class ChannelJirachi +{ + /// + /// Checks if the seed that immediately generates the PID/IV is valid. + /// + /// Seed that generates the PID/IV + /// True if valid. + public static bool IsPossible(uint seed) => GetPossible(seed).Pattern != ChannelJirachiRandomResult.None; + + /// + /// Checks if any of the 3 Jirachi float patterns are possible. + /// + /// Seed that generates the PID/IV + /// First possible float pattern and origin seed (pre-menu). + public static (ChannelJirachiRandomResult Pattern, uint Seed) GetPossible(uint seed) + { + // Jirachi floating into the screen has 3 different patterns to follow + // Check each of them + for (uint i = 0; i < 3; i++) + { + var origin = seed; + if (!IsValid(ref origin, i)) + continue; + return ((ChannelJirachiRandomResult)(i + 1), origin); + } + return default; + } + + /// + /// Check if the seed is valid for the Jirachi float pattern type AND menu pattern. + /// + /// Seed that generated the PID/IV, to be reversed into the true origin seed. + /// + /// True if valid. + public static bool IsValid(ref uint origin, uint possibleBranch) + { + if (!IsValidAccept(ref origin, possibleBranch)) + return false; + if (!IsValidMenu(ref origin)) + return false; + origin = XDRNG.Prev(origin); // reverse to the seed before the menu pattern + return true; + } + + /// + /// Check if the seed is valid for the Accept random chances. + /// + /// Seed that generated the PID/IV, to be reversed to the post-menu seed. + /// Float pattern (0-2, not 1-3). + public static bool IsValidAccept(ref uint seed, uint acceptPivot) + { + // The game checks for two random results (25% and 33%). + // If either passes, we advance once, otherwise we advance twice. + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(acceptPivot, 3u, nameof(acceptPivot)); + + // 71.9% chance of passing at least one of the following branches. + // 28.1% chance of failing all branches. + + if (acceptPivot == 0) // 8 advances total + { + // 2: 25% fails, 33% fails. 2 advances. ~50.25% chance of success. + seed = XDRNG.Prev2(seed); // 2 + if (XDRNG.Prev16(ref seed) <= 0x547A) // 33% should fail + return false; + if (XDRNG.Prev16(ref seed) <= 0x4000) // 25% should fail + return false; + } + else if (acceptPivot == 1) // 6 advances total + { + // 0: 25% passes, 33% unchecked. 1 advance. 25% chance of success. + seed = XDRNG.Prev(seed); // 1 + if (XDRNG.Prev16(ref seed) > 0x4000) // 25% should pass + return false; + } + else // 7 advances total + { + // 25% fails, 33% passes. 1 advance. 24.75% chance of success. + seed = XDRNG.Prev(seed); // 1 + if (XDRNG.Prev16(ref seed) > 0x547A) // 33% should pass + return false; + if (XDRNG.Prev16(ref seed) <= 0x4000) // 25% should fail + return false; + } + seed = XDRNG.Prev4(seed); + return true; + } + + /// + /// Check if the seed is valid for the Jirachi menu pattern. + /// + /// Already reversed to the seed immediately after the menu pattern, to be reversed to the origin seed. + /// True if valid. + public static bool IsValidMenu(ref uint seed) + { + // 25% chance of being an impossible seed (0 as the final result). + // 2/3 chance of being an impossible seed (end value reoccurs before the other two values are seen). + // 25% chance of a seed being valid. + + // From the starting seed, the game determines the order of [1,2,3] by looping until all 3 are generated via (seed >> 30). + // Track which values we've seen. If we've not yet seen the result, add it to the bitmask. + var end = seed >> 30; + // Menu can't end on a 0; we expect 1-3. + if (end == 0) + return false; + + // Start with the final result. + var seen = 1u << (int)end; + + // Iterate backwards until all 3 results are observed. + // If we encounter the ending result again, then the seed isn't legal -- a duplicate end value would be impossible to stop on. + while (true) + { + seed = XDRNG.Prev(seed); + var pattern = seed >> 30; // 0-3 + if (pattern == end) + return false; // Duplicate. Input seed cannot be landed on. + + // Ignore skipping 0; add it to the bitmask anyway (reduce branching). + seen |= 1u << (int)pattern; + if (seen >= BitPattern123) // only true if 1-3 bits set. 0th bit irrelevant. + return true; // Seen all! seed is legal. + } + } + + private const byte BitPattern123 = 0b1110; // ignore bit 0; we need to see bits 1-3 + + /// + /// Runs the Menu pattern calculation forward to see which pattern order is generated. + /// + /// Initial seed (not yet advanced once). + /// Order and final seed. + public static (byte First, byte Second, byte Third, uint EndSeed) GetMenuPattern(uint seed) + { + Span pattern = stackalloc byte[3]; + var result = GetMenuPattern(seed, pattern); + return (pattern[0], pattern[1], pattern[2], result); + } + + /// + /// Runs the Menu pattern calculation forward to see which pattern order is generated. + /// + /// Initial seed (not yet advanced once). + /// Result to store the order. + /// Final seed. + public static uint GetMenuPattern(uint seed, [Length(3,3)] Span pattern) + { + int i = 0; + while (i != pattern.Length) + { + seed = XDRNG.Next(seed); + var p = (byte)(seed >> 30); + if (pattern.Contains(p)) + continue; // 0 will always return true, yay zero initialized arrays! + pattern[i++] = p; + } + return seed; + } + + /// + /// Skips from origin to the PID/IV generation seed. + /// + public static uint SkipToPIDIV(uint seed) + { + seed = SkipMenuPattern(seed); + seed = SkipAccept(seed); + return seed; + } + + /// + /// Skips the menu pattern calculation to the seed that is used by the Accept step. + /// + public static uint SkipMenuPattern(uint seed) + { + uint seen = 0; + while (true) + { + seed = XDRNG.Next(seed); + var p = (byte)(seed >> 30); + seen |= 1u << p; + if (seen >= BitPattern123) // only true if 1-3 bits set. 0th bit irrelevant. + return seed; + } + } + + /// + /// Skips the 25% and 33% calculation done when accepting the Jirachi to the end seed that then can be used to generate the PID/IV. + /// + public static uint SkipAccept(uint seed) + { + seed = XDRNG.Next4(seed); + + // If we pass either a 25% or 33% check, we advance once. + // Each check is independent of the other, using a different random value. + var rand = XDRNG.Next16(ref seed); + if (rand <= 0x4000) // 25% + return XDRNG.Next(seed); + + rand = XDRNG.Next16(ref seed); + if (rand <= 0x547A) // 33% + return XDRNG.Next(seed); + + // Both failed, advance twice. + return XDRNG.Next2(seed); + } +} + +public enum ChannelJirachiRandomResult : byte +{ + None = 0, + + // Ordered by most likely to occur. + BothFail = 1, // 50.25% + FirstPass = 2, // 25% + SecondPass = 3, // 24.75% +} diff --git a/PKHeX.Core/MysteryGifts/WC3.cs b/PKHeX.Core/MysteryGifts/WC3.cs index 6572d2f59..d7f69a785 100644 --- a/PKHeX.Core/MysteryGifts/WC3.cs +++ b/PKHeX.Core/MysteryGifts/WC3.cs @@ -210,6 +210,7 @@ private void SetPINGA(PK3 pk, EncounterCriteria _) { PIDType.BACD_R => seed & 0x0000FFFF, // u16 PIDType.BACD_R_S => seed & 0x000000FF, // u8 + PIDType.Channel => ChannelJirachi.SkipToPIDIV(seed), _ => seed, };