sv_rankings.c (35537B)
1 /* 2 =========================================================================== 3 Copyright (C) 1999-2005 Id Software, Inc. 4 5 This file is part of Quake III Arena source code. 6 7 Quake III Arena source code is free software; you can redistribute it 8 and/or modify it under the terms of the GNU General Public License as 9 published by the Free Software Foundation; either version 2 of the License, 10 or (at your option) any later version. 11 12 Quake III Arena source code is distributed in the hope that it will be 13 useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 GNU General Public License for more details. 16 17 You should have received a copy of the GNU General Public License 18 along with Foobar; if not, write to the Free Software 19 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 20 =========================================================================== 21 */ 22 // sv_rankings.c -- global rankings interface 23 24 #include "server.h" 25 #include "..\rankings\1.0\gr\grapi.h" 26 #include "..\rankings\1.0\gr\grlog.h" 27 28 typedef struct 29 { 30 GR_CONTEXT context; 31 uint64_t game_id; 32 uint64_t match; 33 uint64_t player_id; 34 GR_PLAYER_TOKEN token; 35 grank_status_t grank_status; 36 grank_status_t final_status; // status to set after cleanup 37 uint32_t grank; // global rank 38 char name[32]; 39 } ranked_player_t; 40 41 static int s_rankings_contexts = 0; 42 static qboolean s_rankings_active = qfalse; 43 static GR_CONTEXT s_server_context = 0; 44 static uint64_t s_server_match = 0; 45 static char* s_rankings_game_key = NULL; 46 static uint64_t s_rankings_game_id = 0; 47 static ranked_player_t* s_ranked_players = NULL; 48 static qboolean s_server_quitting = qfalse; 49 static const char s_ascii_encoding[] = 50 "0123456789abcdef" 51 "ghijklmnopqrstuv" 52 "wxyzABCDEFGHIJKL" 53 "MNOPQRSTUVWXYZ[]"; 54 55 // private functions 56 static void SV_RankNewGameCBF( GR_NEWGAME* gr_newgame, void* cbf_arg ); 57 static void SV_RankUserCBF( GR_LOGIN* gr_login, void* cbf_arg ); 58 static void SV_RankJoinGameCBF( GR_JOINGAME* gr_joingame, void* cbf_arg ); 59 static void SV_RankSendReportsCBF( GR_STATUS* gr_status, void* cbf_arg ); 60 static void SV_RankCleanupCBF( GR_STATUS* gr_status, void* cbf_arg ); 61 static void SV_RankCloseContext( ranked_player_t* ranked_player ); 62 static int SV_RankAsciiEncode( char* dest, const unsigned char* src, 63 int src_len ); 64 static int SV_RankAsciiDecode( unsigned char* dest, const char* src, 65 int src_len ); 66 static void SV_RankEncodeGameID( uint64_t game_id, char* result, 67 int len ); 68 static uint64_t SV_RankDecodePlayerID( const char* string ); 69 static void SV_RankDecodePlayerKey( const char* string, GR_PLAYER_TOKEN key ); 70 static char* SV_RankStatusString( GR_STATUS status ); 71 static void SV_RankError( const char* fmt, ... ); 72 static char SV_RankGameKey[64]; 73 74 /* 75 ================ 76 SV_RankBegin 77 ================ 78 */ 79 void SV_RankBegin( char *gamekey ) 80 { 81 GR_INIT init; 82 GR_STATUS status; 83 84 assert( s_rankings_contexts == 0 ); 85 assert( !s_rankings_active ); 86 assert( s_ranked_players == NULL ); 87 88 if( sv_enableRankings->integer == 0 || Cvar_VariableValue( "g_gametype" ) == GT_SINGLE_PLAYER ) 89 { 90 s_rankings_active = qfalse; 91 if( sv_rankingsActive->integer == 1 ) 92 { 93 Cvar_Set( "sv_rankingsActive", "0" ); 94 } 95 return; 96 } 97 98 // only allow official game key on pure servers 99 if( strcmp(gamekey, GR_GAMEKEY) == 0 ) 100 { 101 /* 102 if( Cvar_VariableValue("sv_pure") != 1 ) 103 { 104 Cvar_Set( "sv_enableRankings", "0" ); 105 return; 106 } 107 */ 108 109 // substitute game-specific game key 110 switch( (int)Cvar_VariableValue("g_gametype") ) 111 { 112 case GT_FFA: 113 gamekey = "Q3 Free For All"; 114 break; 115 case GT_TOURNAMENT: 116 gamekey = "Q3 Tournament"; 117 break; 118 case GT_TEAM: 119 gamekey = "Q3 Team Deathmatch"; 120 break; 121 case GT_CTF: 122 gamekey = "Q3 Capture the Flag"; 123 break; 124 case GT_1FCTF: 125 gamekey = "Q3 One Flag CTF"; 126 break; 127 case GT_OBELISK: 128 gamekey = "Q3 Overload"; 129 break; 130 case GT_HARVESTER: 131 gamekey = "Q3 Harvester"; 132 break; 133 default: 134 break; 135 } 136 } 137 s_rankings_game_key = gamekey; 138 139 // initialize rankings 140 GRankLogLevel( GRLOG_OFF ); 141 memset(SV_RankGameKey,0,sizeof(SV_RankGameKey)); 142 strncpy(SV_RankGameKey,gamekey,sizeof(SV_RankGameKey)-1); 143 init = GRankInit( 1, SV_RankGameKey, GR_OPT_POLL, GR_OPT_END ); 144 s_server_context = init.context; 145 s_rankings_contexts++; 146 Com_DPrintf( "SV_RankBegin(); GR_GAMEKEY is %s\n", gamekey ); 147 Com_DPrintf( "SV_RankBegin(); s_rankings_contexts=%d\n",s_rankings_contexts ); 148 Com_DPrintf( "SV_RankBegin(); s_server_context=%d\n",init.context ); 149 150 // new game 151 if(!strlen(Cvar_VariableString( "sv_leagueName" ))) 152 { 153 status = GRankNewGameAsync 154 ( 155 s_server_context, 156 SV_RankNewGameCBF, 157 NULL, 158 GR_OPT_LEAGUENAME, 159 (void*)(Cvar_VariableString( "sv_leagueName" )), 160 GR_OPT_END 161 ); 162 } 163 else 164 { 165 status = GRankNewGameAsync 166 ( 167 s_server_context, 168 SV_RankNewGameCBF, 169 NULL, 170 GR_OPT_END 171 ); 172 } 173 174 if( status != GR_STATUS_PENDING ) 175 { 176 SV_RankError( "SV_RankBegin: Expected GR_STATUS_PENDING, got %s", 177 SV_RankStatusString( status ) ); 178 return; 179 } 180 181 // logging 182 if( com_developer->value ) 183 { 184 GRankLogLevel( GRLOG_TRACE ); 185 } 186 187 // allocate rankings info for each player 188 s_ranked_players = Z_Malloc( sv_maxclients->value * 189 sizeof(ranked_player_t) ); 190 memset( (void*)s_ranked_players, 0 ,sv_maxclients->value 191 * sizeof(ranked_player_t)); 192 } 193 194 /* 195 ================ 196 SV_RankEnd 197 ================ 198 */ 199 void SV_RankEnd( void ) 200 { 201 GR_STATUS status; 202 int i; 203 204 Com_DPrintf( "SV_RankEnd();\n" ); 205 206 if( !s_rankings_active ) 207 { 208 // cleanup after error during game 209 if( s_ranked_players != NULL ) 210 { 211 for( i = 0; i < sv_maxclients->value; i++ ) 212 { 213 if( s_ranked_players[i].context != 0 ) 214 { 215 SV_RankCloseContext( &(s_ranked_players[i]) ); 216 } 217 } 218 } 219 if( s_server_context != 0 ) 220 { 221 SV_RankCloseContext( NULL ); 222 } 223 224 return; 225 } 226 227 for( i = 0; i < sv_maxclients->value; i++ ) 228 { 229 if( s_ranked_players[i].grank_status == QGR_STATUS_ACTIVE ) 230 { 231 SV_RankUserLogout( i ); 232 Com_DPrintf( "SV_RankEnd: SV_RankUserLogout %d\n",i ); 233 } 234 } 235 236 assert( s_server_context != 0 ); 237 238 // send match reports, proceed to SV_RankSendReportsCBF 239 status = GRankSendReportsAsync 240 ( 241 s_server_context, 242 0, 243 SV_RankSendReportsCBF, 244 NULL, 245 GR_OPT_END 246 ); 247 248 if( status != GR_STATUS_PENDING ) 249 { 250 SV_RankError( "SV_RankEnd: Expected GR_STATUS_PENDING, got %s", 251 SV_RankStatusString( status ) ); 252 } 253 254 s_rankings_active = qfalse; 255 Cvar_Set( "sv_rankingsActive", "0" ); 256 } 257 258 /* 259 ================ 260 SV_RankPoll 261 ================ 262 */ 263 void SV_RankPoll( void ) 264 { 265 GRankPoll(); 266 } 267 268 /* 269 ================ 270 SV_RankCheckInit 271 ================ 272 */ 273 qboolean SV_RankCheckInit( void ) 274 { 275 return (s_rankings_contexts > 0); 276 } 277 278 /* 279 ================ 280 SV_RankActive 281 ================ 282 */ 283 qboolean SV_RankActive( void ) 284 { 285 return s_rankings_active; 286 } 287 288 /* 289 ================= 290 SV_RankUserStatus 291 ================= 292 */ 293 grank_status_t SV_RankUserStatus( int index ) 294 { 295 if( !s_rankings_active ) 296 { 297 return GR_STATUS_ERROR; 298 } 299 300 assert( s_ranked_players != NULL ); 301 assert( index >= 0 ); 302 assert( index < sv_maxclients->value ); 303 304 return s_ranked_players[index].grank_status; 305 } 306 307 /* 308 ================ 309 SV_RankUserGRank 310 ================ 311 */ 312 int SV_RankUserGrank( int index ) 313 { 314 if( !s_rankings_active ) 315 { 316 return 0; 317 } 318 319 assert( s_ranked_players != NULL ); 320 assert( index >= 0 ); 321 assert( index < sv_maxclients->value ); 322 323 return s_ranked_players[index].grank; 324 } 325 326 /* 327 ================ 328 SV_RankUserReset 329 ================ 330 */ 331 void SV_RankUserReset( int index ) 332 { 333 if( !s_rankings_active ) 334 { 335 return; 336 } 337 338 assert( s_ranked_players != NULL ); 339 assert( index >= 0 ); 340 assert( index < sv_maxclients->value ); 341 342 switch( s_ranked_players[index].grank_status ) 343 { 344 case QGR_STATUS_SPECTATOR: 345 case QGR_STATUS_NO_USER: 346 case QGR_STATUS_BAD_PASSWORD: 347 case QGR_STATUS_USER_EXISTS: 348 case QGR_STATUS_NO_MEMBERSHIP: 349 case QGR_STATUS_TIMEOUT: 350 case QGR_STATUS_ERROR: 351 s_ranked_players[index].grank_status = QGR_STATUS_NEW; 352 break; 353 default: 354 break; 355 } 356 } 357 358 /* 359 ================ 360 SV_RankUserSpectate 361 ================ 362 */ 363 void SV_RankUserSpectate( int index ) 364 { 365 if( !s_rankings_active ) 366 { 367 return; 368 } 369 370 assert( s_ranked_players != NULL ); 371 assert( index >= 0 ); 372 assert( index < sv_maxclients->value ); 373 374 // GRANK_FIXME - check current status? 375 s_ranked_players[index].grank_status = QGR_STATUS_SPECTATOR; 376 } 377 378 /* 379 ================ 380 SV_RankUserCreate 381 ================ 382 */ 383 void SV_RankUserCreate( int index, char* username, char* password, 384 char* email ) 385 { 386 GR_INIT init; 387 GR_STATUS status; 388 389 assert( index >= 0 ); 390 assert( index < sv_maxclients->value ); 391 assert( username != NULL ); 392 assert( password != NULL ); 393 assert( email != NULL ); 394 assert( s_ranked_players ); 395 assert( s_ranked_players[index].grank_status != QGR_STATUS_ACTIVE ); 396 397 Com_DPrintf( "SV_RankUserCreate( %d, %s, \"****\", %s );\n", index, 398 username, email ); 399 400 if( !s_rankings_active ) 401 { 402 Com_DPrintf( "SV_RankUserCreate: Not ready to create\n" ); 403 s_ranked_players[index].grank_status = QGR_STATUS_ERROR; 404 return; 405 } 406 407 if( s_ranked_players[index].grank_status == QGR_STATUS_ACTIVE ) 408 { 409 Com_DPrintf( "SV_RankUserCreate: Got Create from active player\n" ); 410 return; 411 } 412 413 // get a separate context for the new user 414 init = GRankInit( 0, SV_RankGameKey, GR_OPT_POLL, GR_OPT_END ); 415 s_ranked_players[index].context = init.context; 416 s_rankings_contexts++; 417 Com_DPrintf( "SV_RankUserCreate(); s_rankings_contexts=%d\n",s_rankings_contexts ); 418 Com_DPrintf( "SV_RankUserCreate(); s_ranked_players[%d].context=%d\n",index,init.context ); 419 420 // attempt to create a new account, proceed to SV_RankUserCBF 421 status = GRankUserCreateAsync 422 ( 423 s_ranked_players[index].context, 424 username, 425 password, 426 email, 427 SV_RankUserCBF, 428 (void*)&s_ranked_players[index], 429 GR_OPT_END 430 ); 431 432 if( status == GR_STATUS_PENDING ) 433 { 434 s_ranked_players[index].grank_status = QGR_STATUS_PENDING; 435 s_ranked_players[index].final_status = QGR_STATUS_NEW; 436 } 437 else 438 { 439 SV_RankError( "SV_RankUserCreate: Expected GR_STATUS_PENDING, got %s", 440 SV_RankStatusString( status ) ); 441 } 442 } 443 444 /* 445 ================ 446 SV_RankUserLogin 447 ================ 448 */ 449 void SV_RankUserLogin( int index, char* username, char* password ) 450 { 451 GR_INIT init; 452 GR_STATUS status; 453 454 assert( index >= 0 ); 455 assert( index < sv_maxclients->value ); 456 assert( username != NULL ); 457 assert( password != NULL ); 458 assert( s_ranked_players ); 459 assert( s_ranked_players[index].grank_status != QGR_STATUS_ACTIVE ); 460 461 Com_DPrintf( "SV_RankUserLogin( %d, %s, \"****\" );\n", index, username ); 462 463 if( !s_rankings_active ) 464 { 465 Com_DPrintf( "SV_RankUserLogin: Not ready for login\n" ); 466 s_ranked_players[index].grank_status = QGR_STATUS_ERROR; 467 return; 468 } 469 470 if( s_ranked_players[index].grank_status == QGR_STATUS_ACTIVE ) 471 { 472 Com_DPrintf( "SV_RankUserLogin: Got Login from active player\n" ); 473 return; 474 } 475 476 // get a separate context for the new user 477 init = GRankInit( 0, SV_RankGameKey, GR_OPT_POLL, GR_OPT_END ); 478 s_ranked_players[index].context = init.context; 479 s_rankings_contexts++; 480 Com_DPrintf( "SV_RankUserLogin(); s_rankings_contexts=%d\n",s_rankings_contexts ); 481 Com_DPrintf( "SV_RankUserLogin(); s_ranked_players[%d].context=%d\n",index,init.context ); 482 483 // login user, proceed to SV_RankUserCBF 484 status = GRankUserLoginAsync 485 ( 486 s_ranked_players[index].context, 487 username, 488 password, 489 SV_RankUserCBF, 490 (void*)&s_ranked_players[index], 491 GR_OPT_END 492 ); 493 494 if( status == GR_STATUS_PENDING ) 495 { 496 s_ranked_players[index].grank_status = QGR_STATUS_PENDING; 497 s_ranked_players[index].final_status = QGR_STATUS_NEW; 498 } 499 else 500 { 501 SV_RankError( "SV_RankUserLogin: Expected GR_STATUS_PENDING, got %s", 502 SV_RankStatusString( status ) ); 503 } 504 } 505 506 /* 507 =================== 508 SV_RankUserValidate 509 =================== 510 */ 511 qboolean SV_RankUserValidate( int index, const char* player_id, const char* key, int token_len, int rank, char* name ) 512 { 513 GR_INIT init; 514 GR_STATUS status; 515 qboolean rVal; 516 ranked_player_t* ranked_player; 517 int i; 518 519 assert( s_ranked_players ); 520 assert( s_ranked_players[index].grank_status != QGR_STATUS_ACTIVE ); 521 522 rVal = qfalse; 523 524 if( !s_rankings_active ) 525 { 526 Com_DPrintf( "SV_RankUserValidate: Not ready to validate\n" ); 527 s_ranked_players[index].grank_status = QGR_STATUS_ERROR; 528 return rVal; 529 } 530 531 ranked_player = &(s_ranked_players[index]); 532 533 if ( (player_id != NULL) && (key != NULL)) 534 { 535 // the real player_id and key is set when SV_RankJoinGameCBF 536 // is called we do this so that SV_RankUserValidate 537 // can be shared by both server side login and client side login 538 539 // for client side logined in players 540 // server is creating GR_OPT_PLAYERCONTEXT 541 init = GRankInit( 0, SV_RankGameKey, GR_OPT_POLL, GR_OPT_END ); 542 ranked_player->context = init.context; 543 s_rankings_contexts++; 544 Com_DPrintf( "SV_RankUserValidate(); s_rankings_contexts=%d\n",s_rankings_contexts ); 545 Com_DPrintf( "SV_RankUserValidate(); s_ranked_players[%d].context=%d\n",index,init.context ); 546 547 // uudecode player id and player token 548 ranked_player->player_id = SV_RankDecodePlayerID(player_id); 549 Com_DPrintf( "SV_RankUserValidate(); ranked_player->player_id =%u\n", (uint32_t)ranked_player->player_id ); 550 SV_RankDecodePlayerKey(key, ranked_player->token); 551 552 // save name and check for duplicates 553 Q_strncpyz( ranked_player->name, name, sizeof(ranked_player->name) ); 554 for( i = 0; i < sv_maxclients->value; i++ ) 555 { 556 if( (i != index) && (s_ranked_players[i].grank_status == QGR_STATUS_ACTIVE) && 557 (strcmp( s_ranked_players[i].name, name ) == 0) ) 558 { 559 Com_DPrintf( "SV_RankUserValidate: Duplicate login\n" ); 560 ranked_player->grank_status = QGR_STATUS_NO_USER; 561 ranked_player->final_status = QGR_STATUS_NEW; 562 ranked_player->grank = 0; 563 return qfalse; 564 } 565 } 566 567 // then validate 568 status = GRankPlayerValidate( 569 s_server_context, 570 ranked_player->player_id, 571 ranked_player->token, 572 token_len, 573 GR_OPT_PLAYERCONTEXT, 574 ranked_player->context, 575 GR_OPT_END); 576 } 577 else 578 { 579 // make server side login (bots) happy 580 status = GR_STATUS_OK; 581 } 582 583 if (status == GR_STATUS_OK) 584 { 585 ranked_player->grank_status = QGR_STATUS_ACTIVE; 586 ranked_player->final_status = QGR_STATUS_NEW; 587 ranked_player->grank = rank; 588 rVal = qtrue; 589 } 590 else if (status == GR_STATUS_INVALIDUSER) 591 { 592 ranked_player->grank_status = QGR_STATUS_INVALIDUSER; 593 ranked_player->final_status = QGR_STATUS_NEW; 594 ranked_player->grank = 0; 595 rVal = qfalse; 596 } 597 else 598 { 599 SV_RankError( "SV_RankUserValidate: Unexpected status %s", 600 SV_RankStatusString( status ) ); 601 s_ranked_players[index].grank_status = QGR_STATUS_ERROR; 602 ranked_player->grank = 0; 603 } 604 605 return rVal; 606 } 607 608 /* 609 ================ 610 SV_RankUserLogout 611 ================ 612 */ 613 void SV_RankUserLogout( int index ) 614 { 615 GR_STATUS status; 616 GR_STATUS cleanup_status; 617 618 if( !s_rankings_active ) 619 { 620 return; 621 } 622 623 assert( index >= 0 ); 624 assert( index < sv_maxclients->value ); 625 assert( s_ranked_players ); 626 627 if( s_ranked_players[index].context == 0 ) { 628 return; 629 } 630 631 Com_DPrintf( "SV_RankUserLogout( %d );\n", index ); 632 633 // masqueraded player may not be active yet, if they fail validation, 634 // but still they have a context needs to be cleaned 635 // what matters is the s_ranked_players[index].context 636 637 // send reports, proceed to SV_RankSendReportsCBF 638 status = GRankSendReportsAsync 639 ( 640 s_ranked_players[index].context, 641 0, 642 SV_RankSendReportsCBF, 643 (void*)&s_ranked_players[index], 644 GR_OPT_END 645 ); 646 647 if( status == GR_STATUS_PENDING ) 648 { 649 s_ranked_players[index].grank_status = QGR_STATUS_PENDING; 650 s_ranked_players[index].final_status = QGR_STATUS_NEW; 651 } 652 else 653 { 654 SV_RankError( "SV_RankUserLogout: Expected GR_STATUS_PENDING, got %s", 655 SV_RankStatusString( status ) ); 656 657 cleanup_status = GRankCleanupAsync 658 ( 659 s_ranked_players[index].context, 660 0, 661 SV_RankCleanupCBF, 662 (void*)&s_ranked_players[index], 663 GR_OPT_END 664 ); 665 666 if( cleanup_status != GR_STATUS_PENDING ) 667 { 668 SV_RankError( "SV_RankUserLogout: Expected " 669 "GR_STATUS_PENDING from GRankCleanupAsync, got %s", 670 SV_RankStatusString( cleanup_status ) ); 671 SV_RankCloseContext( &(s_ranked_players[index]) ); 672 } 673 } 674 } 675 676 /* 677 ================ 678 SV_RankReportInt 679 ================ 680 */ 681 void SV_RankReportInt( int index1, int index2, int key, int value, 682 qboolean accum ) 683 { 684 GR_STATUS status; 685 GR_CONTEXT context; 686 uint64_t match; 687 uint64_t user1; 688 uint64_t user2; 689 int opt_accum; 690 691 if( !s_rankings_active ) 692 { 693 return; 694 } 695 696 assert( index1 >= -1 ); 697 assert( index1 < sv_maxclients->value ); 698 assert( index2 >= -1 ); 699 assert( index2 < sv_maxclients->value ); 700 assert( s_ranked_players ); 701 702 // Com_DPrintf( "SV_RankReportInt( %d, %d, %d, %d, %d );\n", index1, index2, 703 // key, value, accum ); 704 705 // get context, match, and player_id for player index1 706 if( index1 == -1 ) 707 { 708 context = s_server_context; 709 match = s_server_match; 710 user1 = 0; 711 } 712 else 713 { 714 if( s_ranked_players[index1].grank_status != QGR_STATUS_ACTIVE ) 715 { 716 Com_DPrintf( "SV_RankReportInt: Expecting QGR_STATUS_ACTIVE" 717 " Got Unexpected status %d for player %d\n", 718 s_ranked_players[index1].grank_status, index1 ); 719 return; 720 } 721 722 context = s_ranked_players[index1].context; 723 match = s_ranked_players[index1].match; 724 user1 = s_ranked_players[index1].player_id; 725 } 726 727 // get player_id for player index2 728 if( index2 == -1 ) 729 { 730 user2 = 0; 731 } 732 else 733 { 734 if( s_ranked_players[index2].grank_status != QGR_STATUS_ACTIVE ) 735 { 736 Com_DPrintf( "SV_RankReportInt: Expecting QGR_STATUS_ACTIVE" 737 " Got Unexpected status %d for player %d\n", 738 s_ranked_players[index2].grank_status, index2 ); 739 return; 740 } 741 742 user2 = s_ranked_players[index2].player_id; 743 } 744 745 opt_accum = accum ? GR_OPT_ACCUM : GR_OPT_END; 746 747 status = GRankReportInt 748 ( 749 context, 750 match, 751 user1, 752 user2, 753 key, 754 value, 755 opt_accum, 756 GR_OPT_END 757 ); 758 759 if( status != GR_STATUS_OK ) 760 { 761 SV_RankError( "SV_RankReportInt: Unexpected status %s", 762 SV_RankStatusString( status ) ); 763 } 764 765 if( user2 != 0 ) 766 { 767 context = s_ranked_players[index2].context; 768 match = s_ranked_players[index2].match; 769 770 status = GRankReportInt 771 ( 772 context, 773 match, 774 user1, 775 user2, 776 key, 777 value, 778 opt_accum, 779 GR_OPT_END 780 ); 781 782 if( status != GR_STATUS_OK ) 783 { 784 SV_RankError( "SV_RankReportInt: Unexpected status %s", 785 SV_RankStatusString( status ) ); 786 } 787 } 788 } 789 790 /* 791 ================ 792 SV_RankReportStr 793 ================ 794 */ 795 void SV_RankReportStr( int index1, int index2, int key, char* value ) 796 { 797 GR_STATUS status; 798 GR_CONTEXT context; 799 uint64_t match; 800 uint64_t user1; 801 uint64_t user2; 802 803 if( !s_rankings_active ) 804 { 805 return; 806 } 807 808 assert( index1 >= -1 ); 809 assert( index1 < sv_maxclients->value ); 810 assert( index2 >= -1 ); 811 assert( index2 < sv_maxclients->value ); 812 assert( s_ranked_players ); 813 814 // Com_DPrintf( "SV_RankReportStr( %d, %d, %d, \"%s\" );\n", index1, index2, 815 // key, value ); 816 817 // get context, match, and player_id for player index1 818 if( index1 == -1 ) 819 { 820 context = s_server_context; 821 match = s_server_match; 822 user1 = 0; 823 } 824 else 825 { 826 if( s_ranked_players[index1].grank_status != QGR_STATUS_ACTIVE ) 827 { 828 Com_DPrintf( "SV_RankReportStr: Unexpected status %d\n", 829 s_ranked_players[index1].grank_status ); 830 return; 831 } 832 833 context = s_ranked_players[index1].context; 834 match = s_ranked_players[index1].match; 835 user1 = s_ranked_players[index1].player_id; 836 } 837 838 // get player_id for player index2 839 if( index2 == -1 ) 840 { 841 user2 = 0; 842 } 843 else 844 { 845 if( s_ranked_players[index2].grank_status != QGR_STATUS_ACTIVE ) 846 { 847 Com_DPrintf( "SV_RankReportStr: Unexpected status %d\n", 848 s_ranked_players[index2].grank_status ); 849 return; 850 } 851 852 user2 = s_ranked_players[index2].player_id; 853 } 854 855 status = GRankReportStr 856 ( 857 context, 858 match, 859 user1, 860 user2, 861 key, 862 value, 863 GR_OPT_END 864 ); 865 866 if( status != GR_STATUS_OK ) 867 { 868 SV_RankError( "SV_RankReportStr: Unexpected status %s", 869 SV_RankStatusString( status ) ); 870 } 871 872 if( user2 != 0 ) 873 { 874 context = s_ranked_players[index2].context; 875 match = s_ranked_players[index2].match; 876 877 status = GRankReportStr 878 ( 879 context, 880 match, 881 user1, 882 user2, 883 key, 884 value, 885 GR_OPT_END 886 ); 887 888 if( status != GR_STATUS_OK ) 889 { 890 SV_RankError( "SV_RankReportInt: Unexpected status %s", 891 SV_RankStatusString( status ) ); 892 } 893 } 894 } 895 896 /* 897 ================ 898 SV_RankQuit 899 ================ 900 */ 901 void SV_RankQuit( void ) 902 { 903 int i; 904 int j = 0; 905 // yuck 906 907 while( s_rankings_contexts > 1 ) 908 { 909 assert(s_ranked_players); 910 if( s_ranked_players != NULL ) 911 { 912 for( i = 0; i < sv_maxclients->value; i++ ) 913 { 914 // check for players that weren't yet active in SV_RankEnd 915 if( s_ranked_players[i].grank_status == QGR_STATUS_ACTIVE ) 916 { 917 SV_RankUserLogout( i ); 918 Com_DPrintf( "SV_RankQuit: SV_RankUserLogout %d\n",i ); 919 } 920 else 921 { 922 if( s_ranked_players[i].context ) 923 { 924 GR_STATUS cleanup_status; 925 cleanup_status = GRankCleanupAsync 926 ( 927 s_ranked_players[i].context, 928 0, 929 SV_RankCleanupCBF, 930 (void*)&(s_ranked_players[i]), 931 GR_OPT_END 932 ); 933 934 if( cleanup_status != GR_STATUS_PENDING ) 935 { 936 SV_RankError( "SV_RankQuit: Expected " 937 "GR_STATUS_PENDING from GRankCleanupAsync, got %s", 938 SV_RankStatusString( cleanup_status ) ); 939 } 940 } 941 } 942 } 943 } 944 SV_RankPoll(); 945 946 // should've finished by now 947 assert( (j++) < 68 ); 948 } 949 } 950 951 /* 952 ============================================================================== 953 954 Private Functions 955 956 ============================================================================== 957 */ 958 959 /* 960 ================= 961 SV_RankNewGameCBF 962 ================= 963 */ 964 static void SV_RankNewGameCBF( GR_NEWGAME* gr_newgame, void* cbf_arg ) 965 { 966 GR_MATCH match; 967 int i; 968 969 assert( gr_newgame != NULL ); 970 assert( cbf_arg == NULL ); 971 972 Com_DPrintf( "SV_RankNewGameCBF( %08X, %08X );\n", gr_newgame, cbf_arg ); 973 974 if( gr_newgame->status == GR_STATUS_OK ) 975 { 976 char info[MAX_INFO_STRING]; 977 char gameid[sizeof(s_ranked_players[i].game_id) * 4 / 3 + 2]; 978 979 // save game id 980 s_rankings_game_id = gr_newgame->game_id; 981 982 // encode gameid 983 memset(gameid,0,sizeof(gameid)); 984 SV_RankEncodeGameID(s_rankings_game_id,gameid,sizeof(gameid)); 985 986 // set CS_GRANK rankingsGameID to pass to client 987 memset(info,0,sizeof(info)); 988 Info_SetValueForKey( info, "rankingsGameKey", s_rankings_game_key ); 989 Info_SetValueForKey( info, "rankingsGameID", gameid ); 990 SV_SetConfigstring( CS_GRANK, info ); 991 992 // initialize client status 993 for( i = 0; i < sv_maxclients->value; i++ ) 994 s_ranked_players[i].grank_status = QGR_STATUS_NEW; 995 996 // start new match 997 match = GRankStartMatch( s_server_context ); 998 s_server_match = match.match; 999 1000 // ready to go 1001 s_rankings_active = qtrue; 1002 Cvar_Set( "sv_rankingsActive", "1" ); 1003 1004 } 1005 else if( gr_newgame->status == GR_STATUS_BADLEAGUE ) 1006 { 1007 SV_RankError( "SV_RankNewGameCBF: Invalid League name\n" ); 1008 } 1009 else 1010 { 1011 //GRank handle new game failure 1012 // force SV_RankEnd() to run 1013 //SV_RankEnd(); 1014 SV_RankError( "SV_RankNewGameCBF: Unexpected status %s", 1015 SV_RankStatusString( gr_newgame->status ) ); 1016 } 1017 } 1018 1019 /* 1020 ================ 1021 SV_RankUserCBF 1022 ================ 1023 */ 1024 static void SV_RankUserCBF( GR_LOGIN* gr_login, void* cbf_arg ) 1025 { 1026 ranked_player_t* ranked_player; 1027 GR_STATUS join_status; 1028 GR_STATUS cleanup_status; 1029 1030 assert( gr_login != NULL ); 1031 assert( cbf_arg != NULL ); 1032 1033 Com_DPrintf( "SV_RankUserCBF( %08X, %08X );\n", gr_login, cbf_arg ); 1034 1035 ranked_player = (ranked_player_t*)cbf_arg; 1036 assert(ranked_player); 1037 assert( ranked_player->context ); 1038 1039 switch( gr_login->status ) 1040 { 1041 case GR_STATUS_OK: 1042 // attempt to join the game, proceed to SV_RankJoinGameCBF 1043 join_status = GRankJoinGameAsync 1044 ( 1045 ranked_player->context, 1046 s_rankings_game_id, 1047 SV_RankJoinGameCBF, 1048 cbf_arg, 1049 GR_OPT_END 1050 ); 1051 1052 if( join_status != GR_STATUS_PENDING ) 1053 { 1054 SV_RankError( "SV_RankUserCBF: Expected GR_STATUS_PENDING " 1055 "from GRankJoinGameAsync, got %s", 1056 SV_RankStatusString( join_status ) ); 1057 } 1058 break; 1059 case GR_STATUS_NOUSER: 1060 Com_DPrintf( "SV_RankUserCBF: Got status %s\n", 1061 SV_RankStatusString( gr_login->status ) ); 1062 ranked_player->final_status = QGR_STATUS_NO_USER; 1063 break; 1064 case GR_STATUS_BADPASSWORD: 1065 Com_DPrintf( "SV_RankUserCBF: Got status %s\n", 1066 SV_RankStatusString( gr_login->status ) ); 1067 ranked_player->final_status = QGR_STATUS_BAD_PASSWORD; 1068 break; 1069 case GR_STATUS_TIMEOUT: 1070 Com_DPrintf( "SV_RankUserCBF: Got status %s\n", 1071 SV_RankStatusString( gr_login->status ) ); 1072 ranked_player->final_status = QGR_STATUS_TIMEOUT; 1073 break; 1074 default: 1075 Com_DPrintf( "SV_RankUserCBF: Unexpected status %s\n", 1076 SV_RankStatusString( gr_login->status ) ); 1077 ranked_player->final_status = QGR_STATUS_ERROR; 1078 break; 1079 } 1080 1081 if( ranked_player->final_status != QGR_STATUS_NEW ) 1082 { 1083 // login or create failed, so clean up before the next attempt 1084 cleanup_status = GRankCleanupAsync 1085 ( 1086 ranked_player->context, 1087 0, 1088 SV_RankCleanupCBF, 1089 (void*)ranked_player, 1090 GR_OPT_END 1091 ); 1092 1093 if( cleanup_status != GR_STATUS_PENDING ) 1094 { 1095 SV_RankError( "SV_RankUserCBF: Expected GR_STATUS_PENDING " 1096 "from GRankCleanupAsync, got %s", 1097 SV_RankStatusString( cleanup_status ) ); 1098 SV_RankCloseContext( ranked_player ); 1099 } 1100 } 1101 } 1102 1103 /* 1104 ================ 1105 SV_RankJoinGameCBF 1106 ================ 1107 */ 1108 static void SV_RankJoinGameCBF( GR_JOINGAME* gr_joingame, void* cbf_arg ) 1109 { 1110 ranked_player_t* ranked_player; 1111 GR_MATCH match; 1112 GR_STATUS cleanup_status; 1113 1114 assert( gr_joingame != NULL ); 1115 assert( cbf_arg != NULL ); 1116 1117 Com_DPrintf( "SV_RankJoinGameCBF( %08X, %08X );\n", gr_joingame, cbf_arg ); 1118 1119 ranked_player = (ranked_player_t*)cbf_arg; 1120 1121 assert( ranked_player ); 1122 assert( ranked_player->context != 0 ); 1123 1124 if( gr_joingame->status == GR_STATUS_OK ) 1125 { 1126 int i; 1127 // save user id 1128 ranked_player->player_id = gr_joingame->player_id; 1129 memcpy(ranked_player->token,gr_joingame->token, 1130 sizeof(GR_PLAYER_TOKEN)) ; 1131 match = GRankStartMatch( ranked_player->context ); 1132 ranked_player->match = match.match; 1133 ranked_player->grank = gr_joingame->rank; 1134 1135 // find the index and call SV_RankUserValidate 1136 for (i=0;i<sv_maxclients->value;i++) 1137 if ( ranked_player == &s_ranked_players[i] ) 1138 SV_RankUserValidate(i,NULL,NULL,0, gr_joingame->rank,ranked_player->name); 1139 } 1140 else 1141 { 1142 //GRand handle join game failure 1143 SV_RankError( "SV_RankJoinGameCBF: Unexpected status %s", 1144 SV_RankStatusString( gr_joingame->status ) ); 1145 1146 cleanup_status = GRankCleanupAsync 1147 ( 1148 ranked_player->context, 1149 0, 1150 SV_RankCleanupCBF, 1151 cbf_arg, 1152 GR_OPT_END 1153 ); 1154 1155 if( cleanup_status != GR_STATUS_PENDING ) 1156 { 1157 SV_RankError( "SV_RankJoinGameCBF: Expected " 1158 "GR_STATUS_PENDING from GRankCleanupAsync, got %s", 1159 SV_RankStatusString( cleanup_status ) ); 1160 SV_RankCloseContext( ranked_player ); 1161 } 1162 } 1163 } 1164 1165 /* 1166 ================ 1167 SV_RankSendReportsCBF 1168 ================ 1169 */ 1170 static void SV_RankSendReportsCBF( GR_STATUS* status, void* cbf_arg ) 1171 { 1172 ranked_player_t* ranked_player; 1173 GR_CONTEXT context; 1174 GR_STATUS cleanup_status; 1175 1176 assert( status != NULL ); 1177 // NULL cbf_arg means server is sending match reports 1178 1179 Com_DPrintf( "SV_RankSendReportsCBF( %08X, %08X );\n", status, cbf_arg ); 1180 1181 ranked_player = (ranked_player_t*)cbf_arg; 1182 if( ranked_player == NULL ) 1183 { 1184 Com_DPrintf( "SV_RankSendReportsCBF: server\n" ); 1185 context = s_server_context; 1186 } 1187 else 1188 { 1189 Com_DPrintf( "SV_RankSendReportsCBF: player\n" ); 1190 context = ranked_player->context; 1191 } 1192 1193 //assert( context != 0 ); 1194 if( *status != GR_STATUS_OK ) 1195 { 1196 SV_RankError( "SV_RankSendReportsCBF: Unexpected status %s", 1197 SV_RankStatusString( *status ) ); 1198 } 1199 1200 if( context == 0 ) 1201 { 1202 Com_DPrintf( "SV_RankSendReportsCBF: WARNING: context == 0" ); 1203 SV_RankCloseContext( ranked_player ); 1204 } 1205 else 1206 { 1207 cleanup_status = GRankCleanupAsync 1208 ( 1209 context, 1210 0, 1211 SV_RankCleanupCBF, 1212 cbf_arg, 1213 GR_OPT_END 1214 ); 1215 1216 if( cleanup_status != GR_STATUS_PENDING ) 1217 { 1218 SV_RankError( "SV_RankSendReportsCBF: Expected " 1219 "GR_STATUS_PENDING from GRankCleanupAsync, got %s", 1220 SV_RankStatusString( cleanup_status ) ); 1221 SV_RankCloseContext( ranked_player ); 1222 } 1223 } 1224 } 1225 1226 /* 1227 ================ 1228 SV_RankCleanupCBF 1229 ================ 1230 */ 1231 static void SV_RankCleanupCBF( GR_STATUS* status, void* cbf_arg ) 1232 { 1233 ranked_player_t* ranked_player; 1234 ranked_player = (ranked_player_t*)cbf_arg; 1235 1236 assert( status != NULL ); 1237 // NULL cbf_arg means server is cleaning up 1238 1239 Com_DPrintf( "SV_RankCleanupCBF( %08X, %08X );\n", status, cbf_arg ); 1240 1241 if( *status != GR_STATUS_OK ) 1242 { 1243 SV_RankError( "SV_RankCleanupCBF: Unexpected status %s", 1244 SV_RankStatusString( *status ) ); 1245 } 1246 1247 SV_RankCloseContext( ranked_player ); 1248 } 1249 1250 /* 1251 ================ 1252 SV_RankCloseContext 1253 ================ 1254 */ 1255 static void SV_RankCloseContext( ranked_player_t* ranked_player ) 1256 { 1257 if( ranked_player == NULL ) 1258 { 1259 // server cleanup 1260 if( s_server_context == 0 ) 1261 { 1262 return; 1263 } 1264 s_server_context = 0; 1265 s_server_match = 0; 1266 } 1267 else 1268 { 1269 // player cleanup 1270 if( s_ranked_players == NULL ) 1271 { 1272 return; 1273 } 1274 if( ranked_player->context == 0 ) 1275 { 1276 return; 1277 } 1278 ranked_player->context = 0; 1279 ranked_player->match = 0; 1280 ranked_player->player_id = 0; 1281 memset( ranked_player->token, 0, sizeof(GR_PLAYER_TOKEN) ); 1282 ranked_player->grank_status = ranked_player->final_status; 1283 ranked_player->final_status = QGR_STATUS_NEW; 1284 ranked_player->name[0] = '\0'; 1285 } 1286 1287 assert( s_rankings_contexts > 0 ); 1288 s_rankings_contexts--; 1289 Com_DPrintf( "SV_RankCloseContext: s_rankings_contexts = %d\n", 1290 s_rankings_contexts ); 1291 1292 if( s_rankings_contexts == 0 ) 1293 { 1294 GRankLogLevel( GRLOG_OFF ); 1295 1296 if( s_ranked_players != NULL ) 1297 { 1298 Z_Free( s_ranked_players ); 1299 s_ranked_players = NULL; 1300 } 1301 1302 s_rankings_active = qfalse; 1303 Cvar_Set( "sv_rankingsActive", "0" ); 1304 } 1305 } 1306 1307 /* 1308 ================ 1309 SV_RankAsciiEncode 1310 1311 Encodes src_len bytes of binary data from the src buffer as ASCII text, 1312 using 6 bits per character. The result string is null-terminated and 1313 stored in the dest buffer. 1314 1315 The dest buffer must be at least (src_len * 4) / 3 + 2 bytes in length. 1316 1317 Returns the length of the result string, not including the null. 1318 ================ 1319 */ 1320 static int SV_RankAsciiEncode( char* dest, const unsigned char* src, 1321 int src_len ) 1322 { 1323 unsigned char bin[3]; 1324 unsigned char txt[4]; 1325 int dest_len = 0; 1326 int i; 1327 int j; 1328 int num_chars; 1329 1330 assert( dest != NULL ); 1331 assert( src != NULL ); 1332 1333 for( i = 0; i < src_len; i += 3 ) 1334 { 1335 // read three bytes of input 1336 for( j = 0; j < 3; j++ ) 1337 { 1338 bin[j] = (i + j < src_len) ? src[i + j] : 0; 1339 } 1340 1341 // get four 6-bit values from three bytes 1342 txt[0] = bin[0] >> 2; 1343 txt[1] = ((bin[0] << 4) | (bin[1] >> 4)) & 63; 1344 txt[2] = ((bin[1] << 2) | (bin[2] >> 6)) & 63; 1345 txt[3] = bin[2] & 63; 1346 1347 // store ASCII encoding of 6-bit values 1348 num_chars = (i + 2 < src_len) ? 4 : ((src_len - i) * 4) / 3 + 1; 1349 for( j = 0; j < num_chars; j++ ) 1350 { 1351 dest[dest_len++] = s_ascii_encoding[txt[j]]; 1352 } 1353 } 1354 1355 dest[dest_len] = '\0'; 1356 1357 return dest_len; 1358 } 1359 1360 /* 1361 ================ 1362 SV_RankAsciiDecode 1363 1364 Decodes src_len characters of ASCII text from the src buffer, stores 1365 the binary result in the dest buffer. 1366 1367 The dest buffer must be at least (src_len * 3) / 4 bytes in length. 1368 1369 Returns the length of the binary result, or zero for invalid input. 1370 ================ 1371 */ 1372 static int SV_RankAsciiDecode( unsigned char* dest, const char* src, 1373 int src_len ) 1374 { 1375 static unsigned char s_inverse_encoding[256]; 1376 static char s_init = 0; 1377 1378 unsigned char bin[3]; 1379 unsigned char txt[4]; 1380 int dest_len = 0; 1381 int i; 1382 int j; 1383 int num_bytes; 1384 1385 assert( dest != NULL ); 1386 assert( src != NULL ); 1387 1388 if( !s_init ) 1389 { 1390 // initialize lookup table for decoding 1391 memset( s_inverse_encoding, 255, sizeof(s_inverse_encoding) ); 1392 for( i = 0; i < 64; i++ ) 1393 { 1394 s_inverse_encoding[s_ascii_encoding[i]] = i; 1395 } 1396 s_init = 1; 1397 } 1398 1399 for( i = 0; i < src_len; i += 4 ) 1400 { 1401 // read four characters of input, decode them to 6-bit values 1402 for( j = 0; j < 4; j++ ) 1403 { 1404 txt[j] = (i + j < src_len) ? s_inverse_encoding[src[i + j]] : 0; 1405 if (txt[j] == 255) 1406 { 1407 return 0; // invalid input character 1408 } 1409 } 1410 1411 // get three bytes from four 6-bit values 1412 bin[0] = (txt[0] << 2) | (txt[1] >> 4); 1413 bin[1] = (txt[1] << 4) | (txt[2] >> 2); 1414 bin[2] = (txt[2] << 6) | txt[3]; 1415 1416 // store binary data 1417 num_bytes = (i + 3 < src_len) ? 3 : ((src_len - i) * 3) / 4; 1418 for( j = 0; j < num_bytes; j++ ) 1419 { 1420 dest[dest_len++] = bin[j]; 1421 } 1422 } 1423 1424 return dest_len; 1425 } 1426 1427 /* 1428 ================ 1429 SV_RankEncodeGameID 1430 ================ 1431 */ 1432 static void SV_RankEncodeGameID( uint64_t game_id, char* result, 1433 int len ) 1434 { 1435 assert( result != NULL ); 1436 1437 if( len < ( ( sizeof(game_id) * 4) / 3 + 2) ) 1438 { 1439 Com_DPrintf( "SV_RankEncodeGameID: result buffer too small\n" ); 1440 result[0] = '\0'; 1441 } 1442 else 1443 { 1444 qint64 gameid = LittleLong64(*(qint64*)&game_id); 1445 SV_RankAsciiEncode( result, (unsigned char*)&gameid, 1446 sizeof(qint64) ); 1447 } 1448 } 1449 1450 /* 1451 ================ 1452 SV_RankDecodePlayerID 1453 ================ 1454 */ 1455 static uint64_t SV_RankDecodePlayerID( const char* string ) 1456 { 1457 unsigned char buffer[9]; 1458 int len; 1459 qint64 player_id; 1460 1461 assert( string != NULL ); 1462 1463 len = strlen (string) ; 1464 Com_DPrintf( "SV_RankDecodePlayerID: string length %d\n",len ); 1465 SV_RankAsciiDecode( buffer, string, len ); 1466 player_id = LittleLong64(*(qint64*)buffer); 1467 return *(uint64_t*)&player_id; 1468 } 1469 1470 /* 1471 ================ 1472 SV_RankDecodePlayerKey 1473 ================ 1474 */ 1475 static void SV_RankDecodePlayerKey( const char* string, GR_PLAYER_TOKEN key ) 1476 { 1477 unsigned char buffer[1400]; 1478 int len; 1479 assert( string != NULL ); 1480 1481 len = strlen (string) ; 1482 Com_DPrintf( "SV_RankDecodePlayerKey: string length %d\n",len ); 1483 1484 memset(key,0,sizeof(GR_PLAYER_TOKEN)); 1485 memset(buffer,0,sizeof(buffer)); 1486 memcpy( key, buffer, SV_RankAsciiDecode( buffer, string, len ) ); 1487 } 1488 1489 /* 1490 ================ 1491 SV_RankStatusString 1492 ================ 1493 */ 1494 static char* SV_RankStatusString( GR_STATUS status ) 1495 { 1496 switch( status ) 1497 { 1498 case GR_STATUS_OK: return "GR_STATUS_OK"; 1499 case GR_STATUS_ERROR: return "GR_STATUS_ERROR"; 1500 case GR_STATUS_BADPARAMS: return "GR_STATUS_BADPARAMS"; 1501 case GR_STATUS_NETWORK: return "GR_STATUS_NETWORK"; 1502 case GR_STATUS_NOUSER: return "GR_STATUS_NOUSER"; 1503 case GR_STATUS_BADPASSWORD: return "GR_STATUS_BADPASSWORD"; 1504 case GR_STATUS_BADGAME: return "GR_STATUS_BADGAME"; 1505 case GR_STATUS_PENDING: return "GR_STATUS_PENDING"; 1506 case GR_STATUS_BADDOMAIN: return "GR_STATUS_BADDOMAIN"; 1507 case GR_STATUS_DOMAINLOCK: return "GR_STATUS_DOMAINLOCK"; 1508 case GR_STATUS_TIMEOUT: return "GR_STATUS_TIMEOUT"; 1509 case GR_STATUS_INVALIDUSER: return "GR_STATUS_INVALIDUSER"; 1510 case GR_STATUS_INVALIDCONTEXT: return "GR_STATUS_INVALIDCONTEXT"; 1511 default: return "(UNKNOWN)"; 1512 } 1513 } 1514 1515 /* 1516 ================ 1517 SV_RankError 1518 ================ 1519 */ 1520 static void SV_RankError( const char* fmt, ... ) 1521 { 1522 va_list arg_ptr; 1523 char text[1024]; 1524 1525 va_start( arg_ptr, fmt ); 1526 vsprintf( text, fmt, arg_ptr ); 1527 va_end( arg_ptr ); 1528 1529 Com_DPrintf( "****************************************\n" ); 1530 Com_DPrintf( "SV_RankError: %s\n", text ); 1531 Com_DPrintf( "****************************************\n" ); 1532 1533 s_rankings_active = qfalse; 1534 Cvar_Set( "sv_rankingsActive", "0" ); 1535 // FIXME - attempt clean shutdown? 1536 } 1537