mirror of
https://github.com/kwsch/PKHeX.git
synced 2026-04-25 16:35:02 -05:00
253 lines
9.9 KiB
C#
253 lines
9.9 KiB
C#
using System;
|
|
using static PKHeX.Core.LegalityCheckResultCode;
|
|
|
|
namespace PKHeX.Core;
|
|
|
|
/// <summary>
|
|
/// Verifies the <see cref="PKM.EncryptionConstant"/>.
|
|
/// </summary>
|
|
public sealed class PIDVerifier : Verifier
|
|
{
|
|
protected override CheckIdentifier Identifier => CheckIdentifier.PID;
|
|
|
|
public override void Verify(LegalityAnalysis data)
|
|
{
|
|
var pk = data.Entity;
|
|
if (pk.Format >= 6)
|
|
VerifyEC(data);
|
|
|
|
var enc = data.EncounterMatch;
|
|
if (enc.Species == (int)Species.Wurmple)
|
|
VerifyECPIDWurmple(data);
|
|
else if (enc.Species is (int)Species.Tandemaus or (int)Species.Dunsparce)
|
|
VerifyEC100(data, enc.Species);
|
|
|
|
if (pk.PID == 0)
|
|
data.AddLine(Get(Severity.Fishy, PIDZero));
|
|
if (!pk.Nature.IsFixed()) // out of range
|
|
data.AddLine(GetInvalid(PIDNatureMismatch));
|
|
if (data.Info.EncounterMatch is IEncounterEgg egg)
|
|
VerifyEggPID(data, pk, egg);
|
|
|
|
VerifyShiny(data);
|
|
}
|
|
|
|
private static void VerifyEggPID(LegalityAnalysis data, PKM pk, IEncounterEgg egg)
|
|
{
|
|
if (egg is EncounterEgg5)
|
|
{
|
|
// Gen5 eggs use rand(0xFFFFFFFF), which never yields 0xFFFFFFFF (max 0xFFFFFFFE).
|
|
// Masuda Method does the same as the original PID roll. PID is never re-rolled a different way.
|
|
if (pk.EncryptionConstant == uint.MaxValue)
|
|
data.AddLine(Get(CheckIdentifier.EC, Severity.Invalid, PIDEncryptZero));
|
|
}
|
|
else if (egg is EncounterEgg4)
|
|
{
|
|
// Gen4 Eggs are "egg available" based on the stored PID value in the save file.
|
|
// If this value is 0 or is generated as 0 (possible), the game will see "false" and no egg is available.
|
|
// Only a non-zero value is possible to obtain.
|
|
// However, With Masuda Method, the egg PID is re-rolled with the ARNG (until shiny, at most 4 times) upon receipt.
|
|
// None of the un-rolled states share the same shiny-xor as PID=0, you can re-roll into an all-zero PID.
|
|
// Flag it as fishy, because more often than not, it is hacked rather than a legitimately obtained egg.
|
|
if (pk.EncryptionConstant == 0)
|
|
data.AddLine(Get(CheckIdentifier.EC, Severity.Fishy, PIDEncryptZero));
|
|
|
|
if (Breeding.IsGenderSpeciesDetermination(egg.Species))
|
|
VerifyEggGender8000(data, pk);
|
|
}
|
|
else if (egg is EncounterEgg3)
|
|
{
|
|
if (!Daycare3.IsValidProcPID(pk.EncryptionConstant, egg.Version))
|
|
data.AddLine(Get(CheckIdentifier.EC, Severity.Invalid, PIDEncryptZero));
|
|
|
|
if (Breeding.IsGenderSpeciesDetermination(egg.Species))
|
|
VerifyEggGender8000(data, pk);
|
|
// PID and IVs+Inheritance randomness is sufficiently random; any permutation of vBlank correlations is possible.
|
|
}
|
|
}
|
|
|
|
private static void VerifyEggGender8000(LegalityAnalysis data, PKM pk)
|
|
{
|
|
var gender = pk.Gender;
|
|
if (Breeding.IsValidSpeciesBit34(pk.EncryptionConstant, gender))
|
|
return; // 50/50 chance!
|
|
if (gender == 1 || IsEggBitRequiredMale34(data.Info.Moves))
|
|
data.AddLine(GetInvalid(CheckIdentifier.EC, PIDGenderMismatch));
|
|
}
|
|
|
|
private void VerifyShiny(LegalityAnalysis data)
|
|
{
|
|
var pk = data.Entity;
|
|
var enc = data.EncounterMatch;
|
|
|
|
if (!enc.Shiny.IsValid(pk))
|
|
data.AddLine(GetInvalid(CheckIdentifier.Shiny, EncStaticPIDShiny));
|
|
|
|
switch (enc)
|
|
{
|
|
// Forced PID or generated without an encounter
|
|
// Crustle has 0x80 for its StartWildBattle flag; dunno what it does, but sometimes it doesn't align with the expected PID xor.
|
|
case EncounterStatic5 { IsWildCorrelationPID: true }:
|
|
VerifyG5PID_IDCorrelation(data);
|
|
break;
|
|
case EncounterSlot5 {IsHiddenGrotto: true} when pk.IsShiny:
|
|
data.AddLine(GetInvalid(CheckIdentifier.Shiny, G5PIDShinyGrotto));
|
|
break;
|
|
case EncounterSlot5 {IsHiddenGrotto: false}:
|
|
VerifyG5PID_IDCorrelation(data);
|
|
break;
|
|
|
|
case PCD d: // fixed PID
|
|
if (d.IsFixedPID() && pk.EncryptionConstant != d.Gift.PK.PID)
|
|
data.AddLine(GetInvalid(CheckIdentifier.Shiny, EncGiftPIDMismatch));
|
|
break;
|
|
|
|
case WC7 { IsAshGreninja: true } when pk.IsShiny:
|
|
data.AddLine(GetInvalid(CheckIdentifier.Shiny, EncGiftShinyMismatch));
|
|
break;
|
|
// Underground Raids are originally anti-shiny on encounter.
|
|
// When selecting a prize at the end, the game rolls and force-shiny is applied to be XOR=1.
|
|
case EncounterStatic8U u when !u.IsShinyXorValid(pk.ShinyXor):
|
|
data.AddLine(GetInvalid(CheckIdentifier.Shiny, EncStaticPIDShiny));
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void VerifyG5PID_IDCorrelation(LegalityAnalysis data)
|
|
{
|
|
var pk = data.Entity;
|
|
var result = MonochromeRNG.GetBitXor(pk, pk.EncryptionConstant);
|
|
if (result != 0)
|
|
data.AddLine(GetInvalid(PIDTypeMismatch));
|
|
}
|
|
|
|
private static void VerifyECPIDWurmple(LegalityAnalysis data)
|
|
{
|
|
var pk = data.Entity;
|
|
|
|
if (pk.Species == (int)Species.Wurmple)
|
|
{
|
|
// Indicate what it will evolve into
|
|
var evoVal = WurmpleUtil.GetWurmpleEvoVal(pk.EncryptionConstant);
|
|
var evolvesTo = evoVal == WurmpleEvolution.Silcoon ? (ushort)Species.Beautifly : (ushort)Species.Dustox;
|
|
data.AddLine(GetValid(CheckIdentifier.EC, HintEvolvesToSpecies_0, evolvesTo));
|
|
}
|
|
else if (!WurmpleUtil.IsWurmpleEvoValid(pk))
|
|
{
|
|
data.AddLine(GetInvalid(CheckIdentifier.EC, PIDEncryptWurmple));
|
|
}
|
|
}
|
|
|
|
private static void VerifyEC100(LegalityAnalysis data, ushort encSpecies)
|
|
{
|
|
var pk = data.Entity;
|
|
if (pk.Species != encSpecies)
|
|
return; // Evolved, don't need to calculate the final evolution for the verbose report.
|
|
|
|
// Indicate the evolution for the user.
|
|
var rare = EvolutionRestrictions.IsEvolvedSpeciesFormRare(pk.EncryptionConstant);
|
|
var hint = rare ? (byte)1 : (byte)0;
|
|
data.AddLine(GetValid(CheckIdentifier.EC, HintEvolvesToRareForm_0, hint));
|
|
}
|
|
|
|
private static void VerifyEC(LegalityAnalysis data)
|
|
{
|
|
var pk = data.Entity;
|
|
var Info = data.Info;
|
|
|
|
if (pk.EncryptionConstant == 0)
|
|
{
|
|
if (Info.EncounterMatch is WC8 {IsHOMEGift: true})
|
|
return; // HOME Gifts
|
|
data.AddLine(Get(CheckIdentifier.EC, Severity.Fishy, PIDEncryptZero));
|
|
}
|
|
|
|
// Gen3-5 => Gen6 have PID==EC with an edge case exception.
|
|
if (Info.Generation is 3 or 4 or 5)
|
|
{
|
|
VerifyTransferEC(data);
|
|
return;
|
|
}
|
|
|
|
// Gen1-2, Gen6+ should have PID != EC
|
|
if (pk.PID == pk.EncryptionConstant)
|
|
{
|
|
// Check for edge cases
|
|
var enc = Info.EncounterMatch;
|
|
if (enc is WA8 {IsEquivalentFixedECPID: true})
|
|
return;
|
|
if (enc is WB8 {IsEquivalentFixedECPID: true})
|
|
return;
|
|
|
|
data.AddLine(GetInvalid(CheckIdentifier.EC, PIDEqualsEC)); // better to flag than 1:2^32 odds since RNG is not feasible to yield match
|
|
return;
|
|
}
|
|
|
|
// Check for Gen3-5 => Gen6 edge case being incorrectly applied here.
|
|
if ((pk.PID ^ 0x80000000) == pk.EncryptionConstant)
|
|
{
|
|
var xor = pk.ShinyXor;
|
|
if (xor >> 3 == 1) // 8 <= x <= 15
|
|
data.AddLine(Get(CheckIdentifier.EC, Severity.Fishy, TransferEncryptGen6Xor));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the expected <see cref="PKM.EncryptionConstant"/> for a Gen3-5 transfer to Gen6.
|
|
/// </summary>
|
|
/// <param name="pk">Entity to check</param>
|
|
/// <param name="ec">Encryption constant result</param>
|
|
/// <returns>True if the <see cref="ec"/> is appropriate to use.</returns>
|
|
public static bool GetTransferEC(PKM pk, out uint ec)
|
|
{
|
|
var version = pk.Version;
|
|
if (version is 0 or >= GameVersion.X) // Gen6+ ignored
|
|
{
|
|
ec = 0;
|
|
return false;
|
|
}
|
|
|
|
var pid = pk.PID;
|
|
var tmp = pid ^ pk.ID32;
|
|
var XOR = (ushort)(tmp ^ (tmp >> 16));
|
|
|
|
// Ensure we don't have a shiny.
|
|
if (XOR >> 3 == 1) // Illegal, fix. (not 16<XOR>=8)
|
|
ec = pid ^ 0x80000000; // Keep as shiny, so we have to mod the EC
|
|
else if ((XOR ^ 0x8000) >> 3 == 1 && pid != pk.EncryptionConstant)
|
|
ec = pid ^ 0x80000000; // Already anti-shiny, ensure the anti-shiny relationship is present.
|
|
else
|
|
ec = pid; // Ensure the copy correlation is present.
|
|
|
|
return true;
|
|
}
|
|
|
|
private static void VerifyTransferEC(LegalityAnalysis data)
|
|
{
|
|
var pk = data.Entity;
|
|
|
|
// Check to see if the PID and EC are properly configured.
|
|
var expect = PK5.GetTransferPID(pk.EncryptionConstant, pk.ID32, out var bitFlipProc);
|
|
if (pk.PID == expect)
|
|
return;
|
|
|
|
var msg = bitFlipProc ? TransferEncryptGen6BitFlip : TransferEncryptGen6Equals;
|
|
data.AddLine(GetInvalid(CheckIdentifier.EC, msg));
|
|
}
|
|
|
|
private static bool IsEggBitRequiredMale34(ReadOnlySpan<MoveResult> moves)
|
|
{
|
|
// If female, it must match the correlation.
|
|
// If Ditto was used with a Male Nidoran / Volbeat, it'll always be that gender.
|
|
// If Ditto was not used and a Male was obtained, it must match the correlation.
|
|
// This not-Ditto scenario is detectable if the entity has any Inherited level up moves.
|
|
foreach (var move in moves)
|
|
{
|
|
// Egg Moves (passed via Male) are allowed. Check only for inherited level up moves.
|
|
if (move.Info.Method is LearnMethod.InheritLevelUp)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|