]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/playerstats.qc
Consistency-- activator functions should have no other suffix.
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / playerstats.qc
1 #ifdef SVQC
2 //float PS_PM_IN_DB;   // playerstats_prematch_in_db      // db for info COLLECTED at the beginning of a match
3 float PS_GR_OUT_DB;  // playerstats_gamereport_out_db   // db of info SENT at the end of a match
4 //float PS_GR_IN_DB;   // playerstats_gamereport_in_db    // db for info COLLECTED at the end of a match
5 //float PS_B_IN_DB;    // playerstats_playerbasic_in_db   // db for info COLLECTED for basic player info (ELO)
6 // http://stats.xonotic.org/player/GgXRw6piDtFIbMArMuiAi8JG4tiin8VLjZgsKB60Uds=/elo.txt
7 #endif
8
9 #ifdef MENUQC
10 //float PS_D_IN_DB; // playerstats_playerdetail_in_db  // db for info COLLECTED for detailed player profile display
11 // http://stats.xonotic.org/player/me
12 #endif
13
14 #ifdef SVQC
15 //string PS_PM_IN_EVL;   // playerstats_prematch_in_events_last
16 string PS_GR_OUT_TL;   // playerstats_gamereport_out_teams_last
17 string PS_GR_OUT_PL;   // playerstats_gamereport_out_players_las
18 string PS_GR_OUT_EVL;  // playerstats_gamereport_out_events_last
19 //string PS_GR_IN_PL;    // playerstats_gamereport_in_players_last
20 //string PS_GR_IN_EVL;   // playerstats_gamereport_in_events_last
21 //string PS_B_IN_PL;     // playerstats_playerbasic_in_players_las
22 //string PS_B_IN_EVL;    // playerstats_playerbasic_in_events_last
23 #endif
24
25 #ifdef MENUQC
26 //string PS_D_IN_EVL; // playerstats_playerdetail_in_events_last
27 #endif
28
29 #ifdef SVQC
30 void PlayerStats_Prematch(void)
31 {
32         //foobar
33 }
34
35 void PlayerStats_GameReport_AddPlayer(entity e)
36 {
37         if((PS_GR_OUT_DB < 0) || (e.playerstats_id)) { return; }
38
39         // set up player identification
40         string s = string_null;
41
42         if((e.crypto_idfp != "") && (e.cvar_cl_allow_uidtracking == 1))
43                 { s = e.crypto_idfp; }
44         else if(IS_BOT_CLIENT(e))
45                 { s = sprintf("bot#%g#%s", skill, e.cleanname); }
46                 
47         if((s == "") || find(world, playerstats_id, s)) // already have one of the ID - next one can't be tracked then!
48         {
49                 if(IS_BOT_CLIENT(e))
50                         { s = sprintf("bot#%d", e.playerid); }
51                 else
52                         { s = sprintf("player#%d", e.playerid); }
53         }
54         
55         e.playerstats_id = strzone(s);
56
57         // now add the player to the database
58         string key = sprintf("%s:*", e.playerstats_id);
59         string p = db_get(PS_GR_OUT_DB, key);
60         
61         if(p == "")
62         {
63                 if(PS_GR_OUT_PL)
64                 {
65                         db_put(PS_GR_OUT_DB, key, PS_GR_OUT_PL);
66                         strunzone(PS_GR_OUT_PL);
67                 }
68                 else { db_put(PS_GR_OUT_DB, key, "#"); }
69                 PS_GR_OUT_PL = strzone(e.playerstats_id);
70         }
71 }
72
73 void PlayerStats_GameReport_AddTeam(float t)
74 {
75         if(PS_GR_OUT_DB < 0) { return; }
76
77         string key = sprintf("%d", t);
78         string p = db_get(PS_GR_OUT_DB, key);
79         
80         if(p == "")
81         {
82                 if(PS_GR_OUT_TL)
83                 {
84                         db_put(PS_GR_OUT_DB, key, PS_GR_OUT_TL);
85                         strunzone(PS_GR_OUT_TL);
86                 }
87                 else { db_put(PS_GR_OUT_DB, key, "#"); }
88                 PS_GR_OUT_TL = strzone(key);
89         }
90 }
91
92 void PlayerStats_GameReport_AddEvent(string event_id)
93 {
94         if(PS_GR_OUT_DB < 0) { return; }
95
96         string key = sprintf("*:%s", event_id);
97         string p = db_get(PS_GR_OUT_DB, key);
98         
99         if(p == "")
100         {
101                 if(PS_GR_OUT_EVL)
102                 {
103                         db_put(PS_GR_OUT_DB, key, PS_GR_OUT_EVL);
104                         strunzone(PS_GR_OUT_EVL);
105                 }
106                 else { db_put(PS_GR_OUT_DB, key, "#"); }
107                 PS_GR_OUT_EVL = strzone(event_id);
108         }
109 }
110
111 // referred to by PS_GR_P_ADDVAL and PS_GR_T_ADDVAL
112 float PlayerStats_GameReport_Event(string prefix, string event_id, float value)
113 {
114         if((prefix == "") || PS_GR_OUT_DB < 0) { return 0; }
115
116         string key = sprintf("%s:%s", prefix, event_id);
117         float val = stof(db_get(PS_GR_OUT_DB, key));
118         val += value;
119         db_put(PS_GR_OUT_DB, key, ftos(val));
120         return val;
121 }
122
123 void PlayerStats_GameReport_Accuracy(entity p)
124 {
125     entity w;
126     float i;
127
128     for(i = WEP_FIRST; i <= WEP_LAST; ++i)
129     {
130         w = get_weaponinfo(i);
131         
132         #define ACCMAC(suffix,field) \
133                         PS_GR_P_ADDVAL(p, sprintf("acc-%s-%s", w.netname, suffix), p.accuracy.(field[i-1]));
134                         
135         ACCMAC("hit", accuracy_hit)
136         ACCMAC("fired", accuracy_fired)
137         ACCMAC("cnt-hit", accuracy_cnt_hit)
138         ACCMAC("cnt-fired", accuracy_cnt_fired)
139         ACCMAC("frags", accuracy_frags)
140
141         #undef ACCMAC
142     }
143 }
144
145 void PlayerStats_GameReport_FinalizePlayer(entity p)
146 {
147         if((p.playerstats_id == "") || PS_GR_OUT_DB < 0) { return; }
148
149         // add global info!
150         if(p.alivetime)
151         {
152                 PS_GR_P_ADDVAL(p, PLAYERSTATS_ALIVETIME, time - p.alivetime);
153                 p.alivetime = 0;
154         }
155
156         db_put(PS_GR_OUT_DB, sprintf("%s:_playerid", p.playerstats_id), ftos(p.playerid));
157
158         if(p.cvar_cl_allow_uid2name == 1 || IS_BOT_CLIENT(p))
159                 db_put(PS_GR_OUT_DB, sprintf("%s:_netname", p.playerstats_id), p.netname);
160
161         if(teamplay)
162                 db_put(PS_GR_OUT_DB, sprintf("%s:_team", p.playerstats_id), ftos(p.team));
163
164         if(stof(db_get(PS_GR_OUT_DB, sprintf("%s:%s", p.playerstats_id, PLAYERSTATS_ALIVETIME))) > 0)
165                 PS_GR_P_ADDVAL(p, PLAYERSTATS_JOINS, 1);
166
167         PlayerStats_GameReport_Accuracy(p);
168
169         if(IS_REAL_CLIENT(p))
170         {
171                 if(p.latency_cnt)
172                 {
173                         float latency = (p.latency_sum / p.latency_cnt);
174                         if(latency) { PS_GR_P_ADDVAL(p, PLAYERSTATS_AVGLATENCY, latency); }
175                 }
176         }
177
178         strunzone(p.playerstats_id);
179         p.playerstats_id = string_null;
180 }
181
182 void PlayerStats_GameReport(float finished)
183 {
184         if(PS_GR_OUT_DB < 0) { return; }
185         
186         PlayerScore_Sort(score_dummyfield, 0, 0, 0);
187         PlayerScore_Sort(scoreboard_pos, 1, 1, 1);
188         if(teamplay) { PlayerScore_TeamStats(); }
189
190         entity p;
191         FOR_EACH_CLIENT(p)
192         {
193                 // add personal score rank
194                 PS_GR_P_ADDVAL(p, PLAYERSTATS_RANK, p.score_dummyfield);
195
196                 // scoreboard data
197                 if(p.scoreboard_pos)
198                 {
199                         // scoreboard is valid!
200                         PS_GR_P_ADDVAL(p, PLAYERSTATS_SCOREBOARD_VALID, 1);
201
202                         // add scoreboard position
203                         PS_GR_P_ADDVAL(p, PLAYERSTATS_SCOREBOARD_POS, p.scoreboard_pos);
204
205                         // add scoreboard data
206                         PlayerScore_PlayerStats(p);
207
208                         // if the match ended normally, add winning info
209                         if(finished)
210                         {
211                                 PS_GR_P_ADDVAL(p, PLAYERSTATS_WINS, p.winning);
212                                 PS_GR_P_ADDVAL(p, PLAYERSTATS_MATCHES, 1);
213                         }
214                 }
215
216                 // collect final player information
217                 PlayerStats_GameReport_FinalizePlayer(p);
218         }
219
220         if(autocvar_g_playerstats_gamereport_uri != "")
221         {
222                 PlayerStats_GameReport_DelayMapVote = TRUE;
223                 url_multi_fopen(
224                         autocvar_g_playerstats_gamereport_uri,
225                         FILE_APPEND,
226                         PlayerStats_GameReport_Handler,
227                         world
228                 );
229         }
230         else
231         {
232                 PlayerStats_GameReport_DelayMapVote = FALSE;
233                 db_close(PS_GR_OUT_DB);
234                 PS_GR_OUT_DB = -1;
235         }
236 }
237
238 void PlayerStats_GameReport_Init() // initiated before InitGameplayMode so that scores are added properly
239 {
240         if(autocvar_g_playerstats_gamereport_uri == "") { return; }
241
242         PS_GR_OUT_DB = -1;
243         PS_GR_OUT_DB = db_create();
244
245         if(PS_GR_OUT_DB >= 0)
246         {
247                 PlayerStats_GameReport_DelayMapVote = TRUE;
248
249                 serverflags |= SERVERFLAG_PLAYERSTATS;
250
251                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_ALIVETIME);
252                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_AVGLATENCY);
253                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_WINS);
254                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_MATCHES);
255                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_JOINS);
256                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_SCOREBOARD_VALID);
257                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_SCOREBOARD_POS);
258                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_RANK);
259
260                 // accuracy stats
261                 entity w;
262                 float i;
263                 for(i = WEP_FIRST; i <= WEP_LAST; ++i)
264                 {
265                         w = get_weaponinfo(i);
266                         PlayerStats_GameReport_AddEvent(strcat("acc-", w.netname, "-hit"));
267                         PlayerStats_GameReport_AddEvent(strcat("acc-", w.netname, "-fired"));
268                         PlayerStats_GameReport_AddEvent(strcat("acc-", w.netname, "-cnt-hit"));
269                         PlayerStats_GameReport_AddEvent(strcat("acc-", w.netname, "-cnt-fired"));
270                         PlayerStats_GameReport_AddEvent(strcat("acc-", w.netname, "-frags"));
271                 }
272
273                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_3);
274                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_5);
275                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_10);
276                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_15);
277                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_20);
278                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_25);
279                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_30);
280                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_ACHIEVEMENT_BOTLIKE);
281                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD);
282                 PlayerStats_GameReport_AddEvent(PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM);
283         }
284         else { PlayerStats_GameReport_DelayMapVote = FALSE; }
285 }
286
287 void PlayerStats_GameReport_Handler(entity fh, entity pass, float status)
288 {
289         string t, tn;
290         string p, pn;
291         string e, en;
292         string nn, tt;
293         string s;
294
295         switch(status)
296         {
297                 // ======================================
298                 // -- OUTGOING GAME REPORT INFORMATION --
299                 // ======================================
300                 /* SPECIFICATIONS:
301                  * V: format version (always a fixed number) - this MUST be the first line!
302                  * #: comment (MUST be ignored by any parser)
303                  * R: release information on the server
304                  * G: game type
305                  * O: mod name (icon request) as in server browser
306                  * M: map name
307                  * I: match ID (see "matchid" in g_world.qc)
308                  * S: "hostname" of the server
309                  * C: number of "unpure" cvar changes
310                  * U: UDP port number of the server
311                  * D: duration of the match
312                  * L: "ladder" in which the server is participating in
313                  * P: player ID of an existing player; this also sets the owner for all following "n", "e" and "t" lines (lower case!)
314                  * Q: team number of an existing team (format: team#NN); this also sets the owner for all following "e" lines (lower case!)
315                  * i: player index
316                  * n: nickname of the player (optional)
317                  * t: team ID
318                  * e: followed by an event name, a space, and the event count/score
319                  *  event names can be:
320                  *   alivetime: total playing time of the player
321                  *   avglatency: average network latency compounded throughout the match
322                  *   wins: number of games won (can only be set if matches is set)
323                  *   matches: number of matches played to the end (not aborted by map switch)
324                  *   joins: number of matches joined (always 1 unless player never played during the match)
325                  *   scoreboardvalid: set to 1 if the player was there at the end of the match
326                  *   total-<scoreboardname>: total score of that scoreboard item
327                  *   scoreboard-<scoreboardname>: end-of-game score of that scoreboard item (can differ in non-team games)
328                  *   achievement-<achievementname>: achievement counters (their "count" is usually 1 if nonzero at all)
329                  *   kills-<index>: number of kills against the indexed player
330                  *   rank <number>: rank of player
331                  *   acc-<weapon netname>-hit: total damage dealt
332                  *   acc-<weapon netname>-fired: total damage that all fired projectiles *could* have dealt
333                  *   acc-<weapon netname>-cnt-hit: amount of shots that actually hit
334                  *   acc-<weapon netname>-cnt-fired: amount of fired shots
335                  *   acc-<weapon netname>-frags: amount of frags dealt by weapon
336                  */
337                 case URL_READY_CANWRITE:
338                 {
339                         url_fputs(fh, "V 9\n");
340                         #ifdef WATERMARK
341                         url_fputs(fh, sprintf("R %s\n", WATERMARK));
342                         #endif
343                         url_fputs(fh, sprintf("G %s\n", GetGametype()));
344                         url_fputs(fh, sprintf("O %s\n", modname));
345                         url_fputs(fh, sprintf("M %s\n", GetMapname()));
346                         url_fputs(fh, sprintf("I %s\n", matchid));
347                         url_fputs(fh, sprintf("S %s\n", cvar_string("hostname")));
348                         url_fputs(fh, sprintf("C %d\n", cvar_purechanges_count));
349                         url_fputs(fh, sprintf("U %d\n", cvar("port")));
350                         url_fputs(fh, sprintf("D %f\n", max(0, time - game_starttime)));
351                         url_fputs(fh, sprintf("L %s\n", autocvar_g_playerstats_gamereport_ladder));
352
353                         // TEAMS
354                         if(teamplay)
355                         {
356                                 for(t = PS_GR_OUT_TL; (tn = db_get(PS_GR_OUT_DB, sprintf("%d", stof(t)))) != ""; t = tn)
357                                 {
358                                         // start team section
359                                         url_fputs(fh, sprintf("Q team#%s\n", t));
360
361                                         // output team events // todo: does this do unnecessary loops? perhaps we should do a separate "team_events_last" tracker..."
362                                         for(e = PS_GR_OUT_EVL; (en = db_get(PS_GR_OUT_DB, sprintf("*:%s", e))) != ""; e = en)
363                                         {
364                                                 float v = stof(db_get(PS_GR_OUT_DB, sprintf("team#%d:%s", stof(t), e)));
365                                                 if(v != 0) { url_fputs(fh, sprintf("e %s %g\n", e, v)); }
366                                         }
367                                 }
368                         }
369
370                         // PLAYERS
371                         for(p = PS_GR_OUT_PL; (pn = db_get(PS_GR_OUT_DB, sprintf("%s:*", p))) != ""; p = pn)
372                         {
373                                 // start player section
374                                 url_fputs(fh, sprintf("P %s\n", p));
375
376                                 // playerid/index (entity number for this server)
377                                 nn = db_get(PS_GR_OUT_DB, sprintf("%s:_playerid", p));
378                                 if(nn != "") { url_fputs(fh, sprintf("i %s\n", nn)); }
379
380                                 // player name 
381                                 nn = db_get(PS_GR_OUT_DB, sprintf("%s:_netname", p));
382                                 if(nn != "") { url_fputs(fh, sprintf("n %s\n", nn)); }
383
384                                 // team identification number
385                                 if(teamplay)
386                                 {
387                                         tt = db_get(PS_GR_OUT_DB, sprintf("%s:_team", p));
388                                         url_fputs(fh, sprintf("t %s\n", tt));
389                                 }
390
391                                 // output player events
392                                 for(e = PS_GR_OUT_EVL; (en = db_get(PS_GR_OUT_DB, sprintf("*:%s", e))) != ""; e = en)
393                                 {
394                                         float v = stof(db_get(PS_GR_OUT_DB, sprintf("%s:%s", p, e)));
395                                         if(v != 0) { url_fputs(fh, sprintf("e %s %g\n", e, v)); }
396                                 }
397                         }
398                         url_fputs(fh, "\n");
399                         url_fclose(fh);
400                         break;
401                 }
402
403                 // ======================================
404                 // -- INCOMING GAME REPORT INFORMATION --
405                 // ======================================
406                 /* SPECIFICATIONS:
407                  * stuff
408                  */
409                 case URL_READY_CANREAD:
410                 {
411                         // url_fclose is processing, we got a response for writing the data
412                         // this must come from HTTP
413                         print("Got response from player stats server:\n");
414                         while((s = url_fgets(fh))) { print("  ", s, "\n"); }
415                         print("End of response.\n");
416                         url_fclose(fh);
417                         break;
418                 }
419                 
420                 case URL_READY_CLOSED:
421                 {
422                         // url_fclose has finished
423                         print("Player stats written\n");
424                         PlayerStats_GameReport_DelayMapVote = FALSE;
425                         db_close(PS_GR_OUT_DB);
426                         PS_GR_OUT_DB = -1;
427                         break;
428                 }
429                 
430                 case URL_READY_ERROR:
431                 default:
432                 {
433                         print("Player stats writing failed: ", ftos(status), "\n");
434                         PlayerStats_GameReport_DelayMapVote = FALSE;
435                         if(PS_GR_OUT_DB >= 0)
436                         {
437                                 db_close(PS_GR_OUT_DB);
438                                 PS_GR_OUT_DB = -1;
439                         }
440                         break;
441                 }
442         }
443 }
444 #endif // SVQC
445
446 #ifdef MENUQC
447 void PlayerStats_PlayerDetail_Handler(entity fh, entity p, float status)
448 {
449         switch(status)
450         {
451                 case URL_READY_CANWRITE:
452                 {
453                         print("-- Sending data to player stats server\n");
454                         /*url_fputs(fh, "V 1\n");
455                         #ifdef WATERMARK
456                         url_fputs(fh, sprintf("R %s\n", WATERMARK));
457                         #endif
458                         url_fputs(fh, sprintf("l %s\n", cvar_string("_menu_prvm_language"))); // language
459                         url_fputs(fh, sprintf("c %s\n", cvar_string("_menu_prvm_country"))); // country
460                         url_fputs(fh, sprintf("g %s\n", cvar_string("_menu_prvm_gender"))); // gender
461                         url_fputs(fh, sprintf("n %s\n", cvar_string("_cl_name"))); // name
462                         url_fputs(fh, sprintf("m %s %s\n", cvar_string("_cl_playermodel"), cvar_string("_cl_playerskin"))); // model/skin
463                         */url_fputs(fh, "\n");
464                         url_fclose(fh);
465                         break;
466                 }
467                 
468                 case URL_READY_CANREAD:
469                 {
470                         string s = "";
471                         print("-- Got response from player stats server:\n");
472                         //string gametype = string_null;
473                         while((s = url_fgets(fh)))
474                         {
475                                 print("  ", s, "\n");
476                                 /*
477                                 string key = "", value = "", data = "";
478
479                                 n = tokenizebyseparator(s, " "); // key (value) data
480                                 if (n == 1)
481                                         continue;
482                                 else if (n == 2)
483                                 {
484                                 key = argv(0);
485                                 data = argv(1);
486                                 }
487                                 else if (n >= 3)
488                                 {
489                                                                 key = argv(0);
490                                                                 value = argv(1);
491                                                                 data = argv(2);
492                                 }
493
494                                 if (data == "")
495                                 continue;
496
497                                 if (key == "#")
498                                 continue;
499                                 else if (key == "V")
500                                 PlayerInfo_AddItem(p, "_version", data);
501                                 else if (key == "R")
502                                 PlayerInfo_AddItem(p, "_release", data);
503                                 else if (key == "T")
504                                 PlayerInfo_AddItem(p, "_time", data);
505                                 else if (key == "S")
506                                 PlayerInfo_AddItem(p, "_statsurl", data);
507                                 else if (key == "P")
508                                 PlayerInfo_AddItem(p, "_hashkey", data);
509                                 else if (key == "n")
510                                 PlayerInfo_AddItem(p, "_playernick", data);
511                                 else if (key == "i")
512                                 PlayerInfo_AddItem(p, "_playerid", data);
513                                 else if (key == "G")
514                                 gametype = data;
515                                 else if (key == "e" && value != "")
516                                 {
517                                 if (gametype == "")
518                                 PlayerInfo_AddItem(p, value, data);
519                                 else
520                                 PlayerInfo_AddItem(p, sprintf("%s/%s", gametype, value), data);
521                                 }
522                                 else
523                                 continue;
524                                 */
525                         }
526                         print("-- End of response.\n");
527                         url_fclose(fh);
528                         break;
529                 }
530                 case URL_READY_CLOSED:
531                 {
532                         // url_fclose has finished
533                         print("Player stats synchronized with server\n");
534                         break;
535                 }
536                 
537                 case URL_READY_ERROR:
538                 default:
539                 {
540                         print("Receiving player stats failed: ", ftos(status), "\n");
541                         break;
542                 }
543         }
544 }
545
546 void PlayerStats_PlayerDetail()
547 {
548         //PS_D_IN_DB = -1;
549         //PS_D_IN_DB = db_create();
550
551         //if(PS_D_IN_DB < 0) { return; }
552
553         if((autocvar_g_playerstats_playerdetail_uri != "") && (crypto_getmyidstatus(0) > 0))
554         {
555                 //uri = strcat(uri, "/player/", uri_escape(crypto_getmyidfp(0)));
556                 print("Retrieving playerstats from URL: ", autocvar_g_playerstats_playerdetail_uri, "\n");
557                 url_single_fopen(
558                         autocvar_g_playerstats_playerdetail_uri,
559                         FILE_APPEND,
560                         PlayerStats_PlayerDetail_Handler,
561                         world
562                 );
563         }
564 }
565 #endif
566
567 /*
568 void PlayerInfo_AddPlayer(entity e)
569 {
570         if(playerinfo_db < 0)
571                 return;
572
573         string key;
574         key = sprintf("#%d:*", e.playerid); // TODO: use hashkey instead?
575
576         string p;
577         p = db_get(playerinfo_db, key);
578         if(p == "")
579         {
580                 if(playerinfo_last)
581                 {
582                         db_put(playerinfo_db, key, playerinfo_last);
583                         strunzone(playerinfo_last);
584                 }
585                 else
586                         db_put(playerinfo_db, key, "#");
587                 playerinfo_last = strzone(ftos(e.playerid));
588                 print("  Added player ", ftos(e.playerid), " to playerinfo_db\n");//DEBUG//
589         }
590 }
591
592 void PlayerInfo_AddItem(entity e, string item_id, string val)
593 {
594         if(playerinfo_db < 0)
595                 return;
596
597         string key;
598         key = sprintf("*:%s", item_id);
599
600         string p;
601         p = db_get(playerinfo_db, key);
602         if(p == "")
603         {
604                 if(playerinfo_events_last)
605                 {
606                         db_put(playerinfo_db, key, playerinfo_events_last);
607                         strunzone(playerinfo_events_last);
608                 }
609                 else
610                         db_put(playerinfo_db, key, "#");
611                 playerinfo_events_last = strzone(item_id);
612         }
613
614         key = sprintf("#%d:%s", e.playerid, item_id);
615         db_put(playerinfo_db, key, val);
616         print("  Added item ", key, "=", val, " to playerinfo_db\n");//DEBUG//
617 }
618
619 string PlayerInfo_GetItem(entity e, string item_id)
620 {
621         if(playerinfo_db < 0)
622                 return "";
623
624         string key;
625         key = sprintf("#%d:%s",  e.playerid, item_id);
626         return db_get(playerinfo_db, key);
627 }
628
629 string PlayerInfo_GetItemLocal(string item_id)
630 {
631         entity p = spawn();
632         p.playerid = 0;
633         return PlayerInfo_GetItem(p, item_id);
634 }
635
636 void PlayerInfo_ready(entity fh, entity p, float status)
637 {
638         float n;
639         string s;
640
641         PlayerInfo_AddPlayer(p);
642
643         switch(status)
644         {
645                 case URL_READY_CANWRITE:
646                         print("-- Sending data to player stats server\n");
647                         url_fputs(fh, "V 1\n");
648 #ifdef WATERMARK
649                         url_fputs(fh, sprintf("R %s\n", WATERMARK));
650 #endif
651 #ifdef MENUQC
652                         url_fputs(fh, sprintf("l %s\n", cvar_string("_menu_prvm_language"))); // language
653                         url_fputs(fh, sprintf("c %s\n", cvar_string("_menu_prvm_country"))); // country
654                         url_fputs(fh, sprintf("g %s\n", cvar_string("_menu_prvm_gender"))); // gender
655                         url_fputs(fh, sprintf("n %s\n", cvar_string("_cl_name"))); // name
656                         url_fputs(fh, sprintf("m %s %s\n", cvar_string("_cl_playermodel"), cvar_string("_cl_playerskin"))); // model/skin
657 #endif
658                         url_fputs(fh, "\n");
659                         url_fclose(fh);
660                         break;
661                 case URL_READY_CANREAD:
662                         print("-- Got response from player stats server:\n");
663                         string gametype = string_null;
664                         while((s = url_fgets(fh)))
665                         {
666                                 print("  ", s, "\n");
667
668                                 string key = "", value = "", data = "";
669
670                                 n = tokenizebyseparator(s, " "); // key (value) data
671                                 if (n == 1)
672                                         continue;
673                                 else if (n == 2)
674                                 {
675                                         key = argv(0);
676                                         data = argv(1);
677                                 }
678                                 else if (n >= 3)
679                                 {
680                                         key = argv(0);
681                                         value = argv(1);
682                                         data = argv(2);
683                                 }
684
685                                 if (data == "")
686                                         continue;
687
688                                 if (key == "#")
689                                         continue;
690                                 else if (key == "V")
691                                         PlayerInfo_AddItem(p, "_version", data);
692                                 else if (key == "R")
693                                         PlayerInfo_AddItem(p, "_release", data);
694                                 else if (key == "T")
695                                         PlayerInfo_AddItem(p, "_time", data);
696                                 else if (key == "S")
697                                         PlayerInfo_AddItem(p, "_statsurl", data);
698                                 else if (key == "P")
699                                         PlayerInfo_AddItem(p, "_hashkey", data);
700                                 else if (key == "n")
701                                         PlayerInfo_AddItem(p, "_playernick", data);
702                                 else if (key == "i")
703                                         PlayerInfo_AddItem(p, "_playerid", data);
704                                 else if (key == "G")
705                                         gametype = data;
706                                 else if (key == "e" && value != "")
707                                 {
708                                         if (gametype == "")
709                                                 PlayerInfo_AddItem(p, value, data);
710                                         else
711                                                 PlayerInfo_AddItem(p, sprintf("%s/%s", gametype, value), data);
712                                 }
713                                 else
714                                         continue;
715                         }
716                         print("-- End of response.\n");
717                         url_fclose(fh);
718                         break;
719                 case URL_READY_CLOSED:
720                         // url_fclose has finished
721                         print("Player stats synchronized with server\n");
722                         break;
723                 case URL_READY_ERROR:
724                 default:
725                         print("Receiving player stats failed: ", ftos(status), "\n");
726                         break;
727         }
728 }
729
730 void PlayerInfo_Init()
731 {
732         playerinfo_db = -1;
733         playerinfo_db = db_create();
734 }
735
736 #ifdef SVQC
737 void PlayerInfo_Basic(entity p)
738 {
739         print("-- Getting basic PlayerInfo for player ",ftos(p.playerid)," (SVQC)\n");
740
741         if(playerinfo_db < 0)
742                 return;
743
744         string uri;
745         uri = autocvar_g_playerinfo_uri;
746         if(uri != "" && p.crypto_idfp != "")
747         {
748                 uri = strcat(uri, "/elo/", uri_escape(p.crypto_idfp));
749                 print("Retrieving playerstats from URL: ", uri, "\n");
750                 url_single_fopen(uri, FILE_READ, PlayerInfo_ready, p);
751         }
752 }
753 #endif
754
755 #ifdef MENUQC
756 void PlayerInfo_Details()
757 {
758         print("-- Getting detailed PlayerInfo for local player (MENUQC)\n");
759
760         if(playerinfo_db < 0)
761                 return;
762
763         string uri;
764         uri = autocvar_g_playerinfo_uri; // FIXME
765         if(uri != "" && crypto_getmyidstatus(0) > 0)
766         {
767                 //uri = strcat(uri, "/player/", uri_escape(crypto_getmyidfp(0)));
768                 uri = strcat(uri, "/player/me");
769                 print("Retrieving playerstats from URL: ", uri, "\n");
770                 url_single_fopen(uri, FILE_APPEND, PlayerInfo_ready, world);
771         }
772 }
773 #endif
774
775 #ifdef CSQC
776 /*
777  * FIXME - crypto_* builtin functions missing in CSQC (csprogsdefs.qc:885)
778 void PlayerInfo_Details()
779 {
780         print("-- Getting detailed PlayerInfo for local player (CSQC)\n");
781
782         if(playerinfo_db < 0)
783                 return;
784
785         string uri;
786         uri = autocvar_g_playerinfo_uri; // FIXME
787         if(uri != "" && crypto_getmyidstatus(0) > 0)
788         {
789                 uri = strcat(uri, "/player/", uri_escape(crypto_getmyidfp(0)));
790                 print("Retrieving playerstats from URL: ", uri, "\n");
791                 url_single_fopen(uri, FILE_READ, PlayerInfo_ready, p);
792         }
793 }
794
795 #endif
796 */