PKHeX/PKHeX.Core/Legality/Verifiers/MiscVerifier.cs
Kurt cd12962a50 Remove Tradeback state caching in PKM data
Since we have more metadata with move learn sourcing, we can check if it was traded to gen2 to get new moves / deleted.

Adjust call sites appropriately
might have some issues, to be ironed out maybe
2021-08-20 15:59:54 -07:00

507 lines
21 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using static PKHeX.Core.LegalityCheckStrings;
using static PKHeX.Core.CheckIdentifier;
namespace PKHeX.Core
{
/// <summary>
/// Verifies miscellaneous data including <see cref="PKM.FatefulEncounter"/> and minor values.
/// </summary>
public sealed class MiscVerifier : Verifier
{
protected override CheckIdentifier Identifier => Misc;
public override void Verify(LegalityAnalysis data)
{
var pkm = data.pkm;
if (pkm.IsEgg)
{
VerifyMiscEggCommon(data);
if (pkm is IContestStats s && s.HasContestStats())
data.AddLine(GetInvalid(LEggContest, Egg));
switch (pkm)
{
case PK5 pk5 when pk5.PokeStarFame != 0 && pk5.IsEgg:
data.AddLine(GetInvalid(LEggShinyPokeStar, Egg));
break;
case PK4 pk4 when pk4.ShinyLeaf != 0:
data.AddLine(GetInvalid(LEggShinyLeaf, Egg));
break;
case PK4 pk4 when pk4.PokéathlonStat != 0:
data.AddLine(GetInvalid(LEggPokeathlon, Egg));
break;
case PK3 when pkm.Language != 1: // All Eggs are Japanese and flagged specially for localized string
data.AddLine(GetInvalid(string.Format(LOTLanguage, LanguageID.Japanese, (LanguageID)pkm.Language), Egg));
break;
}
if (pkm is IHomeTrack {Tracker: not 0})
data.AddLine(GetInvalid(LTransferTrackerShouldBeZero));
}
else
{
VerifyMiscMovePP(data);
}
switch (pkm)
{
case PK7 {ResortEventStatus: >= 20}:
data.AddLine(GetInvalid(LTransferBad));
break;
case PB7 pb7:
VerifyBelugaStats(data, pb7);
break;
case PK8 pk8:
VerifySWSHStats(data, pk8);
break;
}
if (pkm.Format >= 6)
VerifyFullness(data, pkm);
var enc = data.EncounterMatch;
if (enc is WC8 { IsHOMEGift: true } w)
{
var date = new DateTime(pkm.Met_Year + 2000, pkm.Met_Month, pkm.Met_Day);
if (!EncountersHOME.IsValidDateWC8(w.CardID, date))
data.AddLine(GetInvalid(LDateOutsideDistributionWindow));
}
else if (enc is IOverworldCorrelation8 z)
{
var match = z.IsOverworldCorrelationCorrect(pkm);
var req = z.GetRequirement(pkm);
if (match)
{
var seed = Overworld8RNG.GetOriginalSeed(pkm);
data.Info.PIDIV = new PIDIV(PIDType.Overworld8, seed);
}
bool valid = req switch
{
OverworldCorrelation8Requirement.MustHave => match,
OverworldCorrelation8Requirement.MustNotHave => !match,
_ => true,
};
if (!valid)
data.AddLine(GetInvalid(LPIDTypeMismatch));
}
VerifyMiscFatefulEncounter(data);
}
public void VerifyMiscG1(LegalityAnalysis data)
{
var pkm = data.pkm;
if (pkm.IsEgg)
VerifyMiscEggCommon(data);
if (pkm is not PK1 pk1)
return;
VerifyMiscG1Types(data, pk1);
VerifyMiscG1CatchRate(data, pk1);
}
private void VerifyMiscG1Types(LegalityAnalysis data, PK1 pk1)
{
var Type_A = pk1.Type_A;
var Type_B = pk1.Type_B;
var species = pk1.Species;
if (species == (int)Species.Porygon)
{
// Can have any type combination of any species by using Conversion.
if (!GBRestrictions.TypeIDExists(Type_A))
{
data.AddLine(GetInvalid(LG1TypePorygonFail1));
}
if (!GBRestrictions.TypeIDExists(Type_B))
{
data.AddLine(GetInvalid(LG1TypePorygonFail2));
}
else // Both types exist, ensure a Gen1 species has this combination
{
var TypesAB_Match = PersonalTable.RB.IsValidTypeCombination(Type_A, Type_B);
var result = TypesAB_Match ? GetValid(LG1TypeMatchPorygon) : GetInvalid(LG1TypePorygonFail);
data.AddLine(result);
}
}
else // Types must match species types
{
var pi = PersonalTable.RB[species];
var Type_A_Match = Type_A == pi.Type1;
var Type_B_Match = Type_B == pi.Type2;
var first = Type_A_Match ? GetValid(LG1TypeMatch1) : GetInvalid(LG1Type1Fail);
var second = Type_B_Match || (ParseSettings.AllowGBCartEra && ((species is (int)Species.Magnemite or (int)Species.Magneton) && Type_B == 9)) // Steel Magnemite via Stadium2
? GetValid(LG1TypeMatch2) : GetInvalid(LG1Type2Fail);
data.AddLine(first);
data.AddLine(second);
}
}
private void VerifyMiscG1CatchRate(LegalityAnalysis data, PK1 pk1)
{
var catch_rate = pk1.Catch_Rate;
var tradeback = GBRestrictions.IsTimeCapsuleTransferred(pk1, data.Info.Moves, data.EncounterMatch);
var result = tradeback == TimeCapsuleEvaluation.NotTransferred
? GetWasNotTradeback(tradeback)
: GetWasTradeback(tradeback);
data.AddLine(result);
CheckResult GetWasTradeback(TimeCapsuleEvaluation timeCapsuleEvalution)
{
if (catch_rate == 0 || Legal.HeldItems_GSC.Contains((ushort)catch_rate))
return GetValid(LG1CatchRateMatchTradeback);
if (timeCapsuleEvalution == TimeCapsuleEvaluation.BadCatchRate)
return GetInvalid(LG1CatchRateItem);
return GetWasNotTradeback(timeCapsuleEvalution);
}
CheckResult GetWasNotTradeback(TimeCapsuleEvaluation timeCapsuleEvalution)
{
var e = data.EncounterMatch;
if (e is EncounterStatic1E {Version: GameVersion.Stadium} or EncounterTrade1)
return GetValid(LG1CatchRateMatchPrevious); // Encounters detected by the catch rate, cant be invalid if match this encounters
int species = pk1.Species;
if (GBRestrictions.Species_NotAvailable_CatchRate.Contains(species) && catch_rate == PersonalTable.RB[species].CatchRate)
{
if (species != (int) Species.Dragonite || catch_rate != 45 || !e.Version.Contains(GameVersion.YW))
return GetInvalid(LG1CatchRateEvo);
}
if (!GBRestrictions.RateMatchesEncounter(e.Species, e.Version, catch_rate))
return GetInvalid(timeCapsuleEvalution == TimeCapsuleEvaluation.Transferred12 ? LG1CatchRateChain : LG1CatchRateNone);
return GetValid(LG1CatchRateMatchPrevious);
}
}
private static void VerifyMiscFatefulEncounter(LegalityAnalysis data)
{
var pkm = data.pkm;
var enc = data.EncounterMatch;
switch (enc)
{
case WC3 {Fateful: true} w:
if (w.IsEgg)
{
// Eggs hatched in RS clear the obedience flag!
// Hatching in Gen3 doesn't change the origin version.
if (pkm.Format != 3)
return; // possible hatched in either game, don't bother checking
if (pkm.Met_Location <= 087) // hatched in RS or Emerald
return; // possible hatched in either game, don't bother checking
// else, ensure fateful is active (via below)
}
VerifyFatefulIngameActive(data);
VerifyWC3Shiny(data, w);
return;
case WC3 w:
if (w.Version == GameVersion.XD)
return; // Can have either state
VerifyWC3Shiny(data, w);
break;
case MysteryGift g: // WC3 handled above
VerifyReceivability(data, g);
VerifyFatefulMysteryGift(data, g);
return;
case EncounterStatic {Fateful: true}: // ingame fateful
case EncounterSlot3PokeSpot: // ingame pokespot
case EncounterTrade4RanchSpecial: // ranch varied PID
VerifyFatefulIngameActive(data);
return;
}
if (pkm.FatefulEncounter)
data.AddLine(GetInvalid(LFatefulInvalid, Fateful));
}
private static void VerifyMiscMovePP(LegalityAnalysis data)
{
var pkm = data.pkm;
if (pkm.Move1_PP > pkm.GetMovePP(pkm.Move1, pkm.Move1_PPUps))
data.AddLine(GetInvalid(string.Format(LMovePPTooHigh_0, 1), CurrentMove));
if (pkm.Move2_PP > pkm.GetMovePP(pkm.Move2, pkm.Move2_PPUps))
data.AddLine(GetInvalid(string.Format(LMovePPTooHigh_0, 2), CurrentMove));
if (pkm.Move3_PP > pkm.GetMovePP(pkm.Move3, pkm.Move3_PPUps))
data.AddLine(GetInvalid(string.Format(LMovePPTooHigh_0, 3), CurrentMove));
if (pkm.Move4_PP > pkm.GetMovePP(pkm.Move4, pkm.Move4_PPUps))
data.AddLine(GetInvalid(string.Format(LMovePPTooHigh_0, 4), CurrentMove));
}
private static void VerifyMiscEggCommon(LegalityAnalysis data)
{
var pkm = data.pkm;
if (pkm.Move1_PPUps > 0 || pkm.Move2_PPUps > 0 || pkm.Move3_PPUps > 0 || pkm.Move4_PPUps > 0)
data.AddLine(GetInvalid(LEggPPUp, Egg));
if (pkm.Move1_PP != pkm.GetMovePP(pkm.Move1, 0) || pkm.Move2_PP != pkm.GetMovePP(pkm.Move2, 0) || pkm.Move3_PP != pkm.GetMovePP(pkm.Move3, 0) || pkm.Move4_PP != pkm.GetMovePP(pkm.Move4, 0))
data.AddLine(GetInvalid(LEggPP, Egg));
var enc = data.EncounterMatch;
var HatchCycles = enc is EncounterStatic s ? s.EggCycles : 0;
if (HatchCycles == 0) // no value set
HatchCycles = pkm.PersonalInfo.HatchCycles;
if (pkm.OT_Friendship > HatchCycles)
data.AddLine(GetInvalid(LEggHatchCycles, Egg));
if (pkm.Format >= 6 && enc is EncounterEgg && !MovesMatchRelearn(pkm))
{
var moves = string.Join(", ", ParseSettings.GetMoveNames(pkm.Moves));
var msg = string.Format(LMoveFExpect_0, moves);
data.AddLine(GetInvalid(msg, Egg));
}
if (pkm is PK8 pk8)
{
if (pk8.HasAnyMoveRecordFlag())
data.AddLine(GetInvalid(LEggRelearnFlags, Egg));
if (pk8.StatNature != pk8.Nature)
data.AddLine(GetInvalid(LEggNature, Egg));
}
}
private static bool MovesMatchRelearn(PKM pkm)
{
if (pkm.Move1 != pkm.RelearnMove1)
return false;
if (pkm.Move2 != pkm.RelearnMove2)
return false;
if (pkm.Move3 != pkm.RelearnMove3)
return false;
if (pkm.Move4 != pkm.RelearnMove4)
return false;
return true;
}
private static void VerifyFatefulMysteryGift(LegalityAnalysis data, MysteryGift g)
{
var pkm = data.pkm;
if (g is PGF {IsShiny: true})
{
var Info = data.Info;
Info.PIDIV = MethodFinder.Analyze(pkm);
if (Info.PIDIV.Type != PIDType.G5MGShiny && pkm.Egg_Location != Locations.LinkTrade5)
data.AddLine(GetInvalid(LPIDTypeMismatch, PID));
}
bool shouldHave = GetFatefulState(g);
var result = pkm.FatefulEncounter == shouldHave
? GetValid(LFatefulMystery, Fateful)
: GetInvalid(LFatefulMysteryMissing, Fateful);
data.AddLine(result);
}
private static bool GetFatefulState(MysteryGift g)
{
if (g is WC6 {IsLinkGift: true})
return false; // Pokémon Link fake-gifts do not have Fateful
return true;
}
private static void VerifyReceivability(LegalityAnalysis data, MysteryGift g)
{
var pkm = data.pkm;
switch (g)
{
case WC6 wc6 when !wc6.CanBeReceivedByVersion(pkm.Version) && !pkm.WasTradedEgg:
case WC7 wc7 when !wc7.CanBeReceivedByVersion(pkm.Version) && !pkm.WasTradedEgg:
case WC8 wc8 when !wc8.CanBeReceivedByVersion(pkm.Version) && !pkm.WasTradedEgg:
data.AddLine(GetInvalid(LEncGiftVersionNotDistributed, GameOrigin));
return;
case WC6 wc6 when wc6.RestrictLanguage != 0 && pkm.Language != wc6.RestrictLanguage:
data.AddLine(GetInvalid(string.Format(LOTLanguage, wc6.RestrictLanguage, pkm.Language), CheckIdentifier.Language));
return;
case WC7 wc7 when wc7.RestrictLanguage != 0 && pkm.Language != wc7.RestrictLanguage:
data.AddLine(GetInvalid(string.Format(LOTLanguage, wc7.RestrictLanguage, pkm.Language), CheckIdentifier.Language));
return;
}
}
private static void VerifyWC3Shiny(LegalityAnalysis data, WC3 g3)
{
// check for shiny locked gifts
if (!g3.Shiny.IsValid(data.pkm))
data.AddLine(GetInvalid(LEncGiftShinyMismatch, Fateful));
}
private static void VerifyFatefulIngameActive(LegalityAnalysis data)
{
var pkm = data.pkm;
var result = pkm.FatefulEncounter
? GetValid(LFateful, Fateful)
: GetInvalid(LFatefulMissing, Fateful);
data.AddLine(result);
}
public void VerifyVersionEvolution(LegalityAnalysis data)
{
var pkm = data.pkm;
if (pkm.Format < 7 || data.EncounterMatch.Species == pkm.Species)
return;
// No point using the evolution tree. Just handle certain species.
switch (pkm.Species)
{
case (int)Species.Lycanroc when pkm.Format == 7 && ((pkm.Form == 0 && Moon()) || (pkm.Form == 1 && Sun())):
case (int)Species.Solgaleo when Moon():
case (int)Species.Lunala when Sun():
bool Sun() => (pkm.Version & 1) == 0;
bool Moon() => (pkm.Version & 1) == 1;
if (pkm.IsUntraded)
data.AddLine(GetInvalid(LEvoTradeRequired, Evolution));
break;
}
}
private static void VerifyFullness(LegalityAnalysis data, PKM pkm)
{
if (pkm.IsEgg)
{
if (pkm.Fullness != 0)
data.AddLine(GetInvalid(string.Format(LMemoryStatFullness, 0), Encounter));
if (pkm.Enjoyment != 0)
data.AddLine(GetInvalid(string.Format(LMemoryStatEnjoyment, 0), Encounter));
return;
}
if (pkm.Format >= 8)
{
if (pkm.Fullness > 245) // Exiting camp is -10
data.AddLine(GetInvalid(string.Format(LMemoryStatFullness, "<=245"), Encounter));
if (pkm.Enjoyment != 0)
data.AddLine(GetInvalid(string.Format(LMemoryStatEnjoyment, 0), Encounter));
return;
}
if (pkm.Format != 6 || !pkm.IsUntraded || pkm.XY)
return;
// OR/AS PK6
if (pkm.Fullness == 0)
return;
if (pkm.Species != data.EncounterMatch.Species)
return; // evolved
if (Unfeedable.Contains(pkm.Species))
data.AddLine(GetInvalid(string.Format(LMemoryStatFullness, 0), Encounter));
}
private static readonly HashSet<int> Unfeedable = new()
{
(int)Species.Metapod,
(int)Species.Kakuna,
(int)Species.Pineco,
(int)Species.Silcoon,
(int)Species.Cascoon,
(int)Species.Shedinja,
(int)Species.Spewpa,
};
private static void VerifyBelugaStats(LegalityAnalysis data, PB7 pb7)
{
// ReSharper disable once CompareOfFloatsByEqualityOperator -- THESE MUST MATCH EXACTLY
if (!IsCloseEnough(pb7.HeightAbsolute, pb7.CalcHeightAbsolute))
data.AddLine(GetInvalid(LStatIncorrectHeight, Encounter));
// ReSharper disable once CompareOfFloatsByEqualityOperator -- THESE MUST MATCH EXACTLY
if (!IsCloseEnough(pb7.WeightAbsolute, pb7.CalcWeightAbsolute))
data.AddLine(GetInvalid(LStatIncorrectWeight, Encounter));
if (pb7.Stat_CP != pb7.CalcCP && !IsStarterLGPE(pb7))
data.AddLine(GetInvalid(LStatIncorrectCP, Encounter));
}
private static bool IsCloseEnough(float a, float b)
{
var ia = BitConverter.ToInt32(BitConverter.GetBytes(a), 0);
var ib = BitConverter.ToInt32(BitConverter.GetBytes(b), 0);
return Math.Abs(ia - ib) <= 7;
}
private static bool IsStarterLGPE(ISpeciesForm pk) => pk.Species switch
{
(int)Species.Pikachu when pk.Form == 8 => true,
(int)Species.Eevee when pk.Form == 1 => true,
_ => false,
};
private void VerifySWSHStats(LegalityAnalysis data, PK8 pk8)
{
if (pk8.Favorite)
data.AddLine(GetInvalid(LFavoriteMarkingUnavailable, Encounter));
var social = pk8.Sociability;
if (pk8.IsEgg)
{
if (social != 0)
data.AddLine(GetInvalid(LMemorySocialZero, Encounter));
}
else if (social > byte.MaxValue)
{
data.AddLine(GetInvalid(string.Format(LMemorySocialTooHigh_0, byte.MaxValue), Encounter));
}
var sn = pk8.StatNature;
if (sn != pk8.Nature)
{
// Only allow Serious nature (12); disallow all other neutral natures.
if (sn != 12 && (sn > 24 || sn % 6 == 0))
data.AddLine(GetInvalid(LStatNatureInvalid));
}
var bv = pk8.BattleVersion;
if (bv != 0)
{
if ((bv != (int)GameVersion.SW && bv != (int)GameVersion.SH) || pk8.SWSH)
data.AddLine(GetInvalid(LStatBattleVersionInvalid));
}
var enc = data.EncounterMatch;
bool originGMax = enc is IGigantamax {CanGigantamax: true};
if (originGMax != pk8.CanGigantamax)
{
bool ok = !pk8.IsEgg && pk8.CanToggleGigantamax(pk8.Species, pk8.Form, enc.Species, enc.Form);
var chk = ok ? GetValid(LStatGigantamaxValid) : GetInvalid(LStatGigantamaxInvalid);
data.AddLine(chk);
}
if (pk8.DynamaxLevel != 0)
{
if (!pk8.CanHaveDynamaxLevel(pk8) || pk8.DynamaxLevel > 10)
data.AddLine(GetInvalid(LStatDynamaxInvalid));
}
PersonalInfo? pi = null;
for (int i = 0; i < PersonalInfoSWSH.CountTR; i++)
{
if (!pk8.GetMoveRecordFlag(i))
continue;
if ((pi ??= pk8.PersonalInfo).TMHM[i + 100])
continue;
// Calyrex-0 can have TR flags for Calyrex-1/2 after it has force unlearned them.
// Re-fusing can be reacquire the move via relearner, rather than needing another TR.
// Calyrex-0 cannot reacquire the move via relearner, even though the TR is checked off in the TR list.
if (pk8.Species == (int) Species.Calyrex)
{
var form = pk8.Form;
// Check if another alt form can learn the TR
if ((form != 1 && CanLearnTR((int) Species.Calyrex, 1, i)) || (form != 2 && CanLearnTR((int) Species.Calyrex, 2, i)))
continue;
}
data.AddLine(GetInvalid(string.Format(LMoveSourceTR, ParseSettings.MoveStrings[Legal.TMHM_SWSH[i + PersonalInfoSWSH.CountTM]])));
}
// weight/height scalars can be legally 0 (1:65536) so don't bother checking
}
private static bool CanLearnTR(int species, int form, int tr)
{
var pi = PersonalTable.SWSH.GetFormEntry(species, form);
return pi.TMHM[tr + PersonalInfoSWSH.CountTM];
}
}
}