From d457fb02ae0eaabd37712031f290c3ea79cf320f Mon Sep 17 00:00:00 2001 From: Kurt Date: Wed, 29 Jul 2026 23:35:02 -0500 Subject: [PATCH] Update xoro128 solver, channel IV solver I accidentally reverted my intermediate value sum for the channel solver after I discovered the counterexample -- now with the refinements as detailed in the issue, we can proceed with many `long` => `uint` The xoroshiro128+ solvers aren't really set up to take advantage of the eager returns; the iterator struct (not class) would have to be made heavier to store the intermediate values for a given `assume`. Probably would have to inline the methods and break out their inner Update steps. Probably not worth the hassle :) Closes #4844 Co-Authored-By: StarfBerry <65578604+StarfBerry@users.noreply.github.com> --- PKHeX.Core/Legality/RNG/Algorithms/XDRNG.cs | 56 +++--- .../Algorithms/Xoroshiro128PlusReversal.cs | 190 ++++++++++-------- 2 files changed, 134 insertions(+), 112 deletions(-) diff --git a/PKHeX.Core/Legality/RNG/Algorithms/XDRNG.cs b/PKHeX.Core/Legality/RNG/Algorithms/XDRNG.cs index de1aa3b82..d60a86591 100644 --- a/PKHeX.Core/Legality/RNG/Algorithms/XDRNG.cs +++ b/PKHeX.Core/Legality/RNG/Algorithms/XDRNG.cs @@ -358,12 +358,12 @@ public static int GetSeedsChannel(Span result, uint hp, uint atk, uint def { // https://github.com/StarfBerry/PokeRNG/blob/main/Recovery/LCG_Recovery.py // First row of the BKZ-reduced matrix - const int r0 = -002_528_644; - const int r1 = -024_142_902; - const int r2 = 052_961_366; - const int r3 = 007_565_619; - const int r4 = 024_945_956; - const int r5 = -099_942_057; + const uint r0 = 0xFFD96A7C; // -2528644 + const uint r1 = 0xFE8F9BCA; // -24142902 + const uint r2 = 0x3282056; // 52961366 + const uint r3 = 0x737133; // 7565619 + const uint r4 = 0x17CA524; // 24945956 + const uint r5 = 0xFA0B0157; // -99942057 // Constants to bound the variables in the linear combinations for calculating potential solutions const long lower0 = 0x2A_B966_D1C2; @@ -379,42 +379,44 @@ public static int GetSeedsChannel(Span result, uint hp, uint atk, uint def const long upper4 = 0x10_9800_0000; const long upper5 = -0x07_E800_0000; - // IVs 20/12/6/0/0/3 overflows x5Max `int`, so all are kept as `long` as a precaution. - long f0 = ((-10L * hp) + (23L * atk) - def - (15L * spe) + (52L * spa) - (53L * spd)) << 27; - long x0Min = ((f0 + upper0) >> 32) * r0; // LOWER and UPPER are inverted relative to xmin and xmax because R0 is negative (same with R1 and R5) - long x0Max = ((f0 + lower0) >> 32) * r0; + uint x0Min = (uint)((f0 + upper0) >> 32) * r0; // LOWER and UPPER are inverted relative to xmin and xmax because r0 is negative (same with r1 and r5) + uint x0Max = (uint)((f0 + lower0) >> 32) * r0 - r0; long f1 = ((-14L * hp) + (7L * atk) - (18L * def) - (21L * spe) - (26L * spa) - (24L * spd)) << 27; - long x1Min = ((f1 + upper1) >> 32) * r1; - long x1Max = ((f1 + lower1) >> 32) * r1; + uint x1Min = (uint)((f1 + upper1) >> 32) * r1; + uint x1Max = (uint)((f1 + lower1) >> 32) * r1 - r1; long f2 = ((24L * hp) - (5L * atk) + (22L * def) + (15L * spe) - (5L * spa) - (15L * spd)) << 27; - long x2Min = ((f2 + lower2) >> 32) * r2; - long x2Max = ((f2 + upper2) >> 32) * r2; + uint x2Min = (uint)((f2 + lower2) >> 32) * r2; + uint x2Max = (uint)((f2 + upper2) >> 32) * r2 + r2; long f3 = ((-5L * hp) - (24L * atk) + (26L * def) - (12L * spe) + (9L * spa) + (14L * spd)) << 27; - long x3Min = ((f3 + lower3) >> 32) * r3; - long x3Max = ((f3 + upper3) >> 32) * r3; + uint x3Min = (uint)((f3 + lower3) >> 32) * r3; + uint x3Max = (uint)((f3 + upper3) >> 32) * r3 + r3; long f4 = ((27L * atk) - (18L * spe) - (8L * spa) - spd) << 27; - long x4Min = ((f4 + lower4) >> 32) * r4; - long x4Max = ((f4 + upper4) >> 32) * r4; + uint x4Min = (uint)((f4 + lower4) >> 32) * r4; + uint x4Max = (uint)((f4 + upper4) >> 32) * r4 + r4; long f5 = ((-27L * hp) + (18L * def) + (8L * spe) + spa) << 27; - long x5Min = ((f5 + upper5) >> 32) * r5; - long x5Max = ((f5 + lower5) >> 32) * r5; + uint x5Min = (uint)((f5 + upper5) >> 32) * r5; + uint x5Max = (uint)((f5 + lower5) >> 32) * r5 - r5; // at most 720 iterations in total (around 369 in average, 48 in the best case) int ctr = 0; - for (long x5 = x5Min; x5 <= x5Max; x5 -= r5) + for (uint x5 = x5Min; x5 != x5Max; x5 -= r5) { - for (long x4 = x4Min; x4 <= x4Max; x4 += r4) + for (uint x4 = x4Min; x4 != x4Max; x4 += r4) { - for (long x2 = x2Min; x2 <= x2Max; x2 += r2) + uint l4 = x5 + x4; + for (uint x2 = x2Min; x2 != x2Max; x2 += r2) { - for (long x3 = x3Min; x3 <= x3Max; x3 += r3) + uint l2 = l4 + x2; + for (uint x3 = x3Min; x3 != x3Max; x3 += r3) { - for (long x1 = x1Min; x1 <= x1Max; x1 -= r1) + uint l3 = l2 + x3; + for (uint x1 = x1Min; x1 != x1Max; x1 -= r1) { - for (long x0 = x0Min; x0 <= x0Max; x0 -= r0) + uint l1 = l3 + x1; + for (uint x0 = x0Min; x0 != x0Max; x0 -= r0) { - uint seed = unchecked((uint)(x5 + x4 + x2 + x3 + x1 + x0)); + uint seed = l1 + x0; if ((seed >> 27) != hp) continue; if (Next5(ref seed) != atk) diff --git a/PKHeX.Core/Legality/RNG/Algorithms/Xoroshiro128PlusReversal.cs b/PKHeX.Core/Legality/RNG/Algorithms/Xoroshiro128PlusReversal.cs index 17b6b314f..96d83d671 100644 --- a/PKHeX.Core/Legality/RNG/Algorithms/Xoroshiro128PlusReversal.cs +++ b/PKHeX.Core/Legality/RNG/Algorithms/Xoroshiro128PlusReversal.cs @@ -1,6 +1,5 @@ using System.Collections; using System.Collections.Generic; -using static System.Numerics.BitOperations; using static PKHeX.Core.Xoroshiro128Plus; @@ -11,55 +10,57 @@ namespace PKHeX.Core; /// public static class Xoroshiro128PlusReversal { + /* + * https://github.com/StarfBerry/PokeRNG/blob/1e9b9ddf2494837c7d6704c7b8a3831f644bdea9/Recovery/Xoroshiro_Recovery.py + * Instead of assuming bits 32~37 of the seeds, we assume bits 58~63. + * These new bits make it possible to check if the system of equations is solvable. + * On average, this avoids about half of the calculations. + * + * In the case of two outputs with a skip in between: + * - the old algorithm assumes 1 carry bit and bits 32~36 and 48~53 of the seeds, + * - the new algorithm assumes 2 carry bits and bits 43~50 of the seeds. + * + * The number of brute-forced bits has been reduced from 12 to 10. + * This approach is between 7 and 36 times faster than the old algorithm when benchmarked. + * + * C# yield syntax allocates an object on the heap rather than a struct, so we must manually implement the iterator. + * This manual iterator repeats the intro work before the eager check, but it is still faster than the old algorithm. + * If a fatter struct is used, we can avoid the repeated work by storing the intermediate results in the struct. + * Not really worth the extra effort at this time. + */ + /// /// Reverses two consecutive calls (low 32-bits) to get the original 64-bit seed. /// /// Resulting seed /// First output, low 32-bits /// Second output, low 32-bits - /// Carry bit - /// Brute-forced guess of bits 32-37 + /// Brute-forced guess of bits 58-63 + /// Carry bit /// True if a satisfactory seed was found, false otherwise. - public static bool Explore(out ulong seed, uint out1, uint out2, ulong assume1, ulong assume2) + public static bool Explore(out ulong seed, uint out1, uint out2, byte assume, byte carry) { - seed = 0; + ulong baseSeed = (uint)unchecked(out1 - XOROSHIRO_CONST); + ulong x0 = baseSeed ^ XOROSHIRO_CONST; - // out1 = A + B - seed |= (uint)unchecked(out1 - XOROSHIRO_CONST); + baseSeed |= ((out2 - (x0 >> 27) ^ x0) & 0x1F) << 40; + ulong x1 = (baseSeed << 6) ^ (x0 >> 18) ^ (x0 >> 2); + ulong sub = (out2 >> 18) - (x1 ^ assume); + ulong r = (sub - carry) & 0x3FFF; - // Assume 32-37 - seed |= ((assume2 & 0x3F) << 32); + seed = ((ulong)assume << 58) | baseSeed; + if ((assume & 1u) != (r >> 13)) + return false; // the assumed and recovered bits 58 don't match - ulong s0 = seed, s1 = XOROSHIRO_CONST; - - s1 ^= s0; - ulong x0 = s1; - - //s0 = BitOperations.RotateLeft(s0, 24) ^ s1 ^ (s1 << 16); - s1 = RotateLeft(s1, 37); - - // Extract 40-50 - seed |= (((out2 - s1) ^ x0) & 0x7FF) << 40; - - // Assuming carry, extract 51-58 - seed |= ((((out2 >> 24) - (seed ^ (x0 >> 8) ^ (x0 >> 24)) - assume1) ^ (XOROSHIRO_CONST >> 51)) & 0xFF) << 51; - - // Extract 38-39 - seed |= ((((out2 - ((seed >> 40) ^ x0)) >> 11) ^ (XOROSHIRO_CONST >> 38)) & 3) << 38; - - // Extract the rest - seed |= (((out2 - RotateLeft(seed ^ XOROSHIRO_CONST, 37)) >> 19) ^ (x0 >> 3) ^ (x0 >> 19)) << 59; + // the xor operation to recover the bits can be applied after the check because ((XOROSHIRO_CONST >> 45) >> 13) & 1 == 0 + // + // 45-57 + seed |= ((r ^ 0x1515) << 45); // 0x1515 = (XOROSHIRO_CONST >> 45) & 0x3fff + // 32-39 + seed |= (((((out2 - ((seed >> 40) ^ x0)) >> 5) ^ 0x75) & 0xFF) << 32); // 0x75 = (XOROSHIRO_CONST >> 32) & 0xff var check = new Xoroshiro128Plus(seed); - var test1 = check.Next(); - var test2 = check.Next(); - - // Double-check our result. - if ((uint)test1 != out1) - return false; - if ((uint)test2 != out2) - return false; - return true; + return (uint)check.Next() == out1 && (uint)check.Next() == out2; } /// @@ -68,58 +69,78 @@ public static bool Explore(out ulong seed, uint out1, uint out2, ulong assume1, /// Resulting seed /// First output, low 32-bits /// Second output, low 32-bits - /// Carry bit - /// Brute-forced guess of bits 32-36 - /// Brute-forced guess of bits 48-53 + /// Brute-forced guess of bits 43-50 + /// First carry bit + /// Second carry bit /// True if a satisfactory seed was found, false otherwise. - public static bool ExploreDouble(out ulong seed, uint out1, uint out2, ulong assume1, ulong assume2, ulong assume3) + public static bool ExploreDouble(out ulong seed, uint out1, uint out2, byte assume2, byte carry1, byte carry2) { - seed = 0; + ulong baseSeed = (uint)unchecked(out1 - XOROSHIRO_CONST); + ulong bitsCheck = baseSeed & 7; + ulong x0 = baseSeed ^ XOROSHIRO_CONST; - // out1 = A + B - seed |= (uint)unchecked(out1 - XOROSHIRO_CONST); - seed |= ((assume2 & 0x1F) << 32); // Assume 32-36 - seed |= ((assume3 & 0x3F) << 48); // Assume 48-53 + ulong x1_ = (baseSeed >> 19) ^ (x0 >> 6) ^ 0x56; + ulong x2_ = (x0 >> 16) ^ 0x65; + ulong x3_ = x1_ ^ (x0 >> 27); + ulong x4_ = x2_ ^ (x0 >> 27); + ulong x5 = (baseSeed >> 16) ^ x0 ^ 0x2B1; + ulong x6 = (baseSeed >> 3) ^ (x0 >> 11) ^ 0xE0A; + ulong x7 = XOROSHIRO_CONST ^ (x0 >> 24); + ulong t0 = out2 >> 16; + ulong t1 = out2 >> 27; - ulong s0 = seed, s1 = XOROSHIRO_CONST; + ulong assume = baseSeed | ((ulong)assume2 << 43); + x1_ ^= assume2; + x2_ ^= assume2; + x3_ ^= assume2; + x4_ ^= assume2; - s1 ^= s0; - ulong x0 = s1; + ulong sub0 = t0 - carry1; - s0 = RotateLeft(s0, 24) ^ s1 ^ (s1 << 16); - s1 = RotateLeft(s1, 37); + // 32-36 + ulong tmp = ((((sub0 - x3_) ^ x4_) & 0x1F) << 32) | assume; + x0 = tmp ^ XOROSHIRO_CONST; - s1 ^= s0; + // 37-39 + tmp |= (((sub0 - (x1_ ^ (x0 >> 27))) ^ x2_ ^ (x0 >> 27)) & 0xFF) << 32; + x0 = tmp ^ XOROSHIRO_CONST; - s0 = RotateLeft(s0, 24) ^ s1 ^ (s1 << 16); - //s1 = BitOperations.RotateLeft(s1, 37); + ulong r = (((out2 - (x5 ^ (x0 >> 27) ^ (x0 >> 24))) ^ x6 ^ (x0 >> 27)) & 0x1FFF); + if ((r >> 10) != bitsCheck) // recovered bits 37-39 cannot yield a solution + { + seed = 0; + return false; + } - // Extract 54-63 - seed |= ((out2 - s0) ^ ((seed >> 3) ^ (x0 >> 27) ^ (x0 >> 11) ^ (XOROSHIRO_CONST >> 54))) << 54; + // 54-63 + tmp |= (r & 0x3FF) << 54; + x0 = tmp ^ XOROSHIRO_CONST; + ulong x8 = (tmp >> 30) ^ (x0 >> 17) ^ (x0 >> 54); + ulong x9 = (tmp >> 43) ^ (tmp >> 51) ^ (x0 >> 27) ^ (x0 >> 54) ^ 3; + ulong x10 = x8 ^ (x0 >> 38); + ulong x11 = x9 ^ (x0 >> 38); + ulong x12 = x7 ^ (x0 >> 35) ^ (tmp >> 48); + ulong x13 = (tmp >> 19) ^ (x0 >> 6); + ulong x14 = x13 ^ (x0 >> 27); - // Mix in the new bits + ulong sub1 = t1 - carry2; + seed = ((((sub1 - x10) ^ x11) & 3) << 51) | tmp; x0 = seed ^ XOROSHIRO_CONST; - seed |= ((((out2 >> 21) - ((seed >> 24) ^ (x0 >> 48) ^ (x0 >> 32) ^ (x0 >> 11)) - assume1) ^ ((XOROSHIRO_CONST >> 61) ^ (XOROSHIRO_CONST >> 45) ^ (x0 >> 21) ^ (x0 >> 48) ^ (x0 >> 32))) & 0x7) << 37; - - // Mix in the new bits + seed |= ((((sub0 - (x14 ^ (x0 >> 43))) >> 8) ^ x12 ^ (x0 >> 51)) & 3) << 40; x0 = seed ^ XOROSHIRO_CONST; - seed |= (((((out2 >> 21) - ((seed >> 24) ^ (x0 >> 48) ^ (x0 >> 32) ^ (x0 >> 11)) - assume1) >> 3) ^ (XOROSHIRO_CONST ^ (seed >> 48) ^ (x0 >> 24) ^ (x0 >> 51) ^ (x0 >> 35))) & 0x1F) << 40; - - // Mix in the new bits + seed |= (((sub1 - (x8 ^ (x0 >> 38))) ^ x9 ^ (x0 >> 38)) & 7) << 51; x0 = seed ^ XOROSHIRO_CONST; - seed |= (((((out2 >> 21) - ((seed >> 24) ^ (x0 >> 48) ^ (x0 >> 32) ^ (x0 >> 11)) - assume1) >> 3) ^ (XOROSHIRO_CONST ^ (seed >> 48) ^ (x0 >> 24) ^ (x0 >> 51) ^ (x0 >> 35))) & 0xFF) << 40; - // Double-check our result. + r = ((((sub0 - (x13 ^ (x0 >> 27) ^ (x0 >> 43))) >> 8) ^ x7 ^ (x0 >> 35) ^ (seed >> 48) ^ (x0 >> 51)) & 0x7F); + if ((assume2 & 0xFu) != (r >> 3)) + return false; + + seed |= r << 40; var check = new Xoroshiro128Plus(seed); - var test1 = check.Next(); - _ = check.Next(); // gap - var test2 = check.Next(); - - if ((uint)test1 != out1) + if ((uint)check.Next() != out1) return false; - if ((uint)test2 != out2) - return false; - return true; + _ = check.Next(); + return (uint)check.Next() == out2; } } @@ -137,7 +158,7 @@ public record struct XoroMachineConsecutive(uint First, uint Second) : IEnumerat public readonly ulong Current => seed; private ulong seed; - private byte assume1; + private byte assume1; // assume 58-63 private byte carry; public bool MoveNext() @@ -146,7 +167,7 @@ public bool MoveNext() { while (carry < 2) { - if (Xoroshiro128PlusReversal.Explore(out seed, First, Second, carry++, assume1)) + if (Xoroshiro128PlusReversal.Explore(out seed, First, Second, assume1, carry++)) return true; } carry = 0; @@ -175,9 +196,9 @@ public record struct XoroMachineSkip(uint First, uint Third) : IEnumerator seed; private ulong seed; - private byte assume1; - private byte assume2; - private byte carry; + private byte assume1; // assume 43-50 + private byte carry1; + private byte carry2; public bool MoveNext() { @@ -185,20 +206,19 @@ public bool MoveNext() { do { - while (carry < 2) + while (carry1 < 2) { - if (Xoroshiro128PlusReversal.ExploreDouble(out seed, First, Third, carry++, assume1, assume2)) + if (Xoroshiro128PlusReversal.ExploreDouble(out seed, First, Third, assume1, carry1++, carry2)) return true; } - carry = 0; - } while (++assume2 < 0x40); - assume2 = 0; - } while (++assume1 < 0x20); + carry1 = 0; + } while ((carry2 ^= 1) != 0); + } while (++assume1 != 0); return false; } // IEnumerator Implementation -- used for foreach syntax sugar inlining - public void Reset() => assume1 = assume2 = carry = 0; + public void Reset() => assume1 = carry1 = carry2 = 0; readonly object IEnumerator.Current => Current; public readonly void Dispose() { } public readonly IEnumerator GetEnumerator() => this;