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
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 || (autocvar_g_playerstats_gamereport_uri == cvar_defstring("g_playerstats_gamereport_uri") && cvar("sv_public") <= 0))
273 {
276 PS_GR_OUT_DB = -1;
277 return;
278 }
279
285 NULL
286 );
287}
288
289void PlayerStats_GameReport_Init() // initiated before InitGameplayMode so that scores are added properly
290{
293
294 if(autocvar_g_playerstats_gamereport_uri == "") { return; }
295
297
298 if(PS_GR_OUT_DB >= 0)
299 {
301
302 if(autocvar_g_playerstats_gamereport_uri != cvar_defstring("g_playerstats_gamereport_uri"))
304 else if(checkextension("DP_CRYPTO") && checkextension("DP_QC_URI_POST") && cvar("sv_public") > 0)
305 // XonStat requires player and server IDs, HTTPS POST, and shouldn't publish local/listen games
307
318
319 // accuracy stats
320 FOREACH(Weapons, it != WEP_Null, {
321 PlayerStats_GameReport_AddEvent(strcat("acc-", it.netname, "-real"));
322 PlayerStats_GameReport_AddEvent(strcat("acc-", it.netname, "-hit"));
323 PlayerStats_GameReport_AddEvent(strcat("acc-", it.netname, "-fired"));
324 PlayerStats_GameReport_AddEvent(strcat("acc-", it.netname, "-cnt-hit"));
325 PlayerStats_GameReport_AddEvent(strcat("acc-", it.netname, "-cnt-fired"));
326 PlayerStats_GameReport_AddEvent(strcat("acc-", it.netname, "-frags"));
327 });
328
339
341 }
343}
344
345// this... is a hack, a temporary one until we get a proper duel gametype
346// TODO: remove duel hack after servers have migrated to the proper duel gametype!
348{
349 if(IS_GAMETYPE(DEATHMATCH) && autocvar_g_maxplayers == 2)
350 {
351 // probably duel, but let's make sure
352 int plcount = 0;
353 FOREACH_CLIENT(IS_PLAYER(it), ++plcount);
354 if(plcount <= 2)
355 return "duel";
356 }
357 return GetGametype();
358}
359
361{
362 string t, tn;
363 string p, pn;
364 string e, en;
365 string nn, tt;
366 string s;
367
368 switch(status)
369 {
370 // ======================================
371 // -- OUTGOING GAME REPORT INFORMATION --
372 // ======================================
373 /* SPECIFICATIONS:
374 * V: format version (always a fixed number) - this MUST be the first line!
375 * #: comment (MUST be ignored by any parser)
376 * R: release information on the server
377 * G: gametype
378 * O: mod name (icon request) as in server browser
379 * M: map name
380 * I: match ID (see "matchid" in world.qc)
381 * S: "hostname" of the server
382 * C: number of "unpure" cvar changes
383 * U: UDP port number of the server
384 * D: duration of the match
385 * RP: number of rounds played
386 * L: "ladder" in which the server is participating in
387 * P: player ID of an existing player; this also sets the owner for all following "n", "e" and "t" lines (lower case!)
388 * Q: team number of an existing team (format: team#NN); this also sets the owner for all following "e" lines (lower case!)
389 * i: player index
390 * n: nickname of the player (optional)
391 * t: team ID
392 * r: player ranking enabled / disabled
393 * e: followed by an event name, a space, and the event count/score
394 * event names can be:
395 * alivetime: total playing time of the player
396 * avglatency: average network latency compounded throughout the match
397 * wins: number of games won (can only be set if matches is set)
398 * matches: number of matches played to the end (not aborted by map switch)
399 * joins: number of matches joined (always 1 unless player never played during the match)
400 * scoreboardvalid: set to 1 if the player was there at the end of the match
401 * total-<scoreboardname>: total score of that scoreboard item
402 * scoreboard-<scoreboardname>: end-of-game score of that scoreboard item (can differ in non-team games)
403 * achievement-<achievementname>: achievement counters (their "count" is usually 1 if nonzero at all)
404 * kills-<index>: number of kills against the indexed player
405 * rank <number>: rank of player
406 * handicapgiven: average handicap on given (dealt) damage throughout the match
407 * handicaptaken: average handicap on taken (received) damage throughout the match
408 * acc-<weapon netname>-real: total damage dealt excluding excess after death
409 * acc-<weapon netname>-hit: total damage dealt including excess after death
410 * acc-<weapon netname>-fired: total damage that all fired shots could have dealt
411 * acc-<weapon netname>-cnt-hit: amount of shots that actually hit
412 * acc-<weapon netname>-cnt-fired: amount of fired shots
413 * acc-<weapon netname>-frags: amount of frags dealt by weapon
414 */
416 {
417 url_fputs(fh, "V 9\n");
418 #ifdef WATERMARK
419 url_fputs(fh, sprintf("R %s\n", WATERMARK));
420 #endif
421 url_fputs(fh, sprintf("G %s\n", PlayerStats_GetGametype()));
422 url_fputs(fh, sprintf("O %s\n", modname));
423 url_fputs(fh, sprintf("M %s\n", GetMapname()));
424 url_fputs(fh, sprintf("I %s\n", matchid));
425 url_fputs(fh, sprintf("S %s\n", cvar_string("hostname")));
426 url_fputs(fh, sprintf("C %d\n", cvar_purechanges_count));
427 url_fputs(fh, sprintf("U %d\n", cvar("port")));
428 url_fputs(fh, sprintf("D %f\n", max(0, time - game_starttime)));
429 if (rounds_played > 0)
430 url_fputs(fh, sprintf("RP %d\n", rounds_played));
433
434 // TEAMS
435 if(teamplay)
436 {
437 for(t = PS_GR_OUT_TL; (tn = db_get(PS_GR_OUT_DB, sprintf("%d", stof(t)))) != ""; t = tn)
438 {
439 // start team section
440 url_fputs(fh, sprintf("Q team#%s\n", t));
441
442 // output team events // todo: does this do unnecessary loops? perhaps we should do a separate "team_events_last" tracker..."
443 for(e = PS_GR_OUT_EVL; (en = db_get(PS_GR_OUT_DB, sprintf("*:%s", e))) != ""; e = en)
444 {
445 float v = stof(db_get(PS_GR_OUT_DB, sprintf("team#%d:%s", stof(t), e)));
446 if(v != 0) { url_fputs(fh, sprintf("e %s %g\n", e, v)); }
447 }
448 }
449 }
450
451 // PLAYERS
452 for(p = PS_GR_OUT_PL; (pn = db_get(PS_GR_OUT_DB, sprintf("%s:*", p))) != ""; p = pn)
453 {
454 // start player section
455 url_fputs(fh, sprintf("P %s\n", p));
456
457 // playerid/index (entity number for this server)
458 nn = db_get(PS_GR_OUT_DB, sprintf("%s:_playerid", p));
459 if(nn != "") { url_fputs(fh, sprintf("i %s\n", nn)); }
460
461 // player name
462 nn = db_get(PS_GR_OUT_DB, sprintf("%s:_netname", p));
463 if(nn != "") { url_fputs(fh, sprintf("n %s\n", nn)); }
464
465 // team identification number
466 if(teamplay)
467 {
468 tt = db_get(PS_GR_OUT_DB, sprintf("%s:_team", p));
469 url_fputs(fh, sprintf("t %s\n", tt));
470 }
471
472 // skill ranking enabled
473 nn = db_get(PS_GR_OUT_DB, sprintf("%s:_ranked", p));
474 if(nn != "") { url_fputs(fh, sprintf("r %s\n", nn)); }
475
476 // output player events
477 for(e = PS_GR_OUT_EVL; (en = db_get(PS_GR_OUT_DB, sprintf("*:%s", e))) != ""; e = en)
478 {
479 float v = stof(db_get(PS_GR_OUT_DB, sprintf("%s:%s", p, e)));
480 if(v != 0) { url_fputs(fh, sprintf("e %s %g\n", e, v)); }
481 }
482 }
483 url_fputs(fh, "\n");
484 url_fclose(fh);
485 break;
486 }
487
488 // ======================================
489 // -- INCOMING GAME REPORT INFORMATION --
490 // ======================================
491 /* SPECIFICATIONS:
492 * stuff
493 */
495 {
496 // url_fclose is processing, we got a response for writing the data
497 // this must come from HTTP
498 LOG_DEBUG("Got response from player stats server:");
499 while((s = url_fgets(fh))) { LOG_DEBUG(" ", s); }
500 LOG_DEBUG("End of response.");
501 url_fclose(fh);
502 break;
503 }
504
505 case URL_READY_CLOSED:
506 {
507 // url_fclose has finished
508 LOG_DEBUG("Player stats written");
510 if(PS_GR_OUT_DB >= 0)
511 {
513 PS_GR_OUT_DB = -1;
514 }
515 break;
516 }
517
518 case URL_READY_ERROR:
519 default:
520 {
521 LOG_INFO("Player stats writing failed: ", ftos(status));
523 if(PS_GR_OUT_DB >= 0)
524 {
526 PS_GR_OUT_DB = -1;
527 }
528 break;
529 }
530 }
531}
532
533void PlayerStats_PlayerBasic(entity joiningplayer, float newrequest)
534{
535 GameRules_scoring_add(joiningplayer, SKILL, -1);
536 // http://stats.xonotic.org/skill?hashkey=Cm5mzXa1Vn9VX0%2FySV4Pp8ca9CTZvklERDqrrNOUFZU%3D
539 {
541 if (joiningplayer.crypto_idfp == "")
542 GameRules_scoring_add(joiningplayer, SKILL, -1);
543 else
544 {
545 // create the database if it doesn't already exist
546 if(PS_B_IN_DB < 0)
548
549 // now request the information
550 uri = strcat(uri, "/skill?game_type_cd=", uri_escape(PlayerStats_GetGametype()));
551 uri = strcat(uri, "&hashkey=", uri_escape(joiningplayer.crypto_idfp));
552 LOG_DEBUG("Retrieving playerstats from URL: ", uri);
554 uri,
555 FILE_READ,
557 joiningplayer
558 );
559
560 // set status appropriately // todo: check whether the player info exists in the database previously
561 if(newrequest)
562 {
563 // database still contains useful information, so don't clear it of a useful status
564 joiningplayer.playerstats_basicstatus = PS_B_STATUS_WAITING;
565 }
566 else
567 {
568 // database was previously empty or never hit received status for some reason
569 joiningplayer.playerstats_basicstatus = PS_B_STATUS_UPDATING;
570 }
571 }
572 }
573 else
574 {
575 // server has this disabled, kill the DB and set status to idle
576 GameRules_scoring_add(joiningplayer, SKILL, -1);
577 if(PS_B_IN_DB >= 0)
578 {
580 PS_B_IN_DB = -1;
581
582 FOREACH_CLIENT(IS_REAL_CLIENT(it), it.playerstats_basicstatus = PS_B_STATUS_IDLE);
583 }
584 }
585}
586
587SHUTDOWN(PlayerStats_PlayerBasic_Shutdown)
588{
589 if(PS_B_IN_DB >= 0)
590 {
592 PS_B_IN_DB = -1;
593 }
594
595 if(PS_GR_OUT_DB >= 0)
596 {
598 PS_GR_OUT_DB = -1;
599 }
600}
601
603{
604 // determine whether we should retrieve playerbasic information again
605
606 LOG_DEBUGF("PlayerStats_PlayerBasic_CheckUpdate('%s'): %f",
607 joiningplayer.netname,
608 time
609 );
610
611 // TODO: check to see if this playerid is inside the database already somehow...
612 // for now we'll just check the field, but this won't work for players who disconnect and reconnect properly
613 // although maybe we should just submit another request ANYWAY?
614 if(!joiningplayer.playerstats_basicstatus)
615 {
617 joiningplayer,
618 (joiningplayer.playerstats_basicstatus == PS_B_STATUS_RECEIVED)
619 );
620 }
621}
622
624{
625 switch(status)
626 {
628 {
629 // THIS should be never called
630 LOG_DEBUG("-- Sending data to player stats server");
631 url_fputs(fh, "\n");
632 url_fclose(fh);
633 return;
634 }
635
637 {
638 string temp = "";
639 for (string s; (s = url_fgets(fh)); )
640 {
641 if (temp != "")
642 temp = strcat(temp, "\n", s);
643 else
644 temp = s;
645 }
646 url_fclose(fh);
647 int buf = json_parse(temp, _json_parse_array);
648 EXPECT_NE(-1, buf);
650 if (json_get(buf, "0.game_type_cd") == gametype && gametype != "")
651 {
652 p.m_skill_var = stof(json_get(buf, "0.sigma")) ** 2;
653 p.m_skill_mu = max(0, stof(json_get(buf, "0.mu"))); // can it be < 0 ?!
654 /* we display just the mean skill, since this most accurately represents how players will play on an average match.
655 * displaying mu - 3 * sigma would display worst-case performance, but when applied to every player this becomes meaningless.
656 * also we're not interested in the skill that we're confident players can operate above, rather where we expect them to operate.
657 */
658 GameRules_scoring_add(p, SKILL, p.m_skill_mu + 1);
659 return;
660 }
661 break;
662 }
663 case URL_READY_CLOSED:
664 {
665 // url_fclose has finished
666 LOG_INFO("Player stats synchronized with server");
667 return;
668 }
669
670 case URL_READY_ERROR:
671 default:
672 {
673 LOG_INFO("Receiving player stats failed: ", ftos(status));
674 break;
675 }
676 }
677 GameRules_scoring_add(p, SKILL, -1);
678}
679#endif // SVQC
680
681#ifdef MENUQC
682
683
684#if 0 // reading the entire DB at once
685 string e = "", en = "";
686 float i = 0;
687 for(e = PS_D_IN_EVL; (en = db_get(PS_D_IN_DB, e)) != ""; e = en)
688 {
689 LOG_INFOF("%d:%s:%s", i, e, db_get(PS_D_IN_DB, sprintf("#%s", e)));
690 ++i;
691 }
692#endif
693
694void PlayerStats_PlayerDetail_AddItem(string event, string data)
695{
696 if(PS_D_IN_DB < 0) { return; }
697
698 // create a marker for the event so that we can access it later
699 string marker = sprintf("%s", event);
700 if(db_get(PS_D_IN_DB, marker) == "")
701 {
702 if(PS_D_IN_EVL)
703 {
704 db_put(PS_D_IN_DB, marker, PS_D_IN_EVL);
706 }
707 else { db_put(PS_D_IN_DB, marker, "#"); }
708 PS_D_IN_EVL = strzone(marker);
709 }
710
711 // now actually set the event data
712 db_put(PS_D_IN_DB, sprintf("#%s", event), data);
713 LOG_DEBUG("Added item ", sprintf("#%s", event), "=", data, " to PS_D_IN_DB");
714}
715
717{
718 // http://stats.xonotic.org/player/me
719 if((autocvar_g_playerstats_playerdetail_uri != "") && (crypto_getmyidstatus(0) > 0))
720 {
721 // create the database if it doesn't already exist
722 if(PS_D_IN_DB < 0)
724
725 //uri = strcat(uri, "/player/", uri_escape(crypto_getmyidfp(0)));
726 LOG_DEBUG("Retrieving playerstats from URL: ", autocvar_g_playerstats_playerdetail_uri);
731 NULL
732 );
733
735 }
736 else
737 {
738 // player has this disabled, kill the DB and set status to idle
739 if(PS_D_IN_DB >= 0)
740 {
742 PS_D_IN_DB = -1;
743 }
744
746 }
747}
748
750{
751 // determine whether we should retrieve playerdetail information again
752 float gamecount = cvar("cl_matchcount");
753
754 #if 0
755 LOG_INFOF("PlayerStats_PlayerDetail_CheckUpdate(): %f >= %f, %d > %d",
756 time,
759 gamecount
760 );
761 #endif
762
763 if(
765 ||
766 (gamecount > PS_D_LASTGAMECOUNT)
767 )
768 {
771 PS_D_LASTGAMECOUNT = gamecount;
772 }
773}
774
775void PlayerStats_PlayerDetail_Handler(entity fh, entity unused, float status)
776{
777 switch(status)
778 {
780 {
781 LOG_DEBUG("PlayerStats_PlayerDetail_Handler(): Sending data to player stats server...");
782 url_fputs(fh, "V 1\n");
783 #ifdef WATERMARK
784 url_fputs(fh, sprintf("R %s\n", WATERMARK));
785 #endif
786 url_fputs(fh, sprintf("l %s\n", cvar_string("_menu_prvm_language"))); // language
787 //url_fputs(fh, sprintf("c %s\n", cvar_string("_cl_country"))); // country
788 url_fputs(fh, sprintf("n %s\n", cvar_string("_cl_name"))); // name
789 url_fputs(fh, sprintf("m %s %s\n", cvar_string("_cl_playermodel"), cvar_string("_cl_playerskin"))); // model/skin
790 url_fputs(fh, "\n");
791 url_fclose(fh);
792 break;
793 }
794
796 {
797 //print("PlayerStats_PlayerDetail_Handler(): Got response from player stats server:\n");
798 string input = "";
799 string gametype = "overall";
800 while((input = url_fgets(fh)))
801 {
802 float count = tokenizebyseparator(input, " ");
803 string key = "", event = "", data = "";
804
805 if(argv(0) == "#") { continue; }
806
807 if(count == 2)
808 {
809 key = argv(0);
810 data = substring(input, argv_start_index(1), strlen(input) - argv_start_index(1));
811 }
812 else if(count >= 3)
813 {
814 key = argv(0);
815 event = argv(1);
816 data = substring(input, argv_start_index(2), strlen(input) - argv_start_index(2));
817 }
818 else { continue; }
819
820 switch(key)
821 {
822 // general info
823 case "V": PlayerStats_PlayerDetail_AddItem("version", data); break;
824 case "R": PlayerStats_PlayerDetail_AddItem("release", data); break;
825 case "T": PlayerStats_PlayerDetail_AddItem("time", data); break;
826
827 // player info
828 case "S": PlayerStats_PlayerDetail_AddItem("statsurl", data); break;
829 case "P": PlayerStats_PlayerDetail_AddItem("hashkey", data); break;
830 case "n": PlayerStats_PlayerDetail_AddItem("playernick", data); break;
831 case "i": PlayerStats_PlayerDetail_AddItem("playerid", data); break;
832
833 // other/event info
834 case "G": gametype = data; break;
835 case "e":
836 {
837 if(event != "" && data != "")
838 {
840 sprintf(
841 "%s/%s",
842 gametype,
843 event
844 ),
845 data
846 );
847 }
848 break;
849 }
850
851 default:
852 {
853 LOG_INFOF(
854 "PlayerStats_PlayerDetail_Handler(): ERROR: "
855 "Key went unhandled? Is our version outdated?\n"
856 "PlayerStats_PlayerDetail_Handler(): "
857 "Key '%s', Event '%s', Data '%s'",
858 key,
859 event,
860 data
861 );
862 break;
863 }
864 }
865
866 #if 0
867 LOG_INFOF(
868 "PlayerStats_PlayerDetail_Handler(): "
869 "Key '%s', Event '%s', Data '%s'",
870 key,
871 event,
872 data
873 );
874 #endif
875 }
876 //print("PlayerStats_PlayerDetail_Handler(): End of response.\n");
877 url_fclose(fh);
879 statslist.getStats(statslist);
880 break;
881 }
882
883 case URL_READY_CLOSED:
884 {
885 // url_fclose has finished
886 LOG_INFO("PlayerStats_PlayerDetail_Handler(): Player stats synchronized with server.");
887 break;
888 }
889
890 case URL_READY_ERROR:
891 default:
892 {
893 LOG_INFO("PlayerStats_PlayerDetail_Handler(): Receiving player stats failed: ", ftos(status));
895 if(PS_D_IN_DB >= 0)
896 {
898 PS_D_IN_DB = -1;
899 }
900 break;
901 }
902 }
903}
904#endif
905
906/*
907void PlayerInfo_AddPlayer(entity e)
908{
909 if(playerinfo_db < 0)
910 return;
911
912 string key = sprintf("#%d:*", e.playerid); // TODO: use hashkey instead?
913
914 string p = db_get(playerinfo_db, key);
915 if(p == "")
916 {
917 if(playerinfo_last)
918 {
919 db_put(playerinfo_db, key, playerinfo_last);
920 strunzone(playerinfo_last);
921 }
922 else
923 db_put(playerinfo_db, key, "#");
924 playerinfo_last = strzone(ftos(e.playerid));
925 print(" Added player ", ftos(e.playerid), " to playerinfo_db\n");//DEBUG//
926 }
927}
928
929void PlayerInfo_AddItem(entity e, string item_id, string val)
930{
931 if(playerinfo_db < 0)
932 return;
933
934 string key = sprintf("*:%s", item_id);
935
936 string p = db_get(playerinfo_db, key);
937 if(p == "")
938 {
939 if(playerinfo_events_last)
940 {
941 db_put(playerinfo_db, key, playerinfo_events_last);
942 strunzone(playerinfo_events_last);
943 }
944 else
945 db_put(playerinfo_db, key, "#");
946 playerinfo_events_last = strzone(item_id);
947 }
948
949 key = sprintf("#%d:%s", e.playerid, item_id);
950 db_put(playerinfo_db, key, val);
951 print(" Added item ", key, "=", val, " to playerinfo_db\n");//DEBUG//
952}
953
954string PlayerInfo_GetItem(entity e, string item_id)
955{
956 if(playerinfo_db < 0)
957 return "";
958
959 string key = sprintf("#%d:%s", e.playerid, item_id);
960 return db_get(playerinfo_db, key);
961}
962
963string PlayerInfo_GetItemLocal(string item_id)
964{
965 entity p = spawn();
966 p.playerid = 0;
967 return PlayerInfo_GetItem(p, item_id);
968}
969
970void PlayerInfo_ready(entity fh, entity p, float status)
971{
972 float n;
973 string s;
974
975 PlayerInfo_AddPlayer(p);
976
977 switch(status)
978 {
979 case URL_READY_CANWRITE:
980 print("-- Sending data to player stats server\n");
981 url_fputs(fh, "V 1\n");
982#ifdef WATERMARK
983 url_fputs(fh, sprintf("R %s\n", WATERMARK));
984#endif
985#ifdef MENUQC
986 url_fputs(fh, sprintf("l %s\n", cvar_string("_menu_prvm_language"))); // language
987 url_fputs(fh, sprintf("c %s\n", cvar_string("_menu_prvm_country"))); // country
988 url_fputs(fh, sprintf("n %s\n", cvar_string("_cl_name"))); // name
989 url_fputs(fh, sprintf("m %s %s\n", cvar_string("_cl_playermodel"), cvar_string("_cl_playerskin"))); // model/skin
990#endif
991 url_fputs(fh, "\n");
992 url_fclose(fh);
993 break;
994 case URL_READY_CANREAD:
995 print("-- Got response from player stats server:\n");
996 string gametype = string_null;
997 while((s = url_fgets(fh)))
998 {
999 print(" ", s, "\n");
1000
1001 string key = "", value = "", data = "";
1002
1003 n = tokenizebyseparator(s, " "); // key (value) data
1004 if (n == 1)
1005 continue;
1006 else if (n == 2)
1007 {
1008 key = argv(0);
1009 data = argv(1);
1010 }
1011 else if (n >= 3)
1012 {
1013 key = argv(0);
1014 value = argv(1);
1015 data = argv(2);
1016 }
1017
1018 if (data == "")
1019 continue;
1020
1021 if (key == "#")
1022 continue;
1023 else if (key == "V")
1024 PlayerInfo_AddItem(p, "_version", data);
1025 else if (key == "R")
1026 PlayerInfo_AddItem(p, "_release", data);
1027 else if (key == "T")
1028 PlayerInfo_AddItem(p, "_time", data);
1029 else if (key == "S")
1030 PlayerInfo_AddItem(p, "_statsurl", data);
1031 else if (key == "P")
1032 PlayerInfo_AddItem(p, "_hashkey", data);
1033 else if (key == "n")
1034 PlayerInfo_AddItem(p, "_playernick", data);
1035 else if (key == "i")
1036 PlayerInfo_AddItem(p, "_playerid", data);
1037 else if (key == "G")
1038 gametype = data;
1039 else if (key == "e" && value != "")
1040 {
1041 if (gametype == "")
1042 PlayerInfo_AddItem(p, value, data);
1043 else
1044 PlayerInfo_AddItem(p, sprintf("%s/%s", gametype, value), data);
1045 }
1046 else
1047 continue;
1048 }
1049 print("-- End of response.\n");
1050 url_fclose(fh);
1051 break;
1052 case URL_READY_CLOSED:
1053 // url_fclose has finished
1054 print("Player stats synchronized with server\n");
1055 break;
1056 case URL_READY_ERROR:
1057 default:
1058 print("Receiving player stats failed: ", ftos(status), "\n");
1059 break;
1060 }
1061}
1062
1063void PlayerInfo_Init()
1064{
1065 playerinfo_db = db_create();
1066}
1067
1068#ifdef SVQC
1069void PlayerInfo_Basic(entity p)
1070{
1071 print("-- Getting basic PlayerInfo for player ",ftos(p.playerid)," (SVQC)\n");
1072
1073 if(playerinfo_db < 0)
1074 return;
1075
1076 string uri = autocvar_g_playerinfo_uri;
1077 if(uri != "" && p.crypto_idfp != "")
1078 {
1079 uri = strcat(uri, "/elo/", uri_escape(p.crypto_idfp));
1080 print("Retrieving playerstats from URL: ", uri, "\n");
1081 url_single_fopen(uri, FILE_READ, PlayerInfo_ready, p);
1082 }
1083}
1084#endif
1085
1086#ifdef MENUQC
1087void PlayerInfo_Details()
1088{
1089 print("-- Getting detailed PlayerInfo for local player (MENUQC)\n");
1090
1091 if(playerinfo_db < 0)
1092 return;
1093
1094 string uri = autocvar_g_playerinfo_uri; // FIXME
1095 if(uri != "" && crypto_getmyidstatus(0) > 0)
1096 {
1097 //uri = strcat(uri, "/player/", uri_escape(crypto_getmyidfp(0)));
1098 uri = strcat(uri, "/player/me");
1099 print("Retrieving playerstats from URL: ", uri, "\n");
1100 url_single_fopen(uri, FILE_APPEND, PlayerInfo_ready, NULL);
1101 }
1102}
1103#endif
1104
1105#ifdef CSQC
1106// FIXME - crypto_* builtin functions missing in CSQC (csprogsdefs.qh:885)
1107void PlayerInfo_Details()
1108{
1109 print("-- Getting detailed PlayerInfo for local player (CSQC)\n");
1110
1111 if(playerinfo_db < 0)
1112 return;
1113
1114 string uri = autocvar_g_playerinfo_uri; // FIXME
1115 if(uri != "" && crypto_getmyidstatus(0) > 0)
1116 {
1117 uri = strcat(uri, "/player/", uri_escape(crypto_getmyidfp(0)));
1118 print("Retrieving playerstats from URL: ", uri, "\n");
1119 url_single_fopen(uri, FILE_READ, PlayerInfo_ready, p);
1120 }
1121}
1122
1123#endif
1124*/
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:220
void anticheat_register_to_playerstats()
Definition anticheat.qc:229
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:242
#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:379
string playername(string thename, int teamid, bool team_colorize)
Definition util.qc:2180
const int SERVERFLAG_PLAYERSTATS
Definition constants.qh:19
const int SERVERFLAG_PLAYERSTATS_CUSTOM
Definition constants.qh:20
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:266
ERASEABLE bool _json_parse_array()
Definition json.qc:71
ERASEABLE string json_get(int buf, string key)
Definition json.qc:323
#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:62
#define LOG_DEBUG(...)
Definition log.qh:78
#define LOG_INFOF(...)
Definition log.qh:63
#define LOG_DEBUGF(...)
Definition log.qh:79
ERASEABLE void db_close(int db)
Definition map.qh:86
ERASEABLE int db_create()
Definition map.qh:25
ERASEABLE string db_get(int db, string key)
Definition map.qh:93
ERASEABLE void db_put(int db, string key, string value)
Definition map.qh:103
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:913
void PlayerScore_PlayerStats(entity p)
Definition scores.qc:947
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:745
void PlayerScore_TeamStats()
Definition scores.qc:957
int autocvar_sv_showskill
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:50
entity statslist
Definition statslist.qh:26
#define strfree(this)
Definition string.qh:57
#define GameRules_scoring_add(client, fld, value)
Definition sv_rules.qh:85
int autocvar_g_balance_teams_skill
Definition teamplay.qh:13
bool teamplay
Definition teams.qh:59
#define EXPECT_NE(val1, val2)
Definition test.qh:53
ERASEABLE string url_fgets(entity e)
Definition urllib.qc:291
ERASEABLE void url_multi_fopen(string url, int mode, url_ready_func rdy, entity pass)
Definition urllib.qc:360
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:90
ERASEABLE void url_fclose(entity e)
Definition urllib.qc:209
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:52
#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