Support Gen5/3DS/Switch word filters (#4423)

* Support Gen5/3DS/Switch word filters

- Separate 3DS/Switch word filters
- Add/implement Gen5 word filter
- Adjust behavior of DisableWordFilterPastGen

* Implement halfwidth/fullwidth conversion

Only applies to alphanumeric and kana
This commit is contained in:
abcboy101 2025-01-26 11:38:21 -05:00 committed by GitHub
parent b119302d67
commit 6c3eec3314
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 5120 additions and 1021 deletions

View File

@ -1,95 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace PKHeX.Core;
/// <summary>
/// Bad-word Filter class containing logic to check against unsavory regular expressions.
/// </summary>
public static class WordFilter
{
/// <summary>
/// Regex patterns to check against
/// </summary>
/// <remarks>No need to keep the original pattern strings around; the <see cref="Regex"/> object retrieves this via <see cref="Regex.ToString()"/></remarks>
private static readonly Regex[] Regexes = LoadPatterns(Util.GetStringResource("badwords"));
// if you're running this as a server and don't mind a few extra seconds of startup, add RegexOptions.Compiled for slightly better checking.
private const RegexOptions Options = RegexOptions.CultureInvariant;
private static Regex[] LoadPatterns(ReadOnlySpan<char> patterns)
{
var lineCount = 1 + patterns.Count('\n');
var result = new Regex[lineCount];
int i = 0;
foreach (var line in patterns.EnumerateLines())
result[i++] = new Regex(line.ToString(), Options);
return result;
}
/// <summary>
/// Checks to see if a phrase contains filtered content.
/// </summary>
/// <param name="message">Phrase to check for</param>
/// <param name="regMatch">Matching regex that filters the phrase.</param>
/// <returns>Boolean result if the message is filtered or not.</returns>
public static bool TryMatch(ReadOnlySpan<char> message, [NotNullWhen(true)] out string? regMatch)
{
foreach (var regex in Regexes)
{
foreach (var _ in regex.EnumerateMatches(message))
{
regMatch = regex.ToString();
return true;
}
}
regMatch = null;
return false;
}
/// <summary>
/// Due to some messages repeating (Trainer names), keep a list of repeated values for faster lookup.
/// </summary>
private static readonly ConcurrentDictionary<string, string?>.AlternateLookup<ReadOnlySpan<char>> Lookup =
new ConcurrentDictionary<string, string?>().GetAlternateLookup<ReadOnlySpan<char>>();
/// <summary>
/// Checks to see if a phrase contains filtered content.
/// </summary>
/// <param name="message">Phrase to check for</param>
/// <param name="regMatch">Matching regex that filters the phrase.</param>
/// <returns>Boolean result if the message is filtered or not.</returns>
public static bool IsFiltered(ReadOnlySpan<char> message, [NotNullWhen(true)] out string? regMatch)
{
if (message.IsWhiteSpace() || message.Length <= 1)
{
regMatch = null;
return false;
}
// Check dictionary
if (Lookup.TryGetValue(message, out regMatch))
return regMatch != null;
// Make the string lowercase invariant
Span<char> lowercase = stackalloc char[message.Length];
message.ToLowerInvariant(lowercase);
// not in dictionary, check patterns
if (TryMatch(lowercase, out regMatch))
{
Lookup.TryAdd(message, regMatch);
return true;
}
// didn't match any pattern, cache result
if ((Lookup.Dictionary.Count & ~MAX_COUNT) != 0)
Lookup.Dictionary.Clear(); // reset
Lookup.TryAdd(message, regMatch = null);
return false;
}
private const int MAX_COUNT = (1 << 17) - 1; // arbitrary cap for max dictionary size
}

View File

@ -0,0 +1,70 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// Simplistic normalization of a string used by the Nintendo 3DS and Nintendo Switch games.
/// </summary>
public static class TextNormalizer
{
private const string Dakuten = "カキクケコサシスセソタチツテトハヒフヘホ"; // 'ウ' handled separately
private const string Handakuten = "ハヒフヘホ";
private const string FullwidthKana = "ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン";
private const string SmallKana = "ァィゥェォッャュョヮ"; // 'ヵ', 'ヶ' handled separately
/// <summary>
/// Normalize a string to a simplified form for checking against a bad-word list.
/// </summary>
/// <param name="input">Input string to normalize</param>
/// <param name="output">Output buffer to write the normalized string</param>
public static int Normalize(ReadOnlySpan<char> input, Span<char> output)
{
int ctr = 0;
for (int i = 0; i < input.Length; i++)
{
var c = input[i];
// Skip spaces and halfwidth dakuten/handakuten
if (c is ' ' or '\u3000' or '゙' or '゚')
continue;
// Handle combining halfwidth dakuten/handakuten
ushort ofs = 0;
if (c is >= 'ヲ' and <= 'ン' && i + 1 < input.Length)
{
var d = input[i + 1];
if (d == '゙' && Dakuten.Contains(c))
ofs = 1;
else if (d == '゚' && Handakuten.Contains(c))
ofs = 2;
else if (d == '゙' && c == 'ウ')
ofs = 'ヴ' - 'ウ'; // 0x4E (78)
}
// Fold characters treated identically
c = char.ToLowerInvariant(c); // fold to lowercase
c = (char)(c switch
{
>= 'ぁ' and <= 'ゖ' => c + 0x60, // shift hiragana to katakana
>= '' and <= '' or >= '' and <= '' => c - 0xFEE0, // shift fullwidth numbers/letters to halfwidth
>= 'ヲ' and <= 'ン' => FullwidthKana[c - 'ヲ'] + ofs, // shift halfwidth katakana to fullwidth
_ => c,
});
// Shift small kana to normal kana
if (c is >= 'ァ' and <= 'ヶ')
{
if (SmallKana.Contains(c))
c += (char)1;
else if (c == 'ヵ')
c = 'カ';
else if (c == 'ヶ')
c = 'ケ';
}
output[ctr] = c;
ctr++;
}
return ctr;
}
}

View File

@ -0,0 +1,116 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace PKHeX.Core;
/// <summary>
/// Bad-word Filter class containing logic to check against unsavory regular expressions.
/// </summary>
public static class WordFilter
{
// if you're running this as a server and don't mind a few extra seconds of startup, add RegexOptions.Compiled for slightly better checking.
private const RegexOptions Options = RegexOptions.CultureInvariant;
internal static Regex[] LoadPatterns(ReadOnlySpan<char> patterns)
{
// Make it lowercase invariant
Span<char> lowercase = stackalloc char[patterns.Length];
patterns.ToLowerInvariant(lowercase);
var lineCount = 1 + lowercase.Count('\n');
var result = new Regex[lineCount];
int i = 0;
foreach (var line in lowercase.EnumerateLines())
result[i++] = new Regex(line.ToString(), Options);
return result;
}
/// <summary>
/// Checks to see if a phrase contains filtered content.
/// </summary>
/// <param name="message">Phrase to check</param>
/// <param name="regexes">Console regex set to check against.</param>
/// <param name="regMatch">Matching regex that filters the phrase.</param>
/// <returns>Boolean result if the message is filtered or not.</returns>
internal static bool TryMatch(ReadOnlySpan<char> message, ReadOnlySpan<Regex> regexes, [NotNullWhen(true)] out string? regMatch)
{
// Clean the string
Span<char> clean = stackalloc char[message.Length];
int ctr = TextNormalizer.Normalize(message, clean);
if (ctr != clean.Length)
clean = clean[..ctr];
foreach (var regex in regexes)
{
foreach (var _ in regex.EnumerateMatches(clean))
{
regMatch = regex.ToString();
return true;
}
}
regMatch = null;
return false;
}
/// <inheritdoc cref="IsFiltered(ReadOnlySpan{char}, out string?, EntityContext, EntityContext)"/>
public static bool IsFiltered(ReadOnlySpan<char> message, [NotNullWhen(true)] out string? regMatch,
EntityContext current)
=> IsFiltered(message, out regMatch, current, current);
/// <summary>
/// Checks to see if a phrase contains filtered content.
/// </summary>
/// <param name="message">Phrase to check for</param>
/// <param name="regMatch">Matching regex that filters the phrase.</param>
/// <param name="current">Current context to check.</param>
/// <param name="original">Earliest context to check.</param>
/// <returns>Boolean result if the message is filtered or not.</returns>
public static bool IsFiltered(ReadOnlySpan<char> message, [NotNullWhen(true)] out string? regMatch,
EntityContext current, EntityContext original)
{
regMatch = null;
if (message.IsWhiteSpace() || message.Length <= 1)
return false;
// Only check against the single filter if requested
if (ParseSettings.Settings.WordFilter.DisableWordFilterPastGen)
return IsFilteredCurrentOnly(message, ref regMatch, current, original);
return IsFilteredLookBack(message, out regMatch, current, original);
}
private static bool IsFilteredCurrentOnly(ReadOnlySpan<char> message, ref string? regMatch,
EntityContext current, EntityContext original) => current switch
{
EntityContext.Gen5 => WordFilter5.IsFiltered(message, out regMatch),
EntityContext.Gen6 => WordFilter3DS.IsFilteredGen6(message, out regMatch),
EntityContext.Gen7 when original is EntityContext.Gen6
=> WordFilter3DS.IsFilteredGen6(message, out regMatch),
EntityContext.Gen7 => WordFilter3DS.IsFilteredGen7(message, out regMatch),
_ => current.GetConsole() switch
{
GameConsole.NX => WordFilterNX.IsFiltered(message, out regMatch, original),
_ => false,
},
};
private static bool IsFilteredLookBack(ReadOnlySpan<char> message, [NotNullWhen(true)] out string? regMatch,
EntityContext current, EntityContext original)
{
// Switch 2 backwards transfer? Won't know for another couple years.
if (WordFilterNX.IsFiltered(message, out regMatch, original))
return true;
var generation = original.Generation();
if (generation > 7 || original is EntityContext.Gen7b)
return false;
if (WordFilter3DS.IsFiltered(message, out regMatch, original))
return true;
return generation == 5 && WordFilter5.IsFiltered(message, out regMatch);
// no other word filters (none in Gen3 or Gen4)
}
}

View File

@ -0,0 +1,91 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace PKHeX.Core;
/// <summary>
/// Word filter for 3DS games.
/// </summary>
public static class WordFilter3DS
{
private static readonly Regex[] Regexes = WordFilter.LoadPatterns(Util.GetStringResource("badwords_3ds"));
/// <summary>
/// Regex patterns to check against
/// </summary>
/// <remarks>No need to keep the original pattern strings around; the <see cref="Regex"/> object retrieves this via <see cref="Regex.ToString()"/></remarks>
private static readonly ConcurrentDictionary<string, string?>.AlternateLookup<ReadOnlySpan<char>> Lookup =
new ConcurrentDictionary<string, string?>().GetAlternateLookup<ReadOnlySpan<char>>();
private const int MAX_COUNT = (1 << 17) - 1; // arbitrary cap for max dictionary size
/// <inheritdoc cref="IsFiltered"/>
/// <remarks>Generation 6 is case-sensitive.</remarks>
public static bool IsFilteredGen6(ReadOnlySpan<char> message, [NotNullWhen(true)] out string? regMatch)
=> IsFiltered(message, out regMatch, EntityContext.Gen6);
/// <inheritdoc cref="IsFiltered"/>
/// <remarks>Generation 7 is case-insensitive.</remarks>
public static bool IsFilteredGen7(ReadOnlySpan<char> message, [NotNullWhen(true)] out string? regMatch)
=> IsFiltered(message, out regMatch, EntityContext.Gen7);
/// <summary>
/// Checks to see if a phrase contains filtered content.
/// </summary>
/// <param name="message">Phrase to check</param>
/// <param name="regMatch">Matching regex that filters the phrase.</param>
/// <param name="original">Earliest context to check.</param>
/// <returns>Boolean result if the message is filtered or not.</returns>
public static bool IsFiltered(ReadOnlySpan<char> message, [NotNullWhen(true)] out string? regMatch, EntityContext original)
{
regMatch = null;
if (IsSpeciesName(message, original))
return false;
// Check dictionary
if (Lookup.TryGetValue(message, out regMatch))
return regMatch != null;
// not in dictionary, check patterns
if (WordFilter.TryMatch(message, Regexes, out regMatch))
{
Lookup.TryAdd(message, regMatch);
return true;
}
// didn't match any pattern, cache result
if ((Lookup.Dictionary.Count & ~MAX_COUNT) != 0)
Lookup.Dictionary.Clear(); // reset
Lookup.TryAdd(message, regMatch = null);
return false;
}
/// <summary>
/// Check if the message is a species name
/// </summary>
/// <param name="message">Phrase to check</param>
/// <param name="original">Earliest context to check.</param>
public static bool IsSpeciesName(ReadOnlySpan<char> message, EntityContext original)
{
// Gen6 is case-sensitive, Gen7 is case-insensitive.
if (original is EntityContext.Gen6) // Match case
return IsSpeciesNameGen6(message);
return IsSpeciesNameGen7(message);
}
private static bool IsSpeciesNameGen7(ReadOnlySpan<char> message)
{
if (!SpeciesName.TryGetSpeciesAnyLanguageCaseInsensitive(message, out var s7, 7))
return false;
return s7 <= Legal.MaxSpeciesID_7_USUM;
}
private static bool IsSpeciesNameGen6(ReadOnlySpan<char> message)
{
if (!SpeciesName.TryGetSpeciesAnyLanguage(message, out var s6, 6))
return false;
return s6 <= Legal.MaxSpeciesID_6;
}
}

View File

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace PKHeX.Core;
public static class WordFilter5
{
private static readonly HashSet<string>.AlternateLookup<ReadOnlySpan<char>> Words =
new HashSet<string>(Util.GetStringList("badwords_gen5"))
.GetAlternateLookup<ReadOnlySpan<char>>();
/// <summary>
/// Checks to see if a phrase contains filtered content.
/// </summary>
/// <param name="message">Phrase to check</param>
/// <param name="match">Blocked word that filters the phrase.</param>
/// <returns>Boolean result if the message is filtered or not.</returns>
public static bool IsFiltered(ReadOnlySpan<char> message, [NotNullWhen(true)] out string? match)
{
Span<char> clean = stackalloc char[message.Length];
Normalize(message, clean);
return Words.TryGetValue(clean, out match);
}
/// <summary>
/// Normalize a string to a simplified form for checking against a bad-word list.
/// </summary>
/// <param name="input">Input string to normalize</param>
/// <param name="output">Output buffer to write the normalized string</param>
public static void Normalize(ReadOnlySpan<char> input, Span<char> output)
{
Debug.Assert(input.Length == output.Length);
for (int i = 0; i < input.Length; i++)
{
var c = input[i];
c = char.ToUpperInvariant(c); // fold to uppercase
c = (char)(c switch
{
>= 'ァ' and <= 'ヶ' => c - 0x60, // shift katakana to hiragana
>= '' and <= '' => c - 0xFEE0, // shift fullwidth letters to halfwidth
_ => c,
});
output[i] = c;
}
}
}

View File

@ -0,0 +1,65 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace PKHeX.Core;
/// <summary>
/// Word filter for Switch games.
/// </summary>
public static class WordFilterNX
{
/// <summary>
/// Regex patterns to check against
/// </summary>
/// <remarks>No need to keep the original pattern strings around; the <see cref="Regex"/> object retrieves this via <see cref="Regex.ToString()"/></remarks>
private static readonly Regex[] Regexes = WordFilter.LoadPatterns(Util.GetStringResource("badwords_switch"));
/// <summary>
/// Due to some messages repeating (Trainer names), keep a list of repeated values for faster lookup.
/// </summary>
private static readonly ConcurrentDictionary<string, string?>.AlternateLookup<ReadOnlySpan<char>> Lookup =
new ConcurrentDictionary<string, string?>().GetAlternateLookup<ReadOnlySpan<char>>();
private const int MAX_COUNT = (1 << 17) - 1; // arbitrary cap for max dictionary size
/// <summary>
/// Checks to see if a phrase contains filtered content.
/// </summary>
/// <param name="message">Phrase to check</param>
/// <param name="regMatch">Matching regex that filters the phrase.</param>
/// <param name="original">Earliest context to check.</param>
/// <returns>Boolean result if the message is filtered or not.</returns>
public static bool IsFiltered(ReadOnlySpan<char> message, [NotNullWhen(true)] out string? regMatch, EntityContext original)
{
regMatch = null;
if (IsSpeciesName(message, original))
return false;
// Check dictionary
if (Lookup.TryGetValue(message, out regMatch))
return regMatch != null;
// not in dictionary, check patterns
if (WordFilter.TryMatch(message, Regexes, out regMatch))
{
Lookup.TryAdd(message, regMatch);
return true;
}
// didn't match any pattern, cache result
if ((Lookup.Dictionary.Count & ~MAX_COUNT) != 0)
Lookup.Dictionary.Clear(); // reset
Lookup.TryAdd(message, regMatch = null);
return false;
}
private static bool IsSpeciesName(ReadOnlySpan<char> message, EntityContext origin)
{
var gen = origin.Generation();
if (!SpeciesName.TryGetSpeciesAnyLanguageCaseInsensitive(message, out var species, gen))
return false;
return species <= origin.GetSingleGameVersion().GetMaxSpeciesID();
}
}

View File

@ -5,11 +5,11 @@ namespace PKHeX.Core;
[TypeConverter(typeof(ExpandableObjectConverter))]
public sealed class WordFilterSettings
{
[LocalizedDescription("Checks player given Nicknames and Trainer Names for profanity. Bad words will be flagged using the 3DS console's regex lists.")]
[LocalizedDescription("Checks player given Nicknames and Trainer Names for profanity. Bad words will be flagged using the appropriate console's lists.")]
public bool CheckWordFilter { get; set; } = true;
[LocalizedDescription("Disables the Word Filter check for formats prior to 3DS-era.")]
[LocalizedDescription("Disables retroactive Word Filter checks for earlier formats.")]
public bool DisableWordFilterPastGen { get; set; }
public bool IsEnabled(int gen) => CheckWordFilter && (!DisableWordFilterPastGen || gen >= 6);
public bool IsEnabled(int gen) => CheckWordFilter && (!DisableWordFilterPastGen || gen >= 5);
}

View File

@ -67,7 +67,8 @@ public override void Verify(LegalityAnalysis data)
// Non-nicknamed strings have already been checked.
if (ParseSettings.Settings.WordFilter.IsEnabled(pk.Format) && pk.IsNicknamed)
{
if (WordFilter.IsFiltered(nickname, out var badPattern))
var mostRecentNicknameContext = pk.Format >= 8 ? pk.Context : enc.Context;
if (WordFilter.IsFiltered(nickname, out var badPattern, pk.Context, mostRecentNicknameContext))
data.AddLine(GetInvalid($"Word Filter: {badPattern}"));
if (TrainerNameVerifier.ContainsTooManyNumbers(nickname, data.Info.Generation))
data.AddLine(GetInvalid("Word Filter: Too many numbers."));
@ -164,11 +165,9 @@ private bool VerifyUnNicknamedEncounter(LegalityAnalysis data, PKM pk, ReadOnlyS
return true;
}
}
foreach (var language in Language.GetAvailableGameLanguages(pk.Format))
if (SpeciesName.TryGetSpeciesAnyLanguage(nickname, out var species, pk.Format))
{
if (!SpeciesName.TryGetSpecies(nickname, language, out var species))
continue;
var msg = species == pk.Species && language != pk.Language ? LNickMatchNoOthersFail : LNickMatchLanguageFlag;
var msg = species == pk.Species ? LNickMatchLanguageFlag : LNickMatchNoOthersFail;
data.AddLine(Get(msg, ParseSettings.Settings.Nickname.NicknamedAnotherSpecies));
return true;
}

View File

@ -33,7 +33,7 @@ public override void Verify(LegalityAnalysis data)
trainer = trainer[..len];
if (trainer.Contains('\uffff') && pk is { Format: 4 })
{
data.AddLine(GetInvalid("Trainer Name: Unkown Character"));
data.AddLine(GetInvalid("Trainer Name: Unknown Character"));
return;
}
@ -54,14 +54,14 @@ public override void Verify(LegalityAnalysis data)
if (ParseSettings.Settings.WordFilter.IsEnabled(pk.Format))
{
if (WordFilter.IsFiltered(trainer, out var badPattern))
if (WordFilter.IsFiltered(trainer, out var badPattern, pk.Context, enc.Context))
data.AddLine(GetInvalid($"Word Filter: {badPattern}"));
if (ContainsTooManyNumbers(trainer, data.Info.Generation))
data.AddLine(GetInvalid("Word Filter: Too many numbers."));
Span<char> ht = stackalloc char[pk.TrashCharCountTrainer];
int nameLen = pk.LoadString(pk.HandlingTrainerTrash, ht);
if (WordFilter.IsFiltered(ht[..nameLen], out badPattern))
if (WordFilter.IsFiltered(ht[..nameLen], out badPattern, pk.Context)) // HT context is always the current context
data.AddLine(GetInvalid($"Word Filter: {badPattern}"));
}
}

