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