Files
2026-10-22 16:17:56 +01:00

1175 lines
31 KiB
SourcePawn
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <sourcemod>
#include <sdktools>
#include <regex>
#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<DataPack>(data);
pack.Reset();
int client = GetClientOfUserId(pack.ReadCell());
char sName[MAX_NAME_LENGTH];
pack.ReadString(sName, sizeof(sName));
bool bReplaced = view_as<bool>(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 <target>");
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));
}