3 string playerstats_last;
5 .float playerstats_addedglobalinfo;
6 .string playerstats_id;
8 void PlayerStats_Init() // initiated before InitGameplayMode so that scores are added properly
12 playerstats_waitforme = TRUE;
13 uri = autocvar_g_playerstats_uri;
16 playerstats_db = db_create();
17 if(playerstats_db >= 0)
18 playerstats_waitforme = FALSE; // must wait for it at match end
20 serverflags |= SERVERFLAG_PLAYERSTATS;
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);
32 for(i = WEP_FIRST; i <= WEP_LAST; ++i)
34 w = get_weaponinfo(i);
36 PlayerStats_AddEvent(strcat("acc-", w.netname, "-hit"));
37 PlayerStats_AddEvent(strcat("acc-", w.netname, "-fired"));
39 PlayerStats_AddEvent(strcat("acc-", w.netname, "-cnt-hit"));
40 PlayerStats_AddEvent(strcat("acc-", w.netname, "-cnt-fired"));
42 PlayerStats_AddEvent(strcat("acc-", w.netname, "-frags"));
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);
57 void PlayerStats_AddPlayer(entity e)
61 if(playerstats_db < 0)
67 if(e.crypto_idfp != "" && e.cvar_cl_allow_uidtracking == 1)
69 else if(clienttype(e) == CLIENTTYPE_BOT)
70 s = sprintf("bot#%g#%s", skill, e.cleanname);
72 if(!s || find(world, playerstats_id, s)) // already have one of the ID - next one can't be tracked then!
74 if(clienttype(e) == CLIENTTYPE_BOT)
75 s = sprintf("bot#%d", e.playerid);
77 s = sprintf("player#%d", e.playerid);
80 e.playerstats_id = strzone(s);
83 key = sprintf("%s:*", e.playerstats_id);
86 p = db_get(playerstats_db, key);
91 db_put(playerstats_db, key, playerstats_last);
92 strunzone(playerstats_last);
95 db_put(playerstats_db, key, "#");
96 playerstats_last = strzone(e.playerstats_id);
100 void PlayerStats_AddTeam(float t) // TODO: doesn't this remain unused?
102 if(playerstats_db < 0)
106 key = sprintf("%d", t);
109 p = db_get(playerstats_db, key);
114 db_put(playerstats_db, key, teamstats_last);
115 strunzone(teamstats_last);
118 db_put(playerstats_db, key, "#");
119 teamstats_last = strzone(key);
123 void PlayerStats_AddEvent(string event_id)
125 if(playerstats_db < 0)
129 key = sprintf("*:%s", event_id);
132 p = db_get(playerstats_db, key);
137 db_put(playerstats_db, key, events_last);
138 strunzone(events_last);
141 db_put(playerstats_db, key, "#");
142 events_last = strzone(event_id);
146 void PlayerStats_Event(entity e, string event_id, float value)
148 if(!e.playerstats_id || playerstats_db < 0)
153 key = sprintf("%s:%s", e.playerstats_id, event_id);
154 val = stof(db_get(playerstats_db, key));
156 db_put(playerstats_db, key, ftos(val));
159 void PlayerStats_TeamScore(float t, string event_id, float value) // TODO: doesn't this remain unused?
163 key = sprintf("team#%d:%s", t, event_id);
164 val = stof(db_get(playerstats_db, key));
166 db_put(playerstats_db, key, ftos(val));
172 A collection of lines of the format <key> SPACE <value> NEWLINE, where
173 <key> is always a single character.
175 The following keys are defined:
177 V: format version (always a fixed number) - 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
182 O: mod name (icon request) as in server browser
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)
193 e: followed by an event name, a space, and the event count/score
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
211 Response format (not used yet):
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
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!
229 void PlayerStats_ready(entity fh, entity pass, float status)
238 case URL_READY_CANWRITE:
239 url_fputs(fh, "V 4\n");
241 url_fputs(fh, sprintf("R %s\n", WATERMARK()));
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)
254 url_fputs(fh, sprintf("P %s\n", p));
255 nn = db_get(playerstats_db, sprintf("%s:_playerid", p));
257 url_fputs(fh, sprintf("i %s\n", nn));
258 nn = db_get(playerstats_db, sprintf("%s:_netname", p));
260 url_fputs(fh, sprintf("n %s\n", nn));
263 tt = db_get(playerstats_db, sprintf("%s:_team", p));
264 url_fputs(fh, sprintf("t %s\n", tt));
266 for(e = events_last; (en = db_get(playerstats_db, sprintf("*:%s", e))) != ""; e = en)
269 v = stof(db_get(playerstats_db, sprintf("%s:%s", p, e)));
271 url_fputs(fh, sprintf("e %s %g\n", e, v));
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)))
283 print("End of response.\n");
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);
293 case URL_READY_ERROR:
295 print("Player stats writing failed: ", ftos(status), "\n");
296 playerstats_waitforme = TRUE;
297 if(playerstats_db >= 0)
299 db_close(playerstats_db);
306 //#NO AUTOCVARS START
307 void PlayerStats_Shutdown()
311 if(playerstats_db < 0)
314 uri = autocvar_g_playerstats_uri;
317 playerstats_waitforme = FALSE;
318 url_multi_fopen(uri, FILE_APPEND, PlayerStats_ready, world);
322 playerstats_waitforme = TRUE;
323 db_close(playerstats_db);
329 void PlayerStats_Accuracy(entity p)
335 for(i = WEP_FIRST; i <= WEP_LAST; ++i)
337 w = get_weaponinfo(i);
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]));
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]));
345 PlayerStats_Event(p, strcat("acc-", w.netname, "-frags"), a.(accuracy_frags[i-1]));
347 //backtrace(strcat("adding player stat accuracy for ", p.netname, ".\n"));
350 void PlayerStats_AddGlobalInfo(entity p)
352 if(playerstats_db < 0)
354 if(!p.playerstats_id || playerstats_db < 0)
356 p.playerstats_addedglobalinfo = TRUE;
361 PlayerStats_Event(p, PLAYERSTATS_ALIVETIME, time - p.alivetime);
365 db_put(playerstats_db, sprintf("%s:_playerid", p.playerstats_id), ftos(p.playerid));
367 if(p.cvar_cl_allow_uid2name == 1 || clienttype(p) == CLIENTTYPE_BOT)
368 db_put(playerstats_db, sprintf("%s:_netname", p.playerstats_id), p.netname);
371 db_put(playerstats_db, sprintf("%s:_team", p.playerstats_id), ftos(p.team));
373 if(stof(db_get(playerstats_db, sprintf("%d:%s", p.playerstats_id, PLAYERSTATS_ALIVETIME))) > 0)
374 PlayerStats_Event(p, PLAYERSTATS_JOINS, 1);
376 PlayerStats_Accuracy(p);
378 strunzone(p.playerstats_id);
379 p.playerstats_id = string_null;
382 void PlayerStats_EndMatch(float finished)
385 winner = PlayerScore_Sort(score_dummyfield);
386 FOR_EACH_CLIENT(p) // spectators intentionally not included
388 //PlayerStats_Accuracy(p); // stats are already written with PlayerStats_AddGlobalInfo(entity), don't double them up.
390 if((g_arena || g_lms || g_ca) && (p.alivetime <= 0)) { continue; }
391 else if(p.classname != "player") { continue; }
393 PlayerScore_PlayerStats(p);
394 PlayerStats_Event(p, PLAYERSTATS_SCOREBOARD_VALID, 1);
397 PlayerStats_Event(p, PLAYERSTATS_WINS, p.winning);
398 PlayerStats_Event(p, PLAYERSTATS_MATCHES, 1);
399 PlayerStats_Event(p, PLAYERSTATS_RANK, p.score_dummyfield);