/* =========== ClientUserInfoChanged Called from ClientConnect when the player first connects and directly by the server system when the player updates a userinfo variable. The game can override any of the settings and call trap_SetUserinfo if desired. ============ */ const char *ClientUserinfoChanged( int clientNum, bool forceName ) { gentity_t *ent; const char *s; char model[ MAX_QPATH ]; char buffer[ MAX_QPATH ]; char oldname[ MAX_NAME_LENGTH ]; char newname[ MAX_NAME_LENGTH ]; char err[ MAX_STRING_CHARS ]; bool revertName = false; gclient_t *client; char userinfo[ MAX_INFO_STRING ]; ent = g_entities + clientNum; client = ent->client; trap_GetUserinfo( clientNum, userinfo, sizeof( userinfo ) ); // check for malformed or illegal info strings if ( !Info_Validate( userinfo ) ) { trap_SendServerCommand( ent - g_entities, "disconnect \"illegal or malformed userinfo\"" ); trap_DropClient( ent - g_entities, "dropped: illegal or malformed userinfo" ); return "Illegal or malformed userinfo"; } // If their userinfo overflowed, tremded is in the process of disconnecting them. // If we send our own disconnect, it won't work, so just return to prevent crashes later // in this function. This check must come after the Info_Validate call. else if ( !userinfo[ 0 ] ) { return "Empty (overflowed) userinfo"; } // stickyspec toggle s = Info_ValueForKey( userinfo, "cg_stickySpec" ); client->pers.stickySpec = atoi( s ) != 0; // set name Q_strncpyz( oldname, client->pers.netname, sizeof( oldname ) ); s = Info_ValueForKey( userinfo, "name" ); G_ClientCleanName( s, newname, sizeof( newname ), client ); if ( strcmp( oldname, newname ) ) { if ( !forceName && client->pers.namelog->nameChangeTime && level.time - client->pers.namelog->nameChangeTime <= g_minNameChangePeriod.value * 1000 ) { trap_SendServerCommand( ent - g_entities, va( "print_tr %s %d", QQ( N_("Name change spam protection (g_minNameChangePeriod = $1$)") ), g_minNameChangePeriod.integer ) ); revertName = true; } else if ( !forceName && g_maxNameChanges.integer > 0 && client->pers.namelog->nameChanges >= g_maxNameChanges.integer ) { trap_SendServerCommand( ent - g_entities, va( "print_tr %s %d", QQ( N_("Maximum name changes reached (g_maxNameChanges = $1$)") ), g_maxNameChanges.integer ) ); revertName = true; } else if ( !forceName && client->pers.namelog->muted ) { trap_SendServerCommand( ent - g_entities, va( "print_tr %s", QQ( N_("You cannot change your name while you are muted") ) ) ); revertName = true; } else if ( !G_admin_name_check( ent, newname, err, sizeof( err ) ) ) { trap_SendServerCommand( ent - g_entities, va( "print_tr %s %s %s", QQ( "$1t$ $2$" ), Quote( err ), Quote( newname ) ) ); revertName = true; } else if ( Q_UTF8_Strlen( newname ) > MAX_NAME_CHARACTERS ) { trap_SendServerCommand( ent - g_entities, va( "print_tr %s %d", QQ( N_("Name is too long! Must be less than $1$ characters.") ), MAX_NAME_CHARACTERS ) ); revertName = true; } if ( revertName ) { Q_strncpyz( client->pers.netname, *oldname ? oldname : G_UnnamedClientName( client ), sizeof( client->pers.netname ) ); } else { if( G_IsUnnamed( newname ) ) { Q_strncpyz( client->pers.netname, G_UnnamedClientName( client ), sizeof( client->pers.netname ) ); } else { Q_strncpyz( client->pers.netname, newname, sizeof( client->pers.netname ) ); } if ( !forceName && client->pers.connected == CON_CONNECTED ) { client->pers.namelog->nameChangeTime = level.time; client->pers.namelog->nameChanges++; } if ( *oldname ) { G_LogPrintf( "ClientRename: %i [%s] (%s) \"%s^7\" -> \"%s^7\" \"%s^7\"", clientNum, client->pers.ip.str, client->pers.guid, oldname, client->pers.netname, client->pers.netname ); } } G_namelog_update_name( client ); Info_SetValueForKey(userinfo, "name", client->pers.netname, false); trap_SetUserinfo(clientNum, userinfo); } if ( client->pers.classSelection == PCL_NONE ) { //This looks hacky and frankly it is. The clientInfo string needs to hold different //model details to that of the spawning class or the info change will not be //registered and an axis appears instead of the player model. There is zero chance //the player can spawn with the battlesuit, hence this choice. Com_sprintf( buffer, MAX_QPATH, "%s/%s", BG_ClassModelConfig( PCL_HUMAN_BSUIT )->modelName, BG_ClassModelConfig( PCL_HUMAN_BSUIT )->skinName ); } else { Com_sprintf( buffer, MAX_QPATH, "%s/%s", BG_ClassModelConfig( client->pers.classSelection )->modelName, BG_ClassModelConfig( client->pers.classSelection )->skinName ); if ( BG_ClassModelConfig( client->pers.classSelection )->segmented ) { client->ps.persistant[ PERS_STATE ] |= PS_NONSEGMODEL; } else { client->ps.persistant[ PERS_STATE ] &= ~PS_NONSEGMODEL; } } Q_strncpyz( model, buffer, sizeof( model ) ); // wallwalk follow s = Info_ValueForKey( userinfo, "cg_wwFollow" ); if ( atoi( s ) ) { client->ps.persistant[ PERS_STATE ] |= PS_WALLCLIMBINGFOLLOW; } else { client->ps.persistant[ PERS_STATE ] &= ~PS_WALLCLIMBINGFOLLOW; } // wallwalk toggle s = Info_ValueForKey( userinfo, "cg_wwToggle" ); if ( atoi( s ) ) { client->ps.persistant[ PERS_STATE ] |= PS_WALLCLIMBINGTOGGLE; } else { client->ps.persistant[ PERS_STATE ] &= ~PS_WALLCLIMBINGTOGGLE; } // always sprint s = Info_ValueForKey( userinfo, "cg_sprintToggle" ); if ( atoi( s ) ) { client->ps.persistant[ PERS_STATE ] |= PS_SPRINTTOGGLE; } else { client->ps.persistant[ PERS_STATE ] &= ~PS_SPRINTTOGGLE; } // fly speed s = Info_ValueForKey( userinfo, "cg_flySpeed" ); if ( *s ) { client->pers.flySpeed = atoi( s ); } else { client->pers.flySpeed = BG_Class( PCL_NONE )->speed; } // disable blueprint errors s = Info_ValueForKey( userinfo, "cg_disableBlueprintErrors" ); if ( atoi( s ) ) { client->pers.disableBlueprintErrors = true; } else { client->pers.disableBlueprintErrors = false; } // teamInfo s = Info_ValueForKey( userinfo, "teamoverlay" ); if ( atoi( s ) != 0 ) { // teamoverlay was enabled so we need an update if ( client->pers.teamInfo == 0 ) { client->pers.teamInfo = 1; } } else { client->pers.teamInfo = 0; } s = Info_ValueForKey( userinfo, "cg_unlagged" ); if ( !s[ 0 ] || atoi( s ) != 0 ) { client->pers.useUnlagged = true; } else { client->pers.useUnlagged = false; } Q_strncpyz( client->pers.voice, Info_ValueForKey( userinfo, "voice" ), sizeof( client->pers.voice ) ); // send over a subset of the userinfo keys so other clients can // print scoreboards, display models, and play custom sounds Com_sprintf( userinfo, sizeof( userinfo ), "n\\%s\\t\\%i\\model\\%s\\ig\\%16s\\v\\%s", client->pers.netname, client->pers.team, model, Com_ClientListString( &client->sess.ignoreList ), client->pers.voice ); trap_SetConfigstring( CS_PLAYERS + clientNum, userinfo ); /*G_LogPrintf( "ClientUserinfoChanged: %i %s\n", clientNum, userinfo );*/ return nullptr; }
/* =========== G_ClientCleanName ============ */ static void G_ClientCleanName( const char *in, char *out, int outSize, gclient_t *client ) { int len, colorlessLen; char *p; int spaces; qboolean escaped; qboolean invalid = qfalse; qboolean hasletter = qfalse; //save room for trailing null byte outSize--; len = 0; colorlessLen = 0; p = out; *p = 0; spaces = 0; for ( ; *in; in++ ) { int cp, w; // don't allow leading spaces if ( colorlessLen == 0 && *in == ' ' ) { continue; } // don't allow nonprinting characters or (dead) console keys // but do allow UTF-8 (unvalidated) if ( *in >= 0 && *in < ' ' ) { continue; } // check colors if ( Q_IsColorString( in ) ) { in++; // make sure room in dest for both chars if ( len > outSize - 2 ) { break; } *out++ = Q_COLOR_ESCAPE; *out++ = *in; len += 2; continue; } else if ( !g_emoticonsAllowedInNames.integer && G_IsEmoticon( in, &escaped ) ) { // make sure room in dest for both chars if ( len > outSize - 2 ) { break; } *out++ = '['; *out++ = '['; len += 2; if ( escaped ) { in++; } continue; } cp = Q_UTF8_CodePoint( in ); if ( Q_Unicode_IsAlphaOrIdeo( cp ) ) { hasletter = qtrue; } // don't allow too many consecutive spaces if ( *in == ' ' ) { spaces++; if ( spaces > 3 ) { continue; } } else { spaces = 0; } w = Q_UTF8_WidthCP( cp ); if ( len > outSize - w ) { break; } memcpy( out, in, w ); colorlessLen++; len += w; out += w; in += w - 1; // allow for loop increment } *out = 0; // don't allow names beginning with S_SKIPNOTIFY because it messes up /ignore-related code if ( !Q_strnicmp( p, S_SKIPNOTIFY, 12 ) ) { invalid = qtrue; } // don't allow comment-beginning strings because it messes up various parsers if ( strstr( p, "//" ) || strstr( p, "/*" ) ) { invalid = qtrue; } // don't allow empty names if ( *p == 0 || colorlessLen == 0 ) { invalid = qtrue; } // limit no. of code points if ( Q_UTF8_PrintStrlen( p ) > MAX_NAME_LENGTH_CP ) { invalid = qtrue; } // if something made the name bad, put them back to UnnamedPlayer if ( invalid || !hasletter ) { Q_strncpyz( p, G_UnnamedClientName( client ), outSize ); } }
/* =========== G_ClientCleanName ============ */ static void G_ClientCleanName( const char *in, char *out, int outSize, gclient_t *client ) { --outSize; bool has_visible_characters = false; std::string out_string; bool hasletter = false; int spaces = 0; for ( const auto& token : Color::Parser( in ) ) { if ( out_string.size() + token.Size() > outSize ) { break; } if ( token.Type() == Color::Token::TokenType::CHARACTER ) { int cp = Q_UTF8_CodePoint(token.Begin()); // don't allow leading spaces // TODO: use a Unicode-aware isspace if ( !has_visible_characters && Str::cisspace( cp ) ) { continue; } // don't allow nonprinting characters or (dead) console keys // but do allow UTF-8 (unvalidated) if ( cp >= 0 && cp < ' ' ) { continue; } bool escaped_emote = false; // single trailing ^ will mess up some things if ( cp == Color::Constants::ESCAPE && !*token.End() ) { if ( out_string.size() + 2 > outSize ) { break; } out_string += Color::Constants::ESCAPE; } else if ( !g_emoticonsAllowedInNames.integer && G_IsEmoticon( in, &escaped_emote ) ) { if ( out_string.size() + 2 + token.Size() > outSize ) { break; } out_string += "[["; if ( escaped_emote ) { continue; } } if ( Q_Unicode_IsAlphaOrIdeo( cp ) ) { hasletter = true; } // don't allow too many consecutive spaces // TODO: use a Unicode-aware isspace if ( Str::cisspace( cp ) ) { spaces++; if ( spaces > 3 ) { continue; } } else { spaces = 0; has_visible_characters = true; } } else if ( token.Type() == Color::Token::TokenType::ESCAPE ) { has_visible_characters = true; } out_string.append(token.Begin(), token.Size()); } bool invalid = false; // don't allow names beginning with S_SKIPNOTIFY because it messes up /ignore-related code if ( !out_string.compare( 0, 12, S_SKIPNOTIFY ) ) { invalid = true; } // don't allow comment-beginning strings because it messes up various parsers if ( out_string.find( "//" ) != std::string::npos || out_string.find( "/*" ) != std::string::npos ) { out_string.erase( std::remove( out_string.begin(), out_string.end(), '/' ) ); } // don't allow empty names if ( out_string.empty() || !hasletter ) { invalid = true; } // don't allow names beginning with digits else if ( Str::cisdigit( out_string[0] ) ) { out_string.erase( out_string.begin(), std::find_if_not( out_string.begin(), out_string.end(), Str::cisdigit ) ); } // if something made the name bad, put them back to UnnamedPlayer if ( invalid ) { Q_strncpyz( out, G_UnnamedClientName( client ), outSize ); } else { Q_strncpyz( out, out_string.c_str(), outSize ); } }
/* =========== G_ClientCleanName ============ */ static void G_ClientCleanName( const char *in, char *out, int outSize, gclient_t *client ) { int len, colorlessLen; char *p; int spaces; qboolean escaped; qboolean invalid = qfalse; //save room for trailing null byte outSize--; len = 0; colorlessLen = 0; p = out; *p = 0; spaces = 0; for ( ; *in; in++ ) { // don't allow leading spaces if ( colorlessLen == 0 && *in == ' ' ) { continue; } // don't allow nonprinting characters or (dead) console keys if ( *in < ' ' || *in > '}' || *in == '`' ) { continue; } // check colors if ( Q_IsColorString( in ) ) { in++; // make sure room in dest for both chars if ( len > outSize - 2 ) { break; } *out++ = Q_COLOR_ESCAPE; *out++ = *in; len += 2; continue; } else if ( !g_emoticonsAllowedInNames.integer && G_IsEmoticon( in, &escaped ) ) { // make sure room in dest for both chars if ( len > outSize - 2 ) { break; } *out++ = '['; *out++ = '['; len += 2; if ( escaped ) { in++; } continue; } // don't allow too many consecutive spaces if ( *in == ' ' ) { spaces++; if ( spaces > 3 ) { continue; } } else { spaces = 0; } if ( len > outSize - 1 ) { break; } *out++ = *in; colorlessLen++; len++; } *out = 0; // don't allow names beginning with "[skipnotify]" because it messes up /ignore-related code if ( !Q_stricmpn( p, "[skipnotify]", 12 ) ) { invalid = qtrue; } // don't allow comment-beginning strings because it messes up various parsers if ( strstr( p, "//" ) || strstr( p, "/*" ) ) { invalid = qtrue; } // don't allow empty names if ( *p == 0 || colorlessLen == 0 ) { invalid = qtrue; } // if something made the name bad, put them back to UnnamedPlayer if ( invalid ) { Q_strncpyz( p, G_UnnamedClientName( client ), outSize ); } }