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