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