Xonotic QuakeC
The free, fast arena FPS with crisp movement and a wide array of weapons
playerstats.qc
Go to the documentation of this file.
1#include "playerstats.qh"
2
3#if defined(CSQC)
4#elif defined(MENUQC)
5#elif defined(SVQC)
6 #include <common/constants.qh>
7 #include <common/stats.qh>
8 #include <common/util.qh>
10 #include <server/anticheat.qh>
11 #include <server/client.qh>
12 #include <server/intermission.qh>
13 #include <server/scores.qh>
15 #include <server/world.qh>
16#endif
17
18
19#ifdef GAMEQC
20REPLICATE(cvar_cl_allow_uid2name, int, "cl_allow_uid2name");
21REPLICATE(cvar_cl_allow_uidranking, bool, "cl_allow_uidranking");
22REPLICATE(cvar_cl_allow_uidtracking, int, "cl_allow_uidtracking");
23#endif
24
25#ifdef SVQC
26REPLICATE_APPLYCHANGE("cl_allow_uidtracking", { PlayerStats_GameReport_AddPlayer(this); });
27#endif
28
29#ifdef SVQC
31{
32 //foobar
33}
34
35// Deletes current playerstats DB, creates a new one and fully initializes it
37{
41
42 if (PS_GR_OUT_DB >= 0)
43 {
46 }
47 if(PS_GR_OUT_DB < 0)
48 return;
49
50 for (int i = 0; i < 16; ++i)
51 if (teamscorekeepers[i])
53 FOREACH_CLIENT(true, {
54 // NOTE Adding back a player we are applying any cl_allow_uidtracking change
55 // usually only possible by reconnecting to the server
56 strfree(it.playerstats_id);
57 PlayerStats_GameReport_AddEvent(sprintf("kills-%d", it.playerid));
59 it.handicap_avg_given_sum = 0;
60 it.handicap_avg_taken_sum = 0;
61 });
62 FOREACH(Scores, true, {
63 string label = scores_label(it);
64 if (label == "")
65 continue;
68 });
69 for(int i = 0; i < MAX_TEAMSCORE; ++i)
70 {
71 string label = teamscores_label(i);
72 if (label == "")
73 continue;
76 }
77}
78
80{
81 if((PS_GR_OUT_DB < 0) || (e.playerstats_id)) { return; }
82
83 // set up player identification
84 string s = "";
85
86 if((e.crypto_idfp != "") && (CS_CVAR(e).cvar_cl_allow_uidtracking == 1))
87 { s = e.crypto_idfp; }
88 else if(IS_BOT_CLIENT(e))
89 { s = sprintf("bot#%g#%s", skill, e.cleanname); }
90
91 if((s == "") || find(NULL, playerstats_id, s)) // already have one of the ID - next one can't be tracked then!
92 {
93 if(IS_BOT_CLIENT(e))
94 { s = sprintf("bot#%d", e.playerid); }
95 else
96 { s = sprintf("player#%d", e.playerid); }
97 }
98
99 e.playerstats_id = strzone(s);
100
101 // now add the player to the database
102 string key = sprintf("%s:*", e.playerstats_id);
103 string p = db_get(PS_GR_OUT_DB, key);
104
105 if(p == "")
106 {
107 if(PS_GR_OUT_PL)
108 {
111 }
112 else { db_put(PS_GR_OUT_DB, key, "#"); }
113 PS_GR_OUT_PL = strzone(e.playerstats_id);
114 }
115}
116
118{
119 if(PS_GR_OUT_DB < 0) { return; }
120
121 string key = sprintf("%d", t);
122 string p = db_get(PS_GR_OUT_DB, key);
123
124 if(p == "")
125 {
126 if(PS_GR_OUT_TL)
127 {
130 }
131 else { db_put(PS_GR_OUT_DB, key, "#"); }
132 PS_GR_OUT_TL = strzone(key);
133 }
134}
135
137{
138 if(PS_GR_OUT_DB < 0) { return; }
139
140 string key = sprintf("*:%s", event_id);
141 string p = db_get(PS_GR_OUT_DB, key);
142
143 if(p == "")
144 {
145 if(PS_GR_OUT_EVL)
146 {
149 }
150 else { db_put(PS_GR_OUT_DB, key, "#"); }
151 PS_GR_OUT_EVL = strzone(event_id);
152 }
153}
154
155float PlayerStats_GameReport_Event(string prefix, string event_id, float value)
156{
157 if((prefix == "") || PS_GR_OUT_DB < 0) { return 0; }
158
159 // use a cheaper strcat here since this function is called often in game
160 string key = strcat(prefix, ":", event_id);
161 float val = stof(db_get(PS_GR_OUT_DB, key));
162 val += value;
163 db_put(PS_GR_OUT_DB, key, ftos(val));
164 return val;
165}
166
168{
169 #define ACCMAC(suffix, field) \
170 PlayerStats_GameReport_Event_Player(p, \
171 sprintf("acc-%s-%s", it.netname, suffix), CS(p).accuracy.(field[i-1]));
172 FOREACH(Weapons, it != WEP_Null, {
173 ACCMAC("real", accuracy_real)
174 ACCMAC("hit", accuracy_hit)
175 ACCMAC("fired", accuracy_fired)
176 ACCMAC("cnt-hit", accuracy_cnt_hit)
177 ACCMAC("cnt-fired", accuracy_cnt_fired)
178 ACCMAC("frags", accuracy_frags)
179 });
180 #undef ACCMAC
181}
182
184{
185 if((p.playerstats_id == "") || PS_GR_OUT_DB < 0) { return; }
186
187 // add global info!
188 if(p.alivetime_start)
189 {
191 p.alivetime_start = 0;
192 }
193
194 db_put(PS_GR_OUT_DB, sprintf("%s:_playerid", p.playerstats_id), ftos(p.playerid));
195
196 if(CS_CVAR(p).cvar_cl_allow_uid2name == 1 || IS_BOT_CLIENT(p))
197 db_put(PS_GR_OUT_DB, sprintf("%s:_netname", p.playerstats_id), playername(p.netname, p.team, false));
198
199 if(teamplay)
200 db_put(PS_GR_OUT_DB, sprintf("%s:_team", p.playerstats_id), ftos(p.team));
201
202 if(stof(db_get(PS_GR_OUT_DB, sprintf("%s:%s", p.playerstats_id, PLAYERSTATS_ALIVETIME))) > 0)
204
207
208 if(IS_REAL_CLIENT(p))
209 {
210 if(CS(p).latency_cnt)
211 {
212 float latency = max(0, CS(p).latency_sum / CS(p).latency_cnt);
213 if(latency)
214 {
215 // if previous average latency exists (player disconnected and reconnected)
216 // make the average of previous and current average latency
218 float new_latency = !prev_latency ? latency : (prev_latency + latency) / 2;
219 PlayerStats_GameReport_Event_Player(p, PLAYERSTATS_AVGLATENCY, -prev_latency + new_latency);
220 }
221 }
222
223 db_put(PS_GR_OUT_DB, sprintf("%s:_ranked", p.playerstats_id), ftos(CS_CVAR(p).cvar_cl_allow_uidranking));
224 }
225
226 strfree(p.playerstats_id);
227}
228
229void PlayerStats_GameReport(bool finished)
230{
231 if(PS_GR_OUT_DB < 0) { return; }
232
233 PlayerScore_Sort(score_dummyfield, 0, false, false);
234 PlayerScore_Sort(scoreboard_pos, 1, true, true);
236
237 FOREACH_CLIENT(true, {
238 // add personal score rank
240
241 // scoreboard data
242 if(it.scoreboard_pos)
243 {
244 // scoreboard is valid!
246
247 // add scoreboard position
249
250 // add scoreboard data
252
253 // if the match ended normally, add winning info
254 if(finished)
255 {
258 }
259 }
260
261 // handicap
262 const float given = GameRules_scoring_add(it, DMG, 0);
263 const float taken = GameRules_scoring_add(it, DMGTAKEN, 0);
264 PlayerStats_GameReport_Event_Player(it, PLAYERSTATS_HANDICAP_GIVEN, given <= 0 ? 1 : it.handicap_avg_given_sum / given);
265 PlayerStats_GameReport_Event_Player(it, PLAYERSTATS_HANDICAP_TAKEN, taken <= 0 ? 1 : it.handicap_avg_taken_sum / taken);
266
267 // collect final player information
269 });
270
272 {
275 PS_GR_OUT_DB = -1;
276 return;
277 }
278
284 NULL
285 );
286}
287
288void PlayerStats_GameReport_Init() // initiated before InitGameplayMode so that scores are added properly
289{
292
293 if(autocvar_g_playerstats_gamereport_uri == "") { return; }
294
296
297 if(PS_GR_OUT_DB >= 0)
298 {
300
301 if(autocvar_g_playerstats_gamereport_uri != cvar_defstring("g_playerstats_gamereport_uri"))
303 else if(checkextension("DP_CRYPTO") && checkextension("DP_QC_URI_POST"))
304 // XonStat submission requires player and server IDs, and HTTPS POST
306
317
318 // accuracy stats
319 FOREACH(Weapons, it != WEP_Null, {
320 PlayerStats_GameReport_AddEvent(strcat("acc-", it.netname, "-real"));
321 PlayerStats_GameReport_AddEvent(strcat("acc-", it.netname, "-hit"));
322 PlayerStats_GameReport_AddEvent(strcat("acc-", it.netname, "-fired"));
323 PlayerStats_GameReport_AddEvent(strcat("acc-", it.netname, "-cnt-hit"));
324 PlayerStats_GameReport_AddEvent(strcat("acc-", it.netname, "-cnt-fired"));
325 PlayerStats_GameReport_AddEvent(strcat("acc-", it.netname, "-frags"));
326 });
327
338
340 }
342}
343
344// this... is a hack, a temporary one until we get a proper duel gametype
345// TODO: remove duel hack after servers have migrated to the proper duel gametype!
347{
348 if(IS_GAMETYPE(DEATHMATCH) && autocvar_g_maxplayers == 2)
349 {
350 // probably duel, but let's make sure
351 int plcount = 0;
352 FOREACH_CLIENT(IS_PLAYER(it), ++plcount);
353 if(plcount <= 2)
354 return "duel";
355 }
356 return GetGametype();
357}
358
360{
361 string t, tn;
362 string p, pn;
363 string e, en;
364 string nn, tt;
365 string s;
366
367 switch(status)
368 {
369 // ======================================
370 // -- OUTGOING GAME REPORT INFORMATION --
371 // ======================================
372 /* SPECIFICATIONS:
373 * V: format version (always a fixed number) - this MUST be the first line!
374 * #: comment (MUST be ignored by any parser)
375 * R: release information on the server
376 * G: gametype
377 * O: mod name (icon request) as in server browser
378 * M: map name
379 * I: match ID (see "matchid" in world.qc)
380 * S: "hostname" of the server
381 * C: number of "unpure" cvar changes
382 * U: UDP port number of the server
383 * D: duration of the match
384 * RP: number of rounds played
385 * L: "ladder" in which the server is participating in
386 * P: player ID of an existing player; this also sets the owner for all following "n", "e" and "t" lines (lower case!)
387 * Q: team number of an existing team (format: team#NN); this also sets the owner for all following "e" lines (lower case!)
388 * i: player index
389 * n: nickname of the player (optional)
390 * t: team ID
391 * r: player ranking enabled / disabled
392 * e: followed by an event name, a space, and the event count/score
393 * event names can be:
394 * alivetime: total playing time of the player
395 * avglatency: average network latency compounded throughout the match
396 * wins: number of games won (can only be set if matches is set)
397 * matches: number of matches played to the end (not aborted by map switch)
398 * joins: number of matches joined (always 1 unless player never played during the match)
399 * scoreboardvalid: set to 1 if the player was there at the end of the match
400 * total-<scoreboardname>: total score of that scoreboard item
401 * scoreboard-<scoreboardname>: end-of-game score of that scoreboard item (can differ in non-team games)
402 * achievement-<achievementname>: achievement counters (their "count" is usually 1 if nonzero at all)
403 * kills-<index>: number of kills against the indexed player
404 * rank <number>: rank of player
405 * handicapgiven: average handicap on given (dealt) damage throughout the match
406 * handicaptaken: average handicap on taken (received) damage throughout the match
407 * acc-<weapon netname>-real: total damage dealt excluding excess after death
408 * acc-<weapon netname>-hit: total damage dealt including excess after death
409 * acc-<weapon netname>-fired: total damage that all fired shots could have dealt
410 * acc-<weapon netname>-cnt-hit: amount of shots that actually hit
411 * acc-<weapon netname>-cnt-fired: amount of fired shots
412 * acc-<weapon netname>-frags: amount of frags dealt by weapon
413 */
415 {
416 url_fputs(fh, "V 9\n");
417 #ifdef WATERMARK
418 url_fputs(fh, sprintf("R %s\n", WATERMARK));
419 #endif
420 url_fputs(fh, sprintf("G %s\n", PlayerStats_GetGametype()));
421 url_fputs(fh, sprintf("O %s\n", modname));
422 url_fputs(fh, sprintf("M %s\n", GetMapname()));
423 url_fputs(fh, sprintf("I %s\n", matchid));
424 url_fputs(fh, sprintf("S %s\n", cvar_string("hostname")));
425 url_fputs(fh, sprintf("C %d\n", cvar_purechanges_count));
426 url_fputs(fh, sprintf("U %d\n", cvar("port")));
427 url_fputs(fh, sprintf("D %f\n", max(0, time - game_starttime)));
428 if (rounds_played > 0)
429 url_fputs(fh, sprintf("RP %d\n", rounds_played));
432
433 // TEAMS
434 if(teamplay)
435 {
436 for(t = PS_GR_OUT_TL; (tn = db_get(PS_GR_OUT_DB, sprintf("%d", stof(t)))) != ""; t = tn)
437 {
438 // start team section
439 url_fputs(fh, sprintf("Q team#%s\n", t));
440
441 // output team events // todo: does this do unnecessary loops? perhaps we should do a separate "team_events_last" tracker..."
442 for(e = PS_GR_OUT_EVL; (en = db_get(PS_GR_OUT_DB, sprintf("*:%s", e))) != ""; e = en)
443 {
444 float v = stof(db_get(PS_GR_OUT_DB, sprintf("team#%d:%s", stof(t), e)));
445 if(v != 0) { url_fputs(fh, sprintf("e %s %g\n", e, v)); }
446 }
447 }
448 }
449
450 // PLAYERS
451 for(p = PS_GR_OUT_PL; (pn = db_get(PS_GR_OUT_DB, sprintf("%s:*", p))) != ""; p = pn)
452 {
453 // start player section
454 url_fputs(fh, sprintf("P %s\n", p));
455
456 // playerid/index (entity number for this server)
457 nn = db_get(PS_GR_OUT_DB, sprintf("%s:_playerid", p));
458 if(nn != "") { url_fputs(fh, sprintf("i %s\n", nn)); }
459
460 // player name
461 nn = db_get(PS_GR_OUT_DB, sprintf("%s:_netname", p));
462 if(nn != "") { url_fputs(fh, sprintf("n %s\n", nn)); }
463
464 // team identification number
465 if(teamplay)
466 {
467 tt = db_get(PS_GR_OUT_DB, sprintf("%s:_team", p));
468 url_fputs(fh, sprintf("t %s\n", tt));
469 }
470
471 // skill ranking enabled
472 nn = db_get(PS_GR_OUT_DB, sprintf("%s:_ranked", p));
473 if(nn != "") { url_fputs(fh, sprintf("r %s\n", nn)); }
474
475 // output player events
476 for(e = PS_GR_OUT_EVL; (en = db_get(PS_GR_OUT_DB, sprintf("*:%s", e))) != ""; e = en)
477 {
478 float v = stof(db_get(PS_GR_OUT_DB, sprintf("%s:%s", p, e)));
479 if(v != 0) { url_fputs(fh, sprintf("e %s %g\n", e, v)); }
480 }
481 }
482 url_fputs(fh, "\n");
483 url_fclose(fh);
484 break;
485 }
486
487 // ======================================
488 // -- INCOMING GAME REPORT INFORMATION --
489 // ======================================
490 /* SPECIFICATIONS:
491 * stuff
492 */
494 {
495 // url_fclose is processing, we got a response for writing the data
496 // this must come from HTTP
497 LOG_DEBUG("Got response from player stats server:");
498 while((s = url_fgets(fh))) { LOG_DEBUG(" ", s); }
499 LOG_DEBUG("End of response.");
500 url_fclose(fh);
501 break;
502 }
503
504 case URL_READY_CLOSED:
505 {
506 // url_fclose has finished
507 LOG_DEBUG("Player stats written");
509 if(PS_GR_OUT_DB >= 0)
510 {
512 PS_GR_OUT_DB = -1;
513 }
514 break;
515 }
516
517 case URL_READY_ERROR:
518 default:
519 {
520 LOG_INFO("Player stats writing failed: ", ftos(status));
522 if(PS_GR_OUT_DB >= 0)
523 {
525 PS_GR_OUT_DB = -1;
526 }
527 break;
528 }
529 }
530}
531
532void PlayerStats_PlayerBasic(entity joiningplayer, float newrequest)
533{
534 GameRules_scoring_add(joiningplayer, SKILL, -1);
535 // http://stats.xonotic.org/skill?hashkey=Cm5mzXa1Vn9VX0%2FySV4Pp8ca9CTZvklERDqrrNOUFZU%3D
537 {
539 if (joiningplayer.crypto_idfp == "") {
540 GameRules_scoring_add(joiningplayer, SKILL, -1);
541 } else {
542 // create the database if it doesn't already exist
543 if(PS_B_IN_DB < 0)
545
546 // now request the information
547 uri = strcat(uri, "/skill?game_type_cd=", uri_escape(PlayerStats_GetGametype()));
548 uri = strcat(uri, "&hashkey=", uri_escape(joiningplayer.crypto_idfp));
549 LOG_DEBUG("Retrieving playerstats from URL: ", uri);
551 uri,
552 FILE_READ,
554 joiningplayer
555 );
556
557 // set status appropriately // todo: check whether the player info exists in the database previously
558 if(newrequest)
559 {
560 // database still contains useful information, so don't clear it of a useful status
561 joiningplayer.playerstats_basicstatus = PS_B_STATUS_WAITING;
562 }
563 else
564 {
565 // database was previously empty or never hit received status for some reason
566 joiningplayer.playerstats_basicstatus = PS_B_STATUS_UPDATING;
567 }
568 }
569 }
570 else
571 {
572 // server has this disabled, kill the DB and set status to idle
573 GameRules_scoring_add(joiningplayer, SKILL, -1);
574 if(PS_B_IN_DB >= 0)
575 {
577 PS_B_IN_DB = -1;
578
579 FOREACH_CLIENT(IS_REAL_CLIENT(it), it.playerstats_basicstatus = PS_B_STATUS_IDLE);
580 }
581 }
582}
583
584SHUTDOWN(PlayerStats_PlayerBasic_Shutdown)
585{
586 if(PS_B_IN_DB >= 0)
587 {
589 PS_B_IN_DB = -1;
590 }
591
592 if(PS_GR_OUT_DB >= 0)
593 {
595 PS_GR_OUT_DB = -1;
596 }
597}
598
600{
601 // determine whether we should retrieve playerbasic information again
602
603 LOG_DEBUGF("PlayerStats_PlayerBasic_CheckUpdate('%s'): %f",
604 joiningplayer.netname,
605 time
606 );
607
608 // TODO: check to see if this playerid is inside the database already somehow...
609 // for now we'll just check the field, but this won't work for players who disconnect and reconnect properly
610 // although maybe we should just submit another request ANYWAY?
611 if(!joiningplayer.playerstats_basicstatus)
612 {
614 joiningplayer,
615 (joiningplayer.playerstats_basicstatus == PS_B_STATUS_RECEIVED)
616 );
617 }
618}
619
621{
622 switch(status)
623 {
625 {
626 // THIS should be never called
627 LOG_DEBUG("-- Sending data to player stats server");
628 url_fputs(fh, "\n");
629 url_fclose(fh);
630 return;
631 }
632
634 {
635 string temp = "";
636 for (string s; (s = url_fgets(fh)); )
637 {
638 if (temp != "")
639 temp = strcat(temp, "\n", s);
640 else
641 temp = s;
642 }
643 url_fclose(fh);
644 int buf = json_parse(temp, _json_parse_array);
645 EXPECT_NE(-1, buf);
647 if (json_get(buf, "0.game_type_cd") == gametype && gametype != "")
648 {
649 float mu = stof(json_get(buf, "0.mu"));
650 //float sigma = stof(json_get(buf, "0.sigma"));
651 float skill = max(0, mu);
652 /* we display just the mean skill, since this most accurately represents how players will play on an average match.
653 * displaying mu - 3 * sigma would display worst-case performance, but when applied to every player this becomes meaningless.
654 * also we're not interested in the skill that we're confident players can operate above, rather where we expect them to operate.
655 */
656 GameRules_scoring_add(p, SKILL, skill + 1);
657 return;
658
659 }
660 break;
661 }
662 case URL_READY_CLOSED:
663 {
664 // url_fclose has finished
665 LOG_INFO("Player stats synchronized with server");
666 return;
667 }
668
669 case URL_READY_ERROR:
670 default:
671 {
672 LOG_INFO("Receiving player stats failed: ", ftos(status));
673 break;
674 }
675 }
676 GameRules_scoring_add(p, SKILL, -1);
677}
678#endif // SVQC
679
680#ifdef MENUQC
681
682
683#if 0 // reading the entire DB at once
684 string e = "", en = "";
685 float i = 0;
686 for(e = PS_D_IN_EVL; (en = db_get(PS_D_IN_DB, e)) != ""; e = en)
687 {
688 LOG_INFOF("%d:%s:%s", i, e, db_get(PS_D_IN_DB, sprintf("#%s", e)));
689 ++i;
690 }
691#endif
692
693void PlayerStats_PlayerDetail_AddItem(string event, string data)
694{
695 if(PS_D_IN_DB < 0) { return; }
696
697 // create a marker for the event so that we can access it later
698 string marker = sprintf("%s", event);
699 if(db_get(PS_D_IN_DB, marker) == "")
700 {
701 if(PS_D_IN_EVL)
702 {
703 db_put(PS_D_IN_DB, marker, PS_D_IN_EVL);
705 }
706 else { db_put(PS_D_IN_DB, marker, "#"); }
707 PS_D_IN_EVL = strzone(marker);
708 }
709
710 // now actually set the event data
711 db_put(PS_D_IN_DB, sprintf("#%s", event), data);
712 LOG_DEBUG("Added item ", sprintf("#%s", event), "=", data, " to PS_D_IN_DB");
713}
714
716{
717 // http://stats.xonotic.org/player/me
718 if((autocvar_g_playerstats_playerdetail_uri != "") && (crypto_getmyidstatus(0) > 0))
719 {
720 // create the database if it doesn't already exist
721 if(PS_D_IN_DB < 0)
723
724 //uri = strcat(uri, "/player/", uri_escape(crypto_getmyidfp(0)));
725 LOG_DEBUG("Retrieving playerstats from URL: ", autocvar_g_playerstats_playerdetail_uri);
730 NULL
731 );
732
734 }
735 else
736 {
737 // player has this disabled, kill the DB and set status to idle
738 if(PS_D_IN_DB >= 0)
739 {
741 PS_D_IN_DB = -1;
742 }
743
745 }
746}
747
749{
750 // determine whether we should retrieve playerdetail information again
751 float gamecount = cvar("cl_matchcount");
752
753 #if 0
754 LOG_INFOF("PlayerStats_PlayerDetail_CheckUpdate(): %f >= %f, %d > %d",
755 time,
758 gamecount
759 );
760 #endif
761
762 if(
764 ||
765 (gamecount > PS_D_LASTGAMECOUNT)
766 )
767 {
770 PS_D_LASTGAMECOUNT = gamecount;
771 }
772}
773
774void PlayerStats_PlayerDetail_Handler(entity fh, entity unused, float status)
775{
776 switch(status)
777 {
779 {
780 LOG_DEBUG("PlayerStats_PlayerDetail_Handler(): Sending data to player stats server...");
781 url_fputs(fh, "V 1\n");
782 #ifdef WATERMARK
783 url_fputs(fh, sprintf("R %s\n", WATERMARK));
784 #endif
785 url_fputs(fh, sprintf("l %s\n", cvar_string("_menu_prvm_language"))); // language
786 //url_fputs(fh, sprintf("c %s\n", cvar_string("_cl_country"))); // country
787 url_fputs(fh, sprintf("n %s\n", cvar_string("_cl_name"))); // name
788 url_fputs(fh, sprintf("m %s %s\n", cvar_string("_cl_playermodel"), cvar_string("_cl_playerskin"))); // model/skin
789 url_fputs(fh, "\n");
790 url_fclose(fh);
791 break;
792 }
793
795 {
796 //print("PlayerStats_PlayerDetail_Handler(): Got response from player stats server:\n");
797 string input = "";
798 string gametype = "overall";
799 while((input = url_fgets(fh)))
800 {
801 float count = tokenizebyseparator(input, " ");
802 string key = "", event = "", data = "";
803
804 if(argv(0) == "#") { continue; }
805
806 if(count == 2)
807 {
808 key = argv(0);
809 data = substring(input, argv_start_index(1), strlen(input) - argv_start_index(1));
810 }
811 else if(count >= 3)
812 {
813 key = argv(0);
814 event = argv(1);
815 data = substring(input, argv_start_index(2), strlen(input) - argv_start_index(2));
816 }
817 else { continue; }
818
819 switch(key)
820 {
821 // general info
822 case "V": PlayerStats_PlayerDetail_AddItem("version", data); break;
823 case "R": PlayerStats_PlayerDetail_AddItem("release", data); break;
824 case "T": PlayerStats_PlayerDetail_AddItem("time", data); break;
825
826 // player info
827 case "S": PlayerStats_PlayerDetail_AddItem("statsurl", data); break;
828 case "P": PlayerStats_PlayerDetail_AddItem("hashkey", data); break;
829 case "n": PlayerStats_PlayerDetail_AddItem("playernick", data); break;
830 case "i": PlayerStats_PlayerDetail_AddItem("playerid", data); break;
831
832 // other/event info
833 case "G": gametype = data; break;
834 case "e":
835 {
836 if(event != "" && data != "")
837 {
839 sprintf(
840 "%s/%s",
841 gametype,
842 event
843 ),
844 data
845 );
846 }
847 break;
848 }
849
850 default:
851 {
852 LOG_INFOF(
853 "PlayerStats_PlayerDetail_Handler(): ERROR: "
854 "Key went unhandled? Is our version outdated?\n"
855 "PlayerStats_PlayerDetail_Handler(): "
856 "Key '%s', Event '%s', Data '%s'",
857 key,
858 event,
859 data
860 );
861 break;
862 }
863 }
864
865 #if 0
866 LOG_INFOF(
867 "PlayerStats_PlayerDetail_Handler(): "
868 "Key '%s', Event '%s', Data '%s'",
869 key,
870 event,
871 data
872 );
873 #endif
874 }
875 //print("PlayerStats_PlayerDetail_Handler(): End of response.\n");
876 url_fclose(fh);
878 statslist.getStats(statslist);
879 break;
880 }
881
882 case URL_READY_CLOSED:
883 {
884 // url_fclose has finished
885 LOG_INFO("PlayerStats_PlayerDetail_Handler(): Player stats synchronized with server.");
886 break;
887 }
888
889 case URL_READY_ERROR:
890 default:
891 {
892 LOG_INFO("PlayerStats_PlayerDetail_Handler(): Receiving player stats failed: ", ftos(status));
894 if(PS_D_IN_DB >= 0)
895 {
897 PS_D_IN_DB = -1;
898 }
899 break;
900 }
901 }
902}
903#endif
904
905/*
906void PlayerInfo_AddPlayer(entity e)
907{
908 if(playerinfo_db < 0)
909 return;
910
911 string key;
912 key = sprintf("#%d:*", e.playerid); // TODO: use hashkey instead?
913
914 string p;
915 p = db_get(playerinfo_db, key);
916 if(p == "")
917 {
918 if(playerinfo_last)
919 {
920 db_put(playerinfo_db, key, playerinfo_last);
921 strunzone(playerinfo_last);
922 }
923 else
924 db_put(playerinfo_db, key, "#");
925 playerinfo_last = strzone(ftos(e.playerid));
926 print(" Added player ", ftos(e.playerid), " to playerinfo_db\n");//DEBUG//
927 }
928}
929
930void PlayerInfo_AddItem(entity e, string item_id, string val)
931{
932 if(playerinfo_db < 0)
933 return;
934
935 string key;
936 key = sprintf("*:%s", item_id);
937
938 string p;
939 p = db_get(playerinfo_db, key);
940 if(p == "")
941 {
942 if(playerinfo_events_last)
943 {
944 db_put(playerinfo_db, key, playerinfo_events_last);
945 strunzone(playerinfo_events_last);
946 }
947 else
948 db_put(playerinfo_db, key, "#");
949 playerinfo_events_last = strzone(item_id);
950 }
951
952 key = sprintf("#%d:%s", e.playerid, item_id);
953 db_put(playerinfo_db, key, val);
954 print(" Added item ", key, "=", val, " to playerinfo_db\n");//DEBUG//
955}
956
957string PlayerInfo_GetItem(entity e, string item_id)
958{
959 if(playerinfo_db < 0)
960 return "";
961
962 string key;
963 key = sprintf("#%d:%s", e.playerid, item_id);
964 return db_get(playerinfo_db, key);
965}
966
967string PlayerInfo_GetItemLocal(string item_id)
968{
969 entity p = spawn();
970 p.playerid = 0;
971 return PlayerInfo_GetItem(p, item_id);
972}
973
974void PlayerInfo_ready(entity fh, entity p, float status)
975{
976 float n;
977 string s;
978
979 PlayerInfo_AddPlayer(p);
980
981 switch(status)
982 {
983 case URL_READY_CANWRITE:
984 print("-- Sending data to player stats server\n");
985 url_fputs(fh, "V 1\n");
986#ifdef WATERMARK
987 url_fputs(fh, sprintf("R %s\n", WATERMARK));
988#endif
989#ifdef MENUQC
990 url_fputs(fh, sprintf("l %s\n", cvar_string("_menu_prvm_language"))); // language
991 url_fputs(fh, sprintf("c %s\n", cvar_string("_menu_prvm_country"))); // country
992 url_fputs(fh, sprintf("n %s\n", cvar_string("_cl_name"))); // name
993 url_fputs(fh, sprintf("m %s %s\n", cvar_string("_cl_playermodel"), cvar_string("_cl_playerskin"))); // model/skin
994#endif
995 url_fputs(fh, "\n");
996 url_fclose(fh);
997 break;
998 case URL_READY_CANREAD:
999 print("-- Got response from player stats server:\n");
1000 string gametype = string_null;
1001 while((s = url_fgets(fh)))
1002 {
1003 print(" ", s, "\n");
1004
1005 string key = "", value = "", data = "";
1006
1007 n = tokenizebyseparator(s, " "); // key (value) data
1008 if (n == 1)
1009 continue;
1010 else if (n == 2)
1011 {
1012 key = argv(0);
1013 data = argv(1);
1014 }
1015 else if (n >= 3)
1016 {
1017 key = argv(0);
1018 value = argv(1);
1019 data = argv(2);
1020 }
1021
1022 if (data == "")
1023 continue;
1024
1025 if (key == "#")
1026 continue;
1027 else if (key == "V")
1028 PlayerInfo_AddItem(p, "_version", data);
1029 else if (key == "R")
1030 PlayerInfo_AddItem(p, "_release", data);
1031 else if (key == "T")
1032 PlayerInfo_AddItem(p, "_time", data);
1033 else if (key == "S")
1034 PlayerInfo_AddItem(p, "_statsurl", data);
1035 else if (key == "P")
1036 PlayerInfo_AddItem(p, "_hashkey", data);
1037 else if (key == "n")
1038 PlayerInfo_AddItem(p, "_playernick", data);
1039 else if (key == "i")
1040 PlayerInfo_AddItem(p, "_playerid", data);
1041 else if (key == "G")
1042 gametype = data;
1043 else if (key == "e" && value != "")
1044 {
1045 if (gametype == "")
1046 PlayerInfo_AddItem(p, value, data);
1047 else
1048 PlayerInfo_AddItem(p, sprintf("%s/%s", gametype, value), data);
1049 }
1050 else
1051 continue;
1052 }
1053 print("-- End of response.\n");
1054 url_fclose(fh);
1055 break;
1056 case URL_READY_CLOSED:
1057 // url_fclose has finished
1058 print("Player stats synchronized with server\n");
1059 break;
1060 case URL_READY_ERROR:
1061 default:
1062 print("Receiving player stats failed: ", ftos(status), "\n");
1063 break;
1064 }
1065}
1066
1067void PlayerInfo_Init()
1068{
1069 playerinfo_db = db_create();
1070}
1071
1072#ifdef SVQC
1073void PlayerInfo_Basic(entity p)
1074{
1075 print("-- Getting basic PlayerInfo for player ",ftos(p.playerid)," (SVQC)\n");
1076
1077 if(playerinfo_db < 0)
1078 return;
1079
1080 string uri;
1081 uri = autocvar_g_playerinfo_uri;
1082 if(uri != "" && p.crypto_idfp != "")
1083 {
1084 uri = strcat(uri, "/elo/", uri_escape(p.crypto_idfp));
1085 print("Retrieving playerstats from URL: ", uri, "\n");
1086 url_single_fopen(uri, FILE_READ, PlayerInfo_ready, p);
1087 }
1088}
1089#endif
1090
1091#ifdef MENUQC
1092void PlayerInfo_Details()
1093{
1094 print("-- Getting detailed PlayerInfo for local player (MENUQC)\n");
1095
1096 if(playerinfo_db < 0)
1097 return;
1098
1099 string uri;
1100 uri = autocvar_g_playerinfo_uri; // FIXME
1101 if(uri != "" && crypto_getmyidstatus(0) > 0)
1102 {
1103 //uri = strcat(uri, "/player/", uri_escape(crypto_getmyidfp(0)));
1104 uri = strcat(uri, "/player/me");
1105 print("Retrieving playerstats from URL: ", uri, "\n");
1106 url_single_fopen(uri, FILE_APPEND, PlayerInfo_ready, NULL);
1107 }
1108}
1109#endif
1110
1111#ifdef CSQC
1112// FIXME - crypto_* builtin functions missing in CSQC (csprogsdefs.qh:885)
1113void PlayerInfo_Details()
1114{
1115 print("-- Getting detailed PlayerInfo for local player (CSQC)\n");
1116
1117 if(playerinfo_db < 0)
1118 return;
1119
1120 string uri;
1121 uri = autocvar_g_playerinfo_uri; // FIXME
1122 if(uri != "" && crypto_getmyidstatus(0) > 0)
1123 {
1124 uri = strcat(uri, "/player/", uri_escape(crypto_getmyidfp(0)));
1125 print("Retrieving playerstats from URL: ", uri, "\n");
1126 url_single_fopen(uri, FILE_READ, PlayerInfo_ready, p);
1127 }
1128}
1129
1130#endif
1131*/
float accuracy_fired[REGISTRY_MAX(Weapons)]
Definition accuracy.qh:31
float accuracy_frags[REGISTRY_MAX(Weapons)]
Definition accuracy.qh:27
float accuracy_cnt_hit[REGISTRY_MAX(Weapons)]
Definition accuracy.qh:32
float accuracy_hit[REGISTRY_MAX(Weapons)]
Definition accuracy.qh:30
float accuracy_cnt_fired[REGISTRY_MAX(Weapons)]
Definition accuracy.qh:33
float accuracy_real[REGISTRY_MAX(Weapons)]
Definition accuracy.qh:29
void anticheat_report_to_playerstats(entity this)
Definition anticheat.qc:221
void anticheat_register_to_playerstats()
Definition anticheat.qc:230
float skill
Definition api.qh:35
var entity(vector mins, vector maxs,.entity tofield) findbox_tofield_OrFallback
float count
Definition powerups.qc:22
bool warmup_stage
Definition main.qh:120
entity gametype
Definition main.qh:43
int serverflags
Definition main.qh:211
#define IS_PLAYER(s)
Definition player.qh:243
#define scores_label(this)
Definition scores.qh:146
#define MAX_TEAMSCORE
Definition scores.qh:149
#define teamscores_label(i)
Definition scores.qh:154
float game_starttime
Definition stats.qh:82
int rounds_played
Definition stats.qh:377
string playername(string thename, int teamid, bool team_colorize)
Definition util.qc:2082
const int SERVERFLAG_PLAYERSTATS
Definition constants.qh:18
const int SERVERFLAG_PLAYERSTATS_CUSTOM
Definition constants.qh:19
const float FILE_READ
float time
const float FILE_APPEND
#define strlen
#define tokenizebyseparator
#define argv_start_index
#define pass(name, colormin, colormax)
Weapons
Definition guide.qh:113
string GetMapname()
string GetGametype()
#define FOREACH(list, cond, body)
Definition iter.qh:19
ERASEABLE int json_parse(string in, bool() func)
Definition json.qc:230
bool _json_parse_array()
Definition json.qc:73
ERASEABLE string json_get(int buf, string key)
Definition json.qc:276
#define REPLICATE_APPLYCHANGE(var, ApplyChange_code)
Allows setting code that will be executed on cvar value changes.
Definition replicate.qh:36
#define REPLICATE(...)
Replicates a client cvar into a server field.
Definition replicate.qh:23
#define LOG_INFO(...)
Definition log.qh:65
#define LOG_DEBUG(...)
Definition log.qh:80
#define LOG_INFOF(...)
Definition log.qh:66
#define LOG_DEBUGF(...)
Definition log.qh:81
ERASEABLE void db_close(int db)
Definition map.qh:84
ERASEABLE int db_create()
Definition map.qh:25
ERASEABLE string db_get(int db, string key)
Definition map.qh:91
ERASEABLE void db_put(int db, string key, string value)
Definition map.qh:101
float stof(string val,...)
string substring(string s, float start, float length)
float cvar(string name)
entity find(entity start,.string field, string match)
const string cvar_string(string name)
float checkextension(string ext)
void strunzone(string s)
string ftos(float f)
const string cvar_defstring(string name)
string strzone(string s)
string argv(float n)
float max(float f,...)
strcat(_("^F4Countdown stopped!"), "\n^BG", _("Teams are too unbalanced."))
float PlayerStats_GameReport_Event(string prefix, string event_id, float value)
void PlayerStats_GameReport(bool finished)
void PlayerStats_PlayerBasic(entity joiningplayer, float newrequest)
void PlayerStats_GameReport_FinalizePlayer(entity p)
void PlayerStats_Prematch()
#define ACCMAC(suffix, field)
void PlayerStats_GameReport_Handler(entity fh, entity pass, float status)
void PlayerStats_PlayerBasic_CheckUpdate(entity joiningplayer)
void PlayerStats_PlayerBasic_Handler(entity fh, entity p, float status)
void PlayerStats_GameReport_Accuracy(entity p)
void PlayerStats_GameReport_AddPlayer(entity e)
void PlayerStats_GameReport_AddTeam(int t)
void PlayerStats_GameReport_Init()
void PlayerStats_GameReport_Reset_All()
void PlayerStats_PlayerDetail_CheckUpdate()
void PlayerStats_PlayerDetail()
void PlayerStats_PlayerDetail_AddItem(string event, string data)
void PlayerStats_PlayerDetail_Handler(entity fh, entity unused, float status)
void PlayerStats_GameReport_AddEvent(string event_id)
string PlayerStats_GetGametype()
const string PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD
float PS_B_IN_DB
Definition playerstats.qh:7
float scoreboard_pos
const string PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM
const string PLAYERSTATS_MATCHES
const string PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_25
const string PLAYERSTATS_HANDICAP_TAKEN
const string PLAYERSTATS_WINS
const string PLAYERSTATS_ACHIEVEMENT_BOTLIKE
float PlayerStats_PlayerDetail_Status
float PS_D_LASTGAMECOUNT
const string PLAYERSTATS_JOINS
float PS_D_NEXTUPDATETIME
const string PLAYERSTATS_SCOREBOARD
const float PS_B_STATUS_RECEIVED
string PS_D_IN_EVL
string autocvar_g_playerstats_gamereport_uri
const float PS_B_STATUS_UPDATING
const string PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_20
const float PS_D_STATUS_WAITING
const float PS_D_STATUS_RECEIVED
string autocvar_g_playerstats_playerdetail_uri
const float PS_B_STATUS_WAITING
const string PLAYERSTATS_SCOREBOARD_VALID
string autocvar_g_playerstats_playerbasic_uri
string playerstats_id
const string PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_3
const string PLAYERSTATS_HANDICAP_GIVEN
const string PLAYERSTATS_TOTAL
const string PLAYERSTATS_RANK
const float PS_D_STATUS_ERROR
const string PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_30
int PS_D_IN_DB
const string PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_10
float autocvar_g_playerstats_playerdetail_autoupdatetime
string autocvar_g_playerstats_gamereport_ladder
const string PLAYERSTATS_AVGLATENCY
string PS_GR_OUT_EVL
const string PLAYERSTATS_ALIVETIME
const string PLAYERSTATS_SCOREBOARD_POS
string PS_GR_OUT_PL
const float PS_B_STATUS_IDLE
#define PlayerStats_GameReport_Event_Player(ent, eventid, val)
string PS_GR_OUT_TL
bool PlayerStats_GameReport_DelayMapVote
const float PS_D_STATUS_IDLE
const string PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_15
const string PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_5
int PS_GR_OUT_DB
Definition playerstats.qh:5
#define NULL
Definition post.qh:14
entity teamscorekeepers[16]
Definition scores.qc:20
float score_dummyfield
Definition scores.qc:916
void PlayerScore_PlayerStats(entity p)
Definition scores.qc:950
entity PlayerScore_Sort(.float field, int teams, bool strict, bool nospectators)
Sorts the players and stores their place in the given field, starting with.
Definition scores.qc:748
void PlayerScore_TeamStats()
Definition scores.qc:960
int autocvar_g_maxplayers
Definition client.qh:44
#define CS_CVAR(this)
Definition state.qh:51
ClientState CS(Client this)
Definition state.qh:47
#define SHUTDOWN(func)
before shutdown
Definition static.qh:49
entity statslist
Definition statslist.qh:23
#define strfree(this)
Definition string.qh:59
#define GameRules_scoring_add(client, fld, value)
Definition sv_rules.qh:85
bool teamplay
Definition teams.qh:59
#define EXPECT_NE(val1, val2)
Definition test.qh:52
ERASEABLE string url_fgets(entity e)
Definition urllib.qc:287
ERASEABLE void url_multi_fopen(string url, int mode, url_ready_func rdy, entity pass)
Definition urllib.qc:364
ERASEABLE void url_fputs(entity e, string s)
Definition urllib.qc:312
ERASEABLE void url_single_fopen(string url, int mode, url_ready_func rdy, entity pass)
Definition urllib.qc:87
ERASEABLE void url_fclose(entity e)
Definition urllib.qc:207
const float URL_READY_CLOSED
Definition urllib.qh:15
const float URL_READY_ERROR
Definition urllib.qh:14
const float URL_READY_CANREAD
Definition urllib.qh:17
const float URL_READY_CANWRITE
Definition urllib.qh:16
#define IS_REAL_CLIENT(v)
Definition utils.qh:17
#define FOREACH_CLIENT(cond, body)
Definition utils.qh:50
#define IS_BOT_CLIENT(v)
want: (IS_CLIENT(v) && !IS_REAL_CLIENT(v))
Definition utils.qh:15
float latency_cnt
Definition world.qc:55
float latency_sum
Definition world.qc:54
string matchid
Definition world.qh:63
float cvar_purechanges_count
Definition world.qh:47
string modname
Definition world.qh:49