/*** * * Copyright (c) 1999, Valve LLC. All rights reserved. * * This product contains software technology licensed from Id * Software, Inc. ("Id Technology"). Id Technology (c) 1996 Id Software, Inc. * All Rights Reserved. * * Use, distribution, and modification of this source code and/or resulting * object code is restricted to non-commercial enhancements to products from * Valve LLC. All other use, distribution, or modification is prohibited * without written permission from Valve LLC. * ****/ // // Scoreboard.cpp // // implementation of CHudScoreboard class // #include "hud.h" #include "cl_util.h" #include "parsemsg.h" #include "triangleapi.h" #include #include cvar_t *cl_scoreboard_bg; cvar_t *cl_showpacketloss; #if USE_VGUI #include "vgui_TeamFortressViewport.h" #endif DECLARE_COMMAND( m_Scoreboard, ShowScores ) DECLARE_COMMAND( m_Scoreboard, HideScores ) #if !USE_VGUI || USE_NOVGUI_SCOREBOARD DECLARE_MESSAGE( m_Scoreboard, ScoreInfo ) DECLARE_MESSAGE( m_Scoreboard, TeamInfo ) DECLARE_MESSAGE( m_Scoreboard, TeamScore ) #endif int CHudScoreboard::Init( void ) { gHUD.AddHudElem( this ); // Hook messages & commands here // HOOK_COMMAND( "+showscores", ShowScores ); // HOOK_COMMAND( "-showscores", HideScores ); #if !USE_VGUI || USE_NOVGUI_SCOREBOARD HOOK_MESSAGE( ScoreInfo ); HOOK_MESSAGE( TeamScore ); HOOK_MESSAGE( TeamInfo ); #endif InitHUDData(); cl_scoreboard_bg = CVAR_CREATE( "cl_scoreboard_bg", "1", FCVAR_ARCHIVE ); cl_showpacketloss = CVAR_CREATE( "cl_showpacketloss", "0", FCVAR_ARCHIVE ); return 1; } int CHudScoreboard::VidInit( void ) { // Load sprites here return 1; } void CHudScoreboard::InitHUDData( void ) { memset( g_PlayerExtraInfo, 0, sizeof g_PlayerExtraInfo ); m_iLastKilledBy = 0; m_fLastKillTime = 0; m_iPlayerNum = 0; m_iNumTeams = 0; memset( g_TeamInfo, 0, sizeof g_TeamInfo ); m_iFlags &= ~HUD_ACTIVE; // starts out inactive m_iFlags |= HUD_INTERMISSION; // is always drawn during an intermission } /* The scoreboard We have a minimum width of 1-320 - we could have the field widths scale with it? */ // X positions // relative to the side of the scoreboard #define NAME_RANGE_MIN 20 #define NAME_RANGE_MAX 145 #define NAME_RANGE_MODIFIER 120 #define KILLS_RANGE_MIN 130 #define KILLS_RANGE_MAX 170 #define DIVIDER_POS 180 #define DEATHS_RANGE_MIN 185 #define DEATHS_RANGE_MAX 210 #define PING_RANGE_MIN 245 #define PING_RANGE_MAX 295 #define PL_RANGE_MIN 315 #define PL_RANGE_MAX 375 int SCOREBOARD_WIDTH = 320; // Y positions #define ROW_GAP (gHUD.m_scrinfo.iCharHeight) #define ROW_RANGE_MIN 15 #define ROW_RANGE_MAX ( ScreenHeight - 50 ) int CHudScoreboard::Draw( float fTime ) { int i, j, can_show_packetloss = 0; int FAR_RIGHT; gHUD.m_iNoConsolePrint &= ~( 1 << 0 ); if( !m_iShowscoresHeld && gHUD.m_Health.m_iHealth > 0 && !gHUD.m_iIntermission ) return 1; gHUD.m_iNoConsolePrint |= 1 << 0; GetAllPlayersInfo(); // Packetloss removed on Kelly 'shipping nazi' Bailey's orders if( cl_showpacketloss && cl_showpacketloss->value && ( ScreenWidth >= 400 ) ) { can_show_packetloss = 1; SCOREBOARD_WIDTH = ( ScreenWidth >= 520 ) ? ( 400 - NAME_RANGE_MODIFIER ) : 400; } else { SCOREBOARD_WIDTH = ( ScreenWidth >= 440 ) ? ( 320 - NAME_RANGE_MODIFIER ) : 320; } // just sort the list on the fly // list is sorted first by frags, then by deaths float list_slot = 0; int xpos_rel = ( ScreenWidth - SCOREBOARD_WIDTH ) / 2; // print the heading line int ypos = ROW_RANGE_MIN + ( list_slot * ROW_GAP ); int xpos = NAME_RANGE_MIN + xpos_rel; FAR_RIGHT = can_show_packetloss ? PL_RANGE_MAX : PING_RANGE_MAX; FAR_RIGHT += 5; if( ( ScreenWidth >= 440 && !can_show_packetloss ) || ( ScreenWidth >= 520 && can_show_packetloss ) ) { xpos -= NAME_RANGE_MODIFIER; FAR_RIGHT += NAME_RANGE_MODIFIER; } if( cl_scoreboard_bg && cl_scoreboard_bg->value ) gHUD.DrawDarkRectangle( xpos - 5, ypos - 5, FAR_RIGHT, ROW_RANGE_MAX ); if( !gHUD.m_Teamplay ) DrawUtfString( xpos, ypos, NAME_RANGE_MAX + xpos_rel, "Player", 255, 140, 0 ); else DrawUtfString( xpos, ypos, NAME_RANGE_MAX + xpos_rel, "Teams", 255, 140, 0 ); gHUD.DrawHudStringReverse( KILLS_RANGE_MAX + xpos_rel, ypos, 0, "kills", 255, 140, 0 ); DrawUtfString( DIVIDER_POS + xpos_rel, ypos, ScreenWidth, "/", 255, 140, 0 ); DrawUtfString( DEATHS_RANGE_MIN + xpos_rel + 5, ypos, ScreenWidth, "deaths", 255, 140, 0 ); DrawUtfString( PING_RANGE_MAX + xpos_rel - 35, ypos, ScreenWidth, "latency", 255, 140, 0 ); if( can_show_packetloss ) { DrawUtfString( PL_RANGE_MAX + xpos_rel - 35, ypos, ScreenWidth, "pkt loss", 255, 140, 0 ); } list_slot += 1.2f; ypos = ROW_RANGE_MIN + ( list_slot * ROW_GAP ); // xpos = NAME_RANGE_MIN + xpos_rel; FillRGBA( xpos - 4, ypos, FAR_RIGHT -2, 1, 255, 140, 0, 255 ); // draw the seperator line list_slot += 0.8f; if( !gHUD.m_Teamplay ) { // it's not teamplay, so just draw a simple player list DrawPlayers( xpos_rel, list_slot ); return 1; } // clear out team scores for( i = 1; i <= m_iNumTeams; i++ ) { if( !g_TeamInfo[i].scores_overriden ) g_TeamInfo[i].frags = g_TeamInfo[i].deaths = 0; g_TeamInfo[i].ping = g_TeamInfo[i].packetloss = 0; } // recalc the team scores, then draw them for( i = 1; i < MAX_PLAYERS; i++ ) { //if( g_PlayerInfoList[i].name == NULL ) // continue; // empty player slot, skip if( g_PlayerExtraInfo[i].teamname[0] == 0 ) continue; // skip over players who are not in a team // find what team this player is in for( j = 1; j <= m_iNumTeams; j++ ) { if( !stricmp( g_PlayerExtraInfo[i].teamname, g_TeamInfo[j].name ) ) break; } if( j > m_iNumTeams ) // player is not in a team, skip to the next guy continue; if( !g_TeamInfo[j].scores_overriden ) { g_TeamInfo[j].frags += g_PlayerExtraInfo[i].frags; g_TeamInfo[j].deaths += g_PlayerExtraInfo[i].deaths; } g_TeamInfo[j].ping += g_PlayerInfoList[i].ping; g_TeamInfo[j].packetloss += g_PlayerInfoList[i].packetloss; if( g_PlayerInfoList[i].thisplayer ) g_TeamInfo[j].ownteam = TRUE; else g_TeamInfo[j].ownteam = FALSE; } // find team ping/packetloss averages for( i = 1; i <= m_iNumTeams; i++ ) { g_TeamInfo[i].already_drawn = FALSE; if( g_TeamInfo[i].players > 0 ) { g_TeamInfo[i].ping /= g_TeamInfo[i].players; // use the average ping of all the players in the team as the teams ping g_TeamInfo[i].packetloss /= g_TeamInfo[i].players; // use the average ping of all the players in the team as the teams ping } } // Draw the teams while( 1 ) { int highest_frags = -99999; int lowest_deaths = 99999; int best_team = 0; for( i = 1; i <= m_iNumTeams; i++ ) { if( g_TeamInfo[i].players < 0 ) continue; if( !g_TeamInfo[i].already_drawn && g_TeamInfo[i].frags >= highest_frags ) { if( g_TeamInfo[i].frags > highest_frags || g_TeamInfo[i].deaths < lowest_deaths ) { best_team = i; lowest_deaths = g_TeamInfo[i].deaths; highest_frags = g_TeamInfo[i].frags; } } } // draw the best team on the scoreboard if( !best_team ) break; // draw out the best team team_info_t *team_info = &g_TeamInfo[best_team]; ypos = ROW_RANGE_MIN + ( list_slot * ROW_GAP ); // check we haven't drawn too far down if( ypos > ROW_RANGE_MAX ) // don't draw to close to the lower border break; xpos = NAME_RANGE_MIN + xpos_rel; if( ( ScreenWidth >= 440 && !can_show_packetloss ) || ( ScreenWidth >= 520 && can_show_packetloss ) ) { xpos -= NAME_RANGE_MODIFIER; } int r = 255, g = 225, b = 55; // draw the stuff kinda yellowish if( team_info->ownteam ) // if it is their team, draw the background different color { // overlay the background in blue, then draw the score text over it FillRGBA( xpos - 5, ypos, FAR_RIGHT, ROW_GAP, 0, 0, 255, 70 ); } // draw their name (left to right) DrawUtfString( xpos, ypos, NAME_RANGE_MAX + xpos_rel, team_info->name, r, g, b ); // draw kills (right to left) xpos = KILLS_RANGE_MAX + xpos_rel; gHUD.DrawHudNumberString( xpos, ypos, KILLS_RANGE_MIN + xpos_rel, team_info->frags, r, g, b ); // draw divider xpos = DIVIDER_POS + xpos_rel; DrawUtfString( xpos, ypos, xpos + 20, "/", r, g, b ); // draw deaths xpos = DEATHS_RANGE_MAX + xpos_rel; gHUD.DrawHudNumberString( xpos, ypos, DEATHS_RANGE_MIN + xpos_rel, team_info->deaths, r, g, b ); // draw ping // draw ping & packetloss static char buf[64]; sprintf( buf, "%d", team_info->ping ); xpos = ( ( PING_RANGE_MAX - PING_RANGE_MIN ) / 2) + PING_RANGE_MIN + xpos_rel + 25; UnpackRGB( r, g, b, RGB_YELLOWISH ); gHUD.DrawHudStringReverse( xpos, ypos, xpos - 50, buf, r, g, b ); // Packetloss removed on Kelly 'shipping nazi' Bailey's orders if( can_show_packetloss ) { xpos = ( ( PL_RANGE_MAX - PL_RANGE_MIN ) / 2) + PL_RANGE_MIN + xpos_rel + 25; sprintf( buf, " %d", team_info->packetloss ); DrawUtfString( xpos, ypos, xpos+50, buf, r, g, b ); } team_info->already_drawn = TRUE; // set the already_drawn to be TRUE, so this team won't get drawn again list_slot++; // draw all the players that belong to this team, indented slightly list_slot = DrawPlayers( xpos_rel, list_slot, 10, team_info->name ); } // draw all the players who are not in a team list_slot += 0.5f; DrawPlayers( xpos_rel, list_slot, 0, "" ); return 1; } extern float *GetClientColor( int client ); // returns the ypos where it finishes drawing int CHudScoreboard::DrawPlayers( int xpos_rel, float list_slot, int nameoffset, const char *team ) { int can_show_packetloss = 0; int FAR_RIGHT; // Packetloss removed on Kelly 'shipping nazi' Bailey's orders if( cl_showpacketloss && cl_showpacketloss->value && ( ScreenWidth >= 400 ) ) { can_show_packetloss = 1; SCOREBOARD_WIDTH = 400; } else { SCOREBOARD_WIDTH = 320; } FAR_RIGHT = can_show_packetloss ? PL_RANGE_MAX : PING_RANGE_MAX; FAR_RIGHT += 5; if( ( ScreenWidth >= 440 && !can_show_packetloss ) || ( ScreenWidth >= 520 && can_show_packetloss ) ) { FAR_RIGHT += NAME_RANGE_MODIFIER; } // draw the players, in order, and restricted to team if set while( 1 ) { // Find the top ranking player int highest_frags = -99999; int lowest_deaths = 99999; int best_player = 0; for( int i = 1; i < MAX_PLAYERS; i++ ) { if( g_PlayerInfoList[i].name && g_PlayerExtraInfo[i].frags >= highest_frags ) { if( !( team && stricmp( g_PlayerExtraInfo[i].teamname, team ) ) ) // make sure it is the specified team { extra_player_info_t *pl_info = &g_PlayerExtraInfo[i]; if( pl_info->frags > highest_frags || pl_info->deaths < lowest_deaths ) { best_player = i; lowest_deaths = pl_info->deaths; highest_frags = pl_info->frags; } } } } if( !best_player ) break; // draw out the best player hud_player_info_t *pl_info = &g_PlayerInfoList[best_player]; int ypos = ROW_RANGE_MIN + ( list_slot * ROW_GAP ); // check we haven't drawn too far down if( ypos > ROW_RANGE_MAX ) // don't draw to close to the lower border break; int xpos = NAME_RANGE_MIN + xpos_rel; if( ( ScreenWidth >= 440 && !can_show_packetloss ) || ( ScreenWidth >= 520 && can_show_packetloss ) ) { xpos -= NAME_RANGE_MODIFIER; } int r = 255, g = 255, b = 255; float *colors = GetClientColor( best_player ); r *= colors[0], g *= colors[1], b *= colors[2]; if( best_player == m_iLastKilledBy && m_fLastKillTime && m_fLastKillTime > gHUD.m_flTime ) { if( pl_info->thisplayer ) { // green is the suicide color? i wish this could do grey... FillRGBA( xpos - 5, ypos, FAR_RIGHT, ROW_GAP, 80, 155, 0, 70 ); } else { // Highlight the killers name - overlay the background in red, then draw the score text over it FillRGBA( xpos - 5, ypos, FAR_RIGHT, ROW_GAP, 255, 0, 0, ( (float)15 * (float)( m_fLastKillTime - gHUD.m_flTime ) ) ); } } else if( pl_info->thisplayer ) // if it is their name, draw it a different color { // overlay the background in blue, then draw the score text over it FillRGBA( xpos - 5, ypos, FAR_RIGHT, ROW_GAP, 0, 0, 255, 70 ); } if( g_PlayerExtraInfo[best_player].iHasFlag ) { HSPRITE hFlag = 0; if( g_PlayerExtraInfo[best_player].teamnumber == 1 ) { hFlag = gHUD.m_FlagStat.m_hBlueFlag; } else if( g_PlayerExtraInfo[best_player].teamnumber == 2 ) { hFlag = gHUD.m_FlagStat.m_hRedFlag; } SPR_Set( hFlag, 255, 255, 255 ); SPR_DrawHoles( 1, xpos, ypos + 5, NULL ); } // draw their name (left to right) DrawUtfString( xpos + nameoffset, ypos, NAME_RANGE_MAX + xpos_rel, pl_info->name, r, g, b ); // draw kills (right to left) xpos = KILLS_RANGE_MAX + xpos_rel; gHUD.DrawHudNumberString( xpos, ypos, KILLS_RANGE_MIN + xpos_rel, g_PlayerExtraInfo[best_player].frags, r, g, b ); // draw divider xpos = DIVIDER_POS + xpos_rel; DrawUtfString( xpos, ypos, xpos + 20, "/", r, g, b ); // draw deaths xpos = DEATHS_RANGE_MAX + xpos_rel; gHUD.DrawHudNumberString( xpos, ypos, DEATHS_RANGE_MIN + xpos_rel, g_PlayerExtraInfo[best_player].deaths, r, g, b ); // draw ping & packetloss static char buf[64]; sprintf( buf, "%d", g_PlayerInfoList[best_player].ping ); xpos = ( ( PING_RANGE_MAX - PING_RANGE_MIN ) / 2 ) + PING_RANGE_MIN + xpos_rel + 25; gHUD.DrawHudStringReverse( xpos, ypos, xpos - 50, buf, r, g, b ); // Packetloss removed on Kelly 'shipping nazi' Bailey's orders if( can_show_packetloss ) { if( g_PlayerInfoList[best_player].packetloss >= 63 ) { UnpackRGB( r, g, b, RGB_REDISH ); sprintf( buf, " !!!!" ); } else { sprintf( buf, " %d", g_PlayerInfoList[best_player].packetloss ); } xpos = ( ( PL_RANGE_MAX - PL_RANGE_MIN ) / 2 ) + PL_RANGE_MIN + xpos_rel + 25; DrawUtfString( xpos, ypos, xpos+50, buf, r, g, b ); } pl_info->name = NULL; // set the name to be NULL, so this client won't get drawn again list_slot++; } return list_slot; } void CHudScoreboard::GetAllPlayersInfo( void ) { for( int i = 1; i < MAX_PLAYERS; i++ ) { GetPlayerInfo( i, &g_PlayerInfoList[i] ); if( g_PlayerInfoList[i].thisplayer ) m_iPlayerNum = i; // !!!HACK: this should be initialized elsewhere... maybe gotten from the engine } } int CHudScoreboard::MsgFunc_ScoreInfo( const char *pszName, int iSize, void *pbuf ) { m_iFlags |= HUD_ACTIVE; BEGIN_READ( pbuf, iSize ); short cl = READ_BYTE(); short frags = READ_SHORT(); short deaths = READ_SHORT(); // short playerclass = READ_SHORT(); short teamnumber = READ_SHORT(); if( cl > 0 && cl <= MAX_PLAYERS ) { g_PlayerExtraInfo[cl].frags = frags; g_PlayerExtraInfo[cl].deaths = deaths; // g_PlayerExtraInfo[cl].playerclass = playerclass; g_PlayerExtraInfo[cl].teamnumber = teamnumber; #if USE_VGUI gViewPort->UpdateOnPlayerInfo(); #endif } return 1; } // Message handler for TeamInfo message // accepts two values: // byte: client number // string: client team name int CHudScoreboard::MsgFunc_TeamInfo( const char *pszName, int iSize, void *pbuf ) { int i, j; BEGIN_READ( pbuf, iSize ); short cl = READ_BYTE(); if( cl > 0 && cl <= MAX_PLAYERS ) { // set the players team strncpy( g_PlayerExtraInfo[cl].teamname, READ_STRING(), MAX_TEAM_NAME ); } // rebuild the list of teams // clear out player counts from teams for( i = 1; i <= m_iNumTeams; i++ ) { g_TeamInfo[i].players = 0; } // rebuild the team list GetAllPlayersInfo(); m_iNumTeams = 0; for( i = 1; i < MAX_PLAYERS; i++ ) { //if ( g_PlayerInfoList[i].name == NULL ) // continue; if( g_PlayerExtraInfo[i].teamname[0] == 0 ) continue; // skip over players who are not in a team // is this player in an existing team? for( j = 1; j <= m_iNumTeams; j++ ) { if( g_TeamInfo[j].name[0] == '\0' ) break; if( !stricmp( g_PlayerExtraInfo[i].teamname, g_TeamInfo[j].name ) ) break; } if( j > m_iNumTeams ) { // they aren't in a listed team, so make a new one // search through for an empty team slot for( j = 1; j <= m_iNumTeams; j++ ) { if( g_TeamInfo[j].name[0] == '\0' ) break; } m_iNumTeams = Q_max( j, m_iNumTeams ); strncpy( g_TeamInfo[j].name, g_PlayerExtraInfo[i].teamname, MAX_TEAM_NAME ); g_TeamInfo[j].players = 0; } g_TeamInfo[j].players++; } // clear out any empty teams for( i = 1; i <= m_iNumTeams; i++ ) { if( g_TeamInfo[i].players < 1 ) memset( &g_TeamInfo[i], 0, sizeof(team_info_t) ); } return 1; } // Message handler for TeamScore message // accepts three values: // string: team name // short: teams kills // short: teams deaths // if this message is never received, then scores will simply be the combined totals of the players. int CHudScoreboard::MsgFunc_TeamScore( const char *pszName, int iSize, void *pbuf ) { BEGIN_READ( pbuf, iSize ); char *TeamName = READ_STRING(); int i; // find the team matching the name for( i = 1; i <= m_iNumTeams; i++ ) { if( !stricmp( TeamName, g_TeamInfo[i].name ) ) break; } if( i > m_iNumTeams ) return 1; // use this new score data instead of combined player scores g_TeamInfo[i].scores_overriden = TRUE; g_TeamInfo[i].frags = READ_SHORT(); g_TeamInfo[i].deaths = READ_SHORT(); return 1; } void CHudScoreboard::DeathMsg( int killer, int victim ) { // if we were the one killed, or the world killed us, set the scoreboard to indicate suicide if( victim == m_iPlayerNum || killer == 0 ) { m_iLastKilledBy = killer ? killer : m_iPlayerNum; m_fLastKillTime = gHUD.m_flTime + 10; // display who we were killed by for 10 seconds if( killer == m_iPlayerNum ) m_iLastKilledBy = m_iPlayerNum; } } void CHudScoreboard::UserCmd_ShowScores( void ) { m_iShowscoresHeld = TRUE; } void CHudScoreboard::UserCmd_HideScores( void ) { m_iShowscoresHeld = FALSE; }