View File

@ -52,7 +52,10 @@ public static class SpeciesName
/// </summary>
private static readonly Dictionary<string, ushort>.AlternateLookup<ReadOnlySpan<char>>[] SpeciesDict = GetDictionary(SpeciesLang);
private static Dictionary<string, ushort>.AlternateLookup<ReadOnlySpan<char>>[] GetDictionary(string[][] names)
/// <inheritdoc cref="SpeciesDict"/>
private static readonly Dictionary<string, ushort>.AlternateLookup<ReadOnlySpan<char>>[] SpeciesDictLower = GetDictionary(SpeciesLang, true);
private static Dictionary<string, ushort>.AlternateLookup<ReadOnlySpan<char>>[] GetDictionary(string[][] names, bool lower = false)
{
var result = new Dictionary<string, ushort>.AlternateLookup<ReadOnlySpan<char>>[names.Length];
for (int i = 0; i < result.Length; i++)
@ -61,7 +64,10 @@ public static class SpeciesName
var capacity = Math.Max(speciesList.Length - 1, 0);
var dict = new Dictionary<string, ushort>(capacity);
for (ushort species = 1; species < speciesList.Length; species++)
dict[speciesList[species]] = species;
{
var key = speciesList[species];
dict[lower ? key.ToLowerInvariant() : key] = species;
}
result[i] = dict.GetAlternateLookup<ReadOnlySpan<char>>();
}
return result;
@ -323,9 +329,28 @@ public static bool TryGetSpecies(ReadOnlySpan<char> speciesName, int language, o
return SpeciesDict[language].TryGetValue(speciesName, out species);
}
/// <inheritdoc cref="TryGetSpecies(ReadOnlySpan{char}, int, out ushort)"/>
public static bool TryGetSpecies(string speciesName, int language, out ushort species)
public static bool TryGetSpeciesAnyLanguage(ReadOnlySpan<char> speciesName, out ushort species, byte generation = LatestGeneration)
{
return SpeciesDict[language].TryGetValue(speciesName, out species);
foreach (var language in Language.GetAvailableGameLanguages(generation))
{
if (SpeciesDict[language].TryGetValue(speciesName, out species))
return true;
}
species = 0;
return false;
}
public static bool TryGetSpeciesAnyLanguageCaseInsensitive(ReadOnlySpan<char> speciesName, out ushort species, byte generation = LatestGeneration)
{
Span<char> lowercase = stackalloc char[speciesName.Length];
speciesName.ToLowerInvariant(lowercase);
foreach (var language in Language.GetAvailableGameLanguages(generation))
{
if (SpeciesDictLower[language].TryGetValue(lowercase, out species))
return true;
}
species = 0;
return false;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,362 @@
9/11
ABRUTI
ABRUTIE
ADHD
AFFANCULO
ANAL
ANALPLUG
ANALSEX
ARSCH
ARSCHLOCH
ASS
BAGASCIA
BAISE
BAISER
BAISé
BALDRACCA
BASTARD
BATARD
BATTONA
BITCH
BITE
BLOWJOB
BOCCHINARA
BOCCHINARO
BOLLERA
BOUGNOUL
BRANLEUR
BULLSHIT
BURNE
CABRON
CABRONA
CABRONAZO
CABRóN
CAPULLA
CAPULLO
CAZZI
CAZZO
CHIAVARE
CHICHI
CHIER
CHINK
CHOCHO
CLIT
COCK
COCKSUCKER
COCU
COGLIONE
COJON
COJONES
COJóN
COMEPOLLAS
CON
CONNARD
CONNASSE
CONNE
CONO
COON
COUILLE
COUILLON
COUILLONNE
COñO
CREVARD
CUL
CULATTONE
CULO
CUM
CUMSHOT
CUNT
DAMN
DICK
DICKHEAD
DILDO
DIO BESTIA
DIO CANE
DIO PORCO
DRECKSACK
DRECKSAU
DYKE
ENCULE
ENCULEE
ENCULER
ENCULé
ENCULéE
ENFOIRE
ENFOIRé
F.U.C.K.
FAG
FAGGOT
FAGS
FANCULO
FCUK
FICA
FICKEN
FICKFRESSE
FIGA
FION
FOLLAR
FOLLEN
FOTTERE
FOTZE
FOUTRE
FROCIO
FUCK
FUCKER
FUCT
FUK
FURCIA
GILIPOLLAS
GOBSHITE
GODDAMN
GYPO
HACKFRESSE
HANDJOB
HIJAPUTA
HIJO PUTA
HIJOPUTA
HITLER
HOLOCAUST
HOMO
HORE
HOSTIA
HURENSOHN
INCULARE
JESUSSUCKS
JIZZ
JIZZUM
JODER
JODETE
JOPUTA
JUDENSAU
JóDETE
KACKE
KAFFIR
KANACKE
KIKE
KUNT
KZ
LESBO
MAMADA
MAMON
MAMONA
MAMóN
MARICA
MARICON
MARICONA
MARICONAZO
MARICóN
MASTURBATE
MERDE
MIGNOTTA
MINCHIA
MISSGEBURT
MOLEST
NAZI
NEGER
NEGRE
NEGRESSE
NEGRO
NIGGER
NIQUE
NIQUER
NUTTE
NèGRE
NéGRESSE
OJETE
OSTIA
PADOPHILER
PAEDO
PAEDOPHILE
PAJILLERO
PAKI
PARTOUZE
PD
PECKER
PEDE
PEDO
PEDOFILE
PEDOPHILE
PENDON
PENDóN
PENIS
PETASSE
PHUK
PICHA
PINE
PISSER
POLLA
POLLON
POLLóN
POLVO
POMPINARA
POMPINO
POOF
PORCO DIO
POTORRO
POUFFE
POUFFIASSE
PUSSY
PUTA
PUTAIN
PUTE
PUTO
PUTON
PUTTANA
PUTóN
PäDOPHILER
PéDé
PéTASSE
QUEER
RAPE
RAPED
RAPES
RAPIST
RICCHIONE
ROTTINCULO
SA
SALAUD
SALOP
SALOPARD
SALOPE
SAU
SBORRA
SCHEIßE
SCHLAMPE
SCHWANZ
SCHWUCHTEL
SCROTUM
SEGAIOLO
SEX
SHIT
SHIZ
SIEG HEIL!
SLAG
SLUT
SODOMIE
SPASTI
SPASTIC
SPAZ
SPERM
SPUNK
SS
STRICHER
SUCER
TAPETTE
TARE
TARé
TITS
TORTILLERA
TROIA
TROIETTA
TROIONA
TROIONE
TWAT
VAFFANCULO
VAGIN
VAGINA
VOLLIDIOT
VULVA
WANK
WANKER
WETBACK
WHOR
WHORE
WICHSER
WOG
ZOB
ZOCCOLA
ZORRON
ZORRóN
あいえき
いぬごろし
いんぱい
いんもう
うんこ
うんち
おまんこ
おめこ
かたわ
きちがい
くろんぼ
けとう
ころす
ごうかん
さんごくじん
しなじん
しね
せいえき
せっくす
ちゃんころ
ちんこ
ちんちん
ちんば
ちんぽ
つんぼ
とさつ
どかた
どもり
にぐろ
にんぴにん
ひにん
びっこ
ふぇら
ぶらく
ぺにす
まんこ
めくら
やらせろ
やりまん
りょうじょく
れいぷ
ろりこん
ファック
강간
개새끼
개지랄
걸레같은년
걸레년
귀두
내꺼빨아
내꺼핥아
니미랄
딸딸이
미친년
미친놈
병신
보지
부랄
불알
빠구리
빠굴이
사까시
성감대
성관계
성폭행
성행위
섹스
시팔년
시팔놈
쌍넘
쌍년
쌍놈
쌍뇬
씨발
씨발넘
씨발년
씨발놈
씨발뇬
씹새끼
엄창
염병
오르가즘
왕자지
유두
자지
잠지
정액
좆까
창녀
콘돔
클리토리스
페니스
후장

View File

@ -170,7 +170,7 @@ LocalizedDescription.CheckWordFilter=Überprüft Spitznamen und Trainer Namen na
LocalizedDescription.CurrentHandlerMismatch=Schweregrad mit dem der aktuelle Besitzer eines Pokémons in der Legalitäts Analyse geprüft wird.
LocalizedDescription.DefaultBoxExportNamer=Selected File namer to use for box exports for the GUI, if multiple are available.
LocalizedDescription.DisableScalingDpi=Disables the GUI scaling based on Dpi on program startup, falling back to font scaling.
LocalizedDescription.DisableWordFilterPastGen=Disables the Word Filter check for formats prior to 3DS-era.
LocalizedDescription.DisableWordFilterPastGen=Disables retroactive Word Filter checks for earlier formats.
LocalizedDescription.ExportLegalityVerboseProperties=Display all properties of the encounter (auto-generated) when exporting a verbose report.
LocalizedDescription.ExtraProperties=Extra entity properties to try and show in addition to the default properties displayed.
LocalizedDescription.Female=Female gender color.

View File

@ -166,11 +166,11 @@ LocalizedDescription.BAKEnabled=Automatic Save File Backups Enabled
LocalizedDescription.BAKPrompt=Tracks if the "Create Backup" prompt has been issued to the user.
LocalizedDescription.BoxExport=Settings to use for box exports.
LocalizedDescription.CheckActiveHandler=Checks the last loaded player save file data and Current Handler state to determine if the Pokémon's Current Handler does not match the expected value.
LocalizedDescription.CheckWordFilter=Checks player given Nicknames and Trainer Names for profanity. Bad words will be flagged using the 3DS console's regex lists.
LocalizedDescription.CheckWordFilter=Checks player given Nicknames and Trainer Names for profanity. Bad words will be flagged using the appropriate console's lists.
LocalizedDescription.CurrentHandlerMismatch=Severity to flag a Legality Check if Pokémon's Current Handler does not match the expected value.
LocalizedDescription.DefaultBoxExportNamer=Selected File namer to use for box exports for the GUI, if multiple are available.
LocalizedDescription.DisableScalingDpi=Disables the GUI scaling based on Dpi on program startup, falling back to font scaling.
LocalizedDescription.DisableWordFilterPastGen=Disables the Word Filter check for formats prior to 3DS-era.
LocalizedDescription.DisableWordFilterPastGen=Disables retroactive Word Filter checks for earlier formats.
LocalizedDescription.ExportLegalityVerboseProperties=Display all properties of the encounter (auto-generated) when exporting a verbose report.
LocalizedDescription.ExtraProperties=Extra entity properties to try and show in addition to the default properties displayed.
LocalizedDescription.Female=Female gender color.

View File

@ -166,11 +166,11 @@ LocalizedDescription.BAKEnabled=Sauvegarde automatique des fichiers activée
LocalizedDescription.BAKPrompt=Tracks if the "Create Backup" prompt has been issued to the user.
LocalizedDescription.BoxExport=Settings to use for box exports.
LocalizedDescription.CheckActiveHandler=Checks the last loaded player save file data and Current Handler state to determine if the Pokémon's Current Handler does not match the expected value.
LocalizedDescription.CheckWordFilter=Checks player given Nicknames and Trainer Names for profanity. Bad words will be flagged using the 3DS console's regex lists.
LocalizedDescription.CheckWordFilter=Checks player given Nicknames and Trainer Names for profanity. Bad words will be flagged using the appropriate console's lists.
LocalizedDescription.CurrentHandlerMismatch=Severity to flag a Legality Check if Pokémon's Current Handler does not match the expected value.
LocalizedDescription.DefaultBoxExportNamer=Selected File namer to use for box exports for the GUI, if multiple are available.
LocalizedDescription.DisableScalingDpi=Disables the GUI scaling based on Dpi on program startup, falling back to font scaling.
LocalizedDescription.DisableWordFilterPastGen=Disables the Word Filter check for formats prior to 3DS-era.
LocalizedDescription.DisableWordFilterPastGen=Disables retroactive Word Filter checks for earlier formats.
LocalizedDescription.ExportLegalityVerboseProperties=Display all properties of the encounter (auto-generated) when exporting a verbose report.
LocalizedDescription.ExtraProperties=Extra entity properties to try and show in addition to the default properties displayed.
LocalizedDescription.Female=Female gender color.

View File

@ -170,7 +170,7 @@ LocalizedDescription.CheckWordFilter=Controlla la volgarità di Soprannomi e Nom
LocalizedDescription.CurrentHandlerMismatch=Forza una segnalazione di legalità se l'Ultimo Allenatore non corrisponde al valore aspettato.
LocalizedDescription.DefaultBoxExportNamer=Selected File namer to use for box exports for the GUI, if multiple are available.
LocalizedDescription.DisableScalingDpi=Disables the GUI scaling based on Dpi on program startup, falling back to font scaling.
LocalizedDescription.DisableWordFilterPastGen=Disables the Word Filter check for formats prior to 3DS-era.
LocalizedDescription.DisableWordFilterPastGen=Disables retroactive Word Filter checks for earlier formats.
LocalizedDescription.ExportLegalityVerboseProperties=Display all properties of the encounter (auto-generated) when exporting a verbose report.
LocalizedDescription.ExtraProperties=Extra entity properties to try and show in addition to the default properties displayed.
LocalizedDescription.Female=Female gender color.

View File

@ -166,11 +166,11 @@ LocalizedDescription.BAKEnabled=세이브 파일 자동 백업 사용
LocalizedDescription.BAKPrompt=Tracks if the "Create Backup" prompt has been issued to the user.
LocalizedDescription.BoxExport=Settings to use for box exports.
LocalizedDescription.CheckActiveHandler=Checks the last loaded player save file data and Current Handler state to determine if the Pokémon's Current Handler does not match the expected value.
LocalizedDescription.CheckWordFilter=Checks player given Nicknames and Trainer Names for profanity. Bad words will be flagged using the 3DS console's regex lists.
LocalizedDescription.CheckWordFilter=Checks player given Nicknames and Trainer Names for profanity. Bad words will be flagged using the appropriate console's lists.
LocalizedDescription.CurrentHandlerMismatch=Severity to flag a Legality Check if Pokémon's Current Handler does not match the expected value.
LocalizedDescription.DefaultBoxExportNamer=Selected File namer to use for box exports for the GUI, if multiple are available.
LocalizedDescription.DisableScalingDpi=Disables the GUI scaling based on Dpi on program startup, falling back to font scaling.
LocalizedDescription.DisableWordFilterPastGen=Disables the Word Filter check for formats prior to 3DS-era.
LocalizedDescription.DisableWordFilterPastGen=Disables retroactive Word Filter checks for earlier formats.
LocalizedDescription.ExportLegalityVerboseProperties=Display all properties of the encounter (auto-generated) when exporting a verbose report.
LocalizedDescription.ExtraProperties=Extra entity properties to try and show in addition to the default properties displayed.
LocalizedDescription.Female=Female gender color.

View File

@ -170,7 +170,7 @@ LocalizedDescription.CheckWordFilter=檢查昵稱和訓練家名稱是否存在
LocalizedDescription.CurrentHandlerMismatch=如果寶可夢現時持有人與預期值不匹配,則使用高等級合法性檢查。
LocalizedDescription.DefaultBoxExportNamer=Selected File namer to use for box exports for the GUI, if multiple are available.
LocalizedDescription.DisableScalingDpi=Disables the GUI scaling based on Dpi on program startup, falling back to font scaling.
LocalizedDescription.DisableWordFilterPastGen=Disables the Word Filter check for formats prior to 3DS-era.
LocalizedDescription.DisableWordFilterPastGen=Disables retroactive Word Filter checks for earlier formats.
LocalizedDescription.ExportLegalityVerboseProperties=Display all properties of the encounter (auto-generated) when exporting a verbose report.
LocalizedDescription.ExtraProperties=Extra entity properties to try and show in addition to the default properties displayed.
LocalizedDescription.Female=Female gender color.

View File

@ -13,13 +13,78 @@ public class LegalityTest
static LegalityTest() => TestUtil.InitializeLegality();
[Theory]
[InlineData("censor")]
[InlineData("buttnugget")]
[InlineData("18넘")]
[InlineData("inoffensive", false)]
public void CensorsBadWords(string badword, bool value = true)
[InlineData("Ass")]
[InlineData("")]
[InlineData("9/11")]
[InlineData("", false)]
[InlineData("baise")]
[InlineData("baisé", false)]
[InlineData("BAISÉ", false)]
[InlineData("scheiße")]
[InlineData("SCHEISSE", false)]
[InlineData("RICCHIONE ")]
[InlineData("RICCHIONE", false)]
[InlineData("せっくす")]
[InlineData("セックス")]
[InlineData("ふぁっく", false)]
[InlineData("ファック", false)]
[InlineData("kofagrigus", false)]
[InlineData("cofagrigus", false)]
public void CensorsBadWordsGen5(string badword, bool value = true)
{
WordFilter.TryMatch(badword, out _).Should().Be(value, "the word should have been identified as a bad word");
var result = WordFilter5.IsFiltered(badword, out _);
result.Should().Be(value, $"the word {(value ? "should" : "should not")} have been identified as a bad word");
}
[Theory]
[InlineData("kofagrigus")]
[InlineData("cofagrigus")]
[InlineData("Cofagrigus", false)]
public void CensorsBadWordsGen6(string badword, bool value = true)
{
var result = WordFilter3DS.IsFilteredGen6(badword, out _);
result.Should().Be(value, $"the word {(value ? "should" : "should not")} have been identified as a bad word");
}
[Theory]
[InlineData("badword")]
[InlineData("butt nuggets")]
[InlineData("18년")]
[InlineData("ふぁっく")]
[InlineData("")]
[InlineData("")]
[InlineData("gmail.com")]
[InlineData("kofagrigus")]
[InlineData("cofagrigus", false)]
[InlineData("Cofagrigus", false)]
[InlineData("inoffensive", false)]
public void CensorsBadWordsGen7(string badword, bool value = true)
{
var result = WordFilter3DS.IsFilteredGen7(badword, out _);
result.Should().Be(value, $"the word {(value ? "should" : "should not")} have been identified as a bad word");
}
[Theory]
[InlineData("badword")]
[InlineData("butt nuggets")]
[InlineData("18넘")]
[InlineData("ふぁっく")]
[InlineData("ヴァギナ")]
[InlineData("オッパイ")]
[InlineData("ファッ゙ク")]
[InlineData("ファヅク", false)]
[InlineData("ファッグ", false)]
[InlineData("sh!t")]
[InlineData("sht", false)]
[InlineData("abu$e")]
[InlineData("kofagrigus")]
[InlineData("cofagrigus", false)]
[InlineData("Cofagrigus", false)]
[InlineData("inoffensive", false)]
public void CensorsBadWordsSwitch(string badword, bool value = true)
{
var result = WordFilterNX.IsFiltered(badword, out _, EntityContext.Gen9);
result.Should().Be(value, $"the word {(value ? "should" : "should not")} have been identified as a bad word");
}
[Theory]