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