Counter Strike : Global Offensive Source Code
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

672 lines
23 KiB

//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
//=============================================================================//
#include "cbase.h"
#include "cdll_int.h"
#include "gameinterface.h"
#include "mapentities.h"
#include "cs_gameinterface.h"
#include "ai_responsesystem.h"
#include "iachievementmgr.h"
#include "fmtstr.h"
#include "gametypes.h"
#include "matchmaking/imatchframework.h"
#include "cs_shareddefs.h"
#include "cs_gamerules.h"
#include "gametypes.h"
#include "engine/inetsupport.h"
#include "dedicated_server_ugc_manager.h"
#include "cs_player.h"
#include "server_log_http_dispatcher.h"
#include "netmessages.h"
#include "usermessages.h"
// NOTE: This has to be the last file included!
#include "tier0/memdbgon.h"
//////////////////////////////////////////////////////////////////////////
//
// Convars
//
ConVar sv_workshop_allow_other_maps( "sv_workshop_allow_other_maps", "1", FCVAR_RELEASE, "When hosting a workshop collection, users can play other workshop map on this server when it is empty and then mapcycle into this server collection." );
static ConVar tv_allow_camera_man_steamid( "tv_allow_camera_man_steamid", "", FCVAR_RELEASE, "Allows tournament production cameraman to run csgo.exe -interactivecaster on SteamID 7650123456XXX and be the camera man." );
// #define SVGC_RESERVATION_DEBUG 1
// -------------------------------------------------------------------------------------------- //
// Mod-specific CServerGameClients implementation.
// -------------------------------------------------------------------------------------------- //
void CServerGameClients::GetPlayerLimits( int& minplayers, int& maxplayers, int &defaultMaxPlayers ) const
{
minplayers = 1; // allow single player for the test maps (but we default to multi)
maxplayers = MAX_PLAYERS;
defaultMaxPlayers = MAX_PLAYERS;
}
// -------------------------------------------------------------------------------------------- //
// Mod-specific CServerGameDLL implementation.
// -------------------------------------------------------------------------------------------- //
void CServerGameDLL::LevelInit_ParseAllEntities( const char *pMapEntities )
{
if ( Q_strcmp( STRING(gpGlobals->mapname), "cs_" ) )
{
// don't precache AI responses (hostages) if it's not a hostage rescure map
extern IResponseSystem *g_pResponseSystem;
g_pResponseSystem->PrecacheResponses( false );
}
}
//
// Twitch.tv reservation updates
//
class ClientJob_EMsgGCCStrike15_v2_GC2ServerReservationUpdate : public GCSDK::CGCClientJob
{
public:
ClientJob_EMsgGCCStrike15_v2_GC2ServerReservationUpdate( GCSDK::CGCClient *pGCClient )
: GCSDK::CGCClientJob( pGCClient )
{
}
virtual bool BYieldingRunJobFromMsg( GCSDK::IMsgNetPacket *pNetPacket )
{
GCSDK::CProtoBufMsg<CMsgGCCStrike15_v2_GC2ServerReservationUpdate> msg( pNetPacket );
uint32 numTotalViewers = msg.Body().viewers_external_total();
uint32 numSteamLinkedViewers = msg.Body().viewers_external_steam();
engine->UpdateHltvExternalViewers( numTotalViewers, numSteamLinkedViewers );
return true;
}
};
GC_REG_CLIENT_JOB( ClientJob_EMsgGCCStrike15_v2_GC2ServerReservationUpdate, k_EMsgGCCStrike15_v2_GC2ServerReservationUpdate );
void GCCStrikeWelcomeMessageReceived( CMsgCStrike15Welcome const &msgCStrike )
{
}
bool Helper_FillServerReservationStateAndPlayers( CMsgGCCStrike15_v2_MatchmakingServerReservationResponse &msgbody )
{
if ( !engine->IsDedicatedServer() )
return false;
msgbody.set_server_version( ( ( INetSupport * ) g_pMatchFramework->GetMatchExtensions()->GetRegisteredExtensionInterface( INETSUPPORT_VERSION_STRING ) )->GetEngineBuildNumber() );
msgbody.set_map( STRING( gpGlobals->mapname ) );
static ConVarRef sv_steamdatagramtransport_port( "sv_steamdatagramtransport_port" );
// Expose information about our community server GOTV port so that clients could connect
static ConVarRef tv_advertise_watchable( "tv_advertise_watchable" );
static int s_nTvPort = 0; // make the TV port sticky: if we reported it non-zero once then keep reporting
CEngineHltvInfo_t engineHltvInfo;
if ( tv_advertise_watchable.GetBool() &&
engine->GetEngineHltvInfo( engineHltvInfo ) && engineHltvInfo.m_bBroadcastActive )
{
s_nTvPort = engineHltvInfo.m_nTvPort;
}
if ( s_nTvPort )
{
msgbody.mutable_tv_info()->set_tv_udp_port( s_nTvPort );
}
// Build the list of players who are actively playing on the game server
for ( int i = 1; i <= gpGlobals->maxClients; i++ )
{
CBasePlayer *pPlayer = UTIL_PlayerByIndex( i );
if ( pPlayer )
{
if ( pPlayer->IsBot() )
continue;
CSteamID steamIdPlayer;
if ( !pPlayer->GetSteamID( &steamIdPlayer ) )
continue;
if ( !steamIdPlayer.IsValid() )
continue;
switch ( pPlayer->GetTeamNumber() )
{
case TEAM_CT:
case TEAM_TERRORIST:
msgbody.add_reward_player_accounts( steamIdPlayer.GetAccountID() );
break;
default:
msgbody.add_idle_player_accounts( steamIdPlayer.GetAccountID() );
break;
}
}
}
return true;
}
void CServerGameDLL::UpdateGCInformation()
{
/** Removed for partner depot **/
}
// Marks the queue matchmaking game as starting
void CServerGameDLL::ReportGCQueuedMatchStart( int32 iReservationStage, uint32 *puiConfirmedAccounts, int numConfirmedAccounts )
{
/** Removed for partner depot **/
}
//-----------------------------------------------------------------------------
// Purpose: A user has had their network id setup and validated
//-----------------------------------------------------------------------------
void CServerGameClients::NetworkIDValidated( const char *pszUserName, const char *pszNetworkID, CSteamID steamID )
{
/** Removed for partner depot **/
}
//
// Order workshop maps by MRU
//
static int Helper_SortWorkshopMapsMRU( const DedicatedServerUGCFileInfo_t * const *a, const DedicatedServerUGCFileInfo_t * const *b )
{
if ( (*a)->m_dblPlatFloatTimeReceived != (*b)->m_dblPlatFloatTimeReceived )
return ( (*a)->m_dblPlatFloatTimeReceived > (*b)->m_dblPlatFloatTimeReceived ) ? -1 : 1;
else
return 0;
}
//
// Matchmaking game data buffer to set into SteamGameServer()->SetGameData
//
void CServerGameDLL::GetMatchmakingGameData( char *buf, size_t bufSize )
{
char * const bufBase = buf;
int len = 0;
extern ConVar game_type;
extern ConVar game_mode;
// Put the game key
Q_snprintf( buf, bufSize, "g:csgo,gt:%u,gm:%u,", game_type.GetInt(), game_mode.GetInt() );
len = strlen( buf );
buf += len;
bufSize -= len;
if ( gpGlobals && !StringIsEmpty( gpGlobals->mapGroupName.ToCStr() ) )
{
const CUtlStringList* mapsInGroup = g_pGameTypes->GetMapGroupMapList( gpGlobals->mapGroupName.ToCStr() );
if ( mapsInGroup && g_pGameTypes->IsWorkshopMapGroup( gpGlobals->mapGroupName.ToCStr() ) )
{
if ( sv_workshop_allow_other_maps.GetBool() && ( bufSize >= 7 ) )
{ // Advertise support for other maps
Q_strncpy( buf, "wks:1,", 7 );
buf += 6;
bufSize -= 7;
}
CUtlVector< PublishedFileId_t > arrAdvertisedFileIds;
FOR_EACH_VEC( *mapsInGroup, i )
{
PublishedFileId_t id = DedicatedServerWorkshop().GetUGCMapPublishedFileID((*mapsInGroup)[i]);
CFmtStr szIdAsHexString( "%llx", id );
size_t len = szIdAsHexString.Length();
if ( bufSize <= len + 1 )
{
Warning( "GameData: Too many community maps installed, not advertising for map id \"%llu (0x%s)\"\n", id, szIdAsHexString.Access() );
continue;
}
Q_strncpy( buf, szIdAsHexString.Access(), len + 1 );
buf += len;
*( buf ++ ) = ',';
bufSize -= len + 1;
arrAdvertisedFileIds.AddToTail( id );
}
// Advertise maps that have been recently checked and downloaded from Workshop
if ( sv_workshop_allow_other_maps.GetBool() )
{
CUtlVector<const DedicatedServerUGCFileInfo_t *> arrInfoMaps;
DedicatedServerWorkshop().GetWorkshopMasWithValidUgcInformation( arrInfoMaps );
arrInfoMaps.Sort( Helper_SortWorkshopMapsMRU );
FOR_EACH_VEC( arrInfoMaps, iInfoMap )
{
PublishedFileId_t id = arrInfoMaps[iInfoMap]->fileId;
if ( arrAdvertisedFileIds.Find( id ) != arrAdvertisedFileIds.InvalidIndex() )
continue; // already advertised
CFmtStr szIdAsHexString( "%llx", id );
size_t len = szIdAsHexString.Length();
if ( bufSize <= len + 1 )
break; // Advertise only as much downloaded stuff as can fit
Q_strncpy( buf, szIdAsHexString.Access(), len + 1 );
buf += len;
*( buf ++ ) = ',';
bufSize -= len + 1;
}
}
}
}
// Trim the last comma if anything was written
if ( buf > bufBase )
buf[ -1 ] = 0;
}
// this returns true if they were already in the list or were successfully added
// returns false if they were not added (not allowed to be a caster)
bool AddAccountToActiveCasters( const CSteamID &steamID )
{
// first check if they are already in the list
bool bAlreadyAdded = false;
for ( int j = 0; j < CSGameRules()->m_arrTournamentActiveCasterAccounts.Count(); j++ )
{
if ( steamID.GetAccountID() == CSGameRules()->m_arrTournamentActiveCasterAccounts[ j ] )
{
// this caster is already in the list so skip adding them, but allow them
bAlreadyAdded = true;
break;
}
if ( CSGameRules()->m_arrTournamentActiveCasterAccounts[ j ] )
{
// already have an active caster, so don't allow another
return false;
}
}
if ( !bAlreadyAdded )
{
// not already added, so find an empty slot and put them in it
for (int j = 0; j < CSGameRules()->m_arrTournamentActiveCasterAccounts.Count(); j++ )
{
if ( CSGameRules()->m_arrTournamentActiveCasterAccounts[ j ] == 0 )
{
CSGameRules()->m_arrTournamentActiveCasterAccounts.Set( j, steamID.GetAccountID() );
if ( steamapicontext->SteamUser() && steamapicontext->SteamFriends() )
{
const char *pszName = steamapicontext->SteamFriends()->GetFriendPersonaName( steamID );
ConMsg( "Adding %s (ID:%d) to active caster list!\n", pszName, steamID.GetAccountID() );
}
else
{
ConMsg( "Adding ID:%d to active caster list!\n", steamID.GetAccountID() );
}
break;
}
}
}
return true;
}
// validate if player is a caster and is not playing in the current game, then add them to the active caster list
// returns false if they are not allow to be a caster
bool CServerGameDLL::ValidateAndAddActiveCaster( const CSteamID &steamID )
{
// check if they are a player in the current game. Note: players can be casters sometimes (and might be in the casters list below), but we don't want their voice data "public" when they are playing
for ( int i = 0; i < CCSGameRules::sm_QueuedServerReservation.account_ids().size(); i++ )
{
if ( steamID.GetAccountID() == CCSGameRules::sm_QueuedServerReservation.account_ids( i ) )
{
// this is a player
return false;
}
}
// they weren't in the player list, so now check the caster list
for ( int i = 0; i < CCSGameRules::sm_QueuedServerReservation.tournament_casters_account_ids().size(); i++ )
{
if ( steamID.GetAccountID() == CCSGameRules::sm_QueuedServerReservation.tournament_casters_account_ids( i ) )
{
// this is a caster
return AddAccountToActiveCasters( steamID );
}
}
if ( tv_allow_camera_man_steamid.GetString()[0] && engine->IsDedicatedServer() )
{
CSteamID steamidCameraMan( V_atoui64( tv_allow_camera_man_steamid.GetString() ) );
if ( steamidCameraMan.IsValid() && steamidCameraMan.BIndividualAccount() && steamidCameraMan.GetAccountID() &&
( steamidCameraMan.GetAccountID() == steamID.GetAccountID() ) )
{
return AddAccountToActiveCasters( steamID );
}
}
return false;
}
// Returns which encryption key to use for messages to be encrypted for TV
EncryptedMessageKeyType_t CServerGameDLL::GetMessageEncryptionKey( INetMessage *pMessage )
{
switch ( pMessage->GetType() )
{
case svc_VoiceData:
{
// check the voice data packets for being from an active caster and add the caster flag and use the public key
CSVCMsg_VoiceData_t *pVoiceData = ( CSVCMsg_VoiceData_t * ) pMessage;
CSteamID steamID( static_cast<uint64>( pVoiceData->xuid() ) );
if ( steamID.GetAccountID() )
{
for ( int j = 0; j < CSGameRules()->m_arrTournamentActiveCasterAccounts.Count(); j++ )
{
if ( steamID.GetAccountID() == CSGameRules()->m_arrTournamentActiveCasterAccounts[ j ] )
{
pVoiceData->set_caster( true );
return kEncryptedMessageKeyType_Public;
}
}
}
}
return kEncryptedMessageKeyType_Private;
case svc_UserMessage:
{
CSVCMsg_UserMessage_t *pUsrMessageHeader = ( CSVCMsg_UserMessage_t * ) pMessage;
switch ( pUsrMessageHeader->msg_type() )
{
case CS_UM_SayText:
{
CCSUsrMsg_SayText usrMsg;
if ( usrMsg.ParseFromArray( &pUsrMessageHeader->msg_data().at( 0 ), pUsrMessageHeader->msg_data().size() ) )
{
if ( usrMsg.textallchat() )
return kEncryptedMessageKeyType_Public;
}
}
return kEncryptedMessageKeyType_Private;
case CS_UM_SayText2:
{
CCSUsrMsg_SayText2 usrMsg;
if ( usrMsg.ParseFromArray( &pUsrMessageHeader->msg_data().at( 0 ), pUsrMessageHeader->msg_data().size() ) )
{
if ( usrMsg.textallchat() )
return kEncryptedMessageKeyType_Public;
}
}
return kEncryptedMessageKeyType_Private;
case CS_UM_TextMsg:
case CS_UM_RadioText:
case CS_UM_RawAudio:
case CS_UM_SendAudio:
return kEncryptedMessageKeyType_Private;
default:
return kEncryptedMessageKeyType_None;
}
}
return kEncryptedMessageKeyType_None;
case svc_EncryptedData:
default:
return kEncryptedMessageKeyType_None;
}
}
// If server game dll needs more time before server process quits then
// it should return true to hold game server reservation from this interface method.
// If this method returns false then the server process will clear the reservation
// and might shutdown to meet uptime or memory limit requirements.
bool CServerGameDLL::ShouldHoldGameServerReservation( float flTimeElapsedWithoutClients )
{
/** Removed for partner depot **/
return false; // let the server get unreserved
}
// Pure server validation failed for the given client, client supplied
// data is included in the payload
void CServerGameDLL::OnPureServerFileValidationFailure( edict_t *edictClient, const char *path, const char *fileName, uint32 crc, int32 hashType, int32 len, int packNumber, int packFileID )
{
/** Removed for partner depot **/
}
// Last chance validation on connect packet for the client, non-NULL return value
// causes the client connect to be aborted with the provided error
char const * CServerGameDLL::ClientConnectionValidatePreNetChan( bool bGameServer, char const *adr, int nAuthProtocol, uint64 ullSteamID )
{
/** Removed for partner depot **/
return NULL; // allow connections by default
}
// Network channel notification from engine to game server code
void CServerGameDLL::OnEngineClientNetworkEvent( edict_t *edictClient, uint64 ullSteamID, int nEventType, void *pvParam )
{
/** Removed for partner depot **/
}
// Game server notifying GC with its sync packet
void CServerGameDLL::EngineGotvSyncPacket( const CEngineGotvSyncPacket *pPkt )
{
/** Removed for partner depot **/
}
// GOTV client attempt redirect over SDR
bool CServerGameDLL::OnEngineClientProxiedRedirect( uint64 ullClient, const char *adrProxiedRedirect, const char *adrRegular )
{
/** Removed for partner depot **/
return false;
}
bool CServerGameDLL::LogForHTTPListeners( const char* szLogLine )
{
return GetServerLogHTTPDispatcher()->LogForHTTPListeners( szLogLine );
}
//-----------------------------------------------------------------------------
// Purpose: Called to apply lobby settings to a dedicated server
//-----------------------------------------------------------------------------
void CServerGameDLL::ApplyGameSettings( KeyValues *pKV )
{
if ( !pKV )
{
return;
}
if ( engine )
{
DevMsg( "CServerGameDLL::ApplyGameSettings game settings payload received:\n" );
KeyValuesDumpAsDevMsg( pKV, 1 );
const char* pMapName = NULL;
const char* pMapNameFromKV = pKV->GetString( "game/map" );
const char* pGameType = pKV->GetString( "game/type" );
const char* pGameMode = pKV->GetString( "game/mode" );
const char* pMapGroupName = pKV->GetString( "game/mapgroupname", NULL );
const char* pMapGroupNameToValidate = NULL; // pMapGroupName is ok to be NULL; this variable lets us easily use a non null pMapGroupName or gpGlobals->mapGroupName
if ( !IsValveDS() &&
pMapNameFromKV && StringHasPrefix( pMapNameFromKV, "workshop" ) &&
pMapGroupName && Q_stristr( pMapGroupName, "@workshop" ) )
{
// A community server is getting reserved by a client for a workshop map,
// retain our current workshop collection if we are hosting one to preserve
// map rotation process
pMapGroupName = engine->IsDedicatedServer() ? STRING( gpGlobals->mapGroupName ) : pMapGroupName;
}
if ( pMapGroupName && (pMapGroupName[0] != '\0') && !pMapNameFromKV )
{
// if we have a mapgroup name, then we don't care about any map name from the pKV and we just want the first map from the mapgroup
pMapName = g_pGameTypes->GetRandomMap( pMapGroupName );
pMapGroupNameToValidate = pMapGroupName;
}
else
{
pMapGroupNameToValidate = ( pMapGroupName && (pMapGroupName[0] != '\0') ) ? pMapGroupName : STRING( gpGlobals->mapGroupName );
}
// make sure we are not using a bogus mapgroup name
if ( pMapGroupNameToValidate && !StringIsEmpty( pMapGroupNameToValidate ) && !g_pGameTypes->IsValidMapGroupName( pMapGroupNameToValidate ) )
{
Warning( "ApplyGameSettings: Invalid mapgroup name %s\n", pMapGroupNameToValidate );
return;
}
// only use the map name from the pKV if there was no mapgroup name in the pKV
if ( !pMapName )
{
pMapName = pMapNameFromKV;
}
// For team games we add the prefix "team" to the game type. This is to
// eliminate team game lobbies from searches for QuickMatch and Custom Match
char *teamStr = "team";
const char *pTeamPrefix = Q_strstr( pGameType, teamStr);
if ( pTeamPrefix == pGameType )
{
pGameType += Q_strlen( teamStr );
}
if ( pMapName && pMapName[0] != '\0' )
{
// validate map exists in the mapgroup
if ( !g_pGameTypes->IsValidMapInMapGroup( pMapGroupNameToValidate, pMapName ) )
{
Warning( "ApplyGameSettings: Map %s not part of Mapgroup %s\n", pMapName, pMapGroupNameToValidate );
}
int extraSpectators = 2;
if ( ( pGameType && pGameType[0] != '\0' ) &&
( pGameMode && pGameMode[0] != '\0' ) )
{
// make sure the mapgroup is in this game type & mode
if ( !g_pGameTypes->IsValidMapGroupForTypeAndMode( pMapGroupNameToValidate, pGameType, pGameMode ) )
{
Warning( "ApplyGameSettings: MapGroup %s not part of type %s mode %s\n", pMapGroupNameToValidate, pGameType, pGameMode );
}
// Get the bot difficulty setting before it gets reverted.
ConVarRef cvCustomBotDiff( "custom_bot_difficulty" );
int customBotDiff = cvCustomBotDiff.GetInt();
/*
// FIXME[pmf]: We don't want to reset all replicated convars unless we also re-exec game.cfg on the server,
// otherwise we'll overwrite all the game configuration convars specified in game.cfg
// Reset server enforced convars
g_pCVar->RevertFlaggedConVars( FCVAR_REPLICATED );
*/
// Cheats were disabled; revert all cheat cvars to their default values.
// This must be done heading into multiplayer games because people can play
// demos etc and set cheat cvars with sv_cheats 0.
g_pCVar->RevertFlaggedConVars( FCVAR_CHEAT );
// we know that the loading screen data is correct, so let the loading screen know
//g_pGameTypes->SetLoadingScreenDataIsCorrect( true );
g_pGameTypes->SetRunMapWithDefaultGametype( false );
// Set game_type and game_mode convars.
g_pGameTypes->SetGameTypeAndMode( pGameType, pGameMode );
extern ConVar game_online;
if ( char const *szOnline = pKV->GetString( "system/network", NULL ) )
{
game_online.SetValue( ( !V_stricmp( szOnline, "LIVE" ) ) ? 1 : 0 );
}
extern ConVar game_public;
if ( char const *szAccess = pKV->GetString( "system/access", NULL ) )
{
game_public.SetValue( ( !V_stricmp( szAccess, "public" ) ) ? 1 : 0 );
}
#ifndef CLIENT_DLL
if ( engine->IsDedicatedServer() )
game_online.SetValue( 1 );
#endif
// Special case: set the custom bot difficulty for offline games
if ( !game_online.GetBool() )
{
g_pGameTypes->SetCustomBotDifficulty( customBotDiff );
}
// Make sure that correct number of slots is set for the engine
{
int iType, iMode;
if ( g_pGameTypes->GetGameModeAndTypeIntsFromStrings( pGameType, pGameMode, iType, iMode ) )
{
int iMaxPlayersForTypeMode = g_pGameTypes->GetMaxPlayersForTypeAndMode( iType, iMode );
pKV->SetInt( "members/numSlots", iMaxPlayersForTypeMode );
}
}
// Make sure the settings keys have extra spectator info
pKV->SetInt( "members/numExtraSpectatorSlots", extraSpectators );
}
CFmtStr command;
if ( pMapGroupName )
{
command.AppendFormat( "mapgroup %s\n", pMapGroupName );
}
command.AppendFormat( "nextlevel %s\n", pMapName ); // gamerules will clean it up when they construct for the next map
command.AppendFormat( "map %s reserved\n", pMapName );
Warning( "Executing server command:\n%s\n---\n", command.Access() );
engine->ServerCommand( command );
if ( engine->IsDedicatedServer() )
engine->ServerExecute();
}
}
}
const char * CServerGameClients::ClientNameHandler( uint64 xuid, const char *pchName )
{
CSteamID steamID( xuid );
// In tournament mode force names for the players according to the reservation
if ( steamID.IsValid() && steamID.BIndividualAccount() &&
CCSGameRules::sm_QueuedServerReservation.has_tournament_event() )
{
for ( int32 iTeam = 0; iTeam < CCSGameRules::sm_QueuedServerReservation.tournament_teams().size(); ++iTeam )
{
TournamentTeam const &ttTeam = CCSGameRules::sm_QueuedServerReservation.tournament_teams( iTeam );
for ( int32 iTeamPlayer = 0; iTeamPlayer < ttTeam.players().size(); ++iTeamPlayer )
{
TournamentPlayer const &ttPlayer = ttTeam.players( iTeamPlayer );
if ( ttPlayer.account_id() && ( ttPlayer.account_id() == steamID.GetAccountID() ) )
{
return ( ttPlayer.player_nick().c_str() );
}
}
}
}
// Throttle name changes from clients (hacked clients can set the name convar at any rate)
extern CCSPlayer *ToCSPlayer( CBaseEntity *pEntity );
if ( CCSPlayer* pPlayer = ToCSPlayer( CBasePlayer::GetPlayerBySteamID( steamID ) ) )
{
if ( !pPlayer->CanChangeName() )
return pPlayer->GetPlayerName();
}
// Account not resolved, use whatever name they provided
return pchName;
}
void CServerGameClients::ClientSvcUserMessage( edict_t *pEntity, int nType, int nPassthrough, uint32 cbSize, const void *pvBuffer )
{
CCSPlayer *pPlayer = ToCSPlayer( GetContainingEntity( pEntity ) );
if ( !pPlayer )
return;
switch ( nType )
{
case CS_UM_PlayerDecalDigitalSignature:
{
CCSUsrMsg_PlayerDecalDigitalSignature msg;
if ( msg.ParseFromArray( pvBuffer, cbSize ) )
pPlayer->SprayPaint( msg );
}
return;
}
}