using System; using System.Buffers; using System.Runtime.CompilerServices; namespace Server.Misc; public static class NameVerification { public static readonly SearchValues 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 SearchValues 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 readonly SearchValues Numeric = SearchValues.Create( '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' ); public static readonly SearchValues SpaceDashPeriodQuote = SearchValues.Create(' ', '-', '.', '\''); public static readonly SearchValues StartDisallowed = SearchValues.Create( [ "seer", "counselor", "gm", "admin", "lady", "lord" ], StringComparison.OrdinalIgnoreCase ); 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 readonly SearchValues 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)) { 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 name) => Validate(name, 2, 16, true, false, true, 1, SpaceDashPeriodQuote); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ValidatePetName(ReadOnlySpan name) => Validate(name, 1, 16, true, false, exceptions: null); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ValidateVendorName(ReadOnlySpan name) => Validate(name, 1, 20, true, true); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Validate( ReadOnlySpan name, int minLength, int maxLength, bool allowLetters, bool allowDigits, bool noExceptionsAtStart = true, int maxExceptions = 0, SearchValues exceptions = null ) => Validate( name, minLength, maxLength, allowLetters, allowDigits, noExceptionsAtStart, maxExceptions, exceptions, Disallowed, DisallowedSearchValues, StartDisallowed ); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Validate( ReadOnlySpan name, int minLength, int maxLength, bool allowLetters, bool allowDigits, bool noExceptionsAtStart, int maxExceptions, SearchValues exceptions, ReadOnlySpan disallowed, SearchValues disallowedSV ) => Validate( name, minLength, maxLength, allowLetters, allowDigits, noExceptionsAtStart, maxExceptions, exceptions, disallowed, disallowedSV, null ); public static bool Validate( ReadOnlySpan name, int minLength, int maxLength, bool allowLetters, bool allowDigits, bool noExceptionsAtStart, int maxExceptions, SearchValues exceptions, ReadOnlySpan disallowed, SearchValues disallowedSV, SearchValues startDisallowedSV ) { if (name.Length == 0 || name.Length < minLength || name.Length > maxLength) { return false; } if (exceptions == null) { // We don't have exceptions, so we might be limited to letters or numbers var allowed = allowLetters switch { // 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)) { return false; } } else { // 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; } if (ContainsExceptions(name, exceptions, noExceptionsAtStart, maxExceptions)) { return false; } } if (disallowedSV != null && disallowed.Length > 0 && ContainsDisallowedWord(name, disallowed, disallowedSV)) { return false; } return startDisallowedSV == null || name.IndexOfAny(startDisallowedSV) != 0; } public static bool ContainsExceptions( ReadOnlySpan name, SearchValues exceptions, bool noExceptionsAtStart, int maxExceptions ) { if (!noExceptionsAtStart && maxExceptions is <= -1 or >= int.MaxValue) { return false; } var index = name.IndexOfAny(exceptions); var exceptionCount = 0; while (index != -1) { if (noExceptionsAtStart) { if (index == 0) { return true; } noExceptionsAtStart = false; } if (exceptionCount++ >= maxExceptions) { return true; } if (index + 1 < name.Length) { name = name[(index + 1)..]; index = name.IndexOfAny(exceptions); if (index != 0) { exceptionCount = 0; } } else { index = -1; } } return false; } public static bool ContainsDisallowedWord(ReadOnlySpan name, ReadOnlySpan disallowed, SearchValues 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; } }