From 5bf9865f3f09af99183df1cdb19956189ad455b2 Mon Sep 17 00:00:00 2001 From: Kurt Date: Sun, 9 Mar 2025 20:09:17 -0500 Subject: [PATCH] Add pokespot reverse/forward methods Also adds partial IV specifying for cxd generating --- .../Editing/Applicators/BallApplicator.cs | 4 +- .../ByGeneration/EncounterGenerator3GC.cs | 8 +- .../Templates/Gen3/XD/EncounterSlot3XD.cs | 7 +- .../Legality/Formatting/LegalityFormatting.cs | 2 +- PKHeX.Core/Legality/RNG/CXD/MethodCXD.cs | 7 + PKHeX.Core/Legality/RNG/CXD/MethodPokeSpot.cs | 275 ++++++++++++++++++ PKHeX.Core/Legality/RNG/MethodFinder.cs | 51 ---- PKHeX.Core/Legality/RNG/PIDGenerator.cs | 19 -- Tests/PKHeX.Core.Tests/PKM/PIDIVTests.cs | 11 +- 9 files changed, 296 insertions(+), 88 deletions(-) create mode 100644 PKHeX.Core/Legality/RNG/CXD/MethodPokeSpot.cs diff --git a/PKHeX.Core/Editing/Applicators/BallApplicator.cs b/PKHeX.Core/Editing/Applicators/BallApplicator.cs index 4a72150ce..93d4b628c 100644 --- a/PKHeX.Core/Editing/Applicators/BallApplicator.cs +++ b/PKHeX.Core/Editing/Applicators/BallApplicator.cs @@ -48,10 +48,10 @@ private static int GetLegalBallsEvolvedShedinja(Span result, PKM pk, IEnco { switch (enc) { - case EncounterSlot4 when IsNincadaEvolveInOrigin(pk, enc): + case EncounterSlot4 s4 when IsNincadaEvolveInOrigin(pk, s4): ShedinjaEvolve4.CopyTo(result); return ShedinjaEvolve4.Length; - case EncounterSlot3 when IsNincadaEvolveInOrigin(pk, enc): + case EncounterSlot3 s3 when IsNincadaEvolveInOrigin(pk, s3): return LoadLegalBalls(result, pk, enc); } result[0] = Poke; diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3GC.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3GC.cs index bf1afb55a..fa775a12a 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3GC.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator3GC.cs @@ -31,9 +31,11 @@ public IEnumerable GetEncounters(PKM pk, EvoCriteria[] chain, Le { if (z is EncounterSlot3XD w) { - var pidiv = MethodFinder.GetPokeSpotSeedFirst(pk, w.SlotNumber); - if (pidiv.Type == PIDType.PokeSpot) - info.PIDIV = pidiv; + if (MethodPokeSpot.TryGetOriginSeeds(pk, w, out var pid, out var ivs)) + { + const PIDType type = PIDType.PokeSpot; + info.PIDIV = new PIDIV(type, pid).AsMutated(type, ivs); + } } else if (z is IShadow3 s) { diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterSlot3XD.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterSlot3XD.cs index 60a7f8152..89c888822 100644 --- a/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterSlot3XD.cs +++ b/PKHeX.Core/Legality/Encounters/Templates/Gen3/XD/EncounterSlot3XD.cs @@ -59,10 +59,9 @@ public XK3 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria) private void SetPINGA(XK3 pk, EncounterCriteria criteria, PersonalInfo3 pi) { - var gender = criteria.GetGender(pi); - var nature = criteria.GetNature(); - int ability = criteria.GetAbilityFromNumber(Ability); - PIDGenerator.SetRandomPokeSpotPID(pk, nature, gender, ability, SlotNumber); + MethodPokeSpot.SetRandomPID(pk, criteria, pi.Gender, SlotNumber); + if (criteria.IsSpecifiedIVsAll() && !MethodPokeSpot.TrySetIVs(pk, criteria, LevelMin, LevelMax)) + MethodPokeSpot.SetRandomIVs(pk, criteria, LevelMin, LevelMax); } #endregion diff --git a/PKHeX.Core/Legality/Formatting/LegalityFormatting.cs b/PKHeX.Core/Legality/Formatting/LegalityFormatting.cs index 4a483ab6c..b948f4c2d 100644 --- a/PKHeX.Core/Legality/Formatting/LegalityFormatting.cs +++ b/PKHeX.Core/Legality/Formatting/LegalityFormatting.cs @@ -154,7 +154,7 @@ private static void AddEncounterInfoPIDIV(List lines, LegalInfo info) { var seed = pidiv.OriginSeed; var line = string.Format(L_FOriginSeed_0, seed.ToString("X8")); - if (info is { EncounterMatch: IRandomCorrelationEvent3 } && pidiv.Mutated is not 0 && pidiv.OriginSeed != pidiv.EncounterSeed) + if (pidiv.Mutated is not 0 && pidiv.OriginSeed != pidiv.EncounterSeed) line += $" [{pidiv.EncounterSeed:X8}]"; lines.Add(line); } diff --git a/PKHeX.Core/Legality/RNG/CXD/MethodCXD.cs b/PKHeX.Core/Legality/RNG/CXD/MethodCXD.cs index 546340cdf..5bb344017 100644 --- a/PKHeX.Core/Legality/RNG/CXD/MethodCXD.cs +++ b/PKHeX.Core/Legality/RNG/CXD/MethodCXD.cs @@ -362,6 +362,7 @@ public static bool SetStarterFromIVs(XK3 pk, EncounterCriteria criteria) public static void SetRandomStarter(XK3 pk, EncounterCriteria criteria) { var seed = Util.Rand32(); + bool filterIVs = criteria.IsSpecifiedIVsAny(out var count) && count <= 2; while (true) { @@ -404,6 +405,7 @@ public static void SetRandom(CK3 pk, EncounterCriteria criteria, byte gender) var id32 = pk.ID32; var seed = Util.Rand32(); + bool filterIVs = criteria.IsSpecifiedIVsAny(out var count) && count <= 2; while (true) { // Get PID @@ -425,6 +427,8 @@ public static void SetRandom(CK3 pk, EncounterCriteria criteria, byte gender) var iv32 = iv2 << 15 | iv1; if (criteria.IsSpecifiedHiddenPower() && !criteria.IsSatisfiedHiddenPower(iv32)) continue; + if (filterIVs && !criteria.IsSatisfiedIVs(iv32)) + continue; var abit = XDRNG.Next16(ref seed); pk.PID = pid; @@ -439,6 +443,7 @@ public static void SetRandom(XK3 pk, EncounterCriteria criteria, byte gender) var id32 = pk.ID32; var seed = Util.Rand32(); + bool filterIVs = criteria.IsSpecifiedIVsAny(out var count) && count <= 2; while (true) { // Get PID @@ -460,6 +465,8 @@ public static void SetRandom(XK3 pk, EncounterCriteria criteria, byte gender) var iv32 = iv2 << 15 | iv1; if (criteria.IsSpecifiedHiddenPower() && !criteria.IsSatisfiedHiddenPower(iv32)) continue; + if (filterIVs && !criteria.IsSatisfiedIVs(iv32)) + continue; var abit = XDRNG.Next16(ref seed); pk.PID = pid; diff --git a/PKHeX.Core/Legality/RNG/CXD/MethodPokeSpot.cs b/PKHeX.Core/Legality/RNG/CXD/MethodPokeSpot.cs new file mode 100644 index 000000000..65d219032 --- /dev/null +++ b/PKHeX.Core/Legality/RNG/CXD/MethodPokeSpot.cs @@ -0,0 +1,275 @@ +using System; + +namespace PKHeX.Core; + +public static class MethodPokeSpot +{ + /// + /// Checks if the PID generating seed is valid for the given slot. + /// + /// Encounter slot index (0-2). + /// PID generating seed. + /// Origin seed. + public static PokeSpotSetup IsValidActivation(byte slot, uint seed, out uint origin) + { + // Forward call structure: depends on if Bonsly is available. + // No Bonsly + // 0: Origin + // 1: rand(003) == 0 - Activation + // 2: rand(100) < 10 - if Munchlax available, spawn Munchlax & return. + // x: rand(100) encounter slot + // y: pid + + // Bonsly + // 0: Origin + // 1: rand(003) == 0 - Activation + // 2: ??? + // 3: rand(100) < 30 - Bonsly; < 40 - Munchlax + // x: rand(100) encounter slot + // y: pid + + origin = seed; + // wild + var esv = (seed >> 16) % 100; + if (!IsSlotValid(slot, esv)) + return PokeSpotSetup.Invalid; + + // Assume only Munchlax available + var preSlot = XDRNG.Prev16(ref origin); + if (preSlot % 100 < 10) + return PokeSpotSetup.Invalid; // worst case: only Munchlax available + preSlot = XDRNG.Prev16(ref origin); + if (preSlot % 3 == 0) + return PokeSpotSetup.NoBonsly; + + // Assume both available + if (preSlot % 100 < 30) + return PokeSpotSetup.Invalid; + preSlot = XDRNG.Prev16(ref origin); + if (preSlot % 3 == 0) + return PokeSpotSetup.Bonsly; + return PokeSpotSetup.Invalid; + } + + public static bool TryGetOriginSeeds(PKM pk, EncounterSlot3XD slot, out uint pid, out uint ivs) + { + pid = 0; + ivs = 0; + if (!TryGetOriginSeedPID(pk.PID, slot.SlotNumber, out pid)) + return false; + if (!TryGetOriginSeedIVs(pk, slot.LevelMin, slot.LevelMax, out ivs)) + return false; + return true; + } + + public static bool TryGetOriginSeedIVs(PKM pk, byte levelMin, byte levelMax, out uint origin) + { + var hp = (uint)pk.IV_HP; + var atk = (uint)pk.IV_ATK; + var def = (uint)pk.IV_DEF; + var spa = (uint)pk.IV_SPA; + var spd = (uint)pk.IV_SPD; + var spe = (uint)pk.IV_SPE; + var iv1 = hp | (atk << 5) | (def << 10); + var iv2 = spe | (spa << 5) | (spd << 10); + + return TryGetOriginSeedIVs(iv1 << 16, iv2 << 16, levelMin, levelMax, pk.MetLevel, pk.Format == 3, out origin); + } + + public static bool TryGetOriginSeedPID(uint pid, byte slot, out uint origin) + { + Span seeds = stackalloc uint[XDRNG.MaxCountSeedsPID]; + var count = XDRNG.GetSeeds(seeds, pid); + if (count == 0) + { + origin = 0; + return false; + } + + var reg = seeds[..count]; + foreach (var seed in reg) + { + // check for valid encounter slot info + if (IsValidActivation(slot, seed, out origin) == PokeSpotSetup.Invalid) + continue; + + return true; + } + origin = 0; + return false; + } + + public static bool TryGetOriginSeedIVs(uint iv1, uint iv2, byte levelMin, byte levelMax, byte metLevel, bool hasOriginalMetLevel, out uint origin) + { + Span seeds = stackalloc uint[XDRNG.MaxCountSeedsIV]; + var count = XDRNG.GetSeedsIVs(seeds, iv1, iv2); + if (count == 0) + { + origin = 0; + return false; + } + + var levelDelta = 1u + levelMax - levelMin; + foreach (var preIV in seeds[..count]) + { + // origin + // {u16 fakeID, u16 fakeID} + // ??? + // levelRand + // {u16 fakePID, u16 fakePID} => you are here (origin) + // {u16 iv1, u16 iv2} + // ability + + var lvl16 = XDRNG.Prev2(preIV) >> 16; + var lvlRnd = lvl16 % levelDelta; + var lvl = (byte)(levelMin + lvlRnd); + if (hasOriginalMetLevel ? (metLevel != lvl) : (metLevel < lvl)) + continue; + + var abil16 = XDRNG.Next3(preIV) >> 16; + var abit = abil16 & 1; // don't care about ability, might be reset on evolution + + origin = XDRNG.Prev6(preIV); + return true; + } + + origin = 0; + return false; + } + + public static bool TrySetIVs(XK3 pk, EncounterCriteria criteria, byte levelMin, byte levelMax) + { + Span seeds = stackalloc uint[XDRNG.MaxCountSeedsIV]; + criteria.GetCombinedIVs(out var iv1, out var iv2); + var count = XDRNG.GetSeedsIVs(seeds, iv1 << 16, iv2 << 16); + if (count == 0) + return false; + + var levelDelta = 1u + levelMax - levelMin; + bool checkLevel = criteria.IsSpecifiedLevelRange() && criteria.IsLevelWithinRange(levelMin, levelMax); + foreach (var preIV in seeds[..count]) + { + // origin + // {u16 fakeID, u16 fakeID} + // ??? + // levelRand + // {u16 fakePID, u16 fakePID} => you are here (origin) + // {u16 iv1, u16 iv2} + // ability + + var lvl16 = XDRNG.Prev2(preIV) >> 16; + var lvlRnd = lvl16 % levelDelta; + var lvl = (byte)(levelMin + lvlRnd); + if (checkLevel && !criteria.IsSatisfiedLevelRange(lvl)) + continue; + + var abil16 = XDRNG.Next3(preIV) >> 16; + var abit = abil16 & 1; // don't care about ability, might be reset on evolution + + SetIVs(pk, iv1, iv2); + pk.RefreshAbility((int)(abit & 1)); + pk.CurrentLevel = pk.MetLevel = lvl; + return true; + } + + return false; + } + + public static void SetRandomPID(XK3 pk, EncounterCriteria criteria, byte gender, byte slot) + => pk.PID = GetRandomPID(pk.ID32, criteria, gender, slot, out _); + + public static uint GetRandomPID(uint id32, EncounterCriteria criteria, byte gender, byte slot, out uint origin) + { + var seed = Util.Rand32(); + + while (true) + { + var result = IsValidActivation(slot, seed, out origin); + if (result == PokeSpotSetup.Invalid) + { + seed = XDRNG.Next(seed); + continue; + } + var pid = GetPIDRegular(ref seed); + if (criteria.IsSpecifiedNature() && !criteria.IsSatisfiedNature((Nature)(pid % 25))) + continue; + if (criteria.IsSpecifiedGender() && !criteria.IsSatisfiedGender(EntityGender.GetFromPIDAndRatio(pid, gender))) + continue; + + if (criteria.Shiny.IsShiny() != ShinyUtil.GetIsShiny(id32, pid, 8)) + continue; + + return pid; + } + } + + public static void SetRandomIVs(XK3 pk, EncounterCriteria criteria, byte levelMin, byte levelMax) + { + var levelDelta = 1u + levelMax - levelMin; + var seed = Util.Rand32(); + + bool filterIVs = criteria.IsSpecifiedIVsAny(out var count) && count <= 2; + bool checkLevel = criteria.IsSpecifiedLevelRange() && criteria.IsLevelWithinRange(levelMin, levelMax); + while (true) + { + // origin + // {u16 fakeID, u16 fakeID} + // ??? + // levelRand + // {u16 fakePID, u16 fakePID} => you are here (origin) + // {u16 iv1, u16 iv2} + // ability + var preIV = seed; + + var iv1 = XDRNG.Next15(ref seed); + var iv2 = XDRNG.Next15(ref seed); + var iv32 = iv2 << 15 | iv1; + if (criteria.IsSpecifiedHiddenPower() && !criteria.IsSatisfiedHiddenPower(iv32)) + continue; + if (filterIVs && !criteria.IsSatisfiedIVs(iv32)) + continue; + + var lvl16 = XDRNG.Prev2(preIV) >> 16; + var lvlRnd = lvl16 % levelDelta; + var lvl = (byte)(levelMin + lvlRnd); + if (checkLevel && !criteria.IsSatisfiedLevelRange(lvl)) + continue; + + var abil16 = XDRNG.Next3(preIV) >> 16; + var abit = abil16 & 1; // don't care about ability, might be reset on evolution + + SetIVs(pk, iv1, iv2); + pk.RefreshAbility((int)(abit & 1)); + pk.CurrentLevel = pk.MetLevel = lvl; + return; + } + } + + private static void SetIVs(XK3 pk, uint iv1, uint iv2) + => pk.SetIVs(iv2 << 15 | iv1); + + private static uint GetPIDRegular(ref uint seed) + { + var a = XDRNG.Next16(ref seed); + var b = XDRNG.Next16(ref seed); + return GetPIDRegular(a, b); + } + + private static uint GetPIDRegular(uint a, uint b) => a << 16 | b; + + public static bool IsSlotValid(byte slot, uint esv) => GetSlot(esv) == slot; + + public static byte GetSlot(uint esv) => esv switch + { + < 50 => 0, + < 85 => 1, + _ => 2, + }; +} + +public enum PokeSpotSetup : byte +{ + Invalid, + NoBonsly, + Bonsly, +} diff --git a/PKHeX.Core/Legality/RNG/MethodFinder.cs b/PKHeX.Core/Legality/RNG/MethodFinder.cs index bb5dbb45c..4e53233a9 100644 --- a/PKHeX.Core/Legality/RNG/MethodFinder.cs +++ b/PKHeX.Core/Legality/RNG/MethodFinder.cs @@ -589,55 +589,4 @@ private static PIDIV AnalyzeGB(PKM _) // not implemented; correlation between IVs and RNG hasn't been converted to code. return PIDIV.None; } - - public static bool IsPokeSpotActivation(int slot, uint seed, out uint s) - { - s = seed; - var esv = (seed >> 16) % 100; - if (!IsPokeSpotSlotValid(slot, esv)) - { - // todo - } - // check for valid activation - s = XDRNG.Prev(seed); - if ((s >> 16) % 3 != 0) - { - if ((s >> 16) % 100 < 10) // can't fail a Munchlax/Bonsly encounter check - { - // todo - } - s = XDRNG.Prev(s); - if ((s >> 16) % 3 != 0) // can't activate even if generous - { - // todo - } - } - return true; - } - - private static bool IsPokeSpotSlotValid(int slot, uint esv) => slot switch - { - 0 => esv < 50 , // [0,50) - 1 => esv - 50 < 35, // [50,85) - 2 => esv >= 85, // [85,100) - _ => false, - }; - - public static PIDIV GetPokeSpotSeedFirst(PKM pk, byte slot) - { - // Activate (rand % 3) - // Munchlax / Bonsly (10%/30%) - // Encounter Slot Value (ESV) = 50%/35%/15% rarity (0-49, 50-84, 85-99) - - Span seeds = stackalloc uint[XDRNG.MaxCountSeedsPID]; - int count = XDRNG.GetSeeds(seeds, pk.EncryptionConstant); - var reg = seeds[..count]; - foreach (var seed in reg) - { - // check for valid encounter slot info - if (IsPokeSpotActivation(slot, seed, out uint s)) - return new PIDIV(PokeSpot, s); - } - return default; - } } diff --git a/PKHeX.Core/Legality/RNG/PIDGenerator.cs b/PKHeX.Core/Legality/RNG/PIDGenerator.cs index c72b98135..18670a180 100644 --- a/PKHeX.Core/Legality/RNG/PIDGenerator.cs +++ b/PKHeX.Core/Legality/RNG/PIDGenerator.cs @@ -205,25 +205,6 @@ private static uint SetRandomPokeSpotPID(PKM pk, uint seed) return E; } - public static void SetRandomPokeSpotPID(PKM pk, Nature nature, byte gender, int ability, int slot) - { - var rnd = Util.Rand; - while (true) - { - var seed = rnd.Rand32(); - if (!MethodFinder.IsPokeSpotActivation(slot, seed, out var newSeed)) - continue; - - SetRandomPokeSpotPID(pk, newSeed); - pk.SetRandomIVs(); - - if (!IsValidCriteria4(pk, nature, ability, gender)) - continue; - - return; - } - } - public static uint GetMG5ShinyPID(uint gval, uint av, ushort TID16, ushort SID16) { uint pid = ((gval ^ TID16 ^ SID16) << 16) | gval; diff --git a/Tests/PKHeX.Core.Tests/PKM/PIDIVTests.cs b/Tests/PKHeX.Core.Tests/PKM/PIDIVTests.cs index 2e0467056..4360563a6 100644 --- a/Tests/PKHeX.Core.Tests/PKM/PIDIVTests.cs +++ b/Tests/PKHeX.Core.Tests/PKM/PIDIVTests.cs @@ -146,14 +146,9 @@ public void PIDIVMatchingTest5() public void PIDIVPokeSpotTest() { // XD PokeSpots: Check all 3 Encounter Slots (examples are one for each location). - var pkPS0 = new PK3 { PID = 0x7B2D9DA7 }; // Zubat (Cave) - MethodFinder.GetPokeSpotSeedFirst(pkPS0, 0).Type.Should().Be(PIDType.PokeSpot); // PokeSpot encounter info mismatch (Common) - - var pkPS1 = new PK3 { PID = 0x3EE9AF66 }; // Gligar (Rock) - MethodFinder.GetPokeSpotSeedFirst(pkPS1, 1).Type.Should().Be(PIDType.PokeSpot); // PokeSpot encounter info mismatch (Uncommon) - - var pkPS2 = new PK3 { PID = 0x9B667F3C }; // Surskit (Oasis) - MethodFinder.GetPokeSpotSeedFirst(pkPS2, 2).Type.Should().Be(PIDType.PokeSpot); // PokeSpot encounter info mismatch (Rare) + MethodPokeSpot.TryGetOriginSeedPID(0x7B2D9DA7, 0, out _).Should().BeTrue(); // Zubat (Cave) (Common) + MethodPokeSpot.TryGetOriginSeedPID(0x3EE9AF66, 1, out _).Should().BeTrue(); // Gligar (Rock) (Uncommon) + MethodPokeSpot.TryGetOriginSeedPID(0x9B667F3C, 2, out _).Should().BeTrue(); // Surskit (Oasis) (Rare) } [Theory]