PKHeX/PKHeX.Core/Legality/RNG/Roaming8bRNG.cs
Kurt 47071b41f3
Refactoring: Span-based value writes and method signatures (#3361)
Existing `get`/`set` logic is flawed in that it doesn't work on Big Endian operating systems, and it allocates heap objects when it doesn't need to.

`System.Buffers.Binary.BinaryPrimitives` in the `System.Memory` NuGet package provides both Little Endian and Big Endian methods to read and write data; all the `get`/`set` operations have been reworked to use this new API. This removes the need for PKHeX's manual `BigEndian` class, as all functions are already covered by the BinaryPrimitives API.

The `StringConverter` has now been rewritten to accept a Span to read from & write to, no longer requiring a temporary StringBuilder.

Other Fixes included:
- The Super Training UI for Gen6 has been reworked according to the latest block structure additions.
- Cloning a Stadium2 Save File now works correctly (opening from the Folder browser list).
- Checksum & Sanity properties removed from parent PKM class, and is now implemented via interface.
2022-01-02 21:35:59 -08:00

274 lines
9.4 KiB
C#

using System;
namespace PKHeX.Core
{
/// <summary>
/// Contains logic for the Generation 8b (BD/SP) roaming spawns.
/// </summary>
/// <remarks>
/// Roaming encounters use the pokemon's 32-bit <see cref="PKM.EncryptionConstant"/> as RNG seed.
/// </remarks>
public static class Roaming8bRNG
{
private const int NoMatchIVs = -1;
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
{
var seed = Util.Rand32(rnd);
if (seed == int.MaxValue)
continue; // Unity's Rand is [int.MinValue, int.MaxValue)
if (TryApplyFromSeed(pk, criteria, shiny, flawless, seed))
return;
} while (++ctr != maxAttempts);
TryApplyFromSeed(pk, EncounterCriteria.Unrestricted, shiny, flawless, Util.Rand32(rnd));
}
private static bool TryApplyFromSeed(PKM pk, EncounterCriteria criteria, Shiny shiny, int flawless, uint seed)
{
var xoro = new Xoroshiro128Plus8b(seed);
// Encryption Constant
pk.EncryptionConstant = seed;
// PID
var fakeTID = xoro.NextUInt(); // fakeTID
var pid = xoro.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<int> ivs = stackalloc [] { UNSET, UNSET, UNSET, UNSET, UNSET, UNSET };
const int MAX = 31;
var determined = 0;
while (determined < flawless)
{
var idx = (int)xoro.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)xoro.NextUInt(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)xoro.NextUInt(2));
// Remainder
var scale = (IScaledSize)pk;
scale.HeightScalar = (int)xoro.NextUInt(0x81) + (int)xoro.NextUInt(0x80);
scale.WeightScalar = (int)xoro.NextUInt(0x81) + (int)xoro.NextUInt(0x80);
return true;
}
public static bool ValidateRoamingEncounter(PKM pk, Shiny shiny = Shiny.Random, int flawless = 0)
{
var seed = pk.EncryptionConstant;
if (seed == int.MaxValue)
return false; // Unity's Rand is [int.MinValue, int.MaxValue)
var xoro = new Xoroshiro128Plus8b(seed);
// Check PID
var fakeTID = xoro.NextUInt(); // fakeTID
var pid = xoro.NextUInt();
pid = GetRevisedPID(fakeTID, pid, pk);
if (pk.PID != pid)
return false;
// Check IVs: Create flawless IVs at random indexes, then the random IVs for not flawless.
Span<int> ivs = stackalloc [] { UNSET, UNSET, UNSET, UNSET, UNSET, UNSET };
var determined = 0;
while (determined < flawless)
{
var idx = (int)xoro.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)xoro.NextUInt(31 + 1);
}
if (ivs[0] != pk.IV_HP ) return false;
if (ivs[1] != pk.IV_ATK) return false;
if (ivs[2] != pk.IV_DEF) return false;
if (ivs[3] != pk.IV_SPA) return false;
if (ivs[4] != pk.IV_SPD) return false;
if (ivs[5] != pk.IV_SPE) return false;
// Don't check Hidden ability, as roaming encounters are 1/2 only.
if (pk.AbilityNumber != (1 << (int)xoro.NextUInt(2)))
return false;
return GetIsMatchEnd(pk, xoro) || GetIsMatchEndWithCuteCharm(pk, xoro) || GetIsMatchEndWithSynchronize(pk, xoro);
}
private static bool GetIsMatchEnd(PKM pk, Xoroshiro128Plus8b xoro)
{
// Check that gender matches
var genderRatio = PersonalTable.BDSP.GetFormEntry(pk.Species, pk.Form).Gender;
if (genderRatio == PersonalInfo.RatioMagicGenderless)
{
if (pk.Gender != (int)Gender.Genderless)
return false;
}
else if (genderRatio == PersonalInfo.RatioMagicMale)
{
if (pk.Gender != (int)Gender.Male)
return false;
}
else if (genderRatio == PersonalInfo.RatioMagicFemale)
{
if (pk.Gender != (int)Gender.Female)
return false;
}
else
{
if (pk.Gender != (((int)xoro.NextUInt(253) + 1 < genderRatio) ? 1 : 0))
return false;
}
// Check that the nature matches
if (pk.Nature != (int)xoro.NextUInt(25))
return false;
return GetIsHeightWeightMatch(pk, xoro);
}
private static bool GetIsMatchEndWithCuteCharm(PKM pk, Xoroshiro128Plus8b xoro)
{
// Check that gender matches
// Assume that the gender is a match due to cute charm.
// Check that the nature matches
if (pk.Nature != (int)xoro.NextUInt(25))
return false;
return GetIsHeightWeightMatch(pk, xoro);
}
private static bool GetIsMatchEndWithSynchronize(PKM pk, Xoroshiro128Plus8b xoro)
{
// Check that gender matches
var genderRatio = PersonalTable.BDSP.GetFormEntry(pk.Species, pk.Form).Gender;
if (genderRatio == PersonalInfo.RatioMagicGenderless)
{
if (pk.Gender != (int)Gender.Genderless)
return false;
}
else if (genderRatio == PersonalInfo.RatioMagicMale)
{
if (pk.Gender != (int)Gender.Male)
return false;
}
else if (genderRatio == PersonalInfo.RatioMagicFemale)
{
if (pk.Gender != (int)Gender.Female)
return false;
}
else
{
if (pk.Gender != (((int)xoro.NextUInt(253) + 1 < genderRatio) ? 1 : 0))
return false;
}
// Assume that the nature is a match due to synchronize.
return GetIsHeightWeightMatch(pk, xoro);
}
private static bool GetIsHeightWeightMatch(PKM pk, Xoroshiro128Plus8b xoro)
{
// Check height/weight
if (pk is not IScaledSize s)
return false;
var height = xoro.NextUInt(0x81) + xoro.NextUInt(0x80);
var weight = xoro.NextUInt(0x81) + xoro.NextUInt(0x80);
return s.HeightScalar == height && s.WeightScalar == weight;
}
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;
}
}
}