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