From 2414c632b73268944cd9072cbf3190e4d926290f Mon Sep 17 00:00:00 2001 From: Rudolf Polzer Date: Sun, 28 Nov 2010 18:39:40 +0100 Subject: [PATCH] initial player stats system (reports total "alive time" and kills to a stats server via HTTP) --- defaultXonotic.cfg | 3 + qcsrc/common/util-pre.qh | 6 -- qcsrc/common/util.qc | 40 --------- qcsrc/server/cl_client.qc | 13 +++ qcsrc/server/cl_player.qc | 6 ++ qcsrc/server/defs.qh | 3 +- qcsrc/server/g_damage.qc | 2 + qcsrc/server/g_world.qc | 31 +++++-- qcsrc/server/miscfunctions.qc | 15 ++-- qcsrc/server/playerstats.qc | 154 ++++++++++++++++++++++++++++++++++ qcsrc/server/playerstats.qh | 27 ++++++ qcsrc/server/progs.src | 2 + 12 files changed, 244 insertions(+), 58 deletions(-) create mode 100644 qcsrc/server/playerstats.qc create mode 100644 qcsrc/server/playerstats.qh diff --git a/defaultXonotic.cfg b/defaultXonotic.cfg index 25e3209cd..275c8a28f 100644 --- a/defaultXonotic.cfg +++ b/defaultXonotic.cfg @@ -1997,6 +1997,9 @@ set g_forced_team_yellow "" "list of player IDs for yellow team" set g_forced_team_pink "" "list of player IDs for pink team" set g_forced_team_otherwise "default" "action if a non listed player joins (can be default for default action, spectate for forcing to spectate, or red, blue, yellow, pink)" +// player statistics server URI +set g_playerstats_uri "" + // other config files exec balanceXonotic.cfg exec ctfscoring-ai.cfg diff --git a/qcsrc/common/util-pre.qh b/qcsrc/common/util-pre.qh index e7d67392a..bc086e4a9 100644 --- a/qcsrc/common/util-pre.qh +++ b/qcsrc/common/util-pre.qh @@ -1,8 +1,2 @@ #pragma flag enable subscope #pragma flag enable lo - -//float log(float x); -#define log log_builtin \ - #undef log \ - /* turn the next use of "log" into a declaration of log_builtin */ - diff --git a/qcsrc/common/util.qc b/qcsrc/common/util.qc index 8c87b7fda..e4db850a9 100644 --- a/qcsrc/common/util.qc +++ b/qcsrc/common/util.qc @@ -1,43 +1,3 @@ -// checkextension wrapper for log -float sqrt(float f); // declared later -float exp(float f); // declared later -float pow(float f, float e); // declared later -float checkextension(string s); // declared later -float log_synth(float f) -{ - float p, l; - if(f < 0) - return sqrt(-1); // nan? -inf? - if(f == 0) - return sqrt(-1); // ACTUALLY this should rather be -inf, but we cannot create a +inf in QC - if(f + f == f) - return l; // +inf - if(f < 1) - { - f = 1 / f; - p = -1; - } - else - p = 1; - while(f > 2) - { - f = sqrt(f); - p *= 2; - } - // two steps are good enough - l = ((6-f) * f - 5) / 4.32808512266689022212; - l += exp(-l) * f - 1; - l += exp(-l) * f - 1; - return l * p; -} -float log(float f) -{ - if(checkextension("DP_QC_LOG")) - return log_builtin(f); - else - return log_synth(f); -} - string wordwrap_buffer; void wordwrap_buffer_put(string s) diff --git a/qcsrc/server/cl_client.qc b/qcsrc/server/cl_client.qc index 2985b45b4..68f699f37 100644 --- a/qcsrc/server/cl_client.qc +++ b/qcsrc/server/cl_client.qc @@ -603,6 +603,12 @@ void PutObserverInServer (void) Portal_ClearAll(self); + if(self.alivetime) + { + PlayerStats_Event(self, PLAYERSTATS_ALIVETIME, time - self.alivetime); + self.alivetime = 0; + } + if(self.flagcarried) DropFlag(self.flagcarried, world, world); @@ -1074,6 +1080,9 @@ void PutClientInServer (void) self.switchweapon = w_getbestweapon(self); self.cnt = self.switchweapon; self.weapon = 0; + + if(!self.alivetime) + self.alivetime = time; } else if(self.classname == "observer" || (g_ca && !allowed_to_spawn)) { PutObserverInServer (); } @@ -1701,6 +1710,8 @@ void ClientConnect (void) send_CSQC_cr_maxbullets(self); CheatInitClient(); + + PlayerStats_AddPlayer(self); } /* @@ -1721,6 +1732,8 @@ void ClientDisconnect (void) return; } + PlayerStats_AddGlobalInfo(self); + CheatShutdownClient(); if(self.hitplotfh >= 0) diff --git a/qcsrc/server/cl_player.qc b/qcsrc/server/cl_player.qc index a2943a760..cb20a91a4 100644 --- a/qcsrc/server/cl_player.qc +++ b/qcsrc/server/cl_player.qc @@ -567,6 +567,12 @@ void PlayerDamage (entity inflictor, entity attacker, float damage, float deatht float defer_ClientKill_Now_TeamChange; defer_ClientKill_Now_TeamChange = FALSE; + if(self.alivetime) + { + PlayerStats_Event(self, PLAYERSTATS_ALIVETIME, time - self.alivetime); + self.alivetime = 0; + } + if(valid_damage_for_weaponstats) WeaponStats_LogKill(DEATH_WEAPONOF(deathtype), self.weapon); diff --git a/qcsrc/server/defs.qh b/qcsrc/server/defs.qh index a0718d839..6d967e656 100644 --- a/qcsrc/server/defs.qh +++ b/qcsrc/server/defs.qh @@ -263,7 +263,8 @@ float blockSpectators; //if set, new or existing spectators or observers will be void checkSpectatorBlock(); .float winning; -.float jointime; +.float jointime; // time of joining +.float alivetime; // time of being alive float isJoinAllowed(); #define PREVENT_JOIN_TEXT "^1You may not join the game at this time.\n\nThe player limit reached maximum capacity." diff --git a/qcsrc/server/g_damage.qc b/qcsrc/server/g_damage.qc index 387514be4..cde87fec1 100644 --- a/qcsrc/server/g_damage.qc +++ b/qcsrc/server/g_damage.qc @@ -120,12 +120,14 @@ void GiveFrags (entity attacker, entity targ, float f) { // teamkill PlayerScore_Add(attacker, SP_KILLS, -1); // or maybe add a teamkills field? + PlayerStats_Event(attacker, PLAYERSTATS_KILLS, -1); } } else { // regular frag PlayerScore_Add(attacker, SP_KILLS, 1); + PlayerStats_Event(attacker, PLAYERSTATS_KILLS, 1); } PlayerScore_Add(targ, SP_DEATHS, 1); diff --git a/qcsrc/server/g_world.qc b/qcsrc/server/g_world.qc index 4628aef45..1b9596909 100644 --- a/qcsrc/server/g_world.qc +++ b/qcsrc/server/g_world.qc @@ -897,6 +897,8 @@ void spawnfunc_worldspawn (void) cvar_set("sv_curl_serverpackages", substring(s, 1, -1)); } + PlayerStats_Init(); + world_initialized = 1; } @@ -1397,12 +1399,13 @@ RULES void DumpStats(float final) { - local float file; - local string s; - local float to_console; - local float to_eventlog; - local float to_file; - local float i; + float file; + string s; + float to_console; + float to_eventlog; + float to_file; + float i; + entity e; to_console = cvar("sv_logscores_console"); to_eventlog = cvar("sv_eventlog"); @@ -1497,6 +1500,11 @@ void DumpStats(float final) fputs(file, ":end\n"); fclose(file); } + + // send statistics + FOR_EACH_CLIENT(e) + PlayerStats_AddGlobalInfo(e); + PlayerStats_Shutdown(); } void FixIntermissionClient(entity e) @@ -2718,6 +2726,10 @@ void MapVote_Start() if(mapvote_run) return; + // wait for stats to be sent first + if(!playerstats_sent) + return; + MapInfo_Enumerate(); if(MapInfo_FilterGametype(MapInfo_CurrentGametype(), MapInfo_CurrentFeatures(), MapInfo_RequiredFlags(), MapInfo_ForbiddenFlags(), 1)) mapvote_run = TRUE; @@ -2892,6 +2904,8 @@ void RestoreGame() void SV_Shutdown() { + entity e; + if(gameover > 1) // shutting down already? return; @@ -2902,6 +2916,11 @@ void SV_Shutdown() world_initialized = 0; print("Saving persistent data...\n"); Ban_SaveBans(); + + FOR_EACH_CLIENT(e) + PlayerStats_AddGlobalInfo(e); + PlayerStats_Shutdown(); + if(!cheatcount_total) { if(cvar("sv_db_saveasdump")) diff --git a/qcsrc/server/miscfunctions.qc b/qcsrc/server/miscfunctions.qc index 25222a38f..cb74c8d3a 100644 --- a/qcsrc/server/miscfunctions.qc +++ b/qcsrc/server/miscfunctions.qc @@ -2027,11 +2027,12 @@ float WarpZone_Projectile_Touch_ImpactFilter_Callback() } #define PROJECTILE_TOUCH if(WarpZone_Projectile_Touch()) return -float MAX_IPBAN_URIS = 16; - -float URI_GET_DISCARD = 0; -float URI_GET_IPBAN = 1; -float URI_GET_IPBAN_END = 16; +float MAX_IPBAN_URIS = 16; + +float URI_GET_DISCARD = 0; +float URI_GET_IPBAN = 1; +float URI_GET_IPBAN_END = 16; +float URI_GET_PLAYERSTATS_SENT = 17; void URI_Get_Callback(float id, float status, string data) { @@ -2048,6 +2049,10 @@ void URI_Get_Callback(float id, float status, string data) // online ban list OnlineBanList_URI_Get_Callback(id, status, data); } + else if (id == URI_GET_PLAYERSTATS_SENT) + { + PlayerStats_Sent_URI_Get_Callback(id, status, data); + } else { print("Received HTTP request data for an invalid id ", ftos(id), ".\n"); diff --git a/qcsrc/server/playerstats.qc b/qcsrc/server/playerstats.qc new file mode 100644 index 000000000..7da57f2c2 --- /dev/null +++ b/qcsrc/server/playerstats.qc @@ -0,0 +1,154 @@ +float playerstats_db; +string playerstats_last; +string events_last; +.float playerstats_addedglobalinfo; +float playerstats_requested; + +void PlayerStats_Init() +{ + string uri; + playerstats_sent = TRUE; + uri = cvar_string("g_playerstats_uri"); + if(uri == "") + return; + playerstats_db = db_create(); + if(playerstats_db >= 0) + playerstats_sent = FALSE; // must wait for it at match end + + PlayerStats_AddEvent(PLAYERSTATS_ALIVETIME); + PlayerStats_AddEvent(PLAYERSTATS_KILLS); +} + +void PlayerStats_AddPlayer(entity e) +{ + if(!e.crypto_idfp || playerstats_db < 0) + return; + + string key; + key = sprintf("%s:*", e.crypto_idfp); + + string p; + p = db_get(playerstats_db, key); + if(p == "") + { + if(playerstats_last) + { + db_put(playerstats_db, key, playerstats_last); + strunzone(playerstats_last); + } + else + db_put(playerstats_db, key, "#"); + playerstats_last = strzone(e.crypto_idfp); + } +} + +void PlayerStats_AddEvent(string event_id) +{ + if(playerstats_db < 0) + return; + + string key; + key = sprintf("*:%s", event_id); + + string p; + p = db_get(playerstats_db, key); + if(p == "") + { + if(events_last) + { + db_put(playerstats_db, key, events_last); + strunzone(events_last); + } + else + db_put(playerstats_db, key, "#"); + events_last = strzone(event_id); + } +} + +void PlayerStats_Event(entity e, string event_id, float value) +{ + if(!e.crypto_idfp || playerstats_db < 0) + return; + + string key; + float val; + key = sprintf("%s:%s", e.crypto_idfp, event_id); + val = stof(db_get(playerstats_db, key)); + val += value; + db_put(playerstats_db, key, ftos(val)); +} + +void PlayerStats_Sent_URI_Get_Callback(float id, float status, string data) +{ + if(playerstats_requested) + playerstats_sent = TRUE; +} + +void PlayerStats_Shutdown() +{ + string p, pn; + string e, en; + string nn; + float b; + float i; + string uri; + + if(playerstats_db < 0) + return; + + uri = cvar_string("g_playerstats_uri"); + if(uri != "") + { + b = buf_create(); + i = 0; + + db_dump(playerstats_db, "foo.db"); + + bufstr_set(b, i++, "V 1"); + bufstr_set(b, i++, sprintf("T %s.%06d", strftime(FALSE, "%s"), floor(random() * 1000000))); + bufstr_set(b, i++, sprintf("G %s", GetGametype())); + bufstr_set(b, i++, sprintf("M %s", GetMapname())); + for(p = playerstats_last; (pn = db_get(playerstats_db, sprintf("%s:*", p))) != ""; p = pn) + { + bufstr_set(b, i++, sprintf("P %s", p)); + nn = db_get(playerstats_db, sprintf("%s:_netname", p)); + if(nn != "") + bufstr_set(b, i++, sprintf("n %s", nn)); + for(e = events_last; (en = db_get(playerstats_db, sprintf("*:%s", e))) != ""; e = en) + { + float v; + v = stof(db_get(playerstats_db, sprintf("%s:%s", p, e))); + bufstr_set(b, i++, sprintf("e %s %f", e, v)); + } + } + bufstr_set(b, i++, ""); + + if(crypto_uri_postbuf(uri, URI_GET_PLAYERSTATS_SENT, "text/plain", "\n", b, 0)) + playerstats_requested = TRUE; + else + playerstats_sent = TRUE; // if posting fails, we must continue anyway + + buf_del(b); + } + else + playerstats_sent = TRUE; + + db_close(playerstats_db); + playerstats_db = -1; +} + +void PlayerStats_AddGlobalInfo(entity p) +{ + if(playerstats_db < 0) + return; + if(!p.crypto_idfp || playerstats_db < 0) + return; + p.playerstats_addedglobalinfo = TRUE; + + // add global info! + if(p.alivetime) + PlayerStats_Event(p, PLAYERSTATS_ALIVETIME, time - p.alivetime); + + if(p.cvar_cl_allow_uid2name) + db_put(playerstats_db, sprintf("%s:_netname", p.crypto_idfp), p.netname); +} diff --git a/qcsrc/server/playerstats.qh b/qcsrc/server/playerstats.qh new file mode 100644 index 000000000..b7b425f29 --- /dev/null +++ b/qcsrc/server/playerstats.qh @@ -0,0 +1,27 @@ +// time the player was alive and kicking +string PLAYERSTATS_ALIVETIME = "alivetime"; +string PLAYERSTATS_KILLS = "kills"; + +// delay map switch until this is set +float playerstats_sent; + +// call at initialization +void PlayerStats_Init(); + +// add a new player +void PlayerStats_AddPlayer(entity e); + +// add a new event +void PlayerStats_AddEvent(string event_id); + +// call on each event to track, or at player disconnect OR match end for "global stuff" +void PlayerStats_Event(entity e, string event_id, float value); + +// call at game over +void PlayerStats_Shutdown(); // send stats to the server + +// URI GET callback +void PlayerStats_Sent_URI_Get_Callback(float id, float status, string data); + +// call this whenever a player leaves +void PlayerStats_AddGlobalInfo(entity p); diff --git a/qcsrc/server/progs.src b/qcsrc/server/progs.src index 104f58c38..58cf66339 100644 --- a/qcsrc/server/progs.src +++ b/qcsrc/server/progs.src @@ -41,6 +41,7 @@ csqceffects.qc anticheat.qh cheats.qh +playerstats.qh portals.qh @@ -176,6 +177,7 @@ playerdemo.qc anticheat.qc cheats.qc +playerstats.qc mutators/base.qc mutators/gamemode_keyhunt.qc -- 2.39.2