]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/playerstats.qc
Merge remote-tracking branch 'origin/Mario/infinite_overtimes'
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / playerstats.qc
1 float playerstats_db;
2 string teamstats_last;
3 string playerstats_last;
4 string events_last;
5 .float playerstats_addedglobalinfo;
6 .string playerstats_id;
7
8 #define ALL_ANTICHEATS \
9         ANTICHEAT("_time"); \
10         ANTICHEAT("speedhack"); \
11         ANTICHEAT("speedhack_m1"); \
12         ANTICHEAT("speedhack_m2"); \
13         ANTICHEAT("speedhack_m3"); \
14         ANTICHEAT("speedhack_m4"); \
15         ANTICHEAT("speedhack_m5"); \
16         ANTICHEAT("div0_strafebot_old"); \
17         ANTICHEAT("div0_strafebot_new"); \
18         ANTICHEAT("div0_evade"); \
19         ANTICHEAT("idle_snapaim"); \
20         ANTICHEAT("idle_snapaim_signal"); \
21         ANTICHEAT("idle_snapaim_noise"); \
22         ANTICHEAT("idle_snapaim_m2"); \
23         ANTICHEAT("idle_snapaim_m3"); \
24         ANTICHEAT("idle_snapaim_m4"); \
25         ANTICHEAT("idle_snapaim_m7"); \
26         ANTICHEAT("idle_snapaim_m10");
27
28 void PlayerStats_Init() // initiated before InitGameplayMode so that scores are added properly
29 {
30         string uri;
31         playerstats_db = -1;
32         playerstats_waitforme = TRUE;
33         uri = autocvar_g_playerstats_uri;
34         if(uri == "")
35                 return;
36         playerstats_db = db_create();
37         if(playerstats_db >= 0)
38                 playerstats_waitforme = FALSE; // must wait for it at match end
39
40         serverflags |= SERVERFLAG_PLAYERSTATS;
41
42         PlayerStats_AddEvent(PLAYERSTATS_ALIVETIME);
43         PlayerStats_AddEvent(PLAYERSTATS_AVGLATENCY);
44         PlayerStats_AddEvent(PLAYERSTATS_WINS);
45         PlayerStats_AddEvent(PLAYERSTATS_MATCHES);
46         PlayerStats_AddEvent(PLAYERSTATS_JOINS);
47         PlayerStats_AddEvent(PLAYERSTATS_SCOREBOARD_VALID);
48         PlayerStats_AddEvent(PLAYERSTATS_SCOREBOARD_POS);
49         PlayerStats_AddEvent(PLAYERSTATS_RANK);
50
51     // accuracy stats
52     entity w;
53     float i;
54     for(i = WEP_FIRST; i <= WEP_LAST; ++i)
55     {
56         w = get_weaponinfo(i);
57
58         PlayerStats_AddEvent(strcat("acc-", w.netname, "-hit"));
59         PlayerStats_AddEvent(strcat("acc-", w.netname, "-fired"));
60
61         PlayerStats_AddEvent(strcat("acc-", w.netname, "-cnt-hit"));
62         PlayerStats_AddEvent(strcat("acc-", w.netname, "-cnt-fired"));
63
64         PlayerStats_AddEvent(strcat("acc-", w.netname, "-frags"));
65     }
66
67 #define ANTICHEAT(name) \
68         PlayerStats_AddEvent("anticheat-" name)
69         ALL_ANTICHEATS
70 #undef ANTICHEAT
71
72         PlayerStats_AddEvent(PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_3);
73         PlayerStats_AddEvent(PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_5);
74         PlayerStats_AddEvent(PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_10);
75         PlayerStats_AddEvent(PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_15);
76         PlayerStats_AddEvent(PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_20);
77         PlayerStats_AddEvent(PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_25);
78         PlayerStats_AddEvent(PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_30);
79         PlayerStats_AddEvent(PLAYERSTATS_ACHIEVEMENT_BOTLIKE);
80         PlayerStats_AddEvent(PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD);
81         PlayerStats_AddEvent(PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM);
82 }
83
84 void PlayerStats_AddPlayer(entity e)
85 {
86         string s;
87
88         if(playerstats_db < 0)
89                 return;
90         if(e.playerstats_id)
91                 return;
92
93         s = string_null;
94         if(e.crypto_idfp != "" && e.cvar_cl_allow_uidtracking == 1)
95                 s = e.crypto_idfp;
96         else if(IS_BOT_CLIENT(e))
97                 s = sprintf("bot#%g#%s", skill, e.cleanname);
98
99         if((s == "") || find(world, playerstats_id, s)) // already have one of the ID - next one can't be tracked then!
100         {
101                 if(IS_BOT_CLIENT(e))
102                         s = sprintf("bot#%d", e.playerid);
103                 else
104                         s = sprintf("player#%d", e.playerid);
105         }
106
107         e.playerstats_id = strzone(s);
108
109         string key;
110         key = sprintf("%s:*", e.playerstats_id);
111
112         string p;
113         p = db_get(playerstats_db, key);
114         if(p == "")
115         {
116                 if(playerstats_last)
117                 {
118                         db_put(playerstats_db, key, playerstats_last);
119                         strunzone(playerstats_last);
120                 }
121                 else
122                         db_put(playerstats_db, key, "#");
123                 playerstats_last = strzone(e.playerstats_id);
124         }
125 }
126
127 void PlayerStats_AddTeam(float t)
128 {
129         if(playerstats_db < 0)
130                 return;
131
132         string key;
133         key = sprintf("%d", t);
134
135         string p;
136         p = db_get(playerstats_db, key);
137         if(p == "")
138         {
139                 if(teamstats_last)
140                 {
141                         db_put(playerstats_db, key, teamstats_last);
142                         strunzone(teamstats_last);
143                 }
144                 else
145                         db_put(playerstats_db, key, "#");
146                 teamstats_last = strzone(key);
147         }
148 }
149
150 void PlayerStats_AddEvent(string event_id)
151 {
152         if(playerstats_db < 0)
153                 return;
154
155         string key;
156         key = sprintf("*:%s", event_id);
157
158         string p;
159         p = db_get(playerstats_db, key);
160         if(p == "")
161         {
162                 if(events_last)
163                 {
164                         db_put(playerstats_db, key, events_last);
165                         strunzone(events_last);
166                 }
167                 else
168                         db_put(playerstats_db, key, "#");
169                 events_last = strzone(event_id);
170         }
171 }
172
173 float PlayerStats_Event(entity e, string event_id, float value)
174 {
175         if((e.playerstats_id == "") || playerstats_db < 0)
176                 return 0;
177
178         string key;
179         float val;
180         key = sprintf("%s:%s", e.playerstats_id, event_id);
181         val = stof(db_get(playerstats_db, key));
182         val += value;
183         db_put(playerstats_db, key, ftos(val));
184         return val;
185 }
186
187 float PlayerStats_TeamScore(float t, string event_id, float value)
188 {
189         if(playerstats_db < 0)
190                 return 0;
191
192         string key;
193         float val;
194         key = sprintf("team#%d:%s", t, event_id);
195         val = stof(db_get(playerstats_db, key));
196         val += value;
197         db_put(playerstats_db, key, ftos(val));
198         return val;
199 }
200
201 /*
202         format spec:
203
204         A collection of lines of the format <key> SPACE <value> NEWLINE, where
205         <key> is always a single character.
206
207         The following keys are defined:
208
209         V: format version (always a fixed number) - this MUST be the first line!
210         #: comment (MUST be ignored by any parser)
211         R: release information on the server
212         T: time at which the game ended
213         G: game type
214         O: mod name (icon request) as in server browser
215         M: map name
216         I: match ID (see "matchid" in g_world.qc
217         S: "hostname" of the server
218         C: number of "unpure" cvar changes
219         U: UDP port number of the server
220         D: duration of the match
221         P: player ID of an existing player; this also sets the owner for all following "n", "e" and "t" lines (lower case!)
222         Q: team number of an existing team (format: team#NN); this also sets the owner for all following "e" lines (lower case!)
223         n: nickname of the player (optional)
224         t: team ID
225         i: player index
226         e: followed by an event name, a space, and the event count/score
227                 event names can be:
228                         alivetime: total playing time of the player
229                         avglatency: average network latency compounded throughout the match
230                         wins: number of games won (can only be set if matches is set)
231                         matches: number of matches played to the end (not aborted by map switch)
232                         joins: number of matches joined (always 1 unless player never played during the match)
233                         scoreboardvalid: set to 1 if the player was there at the end of the match
234                         total-<scoreboardname>: total score of that scoreboard item
235                         scoreboard-<scoreboardname>: end-of-game score of that scoreboard item (can differ in non-team games)
236                         achievement-<achievementname>: achievement counters (their "count" is usually 1 if nonzero at all)
237                         kills-<index>: number of kills against the indexed player
238                         rank <number>: rank of player
239                         acc-<weapon netname>-hit: total damage dealt
240                         acc-<weapon netname>-fired: total damage that all fired projectiles *could* have dealt
241                         acc-<weapon netname>-cnt-hit: amount of shots that actually hit
242                         acc-<weapon netname>-cnt-fired: amount of fired shots
243                         acc-<weapon netname>-frags: amount of frags dealt by weapon
244
245         Response format (not used yet): see https://gist.github.com/4284222
246 */
247
248 void PlayerStats_ready(entity fh, entity pass, float status)
249 {
250         string t, tn;
251         string p, pn;
252         string e, en;
253         string nn, tt;
254         string s;
255
256         switch(status)
257         {
258                 case URL_READY_CANWRITE:
259                         url_fputs(fh, "V 7\n");
260 #ifdef WATERMARK
261                         url_fputs(fh, sprintf("R %s\n", WATERMARK));
262 #endif
263                         url_fputs(fh, sprintf("T %s.%06d\n", strftime(FALSE, "%s"), floor(random() * 1000000)));
264                         url_fputs(fh, sprintf("G %s\n", GetGametype()));
265                         url_fputs(fh, sprintf("O %s\n", modname));
266                         url_fputs(fh, sprintf("M %s\n", GetMapname()));
267                         url_fputs(fh, sprintf("I %s\n", matchid));
268                         url_fputs(fh, sprintf("S %s\n", cvar_string("hostname")));
269                         url_fputs(fh, sprintf("C %d\n", cvar_purechanges_count));
270                         url_fputs(fh, sprintf("U %d\n", cvar("port")));
271                         url_fputs(fh, sprintf("D %f\n", max(0, time - game_starttime)));
272                         if(teamplay)
273                         {
274                                 for(t = teamstats_last; (tn = db_get(playerstats_db, sprintf("%d", stof(t)))) != ""; t = tn)
275                                 {
276                                         url_fputs(fh, sprintf("Q team#%s\n", t));
277                                         for(e = events_last; (en = db_get(playerstats_db, sprintf("*:%s", e))) != ""; e = en)
278                                         {
279                                                 float v;
280                                                 v = stof(db_get(playerstats_db, sprintf("team#%d:%s", stof(t), e)));
281                                                 if(v != 0)
282                                                         url_fputs(fh, sprintf("e %s %g\n", e, v));
283                                         }
284                                 }
285                         }
286                         for(p = playerstats_last; (pn = db_get(playerstats_db, sprintf("%s:*", p))) != ""; p = pn)
287                         {
288                                 url_fputs(fh, sprintf("P %s\n", p));
289                                 nn = db_get(playerstats_db, sprintf("%s:_playerid", p));
290                                 if(nn != "")
291                                         url_fputs(fh, sprintf("i %s\n", nn));
292                                 nn = db_get(playerstats_db, sprintf("%s:_netname", p));
293                                 if(nn != "")
294                                         url_fputs(fh, sprintf("n %s\n", nn));
295                                 if(teamplay)
296                                 {
297                                         tt = db_get(playerstats_db, sprintf("%s:_team", p));
298                                         url_fputs(fh, sprintf("t %s\n", tt));
299                                 }
300                                 for(e = events_last; (en = db_get(playerstats_db, sprintf("*:%s", e))) != ""; e = en)
301                                 {
302                                         float v;
303                                         v = stof(db_get(playerstats_db, sprintf("%s:%s", p, e)));
304                                         if(v != 0)
305                                                 url_fputs(fh, sprintf("e %s %g\n", e, v));
306                                 }
307                         }
308                         url_fputs(fh, "\n");
309                         url_fclose(fh);
310                         break;
311                 case URL_READY_CANREAD:
312                         // url_fclose is processing, we got a response for writing the data
313                         // this must come from HTTP
314                         print("Got response from player stats server:\n");
315                         while((s = url_fgets(fh)))
316                                 print("  ", s, "\n");
317                         print("End of response.\n");
318                         url_fclose(fh);
319                         break;
320                 case URL_READY_CLOSED:
321                         // url_fclose has finished
322                         print("Player stats written\n");
323                         playerstats_waitforme = TRUE;
324                         db_close(playerstats_db);
325                         playerstats_db = -1;
326                         break;
327                 case URL_READY_ERROR:
328                 default:
329                         print("Player stats writing failed: ", ftos(status), "\n");
330                         playerstats_waitforme = TRUE;
331                         if(playerstats_db >= 0)
332                         {
333                                 db_close(playerstats_db);
334                                 playerstats_db = -1;
335                         }
336                         break;
337         }
338 }
339
340 //#NO AUTOCVARS START
341 void PlayerStats_Shutdown()
342 {
343         string uri;
344
345         if(playerstats_db < 0)
346                 return;
347
348         uri = autocvar_g_playerstats_uri;
349         if(uri != "")
350         {
351                 playerstats_waitforme = FALSE;
352                 url_multi_fopen(uri, FILE_APPEND, PlayerStats_ready, world);
353         }
354         else
355         {
356                 playerstats_waitforme = TRUE;
357                 db_close(playerstats_db);
358                 playerstats_db = -1;
359         }
360 }
361 //#NO AUTOCVARS END
362
363 void PlayerStats_Accuracy(entity p)
364 {
365     entity a, w;
366     a = p.accuracy;
367     float i;
368
369     for(i = WEP_FIRST; i <= WEP_LAST; ++i)
370     {
371         w = get_weaponinfo(i);
372
373         PlayerStats_Event(p, strcat("acc-", w.netname, "-hit"), a.(accuracy_hit[i-1]));
374         PlayerStats_Event(p, strcat("acc-", w.netname, "-fired"), a.(accuracy_fired[i-1]));
375
376         PlayerStats_Event(p, strcat("acc-", w.netname, "-cnt-hit"), a.(accuracy_cnt_hit[i-1]));
377         PlayerStats_Event(p, strcat("acc-", w.netname, "-cnt-fired"), a.(accuracy_cnt_fired[i-1]));
378
379         PlayerStats_Event(p, strcat("acc-", w.netname, "-frags"), a.(accuracy_frags[i-1]));
380     }
381     //backtrace(strcat("adding player stat accuracy for ", p.netname, ".\n"));
382 }
383
384 void PlayerStats_Anticheat(entity p)
385 {
386         entity oldself = self;
387         self = p;
388
389 #define ANTICHEAT(name) \
390         PlayerStats_Event(p, "anticheat-" name, anticheat_getvalue(name))
391         ALL_ANTICHEATS
392 #undef ANTICHEAT
393         self = oldself;
394 }
395
396 void PlayerStats_AddGlobalInfo(entity p)
397 {
398         if(playerstats_db < 0)
399                 return;
400         if((p.playerstats_id == "") || playerstats_db < 0)
401                 return;
402         p.playerstats_addedglobalinfo = TRUE;
403
404         // add global info!
405         if(p.alivetime)
406         {
407                 PlayerStats_Event(p, PLAYERSTATS_ALIVETIME, time - p.alivetime);
408                 p.alivetime = 0;
409         }
410
411         db_put(playerstats_db, sprintf("%s:_playerid", p.playerstats_id), ftos(p.playerid));
412
413         if(p.cvar_cl_allow_uid2name == 1 || IS_BOT_CLIENT(p))
414                 db_put(playerstats_db, sprintf("%s:_netname", p.playerstats_id), p.netname);
415
416         if(teamplay)
417                 db_put(playerstats_db, sprintf("%s:_team", p.playerstats_id), ftos(p.team));
418
419         if(stof(db_get(playerstats_db, sprintf("%d:%s", p.playerstats_id, PLAYERSTATS_ALIVETIME))) > 0)
420                 PlayerStats_Event(p, PLAYERSTATS_JOINS, 1);
421
422         PlayerStats_Accuracy(p);
423
424         PlayerStats_Anticheat(p);
425
426         if(IS_REAL_CLIENT(p))
427         {
428                 if(p.latency_cnt)
429                 {
430                         float latency = (p.latency_sum / p.latency_cnt);
431                         if(latency) { PlayerStats_Event(p, PLAYERSTATS_AVGLATENCY, latency); }
432                 }
433         }
434
435         strunzone(p.playerstats_id);
436         p.playerstats_id = string_null;
437 }
438
439 .float scoreboard_pos;
440 void PlayerStats_EndMatch(float finished)
441 {
442         entity p;
443         PlayerScore_Sort(score_dummyfield, 0, 0, 0);
444         PlayerScore_Sort(scoreboard_pos, 1, 1, 1);
445         if(teamplay)
446                 PlayerScore_TeamStats();
447         FOR_EACH_CLIENT(p)
448         {
449                 // add personal score rank
450                 PlayerStats_Event(p, PLAYERSTATS_RANK, p.score_dummyfield);
451
452                 if(!p.scoreboard_pos)
453                         continue;
454
455                 // scoreboard is valid!
456                 PlayerStats_Event(p, PLAYERSTATS_SCOREBOARD_VALID, 1);
457
458                 // add scoreboard position
459                 PlayerStats_Event(p, PLAYERSTATS_SCOREBOARD_POS, p.scoreboard_pos);
460
461                 // add scoreboard data
462                 PlayerScore_PlayerStats(p);
463
464                 // if the match ended normally, add winning info
465                 if(finished)
466                 {
467                         PlayerStats_Event(p, PLAYERSTATS_WINS, p.winning);
468                         PlayerStats_Event(p, PLAYERSTATS_MATCHES, 1);
469                 }
470         }
471 }
472
473 #undef ALL_ANTICHEATS