diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterSlot4.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterSlot4.cs
index 7735a4f76..9086a6ed2 100644
--- a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterSlot4.cs
+++ b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterSlot4.cs
@@ -182,7 +182,7 @@ public bool IsCompatible(PIDType type, PKM pk)
if (type is PIDType.ChainShiny)
return pk.IsShiny && CanUseRadar;
if (type is PIDType.CuteCharm)
- return MethodFinder.IsCuteCharm4Valid(this, pk);
+ return CuteCharm4.IsValid(this, pk);
return false;
}
diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4.cs
index e4f547bdd..eb490e12c 100644
--- a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4.cs
+++ b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4.cs
@@ -394,7 +394,7 @@ public bool IsCompatible(PIDType type, PKM pk)
if (type is PIDType.Method_1)
return true;
if (type is PIDType.CuteCharm)
- return MethodFinder.IsCuteCharm4Valid(this, pk);
+ return CuteCharm4.IsValid(this, pk);
return false;
}
diff --git a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4Pokewalker.cs b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4Pokewalker.cs
index cebce5670..18a4bfbac 100644
--- a/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4Pokewalker.cs
+++ b/PKHeX.Core/Legality/Encounters/Templates/Gen4/EncounterStatic4Pokewalker.cs
@@ -201,7 +201,7 @@ public bool IsCompatible(PIDType type, PKM pk)
// Pokewalker can sometimes be confused with CuteCharm due to the PID creation routine. Double check if it is okay.
if (type is PIDType.CuteCharm)
- return MethodFinder.IsCuteCharm(pk, pk.EncryptionConstant) && MethodFinder.IsCuteCharm4Valid(this, pk);
+ return CuteCharm4.IsCuteCharm(pk, pk.EncryptionConstant) && CuteCharm4.IsValid(this, pk);
return false;
}
diff --git a/PKHeX.Core/Legality/RNG/ClassicEra/LeadFinder.cs b/PKHeX.Core/Legality/RNG/ClassicEra/LeadFinder.cs
index edf4b42df..09bf20d06 100644
--- a/PKHeX.Core/Legality/RNG/ClassicEra/LeadFinder.cs
+++ b/PKHeX.Core/Legality/RNG/ClassicEra/LeadFinder.cs
@@ -45,7 +45,7 @@ public static class LeadFinder
// There's a very-very rare chance that the PID-IV can be from Cute Charm too.
// It may match Method 1, but since we early-return, we don't check for Cute Charm.
// So, we check for Cute Charm here and try checking Cute Charm frames if it matches.
- if (MethodFinder.IsCuteCharm(pk, pk.EncryptionConstant))
+ if (CuteCharm4.IsCuteCharm(pk, pk.EncryptionConstant))
type = CuteCharm;
}
else if (type is Method_1)
@@ -63,7 +63,7 @@ public static class LeadFinder
// There's a very-very rare chance that the PID-IV can be from Cute Charm too.
// It may match Method 1, but since we early-return, we don't check for Cute Charm.
// So, we check for Cute Charm here and try checking Cute Charm frames if it matches.
- if (MethodFinder.IsCuteCharm(pk, pk.EncryptionConstant))
+ if (CuteCharm4.IsCuteCharm(pk, pk.EncryptionConstant))
type = CuteCharm;
}
if (type is CuteCharm)
diff --git a/PKHeX.Core/Legality/RNG/CuteCharm4.cs b/PKHeX.Core/Legality/RNG/CuteCharm4.cs
new file mode 100644
index 000000000..67c2594d9
--- /dev/null
+++ b/PKHeX.Core/Legality/RNG/CuteCharm4.cs
@@ -0,0 +1,130 @@
+using static PKHeX.Core.Species;
+
+namespace PKHeX.Core;
+
+///
+/// PID generating logic for Generation 4's mechanic.
+///
+public static class CuteCharm4
+{
+ private const uint NatureCount = 25;
+
+ ///
+ /// Gets the PID of a Male wild encounter when the player leads a Female Pokémon with Cute Charm.
+ ///
+ /// Gender ratio of the encounter species.
+ /// Nature of the encounter Pokémon.
+ public static uint GetPIDMale(byte genderRatio, uint nature)
+ {
+ // pid >= ratio is male
+ // get the lowest PID that will be male for Hardy (0)
+ var basePID = NatureCount * ((genderRatio / NatureCount) + 1);
+ // add the desired nature to the base PID
+ return basePID + nature;
+ }
+
+ ///
+ /// Gets the PID of a Female wild encounter when the player leads a Male Pokémon with Cute Charm.
+ ///
+ /// Nature of the encounter Pokémon.
+ public static uint GetPIDFemale(uint nature) => nature;
+
+ ///
+ /// Gets the Gender Ratio of the species. If the species is not in Gen 4, it will grab whatever its de-evolved species is.
+ ///
+ private static byte GetGenderRatio(ushort species)
+ {
+ return species <= Legal.MaxSpeciesID_4
+ ? PersonalTable.HGSS[species].Gender
+ : PKX.GetGenderRatio(species); // fallback (don't bother trying to devolve to Gen1-4 encounter species)
+ }
+
+ private static bool IsAzurillMale(uint pid) => pid is >= 0xC8 and <= 0xE0;
+
+ ///
+ /// Checks if the can have Cute Charm as its PID type. Special consideration for male Azurill encounters, which can change into female.
+ ///
+ /// Encounter (species)
+ /// Current entity state
+ /// True if the Cute Charm type is valid
+ internal static bool IsValid(TEnc enc, PKM pk) where TEnc : ISpeciesForm
+ {
+ if (pk.Gender is not (0 or 1))
+ return pk.Species == (ushort)Shedinja;
+ if (pk.Species is not ((int)Marill or (int)Azumarill))
+ return true;
+ if (!IsAzurillMale(pk.EncryptionConstant)) // recognized as not Azurill
+ return true;
+ return enc.Species == (int)Azurill; // encounter must be male Azurill
+ }
+
+ ///
+ /// Checks if the PID can arise as a result of Cute Charm.
+ ///
+ public static bool IsCuteCharm(PKM pk, uint pid)
+ {
+ if (pid > 0xFF)
+ return false;
+
+ var (species, gender) = GetGenderSpecies(pk, pid, pk.Species);
+
+ switch (gender)
+ {
+ // case 2: break; // can't cute charm a genderless pk
+ case 0: // male
+ var gr = GetGenderRatio(species);
+ if (gr >= PersonalInfo.RatioMagicFemale) // no modification for PID
+ break;
+ var nature = pid % NatureCount;
+ var expect = GetPIDMale(gr, nature);
+ if (pid != expect)
+ break;
+ return true;
+ case 1: // female
+ if (pid >= NatureCount)
+ break; // nope, this isn't a valid nature
+ if (GetGenderRatio(species) >= PersonalInfo.RatioMagicFemale) // no modification for PID
+ break;
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// There are some edge cases when the gender ratio changes across evolutions.
+ ///
+ private static (ushort Species, byte Gender) GetGenderSpecies(PKM pk, uint pid, ushort currentSpecies) => currentSpecies switch
+ {
+ // Nincada evo chain travels from M/F -> Genderless Shedinja
+ (int)Shedinja => ((int)Nincada, EntityGender.GetFromPID((int)Nincada, pid)),
+
+ // These evolved species cannot be encountered with cute charm.
+ // 100% fixed gender does not modify PID; override this with the encounter species for correct calculation.
+ // We can assume the re-mapped species' [gender ratio] is what was encountered.
+ (int)Wormadam => ((int)Burmy, 1),
+ (int)Mothim => ((int)Burmy, 0),
+ (int)Vespiquen => ((int)Combee, 1),
+ (int)Gallade => ((int)Kirlia, 0),
+ (int)Froslass => ((int)Snorunt, 1),
+ // Azurill & Marill/Azumarill collision
+ // Changed gender ratio (25% M -> 50% M) needs special treatment.
+ // Double-check the encounter species with IsValid afterward.
+ (int)Marill or (int)Azumarill when IsAzurillMale(pid) => ((int)Azurill, 0),
+
+ // Future evolutions
+ _ => (GetSpeciesGen4(currentSpecies), pk.Gender),
+ };
+
+ private static ushort GetSpeciesGen4(ushort species) => species switch
+ {
+ <= Legal.MaxSpeciesID_4 => species, // has a valid personal reference, all good
+ (int)Sylveon => (int)Eevee,
+ (int)MrRime => (int)MrMime,
+ (int)Wyrdeer => (int)Stantler,
+ (int)Kleavor => (int)Scyther,
+ (int)Sneasler => (int)Sneasel,
+ (int)Ursaluna => (int)Ursaring,
+ (int)Annihilape => (int)Primeape,
+ _ => species, // throw an exception? Hitting here is an invalid case.
+ };
+}
diff --git a/PKHeX.Core/Legality/RNG/MethodFinder.cs b/PKHeX.Core/Legality/RNG/MethodFinder.cs
index 5e1b005a4..bb5dbb45c 100644
--- a/PKHeX.Core/Legality/RNG/MethodFinder.cs
+++ b/PKHeX.Core/Legality/RNG/MethodFinder.cs
@@ -396,48 +396,12 @@ private static bool GetG5MGShinyMatch(PKM pk, uint pid, out PIDIV pidiv)
private static bool GetCuteCharmMatch(PKM pk, uint pid, out PIDIV pidiv)
{
- if (!IsCuteCharm(pk, pid))
+ if (!CuteCharm4.IsCuteCharm(pk, pid))
return GetNonMatch(out pidiv);
pidiv = PIDIV.CuteCharm;
return true;
}
- public static bool IsCuteCharm(PKM pk, uint pid)
- {
- if (pid > 0xFF)
- return false;
-
- var (species, gender) = GetCuteCharmGenderSpecies(pk, pid, pk.Species);
- static byte getRatio(ushort species)
- {
- return species <= Legal.MaxSpeciesID_4
- ? PersonalTable.HGSS[species].Gender
- : PKX.GetGenderRatio(species);
- }
-
- const uint n = 25;
- switch (gender)
- {
- // case 2: break; // can't cute charm a genderless pk
- case 0: // male
- var gr = getRatio(species);
- if (gr >= PersonalInfo.RatioMagicFemale) // no modification for PID
- break;
- var rate = n * ((gr / n) + 1); // buffered
- var nature = pid % n;
- if (nature + rate != pid)
- break;
- return true;
- case 1: // female
- if (pid >= n)
- break; // nope, this isn't a valid nature
- if (getRatio(species) >= PersonalInfo.RatioMagicFemale) // no modification for PID
- break;
- return true;
- }
- return false;
- }
-
private static bool GetChainShinyMatch(Span seeds, ITrainerID32 pk, uint pid, uint iv1, uint iv2, out PIDIV pidiv)
{
// 13 shiny bits
@@ -550,8 +514,8 @@ private static bool GetPokewalkerMatch(PKM pk, uint actualPID, out PIDIV pidiv)
if (mid is not (0 or midMask))
return GetNonMatch(out pidiv);
- // Quirky Nature is not possible with the algorithm.
var nature = actualPID % 25;
+ // Quirky Nature is not possible with the algorithm.
if (nature == 24)
return GetNonMatch(out pidiv);
@@ -659,57 +623,6 @@ public static bool IsPokeSpotActivation(int slot, uint seed, out uint s)
_ => false,
};
- internal static bool IsCuteCharm4Valid(ISpeciesForm enc, PKM pk)
- {
- if (pk.Gender is not (0 or 1))
- return pk.Species == (ushort)Species.Shedinja;
- if (pk.Species is not ((int)Species.Marill or (int)Species.Azumarill))
- return true;
- if (!IsCuteCharmAzurillMale(pk.PID)) // recognized as not Azurill
- return true;
- return enc.Species == (int)Species.Azurill; // encounter must be male Azurill
- }
-
- private static bool IsCuteCharmAzurillMale(uint pid) => pid is >= 0xC8 and <= 0xE0;
-
- ///
- /// There are some edge cases when the gender ratio changes across evolutions.
- ///
- private static (ushort Species, byte Gender) GetCuteCharmGenderSpecies(PKM pk, uint pid, ushort currentSpecies) => currentSpecies switch
- {
- // Nincada evo chain travels from M/F -> Genderless Shedinja
- (int)Species.Shedinja => ((int)Species.Nincada, EntityGender.GetFromPID((int)Species.Nincada, pid)),
-
- // These evolved species cannot be encountered with cute charm.
- // 100% fixed gender does not modify PID; override this with the encounter species for correct calculation.
- // We can assume the re-mapped species' [gender ratio] is what was encountered.
- (int)Species.Wormadam => ((int)Species.Burmy, 1),
- (int)Species.Mothim => ((int)Species.Burmy, 0),
- (int)Species.Vespiquen => ((int)Species.Combee, 1),
- (int)Species.Gallade => ((int)Species.Kirlia, 0),
- (int)Species.Froslass => ((int)Species.Snorunt, 1),
- // Azurill & Marill/Azumarill collision
- // Changed gender ratio (25% M -> 50% M) needs special treatment.
- // Double-check the encounter species with IsCuteCharm4Valid afterward.
- (int)Species.Marill or (int)Species.Azumarill when IsCuteCharmAzurillMale(pid) => ((int)Species.Azurill, 0),
-
- // Future evolutions
- _ => GetCuteCharmSpeciesGen4(currentSpecies, pk.Gender),
- };
-
- private static (ushort Species, byte Gender) GetCuteCharmSpeciesGen4(ushort species, byte gender) => species switch
- {
- <= Legal.MaxSpeciesID_4 => (species, gender), // has a valid personal reference, all good
- (int)Species.Sylveon => ((int)Species.Eevee, gender),
- (int)Species.MrRime => ((int)Species.MrMime, gender),
- (int)Species.Wyrdeer => ((int)Species.Stantler, gender),
- (int)Species.Kleavor => ((int)Species.Scyther, gender),
- (int)Species.Sneasler => ((int)Species.Sneasel, gender),
- (int)Species.Ursaluna => ((int)Species.Ursaring, gender),
- (int)Species.Annihilape => ((int)Species.Primeape, gender),
- _ => (species, gender), // throw an exception? Hitting here is an invalid case.
- };
-
public static PIDIV GetPokeSpotSeedFirst(PKM pk, byte slot)
{
// Activate (rand % 3)