mirror of
https://github.com/ioquake/ioq3.git
synced 2024-11-14 00:40:39 +00:00
082376ed9e
"/team score" draws an oversized scoreboard in Q3. In Team Arena it draws nothing. They probably intended to replace it with the new .menu UI. But since it didn't happen, go ahead and use the Q3 tournament scoreboard.
534 lines
15 KiB
C
534 lines
15 KiB
C
/*
|
|
===========================================================================
|
|
Copyright (C) 1999-2005 Id Software, Inc.
|
|
|
|
This file is part of Quake III Arena source code.
|
|
|
|
Quake III Arena source code is free software; you can redistribute it
|
|
and/or modify it under the terms of the GNU General Public License as
|
|
published by the Free Software Foundation; either version 2 of the License,
|
|
or (at your option) any later version.
|
|
|
|
Quake III Arena source code is distributed in the hope that it will be
|
|
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Quake III Arena source code; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
===========================================================================
|
|
*/
|
|
//
|
|
// cg_scoreboard -- draw the scoreboard on top of the game screen
|
|
#include "cg_local.h"
|
|
|
|
|
|
#define SCOREBOARD_X (0)
|
|
|
|
#define SB_HEADER 86
|
|
#define SB_TOP (SB_HEADER+32)
|
|
|
|
// Where the status bar starts, so we don't overwrite it
|
|
#define SB_STATUSBAR 420
|
|
|
|
#define SB_NORMAL_HEIGHT 40
|
|
#define SB_INTER_HEIGHT 16 // interleaved height
|
|
|
|
#define SB_MAXCLIENTS_NORMAL ((SB_STATUSBAR - SB_TOP) / SB_NORMAL_HEIGHT)
|
|
#define SB_MAXCLIENTS_INTER ((SB_STATUSBAR - SB_TOP) / SB_INTER_HEIGHT - 1)
|
|
|
|
// Used when interleaved
|
|
|
|
|
|
|
|
#define SB_LEFT_BOTICON_X (SCOREBOARD_X+0)
|
|
#define SB_LEFT_HEAD_X (SCOREBOARD_X+32)
|
|
#define SB_RIGHT_BOTICON_X (SCOREBOARD_X+64)
|
|
#define SB_RIGHT_HEAD_X (SCOREBOARD_X+96)
|
|
// Normal
|
|
#define SB_BOTICON_X (SCOREBOARD_X+32)
|
|
#define SB_HEAD_X (SCOREBOARD_X+64)
|
|
|
|
#define SB_SCORELINE_X 112
|
|
|
|
#define SB_RATING_WIDTH (6 * BIGCHAR_WIDTH) // width 6
|
|
#define SB_SCORE_X (SB_SCORELINE_X + BIGCHAR_WIDTH) // width 6
|
|
#define SB_RATING_X (SB_SCORELINE_X + 6 * BIGCHAR_WIDTH) // width 6
|
|
#define SB_PING_X (SB_SCORELINE_X + 12 * BIGCHAR_WIDTH + 8) // width 5
|
|
#define SB_TIME_X (SB_SCORELINE_X + 17 * BIGCHAR_WIDTH + 8) // width 5
|
|
#define SB_NAME_X (SB_SCORELINE_X + 22 * BIGCHAR_WIDTH) // width 15
|
|
|
|
// The new and improved score board
|
|
//
|
|
// In cases where the number of clients is high, the score board heads are interleaved
|
|
// here's the layout
|
|
|
|
//
|
|
// 0 32 80 112 144 240 320 400 <-- pixel position
|
|
// bot head bot head score ping time name
|
|
//
|
|
// wins/losses are drawn on bot icon now
|
|
|
|
static qboolean localClient; // true if local client has been displayed
|
|
|
|
|
|
/*
|
|
=================
|
|
CG_DrawScoreboard
|
|
=================
|
|
*/
|
|
static void CG_DrawClientScore( int y, score_t *score, float *color, float fade, qboolean largeFormat ) {
|
|
char string[1024];
|
|
vec3_t headAngles;
|
|
clientInfo_t *ci;
|
|
int iconx, headx;
|
|
|
|
if ( score->client < 0 || score->client >= cgs.maxclients ) {
|
|
Com_Printf( "Bad score->client: %i\n", score->client );
|
|
return;
|
|
}
|
|
|
|
ci = &cgs.clientinfo[score->client];
|
|
|
|
iconx = SB_BOTICON_X + (SB_RATING_WIDTH / 2);
|
|
headx = SB_HEAD_X + (SB_RATING_WIDTH / 2);
|
|
|
|
// draw the handicap or bot skill marker (unless player has flag)
|
|
if ( ci->powerups & ( 1 << PW_NEUTRALFLAG ) ) {
|
|
if( largeFormat ) {
|
|
CG_DrawFlagModel( iconx, y - ( 32 - BIGCHAR_HEIGHT ) / 2, 32, 32, TEAM_FREE, qfalse );
|
|
}
|
|
else {
|
|
CG_DrawFlagModel( iconx, y, 16, 16, TEAM_FREE, qfalse );
|
|
}
|
|
} else if ( ci->powerups & ( 1 << PW_REDFLAG ) ) {
|
|
if( largeFormat ) {
|
|
CG_DrawFlagModel( iconx, y - ( 32 - BIGCHAR_HEIGHT ) / 2, 32, 32, TEAM_RED, qfalse );
|
|
}
|
|
else {
|
|
CG_DrawFlagModel( iconx, y, 16, 16, TEAM_RED, qfalse );
|
|
}
|
|
} else if ( ci->powerups & ( 1 << PW_BLUEFLAG ) ) {
|
|
if( largeFormat ) {
|
|
CG_DrawFlagModel( iconx, y - ( 32 - BIGCHAR_HEIGHT ) / 2, 32, 32, TEAM_BLUE, qfalse );
|
|
}
|
|
else {
|
|
CG_DrawFlagModel( iconx, y, 16, 16, TEAM_BLUE, qfalse );
|
|
}
|
|
} else {
|
|
if ( ci->botSkill > 0 && ci->botSkill <= 5 ) {
|
|
if ( cg_drawIcons.integer ) {
|
|
if( largeFormat ) {
|
|
CG_DrawPic( iconx, y - ( 32 - BIGCHAR_HEIGHT ) / 2, 32, 32, cgs.media.botSkillShaders[ ci->botSkill - 1 ] );
|
|
}
|
|
else {
|
|
CG_DrawPic( iconx, y, 16, 16, cgs.media.botSkillShaders[ ci->botSkill - 1 ] );
|
|
}
|
|
}
|
|
} else if ( ci->handicap < 100 ) {
|
|
Com_sprintf( string, sizeof( string ), "%i", ci->handicap );
|
|
if ( cgs.gametype == GT_TOURNAMENT )
|
|
CG_DrawSmallStringColor( iconx, y - SMALLCHAR_HEIGHT/2, string, color );
|
|
else
|
|
CG_DrawSmallStringColor( iconx, y, string, color );
|
|
}
|
|
|
|
// draw the wins / losses
|
|
if ( cgs.gametype == GT_TOURNAMENT ) {
|
|
Com_sprintf( string, sizeof( string ), "%i/%i", ci->wins, ci->losses );
|
|
if( ci->handicap < 100 && !ci->botSkill ) {
|
|
CG_DrawSmallStringColor( iconx, y + SMALLCHAR_HEIGHT/2, string, color );
|
|
}
|
|
else {
|
|
CG_DrawSmallStringColor( iconx, y, string, color );
|
|
}
|
|
}
|
|
}
|
|
|
|
// draw the face
|
|
VectorClear( headAngles );
|
|
headAngles[YAW] = 180;
|
|
if( largeFormat ) {
|
|
CG_DrawHead( headx, y - ( ICON_SIZE - BIGCHAR_HEIGHT ) / 2, ICON_SIZE, ICON_SIZE,
|
|
score->client, headAngles );
|
|
}
|
|
else {
|
|
CG_DrawHead( headx, y, 16, 16, score->client, headAngles );
|
|
}
|
|
|
|
#ifdef MISSIONPACK
|
|
// draw the team task
|
|
if ( ci->teamTask != TEAMTASK_NONE ) {
|
|
if ( ci->teamTask == TEAMTASK_OFFENSE ) {
|
|
CG_DrawPic( headx + 48, y, 16, 16, cgs.media.assaultShader );
|
|
}
|
|
else if ( ci->teamTask == TEAMTASK_DEFENSE ) {
|
|
CG_DrawPic( headx + 48, y, 16, 16, cgs.media.defendShader );
|
|
}
|
|
}
|
|
#endif
|
|
// draw the score line
|
|
if ( score->ping == -1 ) {
|
|
Com_sprintf(string, sizeof(string),
|
|
" connecting %s", ci->name);
|
|
} else if ( ci->team == TEAM_SPECTATOR ) {
|
|
Com_sprintf(string, sizeof(string),
|
|
" SPECT %3i %4i %s", score->ping, score->time, ci->name);
|
|
} else {
|
|
Com_sprintf(string, sizeof(string),
|
|
"%5i %4i %4i %s", score->score, score->ping, score->time, ci->name);
|
|
}
|
|
|
|
// highlight your position
|
|
if ( score->client == cg.snap->ps.clientNum ) {
|
|
float hcolor[4];
|
|
int rank;
|
|
|
|
localClient = qtrue;
|
|
|
|
if ( cg.snap->ps.persistant[PERS_TEAM] == TEAM_SPECTATOR
|
|
|| cgs.gametype >= GT_TEAM ) {
|
|
rank = -1;
|
|
} else {
|
|
rank = cg.snap->ps.persistant[PERS_RANK] & ~RANK_TIED_FLAG;
|
|
}
|
|
if ( rank == 0 ) {
|
|
hcolor[0] = 0;
|
|
hcolor[1] = 0;
|
|
hcolor[2] = 0.7f;
|
|
} else if ( rank == 1 ) {
|
|
hcolor[0] = 0.7f;
|
|
hcolor[1] = 0;
|
|
hcolor[2] = 0;
|
|
} else if ( rank == 2 ) {
|
|
hcolor[0] = 0.7f;
|
|
hcolor[1] = 0.7f;
|
|
hcolor[2] = 0;
|
|
} else {
|
|
hcolor[0] = 0.7f;
|
|
hcolor[1] = 0.7f;
|
|
hcolor[2] = 0.7f;
|
|
}
|
|
|
|
hcolor[3] = fade * 0.7;
|
|
CG_FillRect( SB_SCORELINE_X + BIGCHAR_WIDTH + (SB_RATING_WIDTH / 2), y,
|
|
640 - SB_SCORELINE_X - BIGCHAR_WIDTH, BIGCHAR_HEIGHT+1, hcolor );
|
|
}
|
|
|
|
CG_DrawBigString( SB_SCORELINE_X + (SB_RATING_WIDTH / 2), y, string, fade );
|
|
|
|
// add the "ready" marker for intermission exiting
|
|
if ( cg.snap->ps.stats[ STAT_CLIENTS_READY ] & ( 1 << score->client ) ) {
|
|
CG_DrawBigStringColor( iconx, y, "READY", color );
|
|
}
|
|
}
|
|
|
|
/*
|
|
=================
|
|
CG_TeamScoreboard
|
|
=================
|
|
*/
|
|
static int CG_TeamScoreboard( int y, team_t team, float fade, int maxClients, int lineHeight ) {
|
|
int i;
|
|
score_t *score;
|
|
float color[4];
|
|
int count;
|
|
clientInfo_t *ci;
|
|
|
|
color[0] = color[1] = color[2] = 1.0;
|
|
color[3] = fade;
|
|
|
|
count = 0;
|
|
for ( i = 0 ; i < cg.numScores && count < maxClients ; i++ ) {
|
|
score = &cg.scores[i];
|
|
ci = &cgs.clientinfo[ score->client ];
|
|
|
|
if ( team != ci->team ) {
|
|
continue;
|
|
}
|
|
|
|
CG_DrawClientScore( y + lineHeight * count, score, color, fade, lineHeight == SB_NORMAL_HEIGHT );
|
|
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/*
|
|
=================
|
|
CG_DrawScoreboard
|
|
|
|
Draw the normal in-game scoreboard
|
|
=================
|
|
*/
|
|
qboolean CG_DrawOldScoreboard( void ) {
|
|
int x, y, w, i, n1, n2;
|
|
float fade;
|
|
float *fadeColor;
|
|
char *s;
|
|
int maxClients;
|
|
int lineHeight;
|
|
int topBorderSize, bottomBorderSize;
|
|
|
|
// don't draw amuthing if the menu or console is up
|
|
if ( cg_paused.integer ) {
|
|
cg.deferredPlayerLoading = 0;
|
|
return qfalse;
|
|
}
|
|
|
|
if ( cgs.gametype == GT_SINGLE_PLAYER && cg.predictedPlayerState.pm_type == PM_INTERMISSION ) {
|
|
cg.deferredPlayerLoading = 0;
|
|
return qfalse;
|
|
}
|
|
|
|
// don't draw scoreboard during death while warmup up
|
|
if ( cg.warmup && !cg.showScores ) {
|
|
return qfalse;
|
|
}
|
|
|
|
if ( cg.showScores || cg.predictedPlayerState.pm_type == PM_DEAD ||
|
|
cg.predictedPlayerState.pm_type == PM_INTERMISSION ) {
|
|
fade = 1.0;
|
|
fadeColor = colorWhite;
|
|
} else {
|
|
fadeColor = CG_FadeColor( cg.scoreFadeTime, FADE_TIME );
|
|
|
|
if ( !fadeColor ) {
|
|
// next time scoreboard comes up, don't print killer
|
|
cg.deferredPlayerLoading = 0;
|
|
cg.killerName[0] = 0;
|
|
return qfalse;
|
|
}
|
|
fade = *fadeColor;
|
|
}
|
|
|
|
|
|
// fragged by ... line
|
|
if ( cg.killerName[0] ) {
|
|
s = va("Fragged by %s", cg.killerName );
|
|
w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH;
|
|
x = ( SCREEN_WIDTH - w ) / 2;
|
|
y = 40;
|
|
CG_DrawBigString( x, y, s, fade );
|
|
}
|
|
|
|
// current rank
|
|
if ( cgs.gametype < GT_TEAM) {
|
|
if (cg.snap->ps.persistant[PERS_TEAM] != TEAM_SPECTATOR ) {
|
|
s = va("%s place with %i",
|
|
CG_PlaceString( cg.snap->ps.persistant[PERS_RANK] + 1 ),
|
|
cg.snap->ps.persistant[PERS_SCORE] );
|
|
w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH;
|
|
x = ( SCREEN_WIDTH - w ) / 2;
|
|
y = 60;
|
|
CG_DrawBigString( x, y, s, fade );
|
|
}
|
|
} else {
|
|
if ( cg.teamScores[0] == cg.teamScores[1] ) {
|
|
s = va("Teams are tied at %i", cg.teamScores[0] );
|
|
} else if ( cg.teamScores[0] >= cg.teamScores[1] ) {
|
|
s = va("Red leads %i to %i",cg.teamScores[0], cg.teamScores[1] );
|
|
} else {
|
|
s = va("Blue leads %i to %i",cg.teamScores[1], cg.teamScores[0] );
|
|
}
|
|
|
|
w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH;
|
|
x = ( SCREEN_WIDTH - w ) / 2;
|
|
y = 60;
|
|
CG_DrawBigString( x, y, s, fade );
|
|
}
|
|
|
|
// scoreboard
|
|
y = SB_HEADER;
|
|
|
|
CG_DrawPic( SB_SCORE_X + (SB_RATING_WIDTH / 2), y, 64, 32, cgs.media.scoreboardScore );
|
|
CG_DrawPic( SB_PING_X - (SB_RATING_WIDTH / 2), y, 64, 32, cgs.media.scoreboardPing );
|
|
CG_DrawPic( SB_TIME_X - (SB_RATING_WIDTH / 2), y, 64, 32, cgs.media.scoreboardTime );
|
|
CG_DrawPic( SB_NAME_X - (SB_RATING_WIDTH / 2), y, 64, 32, cgs.media.scoreboardName );
|
|
|
|
y = SB_TOP;
|
|
|
|
// If there are more than SB_MAXCLIENTS_NORMAL, use the interleaved scores
|
|
if ( cg.numScores > SB_MAXCLIENTS_NORMAL ) {
|
|
maxClients = SB_MAXCLIENTS_INTER;
|
|
lineHeight = SB_INTER_HEIGHT;
|
|
topBorderSize = 8;
|
|
bottomBorderSize = 16;
|
|
} else {
|
|
maxClients = SB_MAXCLIENTS_NORMAL;
|
|
lineHeight = SB_NORMAL_HEIGHT;
|
|
topBorderSize = 16;
|
|
bottomBorderSize = 16;
|
|
}
|
|
|
|
localClient = qfalse;
|
|
|
|
if ( cgs.gametype >= GT_TEAM ) {
|
|
//
|
|
// teamplay scoreboard
|
|
//
|
|
y += lineHeight/2;
|
|
|
|
if ( cg.teamScores[0] >= cg.teamScores[1] ) {
|
|
n1 = CG_TeamScoreboard( y, TEAM_RED, fade, maxClients, lineHeight );
|
|
CG_DrawTeamBackground( 0, y - topBorderSize, 640, n1 * lineHeight + bottomBorderSize, 0.33f, TEAM_RED );
|
|
y += (n1 * lineHeight) + BIGCHAR_HEIGHT;
|
|
maxClients -= n1;
|
|
n2 = CG_TeamScoreboard( y, TEAM_BLUE, fade, maxClients, lineHeight );
|
|
CG_DrawTeamBackground( 0, y - topBorderSize, 640, n2 * lineHeight + bottomBorderSize, 0.33f, TEAM_BLUE );
|
|
y += (n2 * lineHeight) + BIGCHAR_HEIGHT;
|
|
maxClients -= n2;
|
|
} else {
|
|
n1 = CG_TeamScoreboard( y, TEAM_BLUE, fade, maxClients, lineHeight );
|
|
CG_DrawTeamBackground( 0, y - topBorderSize, 640, n1 * lineHeight + bottomBorderSize, 0.33f, TEAM_BLUE );
|
|
y += (n1 * lineHeight) + BIGCHAR_HEIGHT;
|
|
maxClients -= n1;
|
|
n2 = CG_TeamScoreboard( y, TEAM_RED, fade, maxClients, lineHeight );
|
|
CG_DrawTeamBackground( 0, y - topBorderSize, 640, n2 * lineHeight + bottomBorderSize, 0.33f, TEAM_RED );
|
|
y += (n2 * lineHeight) + BIGCHAR_HEIGHT;
|
|
maxClients -= n2;
|
|
}
|
|
n1 = CG_TeamScoreboard( y, TEAM_SPECTATOR, fade, maxClients, lineHeight );
|
|
y += (n1 * lineHeight) + BIGCHAR_HEIGHT;
|
|
|
|
} else {
|
|
//
|
|
// free for all scoreboard
|
|
//
|
|
n1 = CG_TeamScoreboard( y, TEAM_FREE, fade, maxClients, lineHeight );
|
|
y += (n1 * lineHeight) + BIGCHAR_HEIGHT;
|
|
n2 = CG_TeamScoreboard( y, TEAM_SPECTATOR, fade, maxClients - n1, lineHeight );
|
|
y += (n2 * lineHeight) + BIGCHAR_HEIGHT;
|
|
}
|
|
|
|
if (!localClient) {
|
|
// draw local client at the bottom
|
|
for ( i = 0 ; i < cg.numScores ; i++ ) {
|
|
if ( cg.scores[i].client == cg.snap->ps.clientNum ) {
|
|
CG_DrawClientScore( y, &cg.scores[i], fadeColor, fade, lineHeight == SB_NORMAL_HEIGHT );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// load any models that have been deferred
|
|
if ( ++cg.deferredPlayerLoading > 10 ) {
|
|
CG_LoadDeferredPlayers();
|
|
}
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
//================================================================================
|
|
|
|
/*
|
|
================
|
|
CG_CenterGiantLine
|
|
================
|
|
*/
|
|
static void CG_CenterGiantLine( float y, const char *string ) {
|
|
float x;
|
|
vec4_t color;
|
|
|
|
color[0] = 1;
|
|
color[1] = 1;
|
|
color[2] = 1;
|
|
color[3] = 1;
|
|
|
|
x = 0.5 * ( 640 - GIANT_WIDTH * CG_DrawStrlen( string ) );
|
|
|
|
CG_DrawStringExt( x, y, string, color, qtrue, qtrue, GIANT_WIDTH, GIANT_HEIGHT, 0 );
|
|
}
|
|
|
|
/*
|
|
=================
|
|
CG_DrawTourneyScoreboard
|
|
|
|
Draw the oversize scoreboard for tournements
|
|
=================
|
|
*/
|
|
void CG_DrawTourneyScoreboard( void ) {
|
|
const char *s;
|
|
vec4_t color;
|
|
int min, tens, ones;
|
|
clientInfo_t *ci;
|
|
int y;
|
|
int i;
|
|
|
|
// request more scores regularly
|
|
if ( cg.scoresRequestTime + 2000 < cg.time ) {
|
|
cg.scoresRequestTime = cg.time;
|
|
trap_SendClientCommand( "score" );
|
|
}
|
|
|
|
// draw the dialog background
|
|
color[0] = color[1] = color[2] = 0;
|
|
color[3] = 1;
|
|
CG_FillRect( 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, color );
|
|
|
|
color[0] = 1;
|
|
color[1] = 1;
|
|
color[2] = 1;
|
|
color[3] = 1;
|
|
|
|
// print the mesage of the day
|
|
s = CG_ConfigString( CS_MOTD );
|
|
if ( !s[0] ) {
|
|
s = "Scoreboard";
|
|
}
|
|
|
|
// print optional title
|
|
CG_CenterGiantLine( 8, s );
|
|
|
|
// print server time
|
|
ones = cg.time / 1000;
|
|
min = ones / 60;
|
|
ones %= 60;
|
|
tens = ones / 10;
|
|
ones %= 10;
|
|
s = va("%i:%i%i", min, tens, ones );
|
|
|
|
CG_CenterGiantLine( 64, s );
|
|
|
|
|
|
// print the two scores
|
|
|
|
y = 160;
|
|
if ( cgs.gametype >= GT_TEAM ) {
|
|
//
|
|
// teamplay scoreboard
|
|
//
|
|
CG_DrawStringExt( 8, y, "Red Team", color, qtrue, qtrue, GIANT_WIDTH, GIANT_HEIGHT, 0 );
|
|
s = va("%i", cg.teamScores[0] );
|
|
CG_DrawStringExt( 632 - GIANT_WIDTH * strlen(s), y, s, color, qtrue, qtrue, GIANT_WIDTH, GIANT_HEIGHT, 0 );
|
|
|
|
y += 64;
|
|
|
|
CG_DrawStringExt( 8, y, "Blue Team", color, qtrue, qtrue, GIANT_WIDTH, GIANT_HEIGHT, 0 );
|
|
s = va("%i", cg.teamScores[1] );
|
|
CG_DrawStringExt( 632 - GIANT_WIDTH * strlen(s), y, s, color, qtrue, qtrue, GIANT_WIDTH, GIANT_HEIGHT, 0 );
|
|
} else {
|
|
//
|
|
// free for all scoreboard
|
|
//
|
|
for ( i = 0 ; i < MAX_CLIENTS ; i++ ) {
|
|
ci = &cgs.clientinfo[i];
|
|
if ( !ci->infoValid ) {
|
|
continue;
|
|
}
|
|
if ( ci->team != TEAM_FREE ) {
|
|
continue;
|
|
}
|
|
|
|
CG_DrawStringExt( 8, y, ci->name, color, qtrue, qtrue, GIANT_WIDTH, GIANT_HEIGHT, 0 );
|
|
s = va("%i", ci->score );
|
|
CG_DrawStringExt( 632 - GIANT_WIDTH * strlen(s), y, s, color, qtrue, qtrue, GIANT_WIDTH, GIANT_HEIGHT, 0 );
|
|
y += 64;
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|