using System; namespace PKHeX.Core { /// /// Verifies the Friendship, Affection, and other miscellaneous stats that can be present for OT/HT data. /// public sealed class HistoryVerifier : Verifier { protected override CheckIdentifier Identifier => CheckIdentifier.Memory; public override void Verify(LegalityAnalysis data) { bool neverOT = !GetCanOTHandle(data.Info.EncounterMatch, data.pkm, data.Info.Generation); VerifyHandlerState(data, neverOT); VerifyTradeState(data); VerifyOTMisc(data, neverOT); VerifyHTMisc(data); } private void VerifyTradeState(LegalityAnalysis data) { var pkm = data.pkm; if (data.pkm is IGeoTrack t) VerifyGeoLocationData(data, t, data.pkm); if (pkm.VC && pkm is PK7 {Geo1_Country: 0}) // VC transfers set Geo1 Country data.AddLine(GetInvalid(LegalityCheckStrings.LGeoMemoryMissing)); if (!pkm.IsUntraded) { // Can't have HT details even as a Link Trade egg, except in some games. if (pkm.IsEgg && !EggStateLegality.IsValidHTEgg(pkm)) data.AddLine(GetInvalid(LegalityCheckStrings.LMemoryArgBadHT)); return; } if (pkm.CurrentHandler != 0) // Badly edited; PKHeX doesn't trip this. data.AddLine(GetInvalid(LegalityCheckStrings.LMemoryHTFlagInvalid)); else if (pkm.HT_Friendship != 0) data.AddLine(GetInvalid(LegalityCheckStrings.LMemoryStatFriendshipHT0)); else if (pkm is IAffection {HT_Affection: not 0}) data.AddLine(GetInvalid(LegalityCheckStrings.LMemoryStatAffectionHT0)); // Don't check trade evolutions if Untraded. The Evolution Chain already checks for trade evolutions. } /// /// Checks if the state is set correctly. /// private void VerifyHandlerState(LegalityAnalysis data, bool neverOT) { var pkm = data.pkm; var Info = data.Info; // HT Flag if ((Info.Generation != pkm.Format || neverOT) && pkm.CurrentHandler != 1) data.AddLine(GetInvalid(LegalityCheckStrings.LTransferHTFlagRequired)); } /// /// Checks the non-Memory data for the details. /// private void VerifyOTMisc(LegalityAnalysis data, bool neverOT) { var pkm = data.pkm; var Info = data.Info; VerifyOTAffection(data, neverOT, Info.Generation, pkm); VerifyOTFriendship(data, neverOT, Info.Generation, pkm); } private void VerifyOTFriendship(LegalityAnalysis data, bool neverOT, int origin, PKM pkm) { if (origin < 0) return; if (origin <= 2) { VerifyOTFriendshipVC12(data, pkm); return; } if (neverOT) { // Verify the original friendship value since it cannot change from the value it was assigned in the original generation. // If none match, then it is not a valid OT friendship. var fs = pkm.OT_Friendship; var enc = data.Info.EncounterMatch; if (GetBaseFriendship(enc, origin) != fs) data.AddLine(GetInvalid(LegalityCheckStrings.LMemoryStatFriendshipOTBaseEvent)); } } private void VerifyOTFriendshipVC12(LegalityAnalysis data, PKM pkm) { // Verify the original friendship value since it cannot change from the value it was assigned in the original generation. // Since some evolutions have different base friendship values, check all possible evolutions for a match. // If none match, then it is not a valid OT friendship. // VC transfers use SM personal info var any = IsMatchFriendship(data.Info.EvoChainsAllGens.Gen7, PersonalTable.USUM, pkm.OT_Friendship); if (!any) data.AddLine(GetInvalid(LegalityCheckStrings.LMemoryStatFriendshipOTBaseEvent)); } private static bool IsMatchFriendship(EvoCriteria[] evos, PersonalTable pt, int fs) { foreach (var z in evos) { if (!pt.IsPresentInGame(z.Species, z.Form)) continue; var entry = pt.GetFormEntry(z.Species, z.Form); if (entry.BaseFriendship == fs) return true; } return false; } private void VerifyOTAffection(LegalityAnalysis data, bool neverOT, int origin, PKM pkm) { if (pkm is not IAffection a) return; if (origin < 6) { // Can gain affection in Gen6 via the Contest glitch applying affection to OT rather than HT. // VC encounters cannot obtain OT affection since they can't visit Gen6. if ((origin <= 2 && a.OT_Affection != 0) || IsInvalidContestAffection(a)) data.AddLine(GetInvalid(LegalityCheckStrings.LMemoryStatAffectionOT0)); } else if (neverOT) { if (origin == 6) { if (pkm.IsUntraded && pkm.XY) { if (a.OT_Affection != 0) data.AddLine(GetInvalid(LegalityCheckStrings.LMemoryStatAffectionOT0)); } else if (IsInvalidContestAffection(a)) { data.AddLine(GetInvalid(LegalityCheckStrings.LMemoryStatAffectionOT0)); } } else { if (a.OT_Affection != 0) data.AddLine(GetInvalid(LegalityCheckStrings.LMemoryStatAffectionOT0)); } } } /// /// Checks the non-Memory data for the details. /// private void VerifyHTMisc(LegalityAnalysis data) { var pkm = data.pkm; var htGender = pkm.HT_Gender; if (htGender > 1 || (pkm.IsUntraded && htGender != 0)) data.AddLine(GetInvalid(string.Format(LegalityCheckStrings.LMemoryHTGender, htGender))); if (pkm is IHandlerLanguage h) VerifyHTLanguage(data, h, pkm); } private void VerifyHTLanguage(LegalityAnalysis data, IHandlerLanguage h, PKM pkm) { if (h.HT_Language == 0) { if (!string.IsNullOrWhiteSpace(pkm.HT_Name)) data.AddLine(GetInvalid(LegalityCheckStrings.LMemoryHTLanguage)); return; } if (string.IsNullOrWhiteSpace(pkm.HT_Name)) data.AddLine(GetInvalid(LegalityCheckStrings.LMemoryHTLanguage)); else if (h.HT_Language > (int)LanguageID.ChineseT) data.AddLine(GetInvalid(LegalityCheckStrings.LMemoryHTLanguage)); } private void VerifyGeoLocationData(LegalityAnalysis data, IGeoTrack t, PKM pkm) { var valid = t.GetValidity(); if (valid == GeoValid.CountryAfterPreviousEmpty) data.AddLine(GetInvalid(LegalityCheckStrings.LGeoBadOrder)); else if (valid == GeoValid.RegionWithoutCountry) data.AddLine(GetInvalid(LegalityCheckStrings.LGeoNoRegion)); if (t.Geo1_Country != 0 && pkm.IsUntraded) // traded data.AddLine(GetInvalid(LegalityCheckStrings.LGeoNoCountryHT)); } // ORAS contests mistakenly apply 20 affection to the OT instead of the current handler's value private static bool IsInvalidContestAffection(IAffection pkm) => pkm.OT_Affection != 255 && pkm.OT_Affection % 20 != 0; public static bool GetCanOTHandle(IEncounterTemplate enc, PKM pkm, int generation) { // Handlers introduced in Generation 6. OT Handling was always the case for Generation 3-5 data. if (generation < 6) return generation >= 3; return enc switch { EncounterTrade => false, EncounterSlot8GO => false, WC6 wc6 when wc6.OT_Name.Length > 0 => false, WC7 wc7 when wc7.OT_Name.Length > 0 && wc7.TID != 18075 => false, // Ash Pikachu QR Gift doesn't set Current Handler WC8 wc8 when wc8.GetHasOT(pkm.Language) => false, WB8 wb8 when wb8.GetHasOT(pkm.Language) => false, WA8 wa8 when wa8.GetHasOT(pkm.Language) => false, WC8 {IsHOMEGift: true} => false, _ => true, }; } private static int GetBaseFriendship(IEncounterTemplate enc, int generation) => enc switch { IFixedOTFriendship f => f.OT_Friendship, { Version: GameVersion.BDSP or GameVersion.BD or GameVersion.SP } => PersonalTable.BDSP.GetFormEntry(enc.Species, enc.Form).BaseFriendship, { Version: GameVersion.PLA } => PersonalTable.LA .GetFormEntry(enc.Species, enc.Form).BaseFriendship, _ => GetBaseFriendship(generation, enc.Species, enc.Form), }; private static int GetBaseFriendship(int generation, int species, int form) => generation switch { 6 => PersonalTable.AO[species].BaseFriendship, 7 => PersonalTable.USUM[species].BaseFriendship, 8 => PersonalTable.SWSH.GetFormEntry(species, form).BaseFriendship, _ => throw new ArgumentOutOfRangeException(nameof(generation)), }; } }