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