Honor more shiny requests in gen6+ encounters

This commit is contained in:
Kurt 2025-07-02 01:08:50 -05:00
parent e69f6b05f8
commit e9d299fc92
23 changed files with 75 additions and 144 deletions

View File

@ -110,6 +110,49 @@ public static ushort GetRandomDVs(Random rand, bool isShiny, sbyte type)
}
}
/// <summary>
/// Generates a random Pokémon ID (PID) based on the provided trainer information, random number generator, and
/// shiny type.
/// </summary>
/// <remarks>The shiny type influences the XOR value applied during PID generation:
/// <list type="bullet">
/// <item><description><see cref="Shiny.AlwaysSquare"/> results in a square shiny PID.</description></item>
/// <item><description><see cref="Shiny.AlwaysStar"/> or <see cref="Shiny.Always"/> results in a star shiny PID.</description></item>
/// <item><description>Other shiny types result in a PID with a random XOR value within the valid range, but never shiny.</description></item>
/// </list>
/// The method ensures that the generated PID adheres to the shiny calculation rules based on the provided trainer's TID and SID.</remarks>
/// <typeparam name="T">The type of the trainer object, which must implement <see cref="ITrainerID32ReadOnly"/>.</typeparam>
/// <param name="tr">The trainer object containing the Trainer ID (TID) and Secret ID (SID) values used for PID generation.</param>
/// <param name="rnd">The random number generator used to produce random values for PID calculation.</param>
/// <param name="type">The shiny type that determines the XOR value used in PID generation.</param>
/// <returns>A 32-bit unsigned integer representing the generated Pokémon ID (PID).</returns>
public static uint GetRandomPID<T>(T tr, Random rnd, Shiny type) where T : ITrainerID32ReadOnly
{
uint pid = rnd.Rand32();
uint xorType = type switch
{
Shiny.Always => (uint)rnd.Next(0, 15 + 1),
Shiny.AlwaysStar => (uint)rnd.Next(1, 15),
Shiny.AlwaysSquare => 0,
_ => (uint)rnd.Next(16, ushort.MaxValue + 1),
};
return ShinyUtil.GetShinyPID(tr.TID16, tr.SID16, pid, xorType);
}
public static uint GetRandomPID<T>(T tr, Random rnd, Shiny encounterShiny, Shiny criteriaShiny) where T : ITrainerID32ReadOnly
{
// Determine actual shiny state to use
var shiny = DetermineFinalShinyState(encounterShiny, criteriaShiny);
return GetRandomPID(tr, rnd, shiny);
}
private static Shiny DetermineFinalShinyState(Shiny template, Shiny criteria) => template switch
{
Shiny.Always when criteria.IsShiny() => criteria, // OK to use
Shiny.Random => criteria, // Can be whatever the criteria is
_ => template, // Use the template shiny state
};
/// <summary>
/// Mashes the IVs into a DV16 value.
/// </summary>

View File

