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