fix: Fixes and optimizes NameVerification and ProfanityProtection (#2153)
### Summary Significantly improves the performance of NameVerification & ProfanityProtection: ```cs | Method | Mean | Error | StdDev | |--------------- |----------:|----------:|----------:| | ValidateName | 728.34 ns | 13.244 ns | 11.059 ns | | ValidateNameSV | 26.62 ns | 0.233 ns | 0.207 ns | ```
This commit is contained in:
parent
0ce2a62a76
commit
a94814a7ef
9 changed files with 624 additions and 440 deletions
137
Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs
Normal file
137
Projects/UOContent.Tests/Tests/Misc/NameVerificationTests.cs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
using System.Buffers;
|
||||
using Server.Misc;
|
||||
using Xunit;
|
||||
|
||||
namespace Server.Tests;
|
||||
|
||||
public class NameVerificationTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("John")]
|
||||
[InlineData("Mary Ann")]
|
||||
[InlineData("Bob-Jones")]
|
||||
[InlineData("O'Malley")]
|
||||
public void ValidatePlayerName_ValidNames_ReturnsTrue(string name)
|
||||
{
|
||||
Assert.True(NameVerification.ValidatePlayerName(name));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Rex")]
|
||||
[InlineData("MrWhiskers")]
|
||||
[InlineData("Fido")]
|
||||
public void ValidatePetName_ValidNames_ReturnsTrue(string name)
|
||||
{
|
||||
Assert.True(NameVerification.ValidatePetName(name));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Mrs-Smith")]
|
||||
[InlineData("Mr. Whiskers")]
|
||||
[InlineData("Dog123")]
|
||||
public void ValidatePetName_InvalidNames_ReturnsFalse(string name)
|
||||
{
|
||||
Assert.False(NameVerification.ValidatePetName(name));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Blacksmith12")]
|
||||
[InlineData("Baker")]
|
||||
[InlineData("Innkeeper")]
|
||||
public void ValidateVendorName_ValidNames_ReturnsTrue(string name)
|
||||
{
|
||||
Assert.True(NameVerification.ValidateVendorName(name));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Blacksmith 123")]
|
||||
[InlineData("Baker-Smith")]
|
||||
[InlineData("*Innkeeper*")]
|
||||
public void ValidateVendorName_ValidNames_ReturnsFalse(string name)
|
||||
{
|
||||
Assert.False(NameVerification.ValidateVendorName(name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_MinimumLengthName_ReturnsTrue()
|
||||
{
|
||||
Assert.True(NameVerification.Validate("Ab", 2, 16, true, false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_MaximumLengthName_ReturnsTrue()
|
||||
{
|
||||
Assert.True(NameVerification.Validate("AbcdefghijklmnopqrsT", 1, 20, true, true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_MaxExceptions_ReturnsTrue()
|
||||
{
|
||||
var exceptions = SearchValues.Create(' ', '-', '.');
|
||||
Assert.True(NameVerification.Validate("A-B.C D", 2, 16, true, false, false, 3, exceptions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_BoundaryOfDisallowedWord_ReturnsTrueWhenNotActuallyDisallowed()
|
||||
{
|
||||
// "ass" is in disallowed, but "class" has proper boundaries
|
||||
Assert.True(NameVerification.ValidatePlayerName("Class"));
|
||||
}
|
||||
|
||||
// Negative Tests
|
||||
[Fact]
|
||||
public void Validate_EmptyName_ReturnsFalse()
|
||||
{
|
||||
Assert.False(NameVerification.Validate("", 1, 20, true, true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_TooShortName_ReturnsFalse()
|
||||
{
|
||||
Assert.False(NameVerification.Validate("A", 2, 16, true, false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_TooLongName_ReturnsFalse()
|
||||
{
|
||||
Assert.False(NameVerification.Validate("AbcdefghijklmnopqrstuvwxyzABCDEF", 2, 16, true, false));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("ass")]
|
||||
[InlineData("GodDamn Fine")]
|
||||
[InlineData("Fuck")]
|
||||
public void Validate_DisallowedWords_ReturnsFalse(string name)
|
||||
{
|
||||
Assert.False(NameVerification.ValidatePlayerName(name));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("GMJohn")]
|
||||
[InlineData("LordBob")]
|
||||
[InlineData("SeerMagic")]
|
||||
public void Validate_DisallowedPrefixes_ReturnsFalse(string name)
|
||||
{
|
||||
Assert.False(NameVerification.ValidatePlayerName(name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_TooManyExceptions_ReturnsFalse()
|
||||
{
|
||||
var exceptions = SearchValues.Create(' ', '-', '.');
|
||||
Assert.False(NameVerification.Validate("A-B.C D-E", 2, 16, true, false, false, 3, exceptions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ExceptionAtStartWhenNotAllowed_ReturnsFalse()
|
||||
{
|
||||
var exceptions = SearchValues.Create(' ', '-', '.');
|
||||
Assert.False(NameVerification.Validate("-John", 2, 16, true, false, true, 1, exceptions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DisallowedCharacters_ReturnsFalse()
|
||||
{
|
||||
Assert.False(NameVerification.Validate("John123", 2, 16, true, false));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
using Server.Misc;
|
||||
using Xunit;
|
||||
|
||||
namespace Server.Tests;
|
||||
|
||||
public class ProfanityProtectionTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("Hello world")]
|
||||
[InlineData("This is a normal conversation")]
|
||||
[InlineData("I would like to trade with you")]
|
||||
public void Speech_WithoutProfanity_PassesValidation(string speech)
|
||||
{
|
||||
Assert.False(ProfanityProtection.ContainsProfanity(speech));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Speech_Empty_PassesValidation()
|
||||
{
|
||||
Assert.False(ProfanityProtection.ContainsProfanity(""));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("I'm going to class tomorrow")] // contains "ass" but in "class"
|
||||
[InlineData("This assignment is hard")] // contains "ass" but in "assignment"
|
||||
[InlineData("That's a nice cocktail")] // contains "cock" but in "cocktail"
|
||||
public void Speech_WithWordsThatLookLikeProfanity_PassesValidation(string speech)
|
||||
{
|
||||
Assert.False(ProfanityProtection.ContainsProfanity(speech));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("This is ass")]
|
||||
[InlineData("What the fuck")]
|
||||
[InlineData("You're a bitch")]
|
||||
public void Speech_WithProfanity_FailsValidation(string speech)
|
||||
{
|
||||
Assert.True(ProfanityProtection.ContainsProfanity(speech));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("ass")] // standalone profanity
|
||||
[InlineData("an ass joke")] // profanity with word boundaries
|
||||
[InlineData("ass.")] // profanity followed by punctuation
|
||||
public void ContainsDisallowedWord_DetectsProfanityWithBoundaries(string speech)
|
||||
{
|
||||
Assert.True(NameVerification.ContainsDisallowedWord(
|
||||
speech,
|
||||
ProfanityProtection.Disallowed,
|
||||
ProfanityProtection.DisallowedSearchValues
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -418,7 +418,7 @@ public static partial class CharacterCreation
|
|||
{
|
||||
name = name.Trim();
|
||||
|
||||
if (!NameVerification.Validate(name, 2, 16, true, false, true, 1, NameVerification.SpaceDashPeriodQuote))
|
||||
if (!NameVerification.ValidatePlayerName(name))
|
||||
{
|
||||
name = "Generic Player";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using Server.Gumps;
|
||||
using Server.Misc;
|
||||
using Server.Mobiles;
|
||||
|
|
@ -78,55 +79,20 @@ namespace Server.Guilds
|
|||
!(m.Deleted || g.Disbanded || m is not PlayerMobile ||
|
||||
m.AccessLevel < AccessLevel.GameMaster && !g.IsMember(m));
|
||||
|
||||
public static bool CheckProfanity(string s, int maxLength = 50)
|
||||
{
|
||||
// return NameVerification.Validate( s, 1, 50, true, true, false, int.MaxValue, ProfanityProtection.Exceptions, ProfanityProtection.Disallowed, ProfanityProtection.StartDisallowed ); //What am I doing wrong, this still allows chars like the <3 symbol... 3 AM. someone change this to use this
|
||||
|
||||
// With testing on OSI, Guild stuff seems to follow a 'simpler' method of profanity protection
|
||||
if (s.Length < 1 || s.Length > maxLength)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var exceptions = ProfanityProtection.Exceptions;
|
||||
|
||||
s = s.ToLower();
|
||||
|
||||
for (var i = 0; i < s.Length; ++i)
|
||||
{
|
||||
var c = s[i];
|
||||
|
||||
if (c is < 'a' or > 'z' && c is < '0' or > '9')
|
||||
{
|
||||
var except = false;
|
||||
|
||||
for (var j = 0; !except && j < exceptions.Length; j++)
|
||||
{
|
||||
if (c == exceptions[j])
|
||||
{
|
||||
except = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!except)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var disallowed = ProfanityProtection.Disallowed;
|
||||
|
||||
for (var i = 0; i < disallowed.Length; i++)
|
||||
{
|
||||
if (s.IndexOfOrdinal(disallowed[i]) != -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool CheckProfanity(string s, int maxLength = 50) =>
|
||||
NameVerification.Validate(
|
||||
s,
|
||||
1,
|
||||
maxLength,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
0,
|
||||
ProfanityProtection.Exceptions,
|
||||
ProfanityProtection.Disallowed,
|
||||
ProfanityProtection.DisallowedSearchValues
|
||||
);
|
||||
|
||||
public void AddHtmlText(int x, int y, int width, int height, TextDefinition text, bool back, bool scroll)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using ModernUO.Serialization;
|
||||
using Server.Gumps;
|
||||
using Server.Misc;
|
||||
|
|
@ -79,15 +80,15 @@ public class NameChangeDeedGump : Gump
|
|||
|
||||
var m = sender.Mobile;
|
||||
|
||||
var newName = info.GetTextEntry(0)?.Trim();
|
||||
var newName = info.GetTextEntry(0).AsSpan().Trim();
|
||||
|
||||
if (!NameVerification.Validate(newName, 2, 16, true, false, true, 1, NameVerification.SpaceDashPeriodQuote))
|
||||
if (!NameVerification.ValidatePlayerName(newName))
|
||||
{
|
||||
m.SendMessage("That name is unacceptable.");
|
||||
return;
|
||||
}
|
||||
|
||||
m.RawName = newName;
|
||||
m.RawName = newName.ToString();
|
||||
m.SendMessage("Your name has been changed!");
|
||||
m.SendMessage($"You are now known as {newName}");
|
||||
m_Sender.Delete();
|
||||
|
|
|
|||
|
|
@ -1,247 +1,290 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Server.Misc
|
||||
namespace Server.Misc;
|
||||
|
||||
public static class NameVerification
|
||||
{
|
||||
public static class NameVerification
|
||||
{
|
||||
public static readonly char[] SpaceDashPeriodQuote =
|
||||
{
|
||||
' ', '-', '.', '\''
|
||||
};
|
||||
public static readonly SearchValues<char> AlphaNumeric = SearchValues.Create(
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
|
||||
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
|
||||
'U', 'V', 'W', 'X', 'Y', 'Z',
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
|
||||
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
|
||||
'u', 'v', 'w', 'x', 'y', 'z'
|
||||
);
|
||||
|
||||
public static readonly char[] Empty = Array.Empty<char>();
|
||||
public static readonly SearchValues<char> Alphabetic = SearchValues.Create(
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
|
||||
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
|
||||
'U', 'V', 'W', 'X', 'Y', 'Z',
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
|
||||
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
|
||||
'u', 'v', 'w', 'x', 'y', 'z'
|
||||
);
|
||||
|
||||
public static string[] StartDisallowed { get; } =
|
||||
{
|
||||
public static readonly SearchValues<char> Numeric = SearchValues.Create(
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
|
||||
);
|
||||
|
||||
public static readonly SearchValues<char> SpaceDashPeriodQuote = SearchValues.Create(' ', '-', '.', '\'');
|
||||
|
||||
public static readonly SearchValues<string> StartDisallowed = SearchValues.Create(
|
||||
[
|
||||
"seer",
|
||||
"counselor",
|
||||
"gm",
|
||||
"admin",
|
||||
"lady",
|
||||
"lord"
|
||||
};
|
||||
],
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
public static string[] Disallowed { get; } =
|
||||
{
|
||||
"jigaboo",
|
||||
"chigaboo",
|
||||
"wop",
|
||||
"kyke",
|
||||
"kike",
|
||||
"tit",
|
||||
"spic",
|
||||
"prick",
|
||||
"piss",
|
||||
"lezbo",
|
||||
"lesbo",
|
||||
"felatio",
|
||||
"dyke",
|
||||
"dildo",
|
||||
"chinc",
|
||||
"chink",
|
||||
"cunnilingus",
|
||||
"cum",
|
||||
"cocksucker",
|
||||
"cock",
|
||||
"clitoris",
|
||||
"clit",
|
||||
"ass",
|
||||
"hitler",
|
||||
"penis",
|
||||
"nigga",
|
||||
"nigger",
|
||||
"klit",
|
||||
"kunt",
|
||||
"jiz",
|
||||
"jism",
|
||||
"jerkoff",
|
||||
"jackoff",
|
||||
"goddamn",
|
||||
"fag",
|
||||
"blowjob",
|
||||
"bitch",
|
||||
"asshole",
|
||||
"dick",
|
||||
"pussy",
|
||||
"snatch",
|
||||
"cunt",
|
||||
"twat",
|
||||
"shit",
|
||||
"fuck",
|
||||
"tailor",
|
||||
"smith",
|
||||
"scholar",
|
||||
"rogue",
|
||||
"novice",
|
||||
"neophyte",
|
||||
"merchant",
|
||||
"medium",
|
||||
"master",
|
||||
"mage",
|
||||
"lb",
|
||||
"journeyman",
|
||||
"grandmaster",
|
||||
"fisherman",
|
||||
"expert",
|
||||
"chef",
|
||||
"carpenter",
|
||||
"british",
|
||||
"blackthorne",
|
||||
"blackthorn",
|
||||
"beggar",
|
||||
"archer",
|
||||
"apprentice",
|
||||
"adept",
|
||||
"gamemaster",
|
||||
"frozen",
|
||||
"squelched",
|
||||
"invulnerable",
|
||||
"osi",
|
||||
"origin"
|
||||
};
|
||||
public static readonly string[] Disallowed =
|
||||
[
|
||||
..ProfanityProtection.Disallowed,
|
||||
"tailor",
|
||||
"smith",
|
||||
"scholar",
|
||||
"rogue",
|
||||
"novice",
|
||||
"neophyte",
|
||||
"merchant",
|
||||
"medium",
|
||||
"master",
|
||||
"mage",
|
||||
"lb",
|
||||
"journeyman",
|
||||
"grandmaster",
|
||||
"fisherman",
|
||||
"expert",
|
||||
"chef",
|
||||
"carpenter",
|
||||
"british",
|
||||
"blackthorne",
|
||||
"blackthorn",
|
||||
"beggar",
|
||||
"archer",
|
||||
"apprentice",
|
||||
"adept",
|
||||
"gamemaster",
|
||||
"frozen",
|
||||
"squelched",
|
||||
"invulnerable",
|
||||
"osi",
|
||||
"origin"
|
||||
];
|
||||
|
||||
public static void Configure()
|
||||
public static readonly SearchValues<string> DisallowedSearchValues = SearchValues.Create(
|
||||
Disallowed,
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
public static void Configure()
|
||||
{
|
||||
CommandSystem.Register("ValidateName", AccessLevel.Administrator, ValidateName_OnCommand);
|
||||
}
|
||||
|
||||
[Usage("ValidateName"), Description("Checks the result of NameValidation on the specified name.")]
|
||||
public static void ValidateName_OnCommand(CommandEventArgs e)
|
||||
{
|
||||
if (Validate(e.ArgString, 2, 16, true, false, true, 1, SpaceDashPeriodQuote))
|
||||
{
|
||||
CommandSystem.Register("ValidateName", AccessLevel.Administrator, ValidateName_OnCommand);
|
||||
e.Mobile.SendMessage(0x59, "That name is considered valid.");
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Mobile.SendMessage(0x22, "That name is considered invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool ValidatePlayerName(ReadOnlySpan<char> name) =>
|
||||
Validate(name, 2, 16, true, false, true, 1, SpaceDashPeriodQuote);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool ValidatePetName(ReadOnlySpan<char> name) => Validate(name, 1, 16, true, false, exceptions: null);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool ValidateVendorName(ReadOnlySpan<char> name) => Validate(name, 1, 20, true, true);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool Validate(
|
||||
ReadOnlySpan<char> name, int minLength, int maxLength, bool allowLetters, bool allowDigits,
|
||||
bool noExceptionsAtStart = true, int maxExceptions = 0, SearchValues<char> exceptions = null
|
||||
) => Validate(
|
||||
name,
|
||||
minLength,
|
||||
maxLength,
|
||||
allowLetters,
|
||||
allowDigits,
|
||||
noExceptionsAtStart,
|
||||
maxExceptions,
|
||||
exceptions,
|
||||
Disallowed,
|
||||
DisallowedSearchValues,
|
||||
StartDisallowed
|
||||
);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool Validate(
|
||||
ReadOnlySpan<char> name, int minLength, int maxLength, bool allowLetters, bool allowDigits,
|
||||
bool noExceptionsAtStart, int maxExceptions, SearchValues<char> exceptions, ReadOnlySpan<string> disallowed,
|
||||
SearchValues<string> disallowedSV
|
||||
) => Validate(
|
||||
name,
|
||||
minLength,
|
||||
maxLength,
|
||||
allowLetters,
|
||||
allowDigits,
|
||||
noExceptionsAtStart,
|
||||
maxExceptions,
|
||||
exceptions,
|
||||
disallowed,
|
||||
disallowedSV,
|
||||
null
|
||||
);
|
||||
|
||||
public static bool Validate(
|
||||
ReadOnlySpan<char> name, int minLength, int maxLength, bool allowLetters, bool allowDigits,
|
||||
bool noExceptionsAtStart, int maxExceptions, SearchValues<char> exceptions, ReadOnlySpan<string> disallowed,
|
||||
SearchValues<string> disallowedSV, SearchValues<string> startDisallowedSV
|
||||
)
|
||||
{
|
||||
if (name.Length == 0 || name.Length < minLength || name.Length > maxLength)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[Usage("ValidateName"), Description("Checks the result of NameValidation on the specified name.")]
|
||||
public static void ValidateName_OnCommand(CommandEventArgs e)
|
||||
if (exceptions == null)
|
||||
{
|
||||
if (Validate(e.ArgString, 2, 16, true, false, true, 1, SpaceDashPeriodQuote))
|
||||
// We don't have exceptions, so we might be limited to letters or numbers
|
||||
var allowed = allowLetters switch
|
||||
{
|
||||
e.Mobile.SendMessage(0x59, "That name is considered valid.");
|
||||
}
|
||||
else
|
||||
// If we don't allow exceptions, then non-alphanumeric is not allowed
|
||||
true when allowDigits && maxExceptions == 0 => AlphaNumeric,
|
||||
true when !allowDigits => Alphabetic,
|
||||
false when allowDigits => Numeric,
|
||||
// Everything has been allowed! Use `Utility.FixHtml()` to stop weird behavior
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (allowed != null && name.ContainsAnyExcept(allowed))
|
||||
{
|
||||
e.Mobile.SendMessage(0x22, "That name is considered invalid.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool Validate(
|
||||
string name, int minLength, int maxLength, bool allowLetters, bool allowDigits,
|
||||
bool noExceptionsAtStart, int maxExceptions, char[] exceptions
|
||||
) =>
|
||||
Validate(
|
||||
name,
|
||||
minLength,
|
||||
maxLength,
|
||||
allowLetters,
|
||||
allowDigits,
|
||||
noExceptionsAtStart,
|
||||
maxExceptions,
|
||||
exceptions,
|
||||
Disallowed,
|
||||
StartDisallowed
|
||||
);
|
||||
|
||||
public static bool Validate(
|
||||
string name, int minLength, int maxLength, bool allowLetters, bool allowDigits,
|
||||
bool noExceptionsAtStart, int maxExceptions, char[] exceptions, string[] disallowed, string[] startDisallowed
|
||||
)
|
||||
else
|
||||
{
|
||||
if (name == null || name.Length < minLength || name.Length > maxLength)
|
||||
// We have exceptions, and at least one of the letters/digits flag is false:
|
||||
var notAllowed = allowLetters switch
|
||||
{
|
||||
true when !allowDigits => Numeric,
|
||||
false when allowDigits => Alphabetic,
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (notAllowed != null && name.ContainsAny(notAllowed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var exceptCount = 0;
|
||||
|
||||
name = name.ToLower();
|
||||
|
||||
if (!allowLetters || !allowDigits ||
|
||||
exceptions.Length > 0 && (noExceptionsAtStart || maxExceptions < int.MaxValue))
|
||||
if (ContainsExceptions(name, exceptions, noExceptionsAtStart, maxExceptions))
|
||||
{
|
||||
for (var i = 0; i < name.Length; ++i)
|
||||
{
|
||||
var c = name[i];
|
||||
|
||||
if (c >= 'a' && c <= 'z')
|
||||
{
|
||||
if (!allowLetters)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
exceptCount = 0;
|
||||
}
|
||||
else if (c >= '0' && c <= '9')
|
||||
{
|
||||
if (!allowDigits)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
exceptCount = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var except = false;
|
||||
|
||||
for (var j = 0; !except && j < exceptions.Length; ++j)
|
||||
{
|
||||
if (c == exceptions[j])
|
||||
{
|
||||
except = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!except || i == 0 && noExceptionsAtStart)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (exceptCount++ == maxExceptions)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < disallowed.Length; ++i)
|
||||
{
|
||||
var indexOf = name.IndexOfOrdinal(disallowed[i]);
|
||||
|
||||
if (indexOf == -1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var badPrefix = indexOf == 0;
|
||||
|
||||
for (var j = 0; !badPrefix && j < exceptions.Length; ++j)
|
||||
{
|
||||
badPrefix = name[indexOf - 1] == exceptions[j];
|
||||
}
|
||||
|
||||
if (!badPrefix)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var badSuffix = indexOf + disallowed[i].Length >= name.Length;
|
||||
|
||||
for (var j = 0; !badSuffix && j < exceptions.Length; ++j)
|
||||
{
|
||||
badSuffix = name[indexOf + disallowed[i].Length] == exceptions[j];
|
||||
}
|
||||
|
||||
if (badSuffix)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < startDisallowed.Length; ++i)
|
||||
{
|
||||
if (name.StartsWithOrdinal(startDisallowed[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (disallowedSV != null && disallowed.Length > 0 && ContainsDisallowedWord(name, disallowed, disallowedSV))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return startDisallowedSV == null || name.IndexOfAny(startDisallowedSV) != 0;
|
||||
}
|
||||
|
||||
public static bool ContainsExceptions(
|
||||
ReadOnlySpan<char> name, SearchValues<char> exceptions, bool noExceptionsAtStart, int maxExceptions
|
||||
)
|
||||
{
|
||||
if (!noExceptionsAtStart && maxExceptions is <= -1 or >= int.MaxValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var index = name.IndexOfAny(exceptions);
|
||||
|
||||
while (index != -1)
|
||||
{
|
||||
if (noExceptionsAtStart)
|
||||
{
|
||||
if (index == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
noExceptionsAtStart = false;
|
||||
}
|
||||
|
||||
if (maxExceptions-- <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (index + 1 < name.Length)
|
||||
{
|
||||
name = name[(index + 1)..];
|
||||
index = name.IndexOfAny(exceptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
index = -1;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool ContainsDisallowedWord(ReadOnlySpan<char> name, ReadOnlySpan<string> disallowed, SearchValues<string> disallowedSV)
|
||||
{
|
||||
var index = name.IndexOfAny(disallowedSV);
|
||||
|
||||
while (index != -1)
|
||||
{
|
||||
var isStartBoundary = index == 0 || !char.IsLetterOrDigit(name[index - 1]);
|
||||
|
||||
if (isStartBoundary)
|
||||
{
|
||||
for (var i = 0; i < disallowed.Length; i++)
|
||||
{
|
||||
var word = disallowed[i].AsSpan();
|
||||
if (index + word.Length > name.Length || !name.Slice(index, word.Length).InsensitiveEquals(word))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// End boundary
|
||||
if (index + word.Length == name.Length || !char.IsLetterOrDigit(name[index + word.Length]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (index + 1 < name.Length)
|
||||
{
|
||||
name = name[(index + 1)..];
|
||||
index = name.IndexOfAny(disallowedSV);
|
||||
}
|
||||
else
|
||||
{
|
||||
index = -1;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,140 +1,147 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
|
||||
namespace Server.Misc
|
||||
namespace Server.Misc;
|
||||
|
||||
public enum ProfanityAction
|
||||
{
|
||||
public enum ProfanityAction
|
||||
None, // no action taken
|
||||
Disallow, // speech is not displayed
|
||||
Criminal, // makes the player criminal, not killable by guards
|
||||
CriminalAction, // makes the player criminal, can be killed by guards
|
||||
Disconnect, // player is kicked
|
||||
Other // some other implementation
|
||||
}
|
||||
|
||||
public static class ProfanityProtection
|
||||
{
|
||||
private static bool Enabled;
|
||||
private static ProfanityAction Action;
|
||||
|
||||
public static void Configure()
|
||||
{
|
||||
None, // no action taken
|
||||
Disallow, // speech is not displayed
|
||||
Criminal, // makes the player criminal, not killable by guards
|
||||
CriminalAction, // makes the player criminal, can be killed by guards
|
||||
Disconnect, // player is kicked
|
||||
Other // some other implementation
|
||||
Enabled = ServerConfiguration.GetSetting("profanityProtection.enabled", false);
|
||||
Action = ServerConfiguration.GetSetting("profanityProtection.action", ProfanityAction.Disallow);
|
||||
|
||||
if (Enabled)
|
||||
{
|
||||
EventSink.Speech += EventSink_Speech;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ProfanityProtection
|
||||
// Used by the guild system
|
||||
public static readonly SearchValues<char> Exceptions = SearchValues.Create(
|
||||
' ', '-', '.', '\'', '"', ',', '_', '+', '=', '~', '`', '!', '^', '*', '\\', '/', ';', ':', '<', '>', '[', ']',
|
||||
'{', '}', '?', '|', '(', ')', '%', '$', '&', '#', '@'
|
||||
);
|
||||
|
||||
public static readonly string[] Disallowed =
|
||||
[
|
||||
"jigaboo",
|
||||
"chigaboo",
|
||||
"wop",
|
||||
"kyke",
|
||||
"kike",
|
||||
"tit",
|
||||
"spic",
|
||||
"prick",
|
||||
"piss",
|
||||
"lezbo",
|
||||
"lesbo",
|
||||
"felatio",
|
||||
"dyke",
|
||||
"dildo",
|
||||
"chinc",
|
||||
"chink",
|
||||
"cunnilingus",
|
||||
"cum",
|
||||
"cocksucker",
|
||||
"cock",
|
||||
"clitoris",
|
||||
"clit",
|
||||
"ass",
|
||||
"hitler",
|
||||
"penis",
|
||||
"nigga",
|
||||
"nigger",
|
||||
"klit",
|
||||
"kunt",
|
||||
"jiz",
|
||||
"jism",
|
||||
"jerkoff",
|
||||
"jackoff",
|
||||
"goddamn",
|
||||
"fag",
|
||||
"blowjob",
|
||||
"bitch",
|
||||
"asshole",
|
||||
"dick",
|
||||
"pussy",
|
||||
"snatch",
|
||||
"cunt",
|
||||
"twat",
|
||||
"shit",
|
||||
"fuck",
|
||||
];
|
||||
|
||||
public static readonly SearchValues<string> DisallowedSearchValues = SearchValues.Create(
|
||||
Disallowed,
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
private static bool OnProfanityDetected(Mobile from, string speech)
|
||||
{
|
||||
// TODO: Move this to configuration
|
||||
private static readonly bool Enabled = false;
|
||||
|
||||
private static readonly ProfanityAction
|
||||
Action = ProfanityAction.Disallow; // change here what to do when profanity is detected
|
||||
|
||||
public static char[] Exceptions { get; } =
|
||||
switch (Action)
|
||||
{
|
||||
' ', '-', '.', '\'', '"', ',', '_', '+', '=', '~', '`', '!', '^', '*', '\\', '/', ';', ':', '<', '>', '[', ']',
|
||||
'{', '}', '?', '|', '(', ')', '%', '$', '&', '#', '@'
|
||||
};
|
||||
case ProfanityAction.None: return true;
|
||||
case ProfanityAction.Disallow: return false;
|
||||
case ProfanityAction.Criminal:
|
||||
from.Criminal = true;
|
||||
return true;
|
||||
case ProfanityAction.CriminalAction:
|
||||
from.CriminalAction(false);
|
||||
return true;
|
||||
case ProfanityAction.Disconnect:
|
||||
{
|
||||
from.NetState?.Disconnect("Using profanity.");
|
||||
|
||||
public static string[] StartDisallowed { get; } = Array.Empty<string>();
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
case ProfanityAction.Other: // TODO: Provide custom implementation if this is chosen
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static string[] Disallowed { get; } =
|
||||
public static bool ContainsProfanity(ReadOnlySpan<char> speech) =>
|
||||
speech.Length > 0 &&
|
||||
!NameVerification.Validate(
|
||||
speech,
|
||||
1,
|
||||
int.MaxValue,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
int.MaxValue, // allow all non-alphanumeric characters
|
||||
null,
|
||||
Disallowed,
|
||||
DisallowedSearchValues
|
||||
);
|
||||
|
||||
private static void EventSink_Speech(SpeechEventArgs e)
|
||||
{
|
||||
var from = e.Mobile;
|
||||
|
||||
if (from.AccessLevel > AccessLevel.Player)
|
||||
{
|
||||
"jigaboo",
|
||||
"chigaboo",
|
||||
"wop",
|
||||
"kyke",
|
||||
"kike",
|
||||
"tit",
|
||||
"spic",
|
||||
"prick",
|
||||
"piss",
|
||||
"lezbo",
|
||||
"lesbo",
|
||||
"felatio",
|
||||
"dyke",
|
||||
"dildo",
|
||||
"chinc",
|
||||
"chink",
|
||||
"cunnilingus",
|
||||
"cum",
|
||||
"cocksucker",
|
||||
"cock",
|
||||
"clitoris",
|
||||
"clit",
|
||||
"ass",
|
||||
"hitler",
|
||||
"penis",
|
||||
"nigga",
|
||||
"nigger",
|
||||
"klit",
|
||||
"kunt",
|
||||
"jiz",
|
||||
"jism",
|
||||
"jerkoff",
|
||||
"jackoff",
|
||||
"goddamn",
|
||||
"fag",
|
||||
"blowjob",
|
||||
"bitch",
|
||||
"asshole",
|
||||
"dick",
|
||||
"pussy",
|
||||
"snatch",
|
||||
"cunt",
|
||||
"twat",
|
||||
"shit",
|
||||
"fuck"
|
||||
};
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
if (Enabled)
|
||||
{
|
||||
EventSink.Speech += EventSink_Speech;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
private static bool OnProfanityDetected(Mobile from, string speech)
|
||||
if (ContainsProfanity(e.Speech))
|
||||
{
|
||||
switch (Action)
|
||||
{
|
||||
case ProfanityAction.None: return true;
|
||||
case ProfanityAction.Disallow: return false;
|
||||
case ProfanityAction.Criminal:
|
||||
from.Criminal = true;
|
||||
return true;
|
||||
case ProfanityAction.CriminalAction:
|
||||
from.CriminalAction(false);
|
||||
return true;
|
||||
case ProfanityAction.Disconnect:
|
||||
{
|
||||
from.NetState?.Disconnect("Using profanity.");
|
||||
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
case ProfanityAction.Other: // TODO: Provide custom implementation if this is chosen
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EventSink_Speech(SpeechEventArgs e)
|
||||
{
|
||||
var from = e.Mobile;
|
||||
|
||||
if (from.AccessLevel > AccessLevel.Player)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NameVerification.Validate(
|
||||
e.Speech,
|
||||
0,
|
||||
int.MaxValue,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
int.MaxValue,
|
||||
Exceptions,
|
||||
Disallowed,
|
||||
StartDisallowed
|
||||
))
|
||||
{
|
||||
e.Blocked = !OnProfanityDetected(from, e.Speech);
|
||||
}
|
||||
e.Blocked = !OnProfanityDetected(from, e.Speech);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,54 +1,31 @@
|
|||
using System;
|
||||
|
||||
namespace Server.Misc
|
||||
namespace Server.Misc;
|
||||
|
||||
public static class RenameRequests
|
||||
{
|
||||
public static class RenameRequests
|
||||
public static void RenameRequest(Mobile from, Mobile targ, string name)
|
||||
{
|
||||
public static void RenameRequest(Mobile from, Mobile targ, string name)
|
||||
if (!from.CanSee(targ) || !from.InRange(targ, 12) || !targ.CanBeRenamedBy(from))
|
||||
{
|
||||
if (from.CanSee(targ) && from.InRange(targ, 12) && targ.CanBeRenamedBy(from))
|
||||
{
|
||||
name = name.Trim();
|
||||
return;
|
||||
}
|
||||
|
||||
if (NameVerification.Validate(
|
||||
name,
|
||||
1,
|
||||
16,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
0,
|
||||
NameVerification.Empty,
|
||||
NameVerification.StartDisallowed,
|
||||
Core.ML ? NameVerification.Disallowed : Array.Empty<string>()
|
||||
))
|
||||
{
|
||||
if (Core.ML)
|
||||
{
|
||||
var disallowed = ProfanityProtection.Disallowed;
|
||||
var span = name.AsSpan().Trim();
|
||||
|
||||
for (var i = 0; i < disallowed.Length; i++)
|
||||
{
|
||||
if (name.IndexOfOrdinal(disallowed[i]) != -1)
|
||||
{
|
||||
from.SendLocalizedMessage(1072622); // That name isn't very polite.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
from.SendLocalizedMessage(
|
||||
1072623,
|
||||
$"{targ.Name}\t{name}"
|
||||
); // Pet ~1_OLDPETNAME~ renamed to ~2_NEWPETNAME~.
|
||||
}
|
||||
|
||||
targ.Name = name;
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendMessage("That name is unacceptable.");
|
||||
}
|
||||
}
|
||||
if (NameVerification.ValidatePetName(span))
|
||||
{
|
||||
// Pet ~1_OLDPETNAME~ renamed to ~2_NEWPETNAME~.
|
||||
from.SendLocalizedMessage(1072623, $"{targ.Name}\t{span}");
|
||||
targ.Name = span.ToString();
|
||||
}
|
||||
else if (span.IndexOfAny(ProfanityProtection.DisallowedSearchValues) != -1)
|
||||
{
|
||||
from.SendLocalizedMessage(1072622); // That name isn't very polite.
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendMessage("That name is unacceptable.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1232,9 +1232,9 @@ public partial class PlayerVendor : Mobile
|
|||
return;
|
||||
}
|
||||
|
||||
var name = text.Trim();
|
||||
var name = text.AsSpan().Trim();
|
||||
|
||||
if (!NameVerification.Validate(name, 1, 20, true, true, true, 0, NameVerification.Empty))
|
||||
if (!NameVerification.ValidateVendorName(name))
|
||||
{
|
||||
m_Vendor.SayTo(from, "That name is unacceptable.");
|
||||
return;
|
||||
|
|
@ -1261,9 +1261,9 @@ public partial class PlayerVendor : Mobile
|
|||
return;
|
||||
}
|
||||
|
||||
var name = text.Trim();
|
||||
var name = text.AsSpan().Trim();
|
||||
|
||||
if (!NameVerification.Validate(name, 1, 20, true, true, true, 0, NameVerification.Empty))
|
||||
if (!NameVerification.ValidateVendorName(name))
|
||||
{
|
||||
m_Vendor.SayTo(from, "That name is unacceptable.");
|
||||
return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue