From: Rudolf Polzer <divverent@alientrap.org>
Date: Sun, 28 Nov 2010 17:39:40 +0000 (+0100)
Subject: initial player stats system (reports total "alive time" and kills to a stats server... 
X-Git-Tag: xonotic-v0.1.0preview~97
X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=2414c632b73268944cd9072cbf3190e4d926290f;p=xonotic%2Fxonotic-data.pk3dir.git

initial player stats system (reports total "alive time" and kills to a stats server via HTTP)
---

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