@ -46,7 +46,7 @@ public PB7 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
var date = this.GetRandomValidDate();
var pk = new PB7
{
PID = rnd.Rand32(),
PID = EncounterUtil.GetRandomPID(tr, rnd, Shiny, criteria.Shiny),
EncryptionConstant = rnd.Rand32(),
Species = Species,
Form = Form,

View File

@ -60,7 +60,7 @@ public PK6 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
EggLocation = tr.Version == Version ? Locations.Daycare5 : Locations.LinkTrade6,
EncryptionConstant = rnd.Rand32(),
PID = rnd.Rand32(),
PID = EncounterUtil.GetRandomPID(tr, rnd, criteria.Shiny),
Nature = criteria.GetNature(),
Gender = criteria.GetGender(pi),

View File

@ -1,4 +1,5 @@
using System;
using System.Security.Cryptography;
using static PKHeX.Core.SlotType6;
namespace PKHeX.Core;
@ -107,12 +108,7 @@ private byte GetWildForm(byte form)
private void SetPINGA(PK6 pk, in EncounterCriteria criteria, PersonalInfo6AO pi)
{
var rnd = Util.Rand;
pk.PID = rnd.Rand32();
if (criteria.Shiny.IsShiny())
pk.PID = ShinyUtil.GetShinyPID(pk.TID16, pk.SID16, pk.PID, criteria.Shiny == Shiny.AlwaysSquare ? 0 : (uint)rnd.Next(1, 15));
else if (criteria.Shiny == Shiny.Never && pk.IsShiny)
pk.PID ^= 0x80000000; // flip top bit to ensure non-shiny
pk.PID = EncounterUtil.GetRandomPID(pk, rnd, criteria.Shiny);
pk.EncryptionConstant = rnd.Rand32();
pk.Nature = criteria.GetNature();
pk.Gender = criteria.GetGender(pi);

View File

@ -101,12 +101,7 @@ private byte GetWildForm(byte form)
private void SetPINGA(PK6 pk, in EncounterCriteria criteria, PersonalInfo6XY pi)
{
var rnd = Util.Rand;
pk.PID = rnd.Rand32();
if (criteria.Shiny.IsShiny())
pk.PID = ShinyUtil.GetShinyPID(pk.TID16, pk.SID16, pk.PID, criteria.Shiny == Shiny.AlwaysSquare ? 0 : (uint)rnd.Next(1, 15));
else if (criteria.Shiny == Shiny.Never && pk.IsShiny)
pk.PID ^= 0x80000000; // flip top bit to ensure non-shiny
pk.PID = EncounterUtil.GetRandomPID(pk, rnd, criteria.Shiny);
pk.EncryptionConstant = rnd.Rand32();
pk.Nature = criteria.GetNature();
pk.Gender = criteria.GetGender(pi);

View File

@ -58,7 +58,7 @@ public PK6 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
var pk = new PK6
{
EncryptionConstant = rnd.Rand32(),
PID = rnd.Rand32(),
PID = EncounterUtil.GetRandomPID(tr, rnd, Shiny, criteria.Shiny),
Species = Species,
Form = Form,
CurrentLevel = LevelMin,

View File

@ -73,7 +73,7 @@ public PK6 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
var geo = tr.GetRegionOrigin(language);
var pk = new PK6
{
PID = rnd.Rand32(),
PID = EncounterUtil.GetRandomPID(tr, rnd, Shiny),
EncryptionConstant = rnd.Rand32(),
Species = Species,
CurrentLevel = Level,

View File

@ -58,7 +58,7 @@ public PK7 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
EggLocation = tr.Version == Version ? Locations.Daycare5 : Locations.LinkTrade6,
EncryptionConstant = rnd.Rand32(),
PID = rnd.Rand32(),
PID = EncounterUtil.GetRandomPID(tr, rnd, criteria.Shiny),
Nature = criteria.GetNature(),
Gender = criteria.GetGender(pi),

View File

@ -90,12 +90,7 @@ private byte GetWildForm(byte form)
private void SetPINGA(PK7 pk, in EncounterCriteria criteria, PersonalInfo7 pi)
{
var rnd = Util.Rand;
pk.PID = rnd.Rand32();
if (criteria.Shiny.IsShiny())
pk.PID = ShinyUtil.GetShinyPID(pk.TID16, pk.SID16, pk.PID, criteria.Shiny == Shiny.AlwaysSquare ? 0 : (uint)rnd.Next(1, 15));
else if (criteria.Shiny == Shiny.Never && pk.IsShiny)
pk.PID ^= 0x80000000; // flip top bit to ensure non-shiny
pk.PID = EncounterUtil.GetRandomPID(pk, rnd, criteria.Shiny);
pk.EncryptionConstant = rnd.Rand32();
pk.Nature = criteria.GetNature();
pk.Gender = criteria.GetGender(pi);

View File

@ -1,3 +1,5 @@
using System.Security.Cryptography;
namespace PKHeX.Core;
/// <summary>
@ -107,17 +109,7 @@ private void SetPINGA(PK7 pk, in EncounterCriteria criteria, PersonalInfo7 pi)
{
var rnd = Util.Rand;
pk.EncryptionConstant = rnd.Rand32();
pk.PID = rnd.Rand32();
if (pk.IsShiny)
{
if (Shiny == Shiny.Never || (Shiny != Shiny.Always && !criteria.Shiny.IsShiny()))
pk.PID ^= 0x1000_0000;
}
else if (Shiny == Shiny.Always || (Shiny != Shiny.Never && criteria.Shiny.IsShiny()))
{
var low = pk.PID & 0xFFFF;
pk.PID = ((low ^ pk.TID16 ^ pk.SID16) << 16) | low;
}
pk.PID = EncounterUtil.GetRandomPID(pk, rnd, criteria.Shiny);
if (IVs.IsSpecified)
criteria.SetRandomIVs(pk, IVs);

View File

@ -71,7 +71,7 @@ public PK7 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
var geo = tr.GetRegionOrigin(language);
var pk = new PK7
{
PID = rnd.Rand32(),
PID = EncounterUtil.GetRandomPID(tr, rnd, Shiny),
EncryptionConstant = rnd.Rand32(),
Species = Species,
Form = Form,

View File

@ -63,12 +63,7 @@ public PB7 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
private void SetPINGA(PB7 pk, in EncounterCriteria criteria, PersonalInfo7GG pi)
{
var rnd = Util.Rand;
pk.PID = rnd.Rand32();
if (criteria.Shiny.IsShiny())
pk.PID = ShinyUtil.GetShinyPID(pk.TID16, pk.SID16, pk.PID, criteria.Shiny == Shiny.AlwaysSquare ? 0 : (uint)rnd.Next(1, 15));
else if (criteria.Shiny == Shiny.Never && pk.IsShiny)
pk.PID ^= 0x80000000; // flip top bit to ensure non-shiny
pk.PID = EncounterUtil.GetRandomPID(pk, rnd, criteria.Shiny);
pk.EncryptionConstant = rnd.Rand32();
pk.Nature = criteria.GetNature();
pk.Gender = criteria.GetGender(pi);

View File

@ -71,7 +71,7 @@ public PB7 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
private void SetPINGA(PB7 pk, in EncounterCriteria criteria, PersonalInfo7GG pi)
{
var rnd = Util.Rand;
pk.PID = rnd.Rand32();
pk.PID = EncounterUtil.GetRandomPID(pk, rnd, Shiny, criteria.Shiny);
pk.EncryptionConstant = rnd.Rand32();
pk.Nature = criteria.GetNature();
pk.Gender = criteria.GetGender(pi);

View File

@ -92,7 +92,7 @@ public PB7 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
private void SetPINGA(PB7 pk, in EncounterCriteria criteria, PersonalInfo7GG pi)
{
var rnd = Util.Rand;
pk.PID = rnd.Rand32();
pk.PID = EncounterUtil.GetRandomPID(pk, rnd, Shiny, criteria.Shiny);
pk.EncryptionConstant = rnd.Rand32();
pk.Nature = criteria.GetNature();
pk.Gender = criteria.GetGender(pi);

View File

@ -55,7 +55,7 @@ public PK8 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
EggLocation = tr.Version == Version ? Locations.Daycare5 : Locations.LinkTrade6,
EncryptionConstant = rnd.Rand32(),
PID = rnd.Rand32(),
PID = EncounterUtil.GetRandomPID(tr, rnd, criteria.Shiny),
Nature = criteria.GetNature(),
Gender = criteria.GetGender(pi),
};

View File

@ -93,7 +93,6 @@ private byte GetWildForm(byte form)
private void SetPINGA(PK8 pk, in EncounterCriteria criteria, PersonalInfo8SWSH pi)
{
bool symbol = Parent.PermitCrossover;
var c = symbol ? EncounterCriteria.Unrestricted : criteria;
pk.RefreshAbility(criteria.GetAbilityFromNumber(Ability));
pk.Nature = pk.StatNature = criteria.GetNature();
pk.Gender = criteria.GetGender(pi);
@ -103,11 +102,7 @@ private void SetPINGA(PK8 pk, in EncounterCriteria criteria, PersonalInfo8SWSH p
{
var rand = Util.Rand;
pk.EncryptionConstant = rand.Rand32();
pk.PID = rand.Rand32();
if (criteria.Shiny.IsShiny())
pk.PID = ShinyUtil.GetShinyPID(pk.TID16, pk.SID16, pk.PID, criteria.Shiny == Shiny.AlwaysSquare ? 0 : (uint)rand.Next(1, 15));
else if (criteria.Shiny == Shiny.Never && pk.IsShiny)
pk.PID ^= 0x80000000; // flip top bit to ensure non-shiny
pk.PID = EncounterUtil.GetRandomPID(pk, rand, criteria.Shiny);
pk.HeightScalar = PokeSizeUtil.GetRandomScalar(rand);
pk.WeightScalar = PokeSizeUtil.GetRandomScalar(rand);
@ -115,6 +110,7 @@ private void SetPINGA(PK8 pk, in EncounterCriteria criteria, PersonalInfo8SWSH p
return;
}
// Don't bother honoring shiny state.
var c = symbol ? EncounterCriteria.Unrestricted : criteria;
Overworld8RNG.ApplyDetails(pk, c, Shiny.Random);
}

View File

@ -58,9 +58,14 @@ public PB8 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
MetDate = date,
EggMetDate = date,
// Disassociated from the Egg RNG; PID only is overwritten if re-rolled.
PID = EncounterUtil.GetRandomPID(tr, rnd, criteria.Shiny),
HeightScalar = PokeSizeUtil.GetRandomScalar(rnd),
WeightScalar = PokeSizeUtil.GetRandomScalar(rnd)
};
SetPINGA(pk, criteria, pi, Util.Rand32());
SetPINGA(pk, criteria, pi, rnd.Rand32());
SetEncounterMoves(pk);
return pk;
@ -170,15 +175,10 @@ private void SetPINGA(PB8 pk, in EncounterCriteria criteria, PersonalInfo8BDSP p
// When generating, the game first generates a template, unrelated from the egg seed.
// This unrelated PID can be retained if the breeding does not use Masuda Method or Shiny Charm re-rolls.
// Height and Weight are also unrelated, via the template.
var templateRand = Util.Rand;
var pid = templateRand.Rand32();
pk.HeightScalar = PokeSizeUtil.GetRandomScalar(templateRand);
pk.WeightScalar = PokeSizeUtil.GetRandomScalar(templateRand);
// For eggs, we'll "randomly" get the right PID via template roll before ever generating the rest of the egg.
// Set the rest of the values as per our generating via the egg seed.
// For eggs, if the PID isn't the desired shiny type, we'll say it was traded from a suitable trainer where it happened to be shiny.
pk.EncryptionConstant = rng.NextUInt(); // PID would be re-rolled after here, but we aren't going to have re-rolls in our hypothetical setup.
pk.PID = GetFinalPID(pk, pid, criteria); // PID dissociated completely (see above)
pk.SetIVs(ivs);
pk.StatNature = pk.Nature = criteria.GetNature(); // Everstone (see above)
pk.Gender = gender;
@ -255,70 +255,4 @@ private static void GetFinalIVs(ref Xoroshiro128Plus8b rng, Span<int> result)
result[i] = (int)baseIV;
}
}
private static uint GetFinalPID(PB8 pk, uint pid, EncounterCriteria criteria)
{
// Assume no Masuda Method, no Shiny Charm; only 1 roll.
var id32 = pk.ID32;
var isShiny = ShinyUtil.GetIsShiny6(id32, pid);
// Ensure the shiny is the correct state.
if (!criteria.Shiny.IsShiny())
{
if (!isShiny)
return pid;
// Must be traded from another game that it was not shiny in.
pid ^= 0x1000_0000;
ForceTradedEgg(pk, pid, uint.MaxValue);
return pid; // Force non-shiny by flipping the shiny bit.
}
// Want a shiny, less likely.
var wantSquare = criteria.Shiny is Shiny.AlwaysSquare;
if (!isShiny)
return ForceShiny(pk, wantSquare, pid);
// Already shiny, but might not be the desired XOR type.
var xor = ShinyUtil.GetShinyXor(pid, id32);
if (wantSquare ? xor != 0 : xor == 0) // wrong type
return ForceShiny(pk, wantSquare, pid);
// Right shiny type.
return pid;
}
private static uint ForceShiny(PB8 pk, bool wantSquare, uint pid)
{
// Must be traded from another game that it just happened to be shiny.
var xorType = wantSquare ? 0u : (uint)Util.Rand.Next(1, 16);
pid = ShinyUtil.GetShinyPID(pk.TID16, pk.SID16, pid, xorType);
ForceTradedEgg(pk, pid, xorType);
return pid;
}
private static void ForceTradedEgg(PB8 pk, uint pid, uint xorType)
{
if (pk.IsUntraded)
{
pk.OriginalTrainerTrash.CopyTo(pk.HandlingTrainerTrash);
pk.HandlingTrainerGender = pk.OriginalTrainerGender;
pk.HandlingTrainerLanguage = (byte)pk.Language;
}
if (!pk.IsEgg)
{
pk.EggLocation = Locations.LinkTrade6NPC;
return;
}
pk.MetLocation = Locations.LinkTrade6NPC; // Egg wasn't the right state, so force traded.
pk.MetDate = EncounterDate.GetDateSwitch(); // Update met date to the trade date.
// Need to determine a fake Trainer ID that allows us to be a traded egg with the right XOR type.
var rand = Util.Rand;
var id32 = rand.Rand32();
if (xorType >= 16) // not shiny desired; this randomness is deferred to here since the hot path is not-IsEgg early return above.
xorType = (uint)rand.Next(16, ushort.MaxValue + 1);
// Use the PID as if it were the TID/SID so the result is instead our TID/SID with resulting xorType.
pk.ID32 = ShinyUtil.GetShinyPID((ushort)(pid << 16), (ushort)pid, id32, xorType);
}
}

View File

@ -96,12 +96,7 @@ public PB8 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
private void SetPINGA(PB8 pk, in EncounterCriteria criteria, PersonalInfo8BDSP pi)
{
var rnd = Util.Rand;
pk.PID = rnd.Rand32();
if (criteria.Shiny.IsShiny())
pk.PID = ShinyUtil.GetShinyPID(pk.TID16, pk.SID16, pk.PID, criteria.Shiny == Shiny.AlwaysSquare ? 0 : (uint)rnd.Next(1, 15));
else if (criteria.Shiny == Shiny.Never && pk.IsShiny)
pk.PID ^= 0x80000000; // flip top bit to ensure non-shiny
pk.PID = EncounterUtil.GetRandomPID(pk, rnd, criteria.Shiny);
pk.EncryptionConstant = rnd.Rand32();
criteria.SetRandomIVs(pk);
pk.Nature = pk.StatNature = criteria.GetNature();

View File

@ -56,7 +56,7 @@ public PK9 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
EggLocation = tr.Version == Version ? Locations.Picnic9 : Locations.LinkTrade6,
EncryptionConstant = rnd.Rand32(),
PID = rnd.Rand32(),
PID = EncounterUtil.GetRandomPID(tr, rnd, criteria.Shiny),
Nature = criteria.GetNature(),
Gender = criteria.GetGender(pi),
};

View File

@ -113,7 +113,7 @@ public PK9 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
private void SetPINGA(PK9 pk, in EncounterCriteria criteria, PersonalInfo9SV pi)
{
var rnd = Util.Rand;
pk.PID = rnd.Rand32();
pk.PID = EncounterUtil.GetRandomPID(pk, rnd, criteria.Shiny);
pk.EncryptionConstant = rnd.Rand32();
pk.Nature = pk.StatNature = criteria.GetNature();
pk.Gender = criteria.GetGender(pi);

View File

@ -111,7 +111,7 @@ public PK9 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
private void SetPINGA(PK9 pk, in EncounterCriteria criteria, PersonalInfo9SV pi)
{
var rnd = Util.Rand;
pk.PID = rnd.Rand32();
pk.PID = EncounterUtil.GetRandomPID(pk, rnd, criteria.Shiny);
pk.EncryptionConstant = rnd.Rand32();
pk.Nature = pk.StatNature = criteria.GetNature();
pk.Gender = criteria.GetGender(pi);

View File

@ -171,12 +171,7 @@ private byte GetWildForm(byte form)
private void SetPINGA(PK9 pk, in EncounterCriteria criteria, PersonalInfo9SV pi)
{
var rnd = Util.Rand;
pk.PID = rnd.Rand32();
if (criteria.Shiny.IsShiny())
pk.PID = ShinyUtil.GetShinyPID(pk.TID16, pk.SID16, pk.PID, criteria.Shiny == Shiny.AlwaysSquare ? 0 : (uint)rnd.Next(1, 15));
else if (criteria.Shiny == Shiny.Never && pk.IsShiny)
pk.PID ^= 0x80000000; // flip top bit to ensure non-shiny
pk.PID = EncounterUtil.GetRandomPID(pk, rnd, criteria.Shiny);
pk.EncryptionConstant = rnd.Rand32();
criteria.SetRandomIVs(pk);

View File

@ -128,12 +128,7 @@ public PK9 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
private void SetPINGA(PK9 pk, in EncounterCriteria criteria, PersonalInfo9SV pi)
{
var rnd = Util.Rand;
pk.PID = rnd.Rand32();
if (criteria.Shiny.IsShiny())
pk.PID = ShinyUtil.GetShinyPID(pk.TID16, pk.SID16, pk.PID, criteria.Shiny == Shiny.AlwaysSquare ? 0 : (uint)rnd.Next(1, 15));
else if (criteria.Shiny == Shiny.Never && pk.IsShiny)
pk.PID ^= 0x80000000; // flip top bit to ensure non-shiny
pk.PID = EncounterUtil.GetRandomPID(pk, rnd, Shiny, criteria.Shiny);
pk.EncryptionConstant = rnd.Rand32();
pk.Nature = pk.StatNature = criteria.GetNature(Nature);
pk.Gender = criteria.GetGender(Gender, pi);