#include #include #include #pragma semicolon 1 #pragma newdecls required #define CHAT_PREFIX "\x04[NameFilter]\x01" #define MAX_STACKED_MARKS 2 // combining marks allowed on one letter (Vietnamese needs 2) #define RELOAD_FILE "data/namefilter2_reload.dat" #define RELOAD_MAX_AGE 120 // seconds; older saves are from a previous server run enum struct SavedName { int iUserId; char sOriginal[MAX_NAME_LENGTH]; char sAssigned[MAX_NAME_LENGTH]; char sCurrent[MAX_NAME_LENGTH]; } char g_sOriginalName[MAXPLAYERS + 1][MAX_NAME_LENGTH]; char g_sAssignedName[MAXPLAYERS + 1][MAX_NAME_LENGTH]; // the filtered name we gave the player ArrayList g_SavedNames; // original names saved over a plugin reload int g_iBlockNameChangeEvents[MAXPLAYERS + 1] = {0, ...}; bool g_bFilteredOnConnect[MAXPLAYERS + 1]; bool g_bReplacedOnConnect[MAXPLAYERS + 1]; bool g_bLastFilterReplaced; // whether the last FilterName() assigned a replacement name ArrayList g_AdvertExprs; ArrayList g_BannedExprs; ArrayList g_ReplacementNames; ArrayList g_ConfusableFrom; ArrayList g_ConfusableTo; ArrayList g_WhitelistCps; int g_iMinNameLength = 2; int g_iMinSurvivedPct = 50; int g_iKeepTypeableRun = 5; public Plugin myinfo = { name = "NameFilter2", author = "marisakirisame.net", description = "Normalises and filters player names", version = "1.0" } public void OnPluginStart() { HookEvent("player_changename", Event_ChangeName, EventHookMode_Pre); HookUserMessage(GetUserMessageId("SayText2"), UserMessage_SayText2, true); RegConsoleCmd("sm_originalname", Command_OriginalName, "Show a player's original name"); RegConsoleCmd("sm_changednames", Command_ChangedNames, "List all players whose names were changed"); RegAdminCmd("sm_namefilter2_reload", Command_Reload, ADMFLAG_CONFIG, "Reload the NameFilter2 config"); LoadConfig(); LoadSavedNames(); for(int i = 1; i <= MaxClients; i++) { if(IsClientConnected(i)) OnClientConnected(i); } } public void OnPluginEnd() { char sPath[PLATFORM_MAX_PATH]; BuildPath(Path_SM, sPath, sizeof(sPath), RELOAD_FILE); File hFile = OpenFile(sPath, "wb"); if(!hFile) return; // null-terminated strings, as names may contain any printable character hFile.WriteInt32(GetTime()); for(int i = 1; i <= MaxClients; i++) { if(!IsClientConnected(i) || IsFakeClient(i) || !g_sOriginalName[i][0]) continue; char sCurrent[MAX_NAME_LENGTH]; GetClientName(i, sCurrent, sizeof(sCurrent)); hFile.WriteInt32(GetClientUserId(i)); hFile.WriteString(g_sOriginalName[i], true); hFile.WriteString(g_sAssignedName[i], true); hFile.WriteString(sCurrent, true); } delete hFile; } void LoadSavedNames() { g_SavedNames = new ArrayList(sizeof(SavedName)); char sPath[PLATFORM_MAX_PATH]; BuildPath(Path_SM, sPath, sizeof(sPath), RELOAD_FILE); File hFile = OpenFile(sPath, "rb"); if(!hFile) return; int iSaved; if(hFile.ReadInt32(iSaved) && GetTime() - iSaved <= RELOAD_MAX_AGE) { SavedName saved; while(hFile.ReadInt32(saved.iUserId) && hFile.ReadString(saved.sOriginal, sizeof(saved.sOriginal)) != -1 && hFile.ReadString(saved.sAssigned, sizeof(saved.sAssigned)) != -1 && hFile.ReadString(saved.sCurrent, sizeof(saved.sCurrent)) != -1) { g_SavedNames.PushArray(saved); } } delete hFile; DeleteFile(sPath); } // restores a late-loaded client's state saved by OnPluginEnd, as long as // they still have the same name. Entries are kept: userids are never reused bool RestoreSavedName(int client, const char[] sName) { int iUserId = GetClientUserId(client); for(int i = 0; i < g_SavedNames.Length; i++) { SavedName saved; g_SavedNames.GetArray(i, saved); if(saved.iUserId != iUserId) continue; if(!StrEqual(saved.sCurrent, sName)) return false; strcopy(g_sOriginalName[client], MAX_NAME_LENGTH, saved.sOriginal); strcopy(g_sAssignedName[client], MAX_NAME_LENGTH, saved.sAssigned); return true; } return false; } public void OnClientConnected(int client) { g_iBlockNameChangeEvents[client] = 0; g_bFilteredOnConnect[client] = false; g_bReplacedOnConnect[client] = false; g_sAssignedName[client][0] = '\0'; char sName[MAX_NAME_LENGTH]; GetClientName(client, sName, sizeof(sName)); strcopy(g_sOriginalName[client], MAX_NAME_LENGTH, sName); if(IsFakeClient(client)) return; if(RestoreSavedName(client, sName)) return; if(FilterName(client, sName)) { g_bFilteredOnConnect[client] = true; g_bReplacedOnConnect[client] = g_bLastFilterReplaced; strcopy(g_sAssignedName[client], MAX_NAME_LENGTH, sName); SetClientName(client, sName); } } public void OnClientPutInServer(int client) { if(IsFakeClient(client)) return; char sName[MAX_NAME_LENGTH]; GetClientName(client, sName, sizeof(sName)); if(FilterName(client, sName)) { g_bFilteredOnConnect[client] = true; if(g_bLastFilterReplaced) g_bReplacedOnConnect[client] = true; g_iBlockNameChangeEvents[client] = 2; strcopy(g_sAssignedName[client], MAX_NAME_LENGTH, sName); SetClientName(client, sName); } if(g_bFilteredOnConnect[client]) { NotifyAdminsFiltered(client, sName, g_bReplacedOnConnect[client]); NotifyClientRenamed(client, sName, g_bReplacedOnConnect[client]); } } public void OnClientDisconnect(int client) { g_sOriginalName[client][0] = '\0'; g_sAssignedName[client][0] = '\0'; g_iBlockNameChangeEvents[client] = 0; g_bFilteredOnConnect[client] = false; g_bReplacedOnConnect[client] = false; } void NotifyAdminsFiltered(int client, const char[] sNewName, bool bReplaced) { PrintToServer("[NameFilter2] \"%s\" (#%d) was %s \"%s\"", g_sOriginalName[client], GetClientUserId(client), bReplaced ? "given replacement name" : "renamed to", sNewName); for(int i = 1; i <= MaxClients; i++) { if(!IsClientInGame(i) || IsFakeClient(i)) continue; if(!CheckCommandAccess(i, "sm_namefilter2_notify", ADMFLAG_GENERIC)) continue; PrintToChat(i, CHAT_PREFIX ... " \x07FF6060%s\x01 (#%d) was renamed to \x0760FF60%s\x01", g_sOriginalName[client], GetClientUserId(client), sNewName); } } void NotifyClientRenamed(int client, const char[] sNewName, bool bReplaced) { if(bReplaced) PrintToChat(client, CHAT_PREFIX ... " Your name was unable to be filtered and was replaced with \x0760FF60%s\x01.", sNewName); else PrintToChat(client, CHAT_PREFIX ... " Your name was filtered to \x0760FF60%s\x01.", sNewName); } public Action Event_ChangeName(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); if(g_iBlockNameChangeEvents[client]) { g_iBlockNameChangeEvents[client]--; SetEventBroadcast(event, true); return Plugin_Handled; } return Plugin_Continue; } public Action UserMessage_SayText2(UserMsg msg_id, BfRead msg, const int[] players, int playersNum, bool reliable, bool init) { if(!reliable) return Plugin_Continue; int client; char sMessage[32]; char sOldName[MAX_NAME_LENGTH]; char sNewName[MAX_NAME_LENGTH]; if(GetUserMessageType() == UM_Protobuf) { PbReadString(msg, "msg_name", sMessage, sizeof(sMessage)); if(!(sMessage[0] == '#' && StrContains(sMessage, "Name_Change") != -1)) return Plugin_Continue; client = PbReadInt(msg, "ent_idx"); PbReadString(msg, "params", sOldName, sizeof(sOldName), 1); PbReadString(msg, "params", sNewName, sizeof(sNewName), 2); } else { client = BfReadByte(msg); BfReadByte(msg); BfReadString(msg, sMessage, sizeof(sMessage)); if(!(sMessage[0] == '#' && StrContains(sMessage, "Name_Change") != -1)) return Plugin_Continue; BfReadString(msg, sOldName, sizeof(sOldName)); BfReadString(msg, sNewName, sizeof(sNewName)); } if(client < 1 || client > MaxClients) return Plugin_Continue; if(g_iBlockNameChangeEvents[client]) { g_iBlockNameChangeEvents[client]--; return Plugin_Handled; } // the name we gave them (Timer_ChangeName's re-apply or any later echo of // it); let it broadcast but keep the original it was filtered from if(g_sAssignedName[client][0] && StrEqual(sNewName, g_sAssignedName[client])) return Plugin_Continue; strcopy(g_sOriginalName[client], MAX_NAME_LENGTH, sNewName); g_sAssignedName[client][0] = '\0'; if(FilterName(client, sNewName)) { strcopy(g_sAssignedName[client], MAX_NAME_LENGTH, sNewName); // CCSPlayer::ChangeName sends this SayText2 before it fires the event // and stores the new name, so a SetClientName from in here is undone // as soon as we return. Re-apply later instead, even when the filtered // name equals the old one: 3 = this change's event, then the timer's // SayText2 and event g_iBlockNameChangeEvents[client] = 3; DataPack pack = new DataPack(); pack.WriteCell(GetClientUserId(client)); pack.WriteString(sNewName); pack.WriteCell(g_bLastFilterReplaced); CreateTimer(0.1, Timer_ChangeName, pack); return Plugin_Handled; } return Plugin_Continue; } public Action Timer_ChangeName(Handle timer, any data) { DataPack pack = view_as(data); pack.Reset(); int client = GetClientOfUserId(pack.ReadCell()); char sName[MAX_NAME_LENGTH]; pack.ReadString(sName, sizeof(sName)); bool bReplaced = view_as(pack.ReadCell()); delete pack; if(client) { SetClientName(client, sName); NotifyAdminsFiltered(client, sName, bReplaced); NotifyClientRenamed(client, sName, bReplaced); } return Plugin_Stop; } public Action Command_OriginalName(int client, int args) { if(args < 1) { ReplyToCommand(client, "[SM] Usage: sm_originalname "); return Plugin_Handled; } char sPattern[MAX_TARGET_LENGTH]; GetCmdArg(1, sPattern, sizeof(sPattern)); char sTargetName[MAX_TARGET_LENGTH]; int aTargets[MAXPLAYERS]; bool bTnIsMl; int iCount = ProcessTargetString(sPattern, client, aTargets, MAXPLAYERS, COMMAND_FILTER_CONNECTED | COMMAND_FILTER_NO_IMMUNITY, sTargetName, sizeof(sTargetName), bTnIsMl); if(iCount <= 0) { ReplyToTargetError(client, iCount); return Plugin_Handled; } for(int i = 0; i < iCount; i++) { char sCurrent[MAX_NAME_LENGTH]; GetClientName(aTargets[i], sCurrent, sizeof(sCurrent)); if(StrEqual(sCurrent, g_sOriginalName[aTargets[i]])) ReplyToCommand(client, "[SM] %N's name was not changed.", aTargets[i]); else ReplyToCommand(client, "[SM] %N's original name: %s", aTargets[i], g_sOriginalName[aTargets[i]]); } return Plugin_Handled; } public Action Command_ChangedNames(int client, int args) { int iFound = 0; char sCurrent[MAX_NAME_LENGTH]; for(int i = 1; i <= MaxClients; i++) { if(!IsClientConnected(i) || IsFakeClient(i)) continue; GetClientName(i, sCurrent, sizeof(sCurrent)); if(StrEqual(sCurrent, g_sOriginalName[i])) continue; PrintToConsole(client, "%s - original name: %s", sCurrent, g_sOriginalName[i]); iFound++; } if(!iFound) PrintToConsole(client, "[SM] No changed names."); else if(client && GetCmdReplySource() == SM_REPLY_TO_CHAT) PrintToChat(client, "[SM] %d changed name%s listed in console.", iFound, (iFound == 1) ? "" : "s"); return Plugin_Handled; } public Action Command_Reload(int client, int args) { LoadConfig(); ReplyToCommand(client, "[SM] NameFilter2 config reloaded."); return Plugin_Handled; } void CloseRegexList(ArrayList list) { if(!list) return; for(int i = 0; i < list.Length; i++) { Handle hRegex = list.Get(i); CloseHandle(hRegex); } delete list; } void LoadConfig() { CloseRegexList(g_AdvertExprs); g_AdvertExprs = null; CloseRegexList(g_BannedExprs); g_BannedExprs = null; delete g_ReplacementNames; delete g_ConfusableFrom; delete g_ConfusableTo; delete g_WhitelistCps; static char sConfigFile[PLATFORM_MAX_PATH]; BuildPath(Path_SM, sConfigFile, sizeof(sConfigFile), "configs/NameFilter2.cfg"); if(!FileExists(sConfigFile)) SetFailState("Could not find config: \"%s\"", sConfigFile); KeyValues Config = new KeyValues("NameFilter2"); if(!Config.ImportFromFile(sConfigFile)) { delete Config; SetFailState("ImportFromFile() failed!"); } char sWhitelist[64] = "äöüÄÖܧğĞšŠ"; if(Config.JumpToKey("settings")) { g_iMinNameLength = Config.GetNum("min_length", 2); g_iMinSurvivedPct = Config.GetNum("min_survived_letters", 50); g_iKeepTypeableRun = Config.GetNum("keep_typeable_run", 5); Config.GetString("whitelist", sWhitelist, sizeof(sWhitelist), sWhitelist); Config.GoBack(); } g_WhitelistCps = new ArrayList(); int iPos = 0; while(sWhitelist[iPos] != '\0') { int cp = DecodeUTF8Char(sWhitelist, iPos); if(cp > 0x7E) g_WhitelistCps.Push(cp); } static char sBuffer[256]; char sError[256]; RegexError iError; g_ConfusableFrom = new ArrayList(ByteCountToCells(16)); g_ConfusableTo = new ArrayList(ByteCountToCells(16)); if(Config.JumpToKey("confusables")) { if(Config.GotoFirstSubKey(false)) { do { char sFrom[16]; Config.GetSectionName(sFrom, sizeof(sFrom)); Config.GetString(NULL_STRING, sBuffer, sizeof(sBuffer)); if(sFrom[0]) { g_ConfusableFrom.PushString(sFrom); g_ConfusableTo.PushString(sBuffer); } } while(Config.GotoNextKey(false)); Config.GoBack(); } Config.GoBack(); } g_AdvertExprs = new ArrayList(); if(Config.JumpToKey("adverts")) { if(Config.GotoFirstSubKey(false)) { do { Config.GetString(NULL_STRING, sBuffer, sizeof(sBuffer)); Handle hRegex = CompileRegex(sBuffer, PCRE_CASELESS, sError, sizeof(sError), iError); if(iError != REGEX_ERROR_NONE) LogError("Error parsing advert filter \"%s\": %s", sBuffer, sError); else g_AdvertExprs.Push(hRegex); } while(Config.GotoNextKey(false)); Config.GoBack(); } Config.GoBack(); } g_BannedExprs = new ArrayList(); if(Config.JumpToKey("banned")) { if(Config.GotoFirstSubKey(false)) { do { Config.GetString(NULL_STRING, sBuffer, sizeof(sBuffer)); Handle hRegex = CompileRegex(sBuffer, PCRE_CASELESS, sError, sizeof(sError), iError); if(iError != REGEX_ERROR_NONE) LogError("Error parsing banned filter \"%s\": %s", sBuffer, sError); else g_BannedExprs.Push(hRegex); } while(Config.GotoNextKey(false)); Config.GoBack(); } Config.GoBack(); } g_ReplacementNames = new ArrayList(ByteCountToCells(MAX_NAME_LENGTH)); if(Config.JumpToKey("names")) { if(Config.GotoFirstSubKey(false)) { do { Config.GetString(NULL_STRING, sBuffer, sizeof(sBuffer)); g_ReplacementNames.PushString(sBuffer); } while(Config.GotoNextKey(false)); Config.GoBack(); } Config.GoBack(); } if(!g_ReplacementNames.Length) { LogError("Warning, you didn't specify any replacement names!"); g_ReplacementNames.PushString("BAD_NAME"); } delete Config; } bool FilterName(int client, char[] sName, int Length = MAX_NAME_LENGTH) { g_bLastFilterReplaced = false; char sBefore[MAX_NAME_LENGTH]; strcopy(sBefore, sizeof(sBefore), sName); // applies to kept names too, so none can hide characters or pose as another StripInvisible(sName); char sClean[MAX_NAME_LENGTH]; strcopy(sClean, sizeof(sClean), sName); // a name an admin can target by typing an ASCII run may keep its // unicode decoration, as long as the checks below find nothing bad bool bKeepOriginal = HasTypeableRun(sClean); bool bContentRemoved = false; ApplyConfusables(sName, Length); int iLettersBefore = CountLetters(sName); FoldToASCII(sName, true); NormalizeWhitespace(sName); // accented Latin letters are only allowed when the name starts with // at least two plain Latin letters if(!HasLatinPrefix(sName)) { FoldToASCII(sName, false); NormalizeWhitespace(sName); } // folding lost too many of the name's letters; the remnant no longer // resembles the name the player chose. Still run the advert/banned // checks on the remnant first if the original might be kept bool bSurvivedFail = CountLetters(sName) * 100 < iLettersBefore * g_iMinSurvivedPct; if(bSurvivedFail && !bKeepOriginal) { AssignReplacementName(client, sName, Length); return true; } RegexError iError; for(int i = 0; i < g_AdvertExprs.Length; i++) { Handle hRegex = g_AdvertExprs.Get(i); // SourceMod Regex bug, bounded loop int Guard; for(Guard = 0; Guard < 100; Guard++) { if(!strlen(sName)) break; int Match = MatchRegex(hRegex, sName, iError); if(iError != REGEX_ERROR_NONE) { LogError("Regex Error: %d", iError); break; } if(Match <= 0) break; char sMatch[MAX_NAME_LENGTH]; if(!GetRegexSubString(hRegex, 0, sMatch, sizeof(sMatch)) || !sMatch[0]) break; if(ReplaceStringEx(sName, Length, sMatch, "") == -1) break; bContentRemoved = true; } if(Guard == 100) LogError("SourceMod Regex failed! \"%s\"", sName); } NormalizeWhitespace(sName); for(int i = 0; i < g_BannedExprs.Length; i++) { if(!strlen(sName)) break; Handle hRegex = g_BannedExprs.Get(i); int Match = MatchRegex(hRegex, sName, iError); if(iError != REGEX_ERROR_NONE) { LogError("Regex Error: %d", iError); continue; } if(Match <= 0) continue; AssignReplacementName(client, sName, Length); return true; } TrimString(sName); if(strlen(sName) < g_iMinNameLength || !HasLetterOrDigit(sName)) { AssignReplacementName(client, sName, Length); return true; } if(bKeepOriginal && !bContentRemoved) { strcopy(sName, Length, sClean); return !StrEqual(sBefore, sClean); } if(bSurvivedFail) { AssignReplacementName(client, sName, Length); return true; } return !StrEqual(sBefore, sName); } // removes characters that render as nothing or reorder the text around them // (zero-width, bidi controls, fillers, tags) and caps stacked combining marks void StripInvisible(char[] sName) { int r = 0; int w = 0; int iMarks = 0; while(sName[r] != '\0') { int rStart = r; int cp = DecodeUTF8Char(sName, r); if(cp < 0 || IsInvisibleCp(cp)) continue; if(IsCombiningMarkCp(cp)) { if(++iMarks > MAX_STACKED_MARKS) continue; } else iMarks = 0; for(int k = rStart; k < r; k++) sName[w++] = sName[k]; } sName[w] = '\0'; } bool IsInvisibleCp(int cp) { return cp < 0x20 || (cp >= 0x7F && cp <= 0x9F) // C0/C1 controls, DEL || cp == 0xAD // soft hyphen || cp == 0x34F // combining grapheme joiner || cp == 0x61C // arabic letter mark || cp == 0x115F || cp == 0x1160 // hangul fillers || cp == 0x3164 || cp == 0xFFA0 // hangul fillers || cp == 0x17B4 || cp == 0x17B5 // khmer inherent vowels || (cp >= 0x180B && cp <= 0x180F) // mongolian selectors, vowel separator || (cp >= 0x200B && cp <= 0x200F) // zero-width space/joiners, LRM/RLM || (cp >= 0x2028 && cp <= 0x202E) // line/paragraph separators, bidi embeds/overrides || (cp >= 0x2060 && cp <= 0x206F) // word joiner, invisible operators, bidi isolates || cp == 0x2800 // braille blank || (cp >= 0xFE00 && cp <= 0xFE0F) // variation selectors || cp == 0xFEFF // zero-width no-break space (BOM) || (cp >= 0xFFF9 && cp <= 0xFFFB) // interlinear annotation || (cp >= 0x1D173 && cp <= 0x1D17A) // musical formatting || (cp >= 0xE0000 && cp <= 0xE0FFF); // tags, variation selectors supplement } bool IsCombiningMarkCp(int cp) { return (cp >= 0x300 && cp <= 0x36F) // combining diacritical marks || (cp >= 0x1AB0 && cp <= 0x1AFF) // extended || (cp >= 0x1DC0 && cp <= 0x1DFF) // supplement || (cp >= 0x20D0 && cp <= 0x20FF) // for symbols || (cp >= 0xFE20 && cp <= 0xFE2F); // half marks } void ApplyConfusables(char[] sName, int Length) { char sFrom[16]; char sTo[16]; for(int i = 0; i < g_ConfusableFrom.Length; i++) { g_ConfusableFrom.GetString(i, sFrom, sizeof(sFrom)); g_ConfusableTo.GetString(i, sTo, sizeof(sTo)); if(sFrom[0]) ReplaceString(sName, Length, sFrom, sTo, true); } } // decodes + folds in-place; output is never longer than input. // whitelisted codepoints are always kept; accented Latin only when // bAllowAccents, otherwise it is reduced to its base letter void FoldToASCII(char[] sName, bool bAllowAccents) { int r = 0; int w = 0; while(sName[r] != '\0') { int rStart = r; int cp = DecodeUTF8Char(sName, r); if(cp < 0) continue; int c = FoldCodepoint(cp); if(c > 0) { sName[w++] = c; continue; } if(IsWhitelistedCp(cp) || (bAllowAccents && IsAccentedLatin(cp))) { for(int k = rStart; k < r; k++) sName[w++] = sName[k]; continue; } // every codepoint FoldLatinLetter maps is >= 2 UTF-8 bytes char sBase[3]; int iBase = FoldLatinLetter(cp, sBase, sizeof(sBase)); for(int k = 0; k < iBase; k++) sName[w++] = sBase[k]; } sName[w] = '\0'; } int CountLetters(const char[] s) { int i = 0; int iLetters = 0; while(s[i] != '\0') { int cp = DecodeUTF8Char(s, i); if(cp > 0 && IsLetterCp(cp)) iLetters++; } return iLetters; } // letters that either survive folding (ASCII, foldable, kept accents) or // belong to an alphabetic script that FoldToASCII strips bool IsLetterCp(int cp) { int c = FoldCodepoint(cp); if(c) return IsCharAlpha(c); char sBase[3]; if(IsAccentedLatin(cp) || FoldLatinLetter(cp, sBase, sizeof(sBase))) return true; return (cp >= 0x370 && cp <= 0x3FF) // Greek || (cp >= 0x400 && cp <= 0x52F) // Cyrillic || (cp >= 0x530 && cp <= 0x58F) // Armenian || (cp >= 0x590 && cp <= 0x5FF) // Hebrew || (cp >= 0x600 && cp <= 0x6FF) // Arabic || (cp >= 0x900 && cp <= 0xDFF) // Indic scripts || (cp >= 0xE00 && cp <= 0xE7F) // Thai || (cp >= 0x1E00 && cp <= 0x1FFF) // Latin ext. additional, Greek ext. || (cp >= 0x3040 && cp <= 0x30FF) // kana || (cp >= 0x3400 && cp <= 0x9FFF) // CJK || (cp >= 0xAC00 && cp <= 0xD7AF);// Hangul } bool HasLetterOrDigit(const char[] s) { int i = 0; while(s[i] != '\0') { int cp = DecodeUTF8Char(s, i); if(cp > 0 && (IsLetterCp(cp) || (cp >= '0' && cp <= '9'))) return true; } return false; } // true if the name has a contiguous printable-ASCII run containing at // least g_iKeepTypeableRun letters/digits; an admin can target such a // name without being able to type its unicode parts bool HasTypeableRun(const char[] s) { if(g_iKeepTypeableRun <= 0) return false; int iRun = 0; for(int i = 0; s[i] != '\0'; i++) { int c = s[i] & 0xFF; if(c < 0x20 || c > 0x7E) { iRun = 0; continue; } if(IsCharAlpha(c) || IsCharNumeric(c)) { if(++iRun >= g_iKeepTypeableRun) return true; } } return false; } bool HasLatinPrefix(const char[] sName) { return IsCharAlpha(sName[0]) && IsCharAlpha(sName[1]); } bool IsWhitelistedCp(int cp) { return g_WhitelistCps.FindValue(cp) != -1; } bool IsAccentedLatin(int cp) { if(cp >= 0xC0 && cp <= 0xFF) // Latin-1 letters return cp != 0xD7 && cp != 0xF7; // × ÷ return cp >= 0x100 && cp <= 0x17F; // Latin Extended-A } // returns the codepoint and advances i, or -1 on an invalid byte (advances past it) int DecodeUTF8Char(const char[] s, int &i) { int b = s[i] & 0xFF; if(b < 0x80) { i++; return b; } int len; int cp; if((b & 0xE0) == 0xC0) { len = 2; cp = b & 0x1F; } else if((b & 0xF0) == 0xE0) { len = 3; cp = b & 0x0F; } else if((b & 0xF8) == 0xF0) { len = 4; cp = b & 0x07; } else { i++; return -1; } for(int k = 1; k < len; k++) { int cb = s[i + k] & 0xFF; if((cb & 0xC0) != 0x80) { i++; return -1; } cp = (cp << 6) | (cb & 0x3F); } i += len; return cp; } // writes the plain ASCII form of a Latin letter with diacritics, stroke or // ligature (Å -> A, ø -> o, æ -> ae) to sOut; returns its length, 0 if none int FoldLatinLetter(int cp, char[] sOut, int Length) { char sLig[3]; switch(cp) { case 0xC6, 0x1E2, 0x1FC: strcopy(sLig, sizeof(sLig), "AE"); // Æ Ǣ Ǽ case 0xDE: strcopy(sLig, sizeof(sLig), "Th"); // Þ case 0xDF: strcopy(sLig, sizeof(sLig), "ss"); // ß case 0xE6, 0x1E3, 0x1FD: strcopy(sLig, sizeof(sLig), "ae"); // æ ǣ ǽ case 0xFE: strcopy(sLig, sizeof(sLig), "th"); // þ case 0x132: strcopy(sLig, sizeof(sLig), "IJ"); // IJ case 0x133: strcopy(sLig, sizeof(sLig), "ij"); // ij case 0x152: strcopy(sLig, sizeof(sLig), "OE"); // Œ case 0x153: strcopy(sLig, sizeof(sLig), "oe"); // œ case 0x1C4, 0x1F1: strcopy(sLig, sizeof(sLig), "DZ"); // DŽ DZ case 0x1C5, 0x1F2: strcopy(sLig, sizeof(sLig), "Dz"); // Dž Dz case 0x1C6, 0x1F3: strcopy(sLig, sizeof(sLig), "dz"); // dž dz case 0x1C7: strcopy(sLig, sizeof(sLig), "LJ"); // LJ case 0x1C8: strcopy(sLig, sizeof(sLig), "Lj"); // Lj case 0x1C9: strcopy(sLig, sizeof(sLig), "lj"); // lj case 0x1CA: strcopy(sLig, sizeof(sLig), "NJ"); // NJ case 0x1CB: strcopy(sLig, sizeof(sLig), "Nj"); // Nj case 0x1CC: strcopy(sLig, sizeof(sLig), "nj"); // nj case 0x1E9E: strcopy(sLig, sizeof(sLig), "SS"); // ẞ } if(sLig[0]) return strcopy(sOut, Length, sLig); // '.' = no base letter static const char aLatinBase[] = "AAAAAA.CEEEEIIIIDNOOOOO.OUUUUY.." // U+00C0 ... "aaaaaa.ceeeeiiiidnooooo.ouuuuy.y" // U+00E0 ... "AaAaAaCcCcCcCcDdDdEeEeEeEeEeGgGg" // U+0100 ... "GgGgHhHhIiIiIiIiIi..JjKkkLlLlLlL" // U+0120 ... "lLlNnNnNnnNnOoOoOo..RrRrRrSsSsSs" // U+0140 ... "SsTtTtTtUuUuUuUuUuUuWwYyYZzZzZzs" // U+0160 ... "bBBb...CcDDDd.E..FfG...IKkl..NnO" // U+0180 ... "Oo..Pp.....tTtTUu.VYyZz........." // U+01A0 ... ".............AaIiOoUuUuUuUuUu.Aa" // U+01C0 ... "Aa..GgGgKkOoOo..j...Gg..NnAa..Oo" // U+01E0 ... "AaAaEeEeIiIiOoOoRrRrUuUuSsTt..Hh" // U+0200 ... "Nd..ZzAaEeOoOoOoOoYylntj..ACcLTs" // U+0220 ... "z..BUAEeJj.qRrYy" // U+0240 ; static const char aLatinExtBase[] = "AaBbBbBbCcDdDdDdDdDdEeEeEeEeEeFf" // U+1E00 ... "GgHhHhHhHhHhIiIiKkKkKkLlLlLlLlMm" // U+1E20 ... "MmMmNnNnNnNnOoOoOoOoPpPpRrRrRrRr" // U+1E40 ... "SsSsSsSsSsTtTtTtTtUuUuUuUuUuVvVv" // U+1E60 ... "WwWwWwWwWwXxXxYyZzZzZzhtwyasss.." // U+1E80 ... "AaAaAaAaAaAaAaAaAaAaAaAaEeEeEeEe" // U+1EA0 ... "EeEeEeEeIiIiOoOoOoOoOoOoOoOoOoOo" // U+1EC0 ... "OoOoUuUuUuUuUuUuUuYyYyYyYy....Yy" // U+1EE0 ; int c = '.'; if(cp >= 0xC0 && cp <= 0x24F) c = aLatinBase[cp - 0xC0]; else if(cp >= 0x1E00 && cp <= 0x1EFF) c = aLatinExtBase[cp - 0x1E00]; if(c == '.') return 0; sOut[0] = c; sOut[1] = '\0'; return 1; } // maps a codepoint to printable ASCII, or 0 to strip it int FoldCodepoint(int cp) { if(cp >= 0x20 && cp <= 0x7E) return cp; if(cp < 0x20 || cp == 0x7F) return 0; if(cp == 0xA0 || cp == 0x3000 // nbsp / ideographic space || (cp >= 0x2000 && cp <= 0x200A) // en/em/thin/hair spaces || cp == 0x202F || cp == 0x205F) // narrow nbsp / math space return ' '; if(cp >= 0xFF01 && cp <= 0xFF5E) // fullwidth ASCII forms return cp - 0xFEE0; if(cp >= 0x1D400 && cp <= 0x1D6A3) // 13 styled Latin alphabets of 52 { int r = (cp - 0x1D400) % 52; return (r < 26) ? ('A' + r) : ('a' + r - 26); } if(cp == 0x1D6A4) // dotless i return 'i'; if(cp == 0x1D6A5) // dotless j return 'j'; if(cp >= 0x1D7CE && cp <= 0x1D7FF) // styled math digits, 5 sets of 10 return '0' + ((cp - 0x1D7CE) % 10); if(cp >= 0x24B6 && cp <= 0x24CF) // circled A-Z return 'A' + (cp - 0x24B6); if(cp >= 0x24D0 && cp <= 0x24E9) // circled a-z return 'a' + (cp - 0x24D0); if(cp >= 0x2460 && cp <= 0x2468) // circled 1-9 return '1' + (cp - 0x2460); if(cp == 0x24EA) // circled 0 return '0'; if(cp >= 0x249C && cp <= 0x24B5) // parenthesized a-z return 'a' + (cp - 0x249C); if(cp >= 0x2474 && cp <= 0x247C) // parenthesized 1-9 return '1' + (cp - 0x2474); if(cp >= 0x2488 && cp <= 0x2490) // digit full stop 1.-9. return '1' + (cp - 0x2488); if(cp >= 0x24F5 && cp <= 0x24FD) // double circled 1-9 return '1' + (cp - 0x24F5); if(cp == 0x24FF) // negative circled 0 return '0'; if(cp >= 0x2776 && cp <= 0x277E) // dingbat negative circled 1-9 return '1' + (cp - 0x2776); if(cp >= 0x2780 && cp <= 0x2788) // dingbat circled sans-serif 1-9 return '1' + (cp - 0x2780); if(cp >= 0x278A && cp <= 0x2792) // dingbat negative circled sans-serif 1-9 return '1' + (cp - 0x278A); if(cp >= 0x1F110 && cp <= 0x1F129) // parenthesized A-Z return 'A' + (cp - 0x1F110); if(cp >= 0x1F130 && cp <= 0x1F149) // squared A-Z return 'A' + (cp - 0x1F130); if(cp >= 0x1F150 && cp <= 0x1F169) // negative circled A-Z return 'A' + (cp - 0x1F150); if(cp >= 0x1F170 && cp <= 0x1F189) // negative squared A-Z return 'A' + (cp - 0x1F170); if(cp >= 0x1F1E6 && cp <= 0x1F1FF) // regional indicator (flag letter) A-Z return 'A' + (cp - 0x1F1E6); if(cp == 0xB9) // superscript 1 return '1'; if(cp == 0xB2 || cp == 0xB3) // superscript 2-3 return '0' + (cp - 0xB0); if(cp == 0x2070) // superscript 0 return '0'; if(cp >= 0x2074 && cp <= 0x2079) // superscript 4-9 return '4' + (cp - 0x2074); if(cp >= 0x2080 && cp <= 0x2089) // subscript 0-9 return '0' + (cp - 0x2080); // superscript (modifier) letters, small capitals and letterlike symbols // (the BMP "holes" in the styled math alphabets), scattered codepoints static const int aLetterCps[] = { 0x1D2C, 0x1D2E, 0x1D30, 0x1D31, 0x1D33, 0x1D34, 0x1D35, 0x1D36, // ᴬ ᴮ ᴰ ᴱ ᴳ ᴴ ᴵ ᴶ 0x1D37, 0x1D38, 0x1D39, 0x1D3A, 0x1D3C, 0x1D3E, 0x1D3F, 0x1D40, // ᴷ ᴸ ᴹ ᴺ ᴼ ᴾ ᴿ ᵀ 0x1D41, 0x1D42, // ᵁ ᵂ 0x1D43, 0x1D47, 0x1D9C, 0x1D48, 0x1D49, 0x1DA0, 0x1D4D, 0x02B0, // ᵃ ᵇ ᶜ ᵈ ᵉ ᶠ ᵍ ʰ 0x2071, 0x02B2, 0x1D4F, 0x02E1, 0x1D50, 0x207F, 0x1D52, 0x1D56, // ⁱ ʲ ᵏ ˡ ᵐ ⁿ ᵒ ᵖ 0x107A5, 0x02B3, 0x02E2, 0x1D57, 0x1D58, 0x1D5B, 0x02B7, 0x02E3, // 𐞥 ʳ ˢ ᵗ ᵘ ᵛ ʷ ˣ 0x02B8, 0x1DBB, // ʸ ᶻ 0x1D00, 0x0299, 0x1D04, 0x1D05, 0x1D07, 0xA730, 0x0262, 0x029C, // ᴀ ʙ ᴄ ᴅ ᴇ ꜰ ɢ ʜ 0x026A, 0x1D0A, 0x1D0B, 0x029F, 0x1D0D, 0x0274, 0x1D0F, 0x1D18, // ɪ ᴊ ᴋ ʟ ᴍ ɴ ᴏ ᴘ 0x0280, 0xA731, 0x1D1B, 0x1D1C, 0x1D20, 0x1D21, 0x028F, 0x1D22, // ʀ ꜱ ᴛ ᴜ ᴠ ᴡ ʏ ᴢ 0x2102, 0x210A, 0x210B, 0x210C, 0x210D, 0x210E, 0x210F, 0x2110, // ℂ ℊ ℋ ℌ ℍ ℎ ℏ ℐ 0x2111, 0x2112, 0x2113, 0x2115, 0x2119, 0x211A, 0x211B, 0x211C, // ℑ ℒ ℓ ℕ ℙ ℚ ℛ ℜ 0x211D, 0x2124, 0x2128, 0x212A, 0x212B, 0x212C, 0x212D, 0x212F, // ℝ ℤ ℨ K(elvin) Å ℬ ℭ ℯ 0x2130, 0x2131, 0x2133, 0x2134, 0x2139, 0x2145, 0x2146, 0x2147, // ℰ ℱ ℳ ℴ ℹ ⅅ ⅆ ⅇ 0x2148, 0x2149 // ⅈ ⅉ }; static const char aLetterAscii[] = "ABDEGHIJKLMNOPRTUW" ... "abcdefghijklmnopqrstuvwxyz" ... "abcdefghijklmnoprstuvwyz" ... "CgHHHhhIILlNPQRRRZZKABCeEFMoiDdeij"; for(int i = 0; i < sizeof(aLetterCps); i++) { if(aLetterCps[i] == cp) return aLetterAscii[i]; } return 0; } void NormalizeWhitespace(char[] sName) { int r = 0; int w = 0; bool bLastSpace = false; while(sName[r] != '\0') { char c = sName[r++]; if(c == ' ' || c == '\t') { if(bLastSpace) continue; bLastSpace = true; sName[w++] = ' '; } else { bLastSpace = false; sName[w++] = c; } } sName[w] = '\0'; TrimString(sName); } void AssignReplacementName(int client, char[] sName, int Length) { g_bLastFilterReplaced = true; int iCount = g_ReplacementNames.Length; int iBase = client % iCount; char sCandidate[MAX_NAME_LENGTH]; for(int i = 0; i < iCount; i++) { g_ReplacementNames.GetString((iBase + i) % iCount, sCandidate, sizeof(sCandidate)); bool bTaken = false; for(int j = 1; j <= MaxClients; j++) { if(j == client || !IsClientConnected(j)) continue; char sOther[MAX_NAME_LENGTH]; GetClientName(j, sOther, sizeof(sOther)); if(StrEqual(sOther, sCandidate)) { bTaken = true; break; } } if(!bTaken) { strcopy(sName, Length, sCandidate); return; } } g_ReplacementNames.GetString(iBase, sCandidate, sizeof(sCandidate)); Format(sName, Length, "%s %d", sCandidate, GetClientUserId(client)); }