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_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);
33 for(i = WEP_FIRST; i <= WEP_LAST; ++i)
35 w = get_weaponinfo(i);
37 PlayerStats_AddEvent(strcat("acc-", w.netname, "-hit"));
38 PlayerStats_AddEvent(strcat("acc-", w.netname, "-fired"));
40 PlayerStats_AddEvent(strcat("acc-", w.netname, "-cnt-hit"));
41 PlayerStats_AddEvent(strcat("acc-", w.netname, "-cnt-fired"));
43 PlayerStats_AddEvent(strcat("acc-", w.netname, "-frags"));
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);
58 void PlayerStats_AddPlayer(entity e)
62 if(playerstats_db < 0)
68 if(e.crypto_idfp != "" && e.cvar_cl_allow_uidtracking == 1)
70 else if(clienttype(e) == CLIENTTYPE_BOT)
71 s = sprintf("bot#%g#%s", skill, e.cleanname);
73 if(!s || find(world, playerstats_id, s)) // already have one of the ID - next one can't be tracked then!
75 if(clienttype(e) == CLIENTTYPE_BOT)
76 s = sprintf("bot#%d", e.playerid);
78 s = sprintf("player#%d", e.playerid);
81 e.playerstats_id = strzone(s);
84 key = sprintf("%s:*", e.playerstats_id);
87 p = db_get(playerstats_db, key);
92 db_put(playerstats_db, key, playerstats_last);
93 strunzone(playerstats_last);
96 db_put(playerstats_db, key, "#");
97 playerstats_last = strzone(e.playerstats_id);
101 void PlayerStats_AddTeam(float t) // TODO: doesn't this remain unused?
103 if(playerstats_db < 0)
107 key = sprintf("%d", t);
110 p = db_get(playerstats_db, key);
115 db_put(playerstats_db, key, teamstats_last);
116 strunzone(teamstats_last);
119 db_put(playerstats_db, key, "#");
120 teamstats_last = strzone(key);
124 void PlayerStats_AddEvent(string event_id)
126 if(playerstats_db < 0)
130 key = sprintf("*:%s", event_id);
133 p = db_get(playerstats_db, key);
138 db_put(playerstats_db, key, events_last);
139 strunzone(events_last);
142 db_put(playerstats_db, key, "#");
143 events_last = strzone(event_id);
147 void PlayerStats_Event(entity e, string event_id, float value)
149 if(!e.playerstats_id || playerstats_db < 0)
154 key = sprintf("%s:%s", e.playerstats_id, event_id);
155 val = stof(db_get(playerstats_db, key));
157 db_put(playerstats_db, key, ftos(val));
160 void PlayerStats_TeamScore(float t, string event_id, float value) // TODO: doesn't this remain unused?
164 key = sprintf("team#%d:%s", t, event_id);
165 val = stof(db_get(playerstats_db, key));
167 db_put(playerstats_db, key, ftos(val));
173 A collection of lines of the format <key> SPACE <value> NEWLINE, where
174 <key> is always a single character.
176 The following keys are defined:
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
183 O: mod name (icon request) as in server browser
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)
194 e: followed by an event name, a space, and the event count/score
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
213 Response format (not used yet):
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
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!
231 void PlayerStats_ready(entity fh, entity pass, float status)
240 case URL_READY_CANWRITE:
241 url_fputs(fh, "V 5\n");
243 url_fputs(fh, sprintf("R %s\n", WATERMARK()));
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)
256 url_fputs(fh, sprintf("P %s\n", p));
257 nn = db_get(playerstats_db, sprintf("%s:_playerid", p));
259 url_fputs(fh, sprintf("i %s\n", nn));
260 nn = db_get(playerstats_db, sprintf("%s:_netname", p));
262 url_fputs(fh, sprintf("n %s\n", nn));
265 tt = db_get(playerstats_db, sprintf("%s:_team", p));
266 url_fputs(fh, sprintf("t %s\n", tt));
268 for(e = events_last; (en = db_get(playerstats_db, sprintf("*:%s", e))) != ""; e = en)
271 v = stof(db_get(playerstats_db, sprintf("%s:%s", p, e)));
273 url_fputs(fh, sprintf("e %s %g\n", e, v));
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)))
285 print("End of response.\n");
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);
295 case URL_READY_ERROR:
297 print("Player stats writing failed: ", ftos(status), "\n");
298 playerstats_waitforme = TRUE;
299 if(playerstats_db >= 0)
301 db_close(playerstats_db);
308 //#NO AUTOCVARS START
309 void PlayerStats_Shutdown()
313 if(playerstats_db < 0)
316 uri = autocvar_g_playerstats_uri;
319 playerstats_waitforme = FALSE;
320 url_multi_fopen(uri, FILE_APPEND, PlayerStats_ready, world);
324 playerstats_waitforme = TRUE;
325 db_close(playerstats_db);
331 void PlayerStats_Accuracy(entity p)
337 for(i = WEP_FIRST; i <= WEP_LAST; ++i)
339 w = get_weaponinfo(i);
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]));
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]));
347 PlayerStats_Event(p, strcat("acc-", w.netname, "-frags"), a.(accuracy_frags[i-1]));
349 //backtrace(strcat("adding player stat accuracy for ", p.netname, ".\n"));
352 void PlayerStats_AddGlobalInfo(entity p)
354 if(playerstats_db < 0)
356 if(!p.playerstats_id || playerstats_db < 0)
358 p.playerstats_addedglobalinfo = TRUE;
363 PlayerStats_Event(p, PLAYERSTATS_ALIVETIME, time - p.alivetime);
367 db_put(playerstats_db, sprintf("%s:_playerid", p.playerstats_id), ftos(p.playerid));
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);
373 db_put(playerstats_db, sprintf("%s:_team", p.playerstats_id), ftos(p.team));
375 if(stof(db_get(playerstats_db, sprintf("%d:%s", p.playerstats_id, PLAYERSTATS_ALIVETIME))) > 0)
376 PlayerStats_Event(p, PLAYERSTATS_JOINS, 1);
378 PlayerStats_Accuracy(p);
380 strunzone(p.playerstats_id);
381 p.playerstats_id = string_null;
384 void PlayerStats_EndMatch(float finished)
387 winner = PlayerScore_Sort(score_dummyfield);
388 FOR_EACH_CLIENT(p) // spectators intentionally not included
390 //PlayerStats_Accuracy(p); // stats are already written with PlayerStats_AddGlobalInfo(entity), don't double them up.
392 if((g_arena || g_lms || g_ca) && (p.alivetime <= 0)) { continue; }
393 else if(p.classname != "player") { continue; }
395 float latency = (p.latency_sum / p.latency_cnt);
396 if(latency) { PlayerStats_Event(p, PLAYERSTATS_AVGLATENCY, latency); }
398 PlayerScore_PlayerStats(p);
399 PlayerStats_Event(p, PLAYERSTATS_SCOREBOARD_VALID, 1);
402 PlayerStats_Event(p, PLAYERSTATS_WINS, p.winning);
403 PlayerStats_Event(p, PLAYERSTATS_MATCHES, 1);
404 PlayerStats_Event(p, PLAYERSTATS_RANK, p.score_dummyfield);