diff --git a/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterStatic8b.cs b/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterStatic8b.cs
index 56afd7a4b..25dc4f031 100644
--- a/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterStatic8b.cs
+++ b/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterStatic8b.cs
@@ -57,13 +57,16 @@ protected override void ApplyDetails(ITrainerInfo sav, EncounterCriteria criteri
pk.Met_Location = pk.Egg_Location = Locations.Default8bNone;
base.ApplyDetails(sav, criteria, pk);
var req = GetRequirement(pk);
- if (req != MustHave)
+ if (req == MustHave) // Roamers
{
- pk.SetRandomEC();
- return;
+ var shiny = Shiny == Shiny.Random ? Shiny.FixedValue : Shiny;
+ Roaming8bRNG.ApplyDetails(pk, criteria, shiny, FlawlessIVCount);
+ }
+ else
+ {
+ var shiny = Shiny == Shiny.Never ? Shiny.Never : Shiny.Random;
+ Wild8bRNG.ApplyDetails(pk, criteria, shiny, FlawlessIVCount);
}
- var shiny = Shiny == Shiny.Random ? Shiny.FixedValue : Shiny;
- Roaming8bRNG.ApplyDetails(pk, criteria, shiny, FlawlessIVCount);
}
protected override void SetMetData(PKM pk, int level, DateTime today)
diff --git a/PKHeX.Core/Legality/RNG/Wild8bRNG.cs b/PKHeX.Core/Legality/RNG/Wild8bRNG.cs
new file mode 100644
index 000000000..17b48a708
--- /dev/null
+++ b/PKHeX.Core/Legality/RNG/Wild8bRNG.cs
@@ -0,0 +1,172 @@
+using System;
+
+namespace PKHeX.Core
+{
+ ///
+ /// Contains logic for the Generation 8b (BD/SP) wild and stationary spawns.
+ ///
+ public static class Wild8bRNG
+ {
+ private const int UNSET = -1;
+
+ public static void ApplyDetails(PKM pk, EncounterCriteria criteria, Shiny shiny = Shiny.FixedValue, int flawless = -1)
+ {
+ if (shiny == Shiny.FixedValue)
+ shiny = criteria.Shiny is Shiny.Random or Shiny.Never ? Shiny.Never : Shiny.Always;
+ if (flawless == -1)
+ flawless = 0;
+
+ int ctr = 0;
+ const int maxAttempts = 50_000;
+ var rnd = Util.Rand;
+ do
+ {
+ ulong s0 = Util.Rand32(rnd) | (ulong)Util.Rand32(rnd) << 32;
+ ulong s1 = Util.Rand32(rnd) | (ulong)Util.Rand32(rnd) << 32;
+ var xors = new XorShift128(s0, s1);
+ if (TryApplyFromSeed(pk, criteria, shiny, flawless, xors))
+ return;
+ } while (++ctr != maxAttempts);
+
+ {
+ ulong s0 = Util.Rand32(rnd) | (ulong)Util.Rand32(rnd) << 32;
+ ulong s1 = Util.Rand32(rnd) | (ulong)Util.Rand32(rnd) << 32;
+ var xors = new XorShift128(s0, s1);
+ TryApplyFromSeed(pk, EncounterCriteria.Unrestricted, shiny, flawless, xors);
+ }
+ }
+
+ public static bool TryApplyFromSeed(PKM pk, EncounterCriteria criteria, Shiny shiny, int flawless, XorShift128 xors)
+ {
+ // Encryption Constant
+ pk.EncryptionConstant = xors.NextUInt();
+
+ // PID
+ var fakeTID = xors.NextUInt(); // fakeTID
+ var pid = xors.NextUInt();
+ pid = GetRevisedPID(fakeTID, pid, pk);
+ if (shiny == Shiny.Never)
+ {
+ if (GetIsShiny(pk.TID, pk.SID, pid))
+ return false;
+ }
+ else if (shiny != Shiny.Random)
+ {
+ if (!GetIsShiny(pk.TID, pk.SID, pid))
+ return false;
+
+ if (shiny == Shiny.AlwaysSquare && pk.ShinyXor != 0)
+ return false;
+ if (shiny == Shiny.AlwaysStar && pk.ShinyXor == 0)
+ return false;
+ }
+ pk.PID = pid;
+
+ // Check IVs: Create flawless IVs at random indexes, then the random IVs for not flawless.
+ Span ivs = stackalloc[] { UNSET, UNSET, UNSET, UNSET, UNSET, UNSET };
+ const int MAX = 31;
+ var determined = 0;
+ while (determined < flawless)
+ {
+ var idx = (int)xors.NextUInt(6);
+ if (ivs[idx] != UNSET)
+ continue;
+ ivs[idx] = 31;
+ determined++;
+ }
+
+ for (var i = 0; i < ivs.Length; i++)
+ {
+ if (ivs[i] == UNSET)
+ ivs[i] = (int)xors.NextInt(0, MAX + 1);
+ }
+
+ if (!criteria.IsIVsCompatible(ivs, 8))
+ return false;
+
+ pk.IV_HP = ivs[0];
+ pk.IV_ATK = ivs[1];
+ pk.IV_DEF = ivs[2];
+ pk.IV_SPA = ivs[3];
+ pk.IV_SPD = ivs[4];
+ pk.IV_SPE = ivs[5];
+
+ // Ability
+ pk.SetAbilityIndex((int)xors.NextUInt(2));
+
+ // Gender (skip this if gender is fixed)
+ var genderRatio = PersonalTable.BDSP.GetFormEntry(pk.Species, pk.Form).Gender;
+ if (genderRatio == PersonalInfo.RatioMagicGenderless)
+ {
+ pk.Gender = 2;
+ }
+ else if (genderRatio == PersonalInfo.RatioMagicMale)
+ {
+ pk.Gender = 0;
+ }
+ else if (genderRatio == PersonalInfo.RatioMagicFemale)
+ {
+ pk.Gender = 1;
+ }
+ else
+ {
+ var next = (((int)xors.NextUInt(253) + 1 < genderRatio) ? 1 : 0);
+ if (criteria.Gender is 0 or 1 && next != criteria.Gender)
+ return false;
+ pk.Gender = next;
+ }
+
+ // Skip nature, assuming Synchronize
+ if (criteria.Nature >= 0 && (int)criteria.Nature < 25)
+ pk.Nature = (int)criteria.Nature;
+ else
+ pk.Nature = (int)xors.NextUInt(25);
+ pk.StatNature = pk.Nature;
+
+ // Remainder
+ var scale = (IScaledSize)pk;
+ scale.HeightScalar = (int)xors.NextUInt(0x81) + (int)xors.NextUInt(0x80);
+ scale.WeightScalar = (int)xors.NextUInt(0x81) + (int)xors.NextUInt(0x80);
+
+ // Item, don't care
+ return true;
+ }
+
+ private static uint GetRevisedPID(uint fakeTID, uint pid, ITrainerID tr)
+ {
+ var xor = GetShinyXor(pid, fakeTID);
+ var newXor = GetShinyXor(pid, (uint)(tr.TID | (tr.SID << 16)));
+
+ var fakeRare = GetRareType(xor);
+ var newRare = GetRareType(newXor);
+
+ if (fakeRare == newRare)
+ return pid;
+
+ var isShiny = xor < 16;
+ if (isShiny)
+ return (((uint)(tr.TID ^ tr.SID) ^ (pid & 0xFFFF) ^ (xor == 0 ? 0u : 1u)) << 16) | (pid & 0xFFFF); // force same shiny star type
+ return pid ^ 0x1000_0000;
+ }
+
+ private static Shiny GetRareType(uint xor) => xor switch
+ {
+ 0 => Shiny.AlwaysSquare,
+ < 16 => Shiny.AlwaysStar,
+ _ => Shiny.Never,
+ };
+
+ private static bool GetIsShiny(int tid, int sid, uint pid)
+ {
+ return GetIsShiny(pid, (uint)((sid << 16) | tid));
+ }
+
+ private static bool GetIsShiny(uint pid, uint oid) => GetShinyXor(pid, oid) < 16;
+
+ private static uint GetShinyXor(uint pid, uint oid)
+ {
+ var xor = pid ^ oid;
+ return (xor ^ (xor >> 16)) & 0xFFFF;
+ }
+ }
+}
diff --git a/Tests/PKHeX.Core.Tests/Legality/Wild8bRNGTests.cs b/Tests/PKHeX.Core.Tests/Legality/Wild8bRNGTests.cs
new file mode 100644
index 000000000..7d7453e5a
--- /dev/null
+++ b/Tests/PKHeX.Core.Tests/Legality/Wild8bRNGTests.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using FluentAssertions;
+using PKHeX.Core;
+using Xunit;
+
+namespace PKHeX.Core.Tests.Legality
+{
+ public static class Wild8bRNGTests
+ {
+ [Fact]
+ public static void TryGenerateLatias()
+ {
+ PB8 test = new() { Species = (int)Species.Latias};
+ ulong s0 = 0xdf9cf5c73e4a160b;
+ ulong s1 = 0xd0b8383103a7f201;
+ Wild8bRNG.TryApplyFromSeed(test, EncounterCriteria.Unrestricted, Shiny.Random, 3, new XorShift128(s0, s1));
+ test.IV_HP.Should().Be(31);
+ test.IV_ATK.Should().Be(4);
+ test.IV_DEF.Should().Be(31);
+ test.IV_SPA.Should().Be(31);
+ test.IV_SPD.Should().Be(6);
+ test.IV_SPE.Should().Be(8);
+ test.HeightScalar.Should().Be(123);
+ test.WeightScalar.Should().Be(115);
+ }
+ }
+}