]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/playerstats.qc
Merge branch 'master' into mario/mutator_minstagib
[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): see https://gist.github.com/4284222
214 */
215
216 void PlayerStats_ready(entity fh, entity pass, float status)
217 {
218         string p, pn;
219         string e, en;
220         string nn, tt;
221         string s;
222
223         switch(status)
224         {
225                 case URL_READY_CANWRITE:
226                         url_fputs(fh, "V 5\n");
227 #ifdef WATERMARK
228                         url_fputs(fh, sprintf("R %s\n", WATERMARK));
229 #endif
230                         url_fputs(fh, sprintf("T %s.%06d\n", strftime(FALSE, "%s"), floor(random() * 1000000)));
231                         url_fputs(fh, sprintf("G %s\n", GetGametype()));
232                         url_fputs(fh, sprintf("O %s\n", modname));
233                         url_fputs(fh, sprintf("M %s\n", GetMapname()));
234                         url_fputs(fh, sprintf("I %s\n", matchid));
235                         url_fputs(fh, sprintf("S %s\n", cvar_string("hostname")));
236                         url_fputs(fh, sprintf("C %d\n", cvar_purechanges_count));
237                         url_fputs(fh, sprintf("U %d\n", cvar("port")));
238                         url_fputs(fh, sprintf("D %f\n", max(0, time - game_starttime)));
239                         for(p = playerstats_last; (pn = db_get(playerstats_db, sprintf("%s:*", p))) != ""; p = pn)
240                         {
241                                 url_fputs(fh, sprintf("P %s\n", p));
242                                 nn = db_get(playerstats_db, sprintf("%s:_playerid", p));
243                                 if(nn != "")
244                                         url_fputs(fh, sprintf("i %s\n", nn));
245                                 nn = db_get(playerstats_db, sprintf("%s:_netname", p));
246                                 if(nn != "")
247                                         url_fputs(fh, sprintf("n %s\n", nn));
248                                 if(teamplay)
249                                 {
250                                         tt = db_get(playerstats_db, sprintf("%s:_team", p));
251                                         url_fputs(fh, sprintf("t %s\n", tt));
252                                 }
253                                 for(e = events_last; (en = db_get(playerstats_db, sprintf("*:%s", e))) != ""; e = en)
254                                 {
255                                         float v;
256                                         v = stof(db_get(playerstats_db, sprintf("%s:%s", p, e)));
257                                         if(v != 0)
258                                                 url_fputs(fh, sprintf("e %s %g\n", e, v));
259                                 }
260                         }
261                         url_fputs(fh, "\n");
262                         url_fclose(fh);
263                         break;
264                 case URL_READY_CANREAD:
265                         // url_fclose is processing, we got a response for writing the data
266                         // this must come from HTTP
267                         print("Got response from player stats server:\n");
268                         while((s = url_fgets(fh)))
269                                 print("  ", s, "\n");
270                         print("End of response.\n");
271                         url_fclose(fh);
272                         break;
273                 case URL_READY_CLOSED:
274                         // url_fclose has finished
275                         print("Player stats written\n");
276                         playerstats_waitforme = TRUE;
277                         db_close(playerstats_db);
278                         playerstats_db = -1;
279                         break;
280                 case URL_READY_ERROR:
281                 default:
282                         print("Player stats writing failed: ", ftos(status), "\n");
283                         playerstats_waitforme = TRUE;
284                         if(playerstats_db >= 0)
285                         {
286                                 db_close(playerstats_db);
287                                 playerstats_db = -1;
288                         }
289                         break;
290         }
291 }
292
293 //#NO AUTOCVARS START
294 void PlayerStats_Shutdown()
295 {
296         string uri;
297
298         if(playerstats_db < 0)
299                 return;
300
301         uri = autocvar_g_playerstats_uri;
302         if(uri != "")
303         {
304                 playerstats_waitforme = FALSE;
305                 url_multi_fopen(uri, FILE_APPEND, PlayerStats_ready, world);
306         }
307         else
308         {
309                 playerstats_waitforme = TRUE;
310                 db_close(playerstats_db);
311                 playerstats_db = -1;
312         }
313 }
314 //#NO AUTOCVARS END
315
316 void PlayerStats_Accuracy(entity p)
317 {
318     entity a, w;
319     a = p.accuracy;
320     float i;
321
322     for(i = WEP_FIRST; i <= WEP_LAST; ++i)
323     {
324         w = get_weaponinfo(i);
325
326         PlayerStats_Event(p, strcat("acc-", w.netname, "-hit"), a.(accuracy_hit[i-1]));
327         PlayerStats_Event(p, strcat("acc-", w.netname, "-fired"), a.(accuracy_fired[i-1]));
328
329         PlayerStats_Event(p, strcat("acc-", w.netname, "-cnt-hit"), a.(accuracy_cnt_hit[i-1]));
330         PlayerStats_Event(p, strcat("acc-", w.netname, "-cnt-fired"), a.(accuracy_cnt_fired[i-1]));
331
332         PlayerStats_Event(p, strcat("acc-", w.netname, "-frags"), a.(accuracy_frags[i-1]));
333     }
334     //backtrace(strcat("adding player stat accuracy for ", p.netname, ".\n"));
335 }
336
337 void PlayerStats_AddGlobalInfo(entity p)
338 {
339         if(playerstats_db < 0)
340                 return;
341         if((p.playerstats_id == "") || playerstats_db < 0)
342                 return;
343         p.playerstats_addedglobalinfo = TRUE;
344
345         // add global info!
346         if(p.alivetime)
347         {
348                 PlayerStats_Event(p, PLAYERSTATS_ALIVETIME, time - p.alivetime);
349                 p.alivetime = 0;
350         }
351
352         db_put(playerstats_db, sprintf("%s:_playerid", p.playerstats_id), ftos(p.playerid));
353         
354         if(p.cvar_cl_allow_uid2name == 1 || clienttype(p) == CLIENTTYPE_BOT)
355                 db_put(playerstats_db, sprintf("%s:_netname", p.playerstats_id), p.netname);
356
357     if(teamplay)
358                 db_put(playerstats_db, sprintf("%s:_team", p.playerstats_id), ftos(p.team));
359
360         if(stof(db_get(playerstats_db, sprintf("%d:%s", p.playerstats_id, PLAYERSTATS_ALIVETIME))) > 0)
361                 PlayerStats_Event(p, PLAYERSTATS_JOINS, 1);
362
363         PlayerStats_Accuracy(p);
364
365         strunzone(p.playerstats_id);
366         p.playerstats_id = string_null;
367 }
368
369 .float scoreboard_pos;
370 void PlayerStats_EndMatch(float finished)
371 {
372         entity p;
373         PlayerScore_Sort(score_dummyfield, 0);
374         PlayerScore_Sort(scoreboard_pos, 1);
375         FOR_EACH_CLIENT(p)
376         {
377                 //PlayerStats_Accuracy(p); // stats are already written with PlayerStats_AddGlobalInfo(entity), don't double them up.
378
379                 if(p.frags == FRAGS_SPECTATOR)
380                         continue;
381
382                 if(clienttype(p) == CLIENTTYPE_REAL)
383                 {
384                         if(p.latency_cnt)
385                         {
386                                 float latency = (p.latency_sum / p.latency_cnt);
387                                 if(latency) { PlayerStats_Event(p, PLAYERSTATS_AVGLATENCY, latency); }
388                         }
389                 }
390                 
391                 PlayerScore_PlayerStats(p);
392                 PlayerStats_Event(p, PLAYERSTATS_SCOREBOARD_VALID, 1);
393                 if(finished)
394                 {
395                         PlayerStats_Event(p, PLAYERSTATS_WINS, p.winning);
396                         PlayerStats_Event(p, PLAYERSTATS_MATCHES, 1);
397                         PlayerStats_Event(p, PLAYERSTATS_RANK, p.score_dummyfield);
398                         PlayerStats_Event(p, PLAYERSTATS_SCOREBOARD_POS, p.scoreboard_pos);
399                 }
400         }
401 }