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)