Extract CuteCharm4 api

This commit is contained in:
Kurt
2025-02-23 11:19:03 -06:00
parent 8ac937b83c
commit a1ea915b52
6 changed files with 137 additions and 94 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -0,0 +1,130 @@
using static PKHeX.Core.Species;
namespace PKHeX.Core;
/// <summary>
/// PID generating logic for Generation 4's <see cref="Ability.CuteCharm"/> mechanic.
/// </summary>
public static class CuteCharm4
{
private const uint NatureCount = 25;
/// <summary>
/// Gets the PID of a Male wild encounter when the player leads a Female Pokémon with Cute Charm.
/// </summary>
/// <param name="genderRatio">Gender ratio of the encounter species.</param>
/// <param name="nature">Nature of the encounter Pokémon.</param>
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;
}
/// <summary>
/// Gets the PID of a Female wild encounter when the player leads a Male Pokémon with Cute Charm.
/// </summary>
/// <param name="nature">Nature of the encounter Pokémon.</param>
public static uint GetPIDFemale(uint nature) => nature;
/// <summary>
/// Gets the Gender Ratio of the species. If the species is not in Gen 4, it will grab whatever its de-evolved species is.
/// </summary>
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;
/// <summary>
/// Checks if the <see cref="pk"/> can have Cute Charm as its PID type. Special consideration for male Azurill encounters, which can change into female.
/// </summary>
/// <param name="enc">Encounter (species)</param>
/// <param name="pk">Current entity state</param>
/// <returns>True if the Cute Charm type is valid</returns>
internal static bool IsValid<TEnc>(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
}
/// <summary>
/// Checks if the PID can arise as a result of Cute Charm.
/// </summary>
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;
}
/// <summary>
/// There are some edge cases when the gender ratio changes across evolutions.
/// </summary>
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.
};
}

View File

@@ -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<uint> 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;
/// <summary>
/// There are some edge cases when the gender ratio changes across evolutions.
/// </summary>
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)