PKHeX/PKHeX.Core/Legality/Verifiers/NicknameVerifier.cs
Kurt 6f9daaed04 Small tweaks to HGSS ball check
small lol
would need fully implemented pal park trash byte checks, big sad
leave stuff stubbed for now, can clamp down later.

restrict some method sigs for IEncounterTemplate (rather than more-derived IEncounterable) for consistency
2026-02-20 01:36:46 -06:00

480 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using static PKHeX.Core.LegalityCheckResultCode;
using static PKHeX.Core.LanguageID;
namespace PKHeX.Core;
/// <summary>
/// Verifies the <see cref="PKM.Nickname"/>.
/// </summary>
public sealed class NicknameVerifier : Verifier
{
protected override CheckIdentifier Identifier => CheckIdentifier.Nickname;
public override void Verify(LegalityAnalysis data)
{
var pk = data.Entity;
// If the Pokémon is not nicknamed, it should match one of the language strings.
Span<char> nickname = stackalloc char[pk.TrashCharCountNickname];
int len = pk.LoadString(pk.NicknameTrash, nickname);
if (len == 0)
{
data.AddLine(GetInvalid(NickLengthShort));
return;
}
nickname = nickname[..len];
if (nickname.Contains('\uffff') && pk is { Format: 4 })
{
data.AddLine(GetInvalid(NickInvalidChar, ushort.MaxValue));
return;
}
var enc = data.EncounterOriginal;
if (enc is ILangNicknamedTemplate n)
{
VerifyFixedNicknameEncounter(data, n, enc, pk, nickname);
if (pk.IsEgg)
VerifyNicknameEgg(data);
if (pk.IsNicknamed)
return;
}
if (pk is { Format: <= 7, IsNicknamed: true }) // can nickname afterward
{
if (pk.VC)
VerifyG1NicknameWithinBounds(data, nickname);
else if (IsMysteryGiftNoNickname(enc))
data.AddLine(Get(ParseSettings.Settings.Nickname.NicknamedMysteryGift(enc.Context), EncGiftNicknamed));
}
if (enc is IFixedTrainer t)
{
VerifyNicknameTrade(data, enc);
if (t is IFixedNickname { IsFixedNickname: true })
return;
}
if (pk.IsEgg)
{
VerifyNicknameEgg(data);
return;
}
if (VerifyUnNicknamedEncounter(data, pk, nickname, enc))
return;
// Non-nicknamed strings have already been checked.
if (ParseSettings.Settings.WordFilter.IsEnabled(pk.Format) && pk.IsNicknamed)
{
var mostRecentNicknameContext = pk.Format >= 8 ? pk.Context : enc.Context;
if (WordFilter.IsFiltered(nickname, pk.Context, mostRecentNicknameContext, out var type, out var badPattern))
data.AddLine(GetInvalid(CheckIdentifier.Nickname, WordFilterFlaggedPattern_01, (ushort)type, (ushort)badPattern));
if (TrainerNameVerifier.ContainsTooManyNumbers(nickname, enc.Generation))
data.AddLine(GetInvalid(CheckIdentifier.Nickname, WordFilterTooManyNumbers_0, (ushort)TrainerNameVerifier.GetMaxNumberCount(enc.Generation)));
}
}
private static bool IsMysteryGiftNoNickname(IEncounterTemplate enc)
{
if (enc is not MysteryGift { IsEgg: false })
return false;
return enc switch
{
PCD pcd => !pcd.Gift.PK.IsNicknamed,
PGF pgf => !pgf.IsNicknamed,
WC6 wc6 => !wc6.IsNicknamed && wc6 is not ({ IsOriginalTrainerNameSet: false }
or { IsLinkGift: true, Species: (int)Species.Glalie or (int)Species.Steelix }), // Can nickname the demo gift
WC7 wc7 => !wc7.IsNicknamed && wc7 is not { IsOriginalTrainerNameSet: false },
_ => true,
};
}
private void VerifyFixedNicknameEncounter(LegalityAnalysis data, ILangNicknamedTemplate n, IEncounterTemplate enc, PKM pk, ReadOnlySpan<char> nickname)
{
var encounterNickname = n.GetNickname(pk.Language);
if (string.IsNullOrWhiteSpace(encounterNickname))
{
if (n is WC8 {IsHOMEGift: true})
{
VerifyHomeGiftNickname(data, enc, pk, nickname);
return;
}
if (n.CanHandleOT(pk.Language))
return;
if (pk.IsNicknamed)
data.AddLine(Get(Severity.Invalid, EncGiftNicknamed));
return;
}
if (!pk.IsNicknamed)
{
// Check if it had a nickname at all
var orig = SpeciesName.GetSpeciesNameGeneration(enc.Species, pk.Language, enc.Generation);
if (orig == encounterNickname)
{
// Didn't have a nickname. Ensure that the language matches the current nickname string.
if (!SpeciesName.IsNicknamed(pk.Species, nickname, pk.Language, pk.Format))
return;
}
// Should have a nickname present.
data.AddLine(GetInvalid(NickMatchLanguageFail));
return;
}
// Encounter has a nickname, and PKM should have it.
bool matches = nickname.SequenceEqual(encounterNickname);
var severity = !matches || !pk.IsNicknamed ? Severity.Invalid : Severity.Valid;
data.AddLine(Get(severity, EncGiftNicknamed));
}
private void VerifyHomeGiftNickname(LegalityAnalysis data, IEncounterTemplate enc, ILangNick pk, ReadOnlySpan<char> nickname)
{
// can nickname on redemption
if (!pk.IsNicknamed)
return;
// Can't nickname everything.
if (enc.Species == (int) Species.Melmetal)
{
data.AddLine(GetInvalid(EncGiftNicknamed));
return;
}
// Ensure the nickname does not match species name
var orig = SpeciesName.GetSpeciesNameGeneration(enc.Species, pk.Language, enc.Generation);
if (nickname.SequenceEqual(orig))
data.AddLine(GetInvalid(NickMatchLanguageFlag));
}
private bool VerifyUnNicknamedEncounter(LegalityAnalysis data, PKM pk, ReadOnlySpan<char> nickname, IEncounterTemplate enc)
{
if (pk.IsNicknamed)
{
if (enc.Generation >= 8 && CanNicknameForeign8Plus(data, pk))
{
// Can only nickname if it matches your language.
// Setting the nickname to the same as the species name does not set the Nickname flag (equals unmodified, no flag)
if (!SpeciesName.IsNicknamed(pk.Species, nickname, pk.Language, pk.Format))
{
data.AddLine(Get(Severity.Invalid, NickMatchLanguageFlag));
return true;
}
}
if (SpeciesName.TryGetSpeciesAnyLanguage(nickname, out var species, pk.Context))
{
var msg = species == pk.Species ? NickMatchLanguageFlag : NickMatchNoOthersFail;
data.AddLine(Get(ParseSettings.Settings.Nickname.NicknamedAnotherSpecies, msg));
return true;
}
if (pk.Format <= 7 && StringConverter.HasEastAsianScriptCharacters(nickname) && pk is not PB7) // East Asian Scripts
{
data.AddLine(GetInvalid(NickInvalidChar));
return true;
}
if (nickname.Length > Legal.GetMaxLengthNickname(enc.Generation, (LanguageID)pk.Language))
{
int length = GetForeignNicknameLength(pk, enc, enc.Generation);
var severe = (length != 0 && nickname.Length <= length) ? Severity.Fishy : Severity.Invalid;
data.AddLine(Get(severe, NickLengthLong));
return true;
}
data.AddLine(GetValid(NickMatchNoOthers));
}
else
{
VerifyUnNicknamed(data, pk, nickname);
}
return false;
}
private void VerifyUnNicknamed(LegalityAnalysis data, PKM pk, ReadOnlySpan<char> nickname)
{
if (pk.Format < 3)
{
// pk1/pk2 IsNicknamed getter checks for match, logic should only reach here if matches.
data.AddLine(GetValid(NickMatchLanguage));
}
else
{
var enc = data.EncounterOriginal;
bool valid = IsNicknameValid(pk, enc, nickname);
var result = valid ? GetValid(NickMatchLanguage) : GetInvalid(NickMatchLanguageFail);
data.AddLine(result);
}
}
private static bool CanNicknameForeign8Plus(LegalityAnalysis data, PKM pk)
{
// I think this method needs to be rewritten and clarified.
var hist = data.Info.EvoChainsAllGens;
if (hist.HasVisitedSWSH || hist.HasVisitedZA)
return true;
if (pk.Format >= 9) // S/V disallows fateful encounter nicknaming
return !pk.FatefulEncounter;
return false;
}
private static int GetForeignNicknameLength(PKM pk, IEncounterTemplate match, byte origin)
{
// HOME gifts already verified prior to reaching here.
System.Diagnostics.Debug.Assert(match is not WC8 {IsHOMEGift:true});
int length = 0;
if (origin is (4 or 5 or 6 or 7) && match.IsEgg && pk.WasTradedEgg)
length = Legal.GetMaxLengthNickname(origin, English);
if (pk.FatefulEncounter)
return length;
if (pk.Format < 8 || pk.BDSP)
return length;
// Can only nickname if the language matches.
var future = Legal.GetMaxLengthNickname(pk.Format, (LanguageID)pk.Language);
return Math.Max(length, future);
}
private static bool IsNicknameValid(PKM pk, IEncounterTemplate enc, ReadOnlySpan<char> nickname)
{
ushort species = pk.Species;
byte format = pk.Format;
var context = pk.Context;
int language = pk.Language;
// Farfetchd and Sirfetchd have different apostrophes in HOME, only if transferred from 3DS or GO => HOME.
// HOME doesn't use the right apostrophe. ' vs
if (format >= 8 && species is (int)Species.Farfetchd or (int)Species.Sirfetchd)
{
// Evolving in-game fixes it. Check only un-evolved and affected languages.
if (species == enc.Species && SpeciesName.IsApostropheFarfetchdLanguage(language))
{
if (enc is { Generation: < 8, Context: not EntityContext.Gen7b }) // 3DS -> HOME
return nickname is "Farfetch'd";
if (enc is EncounterSlot8GO) // GO -> HOME
return species is (int)Species.Farfetchd ? nickname is "Farfetch'd" : nickname is "Sirfetch'd";
}
}
ReadOnlySpan<char> expect = SpeciesName.GetSpeciesNameGeneration(species, language, format);
if (nickname.SequenceEqual(expect))
return true;
// Can't have another language name if it hasn't evolved or wasn't a language-traded egg.
// Ash Greninja can be transferred from one demo language to another recipient language, while retaining the demo's name string.
// Starting in Generation 8, hatched language-traded eggs will take the Language from the trainer that hatched it.
// Also in Generation 8, evolving in a foreign language game will retain the original language as the source for the newly evolved species name.
// Transferring from Gen7->Gen8 realigns the Nickname string to the Language, if not nicknamed.
bool canHaveAnyLanguage = format <= 7 && (enc.Species != species || pk.WasTradedEgg || enc is WC7 {IsAshGreninja: true}) && !pk.GG;
if (canHaveAnyLanguage && !SpeciesName.IsNicknamedAnyLanguage(species, nickname, context))
return true;
if (enc is ILangNick loc && loc.Language != 0 && !loc.IsNicknamed && !SpeciesName.IsNicknamedAnyLanguage(species, nickname, context))
return true; // fixed language without nickname, nice job event maker!
if (format == 5 && enc.Generation != 5) // transfer
return IsMatch45(nickname, species, expect, language, canHaveAnyLanguage);
return false;
}
private static bool IsMatch45(ReadOnlySpan<char> nickname, ushort species, ReadOnlySpan<char> expect, int language, bool canHaveAnyLanguage)
{
if (species is (int)Species.Farfetchd)
{
if (nickname.Length == expect.Length)
{
// Compare as upper -- different apostrophe than Gen4 encoding.
if (IsMatchUpper45(nickname, expect))
return true;
}
if (SpeciesName.IsApostropheFarfetchdLanguage(language))
return false; // must have matched above
}
if (canHaveAnyLanguage)
return !SpeciesName.IsNicknamedAnyLanguage(species, nickname, EntityContext.Gen4);
expect = SpeciesName.GetSpeciesNameGeneration(species, language, 4);
return nickname.SequenceEqual(expect);
}
private static bool IsMatchUpper45(ReadOnlySpan<char> nickname, ReadOnlySpan<char> expect)
{
if (nickname.Length != expect.Length)
return false;
for (int i = 0; i < expect.Length; i++)
{
if (nickname[i] != char.ToUpperInvariant(expect[i]))
return false;
}
return true;
}
private static void VerifyNicknameEgg(LegalityAnalysis data)
{
var pk = data.Entity;
var enc = data.Info.EncounterMatch;
bool flagState = EggStateLegality.IsNicknameFlagSet(enc, pk);
if (pk.IsNicknamed != flagState)
data.AddLine(GetInvalid(CheckIdentifier.Egg, flagState ? NickFlagEggYes : NickFlagEggNo));
Span<char> nickname = stackalloc char[pk.TrashCharCountNickname];
int len = pk.LoadString(pk.NicknameTrash, nickname);
nickname = nickname[..len];
if (pk.Format == 2 && !SpeciesName.IsNicknamedAnyLanguage(0, nickname, EntityContext.Gen2))
data.AddLine(GetValid(CheckIdentifier.Egg, NickMatchLanguageEgg));
else if (!nickname.SequenceEqual(SpeciesName.GetEggName(pk.Language, enc.Generation)))
data.AddLine(GetInvalid(CheckIdentifier.Egg, NickMatchLanguageEggFail));
else
data.AddLine(GetValid(CheckIdentifier.Egg, NickMatchLanguageEgg));
}
private static void VerifyNicknameTrade(LegalityAnalysis data, IEncounterTemplate t)
{
switch (t)
{
case EncounterTrade8b b: VerifyTrade8b(data, b); return;
case EncounterTrade4PID t4: VerifyTrade4(data, t4); return;
case EncounterTrade5BW t5:
VerifyEncounterTrade5(data, t5); return;
default:
VerifyTrade(data, t, data.Entity.Language); return;
}
}
private void VerifyG1NicknameWithinBounds(LegalityAnalysis data, ReadOnlySpan<char> str)
{
var pk = data.Entity;
if (StringConverter1.GetIsEnglish(str))
{
if (str.Length > 10)
data.AddLine(GetInvalid(NickLengthLong, 10));
}
else if (StringConverter1.GetIsJapanese(str))
{
if (str.Length > 5)
data.AddLine(GetInvalid(NickLengthLong, 5));
}
else if (pk.Korean && StringConverter2KOR.GetIsKorean(str))
{
if (str.Length > 5)
data.AddLine(GetInvalid(NickLengthLong, 5));
}
else
{
data.AddLine(GetInvalid(G1CharNick));
}
}
private static void VerifyTrade4(LegalityAnalysis data, EncounterTrade4PID t)
{
var pk = data.Entity;
if (t.IsIncorrectEnglish(pk))
data.AddLine(GetInvalid(CheckIdentifier.Language, OTLanguageShouldBeLeq_0, (byte)Japanese));
var lang = t.DetectOriginalLanguage(pk);
VerifyTrade(data, t, lang);
}
private static void VerifyTrade8b(LegalityAnalysis data, EncounterTrade8b t)
{
var pk = data.Entity;
int lang = pk.Language;
if (t.Species == (int)Species.Magikarp)
{
if (t.IsMagikarpJapaneseTradedBDSP(pk))
{
// Traded replaces the OT Name. Verify only the Nickname now.
VerifyNickname(data, t, (int)German);
return;
}
Span<char> trainer = stackalloc char[pk.TrashCharCountTrainer];
int len = pk.LoadString(pk.OriginalTrainerTrash, trainer);
trainer = trainer[..len];
Span<char> nickname = stackalloc char[pk.TrashCharCountNickname];
len = pk.LoadString(pk.NicknameTrash, nickname);
nickname = nickname[..len];
lang = t.DetectMeisterMagikarpLanguage(nickname, trainer, lang);
if (lang == -1) // err
data.AddLine(GetInvalid(CheckIdentifier.Language, OTLanguageShouldBe_0or1, (byte)Japanese, (byte)German));
}
if (t.IsPijako(pk))
{
// Let it be anything (Nicknamed or not) and just verify the OT.
VerifyTradeOTOnly(data, t);
return;
}
VerifyTrade(data, t, lang);
}
private static void VerifyEncounterTrade5(LegalityAnalysis data, EncounterTrade5BW t)
{
var pk = data.Entity;
var lang = pk.Language;
if (pk.Format == 5 && lang == (int)Japanese)
data.AddLine(GetInvalid(CheckIdentifier.Language, OTLanguageShouldBe_0, 0));
lang = Math.Max(lang, 1);
VerifyTrade(data, t, lang);
}
private static void VerifyTradeOTOnly(LegalityAnalysis data, IFixedTrainer t)
{
var result = CheckTradeOTOnly(data, t);
data.AddLine(result);
}
private static CheckResult CheckTradeOTOnly(LegalityAnalysis data, IFixedTrainer t)
{
var pk = data.Entity;
if (pk.IsNicknamed && (pk.Format < 8 || pk.FatefulEncounter))
return GetInvalid(CheckIdentifier.Nickname, EncTradeChangedNickname);
int lang = pk.Language;
Span<char> trainer = stackalloc char[pk.TrashCharCountTrainer];
int len = pk.LoadString(pk.OriginalTrainerTrash, trainer);
trainer = trainer[..len];
if (!t.IsTrainerMatch(pk, trainer, lang))
return GetInvalid(CheckIdentifier.Trainer, EncTradeIndexBad);
return GetValid(CheckIdentifier.Nickname, EncTradeUnchanged);
}
private static void VerifyTrade(LegalityAnalysis data, IEncounterTemplate t, int language)
{
if (t is IFixedTrainer { IsFixedTrainer: true } ft)
VerifyTrainerName(data, ft, language);
if (t is IFixedNickname { IsFixedNickname: true } fn)
VerifyNickname(data, fn, language);
}
private static void VerifyNickname(LegalityAnalysis data, IFixedNickname fn, int language)
{
var pk = data.Entity;
var result = fn.IsNicknameMatch(pk, pk.Nickname, language)
? GetValid(CheckIdentifier.Nickname, EncTradeUnchanged)
: Get(CheckIdentifier.Nickname, ParseSettings.Settings.Nickname.NicknamedTrade(data.EncounterOriginal.Context), EncTradeChangedNickname);
data.AddLine(result);
}
private static void VerifyTrainerName(LegalityAnalysis data, IFixedTrainer ft, int language)
{
var pk = data.Entity;
Span<char> trainer = stackalloc char[pk.TrashCharCountTrainer];
int len = pk.LoadString(pk.OriginalTrainerTrash, trainer);
trainer = trainer[..len];
if (!ft.IsTrainerMatch(pk, trainer, language))
data.AddLine(GetInvalid(CheckIdentifier.Trainer, EncTradeChangedOT));
}
}