Xonotic
playerstats.qc
Go to the documentation of this file.
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>
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);
201 
202  if(IS_REAL_CLIENT(p))
203  {
204  if(CS(p).latency_cnt)
205  {
206  float latency = (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);
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
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;
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 
282  if(autocvar_g_playerstats_gamereport_uri != cvar_defstring("g_playerstats_gamereport_uri"))
283  {
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 
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);
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);
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 */
ERASEABLE int db_create()
Definition: map.qh:25
ERASEABLE void db_put(int db, string key, string value)
Definition: map.qh:101
int serverflags
Definition: main.qh:184
const float URL_READY_CANREAD
Definition: urllib.qh:17
const float URL_READY_CLOSED
Definition: urllib.qh:15
string string_null
Definition: nil.qh:9
float score_dummyfield
Definition: scores.qc:902
void anticheat_report_to_playerstats(entity this)
Definition: anticheat.qc:205
string GetMapname()
Definition: intermission.qc:18
const float URL_READY_CANWRITE
Definition: urllib.qh:16
entity() spawn
entity teamscorekeepers[16]
Definition: scores.qc:20
ClientState CS(Client this)
Definition: state.qh:47
#define FOREACH_CLIENT(cond, body)
Definition: utils.qh:49
#define GameRules_scoring_add(client, fld, value)
Definition: sv_rules.qh:78
float skill
Definition: api.qh:35
REPLICATE(cvar_cl_casings, bool, "cl_casings")
#define CS_CVAR(this)
Definition: state.qh:51
ERASEABLE void db_close(int db)
Definition: map.qh:84
const float FILE_APPEND
Definition: csprogsdefs.qc:232
entity statslist
Definition: statslist.qh:23
float scoreboard_pos
Definition: scores.qh:8
float accuracy_frags[REGISTRY_MAX(Weapons)]
Definition: accuracy.qh:27
const int SERVERFLAG_PLAYERSTATS
Definition: constants.qh:17
const int SERVERFLAG_PLAYERSTATS_CUSTOM
Definition: constants.qh:18
#define IS_REAL_CLIENT(v)
Definition: utils.qh:17
void PlayerScore_PlayerStats(entity p)
Definition: scores.qc:939
#define teamscores_label(i)
Definition: scores.qh:147
string modname
Definition: world.qh:45
void PlayerScore_TeamStats()
Definition: scores.qc:949
string matchid
Definition: world.qh:57
float cvar_purechanges_count
Definition: world.qh:43
#define LOG_INFOF(...)
Definition: log.qh:71
#define argv_start_index
Definition: dpextensions.qh:27
spree_cen s1 spree_cen s1 spree_cen s1 spree_cen s1 spree_cen s1 spree_cen s1 spree_cen s1 f1 s1 strcat(_("Level %s: "), "^BG%s\3\, _("^BGPress ^F2%s^BG to enter the game"))
ERASEABLE void url_fputs(entity e, string s)
Definition: urllib.qc:312
#define MAX_TEAMSCORE
Definition: scores.qh:142
ERASEABLE void url_fclose(entity e)
Definition: urllib.qc:207
#define NULL
Definition: post.qh:17
#define LOG_INFO(...)
Definition: log.qh:70
#define IS_GAMETYPE(NAME)
Definition: mapinfo.qh:131
float accuracy_cnt_hit[REGISTRY_MAX(Weapons)]
Definition: accuracy.qh:31
REPLICATE_APPLYCHANGE("cl_weaponpriority", for(int slot=0;slot< MAX_WEAPONSLOTS;++slot) { .entity weaponentity=weaponentities[slot];if(this.(weaponentity) &&(this.(weaponentity).m_weapon !=WEP_Null||slot==0)) this.(weaponentity).m_switchweapon=w_getbestweapon(this, weaponentity);})
void anticheat_register_to_playerstats()
Definition: anticheat.qc:214
entity gametype
Definition: main.qh:30
#define SHUTDOWN(func)
before shutdown
Definition: static.qh:49
ERASEABLE string db_get(int db, string key)
Definition: map.qh:91
float teamplay
Definition: progsdefs.qc:31
entity PlayerScore_Sort(.float field, int teams, bool strict, bool nospectators)
Sorts the players and stores their place in the given field, starting with.
Definition: scores.qc:724
ERASEABLE void url_single_fopen(string url, int mode, url_ready_func rdy, entity pass)
Definition: urllib.qc:87
vector v
Definition: ent_cs.qc:116
#define scores_label(this)
Definition: scores.qh:139
float count
Definition: powerups.qc:22
string GetGametype()
Definition: intermission.qc:13
#define tokenizebyseparator
Definition: dpextensions.qh:21
#define IS_BOT_CLIENT(v)
want: (IS_CLIENT(v) && !IS_REAL_CLIENT(v))
Definition: utils.qh:15
const float URL_READY_ERROR
Definition: urllib.qh:14
float accuracy_fired[REGISTRY_MAX(Weapons)]
Definition: accuracy.qh:30
#define strfree(this)
Definition: string.qh:56
float accuracy_cnt_fired[REGISTRY_MAX(Weapons)]
Definition: accuracy.qh:32
float accuracy_hit[REGISTRY_MAX(Weapons)]
Definition: accuracy.qh:29
ERASEABLE void url_multi_fopen(string url, int mode, url_ready_func rdy, entity pass)
Definition: urllib.qc:364
int autocvar_g_maxplayers
Definition: client.qh:43
float time
Definition: csprogsdefs.qc:16
#define pass(name, colormin, colormax)
float latency_cnt
Definition: world.qc:54
#define FOREACH(list, cond, body)
Definition: iter.qh:19
ERASEABLE string url_fgets(entity e)
Definition: urllib.qc:287
#define IS_PLAYER(v)
Definition: utils.qh:9
#define LOG_DEBUGF(...)
Definition: log.qh:86
#define LOG_DEBUG(...)
Definition: log.qh:85