using System; using static PKHeX.Core.LegalityCheckStrings; namespace PKHeX.Core; /// /// Verifies the . /// public sealed class TrainerNameVerifier : Verifier { protected override CheckIdentifier Identifier => CheckIdentifier.Trainer; private static readonly string[] SuspiciousOTNames = { "PKHeX", "PKHeX", }; public override void Verify(LegalityAnalysis data) { var pk = data.Entity; var enc = data.EncounterMatch; if (!IsPlayerOriginalTrainer(enc)) return; // already verified var ot = pk.OT_Name; if (ot.Length == 0) data.AddLine(GetInvalid(LOTShort)); if (IsOTNameSuspicious(ot)) { data.AddLine(Get(LOTSuspicious, Severity.Fishy)); } if (pk.VC) { VerifyOTG1(data); } else if (ot.Length > Legal.GetMaxLengthOT(data.Info.Generation, (LanguageID)pk.Language)) { if (!IsEdgeCaseLength(pk, data.EncounterOriginal, ot)) data.AddLine(Get(LOTLong, Severity.Invalid)); } if (ParseSettings.CheckWordFilter) { if (WordFilter.IsFiltered(ot, out string bad)) data.AddLine(GetInvalid($"Wordfilter: {bad}")); if (ContainsTooManyNumbers(ot, data.Info.Generation)) data.AddLine(GetInvalid("Wordfilter: Too many numbers.")); if (WordFilter.IsFiltered(pk.HT_Name, out bad)) data.AddLine(GetInvalid($"Wordfilter: {bad}")); } } /// /// Checks if any player (human) was the original OT. /// internal static bool IsPlayerOriginalTrainer(IEncounterable enc) => enc switch { EncounterTrade { HasTrainerName: true } => false, MysteryGift { IsEgg: false } => false, EncounterStatic5N => false, _ => true, }; public static bool IsEdgeCaseLength(PKM pk, IEncounterTemplate e, string ot) { if (e.EggEncounter) { if (e is WC3 wc3 && pk.IsEgg && wc3.OT_Name == ot) return true; // Fixed OT Mystery Gift Egg bool eggEdge = pk.IsEgg ? pk.IsTradedEgg || pk.Format == 3 : pk.WasTradedEgg; if (!eggEdge) return false; var len = Legal.GetMaxLengthOT(e.Generation, LanguageID.English); // max case return ot.Length <= len; } if (e is EncounterTrade { HasTrainerName: true }) return true; // already verified if (e is MysteryGift mg && mg.OT_Name.Length == ot.Length) return true; // Mattle Ho-Oh return false; } public void VerifyOTG1(LegalityAnalysis data) { var pk = data.Entity; string tr = pk.OT_Name; if (tr.Length == 0) { if (pk is SK2 {TID: 0, IsRental: true}) { data.AddLine(Get(LOTShort, Severity.Fishy)); } else { data.AddLine(GetInvalid(LOTShort)); return; } } VerifyG1OTWithinBounds(data, tr.AsSpan()); if (pk.OT_Gender == 1) { if (pk is ICaughtData2 {CaughtData:0} || (pk.Format > 2 && pk.VC1) || data is {EncounterOriginal: {Generation:1} or EncounterStatic2E {IsGift:true}}) data.AddLine(GetInvalid(LG1OTGender)); } } private void VerifyG1OTWithinBounds(LegalityAnalysis data, ReadOnlySpan str) { if (StringConverter12.GetIsG1English(str)) { if (str.Length > 7 && data.EncounterOriginal is not EncounterTradeGB) // OT already verified; GER shuckle has 8 chars data.AddLine(GetInvalid(LOTLong)); } else if (StringConverter12.GetIsG1Japanese(str)) { if (str.Length > 5) data.AddLine(GetInvalid(LOTLong)); } else if (data.Entity.Korean && StringConverter2KOR.GetIsG2Korean(str)) { if (str.Length > 5) data.AddLine(GetInvalid(LOTLong)); } else if (data.EncounterOriginal is not EncounterTrade2) // OT already verified; SPA Shuckle/Voltorb transferred from French can yield 2 inaccessible chars { data.AddLine(GetInvalid(LG1CharOT)); } } private static bool IsOTNameSuspicious(string name) { foreach (var s in SuspiciousOTNames) { if (s.StartsWith(name, StringComparison.InvariantCultureIgnoreCase)) return true; } return false; } public static bool ContainsTooManyNumbers(string str, int originalGeneration) { if (originalGeneration <= 3) return false; // no limit from these generations int max = originalGeneration < 6 ? 4 : 5; if (str.Length <= max) return false; int count = GetNumberCount(str); return count > max; } private static int GetNumberCount(string str) { static bool IsNumber(char c) { if ('0' <= c) return c <= '9'; return (uint)(c - '0') <= 9; } int ctr = 0; foreach (var c in str) { if (IsNumber(c)) ++ctr; } return ctr; } }