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