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>
This commit is contained in:
Kurt
2026-07-29 23:35:02 -05:00
parent ce08c00877
commit d457fb02ae
2 changed files with 134 additions and 112 deletions

View File

@@ -358,12 +358,12 @@ public static int GetSeedsChannel(Span<uint> 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<uint> 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)

View File

@@ -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;
/// </summary>
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.
*/
/// <summary>
/// Reverses two consecutive <see cref="Xoroshiro128Plus.Next()"/> calls (low 32-bits) to get the original 64-bit seed.
/// </summary>
/// <param name="seed">Resulting seed</param>
/// <param name="out1">First output, low 32-bits</param>
/// <param name="out2">Second output, low 32-bits</param>
/// <param name="assume1">Carry bit</param>
/// <param name="assume2">Brute-forced guess of bits 32-37</param>
/// <param name="assume">Brute-forced guess of bits 58-63</param>
/// <param name="carry">Carry bit</param>
/// <returns>True if a satisfactory seed was found, false otherwise.</returns>
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;
}
/// <summary>
@@ -68,58 +69,78 @@ public static bool Explore(out ulong seed, uint out1, uint out2, ulong assume1,
/// <param name="seed">Resulting seed</param>
/// <param name="out1">First output, low 32-bits</param>
/// <param name="out2">Second output, low 32-bits</param>
/// <param name="assume1">Carry bit</param>
/// <param name="assume2">Brute-forced guess of bits 32-36</param>
/// <param name="assume3">Brute-forced guess of bits 48-53</param>
/// <param name="assume2">Brute-forced guess of bits 43-50</param>
/// <param name="carry1">First carry bit</param>
/// <param name="carry2">Second carry bit</param>
/// <returns>True if a satisfactory seed was found, false otherwise.</returns>
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<ulong
public readonly ulong Current => 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<ulong> GetEnumerator() => this;